@mohasinac/appkit 4.53.2 → 4.54.0-next.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (179) hide show
  1. package/dist/.build-stamp.json +5 -5
  2. package/dist/_internal/server/features/auth/capabilities.js +7 -5
  3. package/dist/_internal/server/features/checkout/supplier-identity.js +28 -6
  4. package/dist/_internal/server/features/products/listing-tab-counts.js +81 -25
  5. package/dist/_internal/server/features/search/index-build/sources.js +43 -2
  6. package/dist/_internal/server/jobs/core/jobRunners.js +0 -10
  7. package/dist/_internal/server/jobs/core/onOrderCreate.js +10 -104
  8. package/dist/_internal/shared/actions/action-registry.d.ts +1 -1
  9. package/dist/_internal/shared/actions/action-registry.js +128 -0
  10. package/dist/_internal/shared/features/categories/bundle-copy.d.ts +1 -1
  11. package/dist/_internal/shared/features/site-settings/supplier-of-record.d.ts +56 -0
  12. package/dist/_internal/shared/features/site-settings/supplier-of-record.js +57 -0
  13. package/dist/_internal/shared/listing-types/tiers.d.ts +94 -0
  14. package/dist/_internal/shared/listing-types/tiers.js +150 -0
  15. package/dist/cli/index.js +0 -2
  16. package/dist/client.d.ts +21 -2
  17. package/dist/client.js +23 -4
  18. package/dist/constants/api-endpoints.d.ts +21 -0
  19. package/dist/constants/api-endpoints.js +7 -0
  20. package/dist/features/account/components/UserOrdersView.js +26 -7
  21. package/dist/features/addresses/utils/postal-autofill.d.ts +39 -0
  22. package/dist/features/addresses/utils/postal-autofill.js +54 -0
  23. package/dist/features/admin/components/AdminStoreEditorView.js +73 -34
  24. package/dist/features/admin/components/OwnerQuickActions.d.ts +29 -0
  25. package/dist/features/admin/components/OwnerQuickActions.js +142 -0
  26. package/dist/features/admin/schemas/admin-editor-forms.d.ts +1 -1
  27. package/dist/features/admin/schemas/admin-editor-forms.js +4 -1
  28. package/dist/features/auctions/components/MarketplaceAuctionCard.d.ts +11 -1
  29. package/dist/features/auctions/components/MarketplaceAuctionCard.js +9 -2
  30. package/dist/features/auth/permissions/constants.d.ts +29 -2
  31. package/dist/features/auth/permissions/constants.js +68 -1
  32. package/dist/features/blog/components/BlogFeaturedCard.d.ts +11 -1
  33. package/dist/features/blog/components/BlogFeaturedCard.js +9 -2
  34. package/dist/features/cart/components/CartDrawer.js +3 -2
  35. package/dist/features/cart/components/CartGroupLineRow.js +2 -1
  36. package/dist/features/cart/hooks/useAddToCart.js +10 -5
  37. package/dist/features/cart/utils/pending-ops.d.ts +11 -1
  38. package/dist/features/cart/utils/pending-ops.js +14 -2
  39. package/dist/features/categories/components/BrandDetailPageView.js +2 -1
  40. package/dist/features/categories/components/CategoryDetailPageView.js +2 -1
  41. package/dist/features/categories/components/CategoryGrid.d.ts +11 -1
  42. package/dist/features/categories/components/CategoryGrid.js +9 -2
  43. package/dist/features/checkout/actions/checkout-value-otp-actions.d.ts +10 -5
  44. package/dist/features/checkout/actions/checkout-value-otp-actions.js +2 -88
  45. package/dist/features/events/components/EventCard.d.ts +11 -1
  46. package/dist/features/events/components/EventCard.js +9 -2
  47. package/dist/features/layout/AppLayoutShell.js +2 -1
  48. package/dist/features/layout/BottomActions.d.ts +1 -0
  49. package/dist/features/layout/BottomActions.js +330 -0
  50. package/dist/features/layout/BottomChrome.d.ts +45 -0
  51. package/dist/features/layout/BottomChrome.js +5 -17
  52. package/dist/features/layout/SideActionDock.js +36 -13
  53. package/dist/features/layout/index.d.ts +2 -7
  54. package/dist/features/layout/index.js +7 -7
  55. package/dist/features/layout/useIsDesktop.d.ts +36 -0
  56. package/dist/features/layout/useIsDesktop.js +48 -0
  57. package/dist/features/media/messages/en.json +67 -67
  58. package/dist/features/orders/components/YourPurchaseStrip.d.ts +24 -0
  59. package/dist/features/orders/components/YourPurchaseStrip.js +77 -0
  60. package/dist/features/orders/components/index.d.ts +2 -1
  61. package/dist/features/orders/components/index.js +1 -1
  62. package/dist/features/orders/utils/order-announcement-message.d.ts +18 -0
  63. package/dist/features/orders/utils/order-announcement-message.js +16 -0
  64. package/dist/features/orders/utils/payment-proof-message.d.ts +20 -0
  65. package/dist/features/orders/utils/payment-proof-message.js +17 -0
  66. package/dist/features/parts/schemas/firestore.d.ts +227 -0
  67. package/dist/features/parts/schemas/firestore.js +6 -6
  68. package/dist/features/pre-orders/components/MarketplacePreorderCard.d.ts +11 -1
  69. package/dist/features/pre-orders/components/MarketplacePreorderCard.js +10 -3
  70. package/dist/features/products/api/[id]/route.js +8 -1
  71. package/dist/features/products/api/route.js +16 -4
  72. package/dist/features/products/components/AvailabilityScopeHint.js +1 -1
  73. package/dist/features/products/components/InteractiveProductCard.d.ts +7 -1
  74. package/dist/features/products/components/InteractiveProductCard.js +16 -4
  75. package/dist/features/products/components/MarketplaceBundleCard.d.ts +11 -1
  76. package/dist/features/products/components/MarketplaceBundleCard.js +9 -2
  77. package/dist/features/products/components/MarketplacePrizeDrawCard.d.ts +11 -1
  78. package/dist/features/products/components/MarketplacePrizeDrawCard.js +9 -2
  79. package/dist/features/products/components/ProductDetailPageView.js +27 -2
  80. package/dist/features/products/components/ProductGrid.d.ts +14 -1
  81. package/dist/features/products/components/ProductGrid.js +22 -5
  82. package/dist/features/products/components/ProductsIndexListing.js +2 -2
  83. package/dist/features/reviews/api/[id]/route.js +83 -23
  84. package/dist/features/reviews/components/PurchaseReviewAffordance.d.ts +6 -0
  85. package/dist/features/reviews/components/PurchaseReviewAffordance.js +42 -0
  86. package/dist/features/reviews/components/ReviewFormDrawer.d.ts +34 -0
  87. package/dist/features/reviews/components/ReviewFormDrawer.js +127 -0
  88. package/dist/features/reviews/components/index.d.ts +4 -0
  89. package/dist/features/reviews/components/index.js +2 -0
  90. package/dist/features/seller/actions/seller-actions.d.ts +1 -1
  91. package/dist/features/seller/actions/seller-actions.js +49 -3
  92. package/dist/features/seller/components/SellerPayoutSettingsView.js +30 -2
  93. package/dist/features/seller/components/SellerProductShell.d.ts +17 -1
  94. package/dist/features/seller/hooks/useStoreListingEntitlements.d.ts +8 -0
  95. package/dist/features/seller/hooks/useStoreListingEntitlements.js +70 -0
  96. package/dist/features/stores/components/InteractiveStoreCard.d.ts +11 -1
  97. package/dist/features/stores/components/InteractiveStoreCard.js +9 -2
  98. package/dist/features/stores/components/index.d.ts +0 -2
  99. package/dist/features/stores/components/index.js +0 -1
  100. package/dist/features/stores/schemas/index.d.ts +1 -1
  101. package/dist/features/stores/schemas/index.js +10 -1
  102. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +403 -0
  103. package/dist/index.d.ts +8 -27
  104. package/dist/index.js +14 -35
  105. package/dist/next/routing/route-map.d.ts +2 -0
  106. package/dist/next/routing/route-map.js +9 -0
  107. package/dist/providers/shipping-shiprocket/index.d.ts +98 -0
  108. package/dist/providers/shipping-shiprocket/index.js +353 -0
  109. package/dist/schemas/registry.d.ts +2 -2
  110. package/dist/security/csp.d.ts +16 -1
  111. package/dist/security/csp.js +5 -3
  112. package/dist/seed/_helpers/category-forest.js +27 -0
  113. package/dist/seed/_helpers/category-parts-glossary-map.js +1816 -86
  114. package/dist/seed/_helpers/category-parts.d.ts +1 -1
  115. package/dist/seed/_helpers/category-parts.js +13412 -3020
  116. package/dist/seed/categories-seed-data.js +48 -6
  117. package/dist/seed/homepage-sections-seed-data.js +41 -28
  118. package/dist/seed/model-composition-seed-data.js +6 -8
  119. package/dist/seed/part-glossary/chunk0.d.ts +9 -0
  120. package/dist/seed/part-glossary/chunk0.js +35044 -0
  121. package/dist/seed/part-glossary/chunk1.d.ts +9 -0
  122. package/dist/seed/part-glossary/chunk1.js +39955 -0
  123. package/dist/seed/part-glossary/chunk2.d.ts +9 -0
  124. package/dist/seed/part-glossary/chunk2.js +50335 -0
  125. package/dist/seed/part-glossary/chunk3.d.ts +9 -0
  126. package/dist/seed/part-glossary/chunk3.js +52386 -0
  127. package/dist/seed/part-glossary/chunk4.d.ts +9 -0
  128. package/dist/seed/part-glossary/chunk4.js +50549 -0
  129. package/dist/seed/part-glossary-seed-data.d.ts +1 -1
  130. package/dist/seed/part-glossary-seed-data.js +23 -47483
  131. package/dist/server.d.ts +4 -8
  132. package/dist/server.js +8 -20
  133. package/dist/styles.css +516 -358
  134. package/dist/tailwind-utilities.css +1 -1
  135. package/dist/tokens/themes/index.d.ts +3 -1
  136. package/dist/tokens/themes/index.js +11 -1
  137. package/dist/tokens/themes/technical-dark.d.ts +30 -0
  138. package/dist/tokens/themes/technical-dark.js +103 -0
  139. package/dist/tokens/themes/technical-light.d.ts +22 -0
  140. package/dist/tokens/themes/technical-light.js +89 -0
  141. package/dist/tokens/tokens.css +155 -0
  142. package/dist/ui/components/GatedPrice.js +30 -3
  143. package/dist/ui/components/GatedPrice.style.css +22 -19
  144. package/dist/ui/components/QuickCreateModal.style.css +78 -78
  145. package/dist/ui/forms/FormShell.style.css +260 -260
  146. package/package.json +1 -1
  147. package/dist/_internal/server/jobs/core/whatsappCatalogImport.d.ts +0 -27
  148. package/dist/_internal/server/jobs/core/whatsappCatalogImport.js +0 -172
  149. package/dist/_internal/server/jobs/core/whatsappNotify.d.ts +0 -35
  150. package/dist/_internal/server/jobs/core/whatsappNotify.js +0 -140
  151. package/dist/features/admin/components/AdminWhatsAppGuideView.d.ts +0 -2
  152. package/dist/features/admin/components/AdminWhatsAppGuideView.js +0 -35
  153. package/dist/features/orders/components/MarketplaceOrderCard.d.ts +0 -36
  154. package/dist/features/orders/components/MarketplaceOrderCard.js +0 -57
  155. package/dist/features/stores/components/StoreWhatsAppGuideView.d.ts +0 -3
  156. package/dist/features/stores/components/StoreWhatsAppGuideView.js +0 -12
  157. package/dist/features/whatsapp-bot/api/send-status/route.d.ts +0 -12
  158. package/dist/features/whatsapp-bot/api/send-status/route.js +0 -38
  159. package/dist/features/whatsapp-bot/api/webhook/route.d.ts +0 -12
  160. package/dist/features/whatsapp-bot/api/webhook/route.js +0 -43
  161. package/dist/features/whatsapp-bot/components/SellerWhatsAppSettingsView.d.ts +0 -6
  162. package/dist/features/whatsapp-bot/components/SellerWhatsAppSettingsView.js +0 -270
  163. package/dist/features/whatsapp-bot/components/WhatsAppChatButton.d.ts +0 -9
  164. package/dist/features/whatsapp-bot/components/WhatsAppChatButton.js +0 -7
  165. package/dist/features/whatsapp-bot/components/index.d.ts +0 -3
  166. package/dist/features/whatsapp-bot/components/index.js +0 -2
  167. package/dist/features/whatsapp-bot/helpers/payment-proof-message.d.ts +0 -14
  168. package/dist/features/whatsapp-bot/helpers/payment-proof-message.js +0 -17
  169. package/dist/features/whatsapp-bot/helpers/whatsapp.d.ts +0 -52
  170. package/dist/features/whatsapp-bot/helpers/whatsapp.js +0 -283
  171. package/dist/features/whatsapp-bot/index.d.ts +0 -3
  172. package/dist/features/whatsapp-bot/index.js +0 -3
  173. package/dist/features/whatsapp-bot/manifest.d.ts +0 -2
  174. package/dist/features/whatsapp-bot/manifest.js +0 -13
  175. package/dist/features/whatsapp-bot/messages/en.json +0 -7
  176. package/dist/features/whatsapp-bot/server.d.ts +0 -7
  177. package/dist/features/whatsapp-bot/server.js +0 -7
  178. package/dist/features/whatsapp-bot/types/index.d.ts +0 -126
  179. package/dist/features/whatsapp-bot/types/index.js +0 -1
@@ -0,0 +1,70 @@
1
+ "use client";
2
+ /**
3
+ * Which listing types the signed-in seller's own store may create.
4
+ *
5
+ * ── Why this needs a fetch at all ─────────────────────────────────────────
6
+ *
7
+ * 🛑 `StoreDocument.capabilities` is listed in `PRIVATE_STORE_FIELDS` —
8
+ * "internal feature gating; server-enforced, never client-read" — so it is
9
+ * absent from every public store payload and from the session user. A client
10
+ * component genuinely cannot derive this locally, and the alternative a
11
+ * previous draft reached for (publish the capability array to the browser)
12
+ * would ship `lower_commission_rate`, `priority_support` and fifteen more
13
+ * admin-granted entitlements to anyone who opened devtools. Root Cause #70.
14
+ *
15
+ * So the server sends a PROJECTION: `allowedListingTypes`, already resolved by
16
+ * `listingTypesAllowedFor` on the same capability array the write-path gate
17
+ * uses. Client and server therefore agree by construction rather than by two
18
+ * copies of one rule (Root Cause #30).
19
+ *
20
+ * ── Why the loading state is not "denied" ─────────────────────────────────
21
+ *
22
+ * `isLoading` is returned and callers must respect it. The three-state rule
23
+ * from `useCanSeePrices`: "not known yet" rendering as "no" shows an entitled
24
+ * seller a refusal on every page load. The server refuses authoritatively in
25
+ * `createSellerProduct` / `sellerUpdateProduct`, so an optimistic render while
26
+ * the fetch is in flight costs nothing worse than a late refusal.
27
+ *
28
+ * @tag domain:seller
29
+ * @tag layer:hooks
30
+ * @tag access:client
31
+ */
32
+ import { useMemo } from "react";
33
+ import { useApiQuery } from "../../../client/api/useApiQuery.js";
34
+ import { apiClient } from "../../../http/index.js";
35
+ import { SELLER_ENDPOINTS } from "../../../constants/api-endpoints.js";
36
+ import { BASIC_LISTING_TYPES } from "../../../_internal/shared/listing-types/tiers.js";
37
+ export function useStoreListingEntitlements() {
38
+ const query = useApiQuery({
39
+ queryKey: ["store", "listing-entitlements"],
40
+ queryFn: async () => {
41
+ const res = await apiClient.get(SELLER_ENDPOINTS.PAYOUT_SETTINGS);
42
+ return res?.data ?? {};
43
+ },
44
+ /*
45
+ * The answer changes only when an admin or the seller flips a capability,
46
+ * so a short window is pointless churn — but it must not be `Infinity`,
47
+ * because a seller who has just ticked the box in another tab should not
48
+ * have to hard-reload to see the nav and forms open up.
49
+ */
50
+ staleTime: 60000,
51
+ silentBackgroundFailures: true,
52
+ });
53
+ return useMemo(() => {
54
+ /*
55
+ * 🛑 Falls back to the BASIC types, not to everything and not to nothing.
56
+ *
57
+ * Everything would offer a form that the server then refuses — the exact
58
+ * "filled in a long wizard and got rejected at publish" experience this
59
+ * hook exists to prevent. Nothing would lock a seller out of `standard`
60
+ * on a transient network blip, which is the one type every store has.
61
+ */
62
+ const list = query.data?.allowedListingTypes;
63
+ const allowed = new Set(Array.isArray(list) && list.length > 0 ? list : BASIC_LISTING_TYPES);
64
+ return {
65
+ allowed,
66
+ advancedEnabled: query.data?.advancedListingTypesEnabled === true,
67
+ isLoading: query.isLoading,
68
+ };
69
+ }, [query.data, query.isLoading]);
70
+ }
@@ -12,5 +12,15 @@ export interface InteractiveStoreCardProps {
12
12
  visitStore?: string;
13
13
  };
14
14
  className?: string;
15
+ /**
16
+ * Owner-only pencil / clone / bin, for an admin or whoever owns this record.
17
+ * Pass `<OwnerQuickActions>`; it renders nothing for anyone else.
18
+ *
19
+ * 🛑 Rendered OUTSIDE this card's own `<Link>`/`<TextLink>`, as a sibling.
20
+ * A `<button>` inside an `<a>` is invalid HTML that browsers resolve by
21
+ * closing the anchor early, which breaks the CARD's navigation rather than
22
+ * just the button's — `stopPropagation` stops a click but cannot fix a parse.
23
+ */
24
+ renderOwnerActions?: () => React.ReactNode;
15
25
  }
16
- export declare function InteractiveStoreCard({ store, href, selectable, isSelected, onSelect, labels, className, }: InteractiveStoreCardProps): import("react").JSX.Element;
26
+ export declare function InteractiveStoreCard({ store, href, selectable, isSelected, onSelect, labels, className, renderOwnerActions, }: InteractiveStoreCardProps): import("react").JSX.Element;
@@ -10,13 +10,13 @@ const __O = {
10
10
  hidden: "overflow-hidden",
11
11
  };
12
12
  const CLS_RATING_BADGE = "absolute top-2 right-2 flex items-center gap-[var(--appkit-space-1)] rounded-full bg-black/50 backdrop-blur-sm px-[var(--appkit-space-2)] py-[var(--appkit-space-0-5)] text-[length:var(--appkit-text-xs)] font-semibold text-warning";
13
- export function InteractiveStoreCard({ store, href, selectable, isSelected = false, onSelect, labels = {}, className = "", }) {
13
+ export function InteractiveStoreCard({ store, href, selectable, isSelected = false, onSelect, labels = {}, className = "", renderOwnerActions, }) {
14
14
  const initial = store.storeName[0]?.toUpperCase() ?? "S";
15
15
  const [logoBroken, setLogoBroken] = useState(false);
16
16
  const hasLogo = Boolean(store.storeLogoURL) && !logoBroken;
17
17
  const hasBanner = Boolean(store.storeBannerURL);
18
18
  const longPress = useLongPress(() => onSelect?.(store.id, !isSelected));
19
- return (_jsxs(Div, { className: [
19
+ const card = (_jsxs(Div, { className: [
20
20
  "group relative flex flex-col h-full overflow-hidden rounded-xl border bg-[var(--appkit-color-surface)] shadow-sm transition-shadow hover:shadow-lg",
21
21
  isSelected
22
22
  ? "border-[var(--appkit-color-primary,theme(colors.violet.600))] outline outline-2 outline-[var(--appkit-color-primary,theme(colors.violet.600))]"
@@ -26,4 +26,11 @@ export function InteractiveStoreCard({ store, href, selectable, isSelected = fal
26
26
  e.preventDefault();
27
27
  onSelect(store.id, !isSelected);
28
28
  }, label: isSelected ? "Deselect store" : "Select store", position: "top-2 left-2", className: selectable || isSelected ? "opacity-100" : "opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto transition-opacity" })), _jsxs(Link, { href: href, className: "flex flex-col flex-1 min-h-0", children: [_jsxs(Div, { surface: "muted", className: `relative aspect-video w-full ${__O.hidden} flex-shrink-0`, children: [hasBanner ? (_jsx(MediaImage, { src: store.storeBannerURL, alt: `${store.storeName} banner`, size: "banner", className: "h-full w-full object-cover transition-transform duration-500 group-hover:scale-105" })) : (_jsx(Row, { className: "absolute inset-0", align: "center", justify: "center", children: _jsx(Span, { className: "opacity-20 select-none", size: "5xl", "aria-hidden": "true", children: "\uD83C\uDFEA" }) })), _jsx(Scrim, { direction: "bottom-up", intensity: "subtle", className: "absolute inset-x-0 bottom-0 h-12" }), store.averageRating != null && store.averageRating > 0 && (_jsxs(Div, { className: CLS_RATING_BADGE, children: ["\u2605 ", store.averageRating.toFixed(1)] }))] }), _jsxs(Stack, { paddingY: "b-md", className: "flex-1", padding: "x-md", children: [_jsx(Row, { className: "-mt-5 mb-2", align: "end", justify: "between", children: _jsx(Div, { className: "flex-shrink-0", children: hasLogo ? (_jsx(Div, { className: "relative h-10 w-10 border-2 border-[var(--appkit-color-border-subtle)] bg-[var(--appkit-color-surface)]", rounded: "lg", shadow: "md", overflow: "hidden", children: _jsx(MediaImage, { src: store.storeLogoURL, alt: store.storeName, size: "thumbnail", onError: () => setLogoBroken(true) }) })) : (_jsx(Row, { textWeight: "bold", textSize: "base", className: "h-10 w-10 border-2 border-white bg-primary/10 dark:bg-primary/20 text-primary", align: "center", justify: "center", rounded: "lg", shadow: "md", children: initial })) }) }), _jsx(Heading, { level: 3, className: `group-hover:text-primary transition-colors`, truncate: true, size: "sm", weight: "bold", color: "primary", children: store.storeName }), store.storeDescription ? (_jsx(RichText, { html: normalizeRichTextHtml(store.storeDescription), proseClass: "prose prose-sm max-w-none dark:prose-invert prose-p:my-0", className: "mt-1 line-clamp-2 text-[length:var(--appkit-text-xs)] text-[var(--appkit-color-text-muted)] flex-1" })) : (_jsx(Div, { className: "flex-1" })), _jsxs(Row, { color: "muted", textSize: "xs", gap: "sm", className: "mt-2.5", wrap: true, children: [store.totalProducts != null && store.totalProducts > 0 && (_jsxs(Span, { layout: "flex", gap: "2xs", children: [_jsx(Span, { "aria-hidden": "true", children: "\uD83D\uDCE6" }), " ", store.totalProducts, " ", labels.products ?? "products"] })), store.itemsSold != null && store.itemsSold > 0 && (_jsxs(Span, { layout: "flex", gap: "2xs", children: [_jsx(Span, { "aria-hidden": "true", children: "\uD83D\uDECD\uFE0F" }), " ", store.itemsSold, " ", labels.sold ?? "sold"] }))] }), _jsxs(Row, { border: "top-subtle", paddingY: "y-xs-tall", className: "mt-3", align: "center", justify: "between", children: [_jsxs(Span, { size: "xs", weight: "semibold", className: "text-primary group-hover:underline transition-colors", children: [labels.visitStore ?? "Visit store", " \u2192"] }), store.totalReviews != null && store.totalReviews > 0 && (_jsxs(Span, { size: "xs", color: "muted", children: [store.totalReviews, " ", labels.reviews ?? "reviews"] }))] })] })] })] }));
29
+ if (!renderOwnerActions)
30
+ return card;
31
+ /*
32
+ * Sibling of the card, never a child — see the prop's docstring. `z-20`
33
+ * beats this card's own overlays, which all sit at `z-10`.
34
+ */
35
+ return (_jsxs(Div, { className: "relative h-full", children: [card, _jsx(Div, { className: "absolute right-2 bottom-2 z-20", children: renderOwnerActions() })] }));
29
36
  }
@@ -47,5 +47,3 @@ export { StoreCapabilitiesGuideView } from "./StoreCapabilitiesGuideView";
47
47
  export type { StoreCapabilitiesGuideViewProps } from "./StoreCapabilitiesGuideView";
48
48
  export { StoreSettingsGuideView } from "./StoreSettingsGuideView";
49
49
  export type { StoreSettingsGuideViewProps } from "./StoreSettingsGuideView";
50
- export { StoreWhatsAppGuideView } from "./StoreWhatsAppGuideView";
51
- export type { StoreWhatsAppGuideViewProps } from "./StoreWhatsAppGuideView";
@@ -20,4 +20,3 @@ export { StoreOrdersGuideView } from "./StoreOrdersGuideView.js";
20
20
  export { StoreFinanceGuideView } from "./StoreFinanceGuideView.js";
21
21
  export { StoreCapabilitiesGuideView } from "./StoreCapabilitiesGuideView.js";
22
22
  export { StoreSettingsGuideView } from "./StoreSettingsGuideView.js";
23
- export { StoreWhatsAppGuideView } from "./StoreWhatsAppGuideView.js";
@@ -228,7 +228,7 @@ export declare const storeFirestoreSchema: z.ZodObject<{
228
228
  totalReviews: number;
229
229
  averageRating?: number | undefined;
230
230
  }>>;
231
- capabilities: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
231
+ capabilities: z.ZodOptional<z.ZodArray<z.ZodEnum<[string, ...string[]]>, "many">>;
232
232
  customCommissionRate: z.ZodOptional<z.ZodNumber>;
233
233
  googleReviews: z.ZodOptional<z.ZodObject<{
234
234
  placeId: z.ZodString;
@@ -1,6 +1,7 @@
1
1
  export * from "./firestore.js";
2
2
  import { z } from "zod";
3
3
  import { StoreStatusValues } from "./firestore.js";
4
+ import { ALL_STORE_CAPABILITIES } from "../../auth/permissions/constants.js";
4
5
  import { auditTimestampsShape, firestoreDateSchema, rupeesSchema } from "../../../schemas/firestore-helpers.js";
5
6
  // ─── Firestore document schema (W2) ───────────────────────────────────────────
6
7
  // Mirrors StoreDocument in ./firestore.ts. Registered into SCHEMAS.firestore.stores.
@@ -70,7 +71,15 @@ export const storeFirestoreSchema = z.object({
70
71
  averageRating: z.number().optional(),
71
72
  })
72
73
  .optional(),
73
- capabilities: z.array(z.string()).optional(),
74
+ /*
75
+ * 🛑 A `z.enum`, not `z.array(z.string())`. Nothing used to check a stored
76
+ * capability against the union, so a typo saved successfully and the gate it
77
+ * was meant to open never opened — with no error anywhere, because an
78
+ * unrecognised capability is indistinguishable from an ungranted one.
79
+ */
80
+ capabilities: z
81
+ .array(z.enum(ALL_STORE_CAPABILITIES))
82
+ .optional(),
74
83
  customCommissionRate: z.number().min(0).max(100).optional(),
75
84
  googleReviews: z
76
85
  .object({
@@ -7247,6 +7247,409 @@ const rawTesterChecklistItems = [
7247
7247
  ],
7248
7248
  },
7249
7249
  ], { adminOnly: true }),
7250
+ /*
7251
+ * OWNER QUICK-ACTIONS, BUYER PURCHASE CONTEXT, AND THE ADVANCED-TYPE GATE.
7252
+ *
7253
+ * 🛑 Every case here is an explicit BEFORE -> AFTER, and that is not a
7254
+ * stylistic choice. Each defect these cover returned HTTP 200 with plausible
7255
+ * content: a form that saved nothing, a gate that permitted everything, a
7256
+ * review whose search tokens silently described its previous text. A case
7257
+ * reading "check the editor works" passes against all of them.
7258
+ *
7259
+ * The spares case is the proof. Before the fix, every /store/spares/new save
7260
+ * failed Zod validation because the schema's hand-written listingType enum
7261
+ * omitted the type — so a tester told to "check creating a spares listing
7262
+ * works" would answer no, with no idea what correct looked like.
7263
+ */
7264
+ ...group("owner-tools", "Owner tools & purchase context", [
7265
+ {
7266
+ pageKey: "listing-types",
7267
+ pageLabel: "Listing types & the advanced gate",
7268
+ href: "/store/products",
7269
+ cases: [
7270
+ {
7271
+ key: "spares-can-be-created",
7272
+ label: "A spares listing can be created at all",
7273
+ roles: ["seller"],
7274
+ startPage: "/store/spares/new",
7275
+ steps: [
7276
+ "Sign in as the seller who owns store-beyblade-arena.",
7277
+ "Open /store/spares/new and fill in a title, a price and a stock quantity.",
7278
+ "Save as draft, then publish.",
7279
+ "Open /spares and look for the listing.",
7280
+ ],
7281
+ inputs: { title: "QA Spare attack ring", price: 249, stockQuantity: 3 },
7282
+ expectedBehaviour: "productCreateSchema accepts listingType 'spares'. Its enum is DERIVED from ALL_LISTING_TYPES rather than hand-written, so a type in the union cannot be missing from the schema.",
7283
+ expectedUiState: "Both the autosave and the publish succeed, with no field error on listingType. The published row appears on /spares.",
7284
+ endResult: "BEFORE: every save failed validation — the hand-written enum omitted 'spares' while carrying a dead 'bundle' — so the type could not be created at all, even though its 21 pages, its nav entry and its browse page all existed. AFTER: it saves and appears.",
7285
+ },
7286
+ {
7287
+ key: "dead-bundle-type-rejected",
7288
+ label: "listingType 'bundle' is rejected rather than silently stored",
7289
+ roles: ["admin"],
7290
+ startPage: "/admin/products",
7291
+ steps: [
7292
+ "Sign in as admin@letitrip.in / TempPass123!.",
7293
+ "POST to /api/admin/products with a valid product body and listingType set to 'bundle'.",
7294
+ "Read the response status and body.",
7295
+ ],
7296
+ inputs: { listingType: "bundle" },
7297
+ expectedBehaviour: "'bundle' stopped being a listingType in SB-UNI-D — bundles are a categoryType on categories. The derived enum no longer accepts it.",
7298
+ expectedUiState: "A 400 naming listingType. No product is created.",
7299
+ endResult: "BEFORE: it passed validation and wrote a product that pluginFor, LISTING_KIND_ALIAS_MAP and isListingRowAvailable have no branch for — invisible on every surface and uneditable from any UI. AFTER: refused at the door.",
7300
+ },
7301
+ {
7302
+ key: "advanced-types-hidden-for-new-store",
7303
+ label: "A new store sees only Products, Spares and Auctions under Listings",
7304
+ roles: ["seller"],
7305
+ startPage: "/store",
7306
+ steps: [
7307
+ "Sign in as a seller whose store has no advanced_listing_types capability.",
7308
+ "Read every entry in the Listings group of the store sidebar.",
7309
+ "Type /store/live/new directly into the address bar.",
7310
+ ],
7311
+ expectedBehaviour: "The nav filter resolves the store's entitlements SERVER-side — capabilities is a private store field, so a client filter has nothing to read. The create shell refuses a type the store may not use.",
7312
+ expectedUiState: "The Listings group lists Products, Spares and Auctions and nothing else. /store/live/new renders a refusal naming the type and linking to store preferences, not a usable form.",
7313
+ expectedData: { listingNavItems: 3 },
7314
+ endResult: "BEFORE: all eleven entries showed and every per-type new page rendered a complete, usable form for any store. AFTER: three entries, and the advanced pages refuse.",
7315
+ },
7316
+ {
7317
+ key: "advanced-create-refused-server-side",
7318
+ label: "The API refuses an advanced type even when the form is bypassed",
7319
+ roles: ["seller"],
7320
+ startPage: "/store/products",
7321
+ steps: [
7322
+ "Sign in as a seller without the advanced_listing_types capability.",
7323
+ "Call the seller create-product action with listingType 'prize-draw'.",
7324
+ "Read the result.",
7325
+ ],
7326
+ inputs: { listingType: "prize-draw" },
7327
+ expectedBehaviour: "assertListingTypeAllowed runs inside createSellerProduct, where the store document is already loaded — so the refusal costs no extra read and cannot be skipped by calling a different entry point.",
7328
+ expectedUiState: "An authorization error naming the type. No listing is created.",
7329
+ endResult: "BEFORE: the only gate covered auction and pre-order, and it sat behind an if-store check that let a storeless caller through. AFTER: all seven advanced types refused, and a non-admin with no store is refused rather than waved past.",
7330
+ },
7331
+ {
7332
+ key: "cannot-patch-into-advanced-type",
7333
+ label: "A standard listing cannot be PATCHed into an advanced type",
7334
+ roles: ["seller"],
7335
+ startPage: "/store/products",
7336
+ steps: [
7337
+ "Sign in as a seller without the advanced_listing_types capability.",
7338
+ "Create a standard listing and note its id.",
7339
+ "Call the seller update action on it with listingType 'auction', then again with 'prize-draw'.",
7340
+ "Reload the listing and read its type.",
7341
+ ],
7342
+ expectedBehaviour: "The update gate fires on a CHANGE of type. 'auction' is a basic type so it is allowed; 'prize-draw' is advanced and is refused.",
7343
+ expectedUiState: "The prize-draw PATCH returns an authorization error. The listing's type is unchanged after a reload.",
7344
+ endResult: "BEFORE: sellerUpdateProduct had NO type check at all, so the create gate was bypassable in one request. AFTER: it is refused. 🛑 And editing an EXISTING advanced listing still works — the gate fires only on a change, because every per-type edit page re-sends its own type literal.",
7345
+ },
7346
+ {
7347
+ key: "advanced-optin-round-trips",
7348
+ label: "The advanced-types checkbox survives a reload",
7349
+ roles: ["seller"],
7350
+ startPage: "/store/payout-settings",
7351
+ steps: [
7352
+ "Sign in as a seller and open Payout & Preferences from the store sidebar.",
7353
+ "Tick 'Enable advanced listing types' and save.",
7354
+ "RELOAD the page and read the checkbox.",
7355
+ "Open the store sidebar and read the Listings group.",
7356
+ ],
7357
+ expectedBehaviour: "The flag is stored as the advanced_listing_types capability, MERGED into the array rather than replacing it, so admin-granted capabilities survive. The route echoes the saved value back.",
7358
+ expectedUiState: "After the reload the checkbox is still ticked and the Listings group now lists all ten types.",
7359
+ expectedData: { listingNavItems: 10 },
7360
+ endResult: "BEFORE: there was no such control, and /store/payout-settings had no nav entry at all — reachable only by typing the URL. AFTER: it is in the sidebar, and the value round-trips.",
7361
+ },
7362
+ {
7363
+ key: "optin-preserves-admin-capabilities",
7364
+ label: "Saving the seller checkbox does not revoke admin-granted capabilities",
7365
+ roles: ["admin", "seller"],
7366
+ startPage: "/admin/stores",
7367
+ steps: [
7368
+ "As admin, open the store editor for the seller's store and tick 'Verified Seller Badge'. Save.",
7369
+ "Sign in as that seller, open Payout & Preferences, tick 'Enable advanced listing types' and save.",
7370
+ "As admin again, reopen the store editor and read the capability list.",
7371
+ ],
7372
+ expectedBehaviour: "The seller's form owns ONE member of the capabilities array and touches only that member; everything else is read from the stored document and written back unchanged.",
7373
+ expectedUiState: "Verified Seller Badge is still ticked, alongside the new advanced_listing_types.",
7374
+ endResult: "A seller form whose write shape can EXPRESS state it does not own will eventually overwrite it (Root Cause #76). This is the case that proves it does not.",
7375
+ },
7376
+ ],
7377
+ },
7378
+ {
7379
+ pageKey: "owner-quick-actions",
7380
+ pageLabel: "Pencil, clone and bin on public pages",
7381
+ href: "/products",
7382
+ cases: [
7383
+ {
7384
+ key: "no-controls-for-guest",
7385
+ label: "A signed-out visitor sees no pencil, clone or bin anywhere",
7386
+ roles: ["guest"],
7387
+ startPage: "/products",
7388
+ steps: [
7389
+ "Open /products signed out and read a product card.",
7390
+ "Open a product detail page.",
7391
+ "Open /categories and a category page.",
7392
+ ],
7393
+ expectedBehaviour: "The toolbar renders nothing when there is no session, and nothing while the session is still resolving.",
7394
+ expectedUiState: "No edit, duplicate or delete control on any card or detail page.",
7395
+ endResult: "A visitor must never see an owner control, and must never see one flash in and out.",
7396
+ },
7397
+ {
7398
+ key: "no-flash-for-signed-in-owner",
7399
+ label: "The toolbar appears once, without flashing",
7400
+ roles: ["admin"],
7401
+ startPage: "/products",
7402
+ steps: [
7403
+ "Sign in as admin@letitrip.in / TempPass123!.",
7404
+ "HARD-reload /products and watch the first second of the page closely.",
7405
+ "Repeat twice more.",
7406
+ ],
7407
+ expectedBehaviour: "Public pages are ISR and read no session, so loading starts true on every hard load even for a signed-in user. The toolbar renders NOTHING until auth resolves.",
7408
+ expectedUiState: "The controls are absent, then appear once. They never appear, vanish and reappear.",
7409
+ endResult: "A two-state gate here would show nothing, then pop the toolbar in a few hundred ms after every single page load. Appearing late is invisible; flashing is a bug people report.",
7410
+ },
7411
+ {
7412
+ key: "pencil-opens-that-records-editor",
7413
+ label: "The pencil lands on that record's editor, not a list",
7414
+ roles: ["admin"],
7415
+ startPage: "/products",
7416
+ steps: [
7417
+ "Signed in as admin, open a product detail page and note the product title.",
7418
+ "Press the pencil.",
7419
+ "Read the page that opens.",
7420
+ ],
7421
+ expectedBehaviour: "The pencil navigates to that product's edit route.",
7422
+ expectedUiState: "The editor opens on the SAME product, with its title already filled in.",
7423
+ endResult: "BEFORE: there was no control at all — ZERO public detail views rendered any owner affordance — so fixing a typo meant going to the dashboard, finding the list and searching for the row.",
7424
+ },
7425
+ {
7426
+ key: "card-navigation-still-works",
7427
+ label: "A card with owner controls still navigates when clicked",
7428
+ roles: ["admin"],
7429
+ startPage: "/products",
7430
+ steps: [
7431
+ "Signed in as admin, open /products.",
7432
+ "Click a product card anywhere OTHER than the owner controls.",
7433
+ "Go back, then click the pencil on the same card.",
7434
+ ],
7435
+ expectedBehaviour: "The toolbar is rendered OUTSIDE the card's anchor. A button inside an anchor is invalid HTML that browsers resolve by closing the anchor early, which breaks the CARD's navigation.",
7436
+ expectedUiState: "Clicking the card opens the product. Clicking the pencil opens the editor. Neither does both.",
7437
+ endResult: "The card's own link must survive the toolbar being added to it.",
7438
+ },
7439
+ {
7440
+ key: "clone-is-admin-only",
7441
+ label: "A seller sees pencil and bin but NOT clone",
7442
+ roles: ["seller"],
7443
+ startPage: "/products",
7444
+ steps: [
7445
+ "Sign in as the seller who owns store-beyblade-arena.",
7446
+ "Open one of that store's product detail pages.",
7447
+ "Read the owner controls.",
7448
+ ],
7449
+ expectedBehaviour: "Clone is gated on useIsAdmin, not the owner check — useIsOwner returns true for the owner too, so reusing it would hand every seller a staff tool.",
7450
+ expectedUiState: "A pencil and a bin. No duplicate control.",
7451
+ endResult: "Clone is a staff tool; editing and deleting your own listing is ordinary.",
7452
+ },
7453
+ {
7454
+ key: "clone-creates-a-draft-copy",
7455
+ label: "Clone creates a (Copy) draft and opens its editor",
7456
+ roles: ["admin"],
7457
+ startPage: "/products",
7458
+ steps: [
7459
+ "Signed in as admin, open a product detail page and note its title, stock and whether it has bids.",
7460
+ "Press the duplicate control.",
7461
+ "Read the editor that opens: its title, its status, its bid count and its group membership.",
7462
+ "Go back to the original product page and read its title and status.",
7463
+ ],
7464
+ expectedBehaviour: "The duplicate endpoint strips identity (id/slug/barcodeId), analytics (viewCount/avgRating/reviewCount), auction bid state and group membership, titles the copy with a (Copy) suffix and forces status draft. It returns the new id so the UI can land on the copy.",
7465
+ expectedUiState: "The editor opens on a NEW record whose title ends in (Copy), status draft, no bids and no group. The original is untouched.",
7466
+ expectedData: { copyStatus: "draft", copyBidCount: 0 },
7467
+ endResult: "A duplicate must start standalone — silently joined to the source's group is how one edit changes two listings.",
7468
+ },
7469
+ {
7470
+ key: "bin-confirms-before-deleting",
7471
+ label: "The bin confirms, names the record, and cancelling changes nothing",
7472
+ roles: ["admin"],
7473
+ startPage: "/products",
7474
+ steps: [
7475
+ "Signed in as admin, open a product detail page and note its title.",
7476
+ "Press the bin and read the dialog.",
7477
+ "Press Cancel. Reload the page.",
7478
+ "Press the bin again and confirm.",
7479
+ "Try to open the same product URL.",
7480
+ ],
7481
+ expectedBehaviour: "The confirm copy comes from the ACTIONS registry, never inline, and names the record being deleted.",
7482
+ expectedUiState: "The dialog names the product. After Cancel the product is still there on reload. After confirming, the product URL no longer resolves.",
7483
+ endResult: "A destructive control on a PUBLIC page must state what it will destroy, and cancelling must be a true no-op.",
7484
+ },
7485
+ ],
7486
+ },
7487
+ {
7488
+ pageKey: "purchase-context",
7489
+ pageLabel: "Your order & your review on a product page",
7490
+ href: "/products",
7491
+ cases: [
7492
+ {
7493
+ key: "strip-links-to-your-order",
7494
+ label: "A buyer who ordered this item gets a Track link to that order",
7495
+ roles: ["buyer"],
7496
+ startPage: "/user/orders",
7497
+ steps: [
7498
+ "Sign in as a buyer with a seeded order and note the order id and one item in it.",
7499
+ "Open that item's product page.",
7500
+ "Read the strip above the reviews.",
7501
+ "Press Track.",
7502
+ ],
7503
+ expectedBehaviour: "The lookup scans the buyer's recent orders for one containing this product — items[] holds objects, so no Firestore query can match a nested productId.",
7504
+ expectedUiState: "The strip names the order date and its short reference, and Track lands on the track page for THAT order.",
7505
+ endResult: "BEFORE: no surface linked a product to the buyer's own order; the route was remember-you-bought-it, go to /user/orders, page through. 🛑 No price appears in the strip.",
7506
+ },
7507
+ {
7508
+ key: "older-than-window-says-so",
7509
+ label: "A purchase older than 90 days says so rather than denying it",
7510
+ roles: ["buyer"],
7511
+ startPage: "/products",
7512
+ steps: [
7513
+ "Sign in as a buyer and open a product page for an item they have NOT ordered in the last 90 days.",
7514
+ "Read the strip's wording exactly.",
7515
+ ],
7516
+ expectedBehaviour: "The order lookup is a bounded 90-day window, and the response carries windowDays so the UI can state the bound.",
7517
+ expectedUiState: "It reads 'No order for this item in the last 90 days' with a link to all orders. It does NOT read 'you haven't bought this'.",
7518
+ endResult: "🛑 A filtered empty result reported as an absolute zero is a lie the buyer cannot check — the same defect UserOrdersView carries a long comment about.",
7519
+ },
7520
+ {
7521
+ key: "write-then-edit-a-review",
7522
+ label: "A buyer can write a review and then edit it",
7523
+ roles: ["buyer"],
7524
+ startPage: "/products",
7525
+ steps: [
7526
+ "Sign in as a buyer with a delivered order and open one of its items.",
7527
+ "Press 'Write a review' in the strip, give 4 stars, a title and a comment, and save.",
7528
+ "RELOAD the product page and read the strip and the review list.",
7529
+ "Press 'Edit review', change the comment, and save.",
7530
+ "RELOAD again and read the review.",
7531
+ ],
7532
+ inputs: { rating: 4, title: "QA review purchase-context", comment: "First version." },
7533
+ expectedBehaviour: "One form component serves both the inline drawer and /user/reviews/[id]/edit, so the two cannot drift. An approved review edited by its author stays approved.",
7534
+ expectedUiState: "The strip shows 'Your review: 4/5' and the edited comment is the one displayed after the reload.",
7535
+ endResult: "BEFORE: there was NO review form anywhere in the application — useCreateReview had zero component consumers and ViewReviewModal is read-only. AFTER: a review can be written and changed.",
7536
+ },
7537
+ {
7538
+ key: "review-edit-keeps-search-and-images-honest",
7539
+ label: "Editing a review rebuilds its search tokens and its hasImages flag",
7540
+ roles: ["buyer"],
7541
+ startPage: "/user/reviews",
7542
+ steps: [
7543
+ "Sign in as a buyer with an existing review that has at least one image.",
7544
+ "Open /user/reviews and note the review's id.",
7545
+ "Edit the review, replacing a distinctive word in the comment with a new distinctive word, and save.",
7546
+ "Search the product's reviews for the OLD word, then for the NEW word.",
7547
+ "Read the review's stored hasImages against whether it still has images.",
7548
+ ],
7549
+ expectedBehaviour: "The PATCH goes through reviewRepository, so searchTxt is rebuilt and userName stays encrypted; hasImages is derived whenever images is sent.",
7550
+ expectedUiState: "Searching the NEW word finds the review. Searching the OLD word does not.",
7551
+ expectedData: { foundByNewWord: true, foundByOldWord: false },
7552
+ endResult: "BEFORE: the route wrote through a GENERIC repository, so the tokens still described the previous text — the review was findable only by what it used to say — and hasImages went stale.",
7553
+ },
7554
+ {
7555
+ key: "review-get-returns-a-name-not-ciphertext",
7556
+ label: "GET a review by id returns a display name, not enc:v1 ciphertext",
7557
+ roles: ["guest"],
7558
+ startPage: "/reviews",
7559
+ steps: [
7560
+ "Open /api/reviews/{id} for any seeded review.",
7561
+ "Read the userName field and look for a helpfulVoterIds field.",
7562
+ ],
7563
+ expectedBehaviour: "The read goes through reviewRepository, so mapDoc decrypts userName and strips helpfulVoterIds.",
7564
+ expectedUiState: "userName is a readable name. There is no helpfulVoterIds field in the response.",
7565
+ endResult: "BEFORE: the generic repository returned the document verbatim, so this public endpoint served the reviewer's name as raw enc:v1 ciphertext and published the list of everyone who had voted it helpful.",
7566
+ },
7567
+ ],
7568
+ },
7569
+ {
7570
+ pageKey: "order-journey",
7571
+ pageLabel: "Order tracking & search",
7572
+ href: "/user/orders",
7573
+ cases: [
7574
+ {
7575
+ key: "track-offered-before-shipping",
7576
+ label: "Track is offered on a pending order, not only a shipped one",
7577
+ roles: ["buyer"],
7578
+ startPage: "/user/orders",
7579
+ steps: [
7580
+ "Sign in as a buyer with orders in several statuses.",
7581
+ "Find an order that is pending, confirmed or processing.",
7582
+ "Read its row's actions.",
7583
+ "Press Track.",
7584
+ ],
7585
+ expectedBehaviour: "Every pre-delivery status offers Track. The timeline reads correctly for all of them — it shows the stages reached and the stages still ahead.",
7586
+ expectedUiState: "A Track action is present, and it opens the track page for that order.",
7587
+ endResult: "BEFORE: Track appeared only once an order was shipped, so a buyer waiting on a pending order — exactly when they most want to know — had no link at all.",
7588
+ },
7589
+ {
7590
+ key: "track-page-shows-the-parcel",
7591
+ label: "The track page shows items, address, carrier and onward links",
7592
+ roles: ["buyer"],
7593
+ startPage: "/user/orders",
7594
+ steps: [
7595
+ "Open the track page for an order with more than one item and a delivery address.",
7596
+ "Read everything on the page.",
7597
+ ],
7598
+ expectedBehaviour: "Items, the delivery address, the carrier and tracking number, and links to the detail page, the invoice and — once delivered — a return, all come from data already on the order.",
7599
+ expectedUiState: "Item thumbnails and titles with quantities, the delivery address, the short order reference, and the onward action buttons. No price anywhere.",
7600
+ endResult: "BEFORE: the page rendered a timeline and nothing else — a wiring gap, not a data gap. A buyer who wanted their invoice had to go back to the list and find the order again.",
7601
+ },
7602
+ {
7603
+ key: "search-orders-by-item-name",
7604
+ label: "Searching orders matches the item name, and a nonsense term returns the filtered message",
7605
+ roles: ["buyer"],
7606
+ startPage: "/user/orders",
7607
+ steps: [
7608
+ "Sign in as a buyer with several orders and note one item's title.",
7609
+ "Search /user/orders for a distinctive word from that title.",
7610
+ "Now search for 'zzzznope'.",
7611
+ "Read the empty-state wording exactly.",
7612
+ ],
7613
+ inputs: { nonsenseTerm: "zzzznope" },
7614
+ expectedBehaviour: "The in-memory filter matches the item TITLES as well as the order id, and a filtered empty result is reported as filtered.",
7615
+ expectedUiState: "The item word returns that order. 'zzzznope' returns 'No orders match your search or filters.' — NOT 'You haven't placed any orders yet.'",
7616
+ expectedData: { nonsenseResults: 0 },
7617
+ endResult: "BEFORE: the route matched the order id only, so a buyer searching for what they bought got nothing. 🛑 The nonsense control is the point — a real term returns plausible rows whether or not the filter works.",
7618
+ },
7619
+ {
7620
+ key: "invoice-supplier-from-business-block",
7621
+ label: "The invoice names the registered entity from the business block",
7622
+ roles: ["buyer", "admin"],
7623
+ startPage: "/user/orders",
7624
+ steps: [
7625
+ "Open the invoice for a recent order and read the supplier block — legal name, GSTIN, address.",
7626
+ "Compare them against appkit.config.js's business block.",
7627
+ "Download the PDF invoice for the SAME order and compare it line for line with the HTML one.",
7628
+ ],
7629
+ expectedBehaviour: "resolveSupplierIdentity's platform branch reads the injected business identity, and both renderers read the snapshot stamped on the order at creation.",
7630
+ expectedUiState: "The HTML and PDF invoices agree, and the supplier block matches the business block.",
7631
+ endResult: "BEFORE: it read siteSettings.gst, making the invoice path a sixth owner of the statutory identity — and the one that decides what a tax invoice says. 🛑 The snapshot is unchanged, so correcting a typo must NOT reissue historical invoices.",
7632
+ },
7633
+ {
7634
+ key: "user-reviews-list-is-paginated",
7635
+ label: "The user reviews endpoint returns a page, not everything ever written",
7636
+ roles: ["buyer"],
7637
+ startPage: "/user/reviews",
7638
+ steps: [
7639
+ "Sign in as a buyer with several reviews.",
7640
+ "Call /api/user/reviews and read the response keys.",
7641
+ "Call it again with pageSize=1 and read how many reviews come back.",
7642
+ "Call it with pageSize=5000.",
7643
+ ],
7644
+ inputs: { pageSize: 1, oversizedPageSize: 5000 },
7645
+ expectedBehaviour: "The endpoint pages, and clamps pageSize to Rule #6's ceiling of 50.",
7646
+ expectedUiState: "The response carries reviews, total, page, pageSize and totalPages. pageSize=1 returns one review. pageSize=5000 returns at most 50.",
7647
+ expectedData: { maxPageSize: 50 },
7648
+ endResult: "BEFORE: it returned every review the account had ever written, unbounded, in one response.",
7649
+ },
7650
+ ],
7651
+ },
7652
+ ]),
7250
7653
  ];
7251
7654
  const defaultPhases = assignDefaultPhases(rawTesterChecklistItems);
7252
7655
  export const testerChecklistSeedData = rawTesterChecklistItems.map((item, index) => ({