@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.
- package/dist/_internal/server/features/categories/adapters.d.ts +47 -0
- package/dist/_internal/server/features/categories/adapters.js +235 -0
- package/dist/_internal/server/features/checkout/actions.js +161 -22
- package/dist/_internal/server/features/checkout/expand-order-items.js +34 -0
- package/dist/_internal/server/features/orders/adapters.js +21 -0
- package/dist/_internal/server/features/refunds/actions.js +1 -1
- package/dist/_internal/server/features/seo/sitemap.js +54 -15
- package/dist/_internal/server/features/stores/adapters.d.ts +1 -1
- package/dist/_internal/server/features/stores/adapters.js +6 -0
- package/dist/_internal/server/functions/scheduled.d.ts +4 -2
- package/dist/_internal/server/functions/scheduled.js +64 -6
- package/dist/_internal/server/jobs/core/adminAnalytics.d.ts +0 -1
- package/dist/_internal/server/jobs/core/adminAnalytics.js +9 -1
- package/dist/_internal/server/jobs/core/analyticsAlertEvaluate.d.ts +47 -0
- package/dist/_internal/server/jobs/core/analyticsAlertEvaluate.js +468 -0
- package/dist/_internal/server/jobs/core/autoPayoutEligibility.js +14 -1
- package/dist/_internal/server/jobs/core/countersReconcile.js +24 -8
- package/dist/_internal/server/jobs/core/onCategoryWrite.js +54 -0
- package/dist/_internal/server/jobs/core/priceIndexRollup.d.ts +47 -0
- package/dist/_internal/server/jobs/core/priceIndexRollup.js +234 -0
- package/dist/_internal/server/jobs/core/{testerSandboxCleanup.d.ts → testDataCleanup.d.ts} +3 -3
- package/dist/_internal/server/jobs/core/{testerSandboxCleanup.js → testDataCleanup.js} +3 -3
- package/dist/_internal/server/jobs/core/weeklyPayoutEligibility.js +5 -1
- package/dist/_internal/server/jobs/handlers/analyticsAlertEvaluate.d.ts +2 -0
- package/dist/_internal/server/jobs/handlers/analyticsAlertEvaluate.js +2 -0
- package/dist/_internal/server/jobs/handlers/index.d.ts +3 -1
- package/dist/_internal/server/jobs/handlers/index.js +3 -1
- package/dist/_internal/server/jobs/handlers/messages.d.ts +5 -2
- package/dist/_internal/server/jobs/handlers/messages.js +5 -2
- package/dist/_internal/server/jobs/handlers/priceIndexRollup.d.ts +2 -0
- package/dist/_internal/server/jobs/handlers/priceIndexRollup.js +2 -0
- package/dist/_internal/server/jobs/handlers/testDataCleanup.d.ts +2 -0
- package/dist/_internal/server/jobs/handlers/testDataCleanup.js +2 -0
- package/dist/_internal/shared/actions/action-registry.js +34 -0
- package/dist/_internal/shared/checkout/order-math.d.ts +113 -3
- package/dist/_internal/shared/checkout/order-math.js +194 -7
- package/dist/_internal/shared/features/categories/price-guidance.d.ts +110 -0
- package/dist/_internal/shared/features/categories/price-guidance.js +136 -0
- package/dist/_internal/shared/features/site-settings/gst-registration.d.ts +90 -0
- package/dist/_internal/shared/features/site-settings/gst-registration.js +126 -0
- package/dist/_internal/shared/fees/calculator.d.ts +105 -0
- package/dist/_internal/shared/fees/calculator.js +143 -0
- package/dist/_internal/shared/listing-types/art/config.d.ts +0 -6
- package/dist/_internal/shared/listing-types/classified/config.d.ts +0 -6
- package/dist/_internal/shared/listing-types/digital-code/config.d.ts +0 -6
- package/dist/_internal/shared/listing-types/live/config.d.ts +0 -6
- package/dist/_internal/shared/listing-types/standard/config.d.ts +0 -6
- package/dist/_internal/shared/listing-types/stickers/config.d.ts +0 -6
- package/dist/_internal/shared/templating/placeholders.d.ts +70 -0
- package/dist/_internal/shared/templating/placeholders.js +73 -0
- package/dist/client.d.ts +2 -0
- package/dist/client.js +1 -0
- package/dist/constants/api-endpoints.d.ts +42 -0
- package/dist/constants/api-endpoints.js +14 -0
- package/dist/features/admin/actions/admin-actions.d.ts +1 -1
- package/dist/features/admin/actions/admin-actions.js +1 -1
- package/dist/features/admin/components/AdminCategoryEditorView.js +269 -3
- package/dist/features/admin/components/AdminProductEditorView.js +8 -1
- package/dist/features/admin/components/AdminProductsView.js +78 -7
- package/dist/features/admin/components/AdminSiteSettingsView.js +20 -1
- package/dist/features/admin/components/AdminTaxCodeEditorView.d.ts +10 -0
- package/dist/features/admin/components/AdminTaxCodeEditorView.js +160 -0
- package/dist/features/admin/components/AdminTaxCodesView.d.ts +13 -0
- package/dist/features/admin/components/AdminTaxCodesView.js +86 -0
- package/dist/features/admin/components/AdminTesterChecklistView.js +1 -1
- package/dist/features/admin/components/analytics/AdminTopProductsTable.js +1 -1
- package/dist/features/auth/schemas/firestore.d.ts +0 -1
- package/dist/features/blog/schemas/firestore.d.ts +2 -2
- package/dist/features/catalogue/components/CatalogueItemEditorView.js +14 -3
- package/dist/features/categories/api/[id]/description-template/route.d.ts +45 -0
- package/dist/features/categories/api/[id]/description-template/route.js +252 -0
- package/dist/features/categories/api/route.js +156 -11
- package/dist/features/categories/components/CategoryDetailPageView.js +43 -10
- package/dist/features/categories/components/CategoryTemplatePicker.d.ts +65 -0
- package/dist/features/categories/components/CategoryTemplatePicker.js +170 -0
- package/dist/features/categories/constants/listing-categories.d.ts +17 -0
- package/dist/features/categories/constants/listing-categories.js +53 -0
- package/dist/features/categories/hooks/useCategoryTree.js +28 -2
- package/dist/features/categories/repository/categories.repository.d.ts +62 -11
- package/dist/features/categories/repository/categories.repository.js +178 -35
- package/dist/features/categories/repository/price-index.repository.d.ts +115 -0
- package/dist/features/categories/repository/price-index.repository.js +85 -0
- package/dist/features/categories/schemas/category-content.d.ts +206 -0
- package/dist/features/categories/schemas/category-content.js +28 -0
- package/dist/features/categories/schemas/firestore.d.ts +74 -5
- package/dist/features/categories/schemas/firestore.js +74 -3
- package/dist/features/categories/schemas/index.d.ts +1047 -0
- package/dist/features/categories/schemas/index.js +96 -0
- package/dist/features/categories/server.d.ts +1 -0
- package/dist/features/categories/server.js +7 -0
- package/dist/features/categories/types/index.d.ts +9 -0
- package/dist/features/events/schemas/firestore.d.ts +2 -2
- package/dist/features/faq/schemas/firestore.d.ts +2 -1
- package/dist/features/faq/schemas/firestore.js +20 -8
- package/dist/features/orders/schemas/firestore.d.ts +70 -0
- package/dist/features/orders/types/index.d.ts +55 -0
- package/dist/features/products/columns/index.d.ts +0 -1
- package/dist/features/products/columns/index.js +5 -1
- package/dist/features/products/components/ProductFilters.d.ts +0 -9
- package/dist/features/products/components/ProductForm.d.ts +27 -1
- package/dist/features/products/components/ProductForm.js +26 -12
- package/dist/features/products/constants/sieve.d.ts +0 -6
- package/dist/features/products/constants/sieve.js +35 -2
- package/dist/features/products/repository/products.repository.d.ts +115 -1
- package/dist/features/products/repository/products.repository.js +491 -39
- package/dist/features/products/schemas/firestore.d.ts +65 -5
- package/dist/features/products/schemas/firestore.js +9 -0
- package/dist/features/products/schemas/product-features.d.ts +140 -2
- package/dist/features/products/schemas/product-features.js +62 -1
- package/dist/features/promotions/repository/coupons.repository.js +6 -1
- package/dist/features/promotions/schemas/firestore.d.ts +44 -1
- package/dist/features/promotions/schemas/firestore.js +76 -2
- package/dist/features/seller/actions/seller-actions.js +17 -2
- package/dist/features/seller/components/PrintCenterView.d.ts +3 -2
- package/dist/features/seller/components/PrintCenterView.js +3 -2
- package/dist/features/seller/components/SellerAnalyticsAlertsView.js +21 -11
- package/dist/features/seller/components/SellerOrdersView.js +6 -1
- package/dist/features/seller/components/SellerPayoutSettingsView.js +19 -3
- package/dist/features/seller/components/SellerProductShell.d.ts +50 -2
- package/dist/features/seller/components/SellerProductShell.js +101 -9
- package/dist/features/seller/components/SellerProductsFilterDrawer.d.ts +13 -1
- package/dist/features/seller/components/SellerProductsFilterDrawer.js +2 -2
- package/dist/features/seller/components/SellerProductsView.js +108 -3
- package/dist/features/seller/types/index.d.ts +17 -0
- package/dist/features/seller/utils/product-draft-mapping.d.ts +13 -0
- package/dist/features/seller/utils/product-draft-mapping.js +17 -0
- package/dist/features/store-extensions/constants/alert-metrics.d.ts +86 -0
- package/dist/features/store-extensions/constants/alert-metrics.js +142 -0
- package/dist/features/store-extensions/schemas/firestore.d.ts +19 -1
- package/dist/features/stores/schemas/firestore.d.ts +31 -2
- package/dist/features/tax-codes/repository/tax-codes.repository.d.ts +55 -0
- package/dist/features/tax-codes/repository/tax-codes.repository.js +131 -0
- package/dist/features/tax-codes/schemas/firestore.d.ts +125 -0
- package/dist/features/tax-codes/schemas/firestore.js +82 -0
- package/dist/features/tax-codes/schemas/tax-code-form.d.ts +63 -0
- package/dist/features/tax-codes/schemas/tax-code-form.js +152 -0
- package/dist/features/tax-codes/schemas/tax-codes.validators.d.ts +107 -0
- package/dist/features/tax-codes/schemas/tax-codes.validators.js +85 -0
- package/dist/features/tester/actions/index.d.ts +0 -1
- package/dist/features/tester/actions/index.js +0 -1
- package/dist/features/tester/components/AdminTesterFeedbackIssuesView.js +1 -1
- package/dist/features/tester/components/AdminTesterFeedbackListView.js +1 -1
- package/dist/features/tester/components/index.d.ts +0 -4
- package/dist/features/tester/components/index.js +0 -2
- package/dist/features/tester/repository/tester-checklist-item.repository.d.ts +1 -20
- package/dist/features/tester/repository/tester-checklist-item.repository.js +0 -52
- package/dist/features/tester/schemas/firestore.d.ts +0 -6
- package/dist/features/tester/seed-data/authored/admin__admin-detail-round-trips.js +3 -3
- package/dist/features/tester/seed-data/authored/admin__site-system.js +4 -5
- package/dist/features/tester/seed-data/authored/content-discovery__category-counts-and-rollup.js +2 -2
- package/dist/features/tester/seed-data/authored/index.js +0 -2
- package/dist/features/tester/seed-data/authored/page-wiring__reachability.js +0 -19
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +8 -27
- package/dist/index.d.ts +21 -7
- package/dist/index.js +53 -7
- package/dist/next/routing/route-map.d.ts +2 -4
- package/dist/next/routing/route-map.js +1 -2
- package/dist/repositories/index.d.ts +1 -0
- package/dist/repositories/index.js +1 -0
- package/dist/schemas/registry.d.ts +506 -0
- package/dist/seed/_helpers/brand-rows.d.ts +35 -0
- package/dist/seed/_helpers/brand-rows.js +576 -0
- package/dist/seed/_helpers/category-content.d.ts +39 -0
- package/dist/seed/_helpers/category-content.js +634 -0
- package/dist/seed/_helpers/category-forest.d.ts +2 -0
- package/dist/seed/_helpers/category-forest.js +585 -243
- package/dist/seed/_helpers/category-models.d.ts +5 -0
- package/dist/seed/_helpers/category-models.js +1364 -0
- package/dist/seed/_helpers/category-tree.js +17 -1
- package/dist/seed/_helpers/character-features.d.ts +10 -0
- package/dist/seed/_helpers/character-features.js +118 -0
- package/dist/seed/_helpers/search-txt-wrappers.d.ts +10 -0
- package/dist/seed/_helpers/search-txt-wrappers.js +12 -1
- package/dist/seed/categories-seed-data.js +147 -169
- package/dist/seed/index.d.ts +1 -0
- package/dist/seed/index.js +1 -0
- package/dist/seed/manifest.d.ts +1 -1
- package/dist/seed/manifest.js +5 -0
- package/dist/seed/product-features-seed-data.d.ts +1 -8
- package/dist/seed/product-features-seed-data.js +358 -154
- package/dist/seed/products-art-seed-data.js +16 -4
- package/dist/seed/products-auctions-seed-data.js +6 -6
- package/dist/seed/products-classifieds-seed-data.js +17 -7
- package/dist/seed/products-digital-codes-seed-data.js +12 -12
- package/dist/seed/products-preorders-seed-data.js +8 -8
- package/dist/seed/products-prize-draws-seed-data.js +25 -8
- package/dist/seed/products-standard-seed-data.js +10 -60
- package/dist/seed/products-stickers-seed-data.js +6 -6
- package/dist/seed/site-settings-seed-data.js +24 -0
- package/dist/seed/store-extensions-seed-data.js +10 -4
- package/dist/seed/tax-codes-seed-data.d.ts +34 -0
- package/dist/seed/tax-codes-seed-data.js +100 -0
- package/dist/seed/test-data-ttl.d.ts +29 -0
- package/dist/seed/test-data-ttl.js +31 -0
- package/dist/server-entry.d.ts +1 -1
- package/dist/server-entry.js +6 -1
- package/dist/server.d.ts +2 -1
- package/dist/server.js +5 -2
- package/dist/styles.css +40 -0
- package/dist/ui/components/PaginatedSelect.d.ts +14 -0
- package/dist/ui/components/PaginatedSelect.js +0 -0
- package/dist/ui/components/PaginatedSelect.style.css +40 -0
- package/dist/utils/physical-location.d.ts +23 -0
- package/dist/utils/physical-location.js +52 -0
- package/dist/utils/search-txt-builders.d.ts +20 -0
- package/dist/utils/search-txt-builders.js +36 -0
- package/package.json +1 -1
- package/scripts/seed-cli.mjs +4 -1
- package/dist/_internal/server/jobs/handlers/testerSandboxCleanup.d.ts +0 -2
- package/dist/_internal/server/jobs/handlers/testerSandboxCleanup.js +0 -2
- package/dist/features/tester/actions/leaderboard-actions.d.ts +0 -3
- package/dist/features/tester/actions/leaderboard-actions.js +0 -5
- package/dist/features/tester/components/BugHunterLeaderboardView.d.ts +0 -23
- package/dist/features/tester/components/BugHunterLeaderboardView.js +0 -18
- package/dist/features/tester/components/TesterHubView.d.ts +0 -5
- package/dist/features/tester/components/TesterHubView.js +0 -110
- package/dist/features/tester/seed-data/authored/public-pages__bug-hunters.d.ts +0 -2
- package/dist/features/tester/seed-data/authored/public-pages__bug-hunters.js +0 -78
- package/dist/features/tester/seed-data/tester-ttl.d.ts +0 -4
- package/dist/features/tester/seed-data/tester-ttl.js +0 -6
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { roundRupees } from "../../../utils/number.formatter";
|
|
2
|
-
import {
|
|
2
|
+
import { calculateGstInclusive, taxableFromInclusive } from "../fees/calculator";
|
|
3
3
|
/**
|
|
4
4
|
* The single source of truth for "what do we charge for ONE COPY of this line".
|
|
5
5
|
*
|
|
@@ -88,7 +88,22 @@ export function lineTotalFor(item, product) {
|
|
|
88
88
|
* `productById` supplies the live `gstRate`; each member's snapshotted rate is
|
|
89
89
|
* the fallback for a member whose listing has since been archived.
|
|
90
90
|
*/
|
|
91
|
-
export function lineTaxComponentsFor(item,
|
|
91
|
+
export function lineTaxComponentsFor(item,
|
|
92
|
+
/*
|
|
93
|
+
* Tolerates a null value, and always did — every read below is
|
|
94
|
+
* `?.gstRate ?? 0` / `?? null`. The type was merely narrower than the
|
|
95
|
+
* behaviour, which forced the consumer's create-order route (whose
|
|
96
|
+
* `findById` map is honestly `| null`) to cast. `lineTotalFor` already
|
|
97
|
+
* declares `ProductDocument | null` for the same reason.
|
|
98
|
+
*
|
|
99
|
+
* 🛑 Caveat worth knowing: a MISSING product yields `gstRate ?? 0`, and
|
|
100
|
+
* since 2026-10-10 a 0 rate is accumulated into `exemptAmount` — so an
|
|
101
|
+
* unloadable product would be reported as exempt rather than as unknown.
|
|
102
|
+
* Every caller validates products before reaching here (the route throws on
|
|
103
|
+
* a missing or unpublished one), so this is theoretical; it would stop being
|
|
104
|
+
* theoretical if a caller ever skipped that check.
|
|
105
|
+
*/
|
|
106
|
+
productById) {
|
|
92
107
|
const lineTotal = lineTotalFor(item, productById.get(item.productId) ?? null);
|
|
93
108
|
const members = item.groupMembers;
|
|
94
109
|
if (!members?.length) {
|
|
@@ -128,18 +143,94 @@ export function lineTaxComponentsFor(item, productById) {
|
|
|
128
143
|
* one and not the other — here that would mean quoting one tax figure and
|
|
129
144
|
* charging another.
|
|
130
145
|
*/
|
|
131
|
-
export function sumGroupGst(items,
|
|
146
|
+
export function sumGroupGst(items,
|
|
147
|
+
/* `| null` to match `lineTaxComponentsFor` and `lineTotalFor` — see the note
|
|
148
|
+
* there. The consumer's create-order route holds an honestly-nullable map. */
|
|
149
|
+
productById, intraState) {
|
|
132
150
|
let taxableAmount = 0;
|
|
151
|
+
let exemptAmount = 0;
|
|
152
|
+
let cgst = 0;
|
|
153
|
+
let sgst = 0;
|
|
154
|
+
let igst = 0;
|
|
133
155
|
let gstAmount = 0;
|
|
156
|
+
const byRate = new Map();
|
|
134
157
|
for (const item of items) {
|
|
135
158
|
for (const slice of lineTaxComponentsFor(item, productById)) {
|
|
136
|
-
|
|
159
|
+
/*
|
|
160
|
+
* 🛑 A 0%-rated slice is EXEMPT, not absent. It was `continue`d
|
|
161
|
+
* entirely, so `order.taxableAmount` excluded exempt goods while
|
|
162
|
+
* `items[]` included them — and no mixed exempt/taxed order could ever
|
|
163
|
+
* reconcile its own Taxable column against its own summary. It is
|
|
164
|
+
* accumulated separately instead: representable, without inflating the
|
|
165
|
+
* base the tax was actually charged on.
|
|
166
|
+
*/
|
|
167
|
+
if (slice.gstRate <= 0) {
|
|
168
|
+
exemptAmount += slice.taxable;
|
|
137
169
|
continue;
|
|
138
|
-
|
|
139
|
-
|
|
170
|
+
}
|
|
171
|
+
/*
|
|
172
|
+
* 🛑 `slice.taxable` is the line's GROSS share — prices are
|
|
173
|
+
* tax-INCLUSIVE — so the tax is backed OUT of it, never added on top.
|
|
174
|
+
* `lineTaxComponentsFor`'s documented invariant ("slices of a line sum
|
|
175
|
+
* to its line total") stays true of the gross, which is why the
|
|
176
|
+
* back-out lives here rather than in there.
|
|
177
|
+
*
|
|
178
|
+
* ₹999 @ 5% → taxable ₹951.43 + tax ₹47.57 = ₹999.00 exactly.
|
|
179
|
+
*
|
|
180
|
+
* `calculateGstInclusive`, not `calculateGst(…, taxableFromInclusive(…))`:
|
|
181
|
+
* that composition rounds twice and came to ₹999.01, so the invoice
|
|
182
|
+
* stopped reconciling with the amount charged. Under inclusive pricing
|
|
183
|
+
* the tax is a PARTITION of a fixed amount, not an addition to it.
|
|
184
|
+
*/
|
|
185
|
+
const b = calculateGstInclusive(slice.gstRate, intraState, slice.taxable);
|
|
186
|
+
const slabTaxable = b.taxableAmount;
|
|
187
|
+
/*
|
|
188
|
+
* The split comes PER SLICE and is accumulated. `createOrderForGroup`
|
|
189
|
+
* used to re-derive it from the total with `Math.round(gstAmount / 2)` —
|
|
190
|
+
* whole rupees, so ₹161.82 split to 81.00/80.82 instead of 80.91/80.91,
|
|
191
|
+
* and ₹0.52 rounded to 0 and then `|| undefined` dropped the CGST line
|
|
192
|
+
* off the invoice entirely.
|
|
193
|
+
*/
|
|
194
|
+
taxableAmount += slabTaxable;
|
|
195
|
+
cgst += b.cgst;
|
|
196
|
+
sgst += b.sgst;
|
|
197
|
+
igst += b.igst;
|
|
198
|
+
gstAmount += b.gstAmount;
|
|
199
|
+
/*
|
|
200
|
+
* Per-rate, because Rule 46 wants taxable value and tax PER RATE on the
|
|
201
|
+
* invoice. A mixed 5%+18% order cannot produce a compliant document from
|
|
202
|
+
* one aggregate triple, and re-deriving it downstream means a second
|
|
203
|
+
* implementation of this loop.
|
|
204
|
+
*/
|
|
205
|
+
const prev = byRate.get(slice.gstRate);
|
|
206
|
+
if (prev) {
|
|
207
|
+
prev.taxableAmount = roundRupees(prev.taxableAmount + slabTaxable);
|
|
208
|
+
prev.cgst = roundRupees(prev.cgst + b.cgst);
|
|
209
|
+
prev.sgst = roundRupees(prev.sgst + b.sgst);
|
|
210
|
+
prev.igst = roundRupees(prev.igst + b.igst);
|
|
211
|
+
prev.gstAmount = roundRupees(prev.gstAmount + b.gstAmount);
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
byRate.set(slice.gstRate, {
|
|
215
|
+
gstRate: slice.gstRate,
|
|
216
|
+
taxableAmount: roundRupees(slabTaxable),
|
|
217
|
+
cgst: b.cgst,
|
|
218
|
+
sgst: b.sgst,
|
|
219
|
+
igst: b.igst,
|
|
220
|
+
gstAmount: b.gstAmount,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
140
223
|
}
|
|
141
224
|
}
|
|
142
|
-
return {
|
|
225
|
+
return {
|
|
226
|
+
taxableAmount: roundRupees(taxableAmount),
|
|
227
|
+
exemptAmount: roundRupees(exemptAmount),
|
|
228
|
+
cgst: roundRupees(cgst),
|
|
229
|
+
sgst: roundRupees(sgst),
|
|
230
|
+
igst: roundRupees(igst),
|
|
231
|
+
gstAmount: roundRupees(gstAmount),
|
|
232
|
+
byRate: [...byRate.values()].sort((x, y) => x.gstRate - y.gstRate),
|
|
233
|
+
};
|
|
143
234
|
}
|
|
144
235
|
/**
|
|
145
236
|
* Split `total` across members in proportion to their list prices, with the
|
|
@@ -166,6 +257,102 @@ export function allocateAcrossMembers(total, weights) {
|
|
|
166
257
|
out.push(roundRupees(total - running));
|
|
167
258
|
return out;
|
|
168
259
|
}
|
|
260
|
+
/**
|
|
261
|
+
* The GOODS VALUE of an order — the only correct base for a seller payout.
|
|
262
|
+
*
|
|
263
|
+
* 🛑 This is NOT `OrderDocument.totalPrice`, and conflating the two was a live
|
|
264
|
+
* money bug in all four payout paths (`autoPayoutEligibility`,
|
|
265
|
+
* `weeklyPayoutEligibility`, and both `seller-actions` entry points). That
|
|
266
|
+
* field is the GRAND TOTAL — `checkout/actions.ts` builds it as
|
|
267
|
+
* `goods − coupon + shipping + codHandling + whatsappNotify + giftWrap +
|
|
268
|
+
* shipmentProtection + platformFee + platformFeeGst + emiSurcharge + gstAmount`
|
|
269
|
+
* — so using it as the payout base did three wrong things at once:
|
|
270
|
+
*
|
|
271
|
+
* 1. **paid the seller the GST we collected** and must remit ourselves;
|
|
272
|
+
* 2. **took the platform fee twice** — the buyer already paid it as a term of
|
|
273
|
+
* `totalPrice`, and `computePayoutDeduction` then charged the seller a
|
|
274
|
+
* percentage of a figure that already included it;
|
|
275
|
+
* 3. **computed every percentage on an inflated base**, since shipping, COD
|
|
276
|
+
* handling, add-on fees and the EMI surcharge were all inside it.
|
|
277
|
+
*
|
|
278
|
+
* The payout base is the **taxable value**, never the gross. That one sentence
|
|
279
|
+
* also covers GST-inclusive pricing: an inclusive ₹999 @5% backs out to a
|
|
280
|
+
* ₹951.43 taxable value and that is what is paid, while an exclusive ₹999
|
|
281
|
+
* charges the buyer ₹1,048.95 and pays ₹999.
|
|
282
|
+
*
|
|
283
|
+
* 🛑 Computed from `quantity − cancelledQuantity`, NOT from `item.totalPrice`.
|
|
284
|
+
* A partial cancellation sets `cancelledQuantity: item.quantity` and
|
|
285
|
+
* **deliberately leaves `totalPrice` untouched** (`order-actions.ts:97-106`),
|
|
286
|
+
* so summing `totalPrice` would pay the seller in full for lines the buyer
|
|
287
|
+
* cancelled. `stock-restore.ts:20` already derives the live quantity this way.
|
|
288
|
+
*
|
|
289
|
+
* 🛑 Falls back to the FLAT `quantity × unitPrice` when `items` is absent.
|
|
290
|
+
* `items?` is optional on `OrderDocument` and the flat fields are the primary
|
|
291
|
+
* shape, not legacy residue — a bare `Σ items[]` would silently pay **zero**
|
|
292
|
+
* on any flat-shaped order, which is indistinguishable from "this seller is
|
|
293
|
+
* owed nothing".
|
|
294
|
+
*/
|
|
295
|
+
export function orderGoodsValue(order) {
|
|
296
|
+
const items = order.items;
|
|
297
|
+
if (items?.length) {
|
|
298
|
+
const raw = items.reduce((sum, i) => {
|
|
299
|
+
const live = Math.max(0, i.quantity - (i.cancelledQuantity ?? 0));
|
|
300
|
+
return sum + live * i.unitPrice;
|
|
301
|
+
}, 0);
|
|
302
|
+
return roundRupees(raw);
|
|
303
|
+
}
|
|
304
|
+
return roundRupees((order.quantity ?? 0) * (order.unitPrice ?? 0));
|
|
305
|
+
}
|
|
306
|
+
/** Sum of `orderGoodsValue` across orders — the GROSS, not the payout base. */
|
|
307
|
+
export function ordersGoodsValue(orders) {
|
|
308
|
+
return roundRupees(orders.reduce((sum, o) => sum + orderGoodsValue(o), 0));
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* The TAXABLE VALUE of an order — **the payout base.**
|
|
312
|
+
*
|
|
313
|
+
* 🛑 This is the second of two narrowings, and the distinction between them is
|
|
314
|
+
* worth keeping straight:
|
|
315
|
+
*
|
|
316
|
+
* - C0 established the base is the **goods value**, not the order grand total
|
|
317
|
+
* (which folded in GST, the platform fee the buyer already paid, shipping,
|
|
318
|
+
* COD handling, add-ons and the EMI surcharge).
|
|
319
|
+
* - This establishes it is the **taxable value**, not the gross goods value —
|
|
320
|
+
* because every price here is tax-INCLUSIVE, so the goods value still has
|
|
321
|
+
* our GST sitting inside it, and paying that out is the very thing C0 set
|
|
322
|
+
* out to stop.
|
|
323
|
+
*
|
|
324
|
+
* The second narrowing was not possible until the pricing model was settled:
|
|
325
|
+
* under the old add-on-top math the goods value WAS the taxable value, so the
|
|
326
|
+
* two were indistinguishable and C0's fix was complete as written.
|
|
327
|
+
*
|
|
328
|
+
* One sentence covers both seller modes, which is the reason the flag is an
|
|
329
|
+
* authoring convenience and nothing more:
|
|
330
|
+
*
|
|
331
|
+
* **the payout base is the taxable value, never the gross.**
|
|
332
|
+
*
|
|
333
|
+
* - inclusive seller, ₹999 @5% → buyer pays ₹999, taxable ₹951.43, payout ₹951.43
|
|
334
|
+
* - exclusive seller types ₹999 → grossed up on write to ₹1,048.95, buyer
|
|
335
|
+
* pays ₹1,048.95, taxable ₹999.00, payout ₹999.00
|
|
336
|
+
*
|
|
337
|
+
* Per ITEM, using each line's own snapshotted `gstRate`: a mixed 5% + 18%
|
|
338
|
+
* order cannot be backed out with a single blended rate, and an order-level
|
|
339
|
+
* rate does not exist.
|
|
340
|
+
*/
|
|
341
|
+
export function orderTaxableValue(order) {
|
|
342
|
+
const items = order.items;
|
|
343
|
+
if (items?.length) {
|
|
344
|
+
const raw = items.reduce((sum, i) => {
|
|
345
|
+
const live = Math.max(0, i.quantity - (i.cancelledQuantity ?? 0));
|
|
346
|
+
return sum + taxableFromInclusive(live * i.unitPrice, i.gstRate ?? 0);
|
|
347
|
+
}, 0);
|
|
348
|
+
return roundRupees(raw);
|
|
349
|
+
}
|
|
350
|
+
return taxableFromInclusive((order.quantity ?? 0) * (order.unitPrice ?? 0), order.gstRate ?? 0);
|
|
351
|
+
}
|
|
352
|
+
/** Sum of `orderTaxableValue` across orders — the payout base for a batch. */
|
|
353
|
+
export function ordersTaxableValue(orders) {
|
|
354
|
+
return roundRupees(orders.reduce((sum, o) => sum + orderTaxableValue(o), 0));
|
|
355
|
+
}
|
|
169
356
|
/**
|
|
170
357
|
* P-6 — pre-order groups charge each product's own `preOrderDepositPercent`
|
|
171
358
|
* (falling back to the generic COD deposit % when a product doesn't have one
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Price guidance, stated on the basis the SELLER is typing in.
|
|
3
|
+
*
|
|
4
|
+
* `CategoryPriceGuidance` is advisory — it renders as a hint beside the price
|
|
5
|
+
* input and is never written to `price`. This module answers the one question
|
|
6
|
+
* that hint cannot be rendered without: **inclusive of what?**
|
|
7
|
+
*
|
|
8
|
+
* ## The invariant
|
|
9
|
+
*
|
|
10
|
+
* 🛑 **Every figure in a guidance band is TAX-INCLUSIVE.** Both halves, for
|
|
11
|
+
* different reasons, and neither is a choice we get to make:
|
|
12
|
+
*
|
|
13
|
+
* `market` — competitor ASKS. An Indian storefront's displayed price is
|
|
14
|
+
* inclusive by market convention, and worldhobbyshop asserts it
|
|
15
|
+
* outright in its own JSON-LD (`valueAddedTaxIncluded: true`).
|
|
16
|
+
* `sold` — our own completed sales, derived from `items[].unitPrice`, and
|
|
17
|
+
* every stored price in this system is tax-inclusive.
|
|
18
|
+
*
|
|
19
|
+
* So the basis is a documented property of the data rather than a per-row
|
|
20
|
+
* field. A `basis: "inclusive"` column whose every value is the same string is
|
|
21
|
+
* a field that cannot answer a question, and the first row to carry the other
|
|
22
|
+
* value would be a bug rather than a variant.
|
|
23
|
+
*
|
|
24
|
+
* ## Why the hint still has to say so
|
|
25
|
+
*
|
|
26
|
+
* A seller with `pricesIncludeGst: false` types EX-TAX numbers. Their ₹999 and
|
|
27
|
+
* an inclusive ₹999 band are **different quantities** — comparing them is off
|
|
28
|
+
* by the rate, silently, in the direction that makes their listing look
|
|
29
|
+
* cheaper than the market when it is dearer. The hint therefore converts the
|
|
30
|
+
* band into the seller's own basis and labels which one it is on.
|
|
31
|
+
*
|
|
32
|
+
* Pure arithmetic and strings. No React, no Firestore.
|
|
33
|
+
*/
|
|
34
|
+
import type { CategoryPriceGuidance } from "../../../../features/categories/schemas/category-content";
|
|
35
|
+
/**
|
|
36
|
+
* The basis every stored and crawled guidance figure is on.
|
|
37
|
+
*
|
|
38
|
+
* Exported as a named constant so the claim is greppable from the places that
|
|
39
|
+
* depend on it, and so a future change is one edit rather than an archaeology
|
|
40
|
+
* exercise across the renderer, the seed helper and the rollup job.
|
|
41
|
+
*/
|
|
42
|
+
export declare const PRICE_GUIDANCE_TAX_BASIS: "inclusive";
|
|
43
|
+
export interface PriceGuidanceBasis {
|
|
44
|
+
/**
|
|
45
|
+
* How the seller types prices — `StoreDocument.pricesIncludeGst`.
|
|
46
|
+
*
|
|
47
|
+
* 🛑 ABSENT means `true`, matching the schema default and the store route's
|
|
48
|
+
* `?? true`. Three places now agree on that reading; a fourth that treated
|
|
49
|
+
* absence as `false` would convert a band for a seller who never asked.
|
|
50
|
+
*/
|
|
51
|
+
sellerIncludesGst?: boolean;
|
|
52
|
+
/**
|
|
53
|
+
* The rate that will be written onto this listing, resolved the same way
|
|
54
|
+
* `deriveTaxonomy` resolves it (product → category tax code → default).
|
|
55
|
+
*
|
|
56
|
+
* `0` is a deliberate exemption and `undefined` is "we could not resolve
|
|
57
|
+
* one" — and the two must not be collapsed here any more than anywhere else
|
|
58
|
+
* in this codebase. See `describePriceGuidanceBasis`.
|
|
59
|
+
*/
|
|
60
|
+
gstRate?: number;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* One guidance amount, restated on the seller's basis.
|
|
64
|
+
*
|
|
65
|
+
* Inclusive seller, exempt item, or an unresolved rate → unchanged. The last
|
|
66
|
+
* of those is deliberate: converting at `DEFAULT_GST_RATE` would be a guess
|
|
67
|
+
* presented as arithmetic, and the label says so instead.
|
|
68
|
+
*/
|
|
69
|
+
export declare function priceGuidanceAmountOnSellerBasis(amount: number, basis: PriceGuidanceBasis): number;
|
|
70
|
+
/** The inverse — a seller's typed figure, restated as the band's basis. */
|
|
71
|
+
export declare function sellerAmountOnGuidanceBasis(amount: number, basis: PriceGuidanceBasis): number;
|
|
72
|
+
/**
|
|
73
|
+
* The whole guidance object, restated on the seller's basis.
|
|
74
|
+
*
|
|
75
|
+
* 🛑 `sampleSize`, `windowDays`, `soldOutShare` and `asOf` are NOT money and
|
|
76
|
+
* pass through untouched. Mapping every numeric field would silently divide a
|
|
77
|
+
* sample size by 1.05 — the kind of defect that produces a plausible number
|
|
78
|
+
* nobody can trace, which is the failure mode this plan keeps finding.
|
|
79
|
+
*/
|
|
80
|
+
export declare function priceGuidanceOnSellerBasis(guidance: CategoryPriceGuidance, basis: PriceGuidanceBasis): CategoryPriceGuidance;
|
|
81
|
+
/** What the hint must say about which basis its figures are on. */
|
|
82
|
+
export interface PriceGuidanceBasisNote {
|
|
83
|
+
/** A short suffix for the figures themselves — "incl. GST", "before GST". */
|
|
84
|
+
suffix: string;
|
|
85
|
+
/**
|
|
86
|
+
* The longer explanation, or `null` when there is nothing worth saying.
|
|
87
|
+
*
|
|
88
|
+
* `null` for the common case — an inclusive seller on a taxed item is
|
|
89
|
+
* already comparing like with like, so a sentence telling them so is noise
|
|
90
|
+
* that trains people to stop reading hints.
|
|
91
|
+
*/
|
|
92
|
+
note: string | null;
|
|
93
|
+
/** True when the figures were restated rather than shown as stored. */
|
|
94
|
+
converted: boolean;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Four cases, and the fourth is the one worth having this function for.
|
|
98
|
+
*
|
|
99
|
+
* | seller basis | rate | figures | says |
|
|
100
|
+
* |---|---|---|---|
|
|
101
|
+
* | inclusive | > 0 | unchanged | "incl. GST" |
|
|
102
|
+
* | exclusive | > 0 | CONVERTED | "before GST", and that it was converted |
|
|
103
|
+
* | either | 0 | unchanged | nothing about tax — an exempt item has none |
|
|
104
|
+
* | exclusive | unresolved | unchanged | 🛑 that they are NOT comparable |
|
|
105
|
+
*
|
|
106
|
+
* 🛑 The exempt case must not say "incl. GST". The figure is a bare amount and
|
|
107
|
+
* claiming a tax basis on it is false — which is precisely the lie the seller
|
|
108
|
+
* form's old `?? 0` default told on every listing before C0c defect 6.
|
|
109
|
+
*/
|
|
110
|
+
export declare function describePriceGuidanceBasis(basis: PriceGuidanceBasis): PriceGuidanceBasisNote;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Price guidance, stated on the basis the SELLER is typing in.
|
|
3
|
+
*
|
|
4
|
+
* `CategoryPriceGuidance` is advisory — it renders as a hint beside the price
|
|
5
|
+
* input and is never written to `price`. This module answers the one question
|
|
6
|
+
* that hint cannot be rendered without: **inclusive of what?**
|
|
7
|
+
*
|
|
8
|
+
* ## The invariant
|
|
9
|
+
*
|
|
10
|
+
* 🛑 **Every figure in a guidance band is TAX-INCLUSIVE.** Both halves, for
|
|
11
|
+
* different reasons, and neither is a choice we get to make:
|
|
12
|
+
*
|
|
13
|
+
* `market` — competitor ASKS. An Indian storefront's displayed price is
|
|
14
|
+
* inclusive by market convention, and worldhobbyshop asserts it
|
|
15
|
+
* outright in its own JSON-LD (`valueAddedTaxIncluded: true`).
|
|
16
|
+
* `sold` — our own completed sales, derived from `items[].unitPrice`, and
|
|
17
|
+
* every stored price in this system is tax-inclusive.
|
|
18
|
+
*
|
|
19
|
+
* So the basis is a documented property of the data rather than a per-row
|
|
20
|
+
* field. A `basis: "inclusive"` column whose every value is the same string is
|
|
21
|
+
* a field that cannot answer a question, and the first row to carry the other
|
|
22
|
+
* value would be a bug rather than a variant.
|
|
23
|
+
*
|
|
24
|
+
* ## Why the hint still has to say so
|
|
25
|
+
*
|
|
26
|
+
* A seller with `pricesIncludeGst: false` types EX-TAX numbers. Their ₹999 and
|
|
27
|
+
* an inclusive ₹999 band are **different quantities** — comparing them is off
|
|
28
|
+
* by the rate, silently, in the direction that makes their listing look
|
|
29
|
+
* cheaper than the market when it is dearer. The hint therefore converts the
|
|
30
|
+
* band into the seller's own basis and labels which one it is on.
|
|
31
|
+
*
|
|
32
|
+
* Pure arithmetic and strings. No React, no Firestore.
|
|
33
|
+
*/
|
|
34
|
+
import { taxableFromInclusive, grossFromTaxable, } from "../../fees/calculator";
|
|
35
|
+
/**
|
|
36
|
+
* The basis every stored and crawled guidance figure is on.
|
|
37
|
+
*
|
|
38
|
+
* Exported as a named constant so the claim is greppable from the places that
|
|
39
|
+
* depend on it, and so a future change is one edit rather than an archaeology
|
|
40
|
+
* exercise across the renderer, the seed helper and the rollup job.
|
|
41
|
+
*/
|
|
42
|
+
export const PRICE_GUIDANCE_TAX_BASIS = "inclusive";
|
|
43
|
+
/** Whether a conversion is needed at all, and possible. */
|
|
44
|
+
function shouldConvert(basis) {
|
|
45
|
+
const includes = basis.sellerIncludesGst ?? true;
|
|
46
|
+
return !includes && typeof basis.gstRate === "number" && basis.gstRate > 0;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* One guidance amount, restated on the seller's basis.
|
|
50
|
+
*
|
|
51
|
+
* Inclusive seller, exempt item, or an unresolved rate → unchanged. The last
|
|
52
|
+
* of those is deliberate: converting at `DEFAULT_GST_RATE` would be a guess
|
|
53
|
+
* presented as arithmetic, and the label says so instead.
|
|
54
|
+
*/
|
|
55
|
+
export function priceGuidanceAmountOnSellerBasis(amount, basis) {
|
|
56
|
+
if (!shouldConvert(basis))
|
|
57
|
+
return amount;
|
|
58
|
+
return taxableFromInclusive(amount, basis.gstRate);
|
|
59
|
+
}
|
|
60
|
+
/** The inverse — a seller's typed figure, restated as the band's basis. */
|
|
61
|
+
export function sellerAmountOnGuidanceBasis(amount, basis) {
|
|
62
|
+
if (!shouldConvert(basis))
|
|
63
|
+
return amount;
|
|
64
|
+
return grossFromTaxable(amount, basis.gstRate);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The whole guidance object, restated on the seller's basis.
|
|
68
|
+
*
|
|
69
|
+
* 🛑 `sampleSize`, `windowDays`, `soldOutShare` and `asOf` are NOT money and
|
|
70
|
+
* pass through untouched. Mapping every numeric field would silently divide a
|
|
71
|
+
* sample size by 1.05 — the kind of defect that produces a plausible number
|
|
72
|
+
* nobody can trace, which is the failure mode this plan keeps finding.
|
|
73
|
+
*/
|
|
74
|
+
export function priceGuidanceOnSellerBasis(guidance, basis) {
|
|
75
|
+
if (!shouldConvert(basis))
|
|
76
|
+
return guidance;
|
|
77
|
+
const on = (n) => priceGuidanceAmountOnSellerBasis(n, basis);
|
|
78
|
+
return {
|
|
79
|
+
...guidance,
|
|
80
|
+
market: guidance.market
|
|
81
|
+
? {
|
|
82
|
+
...guidance.market,
|
|
83
|
+
median: on(guidance.market.median),
|
|
84
|
+
...(guidance.market.p25 === undefined ? {} : { p25: on(guidance.market.p25) }),
|
|
85
|
+
...(guidance.market.p75 === undefined ? {} : { p75: on(guidance.market.p75) }),
|
|
86
|
+
}
|
|
87
|
+
: undefined,
|
|
88
|
+
sold: guidance.sold
|
|
89
|
+
? {
|
|
90
|
+
...guidance.sold,
|
|
91
|
+
min: on(guidance.sold.min),
|
|
92
|
+
median: on(guidance.sold.median),
|
|
93
|
+
max: on(guidance.sold.max),
|
|
94
|
+
}
|
|
95
|
+
: undefined,
|
|
96
|
+
lastSold: guidance.lastSold
|
|
97
|
+
? { ...guidance.lastSold, amount: on(guidance.lastSold.amount) }
|
|
98
|
+
: undefined,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Four cases, and the fourth is the one worth having this function for.
|
|
103
|
+
*
|
|
104
|
+
* | seller basis | rate | figures | says |
|
|
105
|
+
* |---|---|---|---|
|
|
106
|
+
* | inclusive | > 0 | unchanged | "incl. GST" |
|
|
107
|
+
* | exclusive | > 0 | CONVERTED | "before GST", and that it was converted |
|
|
108
|
+
* | either | 0 | unchanged | nothing about tax — an exempt item has none |
|
|
109
|
+
* | exclusive | unresolved | unchanged | 🛑 that they are NOT comparable |
|
|
110
|
+
*
|
|
111
|
+
* 🛑 The exempt case must not say "incl. GST". The figure is a bare amount and
|
|
112
|
+
* claiming a tax basis on it is false — which is precisely the lie the seller
|
|
113
|
+
* form's old `?? 0` default told on every listing before C0c defect 6.
|
|
114
|
+
*/
|
|
115
|
+
export function describePriceGuidanceBasis(basis) {
|
|
116
|
+
const includes = basis.sellerIncludesGst ?? true;
|
|
117
|
+
const rate = basis.gstRate;
|
|
118
|
+
if (rate === 0) {
|
|
119
|
+
return { suffix: "", note: null, converted: false };
|
|
120
|
+
}
|
|
121
|
+
if (includes) {
|
|
122
|
+
return { suffix: "incl. GST", note: null, converted: false };
|
|
123
|
+
}
|
|
124
|
+
if (typeof rate !== "number" || rate <= 0) {
|
|
125
|
+
return {
|
|
126
|
+
suffix: "incl. GST",
|
|
127
|
+
note: "Your store prices exclude GST, so these figures are not directly comparable to what you type — the tax rate for this category could not be resolved.",
|
|
128
|
+
converted: false,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
suffix: "before GST",
|
|
133
|
+
note: `Market figures are tax-inclusive; shown here with ${rate}% GST removed to match how your store prices.`,
|
|
134
|
+
converted: true,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
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 declare const GSTIN_PATTERN: RegExp;
|
|
49
|
+
/**
|
|
50
|
+
* The published GSTIN check character for the first 14 characters.
|
|
51
|
+
*
|
|
52
|
+
* Base-36 positional weights alternate 1, 2 from the left. For each character:
|
|
53
|
+
* multiply its value by its weight, then add the quotient and the remainder of
|
|
54
|
+
* that product divided by 36. The check value is `(36 - sum % 36) % 36`.
|
|
55
|
+
*
|
|
56
|
+
* Returns `null` when the prefix is not 14 characters of the alphabet — a
|
|
57
|
+
* caller must not treat that as "checksum failed", because it is a different
|
|
58
|
+
* fault with a different message.
|
|
59
|
+
*/
|
|
60
|
+
export declare function gstinCheckCharacter(first14: string): string | null;
|
|
61
|
+
/** Shape AND checksum. Whitespace and case are the caller's to normalise. */
|
|
62
|
+
export declare function isValidGstin(raw: string | undefined | null): boolean;
|
|
63
|
+
export interface GstEnablementInput {
|
|
64
|
+
enabled?: boolean;
|
|
65
|
+
gstin?: string;
|
|
66
|
+
legalName?: string;
|
|
67
|
+
address?: string;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* One problem per field, keyed by the field that owns it, so a caller can pipe
|
|
71
|
+
* each straight into `setFieldError(key, message)` per Rule #9 — rather than a
|
|
72
|
+
* single banner the admin has to map back onto four inputs themselves.
|
|
73
|
+
*/
|
|
74
|
+
export type GstEnablementProblems = Partial<Record<"gstin" | "legalName" | "address", string>>;
|
|
75
|
+
/**
|
|
76
|
+
* What must be true before product GST may be charged.
|
|
77
|
+
*
|
|
78
|
+
* 🛑 Only checked when `enabled` is being set to `true`. Turning GST **off**
|
|
79
|
+
* must never be blocked by the state of the fields it is turning off — that
|
|
80
|
+
* would trap an admin who enabled it with bad data in exactly the state they
|
|
81
|
+
* are trying to leave.
|
|
82
|
+
*
|
|
83
|
+
* `legalName` and `address` are required alongside the number because a tax
|
|
84
|
+
* invoice carries all three, and the invoice renderer snapshots all three onto
|
|
85
|
+
* the order. Enabling with two of them produces an invoice with a blank
|
|
86
|
+
* supplier block, which is not a lesser version of compliance.
|
|
87
|
+
*/
|
|
88
|
+
export declare function validateGstEnablement(input: GstEnablementInput): GstEnablementProblems;
|
|
89
|
+
/** `true` when GST may be enabled with this input. */
|
|
90
|
+
export declare function canEnableGst(input: GstEnablementInput): boolean;
|