@mohasinac/appkit 4.40.0 → 4.41.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/dist/features/categories/repository/categories.repository.js +32 -14
  2. package/dist/features/tester/seed-data/_money-flows.js +65 -49
  3. package/dist/features/tester/seed-data/authored/admin__admin-detail-round-trips.js +6 -4
  4. package/dist/features/tester/seed-data/authored/admin__coupons.js +14 -13
  5. package/dist/features/tester/seed-data/authored/buying__bidding.js +154 -140
  6. package/dist/features/tester/seed-data/authored/buying__browsing-search.js +4 -4
  7. package/dist/features/tester/seed-data/authored/buying__buying-checkout.js +59 -56
  8. package/dist/features/tester/seed-data/authored/buying__buying-coupons.js +18 -18
  9. package/dist/features/tester/seed-data/authored/buying__cart.js +105 -79
  10. package/dist/features/tester/seed-data/authored/buying__image-tile-layout.js +6 -8
  11. package/dist/features/tester/seed-data/authored/buying__my-orders.js +13 -13
  12. package/dist/features/tester/seed-data/authored/buying__offers.js +6 -6
  13. package/dist/features/tester/seed-data/authored/buying__order-detail-actions.js +8 -7
  14. package/dist/features/tester/seed-data/authored/buying__product-detail.js +62 -56
  15. package/dist/features/tester/seed-data/authored/buying__return-request.js +6 -6
  16. package/dist/features/tester/seed-data/authored/buying__reviews-pagination.js +4 -4
  17. package/dist/features/tester/seed-data/authored/buying__reviews.js +1 -1
  18. package/dist/features/tester/seed-data/authored/buying__user-dashboard-extras.js +8 -8
  19. package/dist/features/tester/seed-data/authored/content-discovery__category-counts-and-rollup.js +1 -1
  20. package/dist/features/tester/seed-data/authored/content-discovery__coupons.js +29 -16
  21. package/dist/features/tester/seed-data/authored/content-discovery__event-detail-subroutes.js +9 -9
  22. package/dist/features/tester/seed-data/authored/content-discovery__events.js +69 -36
  23. package/dist/features/tester/seed-data/authored/content-discovery__notifications.js +1 -1
  24. package/dist/features/tester/seed-data/authored/cta-layout__checkout-bottom-bar.js +2 -2
  25. package/dist/features/tester/seed-data/authored/design-ux__back-to-top-button.js +2 -2
  26. package/dist/features/tester/seed-data/authored/design-ux__general-design.js +1 -1
  27. package/dist/features/tester/seed-data/authored/design-ux__status-badge-legibility.js +1 -1
  28. package/dist/features/tester/seed-data/authored/design-ux__sticky-cta-bar.js +3 -3
  29. package/dist/features/tester/seed-data/authored/index.js +0 -2
  30. package/dist/features/tester/seed-data/authored/page-wiring__data-loss.js +2 -2
  31. package/dist/features/tester/seed-data/authored/page-wiring__reachability.js +6 -4
  32. package/dist/features/tester/seed-data/authored/public-pages__auth-error-pages.js +2 -2
  33. package/dist/features/tester/seed-data/authored/public-pages__core-listing-pages.js +1 -1
  34. package/dist/features/tester/seed-data/authored/public-pages__stores-sellers-directories.js +8 -10
  35. package/dist/features/tester/seed-data/authored/selling__become-seller.js +2 -2
  36. package/dist/features/tester/seed-data/authored/selling__digital-content-delivery.js +2 -2
  37. package/dist/features/tester/seed-data/authored/selling__listing-a-product.js +2 -2
  38. package/dist/features/tester/seed-data/authored/selling__listing-edit-roundtrip.js +2 -2
  39. package/dist/features/tester/seed-data/authored/selling__listing-lifecycle.js +2 -2
  40. package/dist/features/tester/seed-data/authored/selling__seller-listing-types.js +2 -2
  41. package/dist/features/tester/seed-data/authored/seo__og-images.js +3 -4
  42. package/dist/features/tester/seed-data/authored/seo__page-metadata.js +6 -8
  43. package/dist/features/tester/seed-data/authored/seo__sitemap-and-robots.js +5 -5
  44. package/dist/features/tester/seed-data/index.d.ts +0 -10
  45. package/dist/features/tester/seed-data/index.js +0 -10
  46. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +75 -89
  47. package/dist/features/tester/seed-data/tester-responses-seed-data.js +5 -5
  48. package/dist/index.d.ts +0 -8
  49. package/dist/index.js +0 -21
  50. package/dist/seed/addresses-seed-data.js +0 -34
  51. package/dist/seed/index.d.ts +1 -1
  52. package/dist/seed/index.js +4 -3
  53. package/dist/seed/manifest.js +11 -12
  54. package/dist/seed/notifications-seed-data.js +4 -4
  55. package/dist/seed/stores-seed-data.js +7 -52
  56. package/dist/seed/users-seed-data.js +3 -83
  57. package/package.json +1 -1
  58. package/scripts/seed-cli.mjs +15 -20
@@ -381,7 +381,7 @@ export class CategoriesRepository extends BaseRepository {
381
381
  .update({ isLeaf: true, updatedAt: new Date() });
382
382
  }
383
383
  }
384
- await this.rewriteProductChains(rebuilt);
384
+ await this.rewriteProductChains(rebuilt, categoryId);
385
385
  return writes.map((w) => w.id);
386
386
  }
387
387
  /**
@@ -405,11 +405,33 @@ export class CategoriesRepository extends BaseRepository {
405
405
  * no repository in this codebase imports another feature's, because that is
406
406
  * the import chain that drags firebase-admin into a client bundle.
407
407
  */
408
- async rewriteProductChains(rebuilt) {
408
+ async rewriteProductChains(rebuilt, movedId) {
409
+ /*
410
+ * ONE query for the whole subtree, matched on `categorySlugs` — NOT N queries
411
+ * on the scalar `category`.
412
+ *
413
+ * 🛑 MEASURED 2026-09-14: `category` is unset on all 95 production products.
414
+ * It is written by deriveTaxonomy, but every product here came from the seed,
415
+ * which hand-writes `categorySlugs` only — so an equality query on that
416
+ * scalar matched nothing and this rewrite silently did nothing at all. The
417
+ * array field is the one that actually exists, and matching the MOVED node
418
+ * gets the entire subtree in a single read because `categorySlugs` holds the
419
+ * full ancestor chain.
420
+ */
421
+ const snap = await this.db
422
+ .collection("products")
423
+ .where(PRODUCT_FIELDS.CATEGORY_SLUGS, "array-contains", movedId)
424
+ .get();
409
425
  const writes = [];
410
- for (const [categoryId, cat] of rebuilt) {
426
+ for (const doc of snap.docs) {
427
+ const current = doc.data().categorySlugs ?? [];
428
+ const leaf = current[0];
429
+ const cat = leaf ? rebuilt.get(leaf) : undefined;
430
+ // A product whose leaf is outside the moved subtree has nothing to re-derive.
431
+ if (!leaf || !cat)
432
+ continue;
411
433
  // Self first, root last — byte-identical to what deriveTaxonomy produces.
412
- const chain = [categoryId, ...(cat.parentIds ?? []).slice().reverse()];
434
+ const chain = [leaf, ...(cat.parentIds ?? []).slice().reverse()];
413
435
  /*
414
436
  * Byte-identical to what deriveTaxonomy produces, INCLUDING the falsy
415
437
  * filter — these two must agree, or a product's names chain would differ
@@ -420,16 +442,12 @@ export class CategoriesRepository extends BaseRepository {
420
442
  cat.name,
421
443
  ...(cat.ancestors ?? []).map((a) => a?.name).reverse(),
422
444
  ].filter((n) => !!n);
423
- const snap = await this.db
424
- .collection("products")
425
- .where(PRODUCT_FIELDS.CATEGORY, "==", categoryId)
426
- .get();
427
- for (const doc of snap.docs) {
428
- writes.push({
429
- id: doc.id,
430
- data: { categorySlugs: chain, categoryNames: names, updatedAt: new Date() },
431
- });
432
- }
445
+ if (chain.join(">") === current.join(">"))
446
+ continue; // nothing moved for this row
447
+ writes.push({
448
+ id: doc.id,
449
+ data: { categorySlugs: chain, categoryNames: names, updatedAt: new Date() },
450
+ });
433
451
  }
434
452
  if (!writes.length)
435
453
  return;
@@ -28,23 +28,23 @@ export const moneyFlowsPages = [
28
28
  {
29
29
  pageKey: "offer-to-purchase",
30
30
  pageLabel: "Offer → accept → pay",
31
- href: "/products/product-tester-offerable",
31
+ href: "/products/product-beyblade-burst-valkyrie",
32
32
  cases: [
33
33
  {
34
34
  key: "buyer-makes-offer",
35
35
  label: "A buyer can make an offer on a seller's product and it appears in their own offers list",
36
36
  roles: ["buyer"],
37
- startPage: "/products/product-tester-offerable",
37
+ startPage: "/products/product-beyblade-burst-valkyrie",
38
38
  steps: [
39
39
  STEP_SIGNIN_BUYER,
40
- "Open /products/product-tester-offerable.",
40
+ "Open /products/product-beyblade-burst-valkyrie.",
41
41
  "Click 'Make Offer'.",
42
42
  "Enter 780 as the offer amount.",
43
43
  "Enter the note 'QA offer offer-to-purchase-buyer-makes-offer'.",
44
44
  "Click 'Submit offer'.",
45
45
  "Open /user/offers.",
46
46
  ],
47
- expectedBehaviour: "An offer is created for 780 against product-tester-offerable, owned by the signed-in buyer, and the owning seller receives an offer-received notification.",
47
+ expectedBehaviour: "An offer is created for 780 against product-beyblade-burst-valkyrie, owned by the signed-in buyer, and the owning seller receives an offer-received notification.",
48
48
  expectedUiState: "The offer modal closes and a success confirmation appears. /user/offers lists a row for the product with amount ₹780 and status Pending. The page does NOT read 'No offers yet'.",
49
49
  endResult: "After reloading /user/offers the row is still listed at ₹780 with status Pending.",
50
50
  },
@@ -52,10 +52,10 @@ export const moneyFlowsPages = [
52
52
  key: "duplicate-offer-refused",
53
53
  label: "A second offer on the same listing while one is still pending is refused",
54
54
  roles: ["buyer"],
55
- startPage: "/products/product-tester-offerable",
55
+ startPage: "/products/product-beyblade-burst-valkyrie",
56
56
  steps: [
57
57
  "Sign in as the buyer ash@pokemonpalace.in, holding the pending 780 offer from the previous case.",
58
- "Open /products/product-tester-offerable.",
58
+ "Open /products/product-beyblade-burst-valkyrie.",
59
59
  "Click 'Make Offer'.",
60
60
  "Enter 800 as the offer amount.",
61
61
  "Click 'Submit offer'.",
@@ -70,15 +70,15 @@ export const moneyFlowsPages = [
70
70
  roles: ["seller"],
71
71
  startPage: "/store/offers",
72
72
  steps: [
73
- "Sign in as tester@letitrip.in, who owns store-tester-qa-seller.",
73
+ "Sign in as tyson@beybladearena.in / TempPass123!, who owns store-beyblade-arena.",
74
74
  "Open /store/offers.",
75
- "Find the row for offer-tester-sandbox-inbound (product-tester-standard-1, offered ₹120 against a listed ₹150).",
75
+ "Find the row for offer-yugi-burst-valkyrie-pending (product-beyblade-burst-valkyrie, offered ₹780 against a listed ₹999).",
76
76
  "Open the row actions menu.",
77
77
  "Click 'Accept'.",
78
78
  "Confirm in the dialog.",
79
79
  "Reload /store/offers.",
80
80
  ],
81
- expectedBehaviour: "The offer moves to Accepted, a locked price of 120 is recorded, and the buyer receives an offer-responded notification.",
81
+ expectedBehaviour: "The offer moves to Accepted, a locked price of 780 is recorded, and the buyer receives an offer-responded notification.",
82
82
  expectedUiState: "Before acting, the row menu offers Accept, Decline and Counter as enabled controls. After confirming, the row's status badge reads Accepted.",
83
83
  endResult: "After reloading /store/offers the status still reads Accepted — not Pending.",
84
84
  href: "/store/offers",
@@ -89,15 +89,15 @@ export const moneyFlowsPages = [
89
89
  roles: ["buyer"],
90
90
  startPage: "/user/offers",
91
91
  steps: [
92
- "Sign in as tester@letitrip.in, who holds offer-tester-sandbox-accepted on product-beyblade-burst-valkyrie (listed ₹999, agreed ₹780).",
92
+ "Sign in as vivaan.kapoor@gmail.com / TempPass123!, who holds offer-kaiba-dranzer-s-accepted on product-beyblade-original-dranzer-s (listed ₹1499, agreed ₹1250).",
93
93
  "Open /user/offers.",
94
94
  "Click through to checkout from the accepted offer.",
95
95
  "Read the line item price in the checkout summary.",
96
96
  "Continue to the payment step.",
97
97
  "Read the order total.",
98
98
  ],
99
- expectedBehaviour: "Every price computed for this line uses the locked 780, not the product's current 999.",
100
- expectedUiState: "The line item reads ₹780.00 and the order total reflects 780 plus fees. ₹999 appears nowhere on the checkout screen for this line.",
99
+ expectedBehaviour: "Every price computed for this line uses the locked 1250, not the product's current 1499.",
100
+ expectedUiState: "The line item reads ₹1,250.00 and the order total reflects 1250 plus fees. ₹1,499 appears nowhere on the checkout screen for this line.",
101
101
  endResult: "Nothing is placed in this case — stop at the payment step. The check is that the two numbers never appear together for the same line.",
102
102
  href: "/user/offers",
103
103
  },
@@ -105,16 +105,16 @@ export const moneyFlowsPages = [
105
105
  key: "owning-seller-cannot-offer",
106
106
  label: "The owning seller sees no Make Offer control on their own listing",
107
107
  roles: ["seller"],
108
- startPage: "/products/product-tester-standard-1",
108
+ startPage: "/products/product-beyblade-burst-valkyrie",
109
109
  steps: [
110
- "Sign in as tester@letitrip.in, who owns store-tester-qa-seller.",
111
- "Open /products/product-tester-standard-1, a listing owned by that store.",
110
+ "Sign in as tyson@beybladearena.in / TempPass123!, who owns store-beyblade-arena.",
111
+ "Open /products/product-beyblade-burst-valkyrie, a listing owned by that store.",
112
112
  "Read the action controls on the listing.",
113
113
  ],
114
114
  expectedBehaviour: "The seller is not offered any purchase path on their own listing — you cannot make an offer on, or buy, your own item.",
115
115
  expectedUiState: "'Make Offer' is ABSENT, not merely disabled. A seller-facing control such as 'Edit listing' is shown instead.",
116
116
  endResult: "Nothing is persisted; this is a read-only check of which controls render for the owner.",
117
- href: "/products/product-tester-standard-1",
117
+ href: "/products/product-beyblade-burst-valkyrie",
118
118
  },
119
119
  {
120
120
  key: "offer-lane-blocks-other-items",
@@ -122,8 +122,9 @@ export const moneyFlowsPages = [
122
122
  roles: ["buyer"],
123
123
  startPage: "/cart",
124
124
  steps: [
125
- "Sign in as tester@letitrip.in with the accepted offer on product-beyblade-burst-valkyrie in the cart.",
126
- "Open /products/product-tester-standard-2 and click 'Add to cart'.",
125
+ "Sign in as vivaan.kapoor@gmail.com / TempPass123!, who holds offer-kaiba-dranzer-s-accepted.",
126
+ "Open /user/offers and start checkout from that accepted offer so its locked line enters the cart, then return to /products.",
127
+ "Open /products/product-beyblade-burst-valkyrie and click 'Add to cart'.",
127
128
  "Open /cart.",
128
129
  "Read which lane or tab each line sits in.",
129
130
  "Attempt to check out the standard item.",
@@ -135,17 +136,17 @@ export const moneyFlowsPages = [
135
136
  },
136
137
  {
137
138
  key: "expired-offer-cannot-checkout",
138
- label: "An accepted offer past its checkout deadline can no longer be paid",
139
+ label: "An offer past its window can no longer be paid",
139
140
  roles: ["buyer"],
140
141
  startPage: "/user/offers",
141
142
  steps: [
142
- "Sign in as tester@letitrip.in.",
143
- "Open /user/offers and find offer-tester-sandbox-expiring, whose checkout window is a fraction of the run window.",
144
- "Note the countdown shown on the row.",
145
- "Reload the page until the countdown reaches zero.",
143
+ "Sign in as vivaan.kapoor@gmail.com / TempPass123!.",
144
+ "Open /user/offers and find offer-kaiba-x-wizard-arrow-expired (product-beyblade-x-wizard-arrow, ₹700), whose window closed two days ago.",
145
+ "Read the row's status badge and the date it names.",
146
+ "Read every control the row offers.",
146
147
  "Attempt to start checkout from that offer.",
147
148
  ],
148
- expectedBehaviour: "Checkout is refused once the deadline has passed; the offer is treated as expired rather than payable.",
149
+ expectedBehaviour: "Checkout is refused once the window has passed; the offer is treated as expired rather than payable.",
149
150
  expectedUiState: "The row reads Expired and offers no checkout control. It must not silently proceed at ₹700.",
150
151
  endResult: "After reloading /user/offers the row still reads Expired.",
151
152
  href: "/user/offers",
@@ -165,7 +166,7 @@ export const moneyFlowsPages = [
165
166
  // seed-data/fixtures/money-flows__auction-win-to-payment.mjs and closing
166
167
  // ~3 minutes after this batch starts.
167
168
  //
168
- // It used to cite auction-tester-sandbox-cycle-1, which 46 OTHER
169
+ // It used to cite auction-beyblade-original-dragoon-storm, which 46 OTHER
169
170
  // citations need LIVE — one fixture with two contradictory
170
171
  // requirements, so this case could never pass its final step and the
171
172
  // rig notes recorded it as unpassable-by-design.
@@ -261,10 +262,10 @@ export const moneyFlowsPages = [
261
262
  key: "cod-order-places",
262
263
  label: "A cash-on-delivery order can be placed and shows the COD fee before confirming",
263
264
  roles: ["buyer"],
264
- startPage: "/products/product-tester-standard-1",
265
+ startPage: "/products/product-beyblade-burst-valkyrie",
265
266
  steps: [
266
267
  STEP_SIGNIN_BUYER,
267
- "Open /products/product-tester-standard-1 and click 'Add to cart'.",
268
+ "Open /products/product-beyblade-burst-valkyrie and click 'Add to cart'.",
268
269
  "Open /checkout and complete the address step.",
269
270
  "Continue to the add-ons and fees step.",
270
271
  "Select 'Cash on Delivery'.",
@@ -282,7 +283,7 @@ export const moneyFlowsPages = [
282
283
  roles: ["buyer"],
283
284
  startPage: "/checkout",
284
285
  steps: [
285
- "Sign in as the buyer ash@pokemonpalace.in with product-tester-standard-2 in the cart.",
286
+ "Sign in as the buyer ash@pokemonpalace.in with product-beyblade-original-dranzer-s in the cart.",
286
287
  "Open /checkout and complete the address step.",
287
288
  "Select UPI / manual payment.",
288
289
  STEP_PLACE_ORDER,
@@ -319,8 +320,8 @@ export const moneyFlowsPages = [
319
320
  startPage: "/checkout",
320
321
  steps: [
321
322
  STEP_SIGNIN_BUYER,
322
- "Add product-tester-standard-1 (store-tester-sandbox) to the cart.",
323
323
  "Add product-beyblade-burst-valkyrie (store-beyblade-arena) to the cart.",
324
+ "Add prizedraw-beyblade-mystery-box (store-letitrip-official) to the cart, so the cart spans two sellers.",
324
325
  "Open /checkout and reach the payment step.",
325
326
  "Write down the exact total and every fee line.",
326
327
  STEP_PLACE_ORDER,
@@ -359,63 +360,78 @@ export const moneyFlowsPages = [
359
360
  key: "out-of-stock-blocked",
360
361
  label: "A sold-out item cannot be added to the cart or checked out",
361
362
  roles: ["buyer"],
362
- startPage: "/products/product-tester-standard-sold",
363
+ startPage: "/products/product-beyblade-burst-valtryek-v3-sold-out",
363
364
  steps: [
364
365
  STEP_SIGNIN_BUYER,
365
- "Open /products/product-tester-standard-sold, a deliberately sold-out fixture.",
366
+ "Open /products/product-beyblade-burst-valtryek-v3-sold-out, a deliberately sold-out fixture.",
366
367
  "Attempt to add it to the cart.",
367
368
  ],
368
369
  expectedBehaviour: "The add is refused and no cart line is created for an unavailable item.",
369
370
  expectedUiState: "The add control is disabled or replaced by a Sold Out state, with the reason stated on the page.",
370
371
  endResult: "After reloading /cart the item is absent.",
371
- href: "/products/product-tester-standard-sold",
372
+ href: "/products/product-beyblade-burst-valtryek-v3-sold-out",
372
373
  },
373
374
  {
375
+ /*
376
+ * 🛑 THIS CASE MOVED FROM THE CART TO THE BUNDLE EDITOR, 2026-09-15.
377
+ *
378
+ * It used to open a deliberately-banned pair of fixtures — two products
379
+ * in different stores wired into one group — and assert that the cart's
380
+ * picker refused them. Those fixtures are gone, and no replacement can
381
+ * be seeded: the guard the case tests is applied at SAVE time, so a
382
+ * cross-store group cannot exist to be opened.
383
+ *
384
+ * The refusal is therefore tested where it actually fires — an admin
385
+ * ATTEMPTS the banned shape and is refused before it is stored.
386
+ */
374
387
  key: "cross-store-group-refused",
375
- label: "A group spanning two stores cannot be added as one cart line",
376
- roles: ["buyer"],
377
- startPage: "/products/product-tester-crossstore-a",
388
+ label: "A bundle whose members span two stores is refused when saved",
389
+ roles: ["admin"],
390
+ startPage: "/admin/bundles",
378
391
  steps: [
379
- STEP_SIGNIN_BUYER,
380
- "Open /products/product-tester-crossstore-a.",
381
- "Open the group picker ('Pick items').",
382
- "Attempt to select members and add them to the cart.",
392
+ "Sign in as admin@letitrip.in / TempPass123!.",
393
+ "Open /admin/bundles and start a new bundle.",
394
+ "Name it 'QA cross-store bundle'.",
395
+ "Add product-beyblade-burst-valkyrie (store-beyblade-arena) as a member.",
396
+ "Add prizedraw-beyblade-mystery-box (store-letitrip-official) as a second member — a different store.",
397
+ "Set the bundle price to 1000 and click Save.",
383
398
  ],
384
- expectedBehaviour: "The picker refuses a cross-store selection; an order cannot span two sellers.",
385
- expectedUiState: "The picker renders read-only — no quantity column and no 'Add selected to cart' control — and explains that the group spans two stores.",
386
- endResult: "After reloading /cart no line was created.",
387
- href: "/products/product-tester-crossstore-a",
399
+ inputs: { bundleName: "QA cross-store bundle", bundlePrice: 1000 },
400
+ expectedBehaviour: "The save is refused because an order belongs to exactly one seller, and a bundle spanning two stores would produce an order containing another seller's product — with no notification, no shipping resolution and no payout for that second seller. The refusal is at save time, not at add-to-cart time, so bundles already stored keep working.",
401
+ expectedUiState: "An error names the two stores, or at least states that a bundle cannot span sellers. The editor stays open with the entered members intact rather than clearing.",
402
+ endResult: "After reloading /admin/bundles there is no bundle named 'QA cross-store bundle'.",
403
+ href: "/admin/bundles",
388
404
  },
389
405
  {
390
406
  key: "classified-has-no-cart",
391
407
  label: "A classified listing offers contact, never Add to Cart",
392
408
  roles: ["buyer"],
393
- startPage: "/classified/classified-tester-sandbox-1",
409
+ startPage: "/classified/classified-beyblade-stadium-set",
394
410
  steps: [
395
411
  STEP_SIGNIN_BUYER,
396
- "Open /classified/classified-tester-sandbox-1.",
412
+ "Open /classified/classified-beyblade-stadium-set.",
397
413
  "Read every action control on the listing.",
398
414
  ],
399
415
  expectedBehaviour: "A classified is a contact-only listing type with no cart path at all.",
400
416
  expectedUiState: "Exactly one of 'Make Offer', 'Request to Buy' or a contact-seller control is present. 'Add to Cart' and 'Buy Now' are ABSENT, not disabled.",
401
417
  endResult: "Nothing is persisted; this is a read-only check of which controls render.",
402
- href: "/classified/classified-tester-sandbox-1",
418
+ href: "/classified/classified-beyblade-stadium-set",
403
419
  },
404
420
  {
405
421
  key: "guest-gated-action-prompts-signin",
406
422
  label: "A gated action while signed out prompts sign-in and then completes the original action",
407
423
  roles: ["guest"],
408
- startPage: "/products/product-tester-standard-1",
424
+ startPage: "/products/product-beyblade-burst-valkyrie",
409
425
  steps: [
410
426
  "Sign out completely.",
411
- "Open /products/product-tester-standard-1.",
427
+ "Open /products/product-beyblade-burst-valkyrie.",
412
428
  "Click the wishlist heart.",
413
429
  "Sign in as ash@pokemonpalace.in through the prompt that appears.",
414
430
  ],
415
431
  expectedBehaviour: "The click is held while signing in and then replayed, so the item ends up on the wishlist.",
416
432
  expectedUiState: "A sign-in prompt appears rather than the click doing nothing. After signing in, the heart shows the saved state.",
417
433
  endResult: "After reloading /wishlist the item is listed.",
418
- href: "/products/product-tester-standard-1",
434
+ href: "/products/product-beyblade-burst-valkyrie",
419
435
  },
420
436
  {
421
437
  key: "banned-account-blocked",
@@ -79,18 +79,20 @@ export const authored = {
79
79
  startPage: "/admin/users",
80
80
  steps: [
81
81
  "Sign in as admin@letitrip.in / TempPass123!.",
82
- "Open /admin/users and find the account claude-tester@letitrip.in.",
83
- "Open its Edit action and write down whether the tester flags are on.",
82
+ "Open /admin/users and find the account rehan.sheikh@gmail.com.",
83
+ "Open its Edit action, turn BOTH tester flags on, and save — no seeded account carries them, so the case has to set up the field it is about.",
84
+ "Reopen the same account and confirm both flags read as on.",
84
85
  "Change ONLY its bio — append the word roundtrip.",
85
86
  "Press Save.",
86
87
  "Reload the list and reopen the same account.",
87
88
  "Read the tester flags again.",
89
+ "Turn both tester flags back off and save.",
88
90
  ],
89
- inputs: { account: "claude-tester@letitrip.in", bioSuffix: "roundtrip" },
91
+ inputs: { account: "rehan.sheikh@gmail.com", bioSuffix: "roundtrip" },
90
92
  expectedBehaviour: "An editor seeded from a LIST row can only send back what that row carried. If the list serializer omits a field the update schema accepts, the editor state defaults to false and the save rewrites it — so editing any other field silently strips it. That is why the check is on a field the form did not touch.",
91
93
  expectedUiState: "Both tester flags are still on after the reload, and the bio carries the appended word.",
92
94
  expectedData: { testerFlagsPreserved: true },
93
- endResult: "The flags survive a second reload. If this fails, restore them before moving on — the bot needs them to run at all.",
95
+ endResult: "The flags survive a second reload, and the final step leaves the account back on its seeded settings with both flags off.",
94
96
  },
95
97
  "checklist-admin-admin-detail-round-trips-store-edit-keeps-verified": {
96
98
  roles: ["admin"],
@@ -62,14 +62,14 @@ export const authored = {
62
62
  startPage: "/admin/coupons",
63
63
  steps: [
64
64
  "Sign in as admin@letitrip.in / TempPass123!.",
65
- "Open /admin/coupons, find TESTERLIMITED, and read its per-user limit, total limit and current usage.",
66
- "Sign in as vivaan.kapoor@gmail.com / TempPass123! and place an order applying TESTERLIMITED.",
65
+ "Open /admin/coupons, find ARENAVIP, and read its per-user limit (1), total limit (50) and current usage (12).",
66
+ "Sign in as vivaan.kapoor@gmail.com / TempPass123! and place an order of 2 × product-beyblade-original-dranzer-s (₹2,998, above ARENAVIP's ₹2,000 minimum) applying ARENAVIP.",
67
67
  "Attempt a second order applying it again and read the refusal.",
68
- "Sign in as rehan.sheikh@gmail.com / TempPass123! and apply TESTERLIMITED at checkout.",
68
+ "Sign in as rehan.sheikh@gmail.com / TempPass123! and apply ARENAVIP at checkout on the same two items.",
69
69
  "Sign back in as admin and read the usage count.",
70
70
  ],
71
- inputs: { code: "TESTERLIMITED", perUserLimit: 1 },
72
- expectedBehaviour: "TESTERLIMITED is one use per user with NO total limit, so exhausting it for one buyer must leave it fully usable by the next. Enforcing a per-user limit as if it were global withdraws the coupon from everybody after a single redemption.",
71
+ inputs: { code: "ARENAVIP", perUserLimit: 1 },
72
+ expectedBehaviour: "ARENAVIP is one use per user, and its total limit of 50 has 38 uses left — so exhausting it for one buyer must leave it usable by the next. Enforcing a per-user limit as if it were global withdraws the coupon from everybody after a single redemption.",
73
73
  expectedUiState: "The same buyer's second attempt is refused with a limit message. A DIFFERENT buyer's attempt is accepted. The admin usage count has risen by the number of redemptions, and the coupon still reads as active.",
74
74
  expectedData: { perUserLimit: 1 },
75
75
  endResult: "The cross-account half is the case — the same-account half alone passes under either reading.",
@@ -80,7 +80,7 @@ export const authored = {
80
80
  steps: [
81
81
  "Sign in as admin@letitrip.in / TempPass123! and confirm QAADMIN15 has a minimum purchase of 500 and a maximum discount of 200.",
82
82
  "Sign in as vivaan.kapoor@gmail.com / TempPass123!.",
83
- "Add product-tester-standard-3 (₹99) to the cart alone and apply QAADMIN15 at checkout.",
83
+ "Add product-beyblade-metal-storm-pegasus (₹99) to the cart alone and apply QAADMIN15 at checkout.",
84
84
  "Read the refusal and whether it names the required amount.",
85
85
  "Add product-beyblade-original-driger-v (₹1,799) so the subtotal clears the minimum, and apply again.",
86
86
  "Read the discount amount.",
@@ -111,17 +111,18 @@ export const authored = {
111
111
  startPage: "/admin/coupons",
112
112
  steps: [
113
113
  "Sign in as admin@letitrip.in / TempPass123!.",
114
- "Open /admin/coupons and find TESTEREXPIRED, reading its end date and its active flag.",
115
- "Note that it is still marked active while its end date is in the past.",
116
- "Sign in as vivaan.kapoor@gmail.com / TempPass123! and apply TESTEREXPIRED at checkout.",
114
+ "Open /admin/coupons, open SEALED20, and write down its end date.",
115
+ "Set its end date to yesterday and LEAVE the Active toggle ON, then save — it is now past its end date while still flagged active.",
116
+ "Sign in as vivaan.kapoor@gmail.com / TempPass123! and apply SEALED20 at checkout.",
117
117
  "Read the exact wording of the refusal.",
118
- "Sign back in as admin, deactivate an ACTIVE coupon, and apply that one as the buyer.",
118
+ "Sign back in as admin, deactivate BLADER50 without touching its dates, and apply that one as the buyer.",
119
119
  "Compare the two refusal messages.",
120
+ "Sign back in as admin, restore SEALED20's end date and reactivate BLADER50.",
120
121
  ],
121
- inputs: { expiredCode: "TESTEREXPIRED" },
122
- expectedBehaviour: "Expiry and deactivation are two separate reasons and both refuse. TESTEREXPIRED is seeded past its end date while still flagged active precisely so the DATE check is exercised rather than the flag — a validator reading only the flag would accept it.",
122
+ inputs: { expiredCode: "SEALED20", deactivatedCode: "BLADER50" },
123
+ expectedBehaviour: "Expiry and deactivation are two separate reasons and both refuse. SEALED20 is pushed past its end date while still flagged active precisely so the DATE check is exercised rather than the flag — a validator reading only the flag would accept it.",
123
124
  expectedUiState: "The expired coupon is refused with a message naming expiry. The deactivated one is refused with a different message. Two identical generic refusals mean only one of the two checks is running.",
124
- endResult: "Reactivate the coupon that was deactivated. Leaving it off silently breaks every later case that applies it.",
125
+ endResult: "SEALED20's end date is restored and BLADER50 is reactivated by the final step. Leaving either changed silently breaks every later case that applies them.",
125
126
  },
126
127
  "checklist-admin-coupons-coupon-admin-is-platform-wide": {
127
128
  roles: ["admin", "buyer"],