@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
@@ -18,6 +18,7 @@
18
18
  * @tag sideEffects:none
19
19
  */
20
20
  import { assignDefaultPhases } from "../utils/phases";
21
+ import { happyPathPages } from "./_happy-path";
21
22
  import { moneyFlowsPages } from "./_money-flows";
22
23
  import { AUTHORED_CASES } from "./authored";
23
24
  const ADMIN_TESTING_GROUP_LABEL = "Admin (Testing)";
@@ -100,6 +101,18 @@ function group(groupKey, groupLabel, pages, opts) {
100
101
  * group and is not globally unique across groups.
101
102
  */
102
103
  const rawTesterChecklistItems = [
104
+ /*
105
+ * ๐Ÿ›‘ FIRST ON PURPOSE. The happy path is the core commerce flow โ€” browse a
106
+ * category, reach a brand and a store, open a standard product, buy it, pay
107
+ * for it, watch the seller fulfil and the admin verify.
108
+ *
109
+ * It leads the catalogue because a run that is interrupted, rate-limited or
110
+ * abandoned half way should have covered "can anyone buy anything" before it
111
+ * covered SEO metadata. Ordering is enforced by HAPPY_PATH_FIRST in
112
+ * ../utils/phases, not by this position โ€” array position alone would be
113
+ * silently undone by the round-robin phase assignment below.
114
+ */
115
+ ...group("happy-path", "Happy Path (core flows)", happyPathPages),
103
116
  ...group("account-auth", "Account & Auth", [
104
117
  {
105
118
  pageKey: "signup-login",
@@ -313,9 +326,21 @@ const rawTesterChecklistItems = [
313
326
  href: "/products",
314
327
  },
315
328
  {
329
+ /*
330
+ * ๐Ÿ›‘ The KEY still says "toggle" and must stay that way: it is the
331
+ * document id (`${testerId}__${checklistItemId}`), so renaming it
332
+ * orphans every recorded response. The LABEL and DESCRIPTION are not
333
+ * part of the id and were still describing the removed control.
334
+ *
335
+ * The showSold/showEnded/showClosed pills were replaced by the
336
+ * three-tab availability bar, and the three fixtures named here
337
+ * ("Test Collectible โ€” Sold Out" et al) went with the tester-sandbox
338
+ * catalogue. A tester reads the label FIRST, so a stale one sends them
339
+ * hunting before the steps ever get a chance to correct them.
340
+ */
316
341
  key: "show-sold-toggle-reveals-items",
317
- label: "The \"Show sold\" / \"Show ended\" / \"Show closed\" toggle on Products/Auctions/Prize Draws listing pages is off by default (hiding sold-out/ended/closed items) and reveals them when switched on",
318
- description: "Verify against the seeded fixtures: \"Test Collectible โ€” Sold Out\" (standard, hidden until \"Show sold\" is on), \"Test Auction โ€” Already Won\" (hidden until \"Show ended\" is on), \"Test Prize Draw โ€” Already Closed\" (hidden until \"Show closed\" is on). All three should be genuinely absent by default, not just from an unrelated broken query.",
342
+ label: "The availability tab bar hides sold-out, ended and closed items by default, and the middle tab reveals them",
343
+ description: "Verify against the seeded fixtures: \"Beyblade X BX-34 Dran Buster (Sold Out)\" on /products, \"Beyblade Burst B-128 Spriggan Requiem (Ended โ€” Bought Out)\" on /auctions, and \"Metal Fight Mystery Draw โ€” Ended (Revealed)\" on /prize-draws. Each must be genuinely absent on 'Available' and present on the middle tab โ€” not merely missing because an unrelated query broke. Only the middle label varies by type: 'Sold & Ended' on /products, 'Ended' on both /auctions and /prize-draws.",
319
344
  href: "/products",
320
345
  },
321
346
  {
@@ -621,12 +646,23 @@ const rawTesterChecklistItems = [
621
646
  label: "Prize-draw collage tiles stack the image ABOVE the title/value caption at full card width โ€” the image is never squashed beside the text at half width",
622
647
  href: "/prize-draws/prizedraw-beyblade-mystery-box",
623
648
  },
624
- {
625
- key: "concern-card-icon-above-label",
626
- label: "Category concern cards show their icon ABOVE the label, not beside it",
627
- description: "This is a deliberate visual change: the card always asked for a vertical layout but had been rendering side-by-side. Confirm the stacked layout looks right rather than assuming the old side-by-side was intended.",
628
- href: "/categories",
629
- },
649
+ /*
650
+ * REMOVED 2026-09-28 โ€” concern-card-icon-above-label.
651
+ *
652
+ * `ConcernCard` is rendered only by `ConcernGrid`, and `ConcernGrid` has
653
+ * NO consumer anywhere in src/ or appkit/src/. Both are exported from
654
+ * appkit's public index, which is why they look alive from a grep of the
655
+ * barrel. Nothing puts them on a page.
656
+ *
657
+ * The case's href said /categories and its overlay startPage said /, and
658
+ * the component renders on neither โ€” so it could only ever be answered
659
+ * "could not test", forever, on every run. A permanently un-passable case
660
+ * is worse than no case: it occupies a batch slot, and a `null` verdict
661
+ * reads as a coverage gap rather than as a catalogue defect.
662
+ *
663
+ * The dead component itself is recorded in docs/TEST-RUN-3-OUTOFSCOPE.md.
664
+ * Restore this case if ConcernGrid is ever mounted.
665
+ */
630
666
  {
631
667
  key: "media-picker-existing-grid",
632
668
  label: "In any media field, \"Choose existing\" shows each file as a square thumbnail with the filename underneath โ€” not thumbnail-beside-filename with a shrunken image",
@@ -2625,6 +2661,12 @@ const rawTesterChecklistItems = [
2625
2661
  description: "Proves the category actually persisted to Firestore rather than only existing in the form's local state.",
2626
2662
  href: "/store/products/new",
2627
2663
  },
2664
+ {
2665
+ key: "deep-category-chain-derived",
2666
+ label: "A product filed against a DEEP category is written with its full ancestor chain, so it appears on every ancestor's page and not just the leaf's",
2667
+ description: "BEFORE: the chain was maintained by hand in the seed, so a form-created listing got a one-element array and was invisible on every ancestor page. AFTER (2026-09-14): ProductRepository.deriveTaxonomy() derives it on create and update. Latent by construction โ€” every seeded product already has the chain, so only a listing created through the UI can catch a regression.",
2668
+ href: "/store/products/new",
2669
+ },
2628
2670
  {
2629
2671
  key: "seller-category-inline-create-duplicate-rejected",
2630
2672
  label: "Creating a category with a name that already exists shows a readable \"already exists\" error, not a generic failure or a silent no-op",
@@ -3004,6 +3046,18 @@ const rawTesterChecklistItems = [
3004
3046
  pageLabel: "Event detail โ€” participate, leaderboard, winner, spin results",
3005
3047
  href: "/events",
3006
3048
  cases: [
3049
+ {
3050
+ key: "spin-results-subroute",
3051
+ label: "The public 'Last 10 Spin Results' feed masks every participant's display name",
3052
+ description: "Re-scoped 2026-09-29. This case used to assert the route listed 'this account's own spins'; it does not and never did โ€” getSpinResultsCached takes only the event id and returns the ten most recent spins across ALL users, so a buyer who had never spun was shown three strangers' prizes with no empty state. The page now has two sections and this case covers the PUBLIC one. What it checks is the masking: a public event page was publishing real winners' full display names beside the prize each won, which is the same shape as the bidder-name leak on public bid history (Root Cause #50). The account-scoped claim moved to my-spins-scoped-to-viewer.",
3053
+ href: "/events/event-daily-beyblade-pull-wheel/spin-results",
3054
+ },
3055
+ {
3056
+ key: "my-spins-scoped-to-viewer",
3057
+ label: "'Your Spins' shows only the signed-in caller's own spins, with an empty state about YOU",
3058
+ description: "The account-scoped half of the old spin-results case, now a section of its own above the public feed. The bug it guards against is specific: when the viewer-scoped block did not exist, someone who had never spun saw the public feed's rows and had no way to tell that none of them were theirs. So the empty state must name the viewer ('You have not spun on this event yet.') rather than the event ('No spins yet'), which would be false while the feed below is full. Signed out it must prompt to sign in, never show anyone else's results.",
3059
+ href: "/events/event-daily-beyblade-pull-wheel/spin-results",
3060
+ },
3007
3061
  { key: "detail-tabs-match-event-type", label: "An event's tabs match its TYPE โ€” a poll shows no spin results, a sale shows no leaderboard" },
3008
3062
  { key: "participate-records-an-entry", label: "Participating records an entry that is still there after a reload" },
3009
3063
  { key: "participate-twice-is-refused", label: "Participating a second time is refused with a reason, not silently duplicated" },
@@ -3058,6 +3112,12 @@ const rawTesterChecklistItems = [
3058
3112
  pageLabel: "Blog",
3059
3113
  href: "/blog",
3060
3114
  cases: [
3115
+ {
3116
+ key: "blog-search-filters-the-list",
3117
+ label: "The blog listing's search box actually filters, and the filtered result survives a reload",
3118
+ description: "BEFORE: BlogIndexPageView never read q while /api/blog did, and listPublished had no search parameter at all -- so the SSR first paint was the unfiltered list, and a public listing hook sets staleTime Infinity once given SSR data, freezing it. AFTER: listPublished takes the same push-down. Needs a nonsense control: a real term returns plausible cards either way.",
3119
+ href: "/blog",
3120
+ },
3061
3121
  { key: "read-post", label: "Reading a blog post renders correctly (images, formatting)" },
3062
3122
  { key: "blog-listing", label: "Blog listing page shows all published posts" },
3063
3123
  { key: "blog-cover-image-display", label: "A blog post's cover image displays on both its listing card and its detail page" },
@@ -3218,6 +3278,12 @@ const rawTesterChecklistItems = [
3218
3278
  pageLabel: "FAQ & Help",
3219
3279
  href: "/faqs",
3220
3280
  cases: [
3281
+ {
3282
+ key: "faq-search-filters-and-survives-reload",
3283
+ label: "The FAQ search box filters the questions, the term round-trips through ?q=, and a nonsense term shows an empty state rather than the full list",
3284
+ description: "No case covered /faqs search, on the one page whose documented near-miss WAS search appearing empty -- it renders zero questions at 2.5s and 62 at 6s, so a count taken early is confidently wrong. Needs both a nonsense control and an explicit wait.",
3285
+ href: "/faqs",
3286
+ },
3221
3287
  { key: "faq-bottom-borders", label: "FAQ question rows show a clear bottom-border divider on the homepage and the FAQs page", href: "/faqs" },
3222
3288
  { key: "faq-mobile-count", label: "Homepage FAQ section shows a good number of questions on mobile, not just 1-2", href: "/" },
3223
3289
  { key: "tabs-mobile-dropdown", label: "Tabs on category/brand/product/event detail pages collapse into a colored dropdown on mobile once there are more than 5 tabs" },
@@ -3507,11 +3573,11 @@ const rawTesterChecklistItems = [
3507
3573
  roles: ["guest", "admin"],
3508
3574
  startPage: "/contact",
3509
3575
  steps: [
3510
- "Open the public contact page as a signed-out visitor.",
3576
+ "Open the public contact page as a signed-out visitor. ๐Ÿ›‘ THIS CASE LISTS roles [guest, admin] AND THE HARNESS RESOLVES THAT TO ADMIN โ€” admin wins whenever present โ€” so the batch opens SIGNED IN. Copy tester/.tester-runs/session-guest.json (an empty storage state, by design) over session.json and CLOSE THE BROWSER first; the MCP reads it only at context creation.",
3511
3577
  "Fill in name, email, subject and a message of at least 10 characters.",
3512
3578
  "Submit the form.",
3513
- "Log in as admin and open /admin/contact.",
3514
- "Check the inbox of whatever address used to receive support mail.",
3579
+ "Swap to tester/.tester-runs/session-admin.json, closing the browser again, then open /admin/contact.",
3580
+ "Prove NO email was sent: node tester/scripts/check-inbox.mjs --since <a timestamp captured before the submission> --subject \"contact\" --expect-none. There is no support address to open โ€” EMAIL_SUPPORT was never configured in any runtime, which is half of why this path was removed โ€” so 'check the inbox' had nowhere to go. --expect-none waits the full window and succeeds only on silence; without it, nothing-arrived is unprovable.",
3515
3581
  ],
3516
3582
  inputs: {
3517
3583
  name: "Tester Probe",
@@ -3539,6 +3605,8 @@ const rawTesterChecklistItems = [
3539
3605
  expectedUiState: "The form shows a failure message, not the success confirmation.",
3540
3606
  endResult: "Nothing appears in /admin/contact for that attempt, and the visitor knows to try again.",
3541
3607
  href: "/admin/contact",
3608
+ requiresHumanChannel: true,
3609
+ humanChannelReason: "Needs the contactSubmissions write forced to fail โ€” a Firestore rules change or a deploy-time fault injection. A browser cannot cause it, so a tester answers null and says so rather than spending turns trying. Kept in the catalogue because it guards the sharpest edge in the change: with the email gone, that write is the ONLY record a customer wrote in, and a save that fails while the form still says thank you loses the message silently.",
3542
3610
  },
3543
3611
  {
3544
3612
  key: "contact-digest-mailto-reply",
@@ -3548,7 +3616,8 @@ const rawTesterChecklistItems = [
3548
3616
  startPage: "/admin/contact",
3549
3617
  steps: [
3550
3618
  "Submit a contact message from the public form.",
3551
- "As admin, trigger the daily digest.",
3619
+ // audit-hardcoded-api-routes-ok: no button exists; the tester pastes this URL into a console by hand.
3620
+ "As admin, trigger the daily digest: run fetch('/api/admin/daily-digest/trigger',{method:'POST'}) from the browser console. There is no button for it โ€” Site Settings only carries a toggle to enable the schedule โ€” and this is the same instruction the other digest case uses.",
3552
3621
  "Open the digest and find the message under Contact messages.",
3553
3622
  "Click its Reply link.",
3554
3623
  ],
@@ -4902,6 +4971,12 @@ const rawTesterChecklistItems = [
4902
4971
  pageLabel: "Prize Draws / Lotteries",
4903
4972
  href: "/admin/prize-draws",
4904
4973
  cases: [
4974
+ {
4975
+ key: "lottery-edit-preserves-bookings",
4976
+ label: "Editing a lottery's prizes leaves its bookings intact -- booked slots stay booked, against the same buyers",
4977
+ description: "BEFORE: the editor sent isBooked:false and weight:0 for every slot and dropped the buyer fields, and the event PATCH was passthrough -- so the first save of a live lottery freed every purchased slot and erased who bought them, with a 200 and no error. AFTER: the write shape cannot express booking state at all, and the merge reattaches by slotNumber rather than array index. Uses the seeded 25-slot lottery, five of them booked.",
4978
+ href: "/admin/lotteries",
4979
+ },
4905
4980
  { key: "prizedraw-create", label: "Admin can create a prize-draw listing, choosing instant or scheduled reveal mode and a 1โ€“15 day duration", href: "/admin/prize-draws" },
4906
4981
  { key: "prizedraw-reveal-winner", label: "Winners are assigned automatically via crypto.randomInt (on payment confirmation for instant mode, or at expiry/sellout for scheduled mode) โ€” never by a manual admin click" },
4907
4982
  { key: "prizedraw-scam-guard", label: "A prize draw with active entries cannot be unpublished, archived, or deleted, and an already-won item's details cannot be edited" },
@@ -4928,11 +5003,25 @@ const rawTesterChecklistItems = [
4928
5003
  {
4929
5004
  pageKey: "classifieds-digitalcodes-live",
4930
5005
  pageLabel: "Classifieds, Digital Codes & Live Listings",
4931
- href: "/admin/classified",
5006
+ /*
5007
+ * ๐Ÿ›‘ /admin/products, not /admin/classified. The three per-type admin
5008
+ * pages exist and are real, but they are browse-only wrappers over the
5009
+ * shared listing-type config โ€” no row actions, no Approve/Reject, no
5010
+ * Quick edit. Moderation lives on /admin/products alone, so an href
5011
+ * pointing at the per-type page sends the tester to a surface where
5012
+ * the thing being tested cannot be done.
5013
+ */
5014
+ href: "/admin/products",
4932
5015
  cases: [
4933
- { key: "classified-create-moderate", label: "Admin can create and moderate classified listings", href: "/admin/classified" },
4934
- { key: "digitalcode-create-moderate", label: "Admin can create and moderate digital-code listings", href: "/admin/digital-codes" },
4935
- { key: "live-create-moderate", label: "Admin can create and moderate live-item listings", href: "/admin/live" },
5016
+ { key: "classified-create-moderate", label: "Admin can create and moderate classified listings", href: "/admin/products" },
5017
+ { key: "digitalcode-create-moderate", label: "Admin can create and moderate digital-code listings", href: "/admin/products" },
5018
+ { key: "live-create-moderate", label: "Admin can create and moderate live-item listings", href: "/admin/products" },
5019
+ {
5020
+ key: "reject-status-has-no-chip",
5021
+ label: "After the Reject row action, the listing is still findable by an admin โ€” some status chip returns it",
5022
+ description: "BEFORE: Reject writes { status: \"rejected\" }, and ProductStatus is draft|published|in_review|archived โ€” no such value. The PATCH schema types status as a bare z.string(), so it persists unvalidated, and the row then matches none of the four status chips: an admin can reach it only under All. AFTER: a rejected listing is isolatable by a status filter. The public half already works โ€” every public query filters on published, so the listing does disappear, which is why this went unnoticed.",
5023
+ href: "/admin/products",
5024
+ },
4936
5025
  ],
4937
5026
  },
4938
5027
  {
@@ -5585,7 +5674,7 @@ const rawTesterChecklistItems = [
5585
5674
  {
5586
5675
  key: "product-created-with-deep-category-visible-at-root",
5587
5676
  label: "A product created through the form against a deep category appears on that category's ROOT page too",
5588
- description: "๐Ÿ›‘ KNOWN GAP, not a regression: nothing on the write side appends ancestors today, so a listing filed at tier 3 gets a one-element chain and is invisible on every ancestor page. Record what actually happens rather than assuming either answer.",
5677
+ description: "BEFORE: nothing on the write side appended ancestors, so a listing filed deep got a one-element chain and was invisible on every ancestor page. AFTER (2026-09-14): ProductRepository.deriveTaxonomy() derives the chain on create and update, so all three pages list it. Latent by construction โ€” every seeded product already carries a correct chain, so only a product created through a form can catch a regression.",
5589
5678
  href: "/admin/products",
5590
5679
  },
5591
5680
  {
@@ -5602,13 +5691,14 @@ const rawTesterChecklistItems = [
5602
5691
  },
5603
5692
  {
5604
5693
  key: "category-type-not-guessable",
5605
- label: "The category editor makes the row's kind explicit โ€” a normal listing category, a brand, a sub-listing group or a bundle โ€” rather than leaving it to be guessed",
5694
+ label: "A row's kind is set by WHICH admin surface authored it, and the surfaces stay disjoint โ€” a brand never appears in the category tree, and vice versa",
5695
+ description: "Brands, bundles, sub-listing groups and ordinary categories share one collection, discriminated by categoryType. The admin category form has no kind dropdown and correctly should not: /admin/brands filters and writes categoryType==brand while /admin/categories writes an ordinary category. This tests that separation rather than the absent control. (Key kept โ€” renaming orphans responses.)",
5606
5696
  href: "/admin/categories",
5607
5697
  },
5608
5698
  {
5609
5699
  key: "delete-category-with-children-refused",
5610
- label: "Deleting a category that still has children or products is refused with a reason naming what depends on it",
5611
- description: "A delete that succeeds and orphans its subtree is unrecoverable. Refusing it, with the count, is the correct outcome.",
5700
+ label: "Deleting a category CASCADES to its parent โ€” its children re-parent to the grandparent and its products are re-filed there with rebuilt ancestor chains",
5701
+ description: "BEFORE: a flat 409 refusal whenever the category had subcategories. AFTER: the delete cascades. Products are re-filed through the repository, so each one's chain is rebuilt from its new leaf; the order is load-bearing because a product must move while the document still exists. The failure to watch for is a product left pointing at the deleted row. (Key kept despite the stale 'refused' wording โ€” renaming it would orphan every response recorded against it.)",
5612
5702
  href: "/admin/categories",
5613
5703
  },
5614
5704
  ],
@@ -5654,7 +5744,13 @@ const rawTesterChecklistItems = [
5654
5744
  pageLabel: "Bans, Unbans & Trust",
5655
5745
  href: "/admin/users",
5656
5746
  cases: [
5657
- { key: "soft-ban-blocks-signin", label: "Soft-banning a user prevents them signing in and tells them why", href: "/admin/users" },
5747
+ {
5748
+ key: "soft-ban-blocks-signin",
5749
+ /* Key kept โ€” renaming it orphans every verdict recorded against it. */
5750
+ label: "A soft ban blocks the ONE action it names and leaves sign-in working; a hard ban refuses sign-in. Both tell the person why",
5751
+ description: "The label used to read \"Soft-banning a user prevents them signing in\", which is false: a soft ban appends to user.softBans[] as a per-action restriction (place_bids, write_reviews, โ€ฆ) and /api/auth/login refuses only on the Auth record being disabled, which is what a HARD ban sets. A tester following the old label would have reported working behaviour as a defect.",
5752
+ href: "/admin/users",
5753
+ },
5658
5754
  { key: "soft-ban-requires-reason", label: "A ban cannot be applied without a reason, and that reason reaches the banned user", href: "/admin/users" },
5659
5755
  {
5660
5756
  key: "unban-restores-access",
@@ -5971,6 +6067,12 @@ const rawTesterChecklistItems = [
5971
6067
  label: "The buyer's own bid page does NOT reveal other bidders",
5972
6068
  description: "Open the same bid at /admin/bids/{id}/view and at /user/bids/{id}/view. The buyer view must not show the other bidder's identity. One `viewer` argument decides this, in one place โ€” if the buyer view leaks a name, that argument is being ignored somewhere.",
5973
6069
  },
6070
+ {
6071
+ key: "detail-page-seller-sees-bidder",
6072
+ label: "A seller opening a bid on their own auction sees who placed it, and the modal and the full page agree",
6073
+ description: "buildBidDetailFields(bid, viewer) has four callers and the seller leg had no case. Its only viewer-dependent row is Bidder, added when the viewer is not the buyer, so this is the other side of the identity case: the buyer must NOT see a competitor, and the seller MUST see who is bidding on their own auction.",
6074
+ href: "/store/bids",
6075
+ },
5974
6076
  ],
5975
6077
  },
5976
6078
  {
@@ -22,6 +22,23 @@ export interface TesterPhaseAssignable {
22
22
  /** Target item count per phase โ€” real phase sizes vary a little around this
23
23
  * since a page's items are never split across two phases. */
24
24
  export declare const DEFAULT_TESTER_PHASE_SIZE = 25;
25
+ /**
26
+ * Groups that take the earliest phases, ahead of the round-robin.
27
+ *
28
+ * ๐Ÿ›‘ THE ROUND-ROBIN BELOW IS RIGHT FOR EVERYTHING ELSE AND WRONG FOR THIS.
29
+ * Interleaving exists so an early stop costs a proportional slice of every
30
+ * group rather than the whole tail โ€” a fair outcome when no group matters more
31
+ * than another. But `happy-path` is not one group among fourteen: it is the
32
+ * question "can anyone buy anything on this site", and a run that covered 7% of
33
+ * it alongside 7% of SEO metadata has answered nothing.
34
+ *
35
+ * So these groups are packed FIRST, in their own catalogue order, and the
36
+ * round-robin then runs over the remainder exactly as before.
37
+ *
38
+ * Adding a group here is a real cost: every group after it loses its early
39
+ * proportional slice. Two entries is the intended size of this list.
40
+ */
41
+ export declare const PRIORITY_GROUPS: readonly string[];
25
42
  /**
26
43
  * Returns a same-length array of 1-based phase numbers, one per input item,
27
44
  * computed by greedily packing whole pages (consecutive runs of the same
@@ -18,6 +18,23 @@
18
18
  /** Target item count per phase โ€” real phase sizes vary a little around this
19
19
  * since a page's items are never split across two phases. */
20
20
  export const DEFAULT_TESTER_PHASE_SIZE = 25;
21
+ /**
22
+ * Groups that take the earliest phases, ahead of the round-robin.
23
+ *
24
+ * ๐Ÿ›‘ THE ROUND-ROBIN BELOW IS RIGHT FOR EVERYTHING ELSE AND WRONG FOR THIS.
25
+ * Interleaving exists so an early stop costs a proportional slice of every
26
+ * group rather than the whole tail โ€” a fair outcome when no group matters more
27
+ * than another. But `happy-path` is not one group among fourteen: it is the
28
+ * question "can anyone buy anything on this site", and a run that covered 7% of
29
+ * it alongside 7% of SEO metadata has answered nothing.
30
+ *
31
+ * So these groups are packed FIRST, in their own catalogue order, and the
32
+ * round-robin then runs over the remainder exactly as before.
33
+ *
34
+ * Adding a group here is a real cost: every group after it loses its early
35
+ * proportional slice. Two entries is the intended size of this list.
36
+ */
37
+ export const PRIORITY_GROUPS = ["happy-path"];
21
38
  /**
22
39
  * Returns a same-length array of 1-based phase numbers, one per input item,
23
40
  * computed by greedily packing whole pages (consecutive runs of the same
@@ -73,7 +90,18 @@ export function assignDefaultPhases(itemsInCatalogOrder, targetPhaseSize = DEFAU
73
90
  byGroup.set(g, []);
74
91
  byGroup.get(g).push(pageId);
75
92
  }
93
+ /*
94
+ * Priority groups first, whole, in catalogue order โ€” then the round-robin over
95
+ * everything else. See PRIORITY_GROUPS for why this one group is exempt from
96
+ * the interleaving the rest depends on.
97
+ */
98
+ const interleaved = [];
99
+ for (const g of PRIORITY_GROUPS) {
100
+ for (const pageId of byGroup.get(g) ?? [])
101
+ interleaved.push(pageId);
102
+ }
76
103
  const groupsLargestFirst = [...byGroup.entries()]
104
+ .filter(([g]) => !PRIORITY_GROUPS.includes(g))
77
105
  .map(([g, pages]) => ({
78
106
  g,
79
107
  pages,
@@ -82,8 +110,7 @@ export function assignDefaultPhases(itemsInCatalogOrder, targetPhaseSize = DEFAU
82
110
  .sort((a, b) => b.cases - a.cases || a.g.localeCompare(b.g))
83
111
  .map((e) => e.g);
84
112
  const cursor = new Map(groupsLargestFirst.map((g) => [g, 0]));
85
- const interleaved = [];
86
- let remaining = pageOrder.length;
113
+ let remaining = pageOrder.length - interleaved.length;
87
114
  while (remaining > 0) {
88
115
  for (const g of groupsLargestFirst) {
89
116
  const i = cursor.get(g);
package/dist/index.d.ts CHANGED
@@ -1766,6 +1766,7 @@ export { eventIdGET } from "./features/events/server";
1766
1766
  export { getEventLeaderboard } from "./features/events/server";
1767
1767
  export { getEventPollResults } from "./features/events/server";
1768
1768
  export { getEventSpinResults } from "./features/events/server";
1769
+ export { getUserSpinResultsForEvent } from "./features/events/server";
1769
1770
  export { getBugHunterLeaderboard } from "./features/tester/server";
1770
1771
  export { getPublicEventById } from "./features/events/server";
1771
1772
  export { listPublicEvents } from "./features/events/server";
package/dist/index.js CHANGED
@@ -3339,6 +3339,8 @@ export { getEventPollResults } from "./features/events/server";
3339
3339
  // [SERVER-ONLY]-Server-only รขโ‚ฌ" uses Node.js, Next.js server internals, or third-party server SDKs (auth, email, payment, shipping).
3340
3340
  // getEventSpinResults - Last N winning spins for a spin_wheel event, most recent first.
3341
3341
  export { getEventSpinResults } from "./features/events/server";
3342
+ // getUserSpinResultsForEvent - The CALLER'S OWN spins for an event, unmasked. Companion to the masked public feed above.
3343
+ export { getUserSpinResultsForEvent } from "./features/events/server";
3342
3344
  // [SERVER-ONLY]-Server-only รขโ‚ฌ" uses Node.js, Next.js server internals, or third-party server SDKs (auth, email, payment, shipping).
3343
3345
  // getBugHunterLeaderboard - Ranks testers by confirmed-bug count.
3344
3346
  export { getBugHunterLeaderboard } from "./features/tester/server";
@@ -36,6 +36,29 @@ export interface ProductJsonLdInput {
36
36
  auctionEndDate?: Date;
37
37
  /** Canonical discriminator (SB1-G Phase 4). */
38
38
  listingType?: ListingType;
39
+ /**
40
+ * The path this listing actually lives at, e.g. `/auctions/{slug}`.
41
+ *
42
+ * ๐Ÿ›‘ Both builders hardcoded `/products/${slug}`, so an auction's own
43
+ * structured data advertised its offer โ€” and its `url` โ€” at a path that
44
+ * belongs to a different listing type. Measured live: the block on
45
+ * `/auctions/auction-beyblade-metal-lightning-l-drago` carried
46
+ * `offers.url: ".../products/auction-beyblade-metal-lightning-l-drago"`,
47
+ * and that URL served a full standard product page with Buy Now and Add to
48
+ * Cart and no bid controls at all. So the one consumer that reads this field
49
+ * โ€” a crawler โ€” was sent to the wrong page, and told it was canonical there.
50
+ *
51
+ * Six of the nine listing types have their own detail route (Root Cause
52
+ * #48), so `/products/{slug}` is a wrong default for two thirds of them.
53
+ *
54
+ * Defaults to `/products/${slug}` when omitted, which is correct for
55
+ * `standard`, `art` and `stickers` โ€” the three that genuinely live there โ€”
56
+ * and keeps every existing caller behaving exactly as before. The caller
57
+ * knows its own route; resolving it here would mean importing the listing-type
58
+ * registry into `seo/`, which must stay reachable from server-only metadata
59
+ * paths without widening its import graph (Root Cause #76).
60
+ */
61
+ detailPath?: string;
39
62
  /**
40
63
  * Denormalised rating, straight off `ProductDocument.avgRating` /
41
64
  * `.reviewCount` โ€” so emitting it costs **zero** extra Firestore reads.
@@ -85,7 +85,7 @@ export function gatedPriceWebPageJsonLd(path) {
85
85
  };
86
86
  }
87
87
  export function productJsonLd(product) {
88
- const url = `${SITE_URL}/products/${product.slug}`;
88
+ const url = `${SITE_URL}${product.detailPath ?? `/products/${product.slug}`}`;
89
89
  // Absolutise, then de-duplicate: `mainImage` is very often also `images[0]`,
90
90
  // which produced the same URL twice in the emitted array.
91
91
  const images = [
@@ -246,7 +246,8 @@ export function searchBoxJsonLd(opts) {
246
246
  };
247
247
  }
248
248
  export function auctionJsonLd(auction) {
249
- const url = `${SITE_URL}/products/${auction.slug}`;
249
+ // An auction lives at /auctions/{slug} โ€” see `detailPath`'s note.
250
+ const url = `${SITE_URL}${auction.detailPath ?? `/auctions/${auction.slug}`}`;
250
251
  const base = productJsonLd(auction);
251
252
  return {
252
253
  ...base,
package/dist/server.d.ts CHANGED
@@ -266,6 +266,7 @@ export { getEventLeaderboard } from "./features/events/server";
266
266
  export { getEventPollResults } from "./features/events/server";
267
267
  export type { PollResultEntry, SpinResultEntry } from "./features/events/index";
268
268
  export { getEventSpinResults } from "./features/events/server";
269
+ export { getUserSpinResultsForEvent } from "./features/events/server";
269
270
  export { getBugHunterLeaderboard } from "./features/tester/server";
270
271
  export { getPublicEventById } from "./features/events/server";
271
272
  export { listPublicEvents } from "./features/events/server";
package/dist/server.js CHANGED
@@ -777,6 +777,8 @@ export { getEventPollResults } from "./features/events/server";
777
777
  // [SERVER-ONLY]-Server-only โ€” uses Node.js, Next.js server internals, or third-party server SDKs (auth, email, payment, shipping).
778
778
  // getEventSpinResults - Last N winning spins for a spin_wheel event, most recent first.
779
779
  export { getEventSpinResults } from "./features/events/server";
780
+ // getUserSpinResultsForEvent - The CALLER'S OWN spins for an event, unmasked. Companion to the masked public feed above.
781
+ export { getUserSpinResultsForEvent } from "./features/events/server";
780
782
  // [SERVER-ONLY]-Server-only โ€” uses Node.js, Next.js server internals, or third-party server SDKs (auth, email, payment, shipping).
781
783
  // getBugHunterLeaderboard - Ranks testers by confirmed-bug count.
782
784
  export { getBugHunterLeaderboard } from "./features/tester/server";