@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
@@ -47,6 +47,97 @@ export const bundleQueryRuleSchema = z.discriminatedUnion("type", [
47
47
  limit: z.number().int().positive(),
48
48
  }),
49
49
  ]);
50
+ /*
51
+ * ── Category-owned content (B3), mirrored here in B5 ────────────────────────
52
+ *
53
+ * 🛑 These three were added to `CategoryDocument` in B3 and NOT to this
54
+ * schema, which is the drift this file has a history of: `highlights` and
55
+ * `faqs` sat in the interface with no mirror for months, and nothing enforces
56
+ * parity between the two. Mirroring them is what lets the admin category
57
+ * editor (C2) validate a template body server-side instead of trusting the
58
+ * form.
59
+ */
60
+ export const categoryDescriptionTemplateSchema = z.object({
61
+ id: z.string(),
62
+ label: z.string(),
63
+ variant: z.enum(["default", "new_in_box", "new_in_packet", "pre_owned", "replica"]),
64
+ body: z.string(),
65
+ /** Absent means "re-parse", never "no slots" — see the field's own doc. */
66
+ placeholders: z.array(z.string()).optional(),
67
+ isTested: z.boolean().optional(),
68
+ version: z.number().int().optional(),
69
+ });
70
+ export const categorySpecificationSchema = z.object({
71
+ name: z.string(),
72
+ value: z.string(),
73
+ unit: z.string().optional(),
74
+ });
75
+ export const categoryPriceGuidanceSchema = z.object({
76
+ currency: z.literal("INR"),
77
+ market: z
78
+ .object({
79
+ median: rupeesSchema,
80
+ // Optional on purpose — see the field docs in category-content.ts. The
81
+ // crawl measured quartiles for some verticals and a median-plus-range
82
+ // for others, and a required p25 would force a range to be retyped as a
83
+ // quartile.
84
+ p25: rupeesSchema.optional(),
85
+ p75: rupeesSchema.optional(),
86
+ sampleSize: z.number().int().positive().optional(),
87
+ asOf: firestoreDateSchema,
88
+ soldOutShare: z.number().min(0).max(1).optional(),
89
+ source: z.string().optional(),
90
+ })
91
+ .optional(),
92
+ /*
93
+ * 🛑 No `.default()` anywhere in here. An absent `sold` means "we have no
94
+ * sales yet" and a zeroed one would render as "things sell here for ₹0" —
95
+ * which is worse than silence, and is exactly what a defaulting schema would
96
+ * manufacture on the first quiet night.
97
+ */
98
+ sold: z
99
+ .object({
100
+ min: rupeesSchema,
101
+ median: rupeesSchema,
102
+ max: rupeesSchema,
103
+ sampleSize: z.number().int().positive(),
104
+ windowDays: z.number().int().positive(),
105
+ byPath: z
106
+ .object({
107
+ standard: z.number().int().nonnegative().optional(),
108
+ auction: z.number().int().nonnegative().optional(),
109
+ offer: z.number().int().nonnegative().optional(),
110
+ })
111
+ .optional(),
112
+ })
113
+ .optional(),
114
+ lastSold: z
115
+ .object({
116
+ amount: rupeesSchema,
117
+ at: firestoreDateSchema,
118
+ via: z.enum(["standard", "auction", "offer"]).optional(),
119
+ })
120
+ .optional(),
121
+ });
122
+ export const categoryProductDefaultsSchema = z.object({
123
+ taxCodeId: z.string().optional(),
124
+ specifications: z.array(categorySpecificationSchema).optional(),
125
+ defaultFeatures: z.array(z.string()).optional(),
126
+ defaultCondition: z.string().optional(),
127
+ defaultAuthenticity: z.enum(["original", "reproduction", "unverified"]).optional(),
128
+ inTheBox: z.array(z.string()).optional(),
129
+ priceGuidance: categoryPriceGuidanceSchema.optional(),
130
+ seoTitleTemplate: z.string().optional(),
131
+ seoDescriptionTemplate: z.string().optional(),
132
+ seoKeywords: z.array(z.string()).optional(),
133
+ weightG: z.number().nonnegative().optional(),
134
+ dimensionsCm: z
135
+ .object({ l: z.number().positive(), w: z.number().positive(), h: z.number().positive() })
136
+ .optional(),
137
+ shippingPaidBy: z.string().optional(),
138
+ fragile: z.boolean().optional(),
139
+ faqTemplates: z.array(z.object({ question: z.string(), answer: z.string() })).optional(),
140
+ });
50
141
  export const bundleItemDetailSchema = z.object({
51
142
  productId: z.string(),
52
143
  drawCount: z.number().int().nonnegative().optional(),
@@ -78,6 +169,11 @@ export const categoryFirestoreSchema = z.object({
78
169
  brandFounded: z.number().int().optional(),
79
170
  highlights: z.array(z.string()).optional(),
80
171
  faqs: z.array(z.object({ question: z.string(), answer: z.string() })).optional(),
172
+ aliases: z.array(z.string()).optional(),
173
+ contentBody: z.string().optional(),
174
+ descriptionTemplates: z.array(categoryDescriptionTemplateSchema).optional(),
175
+ productDefaults: categoryProductDefaultsSchema.optional(),
176
+ searchTxt: z.array(z.string()).optional(),
81
177
  bundleKind: z.enum(["special", "brand"]).optional(),
82
178
  brandSlug: z.string().optional(),
83
179
  bundlePrice: rupeesSchema.optional(),
@@ -7,3 +7,4 @@ export * from "./actions";
7
7
  export { CategoriesRepository, categoriesRepository, } from "./repository/categories.repository";
8
8
  export { GET as categoriesGET, GET, POST } from "./api/route";
9
9
  export { categoryItemGET, categoryItemPATCH, categoryItemDELETE, } from "./api/[id]/route";
10
+ export { GET as categoryDescriptionTemplateGET } from "./api/[id]/description-template/route";
@@ -7,3 +7,10 @@ export * from "./actions";
7
7
  export { CategoriesRepository, categoriesRepository, } from "./repository/categories.repository";
8
8
  export { GET as categoriesGET, GET, POST } from "./api/route";
9
9
  export { categoryItemGET, categoryItemPATCH, categoryItemDELETE, } from "./api/[id]/route";
10
+ /*
11
+ * The seller form's template read. A separate route rather than a field on
12
+ * `categoryItemGET` because that one returns the whole document and is not
13
+ * edge-cached — this is a narrow, hour-cacheable answer to one question, hit
14
+ * on every category change in the form.
15
+ */
16
+ export { GET as categoryDescriptionTemplateGET } from "./api/[id]/description-template/route";
@@ -57,6 +57,15 @@ export interface CategoryItem {
57
57
  metrics?: CategoryMetrics;
58
58
  isFeatured?: boolean;
59
59
  featuredPriority?: number;
60
+ /**
61
+ * Whether the category is live. Declared here because `AdminCategoriesView`
62
+ * reads it off THIS route to render its Status column, and its fallback is
63
+ * `toStringValue(item.status, "Active")` — a field `CategoryDocument` does
64
+ * not have. So an absent `isActive` does not render "unknown", it renders
65
+ * **every row as "Active"**, which is the same silent-wrong-value shape the
66
+ * `parentId` comment in that file documents.
67
+ */
68
+ isActive?: boolean;
60
69
  /** @deprecated Use categoryType === "brand" */ isBrand?: boolean;
61
70
  seo?: CategorySeo;
62
71
  display?: CategoryDisplay;
@@ -16,9 +16,9 @@ export interface EventDocument extends BaseDocument {
16
16
  title: string;
17
17
  description: string;
18
18
  status: EventStatus;
19
- /** Tester sandbox flag — set on the shared admin-seeded test event; swept by testerSandboxCleanup. */
19
+ /** Test-data flag — set on the shared admin-seeded test event; swept by testDataCleanup. */
20
20
  isTestData?: boolean;
21
- /** When isTestData, the cutoff after which testerSandboxCleanup deletes this doc. */
21
+ /** When isTestData, the cutoff after which testDataCleanup deletes this doc. */
22
22
  testDataExpiresAt?: Date;
23
23
  startsAt: Date;
24
24
  endsAt: Date;
@@ -4,6 +4,7 @@
4
4
  import type { FAQCategory, FAQAnswer, FAQStats } from "../types";
5
5
  export type { FAQCategory, FAQAnswer, FAQStats } from "../types";
6
6
  import type { BaseDocument } from "../../../_internal/shared/types/base-document";
7
+ import { extractPlaceholders } from "../../../_internal/shared/templating/placeholders";
7
8
  export interface FAQSEO {
8
9
  slug: string;
9
10
  metaTitle?: string;
@@ -53,7 +54,7 @@ export declare const faqQueryHelpers: {
53
54
  readonly byTag: (tag: string) => readonly ["tags", "array-contains", string];
54
55
  readonly byCreator: (userId: string) => readonly ["createdBy", "==", string];
55
56
  };
56
- export declare function extractVariablePlaceholders(text: string): string[];
57
+ export declare const extractVariablePlaceholders: typeof extractPlaceholders;
57
58
  export declare function usesVariableInterpolation(faq: FAQDocument): boolean;
58
59
  export declare function validateFAQVariables(faq: FAQDocument, siteSettingsVariables: Record<string, string | number>): string[];
59
60
  export declare function isPopularFAQ(faq: FAQDocument): boolean;
@@ -2,6 +2,8 @@
2
2
  * FAQ Feature Firestore Document Types & Constants
3
3
  */
4
4
  import { generateFAQId } from "../../../utils/id-generators";
5
+ // Imported from the DEFINING module, not a barrel (Root Cause #18).
6
+ import { extractPlaceholders, missingPlaceholders, } from "../../../_internal/shared/templating/placeholders";
5
7
  export const FAQS_COLLECTION = "faqs";
6
8
  export const FAQS_INDEXED_FIELDS = [
7
9
  "slug",
@@ -74,21 +76,31 @@ export const faqQueryHelpers = {
74
76
  byTag: (tag) => ["tags", "array-contains", tag],
75
77
  byCreator: (userId) => ["createdBy", "==", userId],
76
78
  };
77
- export function extractVariablePlaceholders(text) {
78
- const regex = /\{\{(\w+)\}\}/g;
79
- return Array.from(text.matchAll(regex), (m) => m[1]);
80
- }
79
+ /*
80
+ * The `{{placeholder}}` grammar moved to `_internal/shared/templating/
81
+ * placeholders.ts` so category description templates can share it instead of
82
+ * carrying a second regex (Root Cause #75).
83
+ *
84
+ * These three keep their original NAMES and signatures on purpose: they have
85
+ * FAQ-specific behaviour layered on the shared primitive — both gate on
86
+ * `useSiteSettings`, and `validateFAQVariables` merges the FAQ's own
87
+ * `variables` over the site-settings map. That layer is why this is a
88
+ * re-export-with-wrapper rather than a plain re-export, and why no FAQ call
89
+ * site changed.
90
+ */
91
+ export const extractVariablePlaceholders = extractPlaceholders;
81
92
  export function usesVariableInterpolation(faq) {
82
93
  if (!faq.useSiteSettings)
83
94
  return false;
84
- return extractVariablePlaceholders(faq.answer.text).length > 0;
95
+ return extractPlaceholders(faq.answer.text).length > 0;
85
96
  }
86
97
  export function validateFAQVariables(faq, siteSettingsVariables) {
87
98
  if (!faq.useSiteSettings)
88
99
  return [];
89
- const placeholders = extractVariablePlaceholders(faq.answer.text);
90
- const available = { ...siteSettingsVariables, ...(faq.variables || {}) };
91
- return placeholders.filter((v) => !(v in available));
100
+ return missingPlaceholders(faq.answer.text, {
101
+ ...siteSettingsVariables,
102
+ ...(faq.variables || {}),
103
+ });
92
104
  }
93
105
  export function isPopularFAQ(faq) {
94
106
  const { views, helpful, notHelpful } = faq.stats;
@@ -180,6 +180,25 @@ export interface OrderDocumentItem {
180
180
  totalPrice: number;
181
181
  /** SB8-F â€" set when the item is a prize-draw entry; drives the reveals badge. */
182
182
  listingType?: ListingType;
183
+ /**
184
+ * The product's LEAF category, snapshotted at order time (C1).
185
+ *
186
+ * 🛑 A SNAPSHOT, not a join, and not for performance alone. `priceIndexRollup`
187
+ * needs to know which category band a realised sale belongs to, and this
188
+ * interface carried no taxonomy field at all — `bundleCategorySlug` is a
189
+ * bundle key, not a category — so the only route was a full products scan
190
+ * per run, which answers nothing for an item whose product has since been
191
+ * deleted.
192
+ *
193
+ * It is also the correct answer rather than the cheap one: a product
194
+ * re-categorised next year must not retroactively move last year's sale into
195
+ * a band it was never part of. Same reasoning as the `hsnCode` / `gstRate` /
196
+ * `finalSale` snapshots below — a past sale's facts are fixed.
197
+ *
198
+ * Absent on every order written before C1, which is why the rollup keeps the
199
+ * products-map fallback rather than treating absence as "no category".
200
+ */
201
+ categorySlug?: string;
183
202
  /** SB8-F â€" per-item reveal status; flips through pending â†' open â†' revealed/closed. */
184
203
  prizeRevealStatus?: "pending" | "open" | "closed" | "revealed";
185
204
  /** Set when the buyer cancels this specific line item post-order (partial cancellation). */
@@ -358,6 +377,57 @@ export interface OrderDocument extends BaseDocument {
358
377
  sgst?: number;
359
378
  /** Inter-state: buyer and seller/store pickup address are in different states. */
360
379
  igst?: number;
380
+ /**
381
+ * Taxable value of 0%-rated (EXEMPT) goods in this order — live plants and
382
+ * animals. **Not** part of `taxableAmount`, which covers positively-rated
383
+ * slices only.
384
+ *
385
+ * 🛑 Without it a mixed exempt/taxed order cannot reconcile its own invoice:
386
+ * `sumGroupGst` skipped exempt slices entirely, so `taxableAmount` excluded
387
+ * them while `items[]` included them, and the Taxable column could never
388
+ * sum to the summary. Exempt is a real, reportable state (nil-rated supply),
389
+ * not an absence.
390
+ */
391
+ exemptAmount?: number;
392
+ /**
393
+ * Per-rate rollup — taxable value and tax for each distinct `gstRate` in the
394
+ * order.
395
+ *
396
+ * 🛑 Required for a Rule 46-compliant invoice, which asks for taxable value
397
+ * and tax **per rate**. The aggregate `taxableAmount`/`gstAmount`/cgst/sgst/
398
+ * igst fields above lose that dimension, so a mixed 5% + 18% order could not
399
+ * produce a compliant document without re-deriving the split downstream —
400
+ * i.e. a second implementation of the loop that already computed it.
401
+ */
402
+ gstByRate?: Array<{
403
+ gstRate: number;
404
+ taxableAmount: number;
405
+ cgst: number;
406
+ sgst: number;
407
+ igst: number;
408
+ gstAmount: number;
409
+ }>;
410
+ /**
411
+ * OUR GST registration as it stood at the moment of supply — LetItRip is the
412
+ * seller of record and invoices under its own GSTIN.
413
+ *
414
+ * 🛑 Snapshotted for two independent reasons, and both matter.
415
+ *
416
+ * **Compliance**: changing our registered details must never rewrite an
417
+ * invoice already issued. Same discipline as `items[].hsnCode` / `gstRate` /
418
+ * `finalSale`, and the same reason `TaxCodeDocument` edits change future
419
+ * derivations only.
420
+ *
421
+ * **Access**: `gst` sits in `PRIVATE_SITE_FIELDS` — withheld from the public
422
+ * site-settings projection precisely because an earlier deny-list version
423
+ * shipped the GSTIN and registered legal name to anonymous callers (Root
424
+ * Cause #70). So the invoice page cannot read it from site settings, and it
425
+ * should not: the buyer is entitled to the GSTIN *on their own invoice*,
426
+ * which is an authenticated per-order read, not public data.
427
+ */
428
+ supplierGstin?: string;
429
+ supplierLegalName?: string;
430
+ supplierAddress?: string;
361
431
  emiEnabled?: boolean;
362
432
  /** Number of monthly installments the buyer chose (2–6). */
363
433
  emiTenureMonths?: number;
@@ -53,6 +53,18 @@ export interface OrderItem {
53
53
  * gate reads the document, so this is a rendering hint only.
54
54
  */
55
55
  finalSale?: boolean;
56
+ /**
57
+ * HSN code and GST rate, snapshotted onto the order item at purchase.
58
+ *
59
+ * 🛑 Surfaced here because the INVOICE needs them per line and could not
60
+ * previously get them: `orderDocumentToOrder` mapped `tax: doc.gstAmount`
61
+ * and nothing else tax-related, so the buyer-facing invoice rendered
62
+ * `["Item", "Qty", "Price"]` and one aggregate Tax row — not because the
63
+ * renderer was lazy, but because the adapter never handed it a rate, an HSN
64
+ * or a split to show. Root Cause #57's exact shape.
65
+ */
66
+ hsnCode?: string;
67
+ gstRate?: 0 | 5 | 12 | 18 | 28;
56
68
  }
57
69
  /**
58
70
  * One entry on an order's timeline — the client-facing shape of
@@ -100,7 +112,50 @@ export interface Order {
100
112
  subtotal: number;
101
113
  shippingCost?: number;
102
114
  discount?: number;
115
+ /** Total GST — product GST plus the GST on our platform fee. */
103
116
  tax?: number;
117
+ /**
118
+ * The GST breakdown the INVOICE needs, and which the adapter did not map
119
+ * until 2026-10-10 — so `/user/orders/[id]/invoice` had no GSTIN, no HSN
120
+ * column, no rate column and one undifferentiated "Tax (GST)" row, and was
121
+ * therefore not Rule 46-compliant in any respect.
122
+ *
123
+ * 🛑 `taxableAmount` excludes `exemptAmount`: a 0%-rated supply is a real
124
+ * reportable state, not an absence, and keeping them separate is what lets
125
+ * the invoice's columns reconcile against its own summary.
126
+ */
127
+ taxableAmount?: number;
128
+ exemptAmount?: number;
129
+ cgst?: number;
130
+ sgst?: number;
131
+ igst?: number;
132
+ /**
133
+ * Taxable value and tax PER RATE — what Rule 46 actually asks for.
134
+ *
135
+ * ✅ This is also why no per-item `gstAmount` was added. Rule 46 requires the
136
+ * tax amount per **rate**, not per **line**; the per-line requirements are
137
+ * HSN, quantity, taxable value and rate, all of which the item already
138
+ * carries or derives from `price × quantity`. A per-item tax amount would be
139
+ * a denormalised mirror of what this array already holds — Root Cause #42's
140
+ * shape — so it was deliberately NOT added.
141
+ */
142
+ gstByRate?: Array<{
143
+ gstRate: number;
144
+ taxableAmount: number;
145
+ cgst: number;
146
+ sgst: number;
147
+ igst: number;
148
+ gstAmount: number;
149
+ }>;
150
+ /**
151
+ * Our GST registration as snapshotted on this order. A tax invoice without
152
+ * the supplier's GSTIN is not a tax invoice — and the invoice page cannot
153
+ * read it from site settings, because `gst` is in `PRIVATE_SITE_FIELDS`
154
+ * after an earlier deny-list projection shipped it publicly (Root Cause #70).
155
+ */
156
+ supplierGstin?: string;
157
+ supplierLegalName?: string;
158
+ supplierAddress?: string;
104
159
  total: number;
105
160
  currency: string;
106
161
  /**
@@ -8,7 +8,6 @@ export declare const PRODUCT_SORT_VALUES: {
8
8
  readonly PRICE_LOW: "price";
9
9
  readonly PRICE_HIGH: "-price";
10
10
  readonly RATING_HIGH: "-avgRating";
11
- readonly POPULAR: "-viewCount";
12
11
  readonly NAME_AZ: "title";
13
12
  readonly NAME_ZA: "-title";
14
13
  };
@@ -6,7 +6,11 @@ export const PRODUCT_SORT_VALUES = {
6
6
  PRICE_LOW: "price",
7
7
  PRICE_HIGH: "-price",
8
8
  RATING_HIGH: "-avgRating",
9
- POPULAR: "-viewCount",
9
+ // 🛑 `POPULAR: "-viewCount"` removed 2026-10-10 — `viewCount` is on no
10
+ // document (0/72 products, 0/58 categories) and nothing writes it, so a
11
+ // Firestore orderBy on it excludes every row. Verified: 0 results against a
12
+ // valid index. See STANDARD_SORT_OPTIONS for the full writeup.
13
+ // `RATING_HIGH` above is fine by contrast — `avgRating` is present on 72/72.
10
14
  NAME_AZ: "title",
11
15
  NAME_ZA: "-title",
12
16
  };
@@ -37,9 +37,6 @@ export declare const PRODUCT_ADMIN_SORT_OPTIONS: readonly [{
37
37
  }, {
38
38
  readonly value: string;
39
39
  readonly label: "Recently Updated";
40
- }, {
41
- readonly value: string;
42
- readonly label: "Most Viewed";
43
40
  }];
44
41
  export declare const PRODUCT_SELLER_SORT_OPTIONS: readonly [{
45
42
  readonly value: string;
@@ -68,9 +65,6 @@ export declare const PRODUCT_SELLER_SORT_OPTIONS: readonly [{
68
65
  }, {
69
66
  readonly value: string;
70
67
  readonly label: "Recently Updated";
71
- }, {
72
- readonly value: string;
73
- readonly label: "Most Viewed";
74
68
  }];
75
69
  export declare const PRODUCT_PUBLIC_SORT_OPTIONS: readonly [{
76
70
  readonly value: string;
@@ -90,9 +84,6 @@ export declare const PRODUCT_PUBLIC_SORT_OPTIONS: readonly [{
90
84
  }, {
91
85
  readonly value: string;
92
86
  readonly label: "Name: Z–A";
93
- }, {
94
- readonly value: string;
95
- readonly label: "Most Viewed";
96
87
  }];
97
88
  export declare function getProductFilterKeys(variant: ProductFilterVariant): readonly string[];
98
89
  export declare function getProductSortOptions(variant: ProductFilterVariant): ReadonlyArray<{
@@ -33,6 +33,32 @@ export interface ProductFormProps {
33
33
  product: ProductFormValue;
34
34
  onChange: (updated: ProductFormValue) => void;
35
35
  isReadonly?: boolean;
36
+ /**
37
+ * Show the Tax & GST section (`gstRate` + `hsnCode`). **Admin only.**
38
+ *
39
+ * 🛑 Defaults to `false`, and the default is the point. LetItRip is the
40
+ * seller of record and invoices under its OWN GSTIN, so a supplier choosing
41
+ * the tax rate on goods we invoice is a compliance hole, not a convenience.
42
+ * The rate is a tax determination: it belongs to the category (via
43
+ * `productDefaults.taxCodeId`, resolved server-side by `deriveTaxonomy`) and
44
+ * to the admin who maintains the tax codes.
45
+ *
46
+ * There is also a one-click foot-gun behind it. The select renders
47
+ * `String(gstRate ?? 0)`, so an undetermined rate DISPLAYS as "0%" — and
48
+ * because `0` is a deliberate exemption while `undefined` means "nobody has
49
+ * said" (see `ProductDocument.gstRate`), a seller who touches that control
50
+ * at all, even re-picking the 0% already shown, writes a hard `0` and
51
+ * **permanently suppresses the category derivation**: `deriveTaxonomy` only
52
+ * derives when `input.gstRate == null`.
53
+ *
54
+ * 🛑 The display default does NOT corrupt the field on its own. `FormField`'s
55
+ * select fires `onChange` from a real DOM event only, and the draft is
56
+ * seeded from the stored document — so an untouched save preserves
57
+ * `undefined`. Verified rather than assumed; an earlier plan revision
58
+ * asserted every save converted "nobody has said" into "exempt", and that
59
+ * was not true of this code.
60
+ */
61
+ canEditTax?: boolean;
36
62
  /**
37
63
  * Render a custom rich-text description editor.
38
64
  * If omitted, a standard textarea field is used.
@@ -76,5 +102,5 @@ export interface ProductFormProps {
76
102
  disabled: boolean;
77
103
  }) => React.ReactNode;
78
104
  }
79
- export declare function ProductForm({ product, onChange, isReadonly, renderDescriptionEditor, renderCategorySelector, renderBrandSelector, renderStoreAddressSelector, onMediaAbort, currencyPrefix, renderGroupSettings, renderGroupJoinField, }: ProductFormProps): React.JSX.Element;
105
+ export declare function ProductForm({ product, onChange, isReadonly, canEditTax, renderDescriptionEditor, renderCategorySelector, renderBrandSelector, renderStoreAddressSelector, onMediaAbort, currencyPrefix, renderGroupSettings, renderGroupJoinField, }: ProductFormProps): React.JSX.Element;
80
106
  export {};
@@ -50,7 +50,9 @@ function readFileAsDataUrl(file) {
50
50
  reader.readAsDataURL(file);
51
51
  });
52
52
  }
53
- export function ProductForm({ product, onChange, isReadonly = false, renderDescriptionEditor, renderCategorySelector, renderBrandSelector, renderStoreAddressSelector, onMediaAbort, currencyPrefix = "", renderGroupSettings, renderGroupJoinField, }) {
53
+ export function ProductForm({ product, onChange, isReadonly = false,
54
+ /* Defaults to false — the seller path must not reach it. See the prop's docs. */
55
+ canEditTax = false, renderDescriptionEditor, renderCategorySelector, renderBrandSelector, renderStoreAddressSelector, onMediaAbort, currencyPrefix = "", renderGroupSettings, renderGroupJoinField, }) {
54
56
  const t = useTranslations("adminProducts");
55
57
  const { upload } = useMediaUpload();
56
58
  const galleryIndexRef = useRef(0);
@@ -179,17 +181,29 @@ export function ProductForm({ product, onChange, isReadonly = false, renderDescr
179
181
  minOfferPercent: e.target.checked
180
182
  ? product.minOfferPercent ?? DEFAULT_MIN_OFFER_PERCENT
181
183
  : undefined,
182
- }), disabled: isReadonly })] }), product.allowOffers && (_jsxs(_Fragment, { children: [_jsx(Alert, { variant: "info", title: t("formAllowOffersHelp"), children: t("formAllowOffersHelp") }), _jsx(FormField, { name: "minOfferPercent", label: t("formMinOfferPercent"), type: "number", value: String(product.minOfferPercent ?? ""), onChange: (value) => update({ minOfferPercent: Number(value) }), disabled: isReadonly, placeholder: String(DEFAULT_MIN_OFFER_PERCENT) })] })), _jsx(Heading, { level: 4, className: "mt-4", children: t("sectionTaxGst") }), _jsxs(FormGroup, { columns: 2, children: [_jsx(FormField, { name: "gstRate", label: t("formGstRate"), type: "select",
183
- // Unset is semantically "exempt" (see ProductDocument.gstRate JSDoc),
184
- // same as 0% — default the control to 0 so the select stays a fixed
185
- // 5-option enum instead of adding a 6th "unset" placeholder entry.
186
- value: String(product.gstRate ?? 0), onChange: (value) => update({ gstRate: Number(value) }), disabled: isReadonly, options: [
187
- { value: "0", label: "0%" },
188
- { value: "5", label: "5%" },
189
- { value: "12", label: "12%" },
190
- { value: "18", label: "18%" },
191
- { value: "28", label: "28%" },
192
- ] }), _jsx(FormField, { name: "hsnCode", label: t("formHsnCode"), type: "text", value: product.hsnCode ?? "", onChange: (value) => update({ hsnCode: value }), disabled: isReadonly, placeholder: "e.g. 9503" })] }), _jsx(Heading, { level: 4, className: "mt-4", children: t("sectionConditionShipping") }), _jsxs(FormGroup, { columns: 2, children: [_jsx(FormField, { name: "condition", label: t("formCondition"), type: "select", value: product.condition || "new", onChange: (value) => update({
184
+ }), disabled: isReadonly })] }), product.allowOffers && (_jsxs(_Fragment, { children: [_jsx(Alert, { variant: "info", title: t("formAllowOffersHelp"), children: t("formAllowOffersHelp") }), _jsx(FormField, { name: "minOfferPercent", label: t("formMinOfferPercent"), type: "number", value: String(product.minOfferPercent ?? ""), onChange: (value) => update({ minOfferPercent: Number(value) }), disabled: isReadonly, placeholder: String(DEFAULT_MIN_OFFER_PERCENT) })] })), canEditTax ? (_jsxs(_Fragment, { children: [_jsx(Heading, { level: 4, className: "mt-4", children: t("sectionTaxGst") }), _jsxs(FormGroup, { columns: 2, children: [_jsx(FormField, { name: "gstRate", label: t("formGstRate"), type: "select",
185
+ /*
186
+ * 🛑 "" is the UNSET option, and it is not cosmetic.
187
+ * `ProductDocument.gstRate` documents that `undefined` and `0`
188
+ * mean different things — zero is a deliberate exemption (live
189
+ * plants and animals), undefined is "nobody has said" — and
190
+ * `deriveTaxonomy` only derives the category's rate while it is
191
+ * `== null`. Collapsing unset to "0%", which this control did
192
+ * until 2026-10-10, made an undetermined rate read as exempt and
193
+ * put a hard `0` one click away.
194
+ */
195
+ value: product.gstRate == null ? "" : String(product.gstRate), onChange: (value) => update({
196
+ gstRate: value === ""
197
+ ? undefined
198
+ : Number(value),
199
+ }), disabled: isReadonly, options: [
200
+ { value: "", label: t("formGstRateUnset") },
201
+ { value: "0", label: "0%" },
202
+ { value: "5", label: "5%" },
203
+ { value: "12", label: "12%" },
204
+ { value: "18", label: "18%" },
205
+ { value: "28", label: "28%" },
206
+ ] }), _jsx(FormField, { name: "hsnCode", label: t("formHsnCode"), type: "text", value: product.hsnCode ?? "", onChange: (value) => update({ hsnCode: value }), disabled: isReadonly, placeholder: "e.g. 9503" })] })] })) : null, _jsx(Heading, { level: 4, className: "mt-4", children: t("sectionConditionShipping") }), _jsxs(FormGroup, { columns: 2, children: [_jsx(FormField, { name: "condition", label: t("formCondition"), type: "select", value: product.condition || "new", onChange: (value) => update({
193
207
  condition: value,
194
208
  }), disabled: isReadonly, options: [
195
209
  { value: "new", label: t("formConditionNew") },
@@ -57,9 +57,6 @@ export declare const STANDARD_SORT_OPTIONS: readonly [{
57
57
  }, {
58
58
  readonly value: string;
59
59
  readonly label: "Recently Updated";
60
- }, {
61
- readonly value: string;
62
- readonly label: "Most Viewed";
63
60
  }];
64
61
  export declare const STANDARD_PUBLIC_SORT_OPTIONS: readonly [{
65
62
  readonly value: string;
@@ -79,9 +76,6 @@ export declare const STANDARD_PUBLIC_SORT_OPTIONS: readonly [{
79
76
  }, {
80
77
  readonly value: string;
81
78
  readonly label: "Name: Z–A";
82
- }, {
83
- readonly value: string;
84
- readonly label: "Most Viewed";
85
79
  }];
86
80
  export declare const AUCTION_SORT_OPTIONS: readonly [{
87
81
  readonly value: string;
@@ -68,7 +68,31 @@ export const STANDARD_SORT_OPTIONS = [
68
68
  { value: sortBy(PRODUCT_FIELDS.FEATURED), label: "Featured First" },
69
69
  { value: sortBy(PRODUCT_FIELDS.IS_PROMOTED), label: "Promoted First" },
70
70
  { value: sortBy(PRODUCT_FIELDS.UPDATED_AT), label: "Recently Updated" },
71
- { value: sortBy(PRODUCT_FIELDS.VIEW_COUNT), label: "Most Viewed" },
71
+ /*
72
+ * 🛑 "Most Viewed" REMOVED 2026-10-10 — it returned zero products, always.
73
+ *
74
+ * It sorted by `viewCount`, and **no document has that field**: measured on
75
+ * production, 0 of 72 published products and 0 of 58 categories carry it.
76
+ * Nothing writes it either — `incrementViewCount` was removed from the
77
+ * render path on 2026-08-31 (correctly: a Firestore write per render) and
78
+ * both repositories' copies have had zero callers since.
79
+ *
80
+ * A Firestore `orderBy` EXCLUDES every document that lacks the ordering
81
+ * field, so this was Root Cause #100 verbatim. Confirmed against the real
82
+ * query shape — `status==published` + `listingType==standard`:
83
+ *
84
+ * no sort -> 16 rows
85
+ * orderBy title -> 16 rows
86
+ * orderBy viewCount -> 0 rows <- and the index EXISTS
87
+ *
88
+ * So it was not a missing index. It was a served query, costing a real
89
+ * round trip, guaranteed to return nothing — and picking it emptied the
90
+ * catalogue with no error anywhere.
91
+ *
92
+ * Real view counts arrive from the `pageViews` collection (already
93
+ * collected on ~30 surfaces) as `metrics.views7d`/`views30d`, rolled up by
94
+ * the daily job. A sort can come back then, against a field that exists.
95
+ */
72
96
  ];
73
97
  export const STANDARD_PUBLIC_SORT_OPTIONS = [
74
98
  STANDARD_SORT_OPTIONS[0],
@@ -77,7 +101,16 @@ export const STANDARD_PUBLIC_SORT_OPTIONS = [
77
101
  STANDARD_SORT_OPTIONS[3],
78
102
  STANDARD_SORT_OPTIONS[4],
79
103
  STANDARD_SORT_OPTIONS[5],
80
- STANDARD_SORT_OPTIONS[9],
104
+ /*
105
+ * 🛑 `STANDARD_SORT_OPTIONS[9]` removed — it WAS "Most Viewed", and its
106
+ * presence here means the zero-result sort was offered on the PUBLIC
107
+ * products page, not only in admin. A shopper picking it emptied the
108
+ * catalogue.
109
+ *
110
+ * Indices 6–8 (Featured First / Promoted First / Recently Updated) are
111
+ * omitted deliberately and always have been — this list is a curated public
112
+ * subset, not a prefix.
113
+ */
81
114
  ];
82
115
  // ---------------------------------------------------------------------------
83
116
  // Auctions — auction-specific base puts auctionEndDate primary