@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
@@ -49,6 +49,19 @@ import type { SellerProductDraft } from "../components/SellerProductShell";
49
49
  export interface StoredProductBlocks {
50
50
  finalSale?: boolean;
51
51
  returnPolicy?: string;
52
+ authenticity?: "original" | "reproduction" | "unverified";
53
+ specifications?: {
54
+ name: string;
55
+ value: string;
56
+ unit?: string;
57
+ }[];
58
+ features?: string[];
59
+ weightG?: number;
60
+ dimensionsCm?: {
61
+ l: number;
62
+ w: number;
63
+ h: number;
64
+ };
52
65
  classified?: {
53
66
  meetupArea?: {
54
67
  city?: string;
@@ -120,6 +120,23 @@ export function productToDraft(product) {
120
120
  */
121
121
  finalSale: product.finalSale,
122
122
  returnPolicy: product.returnPolicy,
123
+ /*
124
+ * The inherited-but-editable fields (C2c). Same category as `finalSale`:
125
+ * flat, stored, and absent from the ten hand-written `initialValues`
126
+ * literals — so the Details section would open EMPTY on a listing that
127
+ * already carries them, and a seller saving anything else would appear to
128
+ * have cleared their own specifications.
129
+ *
130
+ * (The write is partial, so an untouched save does not actually clear
131
+ * them. But a form that shows a listing's authenticity as unanswered when
132
+ * it is answered is telling the seller something untrue, which is exactly
133
+ * the argument `finalSale` above is making.)
134
+ */
135
+ authenticity: product.authenticity,
136
+ specifications: product.specifications,
137
+ features: product.features,
138
+ weightG: product.weightG,
139
+ dimensionsCm: product.dimensionsCm,
123
140
  classifiedCity: classified?.meetupArea?.city,
124
141
  classifiedLocality: classified?.meetupArea?.locality,
125
142
  classifiedPincode: classified?.meetupArea?.pincode,
@@ -0,0 +1,86 @@
1
+ /**
2
+ * ONE alert-metric vocabulary — C3a.
3
+ *
4
+ * ## The defect this closes
5
+ *
6
+ * `AnalyticsAlertDocument.metric` was `z.string()`, and the two places that
7
+ * produce one shared **ZERO tokens**:
8
+ *
9
+ * the seller UI offered daily_revenue · daily_orders · low_stock ·
10
+ * cancelled_orders · pending_payouts · avg_rating ·
11
+ * reviews_count (7, snake_case)
12
+ * the seed wrote min-stock · orders-window · api-error-rate ·
13
+ * reports-1h (4, kebab-case)
14
+ *
15
+ * Not a near-miss — no value in one list appears in the other. And because
16
+ * nothing validated the field, every seeded alert rendered its raw string in
17
+ * the seller's list (`METRIC_OPTIONS.find(...)?.label ?? alert.metric`) while
18
+ * every alert a seller created was equally unrecognised by anything else.
19
+ *
20
+ * It stayed invisible for one reason: **nothing read either value**. There was
21
+ * no evaluator anywhere — zero references across all 59 job files and
22
+ * `functions/` — so `lastTriggeredAt` was declared and written nowhere, and
23
+ * sellers configured alerts that could never fire.
24
+ *
25
+ * ## 🛑 The rule that keeps it closed
26
+ *
27
+ * **A metric is in this map only if the evaluator computes it.** Offering one
28
+ * it cannot is how the original dead promise was built — a dropdown is a
29
+ * commitment, and `low_stock` sitting in a picker with no evaluator behind it
30
+ * is worse than its absence, because the seller stops watching their stock.
31
+ *
32
+ * So `ALERT_METRICS` is a `Record<AlertMetric, AlertMetricDef>`: adding a
33
+ * union member without a definition is a compile error, and the evaluator's
34
+ * own switch is exhaustive over the same union, so adding a member without an
35
+ * implementation is a compile error too. Root Cause #61 — a `Record<Union, T>`
36
+ * is compile-safe where an array literal is not, and this vocabulary existed
37
+ * as two array literals.
38
+ */
39
+ /** Who may set an alert on this metric. */
40
+ export type AlertMetricScope = "seller" | "admin";
41
+ /** How to render the threshold the seller types. */
42
+ export type AlertMetricUnit = "count" | "currency" | "rating";
43
+ export interface AlertMetricDef {
44
+ label: string;
45
+ /** What the number means, so the form can suffix it honestly. */
46
+ unit: AlertMetricUnit;
47
+ /** Which portals may offer it. Seller metrics are evaluated per store. */
48
+ scopes: readonly AlertMetricScope[];
49
+ /**
50
+ * Plain-English statement of what is counted, shown as helper text.
51
+ *
52
+ * Carried because "Daily Orders" does not say whether a cancelled order
53
+ * counts, and an alert whose definition the seller has to guess at is an
54
+ * alert they will not trust when it fires.
55
+ */
56
+ describes: string;
57
+ }
58
+ export declare const ALERT_METRIC_VALUES: readonly ["low_stock", "out_of_stock", "orders_count", "revenue_total", "cancelled_orders", "pending_payout_total", "avg_rating", "reviews_count", "open_reports", "pending_moderation"];
59
+ export type AlertMetric = (typeof ALERT_METRIC_VALUES)[number];
60
+ /**
61
+ * 🛑 `Record<AlertMetric, …>`, not a partial and not an array. A union member
62
+ * with no definition does not compile, and the evaluator switches over the
63
+ * same union exhaustively — so the picker, the evaluator and the stored value
64
+ * cannot drift the way the two array literals did.
65
+ */
66
+ export declare const ALERT_METRICS: Record<AlertMetric, AlertMetricDef>;
67
+ /** Narrow an arbitrary stored string. */
68
+ export declare function isAlertMetric(value: unknown): value is AlertMetric;
69
+ /**
70
+ * The metrics one portal may offer, derived — never a second hand-written list.
71
+ *
72
+ * That is the whole defect this file closes: the seller picker WAS a
73
+ * hand-written list, and it agreed with nothing.
74
+ */
75
+ export declare function alertMetricsForScope(scope: AlertMetricScope): {
76
+ value: AlertMetric;
77
+ def: AlertMetricDef;
78
+ }[];
79
+ /**
80
+ * The stock level `low_stock` counts at or below.
81
+ *
82
+ * Exported so the alert, the products-list filter and the dashboard tile agree
83
+ * on what "low" means. Three surfaces each picking their own number is how a
84
+ * seller gets a warning from one and a clean bill from another.
85
+ */
86
+ export declare const LOW_STOCK_THRESHOLD = 5;
@@ -0,0 +1,142 @@
1
+ /**
2
+ * ONE alert-metric vocabulary — C3a.
3
+ *
4
+ * ## The defect this closes
5
+ *
6
+ * `AnalyticsAlertDocument.metric` was `z.string()`, and the two places that
7
+ * produce one shared **ZERO tokens**:
8
+ *
9
+ * the seller UI offered daily_revenue · daily_orders · low_stock ·
10
+ * cancelled_orders · pending_payouts · avg_rating ·
11
+ * reviews_count (7, snake_case)
12
+ * the seed wrote min-stock · orders-window · api-error-rate ·
13
+ * reports-1h (4, kebab-case)
14
+ *
15
+ * Not a near-miss — no value in one list appears in the other. And because
16
+ * nothing validated the field, every seeded alert rendered its raw string in
17
+ * the seller's list (`METRIC_OPTIONS.find(...)?.label ?? alert.metric`) while
18
+ * every alert a seller created was equally unrecognised by anything else.
19
+ *
20
+ * It stayed invisible for one reason: **nothing read either value**. There was
21
+ * no evaluator anywhere — zero references across all 59 job files and
22
+ * `functions/` — so `lastTriggeredAt` was declared and written nowhere, and
23
+ * sellers configured alerts that could never fire.
24
+ *
25
+ * ## 🛑 The rule that keeps it closed
26
+ *
27
+ * **A metric is in this map only if the evaluator computes it.** Offering one
28
+ * it cannot is how the original dead promise was built — a dropdown is a
29
+ * commitment, and `low_stock` sitting in a picker with no evaluator behind it
30
+ * is worse than its absence, because the seller stops watching their stock.
31
+ *
32
+ * So `ALERT_METRICS` is a `Record<AlertMetric, AlertMetricDef>`: adding a
33
+ * union member without a definition is a compile error, and the evaluator's
34
+ * own switch is exhaustive over the same union, so adding a member without an
35
+ * implementation is a compile error too. Root Cause #61 — a `Record<Union, T>`
36
+ * is compile-safe where an array literal is not, and this vocabulary existed
37
+ * as two array literals.
38
+ */
39
+ export const ALERT_METRIC_VALUES = [
40
+ "low_stock",
41
+ "out_of_stock",
42
+ "orders_count",
43
+ "revenue_total",
44
+ "cancelled_orders",
45
+ "pending_payout_total",
46
+ "avg_rating",
47
+ "reviews_count",
48
+ "open_reports",
49
+ "pending_moderation",
50
+ ];
51
+ /**
52
+ * 🛑 `Record<AlertMetric, …>`, not a partial and not an array. A union member
53
+ * with no definition does not compile, and the evaluator switches over the
54
+ * same union exhaustively — so the picker, the evaluator and the stored value
55
+ * cannot drift the way the two array literals did.
56
+ */
57
+ export const ALERT_METRICS = {
58
+ low_stock: {
59
+ label: "Products low on stock",
60
+ unit: "count",
61
+ scopes: ["seller", "admin"],
62
+ describes: "How many of your published listings have available stock at or below 5. With 76–93% of competitor catalogues sold out, restock is the main lifecycle event.",
63
+ },
64
+ out_of_stock: {
65
+ label: "Products out of stock",
66
+ unit: "count",
67
+ scopes: ["seller", "admin"],
68
+ describes: "How many published listings have zero available stock and cannot be bought.",
69
+ },
70
+ orders_count: {
71
+ label: "Orders in the window",
72
+ unit: "count",
73
+ scopes: ["seller", "admin"],
74
+ describes: "Orders placed in the window, whatever their status. Set this to `== 0` to be told when nothing is selling.",
75
+ },
76
+ revenue_total: {
77
+ label: "Revenue in the window",
78
+ unit: "currency",
79
+ scopes: ["seller", "admin"],
80
+ describes: "Goods value of delivered, paid, unrefunded orders in the window — the same base a payout is computed from, not the grand total.",
81
+ },
82
+ cancelled_orders: {
83
+ label: "Cancelled orders in the window",
84
+ unit: "count",
85
+ scopes: ["seller", "admin"],
86
+ describes: "Orders cancelled in the window.",
87
+ },
88
+ pending_payout_total: {
89
+ label: "Pending payout amount",
90
+ unit: "currency",
91
+ scopes: ["seller", "admin"],
92
+ describes: "Total of payouts not yet marked paid. Not window-scoped — a backlog is a backlog.",
93
+ },
94
+ avg_rating: {
95
+ label: "Average rating",
96
+ unit: "rating",
97
+ scopes: ["seller"],
98
+ describes: "Mean of approved reviews across the whole store. Set this to `< 4` to hear about a slide early.",
99
+ },
100
+ reviews_count: {
101
+ label: "New reviews in the window",
102
+ unit: "count",
103
+ scopes: ["seller"],
104
+ describes: "Approved reviews published in the window.",
105
+ },
106
+ open_reports: {
107
+ label: "Open reports",
108
+ unit: "count",
109
+ scopes: ["admin"],
110
+ describes: "Reports awaiting moderation. Replaces the seeded `reports-1h`, which nothing read.",
111
+ },
112
+ pending_moderation: {
113
+ label: "Listings pending review",
114
+ unit: "count",
115
+ scopes: ["admin"],
116
+ describes: "Listings in `in_review`, waiting on a moderator.",
117
+ },
118
+ };
119
+ /** Narrow an arbitrary stored string. */
120
+ export function isAlertMetric(value) {
121
+ return typeof value === "string" && ALERT_METRIC_VALUES.includes(value);
122
+ }
123
+ /**
124
+ * The metrics one portal may offer, derived — never a second hand-written list.
125
+ *
126
+ * That is the whole defect this file closes: the seller picker WAS a
127
+ * hand-written list, and it agreed with nothing.
128
+ */
129
+ export function alertMetricsForScope(scope) {
130
+ return ALERT_METRIC_VALUES.filter((m) => ALERT_METRICS[m].scopes.includes(scope)).map((m) => ({
131
+ value: m,
132
+ def: ALERT_METRICS[m],
133
+ }));
134
+ }
135
+ /**
136
+ * The stock level `low_stock` counts at or below.
137
+ *
138
+ * Exported so the alert, the products-list filter and the dashboard tile agree
139
+ * on what "low" means. Three surfaces each picking their own number is how a
140
+ * seller gets a warning from one and a clean bill from another.
141
+ */
142
+ export const LOW_STOCK_THRESHOLD = 5;
@@ -1,3 +1,4 @@
1
+ import type { AlertMetric } from "../constants/alert-metrics";
1
2
  import type { FirestoreDocument } from "@mohasinac/appkit";
2
3
  import type { BaseDocument } from "../../../_internal/shared/types/base-document";
3
4
  /**
@@ -76,13 +77,30 @@ export interface AnalyticsAlertDocument extends BaseDocument {
76
77
  scope: AnalyticsScope;
77
78
  ownerId: string;
78
79
  label: string;
79
- metric: string;
80
+ /**
81
+ * 🛑 Was `string`, and that is what let the two producers share ZERO tokens:
82
+ * the seller picker offered seven snake_case values and the seed wrote four
83
+ * kebab-case ones, with no overlap and nothing to notice. Narrowed to the
84
+ * one vocabulary in `constants/alert-metrics.ts`, which the picker derives
85
+ * from and the evaluator switches over exhaustively.
86
+ */
87
+ metric: AlertMetric;
80
88
  operator: AlertOperator;
81
89
  threshold: number;
82
90
  windowHours: number;
83
91
  isActive: boolean;
84
92
  notifyChannels: Array<"in-app" | "email" | "whatsapp">;
93
+ /**
94
+ * When this alert last fired.
95
+ *
96
+ * Declared from the start and written NOWHERE until C3b, because there was
97
+ * no evaluator — which is also why nobody noticed the metric vocabulary was
98
+ * two disjoint lists. It is now the re-notify guard: an alert that is still
99
+ * true must not mail every run.
100
+ */
85
101
  lastTriggeredAt?: Date;
102
+ /** The value observed at `lastTriggeredAt`, so the message can quote it. */
103
+ lastObservedValue?: number;
86
104
  }
87
105
  export declare const ANALYTICS_ALERTS_COLLECTION: "analyticsAlerts";
88
106
  export declare const ANALYTICS_ALERT_INDEXED_FIELDS: readonly ["scope", "ownerId", "isActive", "metric", "createdAt"];
@@ -53,9 +53,9 @@ export interface StoreDocument extends BaseDocument {
53
53
  searchTxt?: string[];
54
54
  storeSlug: string;
55
55
  ownerId: string;
56
- /** Tester sandbox flag — set on the shared admin-seeded test store; swept by testerSandboxCleanup. */
56
+ /** Test-data flag — set on the shared admin-seeded test store; swept by testDataCleanup. */
57
57
  isTestData?: boolean;
58
- /** When isTestData, the cutoff after which testerSandboxCleanup deletes this doc. */
58
+ /** When isTestData, the cutoff after which testDataCleanup deletes this doc. */
59
59
  testDataExpiresAt?: Date;
60
60
  storeName: string;
61
61
  storeDescription?: string;
@@ -120,6 +120,35 @@ export interface StoreDocument extends BaseDocument {
120
120
  * true. Default false — sellers must explicitly opt in.
121
121
  */
122
122
  emiEnabled?: boolean;
123
+ /**
124
+ * How this seller THINKS about the prices they type. An authoring
125
+ * convenience, and nothing more.
126
+ *
127
+ * 🛑 **Every stored `price` in this system is tax-INCLUSIVE** — it is what
128
+ * the buyer pays, with GST inside it, remitted from it. No money path
129
+ * branches on this flag, and that is deliberate: a mode read in five places
130
+ * is five places to drift, which is the defect family C0/C0c spent its
131
+ * whole length removing.
132
+ *
133
+ * `true` (the default) — the seller types the buyer-facing price. ₹999 at 5%
134
+ * means the buyer pays ₹999 and the seller earns from a ₹951.43 taxable
135
+ * value.
136
+ *
137
+ * `false` — the seller types an EX-TAX price, and `ProductRepository`
138
+ * grosses it up **once, at write time**: ₹999 at 5% is stored as ₹1,048.95,
139
+ * so the buyer pays ₹1,048.95 and the seller earns from ₹999.00.
140
+ *
141
+ * Default `true`, which matters twice. It is the Indian B2C convention
142
+ * (and what worldhobbyshop asserts in its own JSON-LD via
143
+ * `valueAddedTaxIncluded: true`); and it means enabling GST changes no
144
+ * displayed price, where defaulting to `false` would raise every listing in
145
+ * the catalogue the moment the switch was flipped.
146
+ *
147
+ * 🛑 Flipping this does NOT re-price existing listings. The gross-up happens
148
+ * at write, so a seller who switches must re-save (or have re-derived) the
149
+ * products they want converted — see `ProductRepository`.
150
+ */
151
+ pricesIncludeGst?: boolean;
123
152
  whatsappConfig?: {
124
153
  /** Seller's WhatsApp Business phone number, digits-only with country code e.g. "919876543210" */
125
154
  phoneNumber?: string;
@@ -0,0 +1,55 @@
1
+ import { BaseRepository, type FirebaseSieveResult, type SieveModel } from "../../../providers/db-firebase";
2
+ import { type TaxCodeCreateInput, type TaxCodeDocument, type TaxCodeUpdateInput } from "../schemas/firestore";
3
+ /**
4
+ * The `taxCodes` collection.
5
+ *
6
+ * Small, read-mostly and read on a hot path — `deriveTaxonomy` resolves a
7
+ * category's `taxCodeId` on every product create and on any update that names
8
+ * a taxonomy field. So the two reads below are single-document and
9
+ * single-field-equality shaped on purpose; neither needs a composite index.
10
+ */
11
+ export declare class TaxCodesRepository extends BaseRepository<TaxCodeDocument> {
12
+ constructor();
13
+ list(model: SieveModel): Promise<FirebaseSieveResult<TaxCodeDocument>>;
14
+ /**
15
+ * Every active code, chapter-then-rate ordered, for the picker.
16
+ *
17
+ * Unpaginated deliberately — this is ~10 rows and will be a few dozen at
18
+ * most. If it ever needs paging, the picker needs `searchTxt` first, and
19
+ * that is a different change.
20
+ */
21
+ listActive(): Promise<TaxCodeDocument[]>;
22
+ /**
23
+ * Every code including inactive ones, for the admin list.
24
+ *
25
+ * 🛑 EXPLICITLY bounded, even though the collection is five rows. Two
26
+ * reasons: `BaseRepository.findAll()` relies on a cap several layers away
27
+ * (`base.ts` clamps perPage at 500), and a route that reads "all of a
28
+ * collection" with no visible bound is the shape that has to be re-audited
29
+ * every time somebody wonders whether it is safe. 200 is far above any
30
+ * plausible number of HSN codes and far below anything that costs.
31
+ */
32
+ listAllBounded(): Promise<TaxCodeDocument[]>;
33
+ /**
34
+ * Resolve a code for derivation.
35
+ *
36
+ * 🛑 Returns `null` for an INACTIVE code as well as a missing one, and the
37
+ * caller must treat both the same way: leave the product's `gstRate` and
38
+ * `hsnCode` alone rather than writing a zero. Deactivating a code must not
39
+ * silently re-rate the next listing to 0% — that is indistinguishable from a
40
+ * deliberate exemption, which is the whole reason `gstRate` distinguishes
41
+ * `undefined` from `0`.
42
+ */
43
+ findResolvable(id: string): Promise<TaxCodeDocument | null>;
44
+ /**
45
+ * Create with a DERIVED id, so the same HSN cannot exist twice.
46
+ *
47
+ * Two rows for one HSN is the failure worth preventing structurally: an
48
+ * admin edits one, categories point at the other, and the rate that reaches
49
+ * an invoice is whichever the data happens to reference. A 409 here is the
50
+ * right answer — the request is well-formed and the world already has it.
51
+ */
52
+ create(input: TaxCodeCreateInput): Promise<TaxCodeDocument>;
53
+ update(id: string, data: TaxCodeUpdateInput): Promise<TaxCodeDocument>;
54
+ }
55
+ export declare const taxCodesRepository: TaxCodesRepository;
@@ -0,0 +1,131 @@
1
+ import { normalizeError } from "../../../errors/normalize";
2
+ import { DatabaseError, ConflictError, ValidationError } from "../../../errors";
3
+ import { BaseRepository, } from "../../../providers/db-firebase";
4
+ import { TAX_CODES_COLLECTION, TAX_CODE_FIELDS, TAX_CODE_SIEVE_FIELDS, createTaxCodeId, isValidHsnCode, } from "../schemas/firestore";
5
+ function failureMessage(prefix, error) {
6
+ return `${prefix}: ${error instanceof Error ? error.message : "Unknown error"}`;
7
+ }
8
+ /**
9
+ * The `taxCodes` collection.
10
+ *
11
+ * Small, read-mostly and read on a hot path — `deriveTaxonomy` resolves a
12
+ * category's `taxCodeId` on every product create and on any update that names
13
+ * a taxonomy field. So the two reads below are single-document and
14
+ * single-field-equality shaped on purpose; neither needs a composite index.
15
+ */
16
+ export class TaxCodesRepository extends BaseRepository {
17
+ constructor() {
18
+ super(TAX_CODES_COLLECTION);
19
+ }
20
+ async list(model) {
21
+ return this.sieveQuery(model, TAX_CODE_SIEVE_FIELDS);
22
+ }
23
+ /**
24
+ * Every active code, chapter-then-rate ordered, for the picker.
25
+ *
26
+ * Unpaginated deliberately — this is ~10 rows and will be a few dozen at
27
+ * most. If it ever needs paging, the picker needs `searchTxt` first, and
28
+ * that is a different change.
29
+ */
30
+ async listActive() {
31
+ try {
32
+ const snap = await this.getCollection()
33
+ .where(TAX_CODE_FIELDS.IS_ACTIVE, "==", true)
34
+ .get();
35
+ return snap.docs
36
+ .map((d) => this.mapDoc(d))
37
+ .sort((a, b) => (a.chapter ?? "").localeCompare(b.chapter ?? "") ||
38
+ a.gstRate - b.gstRate ||
39
+ a.label.localeCompare(b.label));
40
+ }
41
+ catch (error) {
42
+ void normalizeError(error);
43
+ throw new DatabaseError(failureMessage("Failed to list tax codes", error));
44
+ }
45
+ }
46
+ /**
47
+ * Every code including inactive ones, for the admin list.
48
+ *
49
+ * 🛑 EXPLICITLY bounded, even though the collection is five rows. Two
50
+ * reasons: `BaseRepository.findAll()` relies on a cap several layers away
51
+ * (`base.ts` clamps perPage at 500), and a route that reads "all of a
52
+ * collection" with no visible bound is the shape that has to be re-audited
53
+ * every time somebody wonders whether it is safe. 200 is far above any
54
+ * plausible number of HSN codes and far below anything that costs.
55
+ */
56
+ async listAllBounded() {
57
+ try {
58
+ const snap = await this.getCollection().limit(200).get();
59
+ return snap.docs
60
+ .map((d) => this.mapDoc(d))
61
+ .sort((a, b) => a.label.localeCompare(b.label));
62
+ }
63
+ catch (error) {
64
+ void normalizeError(error);
65
+ throw new DatabaseError(failureMessage("Failed to list tax codes", error));
66
+ }
67
+ }
68
+ /**
69
+ * Resolve a code for derivation.
70
+ *
71
+ * 🛑 Returns `null` for an INACTIVE code as well as a missing one, and the
72
+ * caller must treat both the same way: leave the product's `gstRate` and
73
+ * `hsnCode` alone rather than writing a zero. Deactivating a code must not
74
+ * silently re-rate the next listing to 0% — that is indistinguishable from a
75
+ * deliberate exemption, which is the whole reason `gstRate` distinguishes
76
+ * `undefined` from `0`.
77
+ */
78
+ async findResolvable(id) {
79
+ try {
80
+ const snap = await this.getCollection().doc(id).get();
81
+ if (!snap.exists)
82
+ return null;
83
+ const doc = this.mapDoc(snap);
84
+ return doc.isActive ? doc : null;
85
+ }
86
+ catch (error) {
87
+ void normalizeError(error);
88
+ throw new DatabaseError(failureMessage("Failed to read tax code", error));
89
+ }
90
+ }
91
+ /**
92
+ * Create with a DERIVED id, so the same HSN cannot exist twice.
93
+ *
94
+ * Two rows for one HSN is the failure worth preventing structurally: an
95
+ * admin edits one, categories point at the other, and the rate that reaches
96
+ * an invoice is whichever the data happens to reference. A 409 here is the
97
+ * right answer — the request is well-formed and the world already has it.
98
+ */
99
+ async create(input) {
100
+ const hsn = input.hsnCode?.trim() ?? "";
101
+ if (hsn !== "" && !isValidHsnCode(hsn)) {
102
+ throw new ValidationError("An HSN code must be exactly 4, 6 or 8 digits.", { hsnCode: "Must be 4, 6 or 8 digits." });
103
+ }
104
+ if (hsn === "" && input.gstRate !== 0) {
105
+ throw new ValidationError("An HSN code is required for any rate above 0%.", { hsnCode: "Required unless the rate is 0%." });
106
+ }
107
+ const id = createTaxCodeId(hsn);
108
+ const existing = await this.getCollection().doc(id).get();
109
+ if (existing.exists) {
110
+ throw new ConflictError(`A tax code for HSN ${hsn || "(exempt)"} already exists. Edit it instead of creating a second one.`);
111
+ }
112
+ return this.createWithId(id, { ...input, hsnCode: hsn });
113
+ }
114
+ async update(id, data) {
115
+ /*
116
+ * 🛑 `hsnCode` is editable but the id is NOT re-derived from it. Changing
117
+ * the id would orphan every `CategoryProductDefaults.taxCodeId` pointing
118
+ * here — the mirror-drift shape of Root Cause #42, with an invoice at the
119
+ * end of it. To correct a wrong HSN: deactivate this row, create the right
120
+ * one, and repoint the categories.
121
+ */
122
+ if (data.hsnCode !== undefined) {
123
+ const hsn = data.hsnCode.trim();
124
+ if (hsn !== "" && !isValidHsnCode(hsn)) {
125
+ throw new ValidationError("An HSN code must be exactly 4, 6 or 8 digits.", { hsnCode: "Must be 4, 6 or 8 digits." });
126
+ }
127
+ }
128
+ return super.update(id, data);
129
+ }
130
+ }
131
+ export const taxCodesRepository = new TaxCodesRepository();
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Tax codes — Firestore schema
3
+ *
4
+ * An HSN code and its GST rate, as an ENTITY rather than two fields repeated
5
+ * across every category. Changing a rate becomes one edit instead of ~330.
6
+ *
7
+ * ## 🛑 The reference is for AUTHORING. The scalars stay for CORRECTNESS.
8
+ *
9
+ * `CategoryProductDefaults.taxCodeId` points here, and `deriveTaxonomy`
10
+ * resolves it to `ProductDocument.gstRate` / `.hsnCode` **on the product**;
11
+ * the order item then snapshots from the product, exactly as it already does.
12
+ *
13
+ * That layering is not redundancy. Without it, editing a tax code would
14
+ * retroactively change the GST on orders already invoiced — a compliance
15
+ * problem, not a modelling preference. It is the same reason
16
+ * `OrderDocumentItem` already snapshots `hsnCode`/`gstRate`.
17
+ *
18
+ * a category edit -> changes future listings
19
+ * a tax-code edit -> changes future derivations
20
+ * nothing -> rewrites a past sale
21
+ *
22
+ * ## Why this exists at all
23
+ *
24
+ * GST has never been charged on goods here. Recorded in our own source
25
+ * (`_internal/server/features/checkout/actions.ts`): *"0 of 40 live orders had
26
+ * a `gstAmount`, and the order page's Tax row had never rendered."* And
27
+ * `gstRate` is set on zero of the current products.
28
+ *
29
+ * id === slug, prefix "tax-".
30
+ */
31
+ import type { BaseDocument } from "../../../_internal/shared/types/base-document";
32
+ export declare const TAX_CODES_COLLECTION: "taxCodes";
33
+ export declare const TAX_CODE_PREFIX: "tax-";
34
+ /**
35
+ * The GST slabs. A closed union because a rate outside it is a data error, not
36
+ * a new option — and because `ProductDocument.gstRate` already declares this
37
+ * exact set, so a wider type here would not survive the derivation.
38
+ */
39
+ export type GstRate = 0 | 5 | 12 | 18 | 28;
40
+ export declare const GST_RATE_VALUES: readonly [0, 5, 12, 18, 28];
41
+ /**
42
+ * HSN chapters this catalogue touches.
43
+ *
44
+ * 🛑 Only **9503** is verified. 4911 (printed cards/stickers/posters) and 9504
45
+ * (playing cards, board games) are the correct chapters for that merchandise
46
+ * and their CURRENT RATES ARE NOT CONFIRMED — the toys rates themselves moved
47
+ * in the 2025 revision. Verify before seeding a rate against either, because a
48
+ * wrong HSN on a real invoice is worse than an absent one.
49
+ */
50
+ export type HsnChapter = "9503" | "4911" | "9504" | "exempt";
51
+ export interface TaxCodeDocument extends BaseDocument {
52
+ /** 4, 6 or 8 digits. Empty for the exemption row, which has no HSN. */
53
+ hsnCode: string;
54
+ /** Picker label, e.g. "Toys, non-electronic (HSN 9503 0020) — 5%". */
55
+ label: string;
56
+ description?: string;
57
+ gstRate: GstRate;
58
+ /** Groups the picker. ~10 rows today, more once 4911/9504 are verified. */
59
+ chapter?: HsnChapter;
60
+ /**
61
+ * When the CURRENT rate took effect. Informational — it does NOT rewrite
62
+ * history, because history lives in the per-product and per-order-item
63
+ * snapshots. Recorded so an admin can tell a rate that has always been 5%
64
+ * from one that changed.
65
+ */
66
+ effectiveFrom?: Date;
67
+ isActive: boolean;
68
+ /** Why this rate, and what it covers — the thing a future admin will want. */
69
+ notes?: string;
70
+ }
71
+ export type TaxCodeCreateInput = Omit<TaxCodeDocument, "id" | "createdAt" | "updatedAt">;
72
+ export declare const TAX_CODE_UPDATABLE_FIELDS: readonly ["hsnCode", "label", "description", "gstRate", "chapter", "effectiveFrom", "isActive", "notes"];
73
+ export type TaxCodeUpdateInput = Partial<Pick<TaxCodeDocument, (typeof TAX_CODE_UPDATABLE_FIELDS)[number]>>;
74
+ export declare const TAX_CODE_FIELDS: {
75
+ readonly ID: "id";
76
+ readonly HSN_CODE: "hsnCode";
77
+ readonly LABEL: "label";
78
+ readonly GST_RATE: "gstRate";
79
+ readonly CHAPTER: "chapter";
80
+ readonly IS_ACTIVE: "isActive";
81
+ readonly EFFECTIVE_FROM: "effectiveFrom";
82
+ readonly CREATED_AT: "createdAt";
83
+ readonly UPDATED_AT: "updatedAt";
84
+ };
85
+ export declare const TAX_CODE_SIEVE_FIELDS: {
86
+ readonly label: {
87
+ readonly canFilter: true;
88
+ readonly canSort: true;
89
+ };
90
+ readonly hsnCode: {
91
+ readonly canFilter: true;
92
+ readonly canSort: true;
93
+ };
94
+ readonly gstRate: {
95
+ readonly canFilter: true;
96
+ readonly canSort: true;
97
+ };
98
+ readonly chapter: {
99
+ readonly canFilter: true;
100
+ readonly canSort: false;
101
+ };
102
+ readonly isActive: {
103
+ readonly canFilter: true;
104
+ readonly canSort: false;
105
+ };
106
+ readonly createdAt: {
107
+ readonly canFilter: false;
108
+ readonly canSort: true;
109
+ };
110
+ };
111
+ /**
112
+ * Derive the document id from the HSN code.
113
+ *
114
+ * Deterministic, so a seed run is idempotent (Root Cause #25) and so the same
115
+ * HSN cannot be entered twice under two ids — which would give two rows one
116
+ * admin edits and the other serves.
117
+ */
118
+ export declare function createTaxCodeId(hsnCode: string): string;
119
+ /**
120
+ * India's HSN codes are 4, 6 or 8 digits. Checked rather than trusted because
121
+ * the inline-create path in the seller form reaches the same route as the
122
+ * admin editor, and a malformed code propagates onto every product derived
123
+ * from the category that points at it.
124
+ */
125
+ export declare function isValidHsnCode(hsnCode: string): boolean;