Rev 37566 | Go to most recent revision | Blame | Compare with Previous | Last modification | View Log | RSS feed
-- ============================================================================-- Delist logic v2 - rolling 24-month, house-wide 2026-09-09---- Supersedes samsung_delist_pre2025_no_stock_20260909.sql, which was one-- brand with a frozen id list. This runs across EVERY brand and EVERY-- category (mobile and non-mobile) on a rolling 24-month window, so it is-- safe to schedule daily.---- Three changes over v1:-- (NEW 0) rolling 24-month window instead of a hardcoded 2025-01-01, and-- no brand / category restriction.-- (NEW 1) an item with an OUTSTANDING VENDOR PO is never delisted. It is-- skipped, not excluded -- the next run re-evaluates it, so it-- delists by itself once the PO closes.-- (NEW 2) a model that goes FULLY DARK (no active listing left on any of-- its colours) has its movement categorisation moved to OTHER, so-- it stops being offered as a live model.---- Written to be run repeatedly (daily). Every step is idempotent.-- ============================================================================-- ----------------------------------------------------------------------------- WHAT "OUTSTANDING VENDOR PO" MEANS (NEW 1)---- warehouse.purchaseorder.status is an ORDINAL enum (in.shop2020.purchase.POStatus):-- 0 INIT 1 READY 2 PARTIALLY_FULFILLED 3 PRECLOSED 4 CLOSED---- Outstanding = status IN (1,2) AND supplier.internal = 0-- AND lineitem.unfulfilledQuantity > 0---- status 1 is the house definition of an open PO -- the existing named query-- warehouse.selectOpenPo uses exactly "po.status = 1 and s.internal = false".-- status 2 is included because PARTIALLY_FULFILLED is a legitimate open-- state (it happens to hold no unfulfilled lines today).---- INIT (0) is DELIBERATELY EXCLUDED. Measured on prod 2026-09-09:-- status 0 -> 376 unfulfilled lines spanning 2024-04-02 .. 2026-09-03-- status 1 -> 97 unfulfilled lines spanning 2026-09-01 .. 2026-09-09-- INIT is a drawer of abandoned drafts, 2.5 years deep. Nothing ever closes-- them, so counting INIT as "outstanding" would defer those items forever-- instead of for a day. On the 738 rows of the 2026-09-09 Samsung batch,-- INIT would have blocked 13 listings / 10 models, every one a stale draft.---- s.internal = 0 is what makes it a VENDOR po rather than an internal-- stock transfer between our own warehouses.---- COLUMN-NAME TRAP in warehouse.lineitem: the entity field `quantity` maps to-- DB column `initial_qty`, and entity `amendedQty` maps to DB column-- `quantity`. So in raw SQL `quantity` is the AMENDED qty, not the ordered-- one. Use unfulfilledQuantity / fulfilled and do not reach for `quantity`.-- ----------------------------------------------------------------------------- ===========================================================================-- STEP 0 - persistent audit. Created once, appended to by every run.-- Without this a run is IRREVERSIBLE: after step 3 the rows read active=0,-- so the selection predicate (which requires active=1) can no longer-- re-derive what it touched. Same pattern as the freeze table kept for the-- 2026-09-09 Samsung batch.---- ROLLBACK for a given run_date:-- UPDATE catalog.tag_listing tl-- JOIN catalog._delist_audit a ON a.tag_listing_id = tl.id-- SET tl.active = 1, tl.eol_date = NULL-- WHERE a.run_date = '<date>';-- -- and restore categorisation:-- DELETE cc FROM catalog.catagoriesd_catalog cc-- JOIN catalog._delist_cc_audit x ON x.catalog_id = cc.catalog_id-- WHERE x.run_date = '<date>' AND cc.status = 'OTHER' AND cc.end_date IS NULL;-- UPDATE catalog.catagoriesd_catalog cc-- JOIN catalog._delist_cc_audit x ON x.cc_id = cc.id-- SET cc.end_date = NULL-- WHERE x.run_date = '<date>';-- ===========================================================================CREATE TABLE IF NOT EXISTS catalog._delist_audit (tag_listing_id INT NOT NULL,item_id INT,run_date DATE NOT NULL,PRIMARY KEY (tag_listing_id, run_date));CREATE TABLE IF NOT EXISTS catalog._delist_cc_audit (cc_id INT NOT NULL,catalog_id INT NOT NULL,prev_status VARCHAR(299),run_date DATE NOT NULL,PRIMARY KEY (cc_id, run_date));-- ===========================================================================-- STEP 1 - build the candidate set-- ===========================================================================DROP TEMPORARY TABLE IF EXISTS _delist_candidate;CREATE TEMPORARY TABLE _delist_candidate (tag_listing_id INT PRIMARY KEY,item_id INT,catalog_item_id INT,KEY idx_item (item_id),KEY idx_cat (catalog_item_id));-- ROLLING 24-MONTH WINDOW, EVERY BRAND, EVERY CATEGORY (mobile and non-mobile).-- No brand or category predicate: the cron evaluates any itemId.INSERT INTO _delist_candidate (tag_listing_id, item_id, catalog_item_id)SELECT tl.id, tl.item_id, i.catalog_item_idFROM catalog.tag_listing tlJOIN catalog.item i ON i.id = tl.item_idWHERE tl.tag_id = 4 -- default_fofo; tag 7 'test' has no rowsAND tl.active = 1AND tl.start_date < CURDATE() - INTERVAL 24 MONTH-- Internal pseudo-brands. These are NOT sellable catalogue: the app already-- hard-excludes them from every partner listing-- (StoreController/FofoSolr excludeBrands = Dummy, FOC HANDSET, FOC, Live Demo).-- Delisting them would change nothing a partner sees while disturbing demo /-- FOC operations. Remove this line if you want them swept too.AND i.brand NOT IN ('Dummy', 'FOC', 'FOC HANDSET', 'Live Demo')-- no SmartDukaan stockAND NOT EXISTS (SELECT 1 FROM warehouse.view_availability vaWHERE va.item_id = tl.item_id AND va.total > 0)-- no stock at any live partnerAND NOT EXISTS (SELECT 1FROM fofo.inventory_item iiJOIN fofo.fofo_store fs ON fs.id = ii.fofo_idWHERE ii.item_id = tl.item_id AND ii.good_quantity > 0AND fs.active = 1 AND fs.closed = 0)-- nothing already on order from the partner sideAND NOT EXISTS (SELECT 1 FROM warehouse.view_cis vcWHERE vc.item_id = tl.item_id AND vc.indent > 0);-- ===========================================================================-- STEP 2 (NEW 1) - drop anything with an outstanding vendor PO.-- These are SKIPPED, not excluded: tomorrow's run re-evaluates them.-- ===========================================================================DROP TEMPORARY TABLE IF EXISTS _deferred_vendor_po;CREATE TEMPORARY TABLE _deferred_vendor_po (tag_listing_id INT PRIMARY KEY);INSERT INTO _deferred_vendor_po (tag_listing_id)SELECT DISTINCT c.tag_listing_idFROM _delist_candidate cJOIN warehouse.lineitem li ON li.itemId = c.item_idJOIN warehouse.purchaseorder po ON po.id = li.purchaseOrder_idJOIN warehouse.supplier s ON s.id = po.supplierIdWHERE s.internal = 0AND po.status IN (1, 2)AND li.unfulfilledQuantity > 0;-- Report before writing anything.-- NOTE: MySQL cannot reference the same TEMPORARY table twice in one-- statement ("Can't reopen table"), so these counts are deliberately kept as-- separate statements rather than one combined SELECT.SELECT COUNT(*) AS deferred_open_vendor_po FROM _deferred_vendor_po;DELETE c FROM _delist_candidate cJOIN _deferred_vendor_po d ON d.tag_listing_id = c.tag_listing_id;SELECT COUNT(*) AS listings_to_delist FROM _delist_candidate;SELECT COUNT(DISTINCT catalog_item_id) AS models_touched FROM _delist_candidate;-- ===========================================================================-- STEP 3 - the delist itself-- ===========================================================================INSERT IGNORE INTO catalog._delist_audit (tag_listing_id, item_id, run_date)SELECT c.tag_listing_id, c.item_id, CURDATE() FROM _delist_candidate c;UPDATE catalog.tag_listing tlJOIN _delist_candidate c ON c.tag_listing_id = tl.idSET tl.active = 0,tl.eol_date = CURDATE()WHERE tl.active = 1;-- ===========================================================================-- STEP 4 (NEW 2) - models that just went FULLY DARK -> categorisation OTHER---- "Fully dark" = zero active listings left across every colour of that-- catalog_item_id. A model that keeps even one active colour is NOT touched.-- (Real example from the 2026-09-09 batch: catalog 1024414, Samsung S24-- 8GB/256GB, had 3 colours delisted but 2 still active -- it correctly-- stays RUNNING.)---- WHY 'OTHER' AND NOT NULL: catagoriesd_catalog.status is-- `varchar(299) NOT NULL`, so a literal NULL needs a schema change. It is-- also unnecessary -- CatalogMovingEnum.OTHER is defined as "not found", and-- every reader already treats OTHER as "not a live model":-- * /indent/getOutOfStockDetails keeps only {HID, FASTMOVING, RUNNING}-- -> OTHER drops out of the Suggested-PO out-of-stock list-- * Catalog.findAllWithEOLWithOutStock / findAllWithNoGoodStock accept-- (status IS NULL OR 'OTHER' OR 'SLOWMOVING') -> the model becomes-- eligible for Solr eol_no_stock_b and hides from partner browse-- * Catalog.selectAllStatusAndBrandWise matches only requested statuses---- WHY WE INSERT A NEW ROW RATHER THAN JUST END-DATING: the table is a-- slowly-changing dimension (open row = end_date IS NULL), but the native-- query CatagorisedCatalog.getBrandWiseCatalogMovement picks the latest row-- per catalog via MAX(COALESCE(end_date, CURDATE())) and has NO end_date-- filter. An orphaned end-dated RUNNING row would still come back as-- "latest" and still read RUNNING. Closing the row is not enough -- a new-- open OTHER row must outrank it.---- SCOPE: MOBILE ONLY (category 10006, ProfitMandiConstants.MOBILE_CATEGORY_ID).-- Movement classification is a mobile-catalogue concept -- accessories, TVs-- and the rest are never categorised, so writing OTHER rows for them would-- invent data no reader consults. Delisting itself (steps 1-3) stays-- house-wide; only this re-categorisation is narrowed.-- NOTE: the EOL readers Catalog.findAllWithEOLWithOutStock /-- findAllWithNoGoodStock actually span categories (10006, 10009, 10010). If-- 10009/10010 should be re-categorised too, widen the IN list below.---- We only NORMALISE models that already have an open row -- i.e. models-- whose categorisation is currently non-null. We do not manufacture rows for-- models that never had one: every reader treats a missing row exactly like-- OTHER, so inserting would add noise and change no behaviour. (2026-09-09 batch: 170 fully dark -> 139 have no open row-- and are left alone, 31 SLOWMOVING are normalised.)-- ===========================================================================DROP TEMPORARY TABLE IF EXISTS _dark_model;CREATE TEMPORARY TABLE _dark_model (catalog_item_id INT PRIMARY KEY);-- SELF-HEALING, NOT RUN-SCOPED. This deliberately looks at EVERY fully-dark-- mobile model, not just the ones this run delisted. Scoping it to the-- current candidate set would leave models that went dark before the cron-- existed (e.g. the 2026-09-09 Samsung batch) carrying a stale RUNNING /-- SLOWMOVING categorisation forever, because no later run would revisit them.-- Run house-wide it converges instead: once every dark model reads OTHER,-- subsequent runs are no-ops. It also removes the need for a separate-- Samsung backfill.INSERT INTO _dark_model (catalog_item_id)SELECT cc.catalog_idFROM catalog.catagoriesd_catalog ccWHERE cc.end_date IS NULL -- has an open categorisation row ...AND cc.status <> 'OTHER' -- ... that is not already OTHERAND EXISTS ( -- MOBILE ONLY (see note above)SELECT 1 FROM catalog.item imWHERE im.catalog_item_id = cc.catalog_idAND im.category = 10006)AND NOT EXISTS ( -- fully dark: no active colour leftSELECT 1FROM catalog.tag_listing t2JOIN catalog.item i2 ON i2.id = t2.item_idWHERE i2.catalog_item_id = cc.catalog_idAND t2.active = 1);-- 4a. close the open row (only where it is not already OTHER)INSERT IGNORE INTO catalog._delist_cc_audit (cc_id, catalog_id, prev_status, run_date)SELECT cc.id, cc.catalog_id, cc.status, CURDATE()FROM catalog.catagoriesd_catalog ccJOIN _dark_model d ON d.catalog_item_id = cc.catalog_idWHERE cc.end_date IS NULL AND cc.status <> 'OTHER';UPDATE catalog.catagoriesd_catalog ccJOIN _dark_model d ON d.catalog_item_id = cc.catalog_idSET cc.end_date = CURDATE() - INTERVAL 1 DAYWHERE cc.end_date IS NULLAND cc.status <> 'OTHER';-- 4b. open a new OTHER row so it outranks the closed one in-- getBrandWiseCatalogMovement's MAX(COALESCE(end_date, CURDATE()))INSERT INTO catalog.catagoriesd_catalog (catalog_id, status, start_date, end_date)SELECT d.catalog_item_id, 'OTHER', CURDATE(), NULLFROM _dark_model dWHERE EXISTS ( -- had an open row we just closedSELECT 1 FROM catalog.catagoriesd_catalog xWHERE x.catalog_id = d.catalog_item_idAND x.end_date = CURDATE() - INTERVAL 1 DAY)AND NOT EXISTS ( -- idempotency guardSELECT 1 FROM catalog.catagoriesd_catalog yWHERE y.catalog_id = d.catalog_item_idAND y.end_date IS NULL);-- ===========================================================================-- STEP 5 - Solr does not see any of this.-- A raw UPDATE publishes no TagListingChangeListener event. The portal-- catches up at the 06:00/18:00 full reindex (Listing.scheduledPushDataToSolr),-- or run the cron jar with --pushDataToSolr to force it.-- ===========================================================================