@mohasinac/appkit 4.42.1 → 4.42.2

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 (181) hide show
  1. package/dist/_internal/server/features/offers/adapters.d.ts +29 -7
  2. package/dist/_internal/server/features/offers/adapters.js +14 -3
  3. package/dist/_internal/server/features/orders/adapters.js +54 -1
  4. package/dist/client.d.ts +1 -0
  5. package/dist/client.js +1 -0
  6. package/dist/features/account/components/UserOrdersView.js +30 -2
  7. package/dist/features/account/hooks/useAddresses.d.ts +21 -6
  8. package/dist/features/account/hooks/useAddresses.js +1 -1
  9. package/dist/features/addresses/hooks/usePostalLookup.js +84 -12
  10. package/dist/features/admin/components/AdminCategoriesView.js +37 -2
  11. package/dist/features/admin/components/AdminOffersView.js +2 -2
  12. package/dist/features/admin/constants/filter-tabs.d.ts +25 -0
  13. package/dist/features/admin/constants/filter-tabs.js +30 -0
  14. package/dist/features/admin/hooks/useAdminListingData.d.ts +35 -0
  15. package/dist/features/admin/hooks/useAdminListingData.js +45 -9
  16. package/dist/features/cart/schemas/index.d.ts +15 -0
  17. package/dist/features/cart/schemas/index.js +2 -0
  18. package/dist/features/cart/types/index.d.ts +22 -0
  19. package/dist/features/categories/components/CategoriesIndexListing.js +19 -1
  20. package/dist/features/categories/components/CategoriesIndexPageView.js +14 -1
  21. package/dist/features/classified/components/ClassifiedDetailPageView.d.ts +8 -0
  22. package/dist/features/classified/components/ClassifiedDetailPageView.js +1 -0
  23. package/dist/features/events/actions/event-actions.d.ts +13 -0
  24. package/dist/features/events/actions/event-actions.js +75 -1
  25. package/dist/features/events/repository/event-entry.repository.d.ts +17 -0
  26. package/dist/features/events/repository/event-entry.repository.js +36 -0
  27. package/dist/features/homepage/components/BrandsSection.js +1 -1
  28. package/dist/features/homepage/components/CustomerReviewsSection.js +1 -1
  29. package/dist/features/homepage/components/FeaturedBundlesSection.js +1 -1
  30. package/dist/features/homepage/components/ShopByCategorySection.js +1 -1
  31. package/dist/features/media/upload/MediaUploadField.js +21 -3
  32. package/dist/features/orders/components/OrdersList.js +2 -1
  33. package/dist/features/orders/index.d.ts +1 -0
  34. package/dist/features/orders/index.js +1 -0
  35. package/dist/features/orders/types/index.d.ts +2 -0
  36. package/dist/features/orders/utils/order-ref.d.ts +45 -0
  37. package/dist/features/orders/utils/order-ref.js +58 -0
  38. package/dist/features/products/components/MakeOfferButton.d.ts +18 -1
  39. package/dist/features/products/components/MakeOfferButton.js +23 -1
  40. package/dist/features/products/components/ProductDetailPageView.d.ts +5 -0
  41. package/dist/features/products/components/ProductDetailPageView.js +3 -2
  42. package/dist/features/products/repository/products.repository.js +29 -2
  43. package/dist/features/seller/actions/offer-actions.js +32 -0
  44. package/dist/features/seller/components/QuickProductForm.js +15 -1
  45. package/dist/features/seller/components/SellerBidsView.js +3 -1
  46. package/dist/features/seller/components/SellerOffersView.js +2 -2
  47. package/dist/features/seller/components/SellerOrdersView.js +49 -6
  48. package/dist/features/seller/components/SellerProductsCards.js +23 -1
  49. package/dist/features/seller/components/SellerProductsView.js +27 -2
  50. package/dist/features/seller/hooks/useSellerListingData.d.ts +0 -1
  51. package/dist/features/seller/hooks/useSellerListingData.js +17 -19
  52. package/dist/features/shell/SectionForm.js +33 -2
  53. package/dist/features/tester/seed-data/_happy-path.d.ts +23 -0
  54. package/dist/features/tester/seed-data/_happy-path.js +710 -0
  55. package/dist/features/tester/seed-data/_money-flows.js +34 -23
  56. package/dist/features/tester/seed-data/authored/account-auth__profile-settings.js +22 -3
  57. package/dist/features/tester/seed-data/authored/addresses__address-filters.js +17 -12
  58. package/dist/features/tester/seed-data/authored/addresses__postal-lookup.js +7 -6
  59. package/dist/features/tester/seed-data/authored/addresses__postal-validation.js +10 -4
  60. package/dist/features/tester/seed-data/authored/addresses__state-picker.js +13 -8
  61. package/dist/features/tester/seed-data/authored/addresses__unban-request.js +49 -11
  62. package/dist/features/tester/seed-data/authored/admin__admin-detail-round-trips.js +10 -6
  63. package/dist/features/tester/seed-data/authored/admin__bans-and-trust.js +71 -32
  64. package/dist/features/tester/seed-data/authored/admin__blog-faqs.js +80 -31
  65. package/dist/features/tester/seed-data/authored/admin__bug-hunter-rewards.js +62 -25
  66. package/dist/features/tester/seed-data/authored/admin__bulk-actions.js +61 -31
  67. package/dist/features/tester/seed-data/authored/admin__bundles.js +84 -37
  68. package/dist/features/tester/seed-data/authored/admin__buyer-data-admin.js +90 -57
  69. package/dist/features/tester/seed-data/authored/admin__catalog-listings.js +38 -26
  70. package/dist/features/tester/seed-data/authored/admin__category-brand-authoring.js +47 -28
  71. package/dist/features/tester/seed-data/authored/admin__classifieds-digitalcodes-live.js +118 -31
  72. package/dist/features/tester/seed-data/authored/admin__content-deletes.js +30 -24
  73. package/dist/features/tester/seed-data/authored/admin__content-marketing.js +83 -46
  74. package/dist/features/tester/seed-data/authored/admin__coupons.js +8 -8
  75. package/dist/features/tester/seed-data/authored/admin__events-raffles-spin.js +9 -9
  76. package/dist/features/tester/seed-data/authored/admin__firebase-function-effects.js +19 -4
  77. package/dist/features/tester/seed-data/authored/admin__media-watermark.js +77 -33
  78. package/dist/features/tester/seed-data/authored/admin__orders-fulfillment.js +5 -5
  79. package/dist/features/tester/seed-data/authored/admin__prize-draws-lotteries.js +29 -5
  80. package/dist/features/tester/seed-data/authored/admin__site-system.js +45 -23
  81. package/dist/features/tester/seed-data/authored/admin__uncovered-admin-pages.js +53 -28
  82. package/dist/features/tester/seed-data/authored/admin__users-trust.js +20 -20
  83. package/dist/features/tester/seed-data/authored/buying__bidding.js +19 -2
  84. package/dist/features/tester/seed-data/authored/buying__browsing-search.js +20 -5
  85. package/dist/features/tester/seed-data/authored/buying__buying-checkout.js +28 -10
  86. package/dist/features/tester/seed-data/authored/buying__cart.js +10 -3
  87. package/dist/features/tester/seed-data/authored/buying__image-tile-layout.js +0 -16
  88. package/dist/features/tester/seed-data/authored/buying__my-orders.js +27 -5
  89. package/dist/features/tester/seed-data/authored/buying__offers.js +22 -7
  90. package/dist/features/tester/seed-data/authored/buying__order-detail-actions.js +3 -3
  91. package/dist/features/tester/seed-data/authored/buying__product-detail.js +35 -4
  92. package/dist/features/tester/seed-data/authored/buying__return-request.js +19 -6
  93. package/dist/features/tester/seed-data/authored/buying__reviews-pagination.js +36 -18
  94. package/dist/features/tester/seed-data/authored/buying__reviews.js +16 -1
  95. package/dist/features/tester/seed-data/authored/buying__user-dashboard-navigation.js +2 -1
  96. package/dist/features/tester/seed-data/authored/buying__user-uncovered-pages.js +16 -1
  97. package/dist/features/tester/seed-data/authored/community-support__public-profile.js +3 -2
  98. package/dist/features/tester/seed-data/authored/community-support__support-tickets.js +4 -4
  99. package/dist/features/tester/seed-data/authored/content-discovery__blog.js +19 -2
  100. package/dist/features/tester/seed-data/authored/content-discovery__category-brand-relations.js +4 -4
  101. package/dist/features/tester/seed-data/authored/content-discovery__category-counts-and-rollup.js +5 -5
  102. package/dist/features/tester/seed-data/authored/content-discovery__coupons.js +41 -6
  103. package/dist/features/tester/seed-data/authored/content-discovery__event-detail-subroutes.js +63 -8
  104. package/dist/features/tester/seed-data/authored/content-discovery__event-participation.js +29 -29
  105. package/dist/features/tester/seed-data/authored/content-discovery__events.js +2 -2
  106. package/dist/features/tester/seed-data/authored/content-discovery__faq-help.js +23 -1
  107. package/dist/features/tester/seed-data/authored/content-discovery__notifications.js +6 -6
  108. package/dist/features/tester/seed-data/authored/content-discovery__search.js +1 -1
  109. package/dist/features/tester/seed-data/authored/content-discovery__store-reviews-aggregate.js +1 -1
  110. package/dist/features/tester/seed-data/authored/cta-layout__checkout-bottom-bar.js +31 -6
  111. package/dist/features/tester/seed-data/authored/cta-layout__dialog-footers.js +34 -10
  112. package/dist/features/tester/seed-data/authored/cta-layout__editor-action-bar.js +35 -8
  113. package/dist/features/tester/seed-data/authored/cta-layout__navbar-ctas.js +31 -9
  114. package/dist/features/tester/seed-data/authored/cta-layout__product-bottom-bar.js +29 -3
  115. package/dist/features/tester/seed-data/authored/design-ux__back-to-top-button.js +1 -1
  116. package/dist/features/tester/seed-data/authored/design-ux__carousel-arrow-bounds.js +10 -10
  117. package/dist/features/tester/seed-data/authored/design-ux__dashboard-layout.js +3 -3
  118. package/dist/features/tester/seed-data/authored/design-ux__footer-theme.js +2 -2
  119. package/dist/features/tester/seed-data/authored/design-ux__form-validation-errors.js +2 -2
  120. package/dist/features/tester/seed-data/authored/design-ux__general-design.js +4 -4
  121. package/dist/features/tester/seed-data/authored/design-ux__hand-mode-layout.js +1 -1
  122. package/dist/features/tester/seed-data/authored/design-ux__homepage-carousels.js +8 -8
  123. package/dist/features/tester/seed-data/authored/design-ux__status-badge-legibility.js +2 -2
  124. package/dist/features/tester/seed-data/authored/design-ux__sticky-cta-bar.js +2 -2
  125. package/dist/features/tester/seed-data/authored/page-wiring__data-loss.js +39 -16
  126. package/dist/features/tester/seed-data/authored/page-wiring__detail-pages.js +43 -8
  127. package/dist/features/tester/seed-data/authored/page-wiring__drawer-pages.js +40 -14
  128. package/dist/features/tester/seed-data/authored/page-wiring__reachability.js +31 -14
  129. package/dist/features/tester/seed-data/authored/public-pages__auth-error-pages.js +7 -6
  130. package/dist/features/tester/seed-data/authored/public-pages__bug-hunters.js +14 -5
  131. package/dist/features/tester/seed-data/authored/public-pages__core-listing-pages.js +3 -3
  132. package/dist/features/tester/seed-data/authored/public-pages__help-how-it-works.js +1 -1
  133. package/dist/features/tester/seed-data/authored/public-pages__help-scams-guides-subpages.js +4 -2
  134. package/dist/features/tester/seed-data/authored/public-pages__legal-policy-pages.js +3 -3
  135. package/dist/features/tester/seed-data/authored/public-pages__newly-wired-browse-indexes.js +5 -5
  136. package/dist/features/tester/seed-data/authored/public-pages__store-tabs.js +5 -5
  137. package/dist/features/tester/seed-data/authored/public-pages__stores-sellers-directories.js +3 -3
  138. package/dist/features/tester/seed-data/authored/search-and-nav__employee-permissions.js +45 -11
  139. package/dist/features/tester/seed-data/authored/search-and-nav__header-search.js +57 -17
  140. package/dist/features/tester/seed-data/authored/search-and-nav__settings-deep-links.js +36 -5
  141. package/dist/features/tester/seed-data/authored/search-and-nav__sidebar-search.js +29 -4
  142. package/dist/features/tester/seed-data/authored/selling__become-seller.js +23 -0
  143. package/dist/features/tester/seed-data/authored/selling__final-sale-authoring.js +14 -13
  144. package/dist/features/tester/seed-data/authored/selling__listing-a-product.js +14 -9
  145. package/dist/features/tester/seed-data/authored/selling__listing-edit-roundtrip.js +34 -7
  146. package/dist/features/tester/seed-data/authored/selling__listing-lifecycle.js +17 -7
  147. package/dist/features/tester/seed-data/authored/selling__listing-type-fields-roundtrip.js +9 -9
  148. package/dist/features/tester/seed-data/authored/selling__media-limits.js +9 -4
  149. package/dist/features/tester/seed-data/authored/selling__product-upload-details.js +3 -3
  150. package/dist/features/tester/seed-data/authored/selling__quick-add-minimum-details.js +3 -3
  151. package/dist/features/tester/seed-data/authored/selling__sectionised-forms.js +25 -1
  152. package/dist/features/tester/seed-data/authored/selling__seller-analytics-payouts.js +27 -6
  153. package/dist/features/tester/seed-data/authored/selling__seller-bids-bundles-filters.js +19 -1
  154. package/dist/features/tester/seed-data/authored/selling__seller-catalog-org.js +63 -22
  155. package/dist/features/tester/seed-data/authored/selling__seller-custom-brands.js +9 -5
  156. package/dist/features/tester/seed-data/authored/selling__seller-guide.js +7 -6
  157. package/dist/features/tester/seed-data/authored/selling__seller-listing-types.js +3 -2
  158. package/dist/features/tester/seed-data/authored/selling__seller-marketing-extras.js +18 -18
  159. package/dist/features/tester/seed-data/authored/selling__seller-ops-comms.js +13 -12
  160. package/dist/features/tester/seed-data/authored/selling__seller-orders-at-scale.js +19 -3
  161. package/dist/features/tester/seed-data/authored/selling__seller-orders.js +22 -4
  162. package/dist/features/tester/seed-data/authored/selling__seller-shipping-payouts-setup.js +20 -8
  163. package/dist/features/tester/seed-data/authored/selling__store-dashboard-navigation.js +4 -4
  164. package/dist/features/tester/seed-data/authored/selling__store-uncovered-pages.js +2 -2
  165. package/dist/features/tester/seed-data/authored/seo__canonical-and-host.js +5 -5
  166. package/dist/features/tester/seed-data/authored/seo__og-images.js +1 -1
  167. package/dist/features/tester/seed-data/authored/seo__page-metadata.js +7 -7
  168. package/dist/features/tester/seed-data/authored/seo__sitemap-and-robots.js +3 -3
  169. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +123 -21
  170. package/dist/features/tester/utils/phases.d.ts +17 -0
  171. package/dist/features/tester/utils/phases.js +29 -2
  172. package/dist/index.d.ts +1 -0
  173. package/dist/index.js +2 -0
  174. package/dist/seo/json-ld.d.ts +23 -0
  175. package/dist/seo/json-ld.js +3 -2
  176. package/dist/server.d.ts +1 -0
  177. package/dist/server.js +2 -0
  178. package/dist/styles.css +1 -1
  179. package/dist/tailwind-utilities.css +1 -1
  180. package/dist/ui/components/PaginatedSelect.js +0 -0
  181. package/package.json +1 -1
@@ -12,6 +12,29 @@
12
12
  * keeps its layout height while invisible over-reports the tier on every page that
13
13
  * has no bar, pushing floating controls up the screen site-wide.
14
14
  *
15
+ * 🛑 BOTH RECURRING SHAPES ARE PRESENT TODAY, and three cases were pointed at a
16
+ * surface where neither can be observed (checked 2026-09-29).
17
+ *
18
+ * `BulkActionItem` is `{ id, label, variant, onClick }`. There is NO `action`
19
+ * ActionDef on it, so Rule #7's auto-confirmation — which works by resolving
20
+ * `ActionDef.confirmation` inside `<Button action={…}>` — never runs here.
21
+ * `BulkActionBar.handleApply` calls `selectedAction?.onClick()` directly, and
22
+ * `variant: "danger"` only adds `--danger` styling to the trigger and the
23
+ * option. So **bulk Cancel on /admin/orders cancels every selected order with
24
+ * no dialog**, which is exactly the failure this file's own header describes.
25
+ *
26
+ * And every bulk handler is a fire-and-forget loop —
27
+ * `for (const rowId of selection.selectedIds) void handleQuickStatus(rowId, …)`
28
+ * then `clearSelection()` — so there is no aggregated outcome anywhere: no
29
+ * succeeded count, no failed count, and no way to name which rows failed. Two
30
+ * cases asked for those counts; they now expect their absence and say so.
31
+ *
32
+ * 🛑 /admin/products HAS NO DESTRUCTIVE BULK ACTION. Its preset is
33
+ * [FEATURE, PROMOTE, SALE] — three boolean flags — so the confirmation case
34
+ * cannot start there. It now starts on /admin/orders, whose preset carries
35
+ * CANCEL. Note those three are TOGGLES (`!row[field]`), so running "Feature"
36
+ * over a mixed selection UN-features the rows that were already featured.
37
+ *
15
38
  * @tag domain:tester
16
39
  * @tag layer:seed
17
40
  * @tag pattern:none
@@ -53,47 +76,54 @@ export const authored = {
53
76
  },
54
77
  "checklist-admin-bulk-actions-bulk-destructive-confirms": {
55
78
  roles: ["admin"],
56
- startPage: "/admin/products",
79
+ startPage: "/admin/orders",
57
80
  steps: [
58
81
  SIGN_IN_ADMIN,
59
- "Open /admin/products and select three rows.",
60
- "Trigger the destructive bulk action offered.",
61
- "Read the dialog WITHOUT confirming.",
62
- "Check it names the action and how many rows it will affect.",
63
- "Cancel it and check all three rows are untouched.",
64
- "Repeat on /admin/users and /admin/orders.",
82
+ "Open /admin/products and read its bulk actions with three rows selected — they are Feature, Promote and Sale, and NONE of them is destructive, so there is nothing to confirm here.",
83
+ "Open /admin/orders and select ONE order that is already cancelled, so that running the action cannot destroy anything.",
84
+ "Open the bulk action picker and read the options — Mark Shipped, Mark Delivered and Cancel.",
85
+ "Choose Cancel and apply it. Watch carefully for a confirmation dialog BEFORE anything commits.",
86
+ "Record whether a dialog appeared at all, and what the bar did immediately afterwards.",
87
+ "Open /admin/users, select one row, and check whether its Delete bulk action confirms.",
65
88
  ],
66
- expectedBehaviour: "Every destructive bulk action confirms first, naming the count. An action defined inline at its call site rather than in the shared registry has no confirmation configured at all, and so runs immediately and irreversibly — on the largest selection the admin has ever made, since bulk is where selections are largest.",
67
- expectedUiState: "A confirmation naming the action and the row count appears on all three surfaces, and cancelling changes nothing. A destructive action that runs on click is the finding.",
68
- endResult: "Nothing is deleted.",
89
+ inputs: { productBulkActions: "Feature, Promote, Sale", orderBulkActions: "Mark Shipped, Mark Delivered, Cancel" },
90
+ expectedBehaviour: "Every destructive bulk action should confirm first, naming the count. Bulk is where selections are largest, so an unconfirmed destructive action is irreversible across the most rows an admin ever touches at once.",
91
+ expectedUiState: "🛑 EXPECT NO CONFIRMATION, AND RECORD IT AS A FAILURE. `BulkActionItem` carries no ActionDef, so the registry's confirmation never resolves; the bar calls the handler directly and `variant: \"danger\"` only colours the control. Select an ALREADY-CANCELLED order so that proving this costs nothing. Say for each surface whether a dialog appeared.",
92
+ expectedData: { confirmationShownOnOrdersCancel: false },
93
+ endResult: "Nothing that mattered was destroyed — the only order actioned was already cancelled. Do not test this with a live order.",
69
94
  },
70
95
  "checklist-admin-bulk-actions-bulk-action-reports-result": {
71
96
  roles: ["admin"],
72
97
  startPage: "/admin/products",
73
98
  steps: [
74
99
  SIGN_IN_ADMIN,
75
- "Open /admin/products and select three rows.",
76
- "Run a non-destructive bulk action on them, such as a status or flag change.",
77
- "Read the message shown when it completes.",
78
- "Check it states how many rows succeeded and how many failed.",
79
- "Check the affected rows show the change without a manual reload.",
100
+ "Open /admin/products and note which of three rows are already Featured — the bulk action is a TOGGLE, so this matters.",
101
+ "Select three rows that are all NOT featured, so the toggle moves them the same way.",
102
+ "Run the Feature bulk action.",
103
+ "Read whatever message appears when it completes, and record the exact wording.",
104
+ "Check whether the three rows show the change without a manual reload.",
105
+ "Re-select the same three and run Feature again to put them back.",
80
106
  ],
81
- expectedBehaviour: "The result names the counts. A bare 'Done' after an action over N rows tells the admin nothing about whether it was N — and a partial success reported as an unqualified success is how rows silently stay unchanged.",
82
- expectedUiState: "A message naming the succeeded and failed counts, and the rows updated in place. A bare success message is the finding.",
83
- endResult: "Restore whatever the action changed.",
107
+ inputs: { action: "Feature", rowCount: 3 },
108
+ expectedBehaviour: "The result should name the counts. A bare 'Done' after an action over N rows tells the admin nothing about whether it was N — and a partial success reported as an unqualified success is how rows silently stay unchanged.",
109
+ expectedUiState: "🛑 EXPECT NO COUNTS, AND RECORD IT AS A FAILURE. The handler is a fire-and-forget loop over the selected ids followed by clearSelection(), so nothing collects an outcome: there is no succeeded count, no failed count, and no aggregated message to read. Record what IS shown — per-row toast, nothing at all, or a bare success — and whether the rows updated in place. 🛑 Select rows that are all in the same starting state: Feature inverts each row independently, so a mixed selection un-features the ones that were already featured.",
110
+ expectedData: { succeededCountShown: false, failedCountShown: false },
111
+ endResult: "The three rows are back to not-featured. Running the same toggle twice restores them, which is only true because they all started the same way.",
84
112
  },
85
113
  "checklist-admin-bulk-actions-bulk-partial-failure-named": {
86
114
  roles: ["admin"],
87
115
  startPage: "/admin/products",
88
116
  steps: [
89
117
  SIGN_IN_ADMIN,
90
- "Open /admin/products and build a selection mixing rows the action can apply to with rows it cannot — an archived row alongside published ones, for example.",
91
- "Run the action.",
92
- "Read the result message.",
93
- "Check the rows that failed are named rather than only counted.",
94
- "Check the rows that succeeded did in fact change.",
118
+ "Open /admin/orders and build a selection mixing rows the action can apply to with rows it cannot — one PENDING order alongside one already CANCELLED and one already DELIVERED.",
119
+ "Run Mark Shipped over that selection.",
120
+ "Read the result message and record the exact wording.",
121
+ "Check whether the rows that could not be shipped are NAMED, merely counted, or not mentioned at all.",
122
+ "Reload and read each of the three rows' status to find out what actually happened to each.",
123
+ "Restore any order whose status this changed.",
95
124
  ],
96
- expectedBehaviour: "Failures are named. 'Two rows failed' out of a selection of twenty leaves the admin re-checking twenty rows by hand to find which two — and the usual next step is to run the action again on everything, which is worse.",
125
+ inputs: { action: "Mark Shipped", mixedStatuses: "pending + cancelled + delivered" },
126
+ expectedBehaviour: "Failures should be named. 'Two rows failed' out of a selection of twenty leaves the admin re-checking twenty rows by hand to find which two — and the usual next step is to run the action again on everything, which is worse. Naming nothing at all is worse still: the admin cannot even tell that anything failed.",
97
127
  expectedUiState: "The failed rows are identified by title or id, with a reason. A bare failure count is the finding.",
98
128
  endResult: "Restore whatever succeeded.",
99
129
  },
@@ -149,14 +179,14 @@ export const authored = {
149
179
  steps: [
150
180
  SIGN_IN_ADMIN,
151
181
  "Open /admin/offers and select two rows.",
152
- "Read every bulk action offered.",
153
- "Run each one in turn, noting what changed.",
154
- "Reload after each and check whether the records actually changed.",
155
- "Record any action that only cleared the selection.",
182
+ "Read what the bulk bar offers — the correct answer here is NOTHING.",
183
+ "If any bulk action IS offered, run it, reload, and check whether the records actually changed.",
184
+ "Open a single offer's row menu and confirm Cancel is there instead, and that it demands a reason.",
156
185
  ],
157
- expectedBehaviour: "Every offered action does what its label says. A destructively-labelled bulk action once existed here whose handler only cleared the selection — it read as working, cancelled nothing, and an admin using it believed a batch of offers had been cancelled.",
158
- expectedUiState: "Each action produces a visible, persisted change matching its label. An action whose only effect is clearing the selection is the finding, named.",
159
- endResult: "Restore whatever the actions changed.",
186
+ inputs: { expectedBulkActionCount: 0 },
187
+ expectedBehaviour: "Every offered action does what its label says — and where it cannot, it is not offered. A destructively-labelled bulk action once existed here whose handler only cleared the selection: it read as working, cancelled nothing, and an admin using it believed a batch of offers had been cancelled. It was DELETED rather than wired, because cancelling an offer requires a reason and one shared reason across a mixed selection is worse audit data than no bulk action at all.",
188
+ expectedUiState: "No bulk actions are offered on this page. That absence is the pass. A re-appeared bulk Cancel is the finding — whether it works or not — and per-offer Cancel is still available from the row menu, where it can ask for a reason.",
189
+ endResult: "Nothing is changed; no offer is cancelled.",
160
190
  },
161
191
  "checklist-admin-bulk-actions-bulk-users-actions": {
162
192
  roles: ["admin"],
@@ -11,6 +11,33 @@
11
11
  * that mirror, so a write path that updates the rule and forgets the mirror
12
12
  * produces a bundle that renders as "0 items" with no error anywhere.
13
13
  *
14
+ * 🛑 THREE MEMBERS MINIMUM, AND EVERY CASE HERE ASKED FOR TWO (fixed 2026-09-29).
15
+ *
16
+ * BUNDLE_MIN_ITEMS is 3 (bundle-config.ts:10) and bundle-form.ts's superRefine
17
+ * enforces it with "Select at least 3 products (currently 2)." So the create
18
+ * case could never save, and the two cases that READ that bundle were testing
19
+ * something which had never been created — three verdicts of "could not test"
20
+ * for a rule the editor states on its own field label.
21
+ *
22
+ * Every member is now a real seeded id with its real price, so the undiscounted
23
+ * total is arithmetic a tester can check rather than a number to trust.
24
+ *
25
+ * 🛑 EVERY STANDARD LISTING IN THE CATALOGUE BELONGS TO store-beyblade-arena.
26
+ * The only other seller with stock is store-letitrip-official, whose six
27
+ * listings are all prize draws — so the cross-store refusal is exercised with
28
+ * one of those. The guard reads storeId and does not care about listing type.
29
+ * (CLAUDE.md claims product-tester-crossstore-a/b are seeded for exactly this;
30
+ * they are not, anywhere — recorded in OUTOFSCOPE.)
31
+ *
32
+ * 🛑 THE SLUG IS DERIVED AND THEREFORE KNOWABLE: the route slugifies the name
33
+ * and prefixes "bundle-", so 'QA Bundle admin-create' is always
34
+ * /bundles/bundle-qa-bundle-admin-create. The cases used to say "/bundles/{slug}"
35
+ * without saying how to learn it.
36
+ *
37
+ * audit-tester-created-id: bundle-qa-bundle-admin-create — created by case 1 step 2
38
+ * and visited by cases 1-3. It is not a seed fixture and must not be one: the point
39
+ * is to exercise the CREATE path, and a pre-seeded bundle would skip it.
40
+ *
14
41
  * @tag domain:tester
15
42
  * @tag layer:seed
16
43
  * @tag pattern:none
@@ -24,56 +51,69 @@ export const authored = {
24
51
  startPage: "/admin/bundles",
25
52
  steps: [
26
53
  "Sign in as admin@letitrip.in / TempPass123!.",
27
- "Open /admin/bundles and create one named 'QA Bundle admin-create'.",
28
- "Add product-beyblade-burst-valkyrie (₹999) and product-beyblade-x-wizard-arrow (₹899) as members.",
29
- "Set the bundle price to 1500 and save.",
54
+ "Open /admin/bundles/new and set Name to 'QA Bundle admin-create'.",
55
+ "Leave the member-source select on its static option, add exactly TWO members — product-beyblade-burst-valkyrie and product-beyblade-x-wizard-arrow — set 'Bundle price (₹)' to 1500, and try to save.",
56
+ "Read the refusal: the minimum is three, and the field label states the range.",
57
+ "Add product-beyblade-x-knife-shinobi (₹949) as the third member and save.",
30
58
  "RELOAD the editor and read the members, the price and the original total.",
31
- "Open /bundles/{slug} in a private window and read the members, price and discount badge.",
32
- "Attempt to add a member from a different store and read what happens.",
59
+ "Open /bundles/bundle-qa-bundle-admin-create — the slug is the name slugified under a 'bundle-' prefix — and read the members, price and discount badge.",
60
+ "Back in the editor add prizedraw-beyblade-mystery-box, which belongs to store-letitrip-official, as a fourth member, save, and read what happens.",
61
+ "Remove that fourth member and save, leaving the bundle at its three arena members.",
33
62
  ],
34
63
  inputs: {
35
64
  name: "QA Bundle admin-create",
36
65
  member1: "product-beyblade-burst-valkyrie",
37
66
  member2: "product-beyblade-x-wizard-arrow",
67
+ member3: "product-beyblade-x-knife-shinobi",
68
+ crossStoreMember: "prizedraw-beyblade-mystery-box",
38
69
  bundlePrice: 1500,
39
- memberTotal: 1898,
70
+ memberTotal: 2847,
40
71
  },
41
- expectedBehaviour: "The bundle stores its members, its locked price and the members' undiscounted total — the last of these is what drives the discount badge, so a bundle without it shows a price and no saving. Members must come from one store: the store id is the order-splitting key, and a cross-store bundle produces an order belonging to one seller holding another's products.",
42
- expectedUiState: "After the reload both members are listed with a bundle price of ₹1,500 against ₹1,898. The public page shows both members and a discount badge. The cross-store member is refused at save time with a message naming the reason.",
43
- expectedData: { memberCount: 2, bundlePrice: 1500 },
44
- endResult: "Leave the bundle in place — the next three cases read it.",
72
+ expectedBehaviour: "A bundle holds at least three members, stores its locked price, and derives the members' undiscounted total SERVER-SIDE — the editor never sends that total, and it is what drives the discount badge, so a bundle without it shows a price and no saving. Members must come from one store: the store id is the order-splitting and payout key, so a cross-store bundle would produce an order belonging to one seller while holding another seller's products.",
73
+ expectedUiState: "The two-member save is refused inline with 'Select at least 3 products (currently 2).' — not a silent no-op and not a generic banner. With the third added, the reload lists all three, a bundle price of ₹1,500 against an original total of ₹2,847, and the public page shows the three members with a discount badge of roughly 47% off. The cross-store save is refused with a message naming how many sellers the members span, not a 500.",
74
+ expectedData: { memberCount: 3, bundlePrice: 1500, bundleOriginalTotal: 2847 },
75
+ endResult: "The bundle survives with its three arena members — the next two cases read it. The cross-store member is NOT part of it.",
45
76
  },
46
77
  "checklist-admin-bundles-bundle-stock-sync": {
47
78
  roles: ["admin", "seller", "guest"],
48
79
  startPage: "/admin/bundles",
49
80
  steps: [
50
- "Sign in as admin@letitrip.in / TempPass123! and open /bundles/{slug} for 'QA Bundle admin-create' in a private window, noting it is purchasable.",
51
- "Sign in as tyson@beybladearena.in / TempPass123! and set product-beyblade-x-wizard-arrow's stock to 0.",
52
- "Reload the bundle's public page and read its availability.",
53
- "Open /admin/bundles and read the bundle's row.",
54
- "Set the product's stock back to its original value.",
55
- "Reload the bundle's public page and read its availability again.",
81
+ "Open /bundles/bundle-qa-bundle-admin-create signed out and note that it offers purchase.",
82
+ "Sign in as tyson@beybladearena.in / TempPass123!, open /store/products, edit product-beyblade-x-wizard-arrow, and set its stock quantity from 12 to 0.",
83
+ "Reload /bundles/bundle-qa-bundle-admin-create and read its availability.",
84
+ "Sign in as admin@letitrip.in / TempPass123!, open /admin/bundles and read the bundle's row.",
85
+ "Sign back in as tyson@beybladearena.in and set that product's stock back to 12.",
86
+ "Reload /bundles/bundle-qa-bundle-admin-create and read its availability again.",
56
87
  ],
57
- inputs: { member: "product-beyblade-x-wizard-arrow" },
88
+ inputs: { member: "product-beyblade-x-wizard-arrow", stockBefore: 12, stockDuring: 0 },
58
89
  expectedBehaviour: "A bundle is all-or-nothing, so one member going out of stock makes the whole bundle unavailable. That is kept in sync by a hook on the member's stock change rather than computed per request — which is what makes the reverse direction worth testing too: restoring the stock must restore the bundle.",
59
- expectedUiState: "With the member at zero the bundle reads as unavailable and cannot be purchased. After the stock is restored it becomes purchasable again. A bundle still offering purchase with a zero-stock member is the failure.",
60
- endResult: "The member's stock is restored. Both directions matter — a sync that only ever disables is half a sync.",
90
+ expectedUiState: "With the member at zero the bundle reads as unavailable and offers no purchase control. After the stock is restored it becomes purchasable again. A bundle still offering purchase with a zero-stock member is the failure; so is one that stays unavailable after the stock returns.",
91
+ expectedData: { stockAfterRestore: 12 },
92
+ endResult: "The member's stock is back at 12 and the bundle is purchasable. Both directions matter — a sync that only ever disables is half a sync, and leaving the stock at zero silently breaks the next case.",
61
93
  },
62
94
  "checklist-admin-bundles-bundle-edit-delete": {
63
95
  roles: ["admin", "guest"],
64
96
  startPage: "/admin/bundles",
65
97
  steps: [
66
98
  "Sign in as admin@letitrip.in / TempPass123!.",
67
- "Open the editor for 'QA Bundle admin-create' and write down every field.",
68
- "Change ONLY the bundle price to 1400 and save.",
69
- "RELOAD and compare every field against what was written down.",
70
- "Remove one member, save, RELOAD, and read the member list and the original total.",
71
- "Open the public page and read the members and the discount.",
72
- "Delete the bundle and confirm it is gone from /admin/bundles and from /bundles.",
99
+ "Open /admin/bundles, open 'QA Bundle admin-create' for editing, and write down every field — name, description, price, the three members, cover image, brand and active.",
100
+ "Change ONLY 'Bundle price (₹)' to 1400 and save.",
101
+ "RELOAD the editor and compare every field against what was written down.",
102
+ "SWAP one member rather than dropping one: remove product-beyblade-x-knife-shinobi (₹949), add product-beyblade-burst-regalia-genesis (₹1,399), keeping three. Save, RELOAD, and read the member list and the original total.",
103
+ "Open /bundles/bundle-qa-bundle-admin-create and read the members and the discount badge.",
104
+ "Back in the editor click the delete control, confirm, and check the bundle is gone from /admin/bundles and from /bundles.",
73
105
  ],
74
- inputs: { priceBefore: 1500, priceAfter: 1400 },
75
- expectedBehaviour: "An edit writes back only what changed, and removing a member recomputes the undiscounted total — a stale total makes the discount badge claim a saving that no longer exists. The delete removes it from the public listing too.",
76
- expectedUiState: "After the price edit only the price differs. After the member removal the list holds one member and the original total has fallen accordingly. After the delete the bundle is absent from both surfaces.",
106
+ inputs: {
107
+ priceBefore: 1500,
108
+ priceAfter: 1400,
109
+ memberRemoved: "product-beyblade-x-knife-shinobi",
110
+ memberAdded: "product-beyblade-burst-regalia-genesis",
111
+ totalBefore: 2847,
112
+ totalAfter: 3297,
113
+ },
114
+ expectedBehaviour: "An edit writes back only what changed, and changing the member list recomputes the undiscounted total server-side — a stale total makes the discount badge claim a saving that no longer exists. A SWAP rather than a bare removal is what makes this readable: the count stays at the legal minimum of three while the total must move, so a total that did not change cannot be excused as the member rule having been left alone.",
115
+ expectedUiState: "After the price edit only the price differs — every other field matches what was written down. After the swap the list holds three members and the original total reads ₹3,297 rather than ₹2,847, so the badge's saving shrinks. After the delete the bundle is absent from /admin/bundles and /bundles, and its own URL no longer renders it.",
116
+ expectedData: { memberCountAfterSwap: 3, bundleOriginalTotalAfterSwap: 3297 },
77
117
  endResult: "The bundle is deleted. A public page still rendering a deleted bundle is a cache or projection failure worth naming.",
78
118
  },
79
119
  "checklist-admin-bundles-bundle-brand-picker": {
@@ -81,17 +121,24 @@ export const authored = {
81
121
  startPage: "/admin/bundles",
82
122
  steps: [
83
123
  "Sign in as admin@letitrip.in / TempPass123!.",
84
- "Open /admin/bundles and create one named 'QA Bundle brand-picker' with two members from Beyblade Arena.",
85
- "Find the brand picker and read what it offers.",
86
- "Select Beyblade as the bundle's brand and save.",
87
- "RELOAD and read the brand.",
88
- "Open /brands/brand-beyblade in a private window and find the bundle in its Bundles tab.",
89
- "Clear the brand, save, reload, and check the bundle leaves that tab.",
124
+ "Open /admin/bundles/new, set Name to 'QA Bundle brand-picker', and add THREE members from Beyblade Arena — product-beyblade-burst-valkyrie, product-beyblade-x-wizard-arrow and product-beyblade-x-knife-shinobi. Fewer than three is refused.",
125
+ "Set 'Bundle price (₹)' to 2000.",
126
+ "Find the 'Brand' select and read what it offers.",
127
+ "Select Beyblade and save.",
128
+ "RELOAD the editor and read the brand.",
129
+ "Open /brands/brand-beyblade signed out and find the bundle in its Bundles tab.",
130
+ "Back in the editor set Brand to 'No specific brand', save, reload the brand page, and check the bundle has left that tab.",
90
131
  "Delete the bundle.",
91
132
  ],
92
- inputs: { name: "QA Bundle brand-picker", brand: "Beyblade" },
133
+ inputs: {
134
+ name: "QA Bundle brand-picker",
135
+ brand: "Beyblade",
136
+ clearedTo: "No specific brand",
137
+ bundlePrice: 2000,
138
+ },
93
139
  expectedBehaviour: "A bundle's own brand tag is a DIFFERENT field from any brand filter inside a dynamic member rule — the tag says which brand the bundle belongs to regardless of how its members were resolved, and it is what the brand page's Bundles tab filters on. Not every bundle needs one; a genuinely cross-brand bundle leaves it unset.",
94
- expectedUiState: "The picker offers real brands. With the brand set the bundle appears in that brand's Bundles tab; with it cleared it leaves. A bundle appearing on a brand page it was never tagged with means the tab is filtering on the member rule instead.",
95
- endResult: "The bundle is deleted by the final step.",
140
+ expectedUiState: "The select offers the real seeded brands plus a 'No specific brand' option — not a free-text box and not an empty list. With Beyblade set the bundle appears in that brand's Bundles tab; with it cleared it leaves. Worth watching: two of the three members are tagged Takara-Tomy rather than Beyblade, so a bundle still appearing under Beyblade after the tag is cleared means the tab is filtering on the MEMBERS instead of the tag.",
141
+ expectedData: { brandAfterReload: "Beyblade" },
142
+ endResult: "The bundle is deleted by the final step, so /brands/brand-beyblade's Bundles tab holds only the seeded bundles again.",
96
143
  },
97
144
  };
@@ -13,6 +13,26 @@
13
13
  * returns a per-user summary only, so the honest fix was to route to the owning
14
14
  * user rather than widen the payload.
15
15
  *
16
+ * 🛑 /admin/addresses IS AN OWNER-SCOPED LOOKUP, NOT A LISTING (fixed 2026-09-29).
17
+ *
18
+ * It renders `AdminAddressBookView`: an 'Owner type' select (user | store), an
19
+ * 'Owner ID' text field placeholdered `user-...` / `store-...`, and a Search
20
+ * button. There are no columns, no sort dropdown, no filter drawer and no
21
+ * free-text search of addresses — and nothing at all is shown until an owner id
22
+ * is searched. Three cases here described a listing with owner-type filters and
23
+ * a sort default; none of that exists on this page.
24
+ *
25
+ * The banned-address queue those descriptions actually fit is a DIFFERENT
26
+ * route, `ROUTES.ADMIN.BANNED_ADDRESSES` ("Banned Addresses"), backed by
27
+ * `AdminAddressesView` — similar name, different page. Worth stating because
28
+ * reading the plausibly-named component instead of following the route is how
29
+ * this was nearly mis-audited.
30
+ *
31
+ * Its editor DOES exist, at /admin/addresses/new and /admin/addresses/[id]/edit,
32
+ * and per its own header the postal code is now validated per country by schema
33
+ * rather than by `length === 6` — so 'abcdef' should be REJECTED, and the CRUD
34
+ * case expects a pass.
35
+ *
16
36
  * @tag domain:tester
17
37
  * @tag layer:seed
18
38
  * @tag pattern:none
@@ -26,7 +46,7 @@ export const authored = {
26
46
  startPage: "/admin/carts",
27
47
  steps: [
28
48
  "Sign in as admin@letitrip.in / TempPass123!.",
29
- "Open the admin carts listing and read every column.",
49
+ "Open /admin/carts and read every column.",
30
50
  "Check each row identifies whose cart it is and how many items it holds.",
31
51
  "Read whether guest carts are distinguishable from signed-in ones.",
32
52
  "Use every filter and sort offered, confirming each changes the rows.",
@@ -43,14 +63,16 @@ export const authored = {
43
63
  startPage: "/admin/carts",
44
64
  steps: [
45
65
  "Sign in as admin@letitrip.in / TempPass123!.",
46
- "Open the admin carts listing and find a row whose item count is greater than one.",
47
- "Click the row and read what opens.",
48
- "Read whether the individual ITEMS are listed — titles, quantities, prices.",
49
- "Read the row action menu and check it offers a view rather than only mutations.",
50
- "Close and try opening a different row the same way.",
66
+ "Open /admin/carts and find a row whose item count is greater than one.",
67
+ "Click the row and read what opens — it should be a 'Cart Details' modal.",
68
+ "Read its badge: a guest cart reads 'Guest' and a signed-in one reads 'Authenticated'.",
69
+ "Read its fields — Session or User, Last updated, Cart ID.",
70
+ "Read the 'Items in cart' section and check each item carries a title, a quantity and a price.",
71
+ "Close and open a different row the same way.",
51
72
  ],
73
+ inputs: { modalTitle: "Cart Details", itemsHeading: "Items in cart" },
52
74
  expectedBehaviour: "A row opens to show the cart's items. The list already computes the item count from the items array and then discards it, so the data needed to render the detail is present at list time — this is a UI wiring gap rather than a missing query, and the fix is a detail view rather than a wider payload.",
53
- expectedUiState: "Clicking a row opens a detail showing each item with its title, quantity and price. A row that offers only actions, or does nothing on click, is the dead-end failure this page family exists to close.",
75
+ expectedUiState: "Clicking a row opens the 'Cart Details' modal showing each item with its title, quantity and price under 'Items in cart'. A row that does nothing on click, or opens a modal whose items section is empty for a cart the list counted as non-empty, is the dead-end failure this page family exists to close.",
54
76
  endResult: "Read-only; close without acting.",
55
77
  },
56
78
  "checklist-admin-buyer-data-admin-wishlists-admin-view": {
@@ -58,7 +80,7 @@ export const authored = {
58
80
  startPage: "/admin/wishlists",
59
81
  steps: [
60
82
  "Sign in as admin@letitrip.in / TempPass123!.",
61
- "Open the admin wishlists listing and read every column.",
83
+ "Open /admin/wishlists and read every column.",
62
84
  "Check each row names the owning user and the number of items.",
63
85
  "Read whether the item titles themselves are shown in the list.",
64
86
  "Use every filter and sort offered.",
@@ -74,15 +96,15 @@ export const authored = {
74
96
  startPage: "/admin/wishlists",
75
97
  steps: [
76
98
  "Sign in as admin@letitrip.in / TempPass123!.",
77
- "Open the admin wishlists listing and click a row.",
78
- "Read where it lands.",
79
- "Check whether it opens the owning USER rather than a wishlist detail.",
80
- "Go back, open the admin history listing, and click a row there.",
81
- "Read where that lands.",
99
+ "Open /admin/wishlists and click a row.",
100
+ "Read the URL it lands on — it should be that row's owner under /admin/users/{userId}, not a wishlist detail.",
101
+ "Go back, open /admin/history, and click a row there.",
102
+ "Read the URL that lands on.",
82
103
  "From the user page reached, check the wishlist or history is readable in context.",
83
104
  ],
105
+ inputs: { expectedDestination: "/admin/users/{userId}" },
84
106
  expectedBehaviour: "These two rows route to the owning user rather than to a per-record detail, and that is the correct answer rather than a shortcut: their list queries return only a per-user summary, so a detail view would have to fetch separately, and the user page is where the rest of that person's data already is.",
85
- expectedUiState: "Both rows open the owning user's admin page. Neither is inert. From there the wishlist or history contents are reachable. A row that does nothing at all is the failure; a row routing to the user is a pass.",
107
+ expectedUiState: "Both rows navigate to /admin/users/{userId} for that row's owner. Neither is inert. From there the wishlist or history contents are reachable. A row that does nothing at all is the failure; a row routing to the user is a pass. A row whose owner id is missing, so the click does nothing, is also a failure — the navigation is guarded on it.",
86
108
  endResult: "Read-only; close without acting.",
87
109
  },
88
110
  "checklist-admin-buyer-data-admin-history-admin-view": {
@@ -90,7 +112,7 @@ export const authored = {
90
112
  startPage: "/admin/history",
91
113
  steps: [
92
114
  "Sign in as admin@letitrip.in / TempPass123!.",
93
- "Open the admin browsing-history listing and read every column.",
115
+ "Open /admin/history and read every column.",
94
116
  "Check each row names the owning user and the number of entries.",
95
117
  "Read the highest entry count in the list.",
96
118
  "Use every filter and sort offered.",
@@ -107,17 +129,17 @@ export const authored = {
107
129
  startPage: "/admin/notifications",
108
130
  steps: [
109
131
  "Sign in as admin@letitrip.in / TempPass123!.",
110
- "Open the admin notifications listing and read every column.",
132
+ "Open /admin/notifications and read every column.",
111
133
  "Read every type filter offered and select each, noting any that returns nothing.",
112
134
  "For each empty filter, check the unfiltered list for rows of that type.",
113
- "Open a row's view action and read the full body and payload.",
135
+ "Open a row menu's 'View details' action and read the full body and payload in the modal it opens.",
114
136
  "Read whether the recipient is identified.",
115
137
  "Search zzzznope and read the count.",
116
138
  ],
117
139
  inputs: { nonsenseQuery: "zzzznope" },
118
- expectedBehaviour: "Every type chip names a value notifications actually carry. The type union is a single runtime list, and for a long time the filters were built from a nine-value copy while twenty-seven types existed — eighteen were unfilterable and two could not be allow-listed for any channel at all.",
119
- expectedUiState: "Every type filter returns its own rows or is empty with none unfiltered either. A row opens to show the full body and payload rather than only the title. 'zzzznope' returns none.",
120
- expectedData: { nonsenseResultCount: 0 },
140
+ expectedBehaviour: "Every type chip names a value notifications actually carry, and the union has THIRTY values today. The type union is a single runtime list, and for a long time the filters were built from a nine-value copy — most types were unfilterable and two could not be allow-listed for any channel at all. Count the chips against thirty rather than against the seeded data, which covers only ten of them.",
141
+ expectedUiState: "Every type filter returns its own rows or is empty with none unfiltered either. A row opens to show the full body and payload rather than only the title. 'zzzznope' returns none. A chip MISSING for a real type is as much a finding as a chip that matches nothing — record the count you saw.",
142
+ expectedData: { nonsenseResultCount: 0, typeChipCount: 30 },
121
143
  endResult: "Read-only. A chip empty while matching rows sit unfiltered is the finding, named by type.",
122
144
  },
123
145
  "checklist-admin-buyer-data-admin-reviews-admin-view": {
@@ -125,7 +147,7 @@ export const authored = {
125
147
  startPage: "/admin/reviews",
126
148
  steps: [
127
149
  "Sign in as admin@letitrip.in / TempPass123!.",
128
- "Open /admin/reviews and read every column, including any review images.",
150
+ "Open /admin/reviews and read every column, including any review images. Roughly 15-20% of the 79 seeded reviews carry one.",
129
151
  "Check each image tile renders rather than showing a placeholder icon.",
130
152
  "Read the search box's placeholder for how it matches.",
131
153
  "Type a reviewer's FULL name and read the results.",
@@ -144,16 +166,21 @@ export const authored = {
144
166
  startPage: "/admin/addresses",
145
167
  steps: [
146
168
  "Sign in as admin@letitrip.in / TempPass123!.",
147
- "Open the admin addresses listing and read every column.",
148
- "Read whether store pickup addresses and buyer delivery addresses are distinguishable.",
149
- "Use any owner-type filter and confirm it separates the two.",
150
- "Read how names and phone numbers are displayed.",
151
- "Read the sort dropdown's default and check it is one of the offered options.",
152
- "Search zzzznope and read the count.",
169
+ "Open /admin/addresses and read what the page offers before any input — an 'Owner type' select, an 'Owner ID' field and a Search button.",
170
+ "Read whether anything is listed before a search is run.",
171
+ "Leave Owner type on 'user', enter user-admin-letitrip, and click Search.",
172
+ "Read every field shown for each address returned, and record exactly how much of the name and phone number is displayed.",
173
+ "Switch Owner type to 'store', enter store-beyblade-arena, and Search.",
174
+ "Enter zzzznope as the Owner ID and Search.",
153
175
  ],
154
- inputs: { nonsenseQuery: "zzzznope" },
155
- expectedBehaviour: "Both kinds live in one collection discriminated by owner type, so the list must show which is which. Names, phones and street lines are PII-encrypted at rest — how much this list decrypts and displays is worth recording rather than assuming. A default sort that is not among the offered options opens the dropdown with nothing selected, which this listing family has done before.",
156
- expectedUiState: "Rows identify their owner type and an owner-type filter separates them. The sort default is one of the options. 'zzzznope' returns none. Record exactly how much of each name and phone is shown.",
176
+ inputs: {
177
+ userOwner: "user-admin-letitrip",
178
+ storeOwner: "store-beyblade-arena",
179
+ nonsenseOwner: "zzzznope",
180
+ },
181
+ expectedBehaviour: "Both buyer delivery and store pickup addresses live in one collection discriminated by owner type, and this page reaches them one OWNER at a time rather than as a browsable list — which is why the owner type is an input rather than a filter. Names, phones and street lines are PII-encrypted at rest, so how much this page decrypts and displays is worth recording rather than assuming.",
182
+ expectedUiState: "Before a search the page lists nothing, which is correct here rather than an empty-state bug. A searched owner returns their addresses; a nonsense owner id returns 'No addresses found for this owner.' rather than an error or the full collection. Record exactly how much of each name and phone is shown.\n\nNote what this page is NOT: there are no columns, no sort dropdown, no filter drawer and no free-text address search. A case expecting those is describing the Banned Addresses queue, which is a different route.",
183
+ expectedData: { listedBeforeSearch: 0, nonsenseOwnerResultCount: 0 },
157
184
  endResult: "Read-only; nothing persists beyond the URL.",
158
185
  },
159
186
  "checklist-admin-buyer-data-admin-store-addresses-admin-row-opens-detail": {
@@ -161,27 +188,28 @@ export const authored = {
161
188
  startPage: "/admin/addresses",
162
189
  steps: [
163
190
  "Sign in as admin@letitrip.in / TempPass123!.",
164
- "Open the admin addresses listing and click a store-owned row.",
165
- "Read what opens and whether every field is shown, including the landmark.",
166
- "Go back and click a buyer-owned row.",
167
- "Read what opens there.",
168
- "Check the row action menu offers a view rather than only mutations.",
191
+ "Open /admin/addresses, search Owner type 'store' with store-beyblade-arena, and read the card rendered for each address.",
192
+ "Record which fields the card shows — label, full name, street, city, state, postal code, phone — and specifically whether the LANDMARK appears.",
193
+ "Check whether the card is openable at all: a link, a click target or an edit affordance leading to /admin/addresses/{id}/edit.",
194
+ "Search Owner type 'user' with user-admin-letitrip and read the same for a buyer-owned address.",
195
+ "Open /admin/addresses/{one of those ids}/edit directly and compare the fields there against what the card showed.",
169
196
  ],
170
- expectedBehaviour: "A row opens to its full detail for both owner types. The landmark is the field worth naming: a form that cannot express it sends undefined and overwrites, so a detail view that never shows it hides whether the value survived.",
171
- expectedUiState: "Both kinds open to a detail showing every stored field, landmark included. A view affordance exists alongside any actions. A row offering only Delete is a dead end.",
172
- endResult: "Read-only; close without acting.",
197
+ inputs: { storeOwner: "store-beyblade-arena", userOwner: "user-admin-letitrip" },
198
+ expectedBehaviour: "An admin can read every stored field of an address, for both owner types. The landmark is the field worth naming: a form that cannot express it sends undefined and overwrites, so a surface that never shows it hides whether the value survived.",
199
+ expectedUiState: "Both owner types render cards carrying the stored fields. The editor at /admin/addresses/{id}/edit shows every field including the landmark. Record any field the editor has and the card does not — a card that omits the landmark while the editor can write it is the gap this case is for.",
200
+ endResult: "Read-only; close the editor without saving.",
173
201
  },
174
202
  "checklist-admin-buyer-data-admin-addresses-crud-admin": {
175
203
  roles: ["admin"],
176
204
  startPage: "/admin/addresses",
177
205
  steps: [
178
206
  "Sign in as admin@letitrip.in / TempPass123!.",
179
- "Open the admin addresses listing and create one: 'QA Address admin-crud', '13 Test Lane', Indore, Madhya Pradesh, 452010, landmark 'Beside the arena'.",
180
- "Save, RELOAD, and read every field including the landmark.",
181
- "Edit ONLY the street to '14 Test Lane', save, RELOAD, and read every field again.",
207
+ "Open /admin/addresses/new and create one for Owner type 'user', Owner ID user-admin-letitrip: label 'QA Address admin-crud', '13 Test Lane', Indore, Madhya Pradesh, 452010, landmark 'Beside the arena'.",
208
+ "Save, then find it again via /admin/addresses searching that owner, open /admin/addresses/{its id}/edit, and read every field including the landmark.",
209
+ "Edit ONLY the street to '14 Test Lane', save, REOPEN the editor, and read every field again.",
182
210
  "Type abcdef into the postal code field and attempt to save.",
183
- "Read where the error appears.",
184
- "Delete the address and RELOAD to confirm.",
211
+ "Read where the error appears — on the field, or as a banner, or not at all.",
212
+ "Restore the postal code, then delete the address and search that owner again to confirm it is gone.",
185
213
  ],
186
214
  inputs: {
187
215
  name: "QA Address admin-crud",
@@ -191,7 +219,7 @@ export const authored = {
191
219
  badPostcode: "abcdef",
192
220
  },
193
221
  expectedBehaviour: "Create, edit and delete persist and an edit to one field leaves the rest alone. The postal code is validated by FORMAT rather than by length: a length-only rule accepts 'abcdef' as a valid six-character Indian PIN code, and one admin route did exactly that.",
194
- expectedUiState: "After each reload every value holds, including the landmark after the street edit. 'abcdef' is rejected on the postal code field before any request is sent, not returned as a server error with nothing marked.",
222
+ expectedUiState: "After each reopen every value holds, including the landmark after the street edit — a landmark that vanishes when only the street changed is the overwrite this case is for. 'abcdef' is rejected ON THE POSTAL CODE FIELD before any request is sent, not returned as a server error with nothing marked. This half is expected to PASS: the form validates per country by schema now, where it once checked only that the value was six characters long — which 'abcdef' satisfies.",
195
223
  endResult: "The address is deleted by the final step.",
196
224
  },
197
225
  "checklist-admin-buyer-data-admin-stores-admin-view": {
@@ -200,15 +228,20 @@ export const authored = {
200
228
  steps: [
201
229
  "Sign in as admin@letitrip.in / TempPass123!.",
202
230
  "Open /admin/stores and read every column and status chip.",
203
- "Select each status chip in turn and read the rows returned.",
204
- "Note whether the pending and suspended chips return anything.",
205
- "Open a verified store's editor, change ONE unrelated field such as its description, and save.",
206
- "RELOAD and read whether it is still verified, still featured, and whether its capabilities are unchanged.",
231
+ "Select each status chip in turn and read the rows returned — the seed carries store-blader-bazaar as pending and store-vintage-vault-co as suspended, so neither chip should be empty.",
232
+ "Open store-letitrip-official — verified and featured — for editing and write down its verified flag, its featured flag, its capabilities and its admin notes.",
233
+ "Change ONLY its description and save.",
234
+ "RELOAD and read all four of those back.",
207
235
  "Restore the description.",
208
236
  ],
237
+ inputs: {
238
+ store: "store-letitrip-official",
239
+ pendingStore: "store-blader-bazaar",
240
+ suspendedStore: "store-vintage-vault-co",
241
+ },
209
242
  expectedBehaviour: "🛑 This is the highest-severity case on the page. The list serializer once omitted the verified, featured, capabilities and notes fields, so the editor seeded them as undefined and the save handler sent those defaults back unconditionally — saving ANY unrelated field silently un-verified the store and reset its capabilities. The seed carries a pending and a suspended store precisely so those chips are not permanently empty.",
210
- expectedUiState: "Every status chip returns rows or is genuinely empty. After the description save and reload the store is STILL verified, still featured, and its capabilities are unchanged. A store that quietly lost its verified badge is the failure, and the save reported success.",
211
- expectedData: { verifiedPreserved: true },
243
+ expectedUiState: "Every status chip returns rows; the pending and suspended chips have a seeded store each. After the description save and reload the store is STILL verified, still featured, its capabilities are unchanged and its admin notes are intact. A store that quietly lost its verified badge is the failure, and the save reported success. This is expected to PASS — the list serializer now carries all five of those fields — but it is the case worth re-running after any change to that route, because the failure is silent and destructive.",
244
+ expectedData: { verifiedPreserved: true, featuredPreserved: true, capabilitiesPreserved: true },
212
245
  endResult: "The description is restored and the store's flags are as they were. Re-read every flag after the reload, not just the one that was edited.",
213
246
  },
214
247
  "checklist-admin-buyer-data-admin-admin-store-detail-page": {
@@ -216,14 +249,14 @@ export const authored = {
216
249
  startPage: "/admin/stores",
217
250
  steps: [
218
251
  "Sign in as admin@letitrip.in / TempPass123!.",
219
- "Open /admin/stores and open a store's detail panel from the list.",
220
- "Write down every field label it shows.",
221
- "Note the store id and close the panel.",
222
- "Open that store's standalone admin detail page directly by URL.",
223
- "Write down every field label and compare the two lists.",
224
- "Reload the standalone page and confirm it loads the same store.",
225
- "Read whether the store's WhatsApp access token appears anywhere on either surface.",
252
+ "Open /admin/stores and open store-beyblade-arena's detail panel from the list.",
253
+ "Write down every field label it shows, then close the panel.",
254
+ "Open /admin/stores/store-beyblade-arena/view directly.",
255
+ "Write down every field label there and compare the two lists.",
256
+ "Reload that page and confirm it loads the same store rather than 404ing or resetting.",
257
+ "Search both surfaces, and the page source of the standalone one, for a WhatsApp access token.",
226
258
  ],
259
+ inputs: { store: "store-beyblade-arena", standalonePage: "/admin/stores/{id}/view" },
227
260
  expectedBehaviour: "The panel and the page render from one shared content component, so their fields match by construction. The store document's WhatsApp token is decrypted on every read, so any surface that does not deliberately project it is handling plaintext — an admin page is allowed to show more than a public one, but not the raw credential.",
228
261
  expectedUiState: "Both surfaces show the same fields and the standalone page survives a reload. The access token is masked or absent on both. A field in one and not the other means a second copy of the content component has appeared.",
229
262
  expectedData: { rawTokenShown: 0 },