@mohasinac/appkit 4.43.0 → 4.44.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (220) hide show
  1. package/dist/_internal/server/features/categories/adapters.d.ts +47 -0
  2. package/dist/_internal/server/features/categories/adapters.js +235 -0
  3. package/dist/_internal/server/features/checkout/actions.js +161 -22
  4. package/dist/_internal/server/features/checkout/expand-order-items.js +34 -0
  5. package/dist/_internal/server/features/orders/adapters.js +21 -0
  6. package/dist/_internal/server/features/refunds/actions.js +1 -1
  7. package/dist/_internal/server/features/seo/sitemap.js +54 -15
  8. package/dist/_internal/server/features/stores/adapters.d.ts +1 -1
  9. package/dist/_internal/server/features/stores/adapters.js +6 -0
  10. package/dist/_internal/server/functions/scheduled.d.ts +4 -2
  11. package/dist/_internal/server/functions/scheduled.js +64 -6
  12. package/dist/_internal/server/jobs/core/adminAnalytics.d.ts +0 -1
  13. package/dist/_internal/server/jobs/core/adminAnalytics.js +9 -1
  14. package/dist/_internal/server/jobs/core/analyticsAlertEvaluate.d.ts +47 -0
  15. package/dist/_internal/server/jobs/core/analyticsAlertEvaluate.js +468 -0
  16. package/dist/_internal/server/jobs/core/autoPayoutEligibility.js +14 -1
  17. package/dist/_internal/server/jobs/core/countersReconcile.js +24 -8
  18. package/dist/_internal/server/jobs/core/onCategoryWrite.js +54 -0
  19. package/dist/_internal/server/jobs/core/priceIndexRollup.d.ts +47 -0
  20. package/dist/_internal/server/jobs/core/priceIndexRollup.js +234 -0
  21. package/dist/_internal/server/jobs/core/{testerSandboxCleanup.d.ts → testDataCleanup.d.ts} +3 -3
  22. package/dist/_internal/server/jobs/core/{testerSandboxCleanup.js → testDataCleanup.js} +3 -3
  23. package/dist/_internal/server/jobs/core/weeklyPayoutEligibility.js +5 -1
  24. package/dist/_internal/server/jobs/handlers/analyticsAlertEvaluate.d.ts +2 -0
  25. package/dist/_internal/server/jobs/handlers/analyticsAlertEvaluate.js +2 -0
  26. package/dist/_internal/server/jobs/handlers/index.d.ts +3 -1
  27. package/dist/_internal/server/jobs/handlers/index.js +3 -1
  28. package/dist/_internal/server/jobs/handlers/messages.d.ts +5 -2
  29. package/dist/_internal/server/jobs/handlers/messages.js +5 -2
  30. package/dist/_internal/server/jobs/handlers/priceIndexRollup.d.ts +2 -0
  31. package/dist/_internal/server/jobs/handlers/priceIndexRollup.js +2 -0
  32. package/dist/_internal/server/jobs/handlers/testDataCleanup.d.ts +2 -0
  33. package/dist/_internal/server/jobs/handlers/testDataCleanup.js +2 -0
  34. package/dist/_internal/shared/actions/action-registry.js +34 -0
  35. package/dist/_internal/shared/checkout/order-math.d.ts +113 -3
  36. package/dist/_internal/shared/checkout/order-math.js +194 -7
  37. package/dist/_internal/shared/features/categories/price-guidance.d.ts +110 -0
  38. package/dist/_internal/shared/features/categories/price-guidance.js +136 -0
  39. package/dist/_internal/shared/features/site-settings/gst-registration.d.ts +90 -0
  40. package/dist/_internal/shared/features/site-settings/gst-registration.js +126 -0
  41. package/dist/_internal/shared/fees/calculator.d.ts +105 -0
  42. package/dist/_internal/shared/fees/calculator.js +143 -0
  43. package/dist/_internal/shared/listing-types/art/config.d.ts +0 -6
  44. package/dist/_internal/shared/listing-types/classified/config.d.ts +0 -6
  45. package/dist/_internal/shared/listing-types/digital-code/config.d.ts +0 -6
  46. package/dist/_internal/shared/listing-types/live/config.d.ts +0 -6
  47. package/dist/_internal/shared/listing-types/standard/config.d.ts +0 -6
  48. package/dist/_internal/shared/listing-types/stickers/config.d.ts +0 -6
  49. package/dist/_internal/shared/templating/placeholders.d.ts +70 -0
  50. package/dist/_internal/shared/templating/placeholders.js +73 -0
  51. package/dist/client.d.ts +2 -0
  52. package/dist/client.js +1 -0
  53. package/dist/constants/api-endpoints.d.ts +42 -0
  54. package/dist/constants/api-endpoints.js +14 -0
  55. package/dist/features/admin/actions/admin-actions.d.ts +1 -1
  56. package/dist/features/admin/actions/admin-actions.js +1 -1
  57. package/dist/features/admin/components/AdminCategoryEditorView.js +269 -3
  58. package/dist/features/admin/components/AdminProductEditorView.js +8 -1
  59. package/dist/features/admin/components/AdminProductsView.js +78 -7
  60. package/dist/features/admin/components/AdminSiteSettingsView.js +20 -1
  61. package/dist/features/admin/components/AdminTaxCodeEditorView.d.ts +10 -0
  62. package/dist/features/admin/components/AdminTaxCodeEditorView.js +160 -0
  63. package/dist/features/admin/components/AdminTaxCodesView.d.ts +13 -0
  64. package/dist/features/admin/components/AdminTaxCodesView.js +86 -0
  65. package/dist/features/admin/components/AdminTesterChecklistView.js +1 -1
  66. package/dist/features/admin/components/analytics/AdminTopProductsTable.js +1 -1
  67. package/dist/features/auth/schemas/firestore.d.ts +0 -1
  68. package/dist/features/blog/schemas/firestore.d.ts +2 -2
  69. package/dist/features/catalogue/components/CatalogueItemEditorView.js +14 -3
  70. package/dist/features/categories/api/[id]/description-template/route.d.ts +45 -0
  71. package/dist/features/categories/api/[id]/description-template/route.js +252 -0
  72. package/dist/features/categories/api/route.js +156 -11
  73. package/dist/features/categories/components/CategoryDetailPageView.js +43 -10
  74. package/dist/features/categories/components/CategoryTemplatePicker.d.ts +65 -0
  75. package/dist/features/categories/components/CategoryTemplatePicker.js +170 -0
  76. package/dist/features/categories/constants/listing-categories.d.ts +17 -0
  77. package/dist/features/categories/constants/listing-categories.js +53 -0
  78. package/dist/features/categories/hooks/useCategoryTree.js +28 -2
  79. package/dist/features/categories/repository/categories.repository.d.ts +62 -11
  80. package/dist/features/categories/repository/categories.repository.js +178 -35
  81. package/dist/features/categories/repository/price-index.repository.d.ts +115 -0
  82. package/dist/features/categories/repository/price-index.repository.js +85 -0
  83. package/dist/features/categories/schemas/category-content.d.ts +206 -0
  84. package/dist/features/categories/schemas/category-content.js +28 -0
  85. package/dist/features/categories/schemas/firestore.d.ts +74 -5
  86. package/dist/features/categories/schemas/firestore.js +74 -3
  87. package/dist/features/categories/schemas/index.d.ts +1047 -0
  88. package/dist/features/categories/schemas/index.js +96 -0
  89. package/dist/features/categories/server.d.ts +1 -0
  90. package/dist/features/categories/server.js +7 -0
  91. package/dist/features/categories/types/index.d.ts +9 -0
  92. package/dist/features/events/schemas/firestore.d.ts +2 -2
  93. package/dist/features/faq/schemas/firestore.d.ts +2 -1
  94. package/dist/features/faq/schemas/firestore.js +20 -8
  95. package/dist/features/orders/schemas/firestore.d.ts +70 -0
  96. package/dist/features/orders/types/index.d.ts +55 -0
  97. package/dist/features/products/columns/index.d.ts +0 -1
  98. package/dist/features/products/columns/index.js +5 -1
  99. package/dist/features/products/components/ProductFilters.d.ts +0 -9
  100. package/dist/features/products/components/ProductForm.d.ts +27 -1
  101. package/dist/features/products/components/ProductForm.js +26 -12
  102. package/dist/features/products/constants/sieve.d.ts +0 -6
  103. package/dist/features/products/constants/sieve.js +35 -2
  104. package/dist/features/products/repository/products.repository.d.ts +115 -1
  105. package/dist/features/products/repository/products.repository.js +491 -39
  106. package/dist/features/products/schemas/firestore.d.ts +65 -5
  107. package/dist/features/products/schemas/firestore.js +9 -0
  108. package/dist/features/products/schemas/product-features.d.ts +140 -2
  109. package/dist/features/products/schemas/product-features.js +62 -1
  110. package/dist/features/promotions/repository/coupons.repository.js +6 -1
  111. package/dist/features/promotions/schemas/firestore.d.ts +44 -1
  112. package/dist/features/promotions/schemas/firestore.js +76 -2
  113. package/dist/features/seller/actions/seller-actions.js +17 -2
  114. package/dist/features/seller/components/PrintCenterView.d.ts +3 -2
  115. package/dist/features/seller/components/PrintCenterView.js +3 -2
  116. package/dist/features/seller/components/SellerAnalyticsAlertsView.js +21 -11
  117. package/dist/features/seller/components/SellerOrdersView.js +6 -1
  118. package/dist/features/seller/components/SellerPayoutSettingsView.js +19 -3
  119. package/dist/features/seller/components/SellerProductShell.d.ts +50 -2
  120. package/dist/features/seller/components/SellerProductShell.js +101 -9
  121. package/dist/features/seller/components/SellerProductsFilterDrawer.d.ts +13 -1
  122. package/dist/features/seller/components/SellerProductsFilterDrawer.js +2 -2
  123. package/dist/features/seller/components/SellerProductsView.js +108 -3
  124. package/dist/features/seller/types/index.d.ts +17 -0
  125. package/dist/features/seller/utils/product-draft-mapping.d.ts +13 -0
  126. package/dist/features/seller/utils/product-draft-mapping.js +17 -0
  127. package/dist/features/store-extensions/constants/alert-metrics.d.ts +86 -0
  128. package/dist/features/store-extensions/constants/alert-metrics.js +142 -0
  129. package/dist/features/store-extensions/schemas/firestore.d.ts +19 -1
  130. package/dist/features/stores/schemas/firestore.d.ts +31 -2
  131. package/dist/features/tax-codes/repository/tax-codes.repository.d.ts +55 -0
  132. package/dist/features/tax-codes/repository/tax-codes.repository.js +131 -0
  133. package/dist/features/tax-codes/schemas/firestore.d.ts +125 -0
  134. package/dist/features/tax-codes/schemas/firestore.js +82 -0
  135. package/dist/features/tax-codes/schemas/tax-code-form.d.ts +63 -0
  136. package/dist/features/tax-codes/schemas/tax-code-form.js +152 -0
  137. package/dist/features/tax-codes/schemas/tax-codes.validators.d.ts +107 -0
  138. package/dist/features/tax-codes/schemas/tax-codes.validators.js +85 -0
  139. package/dist/features/tester/actions/index.d.ts +0 -1
  140. package/dist/features/tester/actions/index.js +0 -1
  141. package/dist/features/tester/components/AdminTesterFeedbackIssuesView.js +1 -1
  142. package/dist/features/tester/components/AdminTesterFeedbackListView.js +1 -1
  143. package/dist/features/tester/components/index.d.ts +0 -4
  144. package/dist/features/tester/components/index.js +0 -2
  145. package/dist/features/tester/repository/tester-checklist-item.repository.d.ts +1 -20
  146. package/dist/features/tester/repository/tester-checklist-item.repository.js +0 -52
  147. package/dist/features/tester/schemas/firestore.d.ts +0 -6
  148. package/dist/features/tester/seed-data/authored/admin__admin-detail-round-trips.js +3 -3
  149. package/dist/features/tester/seed-data/authored/admin__site-system.js +4 -5
  150. package/dist/features/tester/seed-data/authored/content-discovery__category-counts-and-rollup.js +2 -2
  151. package/dist/features/tester/seed-data/authored/index.js +0 -2
  152. package/dist/features/tester/seed-data/authored/page-wiring__reachability.js +0 -19
  153. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +8 -27
  154. package/dist/index.d.ts +21 -7
  155. package/dist/index.js +53 -7
  156. package/dist/next/routing/route-map.d.ts +2 -4
  157. package/dist/next/routing/route-map.js +1 -2
  158. package/dist/repositories/index.d.ts +1 -0
  159. package/dist/repositories/index.js +1 -0
  160. package/dist/schemas/registry.d.ts +506 -0
  161. package/dist/seed/_helpers/brand-rows.d.ts +35 -0
  162. package/dist/seed/_helpers/brand-rows.js +576 -0
  163. package/dist/seed/_helpers/category-content.d.ts +39 -0
  164. package/dist/seed/_helpers/category-content.js +634 -0
  165. package/dist/seed/_helpers/category-forest.d.ts +2 -0
  166. package/dist/seed/_helpers/category-forest.js +585 -243
  167. package/dist/seed/_helpers/category-models.d.ts +5 -0
  168. package/dist/seed/_helpers/category-models.js +1364 -0
  169. package/dist/seed/_helpers/category-tree.js +17 -1
  170. package/dist/seed/_helpers/character-features.d.ts +10 -0
  171. package/dist/seed/_helpers/character-features.js +118 -0
  172. package/dist/seed/_helpers/search-txt-wrappers.d.ts +10 -0
  173. package/dist/seed/_helpers/search-txt-wrappers.js +12 -1
  174. package/dist/seed/categories-seed-data.js +147 -169
  175. package/dist/seed/index.d.ts +1 -0
  176. package/dist/seed/index.js +1 -0
  177. package/dist/seed/manifest.d.ts +1 -1
  178. package/dist/seed/manifest.js +5 -0
  179. package/dist/seed/product-features-seed-data.d.ts +1 -8
  180. package/dist/seed/product-features-seed-data.js +358 -154
  181. package/dist/seed/products-art-seed-data.js +16 -4
  182. package/dist/seed/products-auctions-seed-data.js +6 -6
  183. package/dist/seed/products-classifieds-seed-data.js +17 -7
  184. package/dist/seed/products-digital-codes-seed-data.js +12 -12
  185. package/dist/seed/products-preorders-seed-data.js +8 -8
  186. package/dist/seed/products-prize-draws-seed-data.js +25 -8
  187. package/dist/seed/products-standard-seed-data.js +10 -60
  188. package/dist/seed/products-stickers-seed-data.js +6 -6
  189. package/dist/seed/site-settings-seed-data.js +24 -0
  190. package/dist/seed/store-extensions-seed-data.js +10 -4
  191. package/dist/seed/tax-codes-seed-data.d.ts +34 -0
  192. package/dist/seed/tax-codes-seed-data.js +100 -0
  193. package/dist/seed/test-data-ttl.d.ts +29 -0
  194. package/dist/seed/test-data-ttl.js +31 -0
  195. package/dist/server-entry.d.ts +1 -1
  196. package/dist/server-entry.js +6 -1
  197. package/dist/server.d.ts +2 -1
  198. package/dist/server.js +5 -2
  199. package/dist/styles.css +40 -0
  200. package/dist/ui/components/PaginatedSelect.d.ts +14 -0
  201. package/dist/ui/components/PaginatedSelect.js +0 -0
  202. package/dist/ui/components/PaginatedSelect.style.css +40 -0
  203. package/dist/utils/physical-location.d.ts +23 -0
  204. package/dist/utils/physical-location.js +52 -0
  205. package/dist/utils/search-txt-builders.d.ts +20 -0
  206. package/dist/utils/search-txt-builders.js +36 -0
  207. package/package.json +1 -1
  208. package/scripts/seed-cli.mjs +4 -1
  209. package/dist/_internal/server/jobs/handlers/testerSandboxCleanup.d.ts +0 -2
  210. package/dist/_internal/server/jobs/handlers/testerSandboxCleanup.js +0 -2
  211. package/dist/features/tester/actions/leaderboard-actions.d.ts +0 -3
  212. package/dist/features/tester/actions/leaderboard-actions.js +0 -5
  213. package/dist/features/tester/components/BugHunterLeaderboardView.d.ts +0 -23
  214. package/dist/features/tester/components/BugHunterLeaderboardView.js +0 -18
  215. package/dist/features/tester/components/TesterHubView.d.ts +0 -5
  216. package/dist/features/tester/components/TesterHubView.js +0 -110
  217. package/dist/features/tester/seed-data/authored/public-pages__bug-hunters.d.ts +0 -2
  218. package/dist/features/tester/seed-data/authored/public-pages__bug-hunters.js +0 -78
  219. package/dist/features/tester/seed-data/tester-ttl.d.ts +0 -4
  220. package/dist/features/tester/seed-data/tester-ttl.js +0 -6
@@ -0,0 +1,126 @@
1
+ /**
2
+ * GST registration validity — the gate on turning product GST ON.
3
+ *
4
+ * ## Why this exists
5
+ *
6
+ * `siteSettings.gst.enabled` decides whether product GST is charged at all
7
+ * (`createOrderForGroup` computes a breakdown only when `gstSettings?.enabled`
8
+ * and a buyer state are both present). Until now the admin form sent
9
+ * `{ enabled, gstin: gstin.trim().toUpperCase(), legalName, address }` with
10
+ * **no validation of any kind**, and `/api/admin/site` types every group as
11
+ * `z.unknown()` — so GST collection could be switched on with an **empty or
12
+ * malformed GSTIN**, after which every invoice charges tax under no valid
13
+ * registration and prints whatever string was typed.
14
+ *
15
+ * That is the same class as C0c defect 4 (`SETTINGS_ENCRYPTION_KEY` unset must
16
+ * fail LOUDLY rather than storing plaintext): a compliance precondition has to
17
+ * be refused at the write, not discovered on an invoice.
18
+ *
19
+ * ## Shape
20
+ *
21
+ * 15 characters: `99AAAAA9999A9Z9`
22
+ *
23
+ * `[0-9]{2}` state code
24
+ * `[A-Z]{5}` first five of the holder's PAN
25
+ * `[0-9]{4}` PAN digits
26
+ * `[A-Z]` PAN entity letter
27
+ * `[0-9A-Z]` registration number within the state
28
+ * `Z` fixed
29
+ * `[0-9A-Z]` checksum
30
+ *
31
+ * ## Why the checksum is BLOCKING and not advisory
32
+ *
33
+ * It was verified against the GSTIN that India's own GST material uses as its
34
+ * worked example (`27AAPFU0939F1ZV`) plus five more real-format registrations,
35
+ * and against every single-character mutation of them. A regex alone accepts a
36
+ * transposed digit, which is the realistic typo on a 15-character code copied
37
+ * off a certificate.
38
+ *
39
+ * 🛑 The risk runs the other way too, and it is why this was measured rather
40
+ * than assumed: a wrong checksum implementation would refuse a LEGITIMATE
41
+ * registration and leave a real business unable to invoice at all — strictly
42
+ * worse than accepting a typo. Do not change the algorithm without re-running
43
+ * the assertions.
44
+ *
45
+ * Pure. No React, no Firestore, no env.
46
+ */
47
+ /** `99AAAAA9999A9Z9` — shape only; see `isValidGstin` for the checksum. */
48
+ export const GSTIN_PATTERN = /^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z][0-9A-Z]Z[0-9A-Z]$/;
49
+ /** The 36 symbols GSTIN checksum arithmetic runs in, in value order. */
50
+ const GSTIN_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
51
+ /**
52
+ * The published GSTIN check character for the first 14 characters.
53
+ *
54
+ * Base-36 positional weights alternate 1, 2 from the left. For each character:
55
+ * multiply its value by its weight, then add the quotient and the remainder of
56
+ * that product divided by 36. The check value is `(36 - sum % 36) % 36`.
57
+ *
58
+ * Returns `null` when the prefix is not 14 characters of the alphabet — a
59
+ * caller must not treat that as "checksum failed", because it is a different
60
+ * fault with a different message.
61
+ */
62
+ export function gstinCheckCharacter(first14) {
63
+ if (first14.length !== 14)
64
+ return null;
65
+ let sum = 0;
66
+ for (let i = 0; i < 14; i += 1) {
67
+ const value = GSTIN_ALPHABET.indexOf(first14[i]);
68
+ if (value < 0)
69
+ return null;
70
+ const product = value * (i % 2 === 0 ? 1 : 2);
71
+ sum += Math.floor(product / 36) + (product % 36);
72
+ }
73
+ return GSTIN_ALPHABET[(36 - (sum % 36)) % 36];
74
+ }
75
+ /** Shape AND checksum. Whitespace and case are the caller's to normalise. */
76
+ export function isValidGstin(raw) {
77
+ if (!raw)
78
+ return false;
79
+ const value = raw.trim().toUpperCase();
80
+ if (!GSTIN_PATTERN.test(value))
81
+ return false;
82
+ return gstinCheckCharacter(value.slice(0, 14)) === value[14];
83
+ }
84
+ /**
85
+ * What must be true before product GST may be charged.
86
+ *
87
+ * 🛑 Only checked when `enabled` is being set to `true`. Turning GST **off**
88
+ * must never be blocked by the state of the fields it is turning off — that
89
+ * would trap an admin who enabled it with bad data in exactly the state they
90
+ * are trying to leave.
91
+ *
92
+ * `legalName` and `address` are required alongside the number because a tax
93
+ * invoice carries all three, and the invoice renderer snapshots all three onto
94
+ * the order. Enabling with two of them produces an invoice with a blank
95
+ * supplier block, which is not a lesser version of compliance.
96
+ */
97
+ export function validateGstEnablement(input) {
98
+ if (input.enabled !== true)
99
+ return {};
100
+ const problems = {};
101
+ const gstin = (input.gstin ?? "").trim().toUpperCase();
102
+ if (gstin.length === 0) {
103
+ problems.gstin = "A GSTIN is required before GST can be charged.";
104
+ }
105
+ else if (!GSTIN_PATTERN.test(gstin)) {
106
+ problems.gstin =
107
+ "A GSTIN is 15 characters in the form 99AAAAA9999A9Z9 — check it against your registration certificate.";
108
+ }
109
+ else if (gstinCheckCharacter(gstin.slice(0, 14)) !== gstin[14]) {
110
+ problems.gstin =
111
+ "That GSTIN fails its own check digit, so it contains a typo. Compare it character by character with your certificate.";
112
+ }
113
+ if ((input.legalName ?? "").trim().length === 0) {
114
+ problems.legalName =
115
+ "The registered legal name is required — it is printed on every tax invoice.";
116
+ }
117
+ if ((input.address ?? "").trim().length === 0) {
118
+ problems.address =
119
+ "The registered address is required — it is printed on every tax invoice.";
120
+ }
121
+ return problems;
122
+ }
123
+ /** `true` when GST may be enabled with this input. */
124
+ export function canEnableGst(input) {
125
+ return Object.keys(validateGstEnablement(input)).length === 0;
126
+ }
@@ -120,4 +120,109 @@ export interface GstBreakdown {
120
120
  igst: number;
121
121
  gstAmount: number;
122
122
  }
123
+ /**
124
+ * The rate used when nobody has determined one — HSN 95030020, non-electronic
125
+ * toys, which is nearly this whole catalogue.
126
+ *
127
+ * 🛑 A LAST RESORT, not a mechanism. The real resolution order is the
128
+ * product's own `gstRate` → the category's `taxCodeId` → this. A listing that
129
+ * reaches this constant means its category has no tax code, and THAT is the
130
+ * defect to fix; every use must warn and flag the product for triage. The
131
+ * measure of success is that it fires for nothing.
132
+ *
133
+ * It exists because the alternative is worse in one direction specifically:
134
+ * under GST-inclusive pricing the tax is already inside the price, so backing
135
+ * out 0% would treat the whole amount as taxable value, pay the seller all of
136
+ * it, and leave us owing tax on money already handed over. Exclusive pricing
137
+ * merely fails to collect, which does not lose money we hold.
138
+ *
139
+ * Belongs in `siteSettings.gst` the day a rate revision has to land without a
140
+ * deploy — `TaxCodeDocument.effectiveFrom` records that toys have already
141
+ * moved once — but a constant is the honest starting point rather than a
142
+ * settings field nobody has a reason to change.
143
+ */
144
+ export declare const DEFAULT_GST_RATE = 5;
145
+ /**
146
+ * Whether this is an intra-state supply (CGST+SGST) or inter-state (IGST).
147
+ *
148
+ * 🛑 NOT a raw `===` on two strings, which is what `createOrderForGroup` and
149
+ * `previewCheckoutPricing` both did. `constants/geo/subdivisions.ts` opens by
150
+ * recording why that fails: the same `state` field held *"Karnataka",
151
+ * "karnataka", "KA" and "Karnatka" depending on which of the three surfaces
152
+ * created the row*. A picker now fixes new addresses, but the store's state
153
+ * and every pre-picker row still arrive unnormalised.
154
+ *
155
+ * 🛑 Returns `null` for "cannot tell", and the distinction matters. The old
156
+ * expression was `!!storeState && storeState === buyerState`, so a store with
157
+ * no address resolved to `false` — i.e. **silently inter-state**, reported as
158
+ * full IGST. Note what this defect is and is not: IGST total equals
159
+ * CGST+SGST total, so **the buyer pays exactly the same amount either way**.
160
+ * It is a compliance and reporting defect on the invoice, not an overcharge —
161
+ * which is why it is safe to surface as `null` and let the caller log rather
162
+ * than guess.
163
+ */
164
+ export declare function isIntraStateSupply(storeState: string | undefined | null, buyerState: string | undefined | null): boolean | null;
165
+ /**
166
+ * Back the tax OUT of a tax-inclusive amount: `gross × 100 / (100 + rate)`.
167
+ *
168
+ * 🛑 **Every price in this system is tax-inclusive.** A listing's `price` is
169
+ * what the buyer pays; the GST is inside it and is remitted from it. That is
170
+ * the Indian B2C convention, it is what worldhobbyshop asserts in its own
171
+ * JSON-LD (`valueAddedTaxIncluded: true`), and it is what makes two listings
172
+ * at ₹999 cost the same regardless of which seller posted them.
173
+ *
174
+ * A seller who would rather think in ex-tax terms sets
175
+ * `StoreDocument.pricesIncludeGst: false`, and their typed price is grossed up
176
+ * **once, at write time**, by `ProductRepository`. So the flag is an
177
+ * AUTHORING convenience and never a display or checkout mode — the money
178
+ * paths have no branch on it at all, which is the whole point: a mode flag
179
+ * read in five places is five places to drift.
180
+ *
181
+ * `rate <= 0` returns the amount unchanged — an exempt supply has no tax to
182
+ * back out, and dividing by 100 would be a silent no-op that merely looked
183
+ * like arithmetic.
184
+ */
185
+ export declare function taxableFromInclusive(gross: number, rate: number): number;
186
+ /**
187
+ * The inverse: gross an EX-TAX amount up to its tax-inclusive equivalent,
188
+ * `taxable × (100 + rate) / 100`.
189
+ *
190
+ * Used in exactly one place — `ProductRepository`, when a store with
191
+ * `pricesIncludeGst: false` saves a price. That is the whole extent of the
192
+ * flag: one conversion, at write time, after which the stored price is
193
+ * tax-inclusive like every other and no money path can tell the difference.
194
+ *
195
+ * ₹999 ex-tax at 5% → ₹1,048.95 inclusive, which backs out to ₹999.00 again.
196
+ * The round trip is exact at 2dp for every rate in the union, asserted rather
197
+ * than assumed.
198
+ */
199
+ export declare function grossFromTaxable(taxable: number, rate: number): number;
200
+ /**
201
+ * The breakdown of a tax-INCLUSIVE amount, with two invariants guaranteed by
202
+ * construction rather than by arithmetic luck:
203
+ *
204
+ * **taxable + gstAmount === gross** (to the paisa)
205
+ * **cgst + sgst === gstAmount** (to the paisa)
206
+ *
207
+ * 🛑 Why this exists instead of `calculateGst(rate, intra, taxableFromInclusive(...))`.
208
+ *
209
+ * That composition rounds TWICE and overshoots. Measured on the ₹999 @ 5% case
210
+ * the plan itself specifies: the taxable rounds UP to 951.43, then
211
+ * `calculateGst`'s intra branch takes `half = round(47.5715 / 2) = 23.79` and
212
+ * returns `round(half × 2) = 47.58` — so taxable + tax came to **₹999.01** and
213
+ * the invoice no longer reconciled with the ₹999 the buyer was charged.
214
+ *
215
+ * Under the old add-on-top model that could not happen: the tax was whatever
216
+ * it was and the total was defined as price + tax, so the invariant was free.
217
+ * Making prices inclusive is what turns it into something that must be
218
+ * enforced — the tax is now a PARTITION of a fixed amount, not an addition to
219
+ * it.
220
+ *
221
+ * So the tax is the **exact complement** of the taxable value, and the split
222
+ * assigns the remainder to the second half rather than rounding both. This is
223
+ * the same discipline `allocateAcrossMembers` already applies, and the same
224
+ * class of defect as the `Math.round(gstAmount / 2)` split that C0c's defect 2
225
+ * removed — one level further out.
226
+ */
227
+ export declare function calculateGstInclusive(rate: number, intraState: boolean, gross: number): GstBreakdown;
123
228
  export declare function calculateGst(rate: number, intraState: boolean, taxableAmount: number): GstBreakdown;
@@ -107,6 +107,149 @@ export function computeShipmentProtectionFee(subtotal, addonSelected, rates) {
107
107
  const percentFee = roundRupees(subtotal * (percent / 100));
108
108
  return Math.max(min, percentFee);
109
109
  }
110
+ /**
111
+ * The rate used when nobody has determined one — HSN 95030020, non-electronic
112
+ * toys, which is nearly this whole catalogue.
113
+ *
114
+ * 🛑 A LAST RESORT, not a mechanism. The real resolution order is the
115
+ * product's own `gstRate` → the category's `taxCodeId` → this. A listing that
116
+ * reaches this constant means its category has no tax code, and THAT is the
117
+ * defect to fix; every use must warn and flag the product for triage. The
118
+ * measure of success is that it fires for nothing.
119
+ *
120
+ * It exists because the alternative is worse in one direction specifically:
121
+ * under GST-inclusive pricing the tax is already inside the price, so backing
122
+ * out 0% would treat the whole amount as taxable value, pay the seller all of
123
+ * it, and leave us owing tax on money already handed over. Exclusive pricing
124
+ * merely fails to collect, which does not lose money we hold.
125
+ *
126
+ * Belongs in `siteSettings.gst` the day a rate revision has to land without a
127
+ * deploy — `TaxCodeDocument.effectiveFrom` records that toys have already
128
+ * moved once — but a constant is the honest starting point rather than a
129
+ * settings field nobody has a reason to change.
130
+ */
131
+ export const DEFAULT_GST_RATE = 5;
132
+ /**
133
+ * Whether this is an intra-state supply (CGST+SGST) or inter-state (IGST).
134
+ *
135
+ * 🛑 NOT a raw `===` on two strings, which is what `createOrderForGroup` and
136
+ * `previewCheckoutPricing` both did. `constants/geo/subdivisions.ts` opens by
137
+ * recording why that fails: the same `state` field held *"Karnataka",
138
+ * "karnataka", "KA" and "Karnatka" depending on which of the three surfaces
139
+ * created the row*. A picker now fixes new addresses, but the store's state
140
+ * and every pre-picker row still arrive unnormalised.
141
+ *
142
+ * 🛑 Returns `null` for "cannot tell", and the distinction matters. The old
143
+ * expression was `!!storeState && storeState === buyerState`, so a store with
144
+ * no address resolved to `false` — i.e. **silently inter-state**, reported as
145
+ * full IGST. Note what this defect is and is not: IGST total equals
146
+ * CGST+SGST total, so **the buyer pays exactly the same amount either way**.
147
+ * It is a compliance and reporting defect on the invoice, not an overcharge —
148
+ * which is why it is safe to surface as `null` and let the caller log rather
149
+ * than guess.
150
+ */
151
+ export function isIntraStateSupply(storeState, buyerState) {
152
+ const a = normaliseStateName(storeState);
153
+ const b = normaliseStateName(buyerState);
154
+ if (!a || !b)
155
+ return null;
156
+ return a === b;
157
+ }
158
+ /** Lowercase, collapse whitespace, drop punctuation — "Tamil Nadu." → "tamil nadu". */
159
+ function normaliseStateName(raw) {
160
+ if (!raw)
161
+ return null;
162
+ const cleaned = raw
163
+ .normalize("NFKD")
164
+ .toLowerCase()
165
+ .replace(/[^a-z\s]/g, " ")
166
+ .replace(/\s+/g, " ")
167
+ .trim();
168
+ return cleaned.length > 0 ? cleaned : null;
169
+ }
170
+ /**
171
+ * Back the tax OUT of a tax-inclusive amount: `gross × 100 / (100 + rate)`.
172
+ *
173
+ * 🛑 **Every price in this system is tax-inclusive.** A listing's `price` is
174
+ * what the buyer pays; the GST is inside it and is remitted from it. That is
175
+ * the Indian B2C convention, it is what worldhobbyshop asserts in its own
176
+ * JSON-LD (`valueAddedTaxIncluded: true`), and it is what makes two listings
177
+ * at ₹999 cost the same regardless of which seller posted them.
178
+ *
179
+ * A seller who would rather think in ex-tax terms sets
180
+ * `StoreDocument.pricesIncludeGst: false`, and their typed price is grossed up
181
+ * **once, at write time**, by `ProductRepository`. So the flag is an
182
+ * AUTHORING convenience and never a display or checkout mode — the money
183
+ * paths have no branch on it at all, which is the whole point: a mode flag
184
+ * read in five places is five places to drift.
185
+ *
186
+ * `rate <= 0` returns the amount unchanged — an exempt supply has no tax to
187
+ * back out, and dividing by 100 would be a silent no-op that merely looked
188
+ * like arithmetic.
189
+ */
190
+ export function taxableFromInclusive(gross, rate) {
191
+ if (rate <= 0)
192
+ return roundRupees(gross);
193
+ return roundRupees((gross * 100) / (100 + rate)); // audit-money-units-ok: tax back-out, not a paise conversion
194
+ }
195
+ /**
196
+ * The inverse: gross an EX-TAX amount up to its tax-inclusive equivalent,
197
+ * `taxable × (100 + rate) / 100`.
198
+ *
199
+ * Used in exactly one place — `ProductRepository`, when a store with
200
+ * `pricesIncludeGst: false` saves a price. That is the whole extent of the
201
+ * flag: one conversion, at write time, after which the stored price is
202
+ * tax-inclusive like every other and no money path can tell the difference.
203
+ *
204
+ * ₹999 ex-tax at 5% → ₹1,048.95 inclusive, which backs out to ₹999.00 again.
205
+ * The round trip is exact at 2dp for every rate in the union, asserted rather
206
+ * than assumed.
207
+ */
208
+ export function grossFromTaxable(taxable, rate) {
209
+ if (rate <= 0)
210
+ return roundRupees(taxable);
211
+ return roundRupees((taxable * (100 + rate)) / 100); // audit-money-units-ok: tax gross-up, not a paise conversion
212
+ }
213
+ /**
214
+ * The breakdown of a tax-INCLUSIVE amount, with two invariants guaranteed by
215
+ * construction rather than by arithmetic luck:
216
+ *
217
+ * **taxable + gstAmount === gross** (to the paisa)
218
+ * **cgst + sgst === gstAmount** (to the paisa)
219
+ *
220
+ * 🛑 Why this exists instead of `calculateGst(rate, intra, taxableFromInclusive(...))`.
221
+ *
222
+ * That composition rounds TWICE and overshoots. Measured on the ₹999 @ 5% case
223
+ * the plan itself specifies: the taxable rounds UP to 951.43, then
224
+ * `calculateGst`'s intra branch takes `half = round(47.5715 / 2) = 23.79` and
225
+ * returns `round(half × 2) = 47.58` — so taxable + tax came to **₹999.01** and
226
+ * the invoice no longer reconciled with the ₹999 the buyer was charged.
227
+ *
228
+ * Under the old add-on-top model that could not happen: the tax was whatever
229
+ * it was and the total was defined as price + tax, so the invariant was free.
230
+ * Making prices inclusive is what turns it into something that must be
231
+ * enforced — the tax is now a PARTITION of a fixed amount, not an addition to
232
+ * it.
233
+ *
234
+ * So the tax is the **exact complement** of the taxable value, and the split
235
+ * assigns the remainder to the second half rather than rounding both. This is
236
+ * the same discipline `allocateAcrossMembers` already applies, and the same
237
+ * class of defect as the `Math.round(gstAmount / 2)` split that C0c's defect 2
238
+ * removed — one level further out.
239
+ */
240
+ export function calculateGstInclusive(rate, intraState, gross) {
241
+ if (rate <= 0) {
242
+ const t = roundRupees(gross);
243
+ return { taxableAmount: t, cgst: 0, sgst: 0, igst: 0, gstAmount: 0 };
244
+ }
245
+ const taxableAmount = taxableFromInclusive(gross, rate);
246
+ const gstAmount = roundRupees(gross - taxableAmount);
247
+ if (intraState) {
248
+ const cgst = roundRupees(gstAmount / 2); // audit-money-units-ok: halving a tax amount
249
+ return { taxableAmount, cgst, sgst: roundRupees(gstAmount - cgst), igst: 0, gstAmount };
250
+ }
251
+ return { taxableAmount, cgst: 0, sgst: 0, igst: gstAmount, gstAmount };
252
+ }
110
253
  export function calculateGst(rate, intraState, taxableAmount) {
111
254
  const gstAmount = roundRupees(taxableAmount * (rate / 100));
112
255
  if (intraState) {
@@ -48,9 +48,6 @@ export declare const config: {
48
48
  }, {
49
49
  readonly value: string;
50
50
  readonly label: "Recently Updated";
51
- }, {
52
- readonly value: string;
53
- readonly label: "Most Viewed";
54
51
  }];
55
52
  publicSortOptions: readonly [{
56
53
  readonly value: string;
@@ -70,9 +67,6 @@ export declare const config: {
70
67
  }, {
71
68
  readonly value: string;
72
69
  readonly label: "Name: Z–A";
73
- }, {
74
- readonly value: string;
75
- readonly label: "Most Viewed";
76
70
  }];
77
71
  extraFacetKeys: readonly string[];
78
72
  };
@@ -48,9 +48,6 @@ export declare const config: {
48
48
  }, {
49
49
  readonly value: string;
50
50
  readonly label: "Recently Updated";
51
- }, {
52
- readonly value: string;
53
- readonly label: "Most Viewed";
54
51
  }];
55
52
  publicSortOptions: readonly [{
56
53
  readonly value: string;
@@ -70,9 +67,6 @@ export declare const config: {
70
67
  }, {
71
68
  readonly value: string;
72
69
  readonly label: "Name: Z–A";
73
- }, {
74
- readonly value: string;
75
- readonly label: "Most Viewed";
76
70
  }];
77
71
  extraFacetKeys: readonly string[];
78
72
  };
@@ -56,9 +56,6 @@ export declare const config: {
56
56
  }, {
57
57
  readonly value: string;
58
58
  readonly label: "Recently Updated";
59
- }, {
60
- readonly value: string;
61
- readonly label: "Most Viewed";
62
59
  }];
63
60
  publicSortOptions: readonly [{
64
61
  readonly value: string;
@@ -78,9 +75,6 @@ export declare const config: {
78
75
  }, {
79
76
  readonly value: string;
80
77
  readonly label: "Name: Z–A";
81
- }, {
82
- readonly value: string;
83
- readonly label: "Most Viewed";
84
78
  }];
85
79
  extraFacetKeys: readonly string[];
86
80
  };
@@ -48,9 +48,6 @@ export declare const config: {
48
48
  }, {
49
49
  readonly value: string;
50
50
  readonly label: "Recently Updated";
51
- }, {
52
- readonly value: string;
53
- readonly label: "Most Viewed";
54
51
  }];
55
52
  publicSortOptions: readonly [{
56
53
  readonly value: string;
@@ -70,9 +67,6 @@ export declare const config: {
70
67
  }, {
71
68
  readonly value: string;
72
69
  readonly label: "Name: Z–A";
73
- }, {
74
- readonly value: string;
75
- readonly label: "Most Viewed";
76
70
  }];
77
71
  extraFacetKeys: readonly string[];
78
72
  };
@@ -48,9 +48,6 @@ export declare const config: {
48
48
  }, {
49
49
  readonly value: string;
50
50
  readonly label: "Recently Updated";
51
- }, {
52
- readonly value: string;
53
- readonly label: "Most Viewed";
54
51
  }];
55
52
  publicSortOptions: readonly [{
56
53
  readonly value: string;
@@ -70,9 +67,6 @@ export declare const config: {
70
67
  }, {
71
68
  readonly value: string;
72
69
  readonly label: "Name: Z–A";
73
- }, {
74
- readonly value: string;
75
- readonly label: "Most Viewed";
76
70
  }];
77
71
  extraFacetKeys: readonly string[];
78
72
  };
@@ -48,9 +48,6 @@ export declare const config: {
48
48
  }, {
49
49
  readonly value: string;
50
50
  readonly label: "Recently Updated";
51
- }, {
52
- readonly value: string;
53
- readonly label: "Most Viewed";
54
51
  }];
55
52
  publicSortOptions: readonly [{
56
53
  readonly value: string;
@@ -70,9 +67,6 @@ export declare const config: {
70
67
  }, {
71
68
  readonly value: string;
72
69
  readonly label: "Name: Z–A";
73
- }, {
74
- readonly value: string;
75
- readonly label: "Most Viewed";
76
70
  }];
77
71
  extraFacetKeys: readonly string[];
78
72
  };
@@ -0,0 +1,70 @@
1
+ /**
2
+ * `{{placeholder}}` interpolation — the ONE implementation.
3
+ *
4
+ * Promoted out of `features/faq/schemas/firestore.ts`, where it had served 63
5
+ * FAQs since the feature shipped. Category description templates need exactly
6
+ * the same thing, and writing a second regex for it is Root Cause #75's shape:
7
+ * one of the two eventually learns about a case the other does not, and the
8
+ * copy a caller reaches is the one they happened to import.
9
+ *
10
+ * `features/faq/schemas/firestore.ts` re-exports these under its own names, so
11
+ * nothing in the FAQ feature changed and no FAQ call site moved.
12
+ *
13
+ * 🛑 NOT a template ENGINE. There are no conditionals, loops, filters or
14
+ * nesting, and none should be added here — a category body is author-facing
15
+ * copy with a handful of named slots, and a real engine would mean user input
16
+ * being evaluated. `\w+` is the whole grammar, which is also why it is safe to
17
+ * run over HTML.
18
+ */
19
+ /**
20
+ * Every placeholder name in `text`, in source order, INCLUDING duplicates.
21
+ *
22
+ * Duplicates are kept deliberately: a caller counting unfilled slots wants to
23
+ * know a body says `{{year}}` three times, and `usedPlaceholders` below is the
24
+ * de-duplicated form for callers that want the set instead.
25
+ */
26
+ export declare function extractPlaceholders(text: string): string[];
27
+ /** The distinct placeholder names in `text`, in first-appearance order. */
28
+ export declare function usedPlaceholders(text: string): string[];
29
+ /**
30
+ * What a slot may resolve to.
31
+ *
32
+ * 🛑 Declared once and shared with `missingPlaceholders`, which only tests key
33
+ * PRESENCE and so does not care about the value type at all. It was
34
+ * `Record<string, unknown>` for exactly that reason, and
35
+ * `audit-unknown-leakage` was right to reject it: `unknown` at a boundary is
36
+ * how a caller ends up casting, and a cast is where this codebase's worst
37
+ * defects live. Sharing the real type costs nothing and means the two
38
+ * functions cannot disagree about what a value is.
39
+ */
40
+ export type PlaceholderValues = Readonly<Record<string, string | number | null | undefined>>;
41
+ export interface InterpolateResult {
42
+ /** The text with every slot replaced. */
43
+ text: string;
44
+ /** Slot names that had no value — the caller reports these to the author. */
45
+ unresolved: string[];
46
+ }
47
+ /**
48
+ * Replace every `{{slot}}` with its value.
49
+ *
50
+ * 🛑 An unresolved slot becomes the EMPTY STRING, never the literal
51
+ * `{{series}}`, and never a placeholder-ish stand-in like `[series]`. A
52
+ * literal brace pair reaching a published product description is the single
53
+ * most visible way this feature can fail, and it is what the `unresolved` list
54
+ * exists to prevent: the caller surfaces "2 of 9 slots could not be filled:
55
+ * {{year}}, {{type}}" rather than silently shipping the braces.
56
+ *
57
+ * A value that is present but empty/whitespace counts as UNRESOLVED. `{{brand}}`
58
+ * resolving to `""` because the brand lookup returned nothing is exactly the
59
+ * case an author needs told about, and treating it as resolved would hide it.
60
+ */
61
+ export declare function interpolate(text: string, values: PlaceholderValues): InterpolateResult;
62
+ /**
63
+ * Which of `text`'s slots have no value in `available`.
64
+ *
65
+ * Separate from `interpolate` because validation runs at AUTHORING time, when
66
+ * there is a schema to check against but no real product to fill from — the
67
+ * admin editor wants to know a template names `{{sereis}}` before anyone
68
+ * publishes from it.
69
+ */
70
+ export declare function missingPlaceholders(text: string, available: PlaceholderValues): string[];
@@ -0,0 +1,73 @@
1
+ /**
2
+ * `{{placeholder}}` interpolation — the ONE implementation.
3
+ *
4
+ * Promoted out of `features/faq/schemas/firestore.ts`, where it had served 63
5
+ * FAQs since the feature shipped. Category description templates need exactly
6
+ * the same thing, and writing a second regex for it is Root Cause #75's shape:
7
+ * one of the two eventually learns about a case the other does not, and the
8
+ * copy a caller reaches is the one they happened to import.
9
+ *
10
+ * `features/faq/schemas/firestore.ts` re-exports these under its own names, so
11
+ * nothing in the FAQ feature changed and no FAQ call site moved.
12
+ *
13
+ * 🛑 NOT a template ENGINE. There are no conditionals, loops, filters or
14
+ * nesting, and none should be added here — a category body is author-facing
15
+ * copy with a handful of named slots, and a real engine would mean user input
16
+ * being evaluated. `\w+` is the whole grammar, which is also why it is safe to
17
+ * run over HTML.
18
+ */
19
+ /** The slot syntax. One capture group: the bare name, no braces. */
20
+ const PLACEHOLDER_RE = /\{\{(\w+)\}\}/g;
21
+ /**
22
+ * Every placeholder name in `text`, in source order, INCLUDING duplicates.
23
+ *
24
+ * Duplicates are kept deliberately: a caller counting unfilled slots wants to
25
+ * know a body says `{{year}}` three times, and `usedPlaceholders` below is the
26
+ * de-duplicated form for callers that want the set instead.
27
+ */
28
+ export function extractPlaceholders(text) {
29
+ return Array.from(text.matchAll(PLACEHOLDER_RE), (m) => m[1]);
30
+ }
31
+ /** The distinct placeholder names in `text`, in first-appearance order. */
32
+ export function usedPlaceholders(text) {
33
+ return Array.from(new Set(extractPlaceholders(text)));
34
+ }
35
+ /**
36
+ * Replace every `{{slot}}` with its value.
37
+ *
38
+ * 🛑 An unresolved slot becomes the EMPTY STRING, never the literal
39
+ * `{{series}}`, and never a placeholder-ish stand-in like `[series]`. A
40
+ * literal brace pair reaching a published product description is the single
41
+ * most visible way this feature can fail, and it is what the `unresolved` list
42
+ * exists to prevent: the caller surfaces "2 of 9 slots could not be filled:
43
+ * {{year}}, {{type}}" rather than silently shipping the braces.
44
+ *
45
+ * A value that is present but empty/whitespace counts as UNRESOLVED. `{{brand}}`
46
+ * resolving to `""` because the brand lookup returned nothing is exactly the
47
+ * case an author needs told about, and treating it as resolved would hide it.
48
+ */
49
+ export function interpolate(text, values) {
50
+ const unresolved = [];
51
+ const out = text.replace(PLACEHOLDER_RE, (_match, name) => {
52
+ const raw = values[name];
53
+ const value = raw == null ? "" : String(raw);
54
+ if (value.trim() === "") {
55
+ if (!unresolved.includes(name))
56
+ unresolved.push(name);
57
+ return "";
58
+ }
59
+ return value;
60
+ });
61
+ return { text: out, unresolved };
62
+ }
63
+ /**
64
+ * Which of `text`'s slots have no value in `available`.
65
+ *
66
+ * Separate from `interpolate` because validation runs at AUTHORING time, when
67
+ * there is a schema to check against but no real product to fill from — the
68
+ * admin editor wants to know a template names `{{sereis}}` before anyone
69
+ * publishes from it.
70
+ */
71
+ export function missingPlaceholders(text, available) {
72
+ return usedPlaceholders(text).filter((name) => !(name in available));
73
+ }
package/dist/client.d.ts CHANGED
@@ -488,6 +488,8 @@ export type { CodHandlingFeeRates, WhatsAppNotifyFeeRates, GiftWrapFeeRates, Shi
488
488
  export { FAQPageContent } from "./features/faq/index";
489
489
  export type { FAQCategory, FAQCategoryItem, FAQSortOption } from "./features/faq/index";
490
490
  export { CategoryInlineSelect } from "./features/seller/components/CategoryInlineSelect";
491
+ export { CategoryTemplatePicker, variantForCondition } from "./features/categories/components/CategoryTemplatePicker";
492
+ export type { CategoryTemplatePickerProps, CategoryTemplateApply } from "./features/categories/components/CategoryTemplatePicker";
491
493
  export { BrandInlineSelect } from "./features/seller/components/BrandInlineSelect";
492
494
  export { API_ENDPOINTS } from "./constants/index";
493
495
  export { SELLER_LISTING_TABS, type SellerListingTabId, } from "./features/products/constants/listing-tabs";
package/dist/client.js CHANGED
@@ -486,6 +486,7 @@ export { checkEmiEligibility, computeEmiSchedule, computeBuyerEmiQuote, } from "
486
486
  export { computeCodHandlingFee } from "./_internal/shared/fees/calculator"; // reexport-from-internal-ok: pure fee-calc utility consumed directly by public checkout UI (CheckoutRouteClient.tsx), same category as the sibling computeEmiSchedule/checkEmiEligibility re-export two lines above
487
487
  export { FAQPageContent } from "./features/faq/index";
488
488
  export { CategoryInlineSelect } from "./features/seller/components/CategoryInlineSelect";
489
+ export { CategoryTemplatePicker, variantForCondition } from "./features/categories/components/CategoryTemplatePicker";
489
490
  export { BrandInlineSelect } from "./features/seller/components/BrandInlineSelect";
490
491
  export { API_ENDPOINTS } from "./constants/index";
491
492
  // [CLIENT-SAFE] Pure, zero-import constant files — dashboard filter-tab