@mohasinac/appkit 4.42.18 → 4.44.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 (263) hide show
  1. package/dist/_internal/client/features/lottery/LotteryDetailView.js +22 -1
  2. package/dist/_internal/server/features/categories/adapters.d.ts +47 -0
  3. package/dist/_internal/server/features/categories/adapters.js +235 -0
  4. package/dist/_internal/server/features/checkout/actions.js +161 -22
  5. package/dist/_internal/server/features/checkout/expand-order-items.js +34 -0
  6. package/dist/_internal/server/features/orders/adapters.js +21 -0
  7. package/dist/_internal/server/features/products/list-public.d.ts +24 -0
  8. package/dist/_internal/server/features/products/list-public.js +10 -2
  9. package/dist/_internal/server/features/refunds/actions.js +1 -1
  10. package/dist/_internal/server/features/seo/sitemap.js +54 -15
  11. package/dist/_internal/server/features/stores/adapters.d.ts +1 -1
  12. package/dist/_internal/server/features/stores/adapters.js +6 -0
  13. package/dist/_internal/server/functions/scheduled.d.ts +4 -2
  14. package/dist/_internal/server/functions/scheduled.js +64 -6
  15. package/dist/_internal/server/jobs/core/adminAnalytics.d.ts +0 -1
  16. package/dist/_internal/server/jobs/core/adminAnalytics.js +9 -1
  17. package/dist/_internal/server/jobs/core/analyticsAlertEvaluate.d.ts +47 -0
  18. package/dist/_internal/server/jobs/core/analyticsAlertEvaluate.js +468 -0
  19. package/dist/_internal/server/jobs/core/autoPayoutEligibility.js +14 -1
  20. package/dist/_internal/server/jobs/core/countersReconcile.js +24 -8
  21. package/dist/_internal/server/jobs/core/onCategoryWrite.js +59 -0
  22. package/dist/_internal/server/jobs/core/onProductWrite.js +31 -0
  23. package/dist/_internal/server/jobs/core/onReviewWrite.js +8 -0
  24. package/dist/_internal/server/jobs/core/onStoreWrite.d.ts +1 -1
  25. package/dist/_internal/server/jobs/core/onStoreWrite.js +8 -2
  26. package/dist/_internal/server/jobs/core/priceIndexRollup.d.ts +47 -0
  27. package/dist/_internal/server/jobs/core/priceIndexRollup.js +234 -0
  28. package/dist/_internal/server/jobs/core/revalidateNotify.d.ts +28 -0
  29. package/dist/_internal/server/jobs/core/revalidateNotify.js +125 -0
  30. package/dist/_internal/server/jobs/core/{testerSandboxCleanup.d.ts → testDataCleanup.d.ts} +3 -3
  31. package/dist/_internal/server/jobs/core/{testerSandboxCleanup.js → testDataCleanup.js} +3 -3
  32. package/dist/_internal/server/jobs/core/weeklyPayoutEligibility.js +5 -1
  33. package/dist/_internal/server/jobs/handlers/analyticsAlertEvaluate.d.ts +2 -0
  34. package/dist/_internal/server/jobs/handlers/analyticsAlertEvaluate.js +2 -0
  35. package/dist/_internal/server/jobs/handlers/index.d.ts +3 -1
  36. package/dist/_internal/server/jobs/handlers/index.js +3 -1
  37. package/dist/_internal/server/jobs/handlers/messages.d.ts +5 -2
  38. package/dist/_internal/server/jobs/handlers/messages.js +5 -2
  39. package/dist/_internal/server/jobs/handlers/priceIndexRollup.d.ts +2 -0
  40. package/dist/_internal/server/jobs/handlers/priceIndexRollup.js +2 -0
  41. package/dist/_internal/server/jobs/handlers/testDataCleanup.d.ts +2 -0
  42. package/dist/_internal/server/jobs/handlers/testDataCleanup.js +2 -0
  43. package/dist/_internal/shared/actions/action-registry.js +34 -0
  44. package/dist/_internal/shared/cache/revalidate-targets.d.ts +49 -0
  45. package/dist/_internal/shared/cache/revalidate-targets.js +157 -0
  46. package/dist/_internal/shared/checkout/order-math.d.ts +113 -3
  47. package/dist/_internal/shared/checkout/order-math.js +194 -7
  48. package/dist/_internal/shared/features/categories/price-guidance.d.ts +110 -0
  49. package/dist/_internal/shared/features/categories/price-guidance.js +136 -0
  50. package/dist/_internal/shared/features/site-settings/gst-registration.d.ts +90 -0
  51. package/dist/_internal/shared/features/site-settings/gst-registration.js +126 -0
  52. package/dist/_internal/shared/fees/calculator.d.ts +105 -0
  53. package/dist/_internal/shared/fees/calculator.js +143 -0
  54. package/dist/_internal/shared/listing-types/art/config.d.ts +0 -6
  55. package/dist/_internal/shared/listing-types/classified/config.d.ts +0 -6
  56. package/dist/_internal/shared/listing-types/digital-code/config.d.ts +0 -6
  57. package/dist/_internal/shared/listing-types/live/config.d.ts +0 -6
  58. package/dist/_internal/shared/listing-types/standard/config.d.ts +0 -6
  59. package/dist/_internal/shared/listing-types/stickers/config.d.ts +0 -6
  60. package/dist/_internal/shared/templating/placeholders.d.ts +70 -0
  61. package/dist/_internal/shared/templating/placeholders.js +73 -0
  62. package/dist/client.d.ts +2 -0
  63. package/dist/client.js +1 -0
  64. package/dist/constants/api-endpoints.d.ts +42 -0
  65. package/dist/constants/api-endpoints.js +14 -0
  66. package/dist/features/admin/actions/admin-actions.d.ts +1 -1
  67. package/dist/features/admin/actions/admin-actions.js +1 -1
  68. package/dist/features/admin/components/AdminCategoryEditorView.js +269 -3
  69. package/dist/features/admin/components/AdminProductEditorView.js +8 -1
  70. package/dist/features/admin/components/AdminProductsView.js +78 -7
  71. package/dist/features/admin/components/AdminSiteSettingsView.js +20 -1
  72. package/dist/features/admin/components/AdminTaxCodeEditorView.d.ts +10 -0
  73. package/dist/features/admin/components/AdminTaxCodeEditorView.js +160 -0
  74. package/dist/features/admin/components/AdminTaxCodesView.d.ts +13 -0
  75. package/dist/features/admin/components/AdminTaxCodesView.js +86 -0
  76. package/dist/features/admin/components/AdminTesterChecklistView.js +1 -1
  77. package/dist/features/admin/components/analytics/AdminTopProductsTable.js +1 -1
  78. package/dist/features/auctions/components/AuctionBottomActions.js +10 -1
  79. package/dist/features/auctions/components/AuctionDetailPageView.js +2 -1
  80. package/dist/features/auctions/components/AuctionLiveStatusBadge.d.ts +20 -0
  81. package/dist/features/auctions/components/AuctionLiveStatusBadge.js +21 -0
  82. package/dist/features/auctions/components/PlaceBidFormClient.d.ts +1 -1
  83. package/dist/features/auctions/components/PlaceBidFormClient.js +20 -2
  84. package/dist/features/auctions/hooks/useIsAuctionEnded.d.ts +41 -0
  85. package/dist/features/auctions/hooks/useIsAuctionEnded.js +76 -0
  86. package/dist/features/auth/schemas/firestore.d.ts +0 -1
  87. package/dist/features/blog/api/route.js +16 -1
  88. package/dist/features/blog/schemas/firestore.d.ts +2 -2
  89. package/dist/features/catalogue/components/CatalogueItemEditorView.js +14 -3
  90. package/dist/features/categories/actions/category-actions.d.ts +2 -2
  91. package/dist/features/categories/actions/category-actions.js +22 -10
  92. package/dist/features/categories/api/[id]/description-template/route.d.ts +45 -0
  93. package/dist/features/categories/api/[id]/description-template/route.js +252 -0
  94. package/dist/features/categories/api/route.js +156 -11
  95. package/dist/features/categories/components/CategoriesIndexListing.d.ts +1 -3
  96. package/dist/features/categories/components/CategoriesIndexListing.js +1 -1
  97. package/dist/features/categories/components/CategoriesIndexPageView.d.ts +27 -1
  98. package/dist/features/categories/components/CategoriesIndexPageView.js +24 -33
  99. package/dist/features/categories/components/CategoryDetailPageView.js +43 -10
  100. package/dist/features/categories/components/CategoryTemplatePicker.d.ts +65 -0
  101. package/dist/features/categories/components/CategoryTemplatePicker.js +170 -0
  102. package/dist/features/categories/constants/listing-categories.d.ts +17 -0
  103. package/dist/features/categories/constants/listing-categories.js +53 -0
  104. package/dist/features/categories/hooks/useCategoryTree.js +28 -2
  105. package/dist/features/categories/repository/categories.repository.d.ts +73 -12
  106. package/dist/features/categories/repository/categories.repository.js +203 -42
  107. package/dist/features/categories/repository/price-index.repository.d.ts +115 -0
  108. package/dist/features/categories/repository/price-index.repository.js +85 -0
  109. package/dist/features/categories/schemas/category-content.d.ts +206 -0
  110. package/dist/features/categories/schemas/category-content.js +28 -0
  111. package/dist/features/categories/schemas/firestore.d.ts +74 -5
  112. package/dist/features/categories/schemas/firestore.js +74 -3
  113. package/dist/features/categories/schemas/index.d.ts +1047 -0
  114. package/dist/features/categories/schemas/index.js +96 -0
  115. package/dist/features/categories/server.d.ts +1 -0
  116. package/dist/features/categories/server.js +7 -0
  117. package/dist/features/categories/types/index.d.ts +9 -0
  118. package/dist/features/events/schemas/firestore.d.ts +2 -2
  119. package/dist/features/faq/schemas/firestore.d.ts +2 -1
  120. package/dist/features/faq/schemas/firestore.js +20 -8
  121. package/dist/features/faq/utils/getHomepageFaqsCached.d.ts +20 -0
  122. package/dist/features/faq/utils/getHomepageFaqsCached.js +22 -0
  123. package/dist/features/homepage/components/MarketplaceHomepageView.js +13 -3
  124. package/dist/features/homepage/lib/live-stats.js +18 -8
  125. package/dist/features/media/MediaImage.js +19 -2
  126. package/dist/features/orders/schemas/firestore.d.ts +70 -0
  127. package/dist/features/orders/types/index.d.ts +55 -0
  128. package/dist/features/products/actions/product-actions.d.ts +0 -9
  129. package/dist/features/products/actions/product-actions.js +25 -0
  130. package/dist/features/products/columns/index.d.ts +0 -1
  131. package/dist/features/products/columns/index.js +5 -1
  132. package/dist/features/products/components/ArtStickersListView.js +7 -1
  133. package/dist/features/products/components/PrizeDrawsSection.js +5 -0
  134. package/dist/features/products/components/ProductFilters.d.ts +0 -9
  135. package/dist/features/products/components/ProductForm.d.ts +27 -1
  136. package/dist/features/products/components/ProductForm.js +26 -12
  137. package/dist/features/products/components/ProductsIndexPageView.js +1 -1
  138. package/dist/features/products/constants/sieve.d.ts +0 -6
  139. package/dist/features/products/constants/sieve.js +35 -2
  140. package/dist/features/products/repository/products.repository.d.ts +115 -1
  141. package/dist/features/products/repository/products.repository.js +491 -39
  142. package/dist/features/products/schemas/firestore.d.ts +65 -5
  143. package/dist/features/products/schemas/firestore.js +9 -0
  144. package/dist/features/products/schemas/product-features.d.ts +140 -2
  145. package/dist/features/products/schemas/product-features.js +62 -1
  146. package/dist/features/promotions/repository/coupons.repository.js +6 -1
  147. package/dist/features/promotions/schemas/firestore.d.ts +44 -1
  148. package/dist/features/promotions/schemas/firestore.js +76 -2
  149. package/dist/features/reviews/repository/reviews.repository.d.ts +22 -0
  150. package/dist/features/reviews/repository/reviews.repository.js +35 -0
  151. package/dist/features/seller/actions/seller-actions.js +17 -2
  152. package/dist/features/seller/components/PrintCenterView.d.ts +3 -2
  153. package/dist/features/seller/components/PrintCenterView.js +3 -2
  154. package/dist/features/seller/components/SellerAnalyticsAlertsView.js +21 -11
  155. package/dist/features/seller/components/SellerOrdersView.js +6 -1
  156. package/dist/features/seller/components/SellerPayoutSettingsView.js +19 -3
  157. package/dist/features/seller/components/SellerProductShell.d.ts +50 -2
  158. package/dist/features/seller/components/SellerProductShell.js +101 -9
  159. package/dist/features/seller/components/SellerProductsFilterDrawer.d.ts +13 -1
  160. package/dist/features/seller/components/SellerProductsFilterDrawer.js +2 -2
  161. package/dist/features/seller/components/SellerProductsView.js +108 -3
  162. package/dist/features/seller/types/index.d.ts +17 -0
  163. package/dist/features/seller/utils/product-draft-mapping.d.ts +13 -0
  164. package/dist/features/seller/utils/product-draft-mapping.js +17 -0
  165. package/dist/features/store-extensions/constants/alert-metrics.d.ts +86 -0
  166. package/dist/features/store-extensions/constants/alert-metrics.js +142 -0
  167. package/dist/features/store-extensions/schemas/firestore.d.ts +19 -1
  168. package/dist/features/stores/schemas/firestore.d.ts +31 -2
  169. package/dist/features/tax-codes/repository/tax-codes.repository.d.ts +55 -0
  170. package/dist/features/tax-codes/repository/tax-codes.repository.js +131 -0
  171. package/dist/features/tax-codes/schemas/firestore.d.ts +125 -0
  172. package/dist/features/tax-codes/schemas/firestore.js +82 -0
  173. package/dist/features/tax-codes/schemas/tax-code-form.d.ts +63 -0
  174. package/dist/features/tax-codes/schemas/tax-code-form.js +152 -0
  175. package/dist/features/tax-codes/schemas/tax-codes.validators.d.ts +107 -0
  176. package/dist/features/tax-codes/schemas/tax-codes.validators.js +85 -0
  177. package/dist/features/tester/actions/index.d.ts +0 -1
  178. package/dist/features/tester/actions/index.js +0 -1
  179. package/dist/features/tester/components/AdminTesterFeedbackIssuesView.js +1 -1
  180. package/dist/features/tester/components/AdminTesterFeedbackListView.js +1 -1
  181. package/dist/features/tester/components/index.d.ts +0 -4
  182. package/dist/features/tester/components/index.js +0 -2
  183. package/dist/features/tester/repository/tester-checklist-item.repository.d.ts +1 -20
  184. package/dist/features/tester/repository/tester-checklist-item.repository.js +0 -52
  185. package/dist/features/tester/schemas/firestore.d.ts +0 -6
  186. package/dist/features/tester/seed-data/authored/admin__admin-detail-round-trips.js +3 -3
  187. package/dist/features/tester/seed-data/authored/admin__site-system.js +4 -5
  188. package/dist/features/tester/seed-data/authored/content-discovery__category-counts-and-rollup.js +2 -2
  189. package/dist/features/tester/seed-data/authored/index.js +0 -2
  190. package/dist/features/tester/seed-data/authored/page-wiring__reachability.js +0 -19
  191. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +10 -29
  192. package/dist/index.d.ts +21 -7
  193. package/dist/index.js +53 -7
  194. package/dist/next/routing/route-map.d.ts +2 -4
  195. package/dist/next/routing/route-map.js +1 -2
  196. package/dist/repositories/index.d.ts +1 -0
  197. package/dist/repositories/index.js +1 -0
  198. package/dist/schemas/registry.d.ts +506 -0
  199. package/dist/seed/_helpers/brand-rows.d.ts +35 -0
  200. package/dist/seed/_helpers/brand-rows.js +576 -0
  201. package/dist/seed/_helpers/category-content.d.ts +39 -0
  202. package/dist/seed/_helpers/category-content.js +634 -0
  203. package/dist/seed/_helpers/category-forest.d.ts +2 -0
  204. package/dist/seed/_helpers/category-forest.js +585 -243
  205. package/dist/seed/_helpers/category-models.d.ts +5 -0
  206. package/dist/seed/_helpers/category-models.js +1364 -0
  207. package/dist/seed/_helpers/category-tree.js +17 -1
  208. package/dist/seed/_helpers/character-features.d.ts +10 -0
  209. package/dist/seed/_helpers/character-features.js +118 -0
  210. package/dist/seed/_helpers/media.d.ts +1 -1
  211. package/dist/seed/_helpers/media.js +44 -8
  212. package/dist/seed/_helpers/search-txt-wrappers.d.ts +10 -0
  213. package/dist/seed/_helpers/search-txt-wrappers.js +12 -1
  214. package/dist/seed/categories-seed-data.js +147 -169
  215. package/dist/seed/index.d.ts +1 -0
  216. package/dist/seed/index.js +1 -0
  217. package/dist/seed/manifest.d.ts +1 -1
  218. package/dist/seed/manifest.js +5 -0
  219. package/dist/seed/orders-seed-data.js +5 -3
  220. package/dist/seed/product-features-seed-data.d.ts +1 -8
  221. package/dist/seed/product-features-seed-data.js +358 -154
  222. package/dist/seed/products-art-seed-data.js +16 -4
  223. package/dist/seed/products-auctions-seed-data.js +6 -6
  224. package/dist/seed/products-classifieds-seed-data.js +17 -7
  225. package/dist/seed/products-digital-codes-seed-data.js +12 -12
  226. package/dist/seed/products-preorders-seed-data.js +8 -8
  227. package/dist/seed/products-prize-draws-seed-data.js +25 -8
  228. package/dist/seed/products-standard-seed-data.js +10 -60
  229. package/dist/seed/products-stickers-seed-data.js +6 -6
  230. package/dist/seed/site-settings-seed-data.js +24 -0
  231. package/dist/seed/store-extensions-seed-data.js +10 -4
  232. package/dist/seed/tax-codes-seed-data.d.ts +34 -0
  233. package/dist/seed/tax-codes-seed-data.js +100 -0
  234. package/dist/seed/test-data-ttl.d.ts +29 -0
  235. package/dist/seed/test-data-ttl.js +31 -0
  236. package/dist/server-entry.d.ts +1 -1
  237. package/dist/server-entry.js +6 -1
  238. package/dist/server.d.ts +4 -1
  239. package/dist/server.js +11 -2
  240. package/dist/styles.css +40 -0
  241. package/dist/ui/components/PaginatedSelect.d.ts +14 -0
  242. package/dist/ui/components/PaginatedSelect.js +0 -0
  243. package/dist/ui/components/PaginatedSelect.style.css +40 -0
  244. package/dist/utils/media-url.d.ts +26 -0
  245. package/dist/utils/media-url.js +144 -0
  246. package/dist/utils/physical-location.d.ts +23 -0
  247. package/dist/utils/physical-location.js +52 -0
  248. package/dist/utils/search-txt-builders.d.ts +20 -0
  249. package/dist/utils/search-txt-builders.js +36 -0
  250. package/package.json +1 -1
  251. package/scripts/seed-cli.mjs +4 -1
  252. package/dist/_internal/server/jobs/handlers/testerSandboxCleanup.d.ts +0 -2
  253. package/dist/_internal/server/jobs/handlers/testerSandboxCleanup.js +0 -2
  254. package/dist/features/tester/actions/leaderboard-actions.d.ts +0 -3
  255. package/dist/features/tester/actions/leaderboard-actions.js +0 -5
  256. package/dist/features/tester/components/BugHunterLeaderboardView.d.ts +0 -23
  257. package/dist/features/tester/components/BugHunterLeaderboardView.js +0 -18
  258. package/dist/features/tester/components/TesterHubView.d.ts +0 -5
  259. package/dist/features/tester/components/TesterHubView.js +0 -110
  260. package/dist/features/tester/seed-data/authored/public-pages__bug-hunters.d.ts +0 -2
  261. package/dist/features/tester/seed-data/authored/public-pages__bug-hunters.js +0 -78
  262. package/dist/features/tester/seed-data/tester-ttl.d.ts +0 -4
  263. package/dist/features/tester/seed-data/tester-ttl.js +0 -6
@@ -50,6 +50,29 @@ export function expandCartLineToOrderRows(item, representative, productById) {
50
50
  // through `isFinalSale` so this is always an explicit boolean, even
51
51
  // though the product field is optional and absent means true.
52
52
  finalSale: isFinalSale(representative),
53
+ /*
54
+ * 🛑 The LEAF category, snapshotted — beside `hsnCode`, `gstRate` and
55
+ * `finalSale`, and for the same reason the first two are here.
56
+ *
57
+ * `OrderDocumentItem` carried no taxonomy field at all (`bundleCategorySlug`
58
+ * is a bundle key, not a category), so `priceIndexRollup` could only find
59
+ * a sale's category by joining every order item back to its product — a
60
+ * full products scan per run, and nothing at all for an item whose
61
+ * product has since been deleted.
62
+ *
63
+ * Snapshotting makes the join disappear for every future order. And a
64
+ * snapshot is the CORRECT answer rather than merely the cheap one: a
65
+ * product re-categorised next year must not retroactively move last
66
+ * year's sale into a band it was never part of.
67
+ *
68
+ * `categorySlugs[0]` is the leaf — the chain is stored self-first — and
69
+ * a sale rolls up to exactly one leaf, so there is nothing to
70
+ * double-count.
71
+ */
72
+ ...(representative.categorySlugs?.[0]
73
+ ? { categorySlug: representative.categorySlugs[0] }
74
+ : {}),
75
+ ...(representative.listingType ? { listingType: representative.listingType } : {}),
53
76
  // Legacy collapsed bundle rows (no groupMembers) keep their old shape so
54
77
  // orders already in flight and their UI stay untouched.
55
78
  ...(item.bundleCategorySlug && item.bundleProductIds
@@ -88,6 +111,17 @@ export function expandCartLineToOrderRows(item, representative, productById) {
88
111
  // Kept on bundle rows so the pre-existing receipt grouping (which keys on
89
112
  // this) keeps working for bundles without any UI change.
90
113
  ...(item.bundleCategorySlug ? { bundleCategorySlug: item.bundleCategorySlug } : {}),
114
+ /*
115
+ * Per MEMBER, like `finalSale` and the GST pair above it — a bundle
116
+ * genuinely spans categories, and attributing every member's sale to the
117
+ * bundle's own leaf would put a ₹1,299 bey's realised price into a
118
+ * sticker leaf's band.
119
+ *
120
+ * Falls back to the cart-line member snapshot when the product has since
121
+ * been deleted, which is the whole reason that snapshot exists.
122
+ */
123
+ ...(product?.categorySlugs?.[0] ? { categorySlug: product.categorySlugs[0] } : {}),
124
+ ...(product?.listingType ? { listingType: product.listingType } : {}),
91
125
  ...(gstRate != null ? { gstRate, ...(hsnCode ? { hsnCode } : {}) } : {}),
92
126
  };
93
127
  });
@@ -60,6 +60,12 @@ export function orderDocumentToOrder(doc) {
60
60
  // cannot tell a final-sale line from a returnable one and offers
61
61
  // every reason on every order (Root Cause #57).
62
62
  ...(item.finalSale != null ? { finalSale: item.finalSale } : {}),
63
+ // Snapshotted tax terms. Without these the invoice cannot render an
64
+ // HSN or a rate column — the SAME omission as `finalSale` above, one
65
+ // field family over, and the reason the buyer-facing invoice showed
66
+ // only Item / Qty / Price (Root Cause #57).
67
+ ...(item.hsnCode ? { hsnCode: item.hsnCode } : {}),
68
+ ...(item.gstRate != null ? { gstRate: item.gstRate } : {}),
63
69
  }))
64
70
  : [
65
71
  {
@@ -159,6 +165,21 @@ export function orderDocumentToOrder(doc) {
159
165
  * GST-compliance problem, not a cosmetic one.
160
166
  */
161
167
  tax: doc.gstAmount,
168
+ /*
169
+ * The rest of the breakdown. `tax` alone was all this adapter mapped, so
170
+ * the invoice had one aggregate "Tax (GST)" row and no way to show a
171
+ * CGST/SGST vs IGST split, a taxable value, or a per-rate table — i.e. it
172
+ * could not be Rule 46-compliant no matter how it was rendered.
173
+ */
174
+ taxableAmount: doc.taxableAmount,
175
+ exemptAmount: doc.exemptAmount,
176
+ cgst: doc.cgst,
177
+ sgst: doc.sgst,
178
+ igst: doc.igst,
179
+ gstByRate: doc.gstByRate,
180
+ supplierGstin: doc.supplierGstin,
181
+ supplierLegalName: doc.supplierLegalName,
182
+ supplierAddress: doc.supplierAddress,
162
183
  codHandlingFee: doc.codHandlingFee,
163
184
  trackingNumber: doc.trackingNumber,
164
185
  shippingCarrier: doc.shippingCarrier,
@@ -90,6 +90,30 @@ export interface PublicProductListInput {
90
90
  pageSize?: number;
91
91
  sorts?: string;
92
92
  cursor?: string | null;
93
+ /**
94
+ * How many documents the BOUNDED-WINDOW path may fetch. Defaults to
95
+ * `PUBLIC_PRODUCT_MAX_PAGE_SIZE` (50) — today's behaviour for every caller
96
+ * that omits it.
97
+ *
98
+ * 🛑 Only meaningful when the query lands on the in-memory path, which is
99
+ * almost always: `hidesTestData` is true for EVERY anonymous viewer, so
100
+ * `inMemory` is true for essentially all public browsing. On that path the
101
+ * fetch is pinned to `page: 1, pageSize: 50` regardless of the caller's
102
+ * `pageSize`, because the filter is a per-row predicate applied after the
103
+ * fetch and before the slice.
104
+ *
105
+ * That is correct for a browse page, which must be able to page deeply. It is
106
+ * wasteful for a FIXED-SIZE homepage strip: `getFeaturedProducts(12)` read 51
107
+ * documents to render 12, and the homepage did that four times — ~200 reads
108
+ * for 44 cards, on a 50K-reads/day free tier (measured 2.1M/week).
109
+ *
110
+ * Set it only where the caller genuinely wants N items and will never page:
111
+ * a larger value than `pageSize` is still needed as headroom, because rows are
112
+ * dropped by the predicate after the fetch. Too small a window silently yields
113
+ * fewer items than asked for — which is why this is opt-in rather than derived
114
+ * from `pageSize`, and why `truncated` still reports honestly below.
115
+ */
116
+ windowSize?: number;
93
117
  }
94
118
  export interface PublicProductListResult {
95
119
  items: Row[];
@@ -607,11 +607,14 @@ export async function listPublicProducts(input, opts) {
607
607
  : hasTypeFacet
608
608
  ? FACET_FETCH_SORT
609
609
  : sorts;
610
+ // Clamped to the 50-doc ceiling so an opt-in can only ever make the window
611
+ // SMALLER — a caller cannot widen the public read budget through this door.
612
+ const windowSize = Math.min(Math.max(1, input.windowSize ?? PUBLIC_PRODUCT_MAX_PAGE_SIZE), PUBLIC_PRODUCT_MAX_PAGE_SIZE);
610
613
  const result = await runQuery(executor, {
611
614
  filters,
612
615
  sorts: fetchSorts,
613
616
  page: inMemory ? 1 : page,
614
- pageSize: inMemory ? PUBLIC_PRODUCT_MAX_PAGE_SIZE : pageSize,
617
+ pageSize: inMemory ? windowSize : pageSize,
615
618
  cursor: inMemory ? null : (input.cursor ?? null),
616
619
  search: input.q,
617
620
  });
@@ -625,7 +628,12 @@ export async function listPublicProducts(input, opts) {
625
628
  let cursor = result.cursor ?? null;
626
629
  let truncated = false;
627
630
  if (inMemory) {
628
- truncated = items.length >= PUBLIC_PRODUCT_MAX_PAGE_SIZE;
631
+ // Against `windowSize`, NOT the 50 ceiling. A caller that opted into a
632
+ // smaller window saturates it at that size, so comparing with 50 would
633
+ // report `truncated: false` for a window that was in fact full — and
634
+ // `truncated` is what makes `total` render as "50+" and keeps the pager
635
+ // offering Next instead of claiming a false last page.
636
+ truncated = items.length >= windowSize;
629
637
  /** OR semantics, matching the single-value `array-contains` pushdown. */
630
638
  const matchesAny = (raw, wanted) => {
631
639
  if (!Array.isArray(raw))
@@ -19,7 +19,7 @@ import { randomUUID } from "crypto";
19
19
  import { getProviders } from "../../../../contracts/registry";
20
20
  import { normalizeError } from "../../../../errors/normalize";
21
21
  import { rupeesToPaise } from "../../../../core/money";
22
- import { orderRepository } from "../../../..";
22
+ import { orderRepository } from "../../../../features/orders/repository/orders.repository";
23
23
  import { ORDER_FIELDS } from "../../../../constants/field-names";
24
24
  import { NotFoundError, ValidationError } from "../../../../errors";
25
25
  import { serverLogger } from "../../../../monitoring";
@@ -1,10 +1,12 @@
1
1
  import { normalizeError } from "../../../../errors/normalize";
2
2
  import { getAdminDb } from "../../../../providers/db-firebase";
3
3
  import { ROUTES } from "../../../../next/routing/route-map";
4
- import { PRODUCT_COLLECTION } from "../../../..";
4
+ import { PRODUCT_COLLECTION } from "../../../../features/products/schemas/firestore";
5
5
  import { EVENTS_COLLECTION, EVENT_FIELDS } from "../../../../features/events";
6
6
  import { BLOG_POSTS_COLLECTION, BLOG_POST_FIELDS } from "../../../../features/blog";
7
7
  import { CATEGORIES_COLLECTION, CATEGORY_FIELDS } from "../../../../features/categories";
8
+ // Imported from the DEFINING module, not the feature barrel (Root Cause #18).
9
+ import { isListingCategory } from "../../../../features/categories/constants/listing-categories";
8
10
  import { STORE_COLLECTION, STORE_FIELDS } from "../../../../features/stores";
9
11
  import { SCAMMER_COLLECTION } from "../../../../features/scams/schemas/firestore";
10
12
  import { serverLogger } from "../../../../monitoring/server-logger";
@@ -16,7 +18,7 @@ const PRODUCT_SLUG = "slug";
16
18
  const PRODUCT_UPDATED_AT = "updatedAt";
17
19
  /**
18
20
  * Tester-sandbox fixtures must never reach the public sitemap. They are created
19
- * and destroyed on a cycle by `testerSandboxCleanup` and by each tester run's
21
+ * and destroyed on a cycle by `testDataCleanup` and by each tester run's
20
22
  * wipe/seed, so anything Google indexes from them 404s shortly after. 34 of 182 sitemap
21
23
  * URLs were disposable test fixtures before this filter existed.
22
24
  *
@@ -58,6 +60,47 @@ function sitemapSectionFailed(label, err) {
58
60
  serverLogger.error(`sitemap: section "${label}" failed and returned 0 URLs`, { section: label, error: err });
59
61
  return [];
60
62
  }
63
+ /**
64
+ * Read every document a query matches, in pages.
65
+ *
66
+ * 🛑 WHY: three category fetchers below carried a bare `.limit(500)` and then
67
+ * filtered in memory, so past 500 active rows the sitemap emitted an
68
+ * **unpredictable subset** — and `scripts/deploy.mjs` only asserts a section is
69
+ * non-ZERO, so truncation passes the deploy gate silently. The taxonomy this
70
+ * plan seeds takes `categories` from 58 to ~330 rows, which is still under 500
71
+ * today; the point is that crossing it would be invisible.
72
+ *
73
+ * `MAX_SITEMAP_DOCS` is a real ceiling, kept well under the 50,000-URL
74
+ * sitemap-file limit, and crossing it is LOGGED rather than swallowed — the
75
+ * opposite of a bare `.limit()`, which cannot tell you it truncated.
76
+ *
77
+ * An explicit `orderBy("__name__")` is required: `startAfter(docSnapshot)`
78
+ * needs a defined sort to page against, and a `where`-only query's implicit
79
+ * document order is not something to rely on across pages.
80
+ */
81
+ const SITEMAP_PAGE_SIZE = 500;
82
+ const MAX_SITEMAP_DOCS = 10000;
83
+ async function readAllDocs(query, label) {
84
+ const out = [];
85
+ let cursor = null;
86
+ for (;;) {
87
+ let page = query.orderBy("__name__").limit(SITEMAP_PAGE_SIZE);
88
+ if (cursor)
89
+ page = page.startAfter(cursor);
90
+ const snap = await page.get();
91
+ if (snap.empty)
92
+ break;
93
+ out.push(...snap.docs);
94
+ if (out.length >= MAX_SITEMAP_DOCS) {
95
+ serverLogger.error(`sitemap: section "${label}" hit the ${MAX_SITEMAP_DOCS}-document ceiling and is TRUNCATED`, { section: label, scanned: out.length });
96
+ return out.slice(0, MAX_SITEMAP_DOCS);
97
+ }
98
+ if (snap.size < SITEMAP_PAGE_SIZE)
99
+ break;
100
+ cursor = snap.docs[snap.docs.length - 1];
101
+ }
102
+ return out;
103
+ }
61
104
  function staticPages(baseUrl) {
62
105
  const page = (path, changeFreq, priority) => ({ url: `${baseUrl}${path}`, lastModified: new Date(), changeFrequency: changeFreq, priority });
63
106
  return [
@@ -250,14 +293,12 @@ async function fetchEventUrls(baseUrl) {
250
293
  async function fetchCategoryTypeUrls(baseUrl, categoryType, buildUrl, label) {
251
294
  try {
252
295
  const db = getAdminDb();
253
- const snap = await db
296
+ const docs = await readAllDocs(db
254
297
  .collection(CATEGORIES_COLLECTION)
255
298
  .where(CATEGORY_FIELDS.CATEGORY_TYPE, "==", categoryType)
256
299
  .where(CATEGORY_FIELDS.IS_ACTIVE, "==", true)
257
- .select(CATEGORY_FIELDS.SLUG, CATEGORY_FIELDS.UPDATED_AT, TEST_DATA_FIELD)
258
- .limit(500)
259
- .get();
260
- return snap.docs
300
+ .select(CATEGORY_FIELDS.SLUG, CATEGORY_FIELDS.UPDATED_AT, TEST_DATA_FIELD), label);
301
+ return docs
261
302
  .filter((doc) => !isTestDoc(doc.data()))
262
303
  .map((doc) => {
263
304
  const data = doc.data();
@@ -289,23 +330,21 @@ async function fetchCategoryTypeUrls(baseUrl, categoryType, buildUrl, label) {
289
330
  * stay in the `.select()` list or it comes back undefined and nothing is
290
331
  * rejected.
291
332
  */
292
- const NON_LISTING_CATEGORY_TYPES = new Set(["brand", "bundle", "sublisting"]);
293
333
  async function fetchCategoryUrls(baseUrl) {
294
334
  try {
295
335
  const db = getAdminDb();
296
- const snap = await db
336
+ const docs = await readAllDocs(db
297
337
  .collection(CATEGORIES_COLLECTION)
298
338
  .where(CATEGORY_FIELDS.IS_ACTIVE, "==", true)
299
- .select(CATEGORY_FIELDS.SLUG, CATEGORY_FIELDS.UPDATED_AT, CATEGORY_FIELDS.CATEGORY_TYPE, TEST_DATA_FIELD)
300
- .limit(500)
301
- .get();
302
- return snap.docs
339
+ .select(CATEGORY_FIELDS.SLUG, CATEGORY_FIELDS.UPDATED_AT, CATEGORY_FIELDS.CATEGORY_TYPE, TEST_DATA_FIELD), "category");
340
+ return docs
303
341
  .filter((doc) => {
304
342
  const data = doc.data();
305
343
  if (isTestDoc(data))
306
344
  return false;
307
- const kind = data[CATEGORY_FIELDS.CATEGORY_TYPE];
308
- return typeof kind !== "string" || !NON_LISTING_CATEGORY_TYPES.has(kind);
345
+ return isListingCategory({
346
+ categoryType: data[CATEGORY_FIELDS.CATEGORY_TYPE],
347
+ });
309
348
  })
310
349
  .map((doc) => {
311
350
  const data = doc.data();
@@ -27,7 +27,7 @@ import type { StoreDetail, StoreListItem } from "../../../../features/stores/typ
27
27
  */
28
28
  export declare const PUBLIC_STORE_FIELDS: readonly ["id", "storeSlug", "ownerId", "storeName", "storeDescription", "storeCategory", "storeLogoURL", "storeBannerURL", "status", "isPublic", "stats", "bio", "location", "website", "socialLinks", "returnPolicy", "shippingPolicy", "isVacationMode", "vacationMessage", "createdAt"];
29
29
  /** Everything else, with the reason it stays server-side. */
30
- export declare const PRIVATE_STORE_FIELDS: readonly ["searchTxt", "updatedAt", "whatsappConfig", "adminNotes", "statusHistory", "statusHistoryTruncated", "isVerified", "suspensionReason", "capabilities", "customCommissionRate", "googleReviews", "shippingConfig", "emiEnabled", "isFeatured", "vacationReturnDate", "isTestData", "testDataExpiresAt"];
30
+ export declare const PRIVATE_STORE_FIELDS: readonly ["searchTxt", "updatedAt", "whatsappConfig", "adminNotes", "statusHistory", "statusHistoryTruncated", "isVerified", "suspensionReason", "capabilities", "customCommissionRate", "googleReviews", "shippingConfig", "emiEnabled", "pricesIncludeGst", "isFeatured", "vacationReturnDate", "isTestData", "testDataExpiresAt"];
31
31
  /**
32
32
  * Accepted input: a `StoreDocument` (the common case) or a loosely-typed
33
33
  * Firestore row. The legacy top-level counters below predate `stats` and are
@@ -46,6 +46,12 @@ export const PRIVATE_STORE_FIELDS = [
46
46
  "googleReviews", // placeId is billed-API config; the RSC reads it server-side
47
47
  "shippingConfig", // resolved server-side into per-item shipping at cart time
48
48
  "emiEnabled", // read server-side by the checkout/product RSCs, never from a public payload
49
+ // A seller AUTHORING preference with no public reader by construction: the
50
+ // stored `price` is always tax-inclusive, so a buyer never needs to know how
51
+ // the seller typed it. Read only by `ProductRepository` at write time, and
52
+ // publishing it would invite a client to "correct" a price that is already
53
+ // correct.
54
+ "pricesIncludeGst",
49
55
  "isFeatured", // ranking input; not part of the public store profile
50
56
  "vacationReturnDate", // not rendered by StoreAboutView; add to PUBLIC when a reader exists
51
57
  "isTestData", // tester-sandbox tagging; filtered by filterTestDataForViewer
@@ -7,6 +7,8 @@ export declare const couponExpiry: import("./types").ScheduledFunctionDefinition
7
7
  export declare const offerExpiry: import("./types").ScheduledFunctionDefinition;
8
8
  export declare const productStatsSync: import("./types").ScheduledFunctionDefinition;
9
9
  export declare const revenueRollup: import("./types").ScheduledFunctionDefinition;
10
+ export declare const priceIndexRollup: import("./types").ScheduledFunctionDefinition;
11
+ export declare const analyticsAlertEvaluate: import("./types").ScheduledFunctionDefinition;
10
12
  export declare const pageViewRollup: import("./types").ScheduledFunctionDefinition;
11
13
  export declare const pageViewPrune: import("./types").ScheduledFunctionDefinition;
12
14
  export declare const dailyStatusDigest: import("./types").ScheduledFunctionDefinition;
@@ -20,9 +22,9 @@ export declare const autoPayoutEligibility: import("./types").ScheduledFunctionD
20
22
  export declare const cleanupRtdbEvents: import("./types").ScheduledFunctionDefinition;
21
23
  export declare const mediaTmpCleanup: import("./types").ScheduledFunctionDefinition;
22
24
  export declare const draftPrune: import("./types").ScheduledFunctionDefinition;
23
- export declare const testerSandboxCleanup: import("./types").ScheduledFunctionDefinition;
25
+ export declare const testDataCleanup: import("./types").ScheduledFunctionDefinition;
24
26
  export declare const prizeDrawExpiryReveal: import("./types").ScheduledFunctionDefinition;
25
27
  export declare const bundleStockSync: import("./types").ScheduledFunctionDefinition;
26
28
  export declare const emiInstallmentReminder: import("./types").ScheduledFunctionDefinition;
27
29
  export declare const catalogueImageStalenessReminder: import("./types").ScheduledFunctionDefinition;
28
- export declare const SCHEDULED_FUNCTIONS: readonly [import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition];
30
+ export declare const SCHEDULED_FUNCTIONS: readonly [import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition];
@@ -1,6 +1,6 @@
1
1
  // Scheduled function definitions (Cloud Scheduler triggers).
2
2
  // Cron syntax accepts both Firebase shorthand and standard cron expressions.
3
- import { auctionSettlementHandler, autoPayoutEligibilityHandler, bundleStockSyncHandler, cartPruneHandler, cleanupRtdbEventsHandler, countersReconcileHandler, couponExpiryHandler, dailyDataCleanupHandler, draftPruneHandler, emiInstallmentReminderHandler, catalogueImageStalenessReminderHandler, mediaTmpCleanupHandler, notificationPruneHandler, offerExpiryHandler, paymentWindowTimeoutHandler, hardBanReinstatementHandler, paymentReviewAutoApproveHandler, pendingOrderTimeoutHandler, positionsReconcileHandler, prizeDrawExpiryRevealHandler, productStatsSyncHandler, revenueRollupHandler, pageViewRollupHandler, pageViewPruneHandler, dailyStatusDigestHandler, weeklyPayoutEligibilityHandler, testerSandboxCleanupHandler, } from "../jobs/handlers";
3
+ import { auctionSettlementHandler, autoPayoutEligibilityHandler, bundleStockSyncHandler, cartPruneHandler, cleanupRtdbEventsHandler, countersReconcileHandler, couponExpiryHandler, dailyDataCleanupHandler, draftPruneHandler, emiInstallmentReminderHandler, catalogueImageStalenessReminderHandler, mediaTmpCleanupHandler, notificationPruneHandler, offerExpiryHandler, paymentWindowTimeoutHandler, hardBanReinstatementHandler, paymentReviewAutoApproveHandler, pendingOrderTimeoutHandler, positionsReconcileHandler, prizeDrawExpiryRevealHandler, productStatsSyncHandler, revenueRollupHandler, priceIndexRollupHandler, analyticsAlertEvaluateHandler, pageViewRollupHandler, pageViewPruneHandler, dailyStatusDigestHandler, weeklyPayoutEligibilityHandler, testDataCleanupHandler, } from "../jobs/handlers";
4
4
  import { defineFunction } from "./define";
5
5
  const REGION = "asia-south1";
6
6
  const EVERY_5_MIN = "every 5 minutes";
@@ -67,6 +67,49 @@ export const revenueRollup = defineFunction({
67
67
  handler: revenueRollupHandler,
68
68
  options: { region: REGION, timeoutSeconds: 300, memory: "256MiB", maxInstances: 1 },
69
69
  });
70
+ /*
71
+ * The realised-price index — 04:30 UTC, i.e. **10:00 IST**, the cadence the
72
+ * page states as "prices as of".
73
+ *
74
+ * 🛑 Its OWN function rather than a pass inside `countersReconcile`, which
75
+ * already runs at its 540s ceiling. A separate job can fail without taking
76
+ * category metrics down with it, and the two share nothing beyond reading
77
+ * products.
78
+ *
79
+ * It writes `priceIndex/{leafId}` and never `categories` — see the
80
+ * repository's header for why that is not a preference. A job writing a price
81
+ * band onto the collection `onCategoryWrite` watches is Root Cause #92's exact
82
+ * shape (1,017,548 invocations in 24h), and `notifyRevalidate` would
83
+ * additionally drop each category page's ISR entry nightly on a project that
84
+ * was paused for exceeding ISR writes by 15.5×.
85
+ */
86
+ export const priceIndexRollup = defineFunction({
87
+ name: "priceIndexRollup",
88
+ description: "Roll up realised sale prices per leaf category into priceIndex/{leafId} (daily 04:30 UTC = 10:00 IST).",
89
+ trigger: { kind: "schedule", cron: "30 4 * * *", timeZone: "UTC" },
90
+ handler: priceIndexRollupHandler,
91
+ options: { region: REGION, timeoutSeconds: 540, memory: "512MiB", maxInstances: 1 },
92
+ });
93
+ /*
94
+ * Evaluate the alert rules sellers and admins configured — hourly, 20 past.
95
+ *
96
+ * 🛑 This feature had a schema, a CRUD UI, two API routes, four seeded
97
+ * fixtures and **no evaluator anywhere**: zero references across all 59 job
98
+ * files, with `lastTriggeredAt` declared and written nowhere. So every alert a
99
+ * seller configured could never fire.
100
+ *
101
+ * Hourly rather than daily, because an alert whose point is "tell me when
102
+ * nothing is selling" is useless a day late — and the job's own 24h
103
+ * re-notify cooldown is what stops hourly evaluation becoming hourly mail.
104
+ * 20 past the hour keeps it clear of the dense :00/:15 band.
105
+ */
106
+ export const analyticsAlertEvaluate = defineFunction({
107
+ name: "analyticsAlertEvaluate",
108
+ description: "Evaluate seller and admin analytics alerts against live metrics and notify on a crossing (hourly at :20).",
109
+ trigger: { kind: "schedule", cron: "20 * * * *", timeZone: "UTC" },
110
+ handler: analyticsAlertEvaluateHandler,
111
+ options: { region: REGION, timeoutSeconds: 300, memory: "256MiB", maxInstances: 1 },
112
+ });
70
113
  /*
71
114
  * The rollup runs BEFORE the prune, and both before the retention boundary
72
115
  * moves. `pageViews` had neither until 2026-08-31: it wrote one document per
@@ -168,11 +211,24 @@ export const draftPrune = defineFunction({
168
211
  handler: draftPruneHandler,
169
212
  options: { region: REGION, timeoutSeconds: 300, memory: "256MiB", maxInstances: 1 },
170
213
  });
171
- export const testerSandboxCleanup = defineFunction({
172
- name: "testerSandboxCleanup",
173
- description: "Delete expired tester QA sandbox test data (daily 05:00 UTC).",
214
+ /*
215
+ * 🛑 RENAMED from `testerSandboxCleanup` (2026-10-10, plan §7). The job never
216
+ * had anything to do with the tester HARNESS — it sweeps any document carrying
217
+ * `isTestData: true` past its `testDataExpiresAt`, which is a seed-data concern
218
+ * and outlives the human tester programme being retired in B2.
219
+ *
220
+ * 🛑 A Scheduler job rename is DELETE-AND-RECREATE on deploy, not a rename:
221
+ * `firebase deploy --only functions` creates `testDataCleanup` and leaves
222
+ * `testerSandboxCleanup` running until it is explicitly deleted. Two jobs
223
+ * sweeping the same documents is not redundancy — it is the two-owners race
224
+ * that got `testerSandboxRefresh` removed (see the note below). Delete the old
225
+ * function in the same deploy.
226
+ */
227
+ export const testDataCleanup = defineFunction({
228
+ name: "testDataCleanup",
229
+ description: "Delete expired isTestData documents past their TTL (daily 05:00 UTC).",
174
230
  trigger: { kind: "schedule", cron: "0 5 * * *", timeZone: "UTC" },
175
- handler: testerSandboxCleanupHandler,
231
+ handler: testDataCleanupHandler,
176
232
  options: { region: REGION, timeoutSeconds: 300, memory: "256MiB", maxInstances: 1 },
177
233
  });
178
234
  // testerSandboxRefresh (every 4h: revert fixtures to seed shape, prune extras) was
@@ -220,6 +276,8 @@ export const SCHEDULED_FUNCTIONS = [
220
276
  offerExpiry,
221
277
  productStatsSync,
222
278
  revenueRollup,
279
+ priceIndexRollup,
280
+ analyticsAlertEvaluate,
223
281
  pageViewRollup,
224
282
  pageViewPrune,
225
283
  dailyStatusDigest,
@@ -233,7 +291,7 @@ export const SCHEDULED_FUNCTIONS = [
233
291
  cleanupRtdbEvents,
234
292
  mediaTmpCleanup,
235
293
  draftPrune,
236
- testerSandboxCleanup,
294
+ testDataCleanup,
237
295
  prizeDrawExpiryReveal,
238
296
  bundleStockSync,
239
297
  emiInstallmentReminder,
@@ -23,7 +23,6 @@ export interface AdminAnalyticsResult {
23
23
  revenue: number;
24
24
  orders: number;
25
25
  mainImage: string;
26
- viewCount: number;
27
26
  }>;
28
27
  }
29
28
  export declare function runAdminAnalytics(_input: void, ctx: JobContext): Promise<AdminAnalyticsResult>;
@@ -82,7 +82,15 @@ export async function runAdminAnalytics(_input, ctx) {
82
82
  const topProductsWithImages = topProducts.map((p, i) => ({
83
83
  ...p,
84
84
  mainImage: topProductDocs[i]?.mainImage ?? "",
85
- viewCount: topProductDocs[i]?.viewCount ?? 0,
85
+ /*
86
+ * 🛑 `viewCount: … ?? 0` removed 2026-10-10. The `?? 0` was the active
87
+ * ingredient: `viewCount` is absent from every product document, and
88
+ * coercing that absence to `0` made it pass `AdminTopProductsTable`'s
89
+ * `!= null` guard, so the table rendered a permanent "0 views".
90
+ *
91
+ * Dropping the key leaves it `undefined`, which is the truth. Real counts
92
+ * come from the `pageViews` collection once it is rolled up.
93
+ */
86
94
  }));
87
95
  return {
88
96
  summary: {
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Core: evaluate the alert rules sellers and admins have configured — C3b.
3
+ *
4
+ * ## 🛑 The defect this closes: there was no evaluator at all
5
+ *
6
+ * `analyticsAlerts` had a schema, a seller CRUD UI, two API routes and four
7
+ * seeded fixtures — and **zero references across all 59 job files and
8
+ * `functions/`**. `lastTriggeredAt` was declared and written nowhere. So every
9
+ * alert a seller configured, and every one the seed created, could never fire.
10
+ *
11
+ * CLAUDE.md overstates this feature for exactly that reason: the parts that
12
+ * exist look complete, and the one that was missing is the one nothing points
13
+ * at. It also hid the metric-vocabulary split that C3a closed — two disjoint
14
+ * lists of metric names cannot be noticed while nothing reads the field.
15
+ *
16
+ * ## The switch is EXHAUSTIVE over `AlertMetric`
17
+ *
18
+ * `satisfies never` on the default branch, so adding a vocabulary member
19
+ * without an implementation does not compile. That is the mechanism behind the
20
+ * vocabulary's rule — *a metric is offerable only if the evaluator computes
21
+ * it* — rather than a convention someone has to remember.
22
+ *
23
+ * ## Read cost, and why it is bounded
24
+ *
25
+ * Metrics are computed ONCE PER OWNER, not once per alert: a seller with four
26
+ * alerts on orders pays for one orders query. Each query is bounded by
27
+ * `QUERY_LIMIT` and every count is reported as saturated when it hits the cap,
28
+ * because a count that silently stopped at 500 is a threshold comparison
29
+ * against a number that is not the number.
30
+ *
31
+ * 🛑 It does NOT scan per-product for `low_stock`. `availableQuantity <= N` is
32
+ * an inequality, and pairing one with an arbitrary sort is what demands an
33
+ * index nobody declares (Root Cause #59) — so the window is bounded and the
34
+ * predicate runs in memory, the same asymmetry § Availability describes.
35
+ */
36
+ import type { AlertOperator, AnalyticsAlertDocument } from "../../../../features/store-extensions/schemas/firestore";
37
+ import type { JobContext } from "../runtime/types";
38
+ /** Compare, with the operator the alert carries. */
39
+ export declare function alertFires(value: number, operator: AlertOperator, threshold: number): boolean;
40
+ /**
41
+ * Whether enough time has passed to notify again.
42
+ *
43
+ * An alert that has NEVER fired always notifies — absence of
44
+ * `lastTriggeredAt` means "not yet", never "recently".
45
+ */
46
+ export declare function mayRenotify(alert: AnalyticsAlertDocument, now: Date): boolean;
47
+ export declare function runAnalyticsAlertEvaluate(ctx: JobContext): Promise<void>;