| 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 |
|
|
|
167 |
-- existing rows: backfill oem_brand from the freeze table
|
|
|
168 |
UPDATE catalog.model_hot_deal mhd
|
|
|
169 |
JOIN catalog._hot_deal_brand_freeze f
|
|
|
170 |
ON f.level = 'catalog' AND f.row_id = mhd.catalog_item_id
|
|
|
171 |
SET mhd.oem_brand = f.old_brand
|
|
|
172 |
WHERE mhd.oem_brand IS NULL;
|
|
|
173 |
|
|
|
174 |
-- missing rows: create them
|
|
|
175 |
INSERT INTO catalog.model_hot_deal
|
|
|
176 |
(brand_id, catalog_item_id, start_date, end_date, oem_brand,
|
|
|
177 |
warranty_months, item_condition, finance_mapping, fresh, affordability,
|
|
|
178 |
created_on, created_by)
|
|
|
179 |
SELECT (SELECT id FROM catalog.brand WHERE name = 'Hot Deal'),
|
|
|
180 |
f.row_id, '2000-01-01', '2099-12-31', f.old_brand,
|
|
|
181 |
0, 'NEW', 0, 0, 0, NOW(), 'hot_deal_brand_migration'
|
|
|
182 |
FROM catalog._hot_deal_brand_freeze f
|
|
|
183 |
WHERE f.level = 'catalog'
|
|
|
184 |
AND NOT EXISTS (SELECT 1 FROM catalog.model_hot_deal m
|
|
|
185 |
WHERE m.catalog_item_id = f.row_id);
|
|
|
186 |
|
|
|
187 |
|
|
|
188 |
-- ---------------------------------------------------------------------------
|
|
|
189 |
-- VERIFY - all three must come back clean before running schema PASS 2.
|
|
|
190 |
-- ---------------------------------------------------------------------------
|
|
|
191 |
|
|
|
192 |
-- 1. every scope row now carries the brand: expect 69 and 31
|
|
|
193 |
SELECT COUNT(*) AS items_on_brand
|
|
|
194 |
FROM catalog.item i JOIN catalog._hot_deal_brand_scope s ON s.item_id = i.id
|
|
|
195 |
WHERE i.brand = 'Hot Deal';
|
|
|
196 |
|
|
|
197 |
SELECT COUNT(DISTINCT c.id) AS catalogs_on_brand
|
|
|
198 |
FROM catalog.catalog c
|
|
|
199 |
JOIN (SELECT DISTINCT catalog_id FROM catalog._hot_deal_brand_scope) s ON s.catalog_id = c.id
|
|
|
200 |
WHERE c.brand = 'Hot Deal';
|
|
|
201 |
|
|
|
202 |
-- 2. no catalog is missing its attributes row or its oem_brand: expect 0
|
|
|
203 |
SELECT COUNT(*) AS catalogs_missing_attributes
|
|
|
204 |
FROM (SELECT DISTINCT catalog_id FROM catalog._hot_deal_brand_scope) s
|
|
|
205 |
LEFT JOIN catalog.model_hot_deal m ON m.catalog_item_id = s.catalog_id
|
|
|
206 |
WHERE m.id IS NULL OR m.oem_brand IS NULL;
|
|
|
207 |
|
|
|
208 |
-- 3. no duplicate attributes rows, which would break schema PASS 2: expect 0
|
|
|
209 |
SELECT COUNT(*) AS duplicate_attribute_rows FROM (
|
|
|
210 |
SELECT catalog_item_id FROM catalog.model_hot_deal
|
|
|
211 |
GROUP BY catalog_item_id HAVING COUNT(*) > 1
|
|
|
212 |
) d;
|
|
|
213 |
|
|
|
214 |
-- 4. eyeball the derived descriptions
|
|
|
215 |
SELECT c.id, c.brand, c.model_name, c.model_number,
|
|
|
216 |
TRIM(CONCAT(c.brand, ' ', c.model_name, ' ', c.model_number)) AS description
|
|
|
217 |
FROM catalog.catalog c
|
|
|
218 |
JOIN (SELECT DISTINCT catalog_id FROM catalog._hot_deal_brand_scope) s ON s.catalog_id = c.id
|
|
|
219 |
ORDER BY c.id LIMIT 10;
|
|
|
220 |
|
|
|
221 |
|
|
|
222 |
-- ============================ ROLLBACK ====================================
|
|
|
223 |
-- Restores brand, brand_id and model_name from the freeze table, then removes
|
|
|
224 |
-- the rows this migration created. Run a FULL Solr reindex afterwards.
|
|
|
225 |
--
|
|
|
226 |
-- UPDATE catalog.catalog c
|
|
|
227 |
-- JOIN catalog._hot_deal_brand_freeze f
|
|
|
228 |
-- ON f.level = 'catalog' AND f.row_id = c.id
|
|
|
229 |
-- SET c.brand = f.old_brand,
|
|
|
230 |
-- c.brand_id = f.old_brand_id,
|
|
|
231 |
-- c.model_name = f.old_model_name;
|
|
|
232 |
--
|
|
|
233 |
-- UPDATE catalog.item i
|
|
|
234 |
-- JOIN catalog._hot_deal_brand_freeze f
|
|
|
235 |
-- ON f.level = 'item' AND f.row_id = i.id
|
|
|
236 |
-- SET i.brand = f.old_brand,
|
|
|
237 |
-- i.model_name = f.old_model_name;
|
|
|
238 |
--
|
|
|
239 |
-- DELETE FROM catalog.model_hot_deal WHERE created_by = 'hot_deal_brand_migration';
|
|
|
240 |
-- UPDATE catalog.model_hot_deal SET oem_brand = NULL;
|
|
|
241 |
--
|
|
|
242 |
-- Keep catalog._hot_deal_brand_freeze and catalog._hot_deal_brand_scope as
|
|
|
243 |
-- the audit record of the run; they are what make it reversible at all.
|
|
|
244 |
-- ============================================================================
|