| 37563 |
amit |
1 |
-- ============================================================================
|
|
|
2 |
-- Delist logic v2 - rolling 24-month, house-wide 2026-09-09
|
|
|
3 |
--
|
|
|
4 |
-- Supersedes samsung_delist_pre2025_no_stock_20260909.sql, which was one
|
|
|
5 |
-- brand with a frozen id list. This runs across EVERY brand and EVERY
|
|
|
6 |
-- category (mobile and non-mobile) on a rolling 24-month window, so it is
|
|
|
7 |
-- safe to schedule daily.
|
|
|
8 |
--
|
|
|
9 |
-- Three changes over v1:
|
|
|
10 |
-- (NEW 0) rolling 24-month window instead of a hardcoded 2025-01-01, and
|
|
|
11 |
-- no brand / category restriction.
|
|
|
12 |
-- (NEW 1) an item with an OUTSTANDING VENDOR PO is never delisted. It is
|
|
|
13 |
-- skipped, not excluded -- the next run re-evaluates it, so it
|
|
|
14 |
-- delists by itself once the PO closes.
|
|
|
15 |
-- (NEW 2) a model that goes FULLY DARK (no active listing left on any of
|
|
|
16 |
-- its colours) has its movement categorisation moved to OTHER, so
|
|
|
17 |
-- it stops being offered as a live model.
|
|
|
18 |
--
|
|
|
19 |
-- Written to be run repeatedly (daily). Every step is idempotent.
|
|
|
20 |
-- ============================================================================
|
|
|
21 |
|
|
|
22 |
-- ---------------------------------------------------------------------------
|
|
|
23 |
-- WHAT "OUTSTANDING VENDOR PO" MEANS (NEW 1)
|
|
|
24 |
--
|
|
|
25 |
-- warehouse.purchaseorder.status is an ORDINAL enum (in.shop2020.purchase.POStatus):
|
|
|
26 |
-- 0 INIT 1 READY 2 PARTIALLY_FULFILLED 3 PRECLOSED 4 CLOSED
|
|
|
27 |
--
|
|
|
28 |
-- Outstanding = status IN (1,2) AND supplier.internal = 0
|
|
|
29 |
-- AND lineitem.unfulfilledQuantity > 0
|
|
|
30 |
--
|
|
|
31 |
-- status 1 is the house definition of an open PO -- the existing named query
|
|
|
32 |
-- warehouse.selectOpenPo uses exactly "po.status = 1 and s.internal = false".
|
|
|
33 |
-- status 2 is included because PARTIALLY_FULFILLED is a legitimate open
|
|
|
34 |
-- state (it happens to hold no unfulfilled lines today).
|
|
|
35 |
--
|
|
|
36 |
-- INIT (0) is DELIBERATELY EXCLUDED. Measured on prod 2026-09-09:
|
|
|
37 |
-- status 0 -> 376 unfulfilled lines spanning 2024-04-02 .. 2026-09-03
|
|
|
38 |
-- status 1 -> 97 unfulfilled lines spanning 2026-09-01 .. 2026-09-09
|
|
|
39 |
-- INIT is a drawer of abandoned drafts, 2.5 years deep. Nothing ever closes
|
|
|
40 |
-- them, so counting INIT as "outstanding" would defer those items forever
|
|
|
41 |
-- instead of for a day. On the 738 rows of the 2026-09-09 Samsung batch,
|
|
|
42 |
-- INIT would have blocked 13 listings / 10 models, every one a stale draft.
|
|
|
43 |
--
|
|
|
44 |
-- s.internal = 0 is what makes it a VENDOR po rather than an internal
|
|
|
45 |
-- stock transfer between our own warehouses.
|
|
|
46 |
--
|
|
|
47 |
-- COLUMN-NAME TRAP in warehouse.lineitem: the entity field `quantity` maps to
|
|
|
48 |
-- DB column `initial_qty`, and entity `amendedQty` maps to DB column
|
|
|
49 |
-- `quantity`. So in raw SQL `quantity` is the AMENDED qty, not the ordered
|
|
|
50 |
-- one. Use unfulfilledQuantity / fulfilled and do not reach for `quantity`.
|
|
|
51 |
-- ---------------------------------------------------------------------------
|
|
|
52 |
|
|
|
53 |
-- ===========================================================================
|
|
|
54 |
-- STEP 0 - persistent audit. Created once, appended to by every run.
|
|
|
55 |
-- Without this a run is IRREVERSIBLE: after step 3 the rows read active=0,
|
|
|
56 |
-- so the selection predicate (which requires active=1) can no longer
|
|
|
57 |
-- re-derive what it touched. Same pattern as the freeze table kept for the
|
|
|
58 |
-- 2026-09-09 Samsung batch.
|
|
|
59 |
--
|
|
|
60 |
-- ROLLBACK for a given run_date:
|
|
|
61 |
-- UPDATE catalog.tag_listing tl
|
|
|
62 |
-- JOIN catalog._delist_audit a ON a.tag_listing_id = tl.id
|
|
|
63 |
-- SET tl.active = 1, tl.eol_date = NULL
|
|
|
64 |
-- WHERE a.run_date = '<date>';
|
|
|
65 |
-- -- and restore categorisation:
|
|
|
66 |
-- DELETE cc FROM catalog.catagoriesd_catalog cc
|
|
|
67 |
-- JOIN catalog._delist_cc_audit x ON x.catalog_id = cc.catalog_id
|
|
|
68 |
-- WHERE x.run_date = '<date>' AND cc.status = 'OTHER' AND cc.end_date IS NULL;
|
|
|
69 |
-- UPDATE catalog.catagoriesd_catalog cc
|
|
|
70 |
-- JOIN catalog._delist_cc_audit x ON x.cc_id = cc.id
|
|
|
71 |
-- SET cc.end_date = NULL
|
|
|
72 |
-- WHERE x.run_date = '<date>';
|
|
|
73 |
-- ===========================================================================
|
|
|
74 |
CREATE TABLE IF NOT EXISTS catalog._delist_audit (
|
|
|
75 |
tag_listing_id INT NOT NULL,
|
|
|
76 |
item_id INT,
|
|
|
77 |
run_date DATE NOT NULL,
|
|
|
78 |
PRIMARY KEY (tag_listing_id, run_date)
|
|
|
79 |
);
|
|
|
80 |
|
|
|
81 |
CREATE TABLE IF NOT EXISTS catalog._delist_cc_audit (
|
|
|
82 |
cc_id INT NOT NULL,
|
|
|
83 |
catalog_id INT NOT NULL,
|
|
|
84 |
prev_status VARCHAR(299),
|
|
|
85 |
run_date DATE NOT NULL,
|
|
|
86 |
PRIMARY KEY (cc_id, run_date)
|
|
|
87 |
);
|
|
|
88 |
|
|
|
89 |
-- ===========================================================================
|
|
|
90 |
-- STEP 1 - build the candidate set
|
|
|
91 |
-- ===========================================================================
|
|
|
92 |
DROP TEMPORARY TABLE IF EXISTS _delist_candidate;
|
|
|
93 |
CREATE TEMPORARY TABLE _delist_candidate (
|
|
|
94 |
tag_listing_id INT PRIMARY KEY,
|
|
|
95 |
item_id INT,
|
|
|
96 |
catalog_item_id INT,
|
|
|
97 |
KEY idx_item (item_id),
|
|
|
98 |
KEY idx_cat (catalog_item_id)
|
|
|
99 |
);
|
|
|
100 |
|
|
|
101 |
-- ROLLING 24-MONTH WINDOW, EVERY BRAND, EVERY CATEGORY (mobile and non-mobile).
|
|
|
102 |
-- No brand or category predicate: the cron evaluates any itemId.
|
|
|
103 |
INSERT INTO _delist_candidate (tag_listing_id, item_id, catalog_item_id)
|
|
|
104 |
SELECT tl.id, tl.item_id, i.catalog_item_id
|
|
|
105 |
FROM catalog.tag_listing tl
|
|
|
106 |
JOIN catalog.item i ON i.id = tl.item_id
|
|
|
107 |
WHERE tl.tag_id = 4 -- default_fofo; tag 7 'test' has no rows
|
|
|
108 |
AND tl.active = 1
|
|
|
109 |
AND tl.start_date < CURDATE() - INTERVAL 24 MONTH
|
|
|
110 |
-- Internal pseudo-brands. These are NOT sellable catalogue: the app already
|
|
|
111 |
-- hard-excludes them from every partner listing
|
|
|
112 |
-- (StoreController/FofoSolr excludeBrands = Dummy, FOC HANDSET, FOC, Live Demo).
|
|
|
113 |
-- Delisting them would change nothing a partner sees while disturbing demo /
|
|
|
114 |
-- FOC operations. Remove this line if you want them swept too.
|
|
|
115 |
AND i.brand NOT IN ('Dummy', 'FOC', 'FOC HANDSET', 'Live Demo')
|
|
|
116 |
-- no SmartDukaan stock
|
|
|
117 |
AND NOT EXISTS (
|
|
|
118 |
SELECT 1 FROM warehouse.view_availability va
|
|
|
119 |
WHERE va.item_id = tl.item_id AND va.total > 0)
|
|
|
120 |
-- no stock at any live partner
|
|
|
121 |
AND NOT EXISTS (
|
|
|
122 |
SELECT 1
|
|
|
123 |
FROM fofo.inventory_item ii
|
|
|
124 |
JOIN fofo.fofo_store fs ON fs.id = ii.fofo_id
|
|
|
125 |
WHERE ii.item_id = tl.item_id AND ii.good_quantity > 0
|
|
|
126 |
AND fs.active = 1 AND fs.closed = 0)
|
|
|
127 |
-- nothing already on order from the partner side
|
|
|
128 |
AND NOT EXISTS (
|
|
|
129 |
SELECT 1 FROM warehouse.view_cis vc
|
|
|
130 |
WHERE vc.item_id = tl.item_id AND vc.indent > 0);
|
|
|
131 |
|
|
|
132 |
-- ===========================================================================
|
|
|
133 |
-- STEP 2 (NEW 1) - drop anything with an outstanding vendor PO.
|
|
|
134 |
-- These are SKIPPED, not excluded: tomorrow's run re-evaluates them.
|
|
|
135 |
-- ===========================================================================
|
|
|
136 |
DROP TEMPORARY TABLE IF EXISTS _deferred_vendor_po;
|
|
|
137 |
CREATE TEMPORARY TABLE _deferred_vendor_po (
|
|
|
138 |
tag_listing_id INT PRIMARY KEY
|
|
|
139 |
);
|
|
|
140 |
|
|
|
141 |
INSERT INTO _deferred_vendor_po (tag_listing_id)
|
|
|
142 |
SELECT DISTINCT c.tag_listing_id
|
|
|
143 |
FROM _delist_candidate c
|
|
|
144 |
JOIN warehouse.lineitem li ON li.itemId = c.item_id
|
|
|
145 |
JOIN warehouse.purchaseorder po ON po.id = li.purchaseOrder_id
|
|
|
146 |
JOIN warehouse.supplier s ON s.id = po.supplierId
|
|
|
147 |
WHERE s.internal = 0
|
|
|
148 |
AND po.status IN (1, 2)
|
|
|
149 |
AND li.unfulfilledQuantity > 0;
|
|
|
150 |
|
|
|
151 |
-- Report before writing anything.
|
|
|
152 |
-- NOTE: MySQL cannot reference the same TEMPORARY table twice in one
|
|
|
153 |
-- statement ("Can't reopen table"), so these counts are deliberately kept as
|
|
|
154 |
-- separate statements rather than one combined SELECT.
|
|
|
155 |
SELECT COUNT(*) AS deferred_open_vendor_po FROM _deferred_vendor_po;
|
|
|
156 |
|
|
|
157 |
DELETE c FROM _delist_candidate c
|
|
|
158 |
JOIN _deferred_vendor_po d ON d.tag_listing_id = c.tag_listing_id;
|
|
|
159 |
|
|
|
160 |
SELECT COUNT(*) AS listings_to_delist FROM _delist_candidate;
|
|
|
161 |
SELECT COUNT(DISTINCT catalog_item_id) AS models_touched FROM _delist_candidate;
|
|
|
162 |
|
|
|
163 |
-- ===========================================================================
|
|
|
164 |
-- STEP 3 - the delist itself
|
|
|
165 |
-- ===========================================================================
|
|
|
166 |
INSERT IGNORE INTO catalog._delist_audit (tag_listing_id, item_id, run_date)
|
|
|
167 |
SELECT c.tag_listing_id, c.item_id, CURDATE() FROM _delist_candidate c;
|
|
|
168 |
|
|
|
169 |
UPDATE catalog.tag_listing tl
|
|
|
170 |
JOIN _delist_candidate c ON c.tag_listing_id = tl.id
|
|
|
171 |
SET tl.active = 0,
|
|
|
172 |
tl.eol_date = CURDATE()
|
|
|
173 |
WHERE tl.active = 1;
|
|
|
174 |
|
|
|
175 |
-- ===========================================================================
|
|
|
176 |
-- STEP 4 (NEW 2) - models that just went FULLY DARK -> categorisation OTHER
|
|
|
177 |
--
|
|
|
178 |
-- "Fully dark" = zero active listings left across every colour of that
|
|
|
179 |
-- catalog_item_id. A model that keeps even one active colour is NOT touched.
|
|
|
180 |
-- (Real example from the 2026-09-09 batch: catalog 1024414, Samsung S24
|
|
|
181 |
-- 8GB/256GB, had 3 colours delisted but 2 still active -- it correctly
|
|
|
182 |
-- stays RUNNING.)
|
|
|
183 |
--
|
|
|
184 |
-- WHY 'OTHER' AND NOT NULL: catagoriesd_catalog.status is
|
|
|
185 |
-- `varchar(299) NOT NULL`, so a literal NULL needs a schema change. It is
|
|
|
186 |
-- also unnecessary -- CatalogMovingEnum.OTHER is defined as "not found", and
|
|
|
187 |
-- every reader already treats OTHER as "not a live model":
|
|
|
188 |
-- * /indent/getOutOfStockDetails keeps only {HID, FASTMOVING, RUNNING}
|
|
|
189 |
-- -> OTHER drops out of the Suggested-PO out-of-stock list
|
|
|
190 |
-- * Catalog.findAllWithEOLWithOutStock / findAllWithNoGoodStock accept
|
|
|
191 |
-- (status IS NULL OR 'OTHER' OR 'SLOWMOVING') -> the model becomes
|
|
|
192 |
-- eligible for Solr eol_no_stock_b and hides from partner browse
|
|
|
193 |
-- * Catalog.selectAllStatusAndBrandWise matches only requested statuses
|
|
|
194 |
--
|
|
|
195 |
-- WHY WE INSERT A NEW ROW RATHER THAN JUST END-DATING: the table is a
|
|
|
196 |
-- slowly-changing dimension (open row = end_date IS NULL), but the native
|
|
|
197 |
-- query CatagorisedCatalog.getBrandWiseCatalogMovement picks the latest row
|
|
|
198 |
-- per catalog via MAX(COALESCE(end_date, CURDATE())) and has NO end_date
|
|
|
199 |
-- filter. An orphaned end-dated RUNNING row would still come back as
|
|
|
200 |
-- "latest" and still read RUNNING. Closing the row is not enough -- a new
|
|
|
201 |
-- open OTHER row must outrank it.
|
|
|
202 |
--
|
|
|
203 |
-- SCOPE: MOBILE ONLY (category 10006, ProfitMandiConstants.MOBILE_CATEGORY_ID).
|
|
|
204 |
-- Movement classification is a mobile-catalogue concept -- accessories, TVs
|
|
|
205 |
-- and the rest are never categorised, so writing OTHER rows for them would
|
|
|
206 |
-- invent data no reader consults. Delisting itself (steps 1-3) stays
|
|
|
207 |
-- house-wide; only this re-categorisation is narrowed.
|
|
|
208 |
-- NOTE: the EOL readers Catalog.findAllWithEOLWithOutStock /
|
|
|
209 |
-- findAllWithNoGoodStock actually span categories (10006, 10009, 10010). If
|
|
|
210 |
-- 10009/10010 should be re-categorised too, widen the IN list below.
|
|
|
211 |
--
|
|
|
212 |
-- We only NORMALISE models that already have an open row -- i.e. models
|
|
|
213 |
-- whose categorisation is currently non-null. We do not manufacture rows for
|
|
|
214 |
-- models that never had one: every reader treats a missing row exactly like
|
|
|
215 |
-- OTHER, so inserting would add noise and change no behaviour. (2026-09-09 batch: 170 fully dark -> 139 have no open row
|
|
|
216 |
-- and are left alone, 31 SLOWMOVING are normalised.)
|
|
|
217 |
-- ===========================================================================
|
|
|
218 |
DROP TEMPORARY TABLE IF EXISTS _dark_model;
|
|
|
219 |
CREATE TEMPORARY TABLE _dark_model (catalog_item_id INT PRIMARY KEY);
|
|
|
220 |
|
|
|
221 |
-- SELF-HEALING, NOT RUN-SCOPED. This deliberately looks at EVERY fully-dark
|
|
|
222 |
-- mobile model, not just the ones this run delisted. Scoping it to the
|
|
|
223 |
-- current candidate set would leave models that went dark before the cron
|
|
|
224 |
-- existed (e.g. the 2026-09-09 Samsung batch) carrying a stale RUNNING /
|
|
|
225 |
-- SLOWMOVING categorisation forever, because no later run would revisit them.
|
|
|
226 |
-- Run house-wide it converges instead: once every dark model reads OTHER,
|
|
|
227 |
-- subsequent runs are no-ops. It also removes the need for a separate
|
|
|
228 |
-- Samsung backfill.
|
|
|
229 |
INSERT INTO _dark_model (catalog_item_id)
|
|
|
230 |
SELECT cc.catalog_id
|
|
|
231 |
FROM catalog.catagoriesd_catalog cc
|
|
|
232 |
WHERE cc.end_date IS NULL -- has an open categorisation row ...
|
|
|
233 |
AND cc.status <> 'OTHER' -- ... that is not already OTHER
|
|
|
234 |
AND EXISTS ( -- MOBILE ONLY (see note above)
|
|
|
235 |
SELECT 1 FROM catalog.item im
|
|
|
236 |
WHERE im.catalog_item_id = cc.catalog_id
|
|
|
237 |
AND im.category = 10006)
|
|
|
238 |
AND NOT EXISTS ( -- fully dark: no active colour left
|
|
|
239 |
SELECT 1
|
|
|
240 |
FROM catalog.tag_listing t2
|
|
|
241 |
JOIN catalog.item i2 ON i2.id = t2.item_id
|
|
|
242 |
WHERE i2.catalog_item_id = cc.catalog_id
|
|
|
243 |
AND t2.active = 1);
|
|
|
244 |
|
|
|
245 |
-- 4a. close the open row (only where it is not already OTHER)
|
|
|
246 |
INSERT IGNORE INTO catalog._delist_cc_audit (cc_id, catalog_id, prev_status, run_date)
|
|
|
247 |
SELECT cc.id, cc.catalog_id, cc.status, CURDATE()
|
|
|
248 |
FROM catalog.catagoriesd_catalog cc
|
|
|
249 |
JOIN _dark_model d ON d.catalog_item_id = cc.catalog_id
|
|
|
250 |
WHERE cc.end_date IS NULL AND cc.status <> 'OTHER';
|
|
|
251 |
|
|
|
252 |
UPDATE catalog.catagoriesd_catalog cc
|
|
|
253 |
JOIN _dark_model d ON d.catalog_item_id = cc.catalog_id
|
|
|
254 |
SET cc.end_date = CURDATE() - INTERVAL 1 DAY
|
|
|
255 |
WHERE cc.end_date IS NULL
|
|
|
256 |
AND cc.status <> 'OTHER';
|
|
|
257 |
|
|
|
258 |
-- 4b. open a new OTHER row so it outranks the closed one in
|
|
|
259 |
-- getBrandWiseCatalogMovement's MAX(COALESCE(end_date, CURDATE()))
|
|
|
260 |
INSERT INTO catalog.catagoriesd_catalog (catalog_id, status, start_date, end_date)
|
|
|
261 |
SELECT d.catalog_item_id, 'OTHER', CURDATE(), NULL
|
|
|
262 |
FROM _dark_model d
|
|
|
263 |
WHERE EXISTS ( -- had an open row we just closed
|
|
|
264 |
SELECT 1 FROM catalog.catagoriesd_catalog x
|
|
|
265 |
WHERE x.catalog_id = d.catalog_item_id
|
|
|
266 |
AND x.end_date = CURDATE() - INTERVAL 1 DAY)
|
|
|
267 |
AND NOT EXISTS ( -- idempotency guard
|
|
|
268 |
SELECT 1 FROM catalog.catagoriesd_catalog y
|
|
|
269 |
WHERE y.catalog_id = d.catalog_item_id
|
|
|
270 |
AND y.end_date IS NULL);
|
|
|
271 |
|
|
|
272 |
-- ===========================================================================
|
|
|
273 |
-- STEP 5 - Solr does not see any of this.
|
|
|
274 |
-- A raw UPDATE publishes no TagListingChangeListener event. The portal
|
|
|
275 |
-- catches up at the 06:00/18:00 full reindex (Listing.scheduledPushDataToSolr),
|
|
|
276 |
-- or run the cron jar with --pushDataToSolr to force it.
|
|
|
277 |
-- ===========================================================================
|