@mohasinac/appkit 4.43.0 → 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 (220) hide show
  1. package/dist/_internal/server/features/categories/adapters.d.ts +47 -0
  2. package/dist/_internal/server/features/categories/adapters.js +235 -0
  3. package/dist/_internal/server/features/checkout/actions.js +161 -22
  4. package/dist/_internal/server/features/checkout/expand-order-items.js +34 -0
  5. package/dist/_internal/server/features/orders/adapters.js +21 -0
  6. package/dist/_internal/server/features/refunds/actions.js +1 -1
  7. package/dist/_internal/server/features/seo/sitemap.js +54 -15
  8. package/dist/_internal/server/features/stores/adapters.d.ts +1 -1
  9. package/dist/_internal/server/features/stores/adapters.js +6 -0
  10. package/dist/_internal/server/functions/scheduled.d.ts +4 -2
  11. package/dist/_internal/server/functions/scheduled.js +64 -6
  12. package/dist/_internal/server/jobs/core/adminAnalytics.d.ts +0 -1
  13. package/dist/_internal/server/jobs/core/adminAnalytics.js +9 -1
  14. package/dist/_internal/server/jobs/core/analyticsAlertEvaluate.d.ts +47 -0
  15. package/dist/_internal/server/jobs/core/analyticsAlertEvaluate.js +468 -0
  16. package/dist/_internal/server/jobs/core/autoPayoutEligibility.js +14 -1
  17. package/dist/_internal/server/jobs/core/countersReconcile.js +24 -8
  18. package/dist/_internal/server/jobs/core/onCategoryWrite.js +54 -0
  19. package/dist/_internal/server/jobs/core/priceIndexRollup.d.ts +47 -0
  20. package/dist/_internal/server/jobs/core/priceIndexRollup.js +234 -0
  21. package/dist/_internal/server/jobs/core/{testerSandboxCleanup.d.ts → testDataCleanup.d.ts} +3 -3
  22. package/dist/_internal/server/jobs/core/{testerSandboxCleanup.js → testDataCleanup.js} +3 -3
  23. package/dist/_internal/server/jobs/core/weeklyPayoutEligibility.js +5 -1
  24. package/dist/_internal/server/jobs/handlers/analyticsAlertEvaluate.d.ts +2 -0
  25. package/dist/_internal/server/jobs/handlers/analyticsAlertEvaluate.js +2 -0
  26. package/dist/_internal/server/jobs/handlers/index.d.ts +3 -1
  27. package/dist/_internal/server/jobs/handlers/index.js +3 -1
  28. package/dist/_internal/server/jobs/handlers/messages.d.ts +5 -2
  29. package/dist/_internal/server/jobs/handlers/messages.js +5 -2
  30. package/dist/_internal/server/jobs/handlers/priceIndexRollup.d.ts +2 -0
  31. package/dist/_internal/server/jobs/handlers/priceIndexRollup.js +2 -0
  32. package/dist/_internal/server/jobs/handlers/testDataCleanup.d.ts +2 -0
  33. package/dist/_internal/server/jobs/handlers/testDataCleanup.js +2 -0
  34. package/dist/_internal/shared/actions/action-registry.js +34 -0
  35. package/dist/_internal/shared/checkout/order-math.d.ts +113 -3
  36. package/dist/_internal/shared/checkout/order-math.js +194 -7
  37. package/dist/_internal/shared/features/categories/price-guidance.d.ts +110 -0
  38. package/dist/_internal/shared/features/categories/price-guidance.js +136 -0
  39. package/dist/_internal/shared/features/site-settings/gst-registration.d.ts +90 -0
  40. package/dist/_internal/shared/features/site-settings/gst-registration.js +126 -0
  41. package/dist/_internal/shared/fees/calculator.d.ts +105 -0
  42. package/dist/_internal/shared/fees/calculator.js +143 -0
  43. package/dist/_internal/shared/listing-types/art/config.d.ts +0 -6
  44. package/dist/_internal/shared/listing-types/classified/config.d.ts +0 -6
  45. package/dist/_internal/shared/listing-types/digital-code/config.d.ts +0 -6
  46. package/dist/_internal/shared/listing-types/live/config.d.ts +0 -6
  47. package/dist/_internal/shared/listing-types/standard/config.d.ts +0 -6
  48. package/dist/_internal/shared/listing-types/stickers/config.d.ts +0 -6
  49. package/dist/_internal/shared/templating/placeholders.d.ts +70 -0
  50. package/dist/_internal/shared/templating/placeholders.js +73 -0
  51. package/dist/client.d.ts +2 -0
  52. package/dist/client.js +1 -0
  53. package/dist/constants/api-endpoints.d.ts +42 -0
  54. package/dist/constants/api-endpoints.js +14 -0
  55. package/dist/features/admin/actions/admin-actions.d.ts +1 -1
  56. package/dist/features/admin/actions/admin-actions.js +1 -1
  57. package/dist/features/admin/components/AdminCategoryEditorView.js +269 -3
  58. package/dist/features/admin/components/AdminProductEditorView.js +8 -1
  59. package/dist/features/admin/components/AdminProductsView.js +78 -7
  60. package/dist/features/admin/components/AdminSiteSettingsView.js +20 -1
  61. package/dist/features/admin/components/AdminTaxCodeEditorView.d.ts +10 -0
  62. package/dist/features/admin/components/AdminTaxCodeEditorView.js +160 -0
  63. package/dist/features/admin/components/AdminTaxCodesView.d.ts +13 -0
  64. package/dist/features/admin/components/AdminTaxCodesView.js +86 -0
  65. package/dist/features/admin/components/AdminTesterChecklistView.js +1 -1
  66. package/dist/features/admin/components/analytics/AdminTopProductsTable.js +1 -1
  67. package/dist/features/auth/schemas/firestore.d.ts +0 -1
  68. package/dist/features/blog/schemas/firestore.d.ts +2 -2
  69. package/dist/features/catalogue/components/CatalogueItemEditorView.js +14 -3
  70. package/dist/features/categories/api/[id]/description-template/route.d.ts +45 -0
  71. package/dist/features/categories/api/[id]/description-template/route.js +252 -0
  72. package/dist/features/categories/api/route.js +156 -11
  73. package/dist/features/categories/components/CategoryDetailPageView.js +43 -10
  74. package/dist/features/categories/components/CategoryTemplatePicker.d.ts +65 -0
  75. package/dist/features/categories/components/CategoryTemplatePicker.js +170 -0
  76. package/dist/features/categories/constants/listing-categories.d.ts +17 -0
  77. package/dist/features/categories/constants/listing-categories.js +53 -0
  78. package/dist/features/categories/hooks/useCategoryTree.js +28 -2
  79. package/dist/features/categories/repository/categories.repository.d.ts +62 -11
  80. package/dist/features/categories/repository/categories.repository.js +178 -35
  81. package/dist/features/categories/repository/price-index.repository.d.ts +115 -0
  82. package/dist/features/categories/repository/price-index.repository.js +85 -0
  83. package/dist/features/categories/schemas/category-content.d.ts +206 -0
  84. package/dist/features/categories/schemas/category-content.js +28 -0
  85. package/dist/features/categories/schemas/firestore.d.ts +74 -5
  86. package/dist/features/categories/schemas/firestore.js +74 -3
  87. package/dist/features/categories/schemas/index.d.ts +1047 -0
  88. package/dist/features/categories/schemas/index.js +96 -0
  89. package/dist/features/categories/server.d.ts +1 -0
  90. package/dist/features/categories/server.js +7 -0
  91. package/dist/features/categories/types/index.d.ts +9 -0
  92. package/dist/features/events/schemas/firestore.d.ts +2 -2
  93. package/dist/features/faq/schemas/firestore.d.ts +2 -1
  94. package/dist/features/faq/schemas/firestore.js +20 -8
  95. package/dist/features/orders/schemas/firestore.d.ts +70 -0
  96. package/dist/features/orders/types/index.d.ts +55 -0
  97. package/dist/features/products/columns/index.d.ts +0 -1
  98. package/dist/features/products/columns/index.js +5 -1
  99. package/dist/features/products/components/ProductFilters.d.ts +0 -9
  100. package/dist/features/products/components/ProductForm.d.ts +27 -1
  101. package/dist/features/products/components/ProductForm.js +26 -12
  102. package/dist/features/products/constants/sieve.d.ts +0 -6
  103. package/dist/features/products/constants/sieve.js +35 -2
  104. package/dist/features/products/repository/products.repository.d.ts +115 -1
  105. package/dist/features/products/repository/products.repository.js +491 -39
  106. package/dist/features/products/schemas/firestore.d.ts +65 -5
  107. package/dist/features/products/schemas/firestore.js +9 -0
  108. package/dist/features/products/schemas/product-features.d.ts +140 -2
  109. package/dist/features/products/schemas/product-features.js +62 -1
  110. package/dist/features/promotions/repository/coupons.repository.js +6 -1
  111. package/dist/features/promotions/schemas/firestore.d.ts +44 -1
  112. package/dist/features/promotions/schemas/firestore.js +76 -2
  113. package/dist/features/seller/actions/seller-actions.js +17 -2
  114. package/dist/features/seller/components/PrintCenterView.d.ts +3 -2
  115. package/dist/features/seller/components/PrintCenterView.js +3 -2
  116. package/dist/features/seller/components/SellerAnalyticsAlertsView.js +21 -11
  117. package/dist/features/seller/components/SellerOrdersView.js +6 -1
  118. package/dist/features/seller/components/SellerPayoutSettingsView.js +19 -3
  119. package/dist/features/seller/components/SellerProductShell.d.ts +50 -2
  120. package/dist/features/seller/components/SellerProductShell.js +101 -9
  121. package/dist/features/seller/components/SellerProductsFilterDrawer.d.ts +13 -1
  122. package/dist/features/seller/components/SellerProductsFilterDrawer.js +2 -2
  123. package/dist/features/seller/components/SellerProductsView.js +108 -3
  124. package/dist/features/seller/types/index.d.ts +17 -0
  125. package/dist/features/seller/utils/product-draft-mapping.d.ts +13 -0
  126. package/dist/features/seller/utils/product-draft-mapping.js +17 -0
  127. package/dist/features/store-extensions/constants/alert-metrics.d.ts +86 -0
  128. package/dist/features/store-extensions/constants/alert-metrics.js +142 -0
  129. package/dist/features/store-extensions/schemas/firestore.d.ts +19 -1
  130. package/dist/features/stores/schemas/firestore.d.ts +31 -2
  131. package/dist/features/tax-codes/repository/tax-codes.repository.d.ts +55 -0
  132. package/dist/features/tax-codes/repository/tax-codes.repository.js +131 -0
  133. package/dist/features/tax-codes/schemas/firestore.d.ts +125 -0
  134. package/dist/features/tax-codes/schemas/firestore.js +82 -0
  135. package/dist/features/tax-codes/schemas/tax-code-form.d.ts +63 -0
  136. package/dist/features/tax-codes/schemas/tax-code-form.js +152 -0
  137. package/dist/features/tax-codes/schemas/tax-codes.validators.d.ts +107 -0
  138. package/dist/features/tax-codes/schemas/tax-codes.validators.js +85 -0
  139. package/dist/features/tester/actions/index.d.ts +0 -1
  140. package/dist/features/tester/actions/index.js +0 -1
  141. package/dist/features/tester/components/AdminTesterFeedbackIssuesView.js +1 -1
  142. package/dist/features/tester/components/AdminTesterFeedbackListView.js +1 -1
  143. package/dist/features/tester/components/index.d.ts +0 -4
  144. package/dist/features/tester/components/index.js +0 -2
  145. package/dist/features/tester/repository/tester-checklist-item.repository.d.ts +1 -20
  146. package/dist/features/tester/repository/tester-checklist-item.repository.js +0 -52
  147. package/dist/features/tester/schemas/firestore.d.ts +0 -6
  148. package/dist/features/tester/seed-data/authored/admin__admin-detail-round-trips.js +3 -3
  149. package/dist/features/tester/seed-data/authored/admin__site-system.js +4 -5
  150. package/dist/features/tester/seed-data/authored/content-discovery__category-counts-and-rollup.js +2 -2
  151. package/dist/features/tester/seed-data/authored/index.js +0 -2
  152. package/dist/features/tester/seed-data/authored/page-wiring__reachability.js +0 -19
  153. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +8 -27
  154. package/dist/index.d.ts +21 -7
  155. package/dist/index.js +53 -7
  156. package/dist/next/routing/route-map.d.ts +2 -4
  157. package/dist/next/routing/route-map.js +1 -2
  158. package/dist/repositories/index.d.ts +1 -0
  159. package/dist/repositories/index.js +1 -0
  160. package/dist/schemas/registry.d.ts +506 -0
  161. package/dist/seed/_helpers/brand-rows.d.ts +35 -0
  162. package/dist/seed/_helpers/brand-rows.js +576 -0
  163. package/dist/seed/_helpers/category-content.d.ts +39 -0
  164. package/dist/seed/_helpers/category-content.js +634 -0
  165. package/dist/seed/_helpers/category-forest.d.ts +2 -0
  166. package/dist/seed/_helpers/category-forest.js +585 -243
  167. package/dist/seed/_helpers/category-models.d.ts +5 -0
  168. package/dist/seed/_helpers/category-models.js +1364 -0
  169. package/dist/seed/_helpers/category-tree.js +17 -1
  170. package/dist/seed/_helpers/character-features.d.ts +10 -0
  171. package/dist/seed/_helpers/character-features.js +118 -0
  172. package/dist/seed/_helpers/search-txt-wrappers.d.ts +10 -0
  173. package/dist/seed/_helpers/search-txt-wrappers.js +12 -1
  174. package/dist/seed/categories-seed-data.js +147 -169
  175. package/dist/seed/index.d.ts +1 -0
  176. package/dist/seed/index.js +1 -0
  177. package/dist/seed/manifest.d.ts +1 -1
  178. package/dist/seed/manifest.js +5 -0
  179. package/dist/seed/product-features-seed-data.d.ts +1 -8
  180. package/dist/seed/product-features-seed-data.js +358 -154
  181. package/dist/seed/products-art-seed-data.js +16 -4
  182. package/dist/seed/products-auctions-seed-data.js +6 -6
  183. package/dist/seed/products-classifieds-seed-data.js +17 -7
  184. package/dist/seed/products-digital-codes-seed-data.js +12 -12
  185. package/dist/seed/products-preorders-seed-data.js +8 -8
  186. package/dist/seed/products-prize-draws-seed-data.js +25 -8
  187. package/dist/seed/products-standard-seed-data.js +10 -60
  188. package/dist/seed/products-stickers-seed-data.js +6 -6
  189. package/dist/seed/site-settings-seed-data.js +24 -0
  190. package/dist/seed/store-extensions-seed-data.js +10 -4
  191. package/dist/seed/tax-codes-seed-data.d.ts +34 -0
  192. package/dist/seed/tax-codes-seed-data.js +100 -0
  193. package/dist/seed/test-data-ttl.d.ts +29 -0
  194. package/dist/seed/test-data-ttl.js +31 -0
  195. package/dist/server-entry.d.ts +1 -1
  196. package/dist/server-entry.js +6 -1
  197. package/dist/server.d.ts +2 -1
  198. package/dist/server.js +5 -2
  199. package/dist/styles.css +40 -0
  200. package/dist/ui/components/PaginatedSelect.d.ts +14 -0
  201. package/dist/ui/components/PaginatedSelect.js +0 -0
  202. package/dist/ui/components/PaginatedSelect.style.css +40 -0
  203. package/dist/utils/physical-location.d.ts +23 -0
  204. package/dist/utils/physical-location.js +52 -0
  205. package/dist/utils/search-txt-builders.d.ts +20 -0
  206. package/dist/utils/search-txt-builders.js +36 -0
  207. package/package.json +1 -1
  208. package/scripts/seed-cli.mjs +4 -1
  209. package/dist/_internal/server/jobs/handlers/testerSandboxCleanup.d.ts +0 -2
  210. package/dist/_internal/server/jobs/handlers/testerSandboxCleanup.js +0 -2
  211. package/dist/features/tester/actions/leaderboard-actions.d.ts +0 -3
  212. package/dist/features/tester/actions/leaderboard-actions.js +0 -5
  213. package/dist/features/tester/components/BugHunterLeaderboardView.d.ts +0 -23
  214. package/dist/features/tester/components/BugHunterLeaderboardView.js +0 -18
  215. package/dist/features/tester/components/TesterHubView.d.ts +0 -5
  216. package/dist/features/tester/components/TesterHubView.js +0 -110
  217. package/dist/features/tester/seed-data/authored/public-pages__bug-hunters.d.ts +0 -2
  218. package/dist/features/tester/seed-data/authored/public-pages__bug-hunters.js +0 -78
  219. package/dist/features/tester/seed-data/tester-ttl.d.ts +0 -4
  220. package/dist/features/tester/seed-data/tester-ttl.js +0 -6
@@ -0,0 +1,234 @@
1
+ /**
2
+ * Core: the realised-price index — what things actually SOLD for here.
3
+ *
4
+ * ## Why this exists
5
+ *
6
+ * Every price signal we show a seller today is an ASK: the seeded `market`
7
+ * band is a competitor's listed price, and a competitor's sold-out row proves
8
+ * the item is GONE, not that it sold at that figure. With 76–93% of listings
9
+ * across the crawled catalogues never selling, the gap between an ask and a
10
+ * realised price is the single most useful thing we can tell a seller — and we
11
+ * are the only party who can measure it on our own sales.
12
+ *
13
+ * A seeded blog post already tells sellers to *"check what similar items sold
14
+ * for on LetItRip in the last 30 days"*, which has never been possible.
15
+ *
16
+ * ## 🛑 `market` and `sold` are NEVER blended
17
+ *
18
+ * They answer different questions and the gap IS the information. Averaging
19
+ * them would launder an inference into a measurement. This job writes only the
20
+ * sold half, into its own collection, and never touches the seeded band.
21
+ *
22
+ * ## The authoritative field
23
+ *
24
+ * **`OrderDocument.items[].unitPrice`** — and specifically not:
25
+ *
26
+ * `OrderDocument.unitPrice` the FIRST line's price (`unitPriceFor(items[0])`),
27
+ * wrong for every other line
28
+ * `OrderDocument.totalPrice` folds in GST, platform fee, shipping and add-ons
29
+ * `BidDocument.status==won` its own comment: `orderId` is absent on a
30
+ * won-but-unpaid bid
31
+ * `OfferDocument==accepted` an agreement, not a sale. `paid` is the sale
32
+ * `product.currentBid` a display mirror a pending buyout never writes
33
+ *
34
+ * Won auctions and accepted offers arrive through the same field via
35
+ * `lockedPrice`, so one field covers all six acquisition paths and
36
+ * `sourceContext` labels each for free.
37
+ *
38
+ * ## Why daily and never live
39
+ *
40
+ * A live index would scan orders per request — the unbounded-per-request
41
+ * pattern that paused this project on Hobby caps. One pre-computed read per
42
+ * row, with a visible "prices as of {date}". Deliberately one day stale.
43
+ */
44
+ import { orderRepository, productRepository } from "../../../../repositories";
45
+ import { priceIndexRepository, } from "../../../../features/categories/repository/price-index.repository";
46
+ /** How far back a band looks. */
47
+ export const PRICE_INDEX_WINDOW_DAYS = 90;
48
+ /**
49
+ * Hard ceiling on the products scan.
50
+ *
51
+ * The scan exists only to build `productId → categorySlugs[0]`, and that join
52
+ * disappears for future orders once `categorySlugs[0]` is snapshotted onto the
53
+ * order item (done in the same change as this job). So the cap is a guard
54
+ * against a catalogue that grows faster than the snapshot backfills, not a
55
+ * permanent design limit — and it is logged when it bites rather than
56
+ * truncating silently.
57
+ */
58
+ const PRODUCT_SCAN_LIMIT = 5000;
59
+ function toDate(v) {
60
+ if (!v)
61
+ return null;
62
+ const d = v instanceof Date ? v : new Date(v);
63
+ return Number.isNaN(d.getTime()) ? null : d;
64
+ }
65
+ /**
66
+ * Which acquisition path produced this sale.
67
+ *
68
+ * `sourceContext.path` is the richer signal — it distinguishes a settled
69
+ * auction win from a buy-now, which `orderType` cannot (both are `"auction"`).
70
+ * Falls back to `orderType`, then to `standard`.
71
+ */
72
+ function pathOf(order) {
73
+ const p = order.sourceContext?.path ?? order.orderType;
74
+ if (p === "auction-won" || p === "auction-buy-now" || p === "auction")
75
+ return "auction";
76
+ if (p === "offer-accepted" || p === "offer")
77
+ return "offer";
78
+ return "standard";
79
+ }
80
+ /**
81
+ * 🛑 Completed means delivered AND paid AND not refunded.
82
+ *
83
+ * The repo's convention elsewhere is `delivered` alone, and **nothing
84
+ * currently excludes a delivered-then-refunded order** — whose realised price
85
+ * is zero. Including one would publish a sale that was undone as evidence of
86
+ * what things sell for.
87
+ */
88
+ function isCompleted(order) {
89
+ if (order.status !== "delivered")
90
+ return false;
91
+ if (order.paymentStatus !== "paid")
92
+ return false;
93
+ return !(order.refunds && order.refunds.length > 0);
94
+ }
95
+ function median(sorted) {
96
+ const n = sorted.length;
97
+ if (n === 0)
98
+ return 0;
99
+ const mid = Math.floor(n / 2);
100
+ const raw = n % 2 === 1 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
101
+ return Math.round(raw * 100) / 100;
102
+ }
103
+ function toBand(samples) {
104
+ const amounts = samples.map((s) => s.amount).sort((a, b) => a - b);
105
+ const byPath = {};
106
+ for (const s of samples)
107
+ byPath[s.via] = (byPath[s.via] ?? 0) + 1;
108
+ const latest = samples.reduce((best, s) => (!best || s.at > best.at ? s : best), null);
109
+ return {
110
+ min: amounts[0],
111
+ median: median(amounts),
112
+ max: amounts[amounts.length - 1],
113
+ sampleSize: samples.length,
114
+ byPath,
115
+ ...(latest ? { lastSold: { amount: latest.amount, at: latest.at, via: latest.via } } : {}),
116
+ };
117
+ }
118
+ /** `YYYYMMDD` in UTC, so the id is stable regardless of where the job runs. */
119
+ function utcDateKey(d) {
120
+ return `${d.getUTCFullYear()}${String(d.getUTCMonth() + 1).padStart(2, "0")}${String(d.getUTCDate()).padStart(2, "0")}`;
121
+ }
122
+ export async function runPriceIndexRollup(ctx) {
123
+ const now = new Date();
124
+ const cutoff = new Date(now.getTime() - PRICE_INDEX_WINDOW_DAYS * 24 * 60 * 60 * 1000);
125
+ /*
126
+ * One products scan into a MAP, never N `findById` calls.
127
+ *
128
+ * `storeAnalytics.ts:94` does the per-item lookup and it is the pattern not
129
+ * to copy — an order with 4 items costs 4 reads there, so a 90-day window
130
+ * over a growing order list is an unbounded read multiplier.
131
+ */
132
+ const products = await productRepository.list({
133
+ page: 1,
134
+ pageSize: PRODUCT_SCAN_LIMIT,
135
+ });
136
+ const rows = products.items ?? [];
137
+ if (rows.length >= PRODUCT_SCAN_LIMIT) {
138
+ ctx.logger.warn("priceIndexRollup: product scan saturated — the leaf join may be incomplete", {
139
+ limit: PRODUCT_SCAN_LIMIT,
140
+ });
141
+ }
142
+ const leafOf = new Map();
143
+ const typeOf = new Map();
144
+ for (const p of rows) {
145
+ if (!p.id)
146
+ continue;
147
+ const leaf = p.categorySlugs?.[0];
148
+ if (leaf)
149
+ leafOf.set(p.id, leaf);
150
+ if (p.listingType)
151
+ typeOf.set(p.id, p.listingType);
152
+ }
153
+ const delivered = (await orderRepository.findByStatus("delivered"));
154
+ /** leafId → listingType → samples */
155
+ const byLeaf = new Map();
156
+ let considered = 0;
157
+ let skippedNoLeaf = 0;
158
+ for (const order of delivered) {
159
+ if (!isCompleted(order))
160
+ continue;
161
+ const at = toDate(order.orderDate) ?? toDate(order.createdAt);
162
+ if (!at || at < cutoff)
163
+ continue;
164
+ const via = pathOf(order);
165
+ for (const item of order.items ?? []) {
166
+ const amount = Number(item.unitPrice ?? 0);
167
+ if (!Number.isFinite(amount) || amount <= 0)
168
+ continue;
169
+ /*
170
+ * A fully-cancelled line is not a sale. A partially-cancelled one is —
171
+ * the units that shipped sold at that unit price, and the unit price is
172
+ * what a band is made of, so the quantity does not weight it.
173
+ */
174
+ const live = (item.quantity ?? 1) - (item.cancelledQuantity ?? 0);
175
+ if (live <= 0)
176
+ continue;
177
+ /*
178
+ * The snapshot first, the join second. New orders carry
179
+ * `categorySlug` and need no products read at all; older ones fall
180
+ * back to the map. An item whose product has since been deleted
181
+ * resolves to neither and is COUNTED AS SKIPPED rather than dropped
182
+ * silently — a band missing sales is indistinguishable from a quiet
183
+ * category unless somebody says so.
184
+ */
185
+ const leaf = item.categorySlug ?? (item.productId ? leafOf.get(item.productId) : undefined);
186
+ if (!leaf) {
187
+ skippedNoLeaf += 1;
188
+ continue;
189
+ }
190
+ const listingType = item.listingType ?? (item.productId ? typeOf.get(item.productId) : undefined) ?? "standard";
191
+ const types = byLeaf.get(leaf) ?? new Map();
192
+ const samples = types.get(listingType) ?? [];
193
+ samples.push({ amount, at, via });
194
+ types.set(listingType, samples);
195
+ byLeaf.set(leaf, types);
196
+ considered += 1;
197
+ }
198
+ }
199
+ const dateKey = utcDateKey(now);
200
+ let written = 0;
201
+ for (const [leafId, types] of byLeaf) {
202
+ const byListingType = {};
203
+ const point = {};
204
+ for (const [listingType, samples] of types) {
205
+ const band = toBand(samples);
206
+ byListingType[listingType] = band;
207
+ point[listingType] = { median: band.median, sampleSize: band.sampleSize };
208
+ }
209
+ /*
210
+ * 🛑 Only leaves WITH sales are written, and nothing is ever zeroed.
211
+ *
212
+ * An absent band means "we have no sales here", which the UI renders as
213
+ * silence. A zeroed one would render as "things sell here for ₹0", which
214
+ * is worse than silence and is exactly what a defaulting write would
215
+ * manufacture on the first quiet night. A refund removing the last sale in
216
+ * a leaf therefore leaves a stale band for a day and then it ages out of
217
+ * the window — which is the correct behaviour for a figure labelled "as
218
+ * of" a date.
219
+ */
220
+ await priceIndexRepository.setForLeaf(leafId, {
221
+ byListingType,
222
+ windowDays: PRICE_INDEX_WINDOW_DAYS,
223
+ });
224
+ await priceIndexRepository.setDailyPoint(leafId, dateKey, point);
225
+ written += 1;
226
+ }
227
+ ctx.logger.info("priceIndexRollup: complete", {
228
+ deliveredScanned: delivered.length,
229
+ salesCounted: considered,
230
+ leavesWritten: written,
231
+ skippedNoLeaf,
232
+ windowDays: PRICE_INDEX_WINDOW_DAYS,
233
+ });
234
+ }
@@ -14,9 +14,9 @@ import type { JobContext } from "../runtime/types";
14
14
  * (This used to be shared with testerSandboxRefresh.ts, which was removed when
15
15
  * the tester runner took over fixture state — see scheduled.ts.)
16
16
  */
17
- export declare const SANDBOX_COLLECTIONS: readonly ["categories", "stores", "products", "blogPosts", "events", "offers"];
18
- export interface TesterSandboxCleanupOptions {
17
+ export declare const TEST_DATA_COLLECTIONS: readonly ["categories", "stores", "products", "blogPosts", "events", "offers"];
18
+ export interface TestDataCleanupOptions {
19
19
  /** true = wipe every isTestData:true doc regardless of testDataExpiresAt (manual force-purge). */
20
20
  force?: boolean;
21
21
  }
22
- export declare function runTesterSandboxCleanup(ctx: JobContext, opts?: TesterSandboxCleanupOptions): Promise<void>;
22
+ export declare function runTestDataCleanup(ctx: JobContext, opts?: TestDataCleanupOptions): Promise<void>;
@@ -15,16 +15,16 @@ import { BID_FIELDS } from "../../../../constants/field-names";
15
15
  * (This used to be shared with testerSandboxRefresh.ts, which was removed when
16
16
  * the tester runner took over fixture state — see scheduled.ts.)
17
17
  */
18
- export const SANDBOX_COLLECTIONS = ["categories", "stores", "products", "blogPosts", "events", "offers"];
18
+ export const TEST_DATA_COLLECTIONS = ["categories", "stores", "products", "blogPosts", "events", "offers"];
19
19
  const BID_CHUNK_SIZE = 30; // Firestore `in` query cap
20
- export async function runTesterSandboxCleanup(ctx, opts = {}) {
20
+ export async function runTestDataCleanup(ctx, opts = {}) {
21
21
  const cutoff = opts.force ? null : new Date();
22
22
  ctx.logger.info(opts.force
23
23
  ? "Force-purging all tester sandbox test data"
24
24
  : "Sweeping expired tester sandbox test data");
25
25
  const allRefs = [];
26
26
  let deletedProductIds = [];
27
- for (const collection of SANDBOX_COLLECTIONS) {
27
+ for (const collection of TEST_DATA_COLLECTIONS) {
28
28
  const refs = await getTestDataRefs(ctx.db, collection, cutoff);
29
29
  allRefs.push(...refs);
30
30
  if (collection === "products") {
@@ -6,6 +6,7 @@ import { storeRepository, userRepository, orderRepository, payoutRepository, sit
6
6
  import { BATCH_LIMIT } from "../handlers/messages";
7
7
  import { getDefaultCurrency } from "../../../../core";
8
8
  import { computePayoutDeduction } from "../../../shared/fees/calculator";
9
+ import { ordersTaxableValue } from "../../../shared/checkout/order-math";
9
10
  export async function runWeeklyPayoutEligibility(ctx) {
10
11
  if (ctx.env("FEATURE_PAYOUTS") !== "true") {
11
12
  ctx.logger.info("FEATURE_PAYOUTS disabled — skipping weekly payout eligibility");
@@ -42,7 +43,10 @@ export async function runWeeklyPayoutEligibility(ctx) {
42
43
  ctx.logger.warn(`Store/seller ${storeId} not found — skipping ${orders.length} order(s)`);
43
44
  continue;
44
45
  }
45
- const grossAmount = orders.reduce((sum, o) => sum + (o.data.totalPrice ?? 0), 0);
46
+ /* The TAXABLE VALUE — see `orderTaxableValue`. This job and
47
+ * `autoPayoutEligibility` must agree, or the weekly run and the automatic
48
+ * one pay different amounts for the same orders. */
49
+ const grossAmount = ordersTaxableValue(orders.map((o) => o.data));
46
50
  const { platformFee, gatewayFee, gstOnFee, netAmount } = computePayoutDeduction(grossAmount, commissions);
47
51
  const payoutInput = {
48
52
  storeId,
@@ -0,0 +1,2 @@
1
+ import type { ScheduleHandler } from "../runtime/types";
2
+ export declare const analyticsAlertEvaluateHandler: ScheduleHandler;
@@ -0,0 +1,2 @@
1
+ import { runAnalyticsAlertEvaluate } from "../core/analyticsAlertEvaluate";
2
+ export const analyticsAlertEvaluateHandler = runAnalyticsAlertEvaluate;
@@ -28,6 +28,8 @@ export { hardBanReinstatementHandler } from "./hardBanReinstatement";
28
28
  export { paymentReviewAutoApproveHandler } from "./paymentReviewAutoApprove";
29
29
  export { productStatsSyncHandler } from "./productStatsSync";
30
30
  export { revenueRollupHandler } from "./revenueRollup";
31
+ export { priceIndexRollupHandler } from "./priceIndexRollup";
32
+ export { analyticsAlertEvaluateHandler } from "./analyticsAlertEvaluate";
31
33
  export { pageViewRollupHandler } from "./pageViewRollup";
32
34
  export { pageViewPruneHandler } from "./pageViewPrune";
33
35
  export { dailyStatusDigestHandler } from "./dailyStatusDigest";
@@ -52,7 +54,7 @@ export { onUserBanChangeHandler } from "./onUserBanChange";
52
54
  export { onScamReportCreateHandler } from "./onScamReportCreate";
53
55
  export { onScamReportUpdateHandler } from "./onScamReportUpdate";
54
56
  export { draftPruneHandler } from "./draftPrune";
55
- export { testerSandboxCleanupHandler } from "./testerSandboxCleanup";
57
+ export { testDataCleanupHandler } from "./testDataCleanup";
56
58
  export { emiInstallmentReminderHandler } from "./emiInstallmentReminder";
57
59
  export { onShipmentItemWriteHandler } from "./onShipmentItemWrite";
58
60
  export { onShipmentLotWriteHandler, onShipmentHeaderWriteHandler } from "./onShipmentAllocationSync";
@@ -28,6 +28,8 @@ export { hardBanReinstatementHandler } from "./hardBanReinstatement";
28
28
  export { paymentReviewAutoApproveHandler } from "./paymentReviewAutoApprove";
29
29
  export { productStatsSyncHandler } from "./productStatsSync";
30
30
  export { revenueRollupHandler } from "./revenueRollup";
31
+ export { priceIndexRollupHandler } from "./priceIndexRollup";
32
+ export { analyticsAlertEvaluateHandler } from "./analyticsAlertEvaluate";
31
33
  export { pageViewRollupHandler } from "./pageViewRollup";
32
34
  export { pageViewPruneHandler } from "./pageViewPrune";
33
35
  export { dailyStatusDigestHandler } from "./dailyStatusDigest";
@@ -64,7 +66,7 @@ export { onScamReportUpdateHandler } from "./onScamReportUpdate";
64
66
  // SB-UNI-Y-1 — 30-day draft prune
65
67
  export { draftPruneHandler } from "./draftPrune";
66
68
  // Tester program — 7-day auto-expiry sweep for the shared admin-seeded test sandbox
67
- export { testerSandboxCleanupHandler } from "./testerSandboxCleanup";
69
+ export { testDataCleanupHandler } from "./testDataCleanup";
68
70
  // Tester program — every-4h revert-live-edits + prune-extras refresh (faster than the 7-day sweep above)
69
71
  // EMI — installment reminder + overdue-flagging sweep
70
72
  export { emiInstallmentReminderHandler } from "./emiInstallmentReminder";
@@ -55,8 +55,11 @@ export declare const JOB_ERROR_MESSAGES: {
55
55
  export declare const CART_TTL_DAYS = 60;
56
56
  /** Default Notification TTL in days (matches functions/config/constants). */
57
57
  export declare const NOTIFICATION_TTL_DAYS = 30;
58
- /** Tester sandbox TTL in days — matches testDataExpiresAt() in features/tester/seed-data/tester-ttl.ts. */
59
- export declare const TESTER_SANDBOX_TTL_DAYS = 7;
58
+ /**
59
+ * Test-data TTL in days. RE-EXPORTED, not re-declared — this was a second
60
+ * literal `7` whose only tie to the real one was a comment saying it matched.
61
+ */
62
+ export { TEST_DATA_TTL_DAYS } from "../../../../seed/test-data-ttl";
60
63
  /** Firestore batch write hard ceiling. */
61
64
  export declare const BATCH_LIMIT = 500;
62
65
  /** Firestore single-query soft ceiling for reconcile sweeps. */
@@ -58,8 +58,11 @@ export const JOB_ERROR_MESSAGES = {
58
58
  export const CART_TTL_DAYS = 60;
59
59
  /** Default Notification TTL in days (matches functions/config/constants). */
60
60
  export const NOTIFICATION_TTL_DAYS = 30;
61
- /** Tester sandbox TTL in days — matches testDataExpiresAt() in features/tester/seed-data/tester-ttl.ts. */
62
- export const TESTER_SANDBOX_TTL_DAYS = 7;
61
+ /**
62
+ * Test-data TTL in days. RE-EXPORTED, not re-declared — this was a second
63
+ * literal `7` whose only tie to the real one was a comment saying it matched.
64
+ */
65
+ export { TEST_DATA_TTL_DAYS } from "../../../../seed/test-data-ttl";
63
66
  /** Firestore batch write hard ceiling. */
64
67
  export const BATCH_LIMIT = 500;
65
68
  /** Firestore single-query soft ceiling for reconcile sweeps. */
@@ -0,0 +1,2 @@
1
+ import type { ScheduleHandler } from "../runtime/types";
2
+ export declare const priceIndexRollupHandler: ScheduleHandler;
@@ -0,0 +1,2 @@
1
+ import { runPriceIndexRollup } from "../core/priceIndexRollup";
2
+ export const priceIndexRollupHandler = runPriceIndexRollup;
@@ -0,0 +1,2 @@
1
+ import type { ScheduleHandler } from "../runtime/types";
2
+ export declare const testDataCleanupHandler: ScheduleHandler;
@@ -0,0 +1,2 @@
1
+ import { runTestDataCleanup } from "../core/testDataCleanup";
2
+ export const testDataCleanupHandler = (ctx) => runTestDataCleanup(ctx);
@@ -528,6 +528,40 @@ export const ACTIONS = {
528
528
  description: "Navigate to the full product listing for this store.",
529
529
  kind: "link",
530
530
  },
531
+ // ── Category description templates (C2c) ─────────────────────────────
532
+ "apply-template": {
533
+ iconKey: "edit",
534
+ id: "store.apply-template",
535
+ label: "Use this description",
536
+ ariaLabel: "Fill the description from this category template",
537
+ description: "Fill the description field from the category's description template, with the listing's own details interpolated.",
538
+ kind: "secondary",
539
+ permissions: ["seller", "admin"],
540
+ },
541
+ "replace-description": {
542
+ iconKey: "edit",
543
+ id: "store.replace-description",
544
+ label: "Replace description",
545
+ ariaLabel: "Replace the description you have written with the category template",
546
+ description: "Overwrite the description with the category's template. The text currently in the field is lost.",
547
+ /*
548
+ * 🛑 `danger` with a confirmation, because it destroys authored text.
549
+ *
550
+ * The picker only ever offers this when `isDescriptionDirty` — i.e. the
551
+ * seller has typed something that is not a body we filled in. Rule #7:
552
+ * an action that cannot be undone needs the confirmation ON the
553
+ * ActionDef, not a `window.confirm` at the call site, so the copy lives
554
+ * with the action and is translatable.
555
+ */
556
+ kind: "danger",
557
+ permissions: ["seller", "admin"],
558
+ confirmation: {
559
+ title: "Replace what you have written?",
560
+ body: "The description you typed will be overwritten by the category template. This cannot be undone.",
561
+ confirmLabel: "Replace it",
562
+ confirmKind: "danger",
563
+ },
564
+ },
531
565
  // ── Offer management ─────────────────────────────────────────────────
532
566
  "accept-offer": {
533
567
  iconKey: "approve",
@@ -101,7 +101,7 @@ export interface LineTaxComponent {
101
101
  * `productById` supplies the live `gstRate`; each member's snapshotted rate is
102
102
  * the fallback for a member whose listing has since been archived.
103
103
  */
104
- export declare function lineTaxComponentsFor(item: CartItemDocument, productById: Map<string, ProductDocument>): LineTaxComponent[];
104
+ export declare function lineTaxComponentsFor(item: CartItemDocument, productById: Map<string, ProductDocument | null>): LineTaxComponent[];
105
105
  /**
106
106
  * Total GST for one order group, and the base it was charged on.
107
107
  *
@@ -111,10 +111,34 @@ export declare function lineTaxComponentsFor(item: CartItemDocument, productById
111
111
  * one and not the other — here that would mean quoting one tax figure and
112
112
  * charging another.
113
113
  */
114
- export declare function sumGroupGst(items: readonly CartItemDocument[], productById: Map<string, ProductDocument>, intraState: boolean): {
114
+ export declare function sumGroupGst(items: readonly CartItemDocument[], productById: Map<string, ProductDocument | null>, intraState: boolean): GroupGstSummary;
115
+ /** One rate's rollup within an order group — what Rule 46 asks an invoice to show. */
116
+ export interface GstRateSlice {
117
+ gstRate: number;
118
+ taxableAmount: number;
119
+ cgst: number;
120
+ sgst: number;
121
+ igst: number;
122
+ gstAmount: number;
123
+ }
124
+ /**
125
+ * What one order group owes, with the split and the rate dimension intact.
126
+ *
127
+ * Widened from `{ taxableAmount, gstAmount }` — additive, so both existing
128
+ * callers keep working while neither has to re-derive a split the per-slice
129
+ * `calculateGst` calls above already computed correctly.
130
+ */
131
+ export interface GroupGstSummary {
132
+ /** Taxable value of slices carrying a POSITIVE rate. */
115
133
  taxableAmount: number;
134
+ /** Taxable value of 0%-rated (exempt) slices. Not part of `taxableAmount`. */
135
+ exemptAmount: number;
136
+ cgst: number;
137
+ sgst: number;
138
+ igst: number;
116
139
  gstAmount: number;
117
- };
140
+ byRate: GstRateSlice[];
141
+ }
118
142
  /**
119
143
  * Split `total` across members in proportion to their list prices, with the
120
144
  * rounding remainder pushed onto the LAST member so the parts sum to exactly
@@ -125,6 +149,92 @@ export declare function sumGroupGst(items: readonly CartItemDocument[], productB
125
149
  * items), which is the only case where proportion is undefined.
126
150
  */
127
151
  export declare function allocateAcrossMembers(total: number, weights: number[]): number[];
152
+ /** Exactly the fields `orderGoodsValue` / `orderTaxableValue` read. */
153
+ export interface PayableOrderLike {
154
+ items?: Array<{
155
+ quantity: number;
156
+ unitPrice: number;
157
+ cancelledQuantity?: number;
158
+ /** Snapshotted at purchase. Absent or 0 means no tax to back out. */
159
+ gstRate?: number;
160
+ }>;
161
+ /** The flat shape, authoritative when `items` is absent. */
162
+ quantity?: number;
163
+ unitPrice?: number;
164
+ gstRate?: number;
165
+ }
166
+ /**
167
+ * The GOODS VALUE of an order — the only correct base for a seller payout.
168
+ *
169
+ * 🛑 This is NOT `OrderDocument.totalPrice`, and conflating the two was a live
170
+ * money bug in all four payout paths (`autoPayoutEligibility`,
171
+ * `weeklyPayoutEligibility`, and both `seller-actions` entry points). That
172
+ * field is the GRAND TOTAL — `checkout/actions.ts` builds it as
173
+ * `goods − coupon + shipping + codHandling + whatsappNotify + giftWrap +
174
+ * shipmentProtection + platformFee + platformFeeGst + emiSurcharge + gstAmount`
175
+ * — so using it as the payout base did three wrong things at once:
176
+ *
177
+ * 1. **paid the seller the GST we collected** and must remit ourselves;
178
+ * 2. **took the platform fee twice** — the buyer already paid it as a term of
179
+ * `totalPrice`, and `computePayoutDeduction` then charged the seller a
180
+ * percentage of a figure that already included it;
181
+ * 3. **computed every percentage on an inflated base**, since shipping, COD
182
+ * handling, add-on fees and the EMI surcharge were all inside it.
183
+ *
184
+ * The payout base is the **taxable value**, never the gross. That one sentence
185
+ * also covers GST-inclusive pricing: an inclusive ₹999 @5% backs out to a
186
+ * ₹951.43 taxable value and that is what is paid, while an exclusive ₹999
187
+ * charges the buyer ₹1,048.95 and pays ₹999.
188
+ *
189
+ * 🛑 Computed from `quantity − cancelledQuantity`, NOT from `item.totalPrice`.
190
+ * A partial cancellation sets `cancelledQuantity: item.quantity` and
191
+ * **deliberately leaves `totalPrice` untouched** (`order-actions.ts:97-106`),
192
+ * so summing `totalPrice` would pay the seller in full for lines the buyer
193
+ * cancelled. `stock-restore.ts:20` already derives the live quantity this way.
194
+ *
195
+ * 🛑 Falls back to the FLAT `quantity × unitPrice` when `items` is absent.
196
+ * `items?` is optional on `OrderDocument` and the flat fields are the primary
197
+ * shape, not legacy residue — a bare `Σ items[]` would silently pay **zero**
198
+ * on any flat-shaped order, which is indistinguishable from "this seller is
199
+ * owed nothing".
200
+ */
201
+ export declare function orderGoodsValue(order: PayableOrderLike): number;
202
+ /** Sum of `orderGoodsValue` across orders — the GROSS, not the payout base. */
203
+ export declare function ordersGoodsValue(orders: readonly PayableOrderLike[]): number;
204
+ /**
205
+ * The TAXABLE VALUE of an order — **the payout base.**
206
+ *
207
+ * 🛑 This is the second of two narrowings, and the distinction between them is
208
+ * worth keeping straight:
209
+ *
210
+ * - C0 established the base is the **goods value**, not the order grand total
211
+ * (which folded in GST, the platform fee the buyer already paid, shipping,
212
+ * COD handling, add-ons and the EMI surcharge).
213
+ * - This establishes it is the **taxable value**, not the gross goods value —
214
+ * because every price here is tax-INCLUSIVE, so the goods value still has
215
+ * our GST sitting inside it, and paying that out is the very thing C0 set
216
+ * out to stop.
217
+ *
218
+ * The second narrowing was not possible until the pricing model was settled:
219
+ * under the old add-on-top math the goods value WAS the taxable value, so the
220
+ * two were indistinguishable and C0's fix was complete as written.
221
+ *
222
+ * One sentence covers both seller modes, which is the reason the flag is an
223
+ * authoring convenience and nothing more:
224
+ *
225
+ * **the payout base is the taxable value, never the gross.**
226
+ *
227
+ * - inclusive seller, ₹999 @5% → buyer pays ₹999, taxable ₹951.43, payout ₹951.43
228
+ * - exclusive seller types ₹999 → grossed up on write to ₹1,048.95, buyer
229
+ * pays ₹1,048.95, taxable ₹999.00, payout ₹999.00
230
+ *
231
+ * Per ITEM, using each line's own snapshotted `gstRate`: a mixed 5% + 18%
232
+ * order cannot be backed out with a single blended rate, and an order-level
233
+ * rate does not exist.
234
+ */
235
+ export declare function orderTaxableValue(order: PayableOrderLike): number;
236
+ /** Sum of `orderTaxableValue` across orders — the payout base for a batch. */
237
+ export declare function ordersTaxableValue(orders: readonly PayableOrderLike[]): number;
128
238
  /**
129
239
  * P-6 — pre-order groups charge each product's own `preOrderDepositPercent`
130
240
  * (falling back to the generic COD deposit % when a product doesn't have one