Subversion Repositories SmartDukaan

Rev

Rev 37640 | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
37640 amit 1
-- ============================================================================
2
--  Hot Deal brand - DATA MIGRATION                                2026-09-15
3
--
4
--  Spec : docs/superpowers/specs/2026-09-10-hot-deal-brand-design.md
5
--  Plan : docs/superpowers/plans/2026-09-11-hot-deal-brand.md
6
--
7
--  Run AFTER hot_deal_brand_schema.sql PASS 1, BEFORE PASS 2.
8
--  Idempotent - every mutating step guards on brand <> 'Hot Deal', so a second
9
--  run matches nothing and cannot double-prefix a model name.
10
--
11
--  WHAT IT DOES
12
--    1. freezes the current brand / model_name of every in-scope row
13
--    2. folds the OEM brand into model_name, then sets brand = 'Hot Deal'
14
--    3. ensures every in-scope catalog has a model_hot_deal attributes row
15
--       carrying oem_brand
16
--
17
--  WHY model_name CHANGES
18
--    Description is DERIVED, never stored: both Catalog.getDescription() and
19
--    Item.getItemDescriptionNoColor() build  brand + model_name + model_number
20
--    at read time. Overwriting brand alone would erase the OEM from every
21
--    label, so it is folded into model_name:
22
--
23
--      brand        'Oneplus'  ->  'Hot Deal'
24
--      model_name   ''         ->  'Oneplus'
25
--      model_number unchanged
26
--      => "Hot Deal Oneplus Y1S Edge 32inch 32HD2A01"
27
--
28
--    model_name is empty on most in-scope rows (the marketing text lives in
29
--    model_number), so the prefix usually lands in a free slot.
30
--
31
--  ALREADY-BILLED DOCUMENTS ARE NOT TOUCHED
32
--    transaction.lineitem and fofo.fofo_order_item snapshot brand/model_name
33
--    at order time, so invoices keep printing the OEM brand. 2,507 in-scope
34
--    lines sit on invoices filed at NIC with an IRN; rewriting the snapshot
35
--    would make a reprint disagree with the PrdDesc held against that IRN.
36
--    That is deliberate - see spec section 8.
37
-- ============================================================================
38
 
39
 
40
-- ---------------------------------------------------------------------------
41
--  SCOPE - FROZEN ID LIST, derived once on prod (hadb1) 2026-09-15
42
--
43
--  Selection rule that produced this list:
44
--      a known hot deal  AND  an active listing
45
--        known hot deal = a model_hot_deal row (expiry irrelevant - brand
46
--                         drives membership now, there are no windows)
47
--                       UNION tag_listing.hot_deals = 1 AND tag_id = 4
48
--                         (the 31 legacy rows from the old
49
--                          /v2/fofo/indent/confirm-hotdeals-pause screen)
50
--        active listing = tag_listing.tag_id = 4 AND active = 1
51
--
52
--  Result: 69 items across 31 catalogs - 27 of the 28 model_hot_deal catalogs
53
--  (one has no active listing) plus 4 legacy-only Ai+ catalogs.
54
--
55
--  ⚠ The ids are FROZEN ON PURPOSE. Do not re-derive the predicate per
56
--    environment. The localhost dev copy holds 26 model_hot_deal rows against
57
--    prod's 29, and the set moved from 69/31 to 68/30 over four days in
58
--    September as listings were reactivated. Pinning the ids is what makes the
59
--    same SKUs migrate in every environment.
60
-- ---------------------------------------------------------------------------
61
 
62
CREATE TABLE IF NOT EXISTS catalog._hot_deal_brand_scope (
63
  item_id    INT NOT NULL PRIMARY KEY,
64
  catalog_id INT NOT NULL,
65
  KEY idx_hd_scope_catalog (catalog_id)
66
) ENGINE=InnoDB;
67
 
68
INSERT IGNORE INTO catalog._hot_deal_brand_scope (item_id, catalog_id) VALUES
69
  (37876,1025252),(37877,1025252),(37878,1025252),(37879,1025253),(37880,1025253),
70
  (37881,1025253),(37882,1025253),(37919,1025253),(37920,1025252),(39579,1026010),
71
  (39759,1026085),(39941,1026085),(39942,1026085),(39943,1026085),(39944,1026085),
72
  (40199,1026268),(40251,1026302),(40412,1026360),(40413,1026360),(40483,1026403),
73
  (40484,1026403),(40485,1026403),(40486,1026403),(40487,1026403),(40488,1026403),
74
  (40497,1026408),(40498,1026408),(40499,1026408),(40500,1026408),(40581,1026443),
75
  (40582,1026444),(40583,1026445),(40584,1026446),(40585,1026447),(40649,1026470),
76
  (40650,1026470),(40651,1026470),(40654,1026473),(40655,1026473),(40656,1026473),
77
  (40657,1026473),(40658,1026473),(40659,1026474),(40660,1026474),(40661,1026474),
78
  (40662,1026474),(40663,1026474),(40664,1026475),(40665,1026475),(40666,1026475),
79
  (40667,1026475),(40668,1026475),(40698,1026488),(40699,1026488),(40700,1026488),
80
  (40701,1026488),(40702,1026488),(40733,1026506),(40753,1026526),(40754,1026527),
81
  (40755,1026528),(40756,1026529),(40826,1026562),(40828,1026564),(40829,1026565),
82
  (40830,1026566),(40860,1026583),(40861,1026584),(40863,1026586);
83
 
84
-- Guard: the list must load whole. 69 items / 31 catalogs.
85
-- MySQL cannot reference the same TEMPORARY table twice in one statement, so
86
-- these are two plain statements against a normal table.
87
SELECT COUNT(*) AS scope_items, COUNT(DISTINCT catalog_id) AS scope_catalogs
88
FROM catalog._hot_deal_brand_scope;
89
 
90
 
91
-- ---------------------------------------------------------------------------
92
--  FREEZE - must run before any UPDATE.
93
--
94
--  This table is the ONLY record of the pre-move state: after the UPDATE the
95
--  old brand cannot be re-derived, because brand now reads 'Hot Deal' and the
96
--  OEM survives only as a prefix inside model_name, which is not safely
97
--  parseable back out.
98
--
99
--  INSERT IGNORE + the brand guard means a re-run will not overwrite the
100
--  original values with already-migrated ones.
101
-- ---------------------------------------------------------------------------
102
 
103
CREATE TABLE IF NOT EXISTS catalog._hot_deal_brand_freeze (
104
  level          VARCHAR(8)   NOT NULL COMMENT 'catalog | item',
105
  row_id         INT          NOT NULL,
106
  old_brand_id   INT          NULL     COMMENT 'catalog level only; item has no brand_id',
107
  old_brand      VARCHAR(64)  NULL,
108
  old_model_name VARCHAR(255) NULL,
109
  run_date       DATE         NOT NULL,
110
  PRIMARY KEY (level, row_id)
111
) ENGINE=InnoDB;
112
 
113
INSERT IGNORE INTO catalog._hot_deal_brand_freeze
114
      (level, row_id, old_brand_id, old_brand, old_model_name, run_date)
115
SELECT 'catalog', c.id, c.brand_id, c.brand, c.model_name, CURDATE()
116
FROM catalog.catalog c
117
JOIN (SELECT DISTINCT catalog_id FROM catalog._hot_deal_brand_scope) s
118
     ON s.catalog_id = c.id
119
WHERE c.brand <> 'Hot Deal';
120
 
121
INSERT IGNORE INTO catalog._hot_deal_brand_freeze
122
      (level, row_id, old_brand_id, old_brand, old_model_name, run_date)
123
SELECT 'item', i.id, NULL, i.brand, i.model_name, CURDATE()
124
FROM catalog.item i
125
JOIN catalog._hot_deal_brand_scope s ON s.item_id = i.id
126
WHERE i.brand <> 'Hot Deal';
127
 
128
 
129
-- ---------------------------------------------------------------------------
130
--  MOVE - prefix the OEM brand into model_name, then overwrite brand.
131
--
132
--  The  brand <> 'Hot Deal'  predicate is what makes this idempotent: on a
133
--  second run nothing matches, so no row can become "Samsung Samsung ...".
134
--  COALESCE guards the NULL model_name rows; TRIM collapses the stray space
135
--  those leave behind.
136
-- ---------------------------------------------------------------------------
137
 
138
UPDATE catalog.catalog c
139
JOIN (SELECT DISTINCT catalog_id FROM catalog._hot_deal_brand_scope) s
140
     ON s.catalog_id = c.id
141
JOIN catalog.brand b ON b.name = 'Hot Deal'
142
SET c.model_name = TRIM(CONCAT(c.brand, ' ', COALESCE(c.model_name, ''))),
143
    c.brand      = 'Hot Deal',
144
    c.brand_id   = b.id
145
WHERE c.brand <> 'Hot Deal';
146
 
147
UPDATE catalog.item i
148
JOIN catalog._hot_deal_brand_scope s ON s.item_id = i.id
149
SET i.model_name = TRIM(CONCAT(i.brand, ' ', COALESCE(i.model_name, ''))),
150
    i.brand      = 'Hot Deal'
151
WHERE i.brand <> 'Hot Deal';
152
 
153
 
154
-- ---------------------------------------------------------------------------
155
--  ATTRIBUTES - every in-scope catalog needs exactly one model_hot_deal row.
156
--
157
--  Catalogs that arrived via the legacy-flag source have no row at all. They
158
--  get one with oem_brand set and the five pill attributes left at defaults,
159
--  so the fofo admin lists them as incomplete and ops can fill them in. A SKU
160
--  with the Hot Deal brand but no pills is valid, not an error - the badge
161
--  comes from the brand, the pills from this row.
162
--
163
--  start_date / end_date are still NOT NULL at this point (they are dropped in
164
--  schema PASS 3), so sentinel values are supplied.
165
-- ---------------------------------------------------------------------------
166
 
37646 amit 167
-- ---------------------------------------------------------------------------
168
--  DEDUPE - one attributes row per catalog.
169
--
170
--  The old model allowed several date-windowed deals for the same catalog, so a
171
--  model re-promoted in a later window has more than one row. That is legal
172
--  history under the old design and a duplicate under the new one, where the
173
--  row IS the attributes. Keep the newest and drop the rest.
174
--
175
--  Found on prod 2026-09-15: catalog 1026529 (Realme) had two rows, created
176
--  2026-08-19 and 2026-09-02 by the same user, with IDENTICAL attributes - so
177
--  keeping the newest lost nothing. Verify that before running elsewhere; if the
178
--  attributes differ, decide which is current instead of taking MAX(id) blindly.
179
--
180
--  Must run before the unique key in schema PASS 2, which this is what makes
181
--  possible.
182
-- ---------------------------------------------------------------------------
183
DELETE m FROM catalog.model_hot_deal m
184
JOIN (SELECT catalog_item_id, MAX(id) AS keep_id
185
      FROM catalog.model_hot_deal
186
      GROUP BY catalog_item_id HAVING COUNT(*) > 1) d
187
  ON d.catalog_item_id = m.catalog_item_id AND m.id < d.keep_id;
188
 
37640 amit 189
-- existing rows: backfill oem_brand from the freeze table
190
UPDATE catalog.model_hot_deal mhd
191
JOIN catalog._hot_deal_brand_freeze f
192
     ON f.level = 'catalog' AND f.row_id = mhd.catalog_item_id
193
SET mhd.oem_brand = f.old_brand
194
WHERE mhd.oem_brand IS NULL;
195
 
196
-- missing rows: create them
197
INSERT INTO catalog.model_hot_deal
198
      (brand_id, catalog_item_id, start_date, end_date, oem_brand,
199
       warranty_months, item_condition, finance_mapping, fresh, affordability,
200
       created_on, created_by)
201
SELECT (SELECT id FROM catalog.brand WHERE name = 'Hot Deal'),
202
       f.row_id, '2000-01-01', '2099-12-31', f.old_brand,
203
       0, 'NEW', 0, 0, 0, NOW(), 'hot_deal_brand_migration'
204
FROM catalog._hot_deal_brand_freeze f
205
WHERE f.level = 'catalog'
206
  AND NOT EXISTS (SELECT 1 FROM catalog.model_hot_deal m
207
                   WHERE m.catalog_item_id = f.row_id);
208
 
209
 
210
-- ---------------------------------------------------------------------------
211
--  VERIFY - all three must come back clean before running schema PASS 2.
212
-- ---------------------------------------------------------------------------
213
 
214
-- 1. every scope row now carries the brand: expect 69 and 31
215
SELECT COUNT(*) AS items_on_brand
216
FROM catalog.item i JOIN catalog._hot_deal_brand_scope s ON s.item_id = i.id
217
WHERE i.brand = 'Hot Deal';
218
 
219
SELECT COUNT(DISTINCT c.id) AS catalogs_on_brand
220
FROM catalog.catalog c
221
JOIN (SELECT DISTINCT catalog_id FROM catalog._hot_deal_brand_scope) s ON s.catalog_id = c.id
222
WHERE c.brand = 'Hot Deal';
223
 
224
-- 2. no catalog is missing its attributes row or its oem_brand: expect 0
225
SELECT COUNT(*) AS catalogs_missing_attributes
226
FROM (SELECT DISTINCT catalog_id FROM catalog._hot_deal_brand_scope) s
227
LEFT JOIN catalog.model_hot_deal m ON m.catalog_item_id = s.catalog_id
228
WHERE m.id IS NULL OR m.oem_brand IS NULL;
229
 
230
-- 3. no duplicate attributes rows, which would break schema PASS 2: expect 0
231
SELECT COUNT(*) AS duplicate_attribute_rows FROM (
232
  SELECT catalog_item_id FROM catalog.model_hot_deal
233
  GROUP BY catalog_item_id HAVING COUNT(*) > 1
234
) d;
235
 
236
-- 4. eyeball the derived descriptions
237
SELECT c.id, c.brand, c.model_name, c.model_number,
238
       TRIM(CONCAT(c.brand, ' ', c.model_name, ' ', c.model_number)) AS description
239
FROM catalog.catalog c
240
JOIN (SELECT DISTINCT catalog_id FROM catalog._hot_deal_brand_scope) s ON s.catalog_id = c.id
241
ORDER BY c.id LIMIT 10;
242
 
243
 
244
-- ============================  ROLLBACK  ====================================
245
--  Restores brand, brand_id and model_name from the freeze table, then removes
246
--  the rows this migration created. Run a FULL Solr reindex afterwards.
247
--
248
--    UPDATE catalog.catalog c
249
--      JOIN catalog._hot_deal_brand_freeze f
250
--        ON f.level = 'catalog' AND f.row_id = c.id
251
--    SET c.brand      = f.old_brand,
252
--        c.brand_id   = f.old_brand_id,
253
--        c.model_name = f.old_model_name;
254
--
255
--    UPDATE catalog.item i
256
--      JOIN catalog._hot_deal_brand_freeze f
257
--        ON f.level = 'item' AND f.row_id = i.id
258
--    SET i.brand      = f.old_brand,
259
--        i.model_name = f.old_model_name;
260
--
261
--    DELETE FROM catalog.model_hot_deal WHERE created_by = 'hot_deal_brand_migration';
262
--    UPDATE catalog.model_hot_deal SET oem_brand = NULL;
263
--
264
--  Keep catalog._hot_deal_brand_freeze and catalog._hot_deal_brand_scope as
265
--  the audit record of the run; they are what make it reversible at all.
266
-- ============================================================================