@mohasinac/appkit 4.37.1 → 4.39.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 (172) hide show
  1. package/dist/_internal/client/features/digital-code/CodeRevealPanel.d.ts +20 -1
  2. package/dist/_internal/client/features/digital-code/CodeRevealPanel.js +21 -1
  3. package/dist/_internal/client/features/digital-code/DigitalContentPoolManager.d.ts +40 -0
  4. package/dist/_internal/client/features/digital-code/DigitalContentPoolManager.js +67 -0
  5. package/dist/_internal/server/features/bundles/og.d.ts +0 -1
  6. package/dist/_internal/server/features/bundles/og.js +2 -18
  7. package/dist/_internal/server/features/catalogue/og.d.ts +0 -1
  8. package/dist/_internal/server/features/catalogue/og.js +1 -9
  9. package/dist/_internal/server/features/checkout/actions.js +18 -0
  10. package/dist/_internal/server/features/classified/og.d.ts +0 -1
  11. package/dist/_internal/server/features/classified/og.js +1 -9
  12. package/dist/_internal/server/features/digital-code/download.d.ts +60 -0
  13. package/dist/_internal/server/features/digital-code/download.js +128 -0
  14. package/dist/_internal/server/features/digital-code/og.d.ts +0 -1
  15. package/dist/_internal/server/features/digital-code/og.js +1 -9
  16. package/dist/_internal/server/features/digital-code/pool.d.ts +101 -0
  17. package/dist/_internal/server/features/digital-code/pool.js +208 -0
  18. package/dist/_internal/server/features/digital-code/upload.d.ts +51 -0
  19. package/dist/_internal/server/features/digital-code/upload.js +144 -0
  20. package/dist/_internal/server/features/grouped/og.d.ts +7 -4
  21. package/dist/_internal/server/features/grouped/og.js +7 -6
  22. package/dist/_internal/server/features/item-requests/og.d.ts +0 -1
  23. package/dist/_internal/server/features/item-requests/og.js +2 -18
  24. package/dist/_internal/server/features/live/og.d.ts +0 -1
  25. package/dist/_internal/server/features/live/og.js +1 -9
  26. package/dist/_internal/server/features/prize-draws/og.js +5 -10
  27. package/dist/_internal/server/features/products/og.js +5 -2
  28. package/dist/_internal/server/features/seo/sitemap.js +10 -2
  29. package/dist/_internal/server/features/shared/listing-adapters.d.ts +1 -2
  30. package/dist/_internal/server/features/shared/listing-adapters.js +5 -3
  31. package/dist/_internal/server/jobs/core/cleanupRtdbEvents.js +40 -0
  32. package/dist/_internal/server/jobs/core/countersReconcile.js +153 -40
  33. package/dist/_internal/server/jobs/core/onCategoryWrite.js +33 -0
  34. package/dist/_internal/server/jobs/core/onProductWrite.js +93 -25
  35. package/dist/_internal/server/notifications/send-recorder.d.ts +79 -0
  36. package/dist/_internal/server/notifications/send-recorder.js +126 -0
  37. package/dist/_internal/shared/listing-types/_registry.d.ts +31 -4
  38. package/dist/_internal/shared/listing-types/art/config.d.ts +1 -0
  39. package/dist/_internal/shared/listing-types/art/config.js +2 -0
  40. package/dist/_internal/shared/listing-types/auction/config.d.ts +1 -0
  41. package/dist/_internal/shared/listing-types/auction/config.js +3 -0
  42. package/dist/_internal/shared/listing-types/classified/config.d.ts +1 -0
  43. package/dist/_internal/shared/listing-types/classified/config.js +3 -0
  44. package/dist/_internal/shared/listing-types/digital-code/config.d.ts +1 -0
  45. package/dist/_internal/shared/listing-types/digital-code/config.js +3 -0
  46. package/dist/_internal/shared/listing-types/live/config.d.ts +1 -0
  47. package/dist/_internal/shared/listing-types/live/config.js +3 -0
  48. package/dist/_internal/shared/listing-types/pre-order/config.d.ts +1 -0
  49. package/dist/_internal/shared/listing-types/pre-order/config.js +2 -0
  50. package/dist/_internal/shared/listing-types/prize-draw/config.d.ts +1 -0
  51. package/dist/_internal/shared/listing-types/prize-draw/config.js +3 -0
  52. package/dist/_internal/shared/listing-types/standard/config.d.ts +1 -0
  53. package/dist/_internal/shared/listing-types/standard/config.js +2 -0
  54. package/dist/_internal/shared/listing-types/stickers/config.d.ts +1 -0
  55. package/dist/_internal/shared/listing-types/stickers/config.js +2 -0
  56. package/dist/client/observability/reportClientError.js +11 -1
  57. package/dist/client.d.ts +6 -0
  58. package/dist/client.js +10 -0
  59. package/dist/core/hooks/useSyncManager.js +11 -1
  60. package/dist/features/account/hooks/useNotifications.js +25 -5
  61. package/dist/features/auctions/components/AuctionBottomActions.js +10 -2
  62. package/dist/features/auctions/components/AuctionDetailPageView.js +12 -3
  63. package/dist/features/auctions/components/LiveBidPrice.js +2 -2
  64. package/dist/features/auctions/components/LiveMinIncrement.d.ts +1 -1
  65. package/dist/features/auctions/components/LiveMinIncrement.js +8 -0
  66. package/dist/features/auctions/components/MarketplaceAuctionCard.js +3 -3
  67. package/dist/features/auctions/components/PlaceBidFormClient.js +16 -1
  68. package/dist/features/auctions/hooks/useAuctions.js +12 -1
  69. package/dist/features/auctions/hooks/useRealtimeBids.d.ts +3 -0
  70. package/dist/features/auctions/hooks/useRealtimeBids.js +138 -36
  71. package/dist/features/blog/components/BlogIndexPageView.js +10 -1
  72. package/dist/features/blog/repository/blog.repository.d.ts +14 -0
  73. package/dist/features/blog/repository/blog.repository.js +23 -1
  74. package/dist/features/catalogue/components/PublicCatalogueItemDetailView.js +2 -2
  75. package/dist/features/catalogue/components/PublicCatalogueView.js +2 -2
  76. package/dist/features/categories/components/BundleCollage.js +10 -5
  77. package/dist/features/categories/components/BundleDetailView.js +7 -3
  78. package/dist/features/categories/components/CategoryDetailPageView.js +1 -1
  79. package/dist/features/categories/components/CategoryProductsListing.js +4 -1
  80. package/dist/features/categories/components/CategoryTree.js +1 -1
  81. package/dist/features/categories/repository/categories.repository.d.ts +34 -2
  82. package/dist/features/categories/repository/categories.repository.js +31 -9
  83. package/dist/features/classified/components/ClassifiedDetailPageView.js +2 -2
  84. package/dist/features/contact/email.js +36 -1
  85. package/dist/features/digital-codes/components/DigitalCodeDetailPageView.js +2 -2
  86. package/dist/features/homepage/components/FeaturedBundlesSection.js +4 -4
  87. package/dist/features/live/components/LiveItemDetailPageView.js +2 -2
  88. package/dist/features/orders/repository/orders.repository.d.ts +21 -1
  89. package/dist/features/orders/repository/orders.repository.js +26 -9
  90. package/dist/features/pre-orders/components/MarketplacePreorderCard.js +3 -3
  91. package/dist/features/pre-orders/components/PreOrderActionsClient.js +2 -2
  92. package/dist/features/pre-orders/components/PreOrderDetailPageView.js +2 -2
  93. package/dist/features/pre-orders/components/PreOrdersIndexListing.js +4 -1
  94. package/dist/features/products/actions/product-actions.d.ts +9 -0
  95. package/dist/features/products/actions/product-actions.js +11 -0
  96. package/dist/features/products/columns/productTableColumns.js +2 -2
  97. package/dist/features/products/components/AuctionsIndexListing.js +4 -1
  98. package/dist/features/products/components/BidHistory.js +2 -2
  99. package/dist/features/products/components/CompareOverlay.js +2 -2
  100. package/dist/features/products/components/GroupMemberPicker.js +5 -5
  101. package/dist/features/products/components/ListingBottomActions.js +12 -3
  102. package/dist/features/products/components/MakeOfferButton.js +7 -1
  103. package/dist/features/products/components/MarketplaceBundleCard.js +4 -4
  104. package/dist/features/products/components/MarketplacePrizeDrawCard.js +2 -2
  105. package/dist/features/products/components/PrizeDrawCollage.js +16 -5
  106. package/dist/features/products/components/PrizeDrawEntryActions.js +2 -2
  107. package/dist/features/products/components/PrizeDrawsIndexListing.js +4 -1
  108. package/dist/features/products/components/PrizeRevealModal.js +2 -2
  109. package/dist/features/products/components/ProductDetailActions.js +10 -3
  110. package/dist/features/products/components/ProductFilters.js +2 -2
  111. package/dist/features/products/components/ProductGrid.js +5 -2
  112. package/dist/features/products/components/ProductsIndexListing.js +12 -1
  113. package/dist/features/products/components/ProductsIndexPageView.js +26 -0
  114. package/dist/features/products/components/ShowGroupSection.js +2 -2
  115. package/dist/features/products/components/SublistingCarouselSection.js +2 -2
  116. package/dist/features/products/constants/listing-tabs.d.ts +36 -12
  117. package/dist/features/products/constants/listing-tabs.js +38 -14
  118. package/dist/features/products/constants/sieve.d.ts +14 -0
  119. package/dist/features/products/constants/sieve.js +39 -0
  120. package/dist/features/products/repository/products.repository.d.ts +35 -0
  121. package/dist/features/products/repository/products.repository.js +116 -3
  122. package/dist/features/products/schemas/firestore.d.ts +37 -0
  123. package/dist/features/seller/actions/seller-actions.js +4 -13
  124. package/dist/features/seller/components/SellerProductShell.d.ts +7 -1
  125. package/dist/features/seller/components/SellerProductShell.js +11 -2
  126. package/dist/features/stores/api/[storeSlug]/reviews/route.d.ts +6 -1
  127. package/dist/features/stores/api/[storeSlug]/reviews/route.js +89 -30
  128. package/dist/features/stores/components/StoresIndexPageView.js +17 -1
  129. package/dist/features/tester/seed-data/authored/admin__users-trust.js +1 -1
  130. package/dist/features/tester/seed-data/authored/buying__bidding.js +5 -4
  131. package/dist/features/tester/seed-data/authored/buying__browsing-search.js +85 -85
  132. package/dist/features/tester/seed-data/authored/buying__image-tile-layout.js +2 -2
  133. package/dist/features/tester/seed-data/authored/buying__offers.js +1 -1
  134. package/dist/features/tester/seed-data/authored/buying__product-detail.js +53 -15
  135. package/dist/features/tester/seed-data/authored/buying__user-dashboard-extras.js +6 -6
  136. package/dist/features/tester/seed-data/authored/content-discovery__search.js +6 -4
  137. package/dist/features/tester/seed-data/authored/design-ux__carousel-arrow-bounds.js +4 -4
  138. package/dist/features/tester/seed-data/authored/design-ux__homepage-carousels.js +2 -2
  139. package/dist/features/tester/seed-data/authored/design-ux__status-badge-legibility.js +4 -4
  140. package/dist/features/tester/seed-data/authored/design-ux__sticky-cta-bar.js +8 -8
  141. package/dist/features/tester/seed-data/authored/public-pages__core-listing-pages.js +8 -8
  142. package/dist/features/tester/seed-data/authored/seo__canonical-and-host.js +16 -0
  143. package/dist/features/tester/seed-data/authored/seo__og-images.js +17 -0
  144. package/dist/features/tester/seed-data/stores-tester-seed-data.js +41 -2
  145. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +36 -6
  146. package/dist/features/tester/utils/phases.js +78 -12
  147. package/dist/features/wishlist/components/WishlistPage.js +2 -2
  148. package/dist/index.d.ts +2 -1
  149. package/dist/index.js +4 -1
  150. package/dist/providers/db-firebase/rtdb-paths.d.ts +7 -0
  151. package/dist/providers/db-firebase/rtdb-paths.js +7 -0
  152. package/dist/react/contexts/SessionContext.js +40 -1
  153. package/dist/react/hooks/useCanSeePrices.d.ts +37 -0
  154. package/dist/react/hooks/useCanSeePrices.js +40 -0
  155. package/dist/seed/addresses-seed-data.js +36 -2
  156. package/dist/seed/users-seed-data.js +10 -2
  157. package/dist/seo/json-ld.d.ts +21 -0
  158. package/dist/seo/json-ld.js +45 -0
  159. package/dist/server.d.ts +3 -0
  160. package/dist/server.js +12 -0
  161. package/dist/styles.css +41 -0
  162. package/dist/ui/components/FilePickerButton.d.ts +32 -0
  163. package/dist/ui/components/FilePickerButton.js +23 -0
  164. package/dist/ui/components/GatedPrice.d.ts +66 -0
  165. package/dist/ui/components/GatedPrice.js +80 -0
  166. package/dist/ui/components/GatedPrice.style.css +40 -0
  167. package/dist/ui/components/PriceDisplay.d.ts +12 -0
  168. package/dist/ui/components/PriceDisplay.js +17 -17
  169. package/dist/ui/components/index.style.css +1 -0
  170. package/dist/ui/index.d.ts +4 -0
  171. package/dist/ui/index.js +8 -0
  172. package/package.json +1 -1
@@ -2,7 +2,7 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import { useEffect, useState } from "react";
4
4
  import Link from "next/link";
5
- import { Div, Row, Span, Text, Modal, SideDrawer, Button } from "../../../ui";
5
+ import { Div, PricesOnly, Row, Span, Text, Modal, SideDrawer, Button } from "../../../ui";
6
6
  import { MediaImage } from "../../media/MediaImage";
7
7
  import { GroupMemberPicker } from "./GroupMemberPicker";
8
8
  import { formatCurrency } from "../../../utils/number.formatter";
@@ -23,7 +23,7 @@ function MemberThumb({ member, isCurrent }) {
23
23
  const price = formatCurrency(member.price, member.currency ?? "INR");
24
24
  return (_jsxs(Link, { href: href, "aria-label": member.title, className: "flex flex-col items-center gap-[var(--appkit-space-1-5)] flex-shrink-0 w-16 group", children: [_jsxs(Div, { rounded: "full", className: `relative w-14 h-14 overflow-hidden border-2 transition-all ${isCurrent
25
25
  ? "border-[var(--appkit-color-primary)] ring-2 ring-[var(--appkit-color-primary)]/30"
26
- : "border-[var(--appkit-color-border)] group-hover:border-[var(--appkit-color-primary)]"}`, children: [_jsx(MediaImage, { src: image, alt: member.title, size: "card" }), member.isGroupParent && (_jsx(Span, { color: "inverse", rounded: "sm", className: "absolute bottom-0 right-0 bg-[var(--appkit-color-primary)] text-[8px] leading-none px-[0.25rem] py-[0.125rem]", children: "Set" }))] }), _jsx(Text, { className: "text-[10px] leading-tight line-clamp-2 w-full", color: "muted", align: "start", children: member.title }), _jsx(Text, { className: "text-[10px]", color: "primary", weight: "semibold", children: price })] }));
26
+ : "border-[var(--appkit-color-border)] group-hover:border-[var(--appkit-color-primary)]"}`, children: [_jsx(MediaImage, { src: image, alt: member.title, size: "card" }), member.isGroupParent && (_jsx(Span, { color: "inverse", rounded: "sm", className: "absolute bottom-0 right-0 bg-[var(--appkit-color-primary)] text-[8px] leading-none px-[0.25rem] py-[0.125rem]", children: "Set" }))] }), _jsx(Text, { className: "text-[10px] leading-tight line-clamp-2 w-full", color: "muted", align: "start", children: member.title }), _jsx(Text, { className: "text-[10px]", color: "primary", weight: "semibold", children: _jsx(PricesOnly, { children: price }) })] }));
27
27
  }
28
28
  export function ShowGroupSection({ groupId, currentSlug, isParent, groupTitle }) {
29
29
  const [open, setOpen] = useState(false);
@@ -3,7 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useEffect, useState } from "react";
4
4
  import Link from "next/link";
5
5
  import { ROUTES } from "../../../next";
6
- import { Div, Row, Span, Text } from "../../../ui";
6
+ import { Div, PricesOnly, Row, Span, Text } from "../../../ui";
7
7
  import { MediaImage } from "../../media/MediaImage";
8
8
  import { formatCurrency } from "../../../utils/number.formatter";
9
9
  import { normalizeListingType } from "../utils/listing-type";
@@ -23,7 +23,7 @@ function ListingThumb({ listing, isCurrent, }) {
23
23
  const price = formatCurrency(listing.price, listing.currency ?? "INR");
24
24
  return (_jsxs(Link, { href: href, "aria-label": listing.title, className: "flex flex-col items-center gap-[var(--appkit-space-1-5)] flex-shrink-0 w-16 group", children: [_jsx(Div, { rounded: "full", className: `relative w-14 h-14 overflow-hidden border-2 transition-all ${isCurrent
25
25
  ? "border-[var(--appkit-color-primary)] ring-2 ring-[var(--appkit-color-primary)]/30"
26
- : "border-[var(--appkit-color-border)] group-hover:border-[var(--appkit-color-primary)]"}`, children: image ? (_jsx(MediaImage, { src: image, alt: listing.title, size: "avatar" })) : (_jsx(Row, { textSize: "xs", color: "faint", className: "w-full h-full", surface: "subtle", align: "center", justify: "center", children: "\u00E2\u2014\u00AF" })) }), _jsx(Text, { className: "text-[10px] leading-tight line-clamp-2 w-full", color: "muted", align: "start", children: listing.title }), _jsx(Text, { className: "text-[10px]", color: "primary", weight: "semibold", children: price })] }));
26
+ : "border-[var(--appkit-color-border)] group-hover:border-[var(--appkit-color-primary)]"}`, children: image ? (_jsx(MediaImage, { src: image, alt: listing.title, size: "avatar" })) : (_jsx(Row, { textSize: "xs", color: "faint", className: "w-full h-full", surface: "subtle", align: "center", justify: "center", children: "\u00E2\u2014\u00AF" })) }), _jsx(Text, { className: "text-[10px] leading-tight line-clamp-2 w-full", color: "muted", align: "start", children: listing.title }), _jsx(Text, { className: "text-[10px]", color: "primary", weight: "semibold", children: _jsx(PricesOnly, { children: price }) })] }));
27
27
  }
28
28
  export function SublistingCarouselSection({ sublistingCategoryId, currentListingId }) {
29
29
  const [open, setOpen] = useState(false);
@@ -33,16 +33,33 @@ export interface ListingTab {
33
33
  entity?: "stores";
34
34
  }
35
35
  /**
36
- * The generic `/products` browse page spans EVERY listing type (2026-08-21).
36
+ * The types the generic `/products` browse page spans — the GENERAL CATALOGUE,
37
+ * not an index of everything.
37
38
  *
38
- * It used to span only `standard|classified|digital-code|live` on the theory
39
- * that auctions/pre-orders/prize-draws/art each had a dedicated page — but
40
- * that made five of nine types unreachable from the main catalogue, broke
41
- * seven legacy `/search/.../tab/<type>` permanent redirects that all point at
42
- * `/products`, and made the cross-type Compare + bulk-cart features (which
43
- * only exist on this page) unusable for those types. The dedicated pages
44
- * remain, and `/products` links to them when a single time-boxed type is
45
- * selected.
39
+ * DERIVED from `ListingTypePlugin.inGeneralCatalogue`, never hand-written: a
40
+ * literal array here is how ten enumerations of one union drifted apart in
41
+ * Root Cause #61, and `audit-listing-type-tab-coverage`'s STAYS_DERIVED rule
42
+ * blocks one.
43
+ *
44
+ * ## History — this spanned all nine types from 2026-08-21 until now
45
+ *
46
+ * The widening was a fix for Root Cause #61 and cited three reasons. Two have
47
+ * since been solved elsewhere, and the third is an accepted cost:
48
+ *
49
+ * 1. "Five of nine types unreachable from the main catalogue." Still true for
50
+ * `classified`/`digital-code`/`live`, which have dedicated pages but no
51
+ * `MAIN_NAV_ITEMS` entry — which is exactly why `inGeneralCatalogue` keeps
52
+ * those three here. The other five are all in the main nav.
53
+ * 2. "Seven legacy `/search/.../tab/<type>` redirects all point at /products."
54
+ * No longer true: that route derives `tabSlug -> browseRoute` from this
55
+ * same registry, and `/search` maps every resource type to its own page.
56
+ * Neither falls through to `/products` for a typed request.
57
+ * 3. "Cross-type Compare + bulk-cart only exist on this page." Still true, and
58
+ * accepted: you can no longer compare an auction against a standard product.
59
+ *
60
+ * A type appearing both here and in the main nav is the bug this fixes — the
61
+ * same item reachable from two places with two different chromes (an auction
62
+ * card in the general grid renders Buy-Now chrome, not bid affordances).
46
63
  */
47
64
  export declare const GENERIC_PRODUCT_LISTING_TYPES: readonly ListingType[];
48
65
  /** The listing types the combined `/art` page spans. */
@@ -60,9 +77,16 @@ export type SellerListingTabId = string;
60
77
  * In-page type-filter chips on the generic `/products` browse page.
61
78
  *
62
79
  * Multi-select (checkbox semantics) — no "All" sentinel chip, because "none
63
- * checked" already means "every type". A sentinel would be a second way to
64
- * express the same state and would have to be cleared whenever a real chip
65
- * was ticked.
80
+ * checked" already means "every type IN THE GENERAL CATALOGUE". A sentinel
81
+ * would be a second way to express the same state and would have to be cleared
82
+ * whenever a real chip was ticked.
83
+ *
84
+ * 🛑 Derived from `GENERIC_PRODUCT_LISTING_TYPES`, NOT `ALL_LISTING_TYPES`. The
85
+ * chips must offer exactly the types the page can actually return: a chip for a
86
+ * type outside the span intersects to empty, and `listPublicProducts` then falls
87
+ * back to the full span — i.e. ticking it would silently widen the results
88
+ * instead of narrowing them. `ADMIN_PRODUCT_LISTING_TYPE_TABS` is a separate
89
+ * constant and correctly stays all-nine.
66
90
  */
67
91
  export declare const PRODUCT_TYPE_FILTER_TABS: readonly ListingTab[];
68
92
  /** In-page type-filter chips on the combined `/art` (Art & Stickers) page. */
@@ -22,18 +22,35 @@
22
22
  import { ALL_LISTING_TYPES } from "../../../_internal/shared/listing-types/feature-flags";
23
23
  import { pluginFor } from "../../../_internal/shared/listing-types/_registry";
24
24
  /**
25
- * The generic `/products` browse page spans EVERY listing type (2026-08-21).
25
+ * The types the generic `/products` browse page spans — the GENERAL CATALOGUE,
26
+ * not an index of everything.
26
27
  *
27
- * It used to span only `standard|classified|digital-code|live` on the theory
28
- * that auctions/pre-orders/prize-draws/art each had a dedicated page — but
29
- * that made five of nine types unreachable from the main catalogue, broke
30
- * seven legacy `/search/.../tab/<type>` permanent redirects that all point at
31
- * `/products`, and made the cross-type Compare + bulk-cart features (which
32
- * only exist on this page) unusable for those types. The dedicated pages
33
- * remain, and `/products` links to them when a single time-boxed type is
34
- * selected.
28
+ * DERIVED from `ListingTypePlugin.inGeneralCatalogue`, never hand-written: a
29
+ * literal array here is how ten enumerations of one union drifted apart in
30
+ * Root Cause #61, and `audit-listing-type-tab-coverage`'s STAYS_DERIVED rule
31
+ * blocks one.
32
+ *
33
+ * ## History — this spanned all nine types from 2026-08-21 until now
34
+ *
35
+ * The widening was a fix for Root Cause #61 and cited three reasons. Two have
36
+ * since been solved elsewhere, and the third is an accepted cost:
37
+ *
38
+ * 1. "Five of nine types unreachable from the main catalogue." Still true for
39
+ * `classified`/`digital-code`/`live`, which have dedicated pages but no
40
+ * `MAIN_NAV_ITEMS` entry — which is exactly why `inGeneralCatalogue` keeps
41
+ * those three here. The other five are all in the main nav.
42
+ * 2. "Seven legacy `/search/.../tab/<type>` redirects all point at /products."
43
+ * No longer true: that route derives `tabSlug -> browseRoute` from this
44
+ * same registry, and `/search` maps every resource type to its own page.
45
+ * Neither falls through to `/products` for a typed request.
46
+ * 3. "Cross-type Compare + bulk-cart only exist on this page." Still true, and
47
+ * accepted: you can no longer compare an auction against a standard product.
48
+ *
49
+ * A type appearing both here and in the main nav is the bug this fixes — the
50
+ * same item reachable from two places with two different chromes (an auction
51
+ * card in the general grid renders Buy-Now chrome, not bid affordances).
35
52
  */
36
- export const GENERIC_PRODUCT_LISTING_TYPES = ALL_LISTING_TYPES;
53
+ export const GENERIC_PRODUCT_LISTING_TYPES = ALL_LISTING_TYPES.filter((t) => pluginFor(t).inGeneralCatalogue);
37
54
  /** The listing types the combined `/art` page spans. */
38
55
  export const ART_STICKERS_LISTING_TYPES = ["art", "stickers"];
39
56
  /**
@@ -87,10 +104,17 @@ export const SELLER_LISTING_TABS = [
87
104
  * In-page type-filter chips on the generic `/products` browse page.
88
105
  *
89
106
  * Multi-select (checkbox semantics) — no "All" sentinel chip, because "none
90
- * checked" already means "every type". A sentinel would be a second way to
91
- * express the same state and would have to be cleared whenever a real chip
92
- * was ticked.
107
+ * checked" already means "every type IN THE GENERAL CATALOGUE". A sentinel
108
+ * would be a second way to express the same state and would have to be cleared
109
+ * whenever a real chip was ticked.
110
+ *
111
+ * 🛑 Derived from `GENERIC_PRODUCT_LISTING_TYPES`, NOT `ALL_LISTING_TYPES`. The
112
+ * chips must offer exactly the types the page can actually return: a chip for a
113
+ * type outside the span intersects to empty, and `listPublicProducts` then falls
114
+ * back to the full span — i.e. ticking it would silently widen the results
115
+ * instead of narrowing them. `ADMIN_PRODUCT_LISTING_TYPE_TABS` is a separate
116
+ * constant and correctly stays all-nine.
93
117
  */
94
- export const PRODUCT_TYPE_FILTER_TABS = ALL_LISTING_TYPES.map(chipTab);
118
+ export const PRODUCT_TYPE_FILTER_TABS = GENERIC_PRODUCT_LISTING_TYPES.map(chipTab);
95
119
  /** In-page type-filter chips on the combined `/art` (Art & Stickers) page. */
96
120
  export const ART_STICKERS_TYPE_FILTER_TABS = ART_STICKERS_LISTING_TYPES.map(chipTab);
@@ -16,6 +16,20 @@ export declare const BASE_SORT_OPTIONS: readonly [{
16
16
  readonly value: string;
17
17
  readonly label: "Price: High to Low";
18
18
  }];
19
+ /**
20
+ * Drop the price sorts when the viewer may not see prices.
21
+ *
22
+ * Ordering a list by price discloses the ranking of every hidden amount, and
23
+ * with paging that is most of the way to a price list — so the option is not
24
+ * offered to signed-out visitors, matching the hidden price-range facet.
25
+ *
26
+ * 🛑 This removes the CONTROL, not the capability. A hand-written
27
+ * `?sort=price:asc` still sorts, exactly as `?f=price>N` still filters — both
28
+ * are the documented, accepted residue of gating the render rather than
29
+ * withholding the field (see CLAUDE.md § "Public Data Projections"). What it
30
+ * buys is that no casual visitor is handed the lever.
31
+ */
32
+ export declare function withoutPriceSorts(options: readonly SortOption[], canSeePrices: boolean): readonly SortOption[];
19
33
  export declare const STANDARD_SORT_OPTIONS: readonly [{
20
34
  readonly value: string;
21
35
  readonly label: "Newest First";
@@ -19,6 +19,45 @@ const BASE_PRICE_SORTS = [
19
19
  ];
20
20
  /** Time + price sorts, the common subset across most listing-type pickers. */
21
21
  export const BASE_SORT_OPTIONS = [...BASE_TIME_SORTS, ...BASE_PRICE_SORTS];
22
+ /**
23
+ * Drop the price sorts when the viewer may not see prices.
24
+ *
25
+ * Ordering a list by price discloses the ranking of every hidden amount, and
26
+ * with paging that is most of the way to a price list — so the option is not
27
+ * offered to signed-out visitors, matching the hidden price-range facet.
28
+ *
29
+ * 🛑 This removes the CONTROL, not the capability. A hand-written
30
+ * `?sort=price:asc` still sorts, exactly as `?f=price>N` still filters — both
31
+ * are the documented, accepted residue of gating the render rather than
32
+ * withholding the field (see CLAUDE.md § "Public Data Projections"). What it
33
+ * buys is that no casual visitor is handed the lever.
34
+ */
35
+ export function withoutPriceSorts(options, canSeePrices) {
36
+ if (canSeePrices)
37
+ return options;
38
+ // Exact match on the field, both directions. `sortBy()` encodes direction as
39
+ // a leading "-", so stripping it yields the bare field name — substring
40
+ // matching would catch an unrelated `pricePerEntry` sort by accident.
41
+ return options.filter((o) => !MONEY_SORT_FIELDS.includes(o.value.replace(/^-/, "")));
42
+ }
43
+ /**
44
+ * Every sort field that orders by an AMOUNT, matched as a substring of the
45
+ * sort value (`sortBy()` emits `field` / `-field`, so one entry covers both
46
+ * directions).
47
+ *
48
+ * `currentBid`, `startingBid` and `buyNowPrice` are here for the same reason
49
+ * `price` is: "Highest Current Bid" ranks every hidden bid on the page, which
50
+ * is the disclosure the gate exists to prevent. `bidCount` is deliberately
51
+ * ABSENT — the number of bids is not an amount, it is already displayed
52
+ * ungated beside every auction, and hiding it would remove a real signal for
53
+ * no gain.
54
+ */
55
+ const MONEY_SORT_FIELDS = [
56
+ PRODUCT_FIELDS.PRICE,
57
+ PRODUCT_FIELDS.CURRENT_BID,
58
+ PRODUCT_FIELDS.STARTING_BID,
59
+ PRODUCT_FIELDS.BUY_NOW_PRICE,
60
+ ];
22
61
  // ---------------------------------------------------------------------------
23
62
  // Standard Products
24
63
  // ---------------------------------------------------------------------------
@@ -12,6 +12,41 @@ export declare class ProductRepository extends BaseRepository<ProductDocument> {
12
12
  private cacheInvalidateForId;
13
13
  findById(id: string): Promise<ProductDocument | null>;
14
14
  protected mapDoc<D = ProductDocument>(snap: DocumentSnapshot): D;
15
+ /**
16
+ * Resolve a product's taxonomy into the shape every READ path expects:
17
+ * `categorySlugs` as the FULL ancestor chain (self first, root last), and a
18
+ * brand recorded as both its id and its display name.
19
+ *
20
+ * 🛑 WHY THIS LIVES IN THE REPOSITORY. The seller and admin create/update
21
+ * paths send a SINGLE category id (`category: "category-x-starters"`) — and
22
+ * `productCreateSchema` (src/validation/request-schemas.ts:129) declares only
23
+ * that scalar, with no `.passthrough()`, so an inbound `categorySlugs` array
24
+ * is stripped before it ever reaches Firestore. `mapDoc` then backfills
25
+ * `categorySlugs = [category]` **on read only**, which is invisible to a
26
+ * query: the category page runs `array-contains-any` against the STORED
27
+ * field, so a UI-created listing was unreachable from its own leaf category
28
+ * and from every ancestor of it. CLAUDE.md recorded the ancestor half of this
29
+ * as an outstanding follow-up; the leaf half was never noticed because every
30
+ * product in the database came from the seed, which hand-writes the chain
31
+ * (measured 2026-09-14: 95/95 products carried a correct chain, so the defect
32
+ * is LATENT — it fires the first time anyone creates a listing through the
33
+ * UI, which is precisely why no page looks broken today).
34
+ *
35
+ * There are ~14 independent write paths — two seller actions, two admin
36
+ * actions, four in `_internal/server/features/products/actions.ts`, the
37
+ * duplicate route, group-children, catalogue promotion, shipment linking and
38
+ * the WhatsApp import. Deriving at each one is Root Cause #75's shape: the
39
+ * copy made before a branch was added never learns about it. `create` and
40
+ * `update` are the only two methods every one of them funnels through.
41
+ *
42
+ * 🛑 It reads `categories` through `this.db` rather than importing
43
+ * `categoriesRepository`. No repository in this codebase imports another
44
+ * feature's repository, and the reason is the Turbopack import-chain trap
45
+ * (Root Cause #6/#18) — a raw collection read has neither problem.
46
+ */
47
+ private deriveTaxonomy;
48
+ /** The keys whose presence means a write could change the product's taxonomy. */
49
+ private static readonly TAXONOMY_KEYS;
15
50
  update(id: string, data: Partial<ProductDocument>): Promise<ProductDocument>;
16
51
  create(input: ProductCreateInput): Promise<ProductDocument>;
17
52
  findByStore(storeId: string): Promise<ProductDocument[]>;
@@ -10,6 +10,9 @@ import { matchesAllSearchTerms, parseSearchTxtQuery, } from "../../../utils/sear
10
10
  // wrapper. Three copies existed and two were missing the same three fields.
11
11
  import { buildProductSearchTxt } from "../../../utils/search-txt-builders";
12
12
  import { PRODUCT_COLLECTION, ProductStatusValues } from "../schemas";
13
+ // Collection NAME only — deliberately not `categoriesRepository`. See
14
+ // deriveTaxonomy()'s header for why this is a raw read.
15
+ import { CATEGORIES_COLLECTION } from "../../categories/schemas/firestore";
13
16
  import { PRODUCT_FIELDS } from "../../../constants/field-names";
14
17
  /**
15
18
  * Canonical listing-kind tokens used in `listingType`. Note: the public Sieve
@@ -92,12 +95,109 @@ export class ProductRepository extends BaseRepository {
92
95
  }
93
96
  return doc;
94
97
  }
98
+ /**
99
+ * Resolve a product's taxonomy into the shape every READ path expects:
100
+ * `categorySlugs` as the FULL ancestor chain (self first, root last), and a
101
+ * brand recorded as both its id and its display name.
102
+ *
103
+ * 🛑 WHY THIS LIVES IN THE REPOSITORY. The seller and admin create/update
104
+ * paths send a SINGLE category id (`category: "category-x-starters"`) — and
105
+ * `productCreateSchema` (src/validation/request-schemas.ts:129) declares only
106
+ * that scalar, with no `.passthrough()`, so an inbound `categorySlugs` array
107
+ * is stripped before it ever reaches Firestore. `mapDoc` then backfills
108
+ * `categorySlugs = [category]` **on read only**, which is invisible to a
109
+ * query: the category page runs `array-contains-any` against the STORED
110
+ * field, so a UI-created listing was unreachable from its own leaf category
111
+ * and from every ancestor of it. CLAUDE.md recorded the ancestor half of this
112
+ * as an outstanding follow-up; the leaf half was never noticed because every
113
+ * product in the database came from the seed, which hand-writes the chain
114
+ * (measured 2026-09-14: 95/95 products carried a correct chain, so the defect
115
+ * is LATENT — it fires the first time anyone creates a listing through the
116
+ * UI, which is precisely why no page looks broken today).
117
+ *
118
+ * There are ~14 independent write paths — two seller actions, two admin
119
+ * actions, four in `_internal/server/features/products/actions.ts`, the
120
+ * duplicate route, group-children, catalogue promotion, shipment linking and
121
+ * the WhatsApp import. Deriving at each one is Root Cause #75's shape: the
122
+ * copy made before a branch was added never learns about it. `create` and
123
+ * `update` are the only two methods every one of them funnels through.
124
+ *
125
+ * 🛑 It reads `categories` through `this.db` rather than importing
126
+ * `categoriesRepository`. No repository in this codebase imports another
127
+ * feature's repository, and the reason is the Turbopack import-chain trap
128
+ * (Root Cause #6/#18) — a raw collection read has neither problem.
129
+ */
130
+ async deriveTaxonomy(input) {
131
+ const out = {};
132
+ const cats = this.db.collection(CATEGORIES_COLLECTION);
133
+ const leaf = (Array.isArray(input.categorySlugs) && input.categorySlugs[0]) ||
134
+ input.category ||
135
+ null;
136
+ /*
137
+ * The brand selector's option value is the brand ROW's id (`brand-beyblade`),
138
+ * but `ProductDocument.brand` is matched by DISPLAY NAME —
139
+ * `BrandDetailPageView` filters `sieveFilter("brand", EQ, brand.name)`, and
140
+ * CLAUDE.md's § "Brand matching is by DISPLAY NAME" says so explicitly. So a
141
+ * seller-created listing stored `brand: "brand-beyblade"` while every seeded
142
+ * one stored `brand: "Beyblade"`, and the brand page matched neither the new
143
+ * row nor counted it. Resolving the id back to its name fixes that; a value
144
+ * that is ALREADY a display name does not resolve as a document id and is
145
+ * left exactly as it was.
146
+ */
147
+ const brandRef = input.brandSlug || input.brand || null;
148
+ const [catSnap, brandSnap] = await Promise.all([
149
+ leaf ? cats.doc(leaf).get() : Promise.resolve(null),
150
+ brandRef ? cats.doc(brandRef).get() : Promise.resolve(null),
151
+ ]);
152
+ if (catSnap?.exists) {
153
+ const cat = catSnap.data();
154
+ // `parentIds` is ordered root-first / nearest-last (category-tree.ts:107),
155
+ // and the stored chain is self-first / root-last. Reversing is what makes
156
+ // the derived value byte-identical to what the seed hand-writes — verified
157
+ // against products-standard-seed-data.ts:197.
158
+ out.categorySlugs = [leaf, ...(cat.parentIds ?? []).slice().reverse()];
159
+ out.category = leaf;
160
+ const names = [
161
+ cat.name,
162
+ ...(cat.ancestors ?? []).map((a) => a?.name).reverse(),
163
+ ].filter((n) => !!n);
164
+ if (names.length)
165
+ out.categoryNames = names;
166
+ }
167
+ else if (leaf) {
168
+ // Never destroy a chain we cannot improve on. A leaf that is not a real
169
+ // category row is a data problem to report, not a reason to blank the field.
170
+ serverLogger.warn("Product category id does not resolve to a category", {
171
+ leaf,
172
+ });
173
+ }
174
+ if (brandSnap?.exists) {
175
+ const brand = brandSnap.data();
176
+ if (brand.categoryType === "brand") {
177
+ out.brandSlug = brandRef;
178
+ if (brand.name)
179
+ out.brand = brand.name;
180
+ }
181
+ }
182
+ return out;
183
+ }
95
184
  async update(id, data) {
96
185
  this.cacheInvalidateForId(id);
97
186
  const current = await super.findById(id);
98
- const merged = { ...current, ...data };
187
+ /*
188
+ * Only re-derive when the write actually names a taxonomy field. Every
189
+ * stock decrement, view increment and status flip would otherwise pay two
190
+ * extra Firestore reads, which Rule #6 budgets at ~3 round-trips for a
191
+ * whole request.
192
+ */
193
+ const touchesTaxonomy = ProductRepository.TAXONOMY_KEYS.some((k) => k in data);
194
+ const derived = touchesTaxonomy
195
+ ? await this.deriveTaxonomy({ ...current, ...data })
196
+ : {};
197
+ const merged = { ...current, ...data, ...derived };
99
198
  const updated = await super.update(id, {
100
199
  ...data,
200
+ ...derived,
101
201
  searchTxt: buildProductSearchTxt(merged),
102
202
  });
103
203
  this.cacheSet(updated);
@@ -117,12 +217,18 @@ export class ProductRepository extends BaseRepository {
117
217
  }
118
218
  });
119
219
  const barcodeId = input.barcodeId ?? await generateBarcodeId(id);
220
+ // Always derive on create — one extra read per listing created, and it is
221
+ // what makes a UI-created product reachable from its own category page at
222
+ // all. A seed row already carrying the correct chain derives to the same
223
+ // value, so this is a no-op for every existing fixture.
224
+ const derived = await this.deriveTaxonomy(input);
225
+ const withTaxonomy = { ...input, ...derived };
120
226
  const productData = {
121
- ...input,
227
+ ...withTaxonomy,
122
228
  barcodeId,
123
229
  slug: id,
124
230
  availableQuantity: input.stockQuantity,
125
- searchTxt: buildProductSearchTxt(input),
231
+ searchTxt: buildProductSearchTxt(withTaxonomy),
126
232
  createdAt: new Date(),
127
233
  updatedAt: new Date(),
128
234
  };
@@ -525,6 +631,13 @@ export class ProductRepository extends BaseRepository {
525
631
  }
526
632
  }
527
633
  ProductRepository.CACHE_TTL_MS = 30000;
634
+ /** The keys whose presence means a write could change the product's taxonomy. */
635
+ ProductRepository.TAXONOMY_KEYS = [
636
+ "categorySlugs",
637
+ "category",
638
+ "brand",
639
+ "brandSlug",
640
+ ];
528
641
  ProductRepository.SIEVE_FIELDS = {
529
642
  id: { canFilter: true, canSort: false },
530
643
  title: { canFilter: true, canSort: true },
@@ -429,9 +429,46 @@ export type ProductUpdateInput = Partial<Pick<ProductDocument, (typeof PRODUCT_U
429
429
  export type ProductAdminUpdateInput = Partial<Omit<ProductDocument, "id" | "createdAt">>;
430
430
  export declare const PRODUCT_CODES_SUBCOLLECTION: "codes";
431
431
  export type ProductCodeStatus = "available" | "claimed" | "revoked";
432
+ /**
433
+ * What the buyer actually receives. A "digital code" listing delivers digital
434
+ * CONTENT — a redemption string is only the commonest shape of it.
435
+ *
436
+ * `"code"` is the default and is what every pre-existing document is, so no
437
+ * migration is needed and no existing listing changes behaviour.
438
+ */
439
+ export type ProductCodeContentKind = "code" | "image" | "file";
432
440
  export interface ProductCodeDocument extends BaseDocument {
433
441
  productId: string;
442
+ /**
443
+ * The redemption string. Required for `contentKind: "code"`, empty for the
444
+ * asset kinds — a QR is not a string.
445
+ */
434
446
  code: string;
447
+ /** Defaults to `"code"` when absent. */
448
+ contentKind?: ProductCodeContentKind;
449
+ /**
450
+ * 🛑 A RAW FIREBASE STORAGE PATH, NEVER a `/media/{slug}` URL, and never a
451
+ * signed URL.
452
+ *
453
+ * Two facts make this load-bearing:
454
+ * - `storage.rules` is `allow read: if true` for every object in the
455
+ * bucket, and
456
+ * - `GET /api/media/[...slug]` applies NO authentication at all — it is a
457
+ * bare exported handler with no session read, by design, because it
458
+ * exists to serve public product photos through a watermarker.
459
+ *
460
+ * So anything reachable at a `/media/` slug is world-readable, and image
461
+ * filenames in this codebase are content-derived and therefore guessable.
462
+ * Putting a paid QR there would publish it. Instead the bytes are streamed by
463
+ * `GET /api/orders/{orderId}/code/asset`, which re-checks that the caller
464
+ * owns the order — the same shape as the invoice route, the one existing
465
+ * precedent for a gated download here.
466
+ */
467
+ assetPath?: string;
468
+ /** Original upload name, used for the `Content-Disposition` filename. */
469
+ fileName?: string;
470
+ /** Verified by magic bytes at upload, replayed as the response Content-Type. */
471
+ contentType?: string;
435
472
  status: ProductCodeStatus;
436
473
  orderId?: string;
437
474
  claimedByUserId?: string;
@@ -448,19 +448,10 @@ export async function listSellerOrders(userId, params) {
448
448
  hasMore: false,
449
449
  };
450
450
  }
451
- const sellerProducts = await productRepository.findByStore(store.id);
452
- const productIds = sellerProducts.map((p) => p.id);
453
- if (productIds.length === 0) {
454
- return {
455
- items: [],
456
- total: 0,
457
- page: 1,
458
- pageSize: params?.pageSize ?? 20,
459
- totalPages: 0,
460
- hasMore: false,
461
- };
462
- }
463
- return orderRepository.listForSeller(productIds, {
451
+ // No products query: orders are scoped by `storeId`, the field a cart is
452
+ // split on. Fetching every product to build a `productId in [...]` clause was
453
+ // both an extra round trip and a hard 30-value Firestore cap.
454
+ return orderRepository.listForSeller(store.id, {
464
455
  filters: params?.filters,
465
456
  sorts: params?.sorts ?? "-createdAt",
466
457
  page: params?.page ?? 1,
@@ -94,6 +94,12 @@ export interface SellerProductShellProps {
94
94
  listingType?: ProductListingMode;
95
95
  initialValues?: SellerProductDraft;
96
96
  productId?: string;
97
+ /**
98
+ * The digital-content pool manager, injected by the consumer so appkit never
99
+ * hard-codes an API path. Rendered in EDIT MODE only — a pool is keyed on a
100
+ * product id that does not exist until the listing has been created once.
101
+ */
102
+ renderDigitalContentPool?: (productId: string) => React.ReactNode;
97
103
  onSave: (draft: SellerProductDraft) => Promise<ProductActionResult>;
98
104
  onPublish: (draft: SellerProductDraft) => Promise<ProductActionResult>;
99
105
  onDiscard?: () => void;
@@ -129,4 +135,4 @@ export interface SellerProductShellProps {
129
135
  */
130
136
  previewSlot?: () => React.ReactNode;
131
137
  }
132
- export declare function SellerProductShell({ mode, listingType, initialValues, onSave, onPublish, onDiscard, isLoading, storeSlug, renderCategorySelector, renderBrandSelector, renderAddressSelector, renderTemplateSelector, onSaveAsTemplate, previewSlot, }: SellerProductShellProps): React.JSX.Element;
138
+ export declare function SellerProductShell({ mode, listingType, initialValues, productId, onSave, onPublish, onDiscard, isLoading, storeSlug, renderCategorySelector, renderBrandSelector, renderAddressSelector, renderTemplateSelector, onSaveAsTemplate, previewSlot, renderDigitalContentPool, }: SellerProductShellProps): React.JSX.Element;
@@ -351,7 +351,13 @@ const EDIT_SECTIONS = [
351
351
  { id: "publish", label: "Publish" },
352
352
  ];
353
353
  // ── Main SellerProductShell ───────────────────────────────────────────────
354
- export function SellerProductShell({ mode, listingType = "standard", initialValues, onSave, onPublish, onDiscard, isLoading = false, storeSlug, renderCategorySelector, renderBrandSelector, renderAddressSelector, renderTemplateSelector, onSaveAsTemplate, previewSlot, }) {
354
+ export function SellerProductShell({ mode, listingType = "standard", initialValues,
355
+ /*
356
+ * `productId` was declared on the props interface and NEVER DESTRUCTURED —
357
+ * dead from the day it was added, because nothing in the shell needed it.
358
+ * The digital-content pool is the first thing that does.
359
+ */
360
+ productId, onSave, onPublish, onDiscard, isLoading = false, storeSlug, renderCategorySelector, renderBrandSelector, renderAddressSelector, renderTemplateSelector, onSaveAsTemplate, previewSlot, renderDigitalContentPool, }) {
355
361
  const [draft, setDraft] = useState(initialValues ?? { status: "draft", condition: "new" });
356
362
  const [formMode, setFormMode] = useState(mode === "create" && listingType === "standard" ? "quick" : "full");
357
363
  /*
@@ -583,5 +589,8 @@ export function SellerProductShell({ mode, listingType = "standard", initialValu
583
589
  ...EDIT_SECTIONS,
584
590
  ...(typeSpecificSection ? [{ id: typeSpecificSection.id, label: typeSpecificSection.label }] : []),
585
591
  ];
586
- return (_jsx(FormShell, { isOpen: true, onClose: handleDiscard, title: title, breadcrumb: breadcrumb, isDirty: isDirty, isLoading: isLoading, sections: editSections, onSaveDraft: handleSave, onPublish: handlePublish, saveLabel: "Save Changes", publishLabel: "Update", previewSlot: previewSlot, footerTopSlot: _jsx(FormShellContext.Provider, { value: shellCtx, children: _jsx(FormErrorSummary, {}) }), children: _jsx(FormShellContext.Provider, { value: shellCtx, children: _jsxs(Stack, { gap: "lg", children: [_jsxs(Section, { id: "basic", children: [_jsx(Heading, { level: 3, className: "mb-4", children: "Basic Info" }), _jsx(StepBasic, { values: draft, onChange: update, renderCategorySelector: renderCategorySelector, renderBrandSelector: renderBrandSelector, renderTemplateSelector: renderTemplateSelector })] }), _jsxs(Section, { id: "media", children: [_jsx(Heading, { level: 3, className: "mb-4", children: "Media" }), _jsx(StepMedia, { values: draft, onChange: update, storeSlug: storeSlug })] }), typeSpecificSection && (_jsxs(Section, { id: typeSpecificSection.id, children: [_jsx(Heading, { level: 3, className: "mb-4", children: typeSpecificSection.sectionHeading ?? typeSpecificSection.label }), typeSpecificSection.render({ values: draft, onChange: update, errors: {} })] })), _jsxs(Section, { id: "pricing", children: [_jsx(Heading, { level: 3, className: "mb-4", children: "Pricing" }), _jsx(StepPricing, { values: draft, onChange: update, listingType: listingType })] }), _jsxs(Section, { id: "shipping", children: [_jsx(Heading, { level: 3, className: "mb-4", children: "Shipping" }), _jsx(StepShipping, { values: draft, onChange: update, renderAddressSelector: renderAddressSelector })] }), _jsxs(Section, { id: "returns", children: [_jsx(Heading, { level: 3, className: "mb-4", children: "Returns" }), _jsx(StepReturns, { values: draft, onChange: update })] }), _jsxs(Section, { id: "publish", children: [_jsx(Heading, { level: 3, className: "mb-4", children: "Publish" }), _jsx(StepPublish, { values: draft, onChange: update }), onSaveAsTemplate && (_jsxs(Div, { className: "mt-4 border-t border-[var(--appkit-color-border)]", padding: "t-md", children: [_jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: () => onSaveAsTemplate(draft), children: "Save as Template" }), _jsx(Text, { className: "mt-1 text-[var(--appkit-color-secondary-text)]", size: "xs", children: "Save these settings as a reusable template for future listings." })] }))] })] }) }) }));
592
+ return (_jsx(FormShell, { isOpen: true, onClose: handleDiscard, title: title, breadcrumb: breadcrumb, isDirty: isDirty, isLoading: isLoading, sections: editSections, onSaveDraft: handleSave, onPublish: handlePublish, saveLabel: "Save Changes", publishLabel: "Update", previewSlot: previewSlot, footerTopSlot: _jsx(FormShellContext.Provider, { value: shellCtx, children: _jsx(FormErrorSummary, {}) }), children: _jsx(FormShellContext.Provider, { value: shellCtx, children: _jsxs(Stack, { gap: "lg", children: [_jsxs(Section, { id: "basic", children: [_jsx(Heading, { level: 3, className: "mb-4", children: "Basic Info" }), _jsx(StepBasic, { values: draft, onChange: update, renderCategorySelector: renderCategorySelector, renderBrandSelector: renderBrandSelector, renderTemplateSelector: renderTemplateSelector })] }), _jsxs(Section, { id: "media", children: [_jsx(Heading, { level: 3, className: "mb-4", children: "Media" }), _jsx(StepMedia, { values: draft, onChange: update, storeSlug: storeSlug })] }), typeSpecificSection && (_jsxs(Section, { id: typeSpecificSection.id, children: [_jsx(Heading, { level: 3, className: "mb-4", children: typeSpecificSection.sectionHeading ?? typeSpecificSection.label }), typeSpecificSection.render({ values: draft, onChange: update, errors: {} }), mode === "edit" &&
593
+ listingType === "digital-code" &&
594
+ productId &&
595
+ renderDigitalContentPool?.(productId)] })), _jsxs(Section, { id: "pricing", children: [_jsx(Heading, { level: 3, className: "mb-4", children: "Pricing" }), _jsx(StepPricing, { values: draft, onChange: update, listingType: listingType })] }), _jsxs(Section, { id: "shipping", children: [_jsx(Heading, { level: 3, className: "mb-4", children: "Shipping" }), _jsx(StepShipping, { values: draft, onChange: update, renderAddressSelector: renderAddressSelector })] }), _jsxs(Section, { id: "returns", children: [_jsx(Heading, { level: 3, className: "mb-4", children: "Returns" }), _jsx(StepReturns, { values: draft, onChange: update })] }), _jsxs(Section, { id: "publish", children: [_jsx(Heading, { level: 3, className: "mb-4", children: "Publish" }), _jsx(StepPublish, { values: draft, onChange: update }), onSaveAsTemplate && (_jsxs(Div, { className: "mt-4 border-t border-[var(--appkit-color-border)]", padding: "t-md", children: [_jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: () => onSaveAsTemplate(draft), children: "Save as Template" }), _jsx(Text, { className: "mt-1 text-[var(--appkit-color-secondary-text)]", size: "xs", children: "Save these settings as a reusable template for future listings." })] }))] })] }) }) }));
587
596
  }
@@ -7,9 +7,14 @@
7
7
  * export { storeReviewsGET as GET } from "@mohasinac/feat-stores";
8
8
  * ```
9
9
  *
10
- * Aggregates approved reviews from up to 20 published products in a store.
10
+ * Aggregates every approved review filed against a store, by `storeId`.
11
11
  * Returns: reviews[], averageRating, totalReviews, ratingDistribution.
12
12
  *
13
+ * It used to aggregate "up to 20 published products by itemsSold" and fan out a
14
+ * review query per product — and since no product carries `itemsSold`, that
15
+ * `orderBy` matched nothing and the endpoint returned zero reviews for every
16
+ * store. See the block comment at the query for the full account.
17
+ *
13
18
  * Requires `db` registered in providers.config via `registerProviders()`.
14
19
  * Collections: "stores", "products", "reviews"
15
20
  */