Subversion Repositories SmartDukaan

Rev

Rev 37566 | Go to most recent revision | Details | Last modification | View Log | RSS feed

Rev Author Line No. Line
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
-- ===========================================================================