@mohasinac/appkit 4.42.18 → 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/client/features/lottery/LotteryDetailView.js +22 -1
- 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/products/list-public.d.ts +24 -0
- package/dist/_internal/server/features/products/list-public.js +10 -2
- 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 +59 -0
- package/dist/_internal/server/jobs/core/onProductWrite.js +31 -0
- package/dist/_internal/server/jobs/core/onReviewWrite.js +8 -0
- package/dist/_internal/server/jobs/core/onStoreWrite.d.ts +1 -1
- package/dist/_internal/server/jobs/core/onStoreWrite.js +8 -2
- 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/revalidateNotify.d.ts +28 -0
- package/dist/_internal/server/jobs/core/revalidateNotify.js +125 -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/cache/revalidate-targets.d.ts +49 -0
- package/dist/_internal/shared/cache/revalidate-targets.js +157 -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/auctions/components/AuctionBottomActions.js +10 -1
- package/dist/features/auctions/components/AuctionDetailPageView.js +2 -1
- package/dist/features/auctions/components/AuctionLiveStatusBadge.d.ts +20 -0
- package/dist/features/auctions/components/AuctionLiveStatusBadge.js +21 -0
- package/dist/features/auctions/components/PlaceBidFormClient.d.ts +1 -1
- package/dist/features/auctions/components/PlaceBidFormClient.js +20 -2
- package/dist/features/auctions/hooks/useIsAuctionEnded.d.ts +41 -0
- package/dist/features/auctions/hooks/useIsAuctionEnded.js +76 -0
- package/dist/features/auth/schemas/firestore.d.ts +0 -1
- package/dist/features/blog/api/route.js +16 -1
- package/dist/features/blog/schemas/firestore.d.ts +2 -2
- package/dist/features/catalogue/components/CatalogueItemEditorView.js +14 -3
- package/dist/features/categories/actions/category-actions.d.ts +2 -2
- package/dist/features/categories/actions/category-actions.js +22 -10
- 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/CategoriesIndexListing.d.ts +1 -3
- package/dist/features/categories/components/CategoriesIndexListing.js +1 -1
- package/dist/features/categories/components/CategoriesIndexPageView.d.ts +27 -1
- package/dist/features/categories/components/CategoriesIndexPageView.js +24 -33
- 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 +73 -12
- package/dist/features/categories/repository/categories.repository.js +203 -42
- 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/faq/utils/getHomepageFaqsCached.d.ts +20 -0
- package/dist/features/faq/utils/getHomepageFaqsCached.js +22 -0
- package/dist/features/homepage/components/MarketplaceHomepageView.js +13 -3
- package/dist/features/homepage/lib/live-stats.js +18 -8
- package/dist/features/media/MediaImage.js +19 -2
- package/dist/features/orders/schemas/firestore.d.ts +70 -0
- package/dist/features/orders/types/index.d.ts +55 -0
- package/dist/features/products/actions/product-actions.d.ts +0 -9
- package/dist/features/products/actions/product-actions.js +25 -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/ArtStickersListView.js +7 -1
- package/dist/features/products/components/PrizeDrawsSection.js +5 -0
- 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/components/ProductsIndexPageView.js +1 -1
- 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/reviews/repository/reviews.repository.d.ts +22 -0
- package/dist/features/reviews/repository/reviews.repository.js +35 -0
- 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 +10 -29
- 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/media.d.ts +1 -1
- package/dist/seed/_helpers/media.js +44 -8
- 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/orders-seed-data.js +5 -3
- 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 +4 -1
- package/dist/server.js +11 -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/media-url.d.ts +26 -0
- package/dist/utils/media-url.js +144 -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
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { normalizeError } from "../../../errors/normalize";
|
|
2
|
+
import { DatabaseError, ConflictError, ValidationError } from "../../../errors";
|
|
3
|
+
import { BaseRepository, } from "../../../providers/db-firebase";
|
|
4
|
+
import { TAX_CODES_COLLECTION, TAX_CODE_FIELDS, TAX_CODE_SIEVE_FIELDS, createTaxCodeId, isValidHsnCode, } from "../schemas/firestore";
|
|
5
|
+
function failureMessage(prefix, error) {
|
|
6
|
+
return `${prefix}: ${error instanceof Error ? error.message : "Unknown error"}`;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* The `taxCodes` collection.
|
|
10
|
+
*
|
|
11
|
+
* Small, read-mostly and read on a hot path — `deriveTaxonomy` resolves a
|
|
12
|
+
* category's `taxCodeId` on every product create and on any update that names
|
|
13
|
+
* a taxonomy field. So the two reads below are single-document and
|
|
14
|
+
* single-field-equality shaped on purpose; neither needs a composite index.
|
|
15
|
+
*/
|
|
16
|
+
export class TaxCodesRepository extends BaseRepository {
|
|
17
|
+
constructor() {
|
|
18
|
+
super(TAX_CODES_COLLECTION);
|
|
19
|
+
}
|
|
20
|
+
async list(model) {
|
|
21
|
+
return this.sieveQuery(model, TAX_CODE_SIEVE_FIELDS);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Every active code, chapter-then-rate ordered, for the picker.
|
|
25
|
+
*
|
|
26
|
+
* Unpaginated deliberately — this is ~10 rows and will be a few dozen at
|
|
27
|
+
* most. If it ever needs paging, the picker needs `searchTxt` first, and
|
|
28
|
+
* that is a different change.
|
|
29
|
+
*/
|
|
30
|
+
async listActive() {
|
|
31
|
+
try {
|
|
32
|
+
const snap = await this.getCollection()
|
|
33
|
+
.where(TAX_CODE_FIELDS.IS_ACTIVE, "==", true)
|
|
34
|
+
.get();
|
|
35
|
+
return snap.docs
|
|
36
|
+
.map((d) => this.mapDoc(d))
|
|
37
|
+
.sort((a, b) => (a.chapter ?? "").localeCompare(b.chapter ?? "") ||
|
|
38
|
+
a.gstRate - b.gstRate ||
|
|
39
|
+
a.label.localeCompare(b.label));
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
void normalizeError(error);
|
|
43
|
+
throw new DatabaseError(failureMessage("Failed to list tax codes", error));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Every code including inactive ones, for the admin list.
|
|
48
|
+
*
|
|
49
|
+
* 🛑 EXPLICITLY bounded, even though the collection is five rows. Two
|
|
50
|
+
* reasons: `BaseRepository.findAll()` relies on a cap several layers away
|
|
51
|
+
* (`base.ts` clamps perPage at 500), and a route that reads "all of a
|
|
52
|
+
* collection" with no visible bound is the shape that has to be re-audited
|
|
53
|
+
* every time somebody wonders whether it is safe. 200 is far above any
|
|
54
|
+
* plausible number of HSN codes and far below anything that costs.
|
|
55
|
+
*/
|
|
56
|
+
async listAllBounded() {
|
|
57
|
+
try {
|
|
58
|
+
const snap = await this.getCollection().limit(200).get();
|
|
59
|
+
return snap.docs
|
|
60
|
+
.map((d) => this.mapDoc(d))
|
|
61
|
+
.sort((a, b) => a.label.localeCompare(b.label));
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
void normalizeError(error);
|
|
65
|
+
throw new DatabaseError(failureMessage("Failed to list tax codes", error));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Resolve a code for derivation.
|
|
70
|
+
*
|
|
71
|
+
* 🛑 Returns `null` for an INACTIVE code as well as a missing one, and the
|
|
72
|
+
* caller must treat both the same way: leave the product's `gstRate` and
|
|
73
|
+
* `hsnCode` alone rather than writing a zero. Deactivating a code must not
|
|
74
|
+
* silently re-rate the next listing to 0% — that is indistinguishable from a
|
|
75
|
+
* deliberate exemption, which is the whole reason `gstRate` distinguishes
|
|
76
|
+
* `undefined` from `0`.
|
|
77
|
+
*/
|
|
78
|
+
async findResolvable(id) {
|
|
79
|
+
try {
|
|
80
|
+
const snap = await this.getCollection().doc(id).get();
|
|
81
|
+
if (!snap.exists)
|
|
82
|
+
return null;
|
|
83
|
+
const doc = this.mapDoc(snap);
|
|
84
|
+
return doc.isActive ? doc : null;
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
void normalizeError(error);
|
|
88
|
+
throw new DatabaseError(failureMessage("Failed to read tax code", error));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Create with a DERIVED id, so the same HSN cannot exist twice.
|
|
93
|
+
*
|
|
94
|
+
* Two rows for one HSN is the failure worth preventing structurally: an
|
|
95
|
+
* admin edits one, categories point at the other, and the rate that reaches
|
|
96
|
+
* an invoice is whichever the data happens to reference. A 409 here is the
|
|
97
|
+
* right answer — the request is well-formed and the world already has it.
|
|
98
|
+
*/
|
|
99
|
+
async create(input) {
|
|
100
|
+
const hsn = input.hsnCode?.trim() ?? "";
|
|
101
|
+
if (hsn !== "" && !isValidHsnCode(hsn)) {
|
|
102
|
+
throw new ValidationError("An HSN code must be exactly 4, 6 or 8 digits.", { hsnCode: "Must be 4, 6 or 8 digits." });
|
|
103
|
+
}
|
|
104
|
+
if (hsn === "" && input.gstRate !== 0) {
|
|
105
|
+
throw new ValidationError("An HSN code is required for any rate above 0%.", { hsnCode: "Required unless the rate is 0%." });
|
|
106
|
+
}
|
|
107
|
+
const id = createTaxCodeId(hsn);
|
|
108
|
+
const existing = await this.getCollection().doc(id).get();
|
|
109
|
+
if (existing.exists) {
|
|
110
|
+
throw new ConflictError(`A tax code for HSN ${hsn || "(exempt)"} already exists. Edit it instead of creating a second one.`);
|
|
111
|
+
}
|
|
112
|
+
return this.createWithId(id, { ...input, hsnCode: hsn });
|
|
113
|
+
}
|
|
114
|
+
async update(id, data) {
|
|
115
|
+
/*
|
|
116
|
+
* 🛑 `hsnCode` is editable but the id is NOT re-derived from it. Changing
|
|
117
|
+
* the id would orphan every `CategoryProductDefaults.taxCodeId` pointing
|
|
118
|
+
* here — the mirror-drift shape of Root Cause #42, with an invoice at the
|
|
119
|
+
* end of it. To correct a wrong HSN: deactivate this row, create the right
|
|
120
|
+
* one, and repoint the categories.
|
|
121
|
+
*/
|
|
122
|
+
if (data.hsnCode !== undefined) {
|
|
123
|
+
const hsn = data.hsnCode.trim();
|
|
124
|
+
if (hsn !== "" && !isValidHsnCode(hsn)) {
|
|
125
|
+
throw new ValidationError("An HSN code must be exactly 4, 6 or 8 digits.", { hsnCode: "Must be 4, 6 or 8 digits." });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return super.update(id, data);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
export const taxCodesRepository = new TaxCodesRepository();
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tax codes — Firestore schema
|
|
3
|
+
*
|
|
4
|
+
* An HSN code and its GST rate, as an ENTITY rather than two fields repeated
|
|
5
|
+
* across every category. Changing a rate becomes one edit instead of ~330.
|
|
6
|
+
*
|
|
7
|
+
* ## 🛑 The reference is for AUTHORING. The scalars stay for CORRECTNESS.
|
|
8
|
+
*
|
|
9
|
+
* `CategoryProductDefaults.taxCodeId` points here, and `deriveTaxonomy`
|
|
10
|
+
* resolves it to `ProductDocument.gstRate` / `.hsnCode` **on the product**;
|
|
11
|
+
* the order item then snapshots from the product, exactly as it already does.
|
|
12
|
+
*
|
|
13
|
+
* That layering is not redundancy. Without it, editing a tax code would
|
|
14
|
+
* retroactively change the GST on orders already invoiced — a compliance
|
|
15
|
+
* problem, not a modelling preference. It is the same reason
|
|
16
|
+
* `OrderDocumentItem` already snapshots `hsnCode`/`gstRate`.
|
|
17
|
+
*
|
|
18
|
+
* a category edit -> changes future listings
|
|
19
|
+
* a tax-code edit -> changes future derivations
|
|
20
|
+
* nothing -> rewrites a past sale
|
|
21
|
+
*
|
|
22
|
+
* ## Why this exists at all
|
|
23
|
+
*
|
|
24
|
+
* GST has never been charged on goods here. Recorded in our own source
|
|
25
|
+
* (`_internal/server/features/checkout/actions.ts`): *"0 of 40 live orders had
|
|
26
|
+
* a `gstAmount`, and the order page's Tax row had never rendered."* And
|
|
27
|
+
* `gstRate` is set on zero of the current products.
|
|
28
|
+
*
|
|
29
|
+
* id === slug, prefix "tax-".
|
|
30
|
+
*/
|
|
31
|
+
import type { BaseDocument } from "../../../_internal/shared/types/base-document";
|
|
32
|
+
export declare const TAX_CODES_COLLECTION: "taxCodes";
|
|
33
|
+
export declare const TAX_CODE_PREFIX: "tax-";
|
|
34
|
+
/**
|
|
35
|
+
* The GST slabs. A closed union because a rate outside it is a data error, not
|
|
36
|
+
* a new option — and because `ProductDocument.gstRate` already declares this
|
|
37
|
+
* exact set, so a wider type here would not survive the derivation.
|
|
38
|
+
*/
|
|
39
|
+
export type GstRate = 0 | 5 | 12 | 18 | 28;
|
|
40
|
+
export declare const GST_RATE_VALUES: readonly [0, 5, 12, 18, 28];
|
|
41
|
+
/**
|
|
42
|
+
* HSN chapters this catalogue touches.
|
|
43
|
+
*
|
|
44
|
+
* 🛑 Only **9503** is verified. 4911 (printed cards/stickers/posters) and 9504
|
|
45
|
+
* (playing cards, board games) are the correct chapters for that merchandise
|
|
46
|
+
* and their CURRENT RATES ARE NOT CONFIRMED — the toys rates themselves moved
|
|
47
|
+
* in the 2025 revision. Verify before seeding a rate against either, because a
|
|
48
|
+
* wrong HSN on a real invoice is worse than an absent one.
|
|
49
|
+
*/
|
|
50
|
+
export type HsnChapter = "9503" | "4911" | "9504" | "exempt";
|
|
51
|
+
export interface TaxCodeDocument extends BaseDocument {
|
|
52
|
+
/** 4, 6 or 8 digits. Empty for the exemption row, which has no HSN. */
|
|
53
|
+
hsnCode: string;
|
|
54
|
+
/** Picker label, e.g. "Toys, non-electronic (HSN 9503 0020) — 5%". */
|
|
55
|
+
label: string;
|
|
56
|
+
description?: string;
|
|
57
|
+
gstRate: GstRate;
|
|
58
|
+
/** Groups the picker. ~10 rows today, more once 4911/9504 are verified. */
|
|
59
|
+
chapter?: HsnChapter;
|
|
60
|
+
/**
|
|
61
|
+
* When the CURRENT rate took effect. Informational — it does NOT rewrite
|
|
62
|
+
* history, because history lives in the per-product and per-order-item
|
|
63
|
+
* snapshots. Recorded so an admin can tell a rate that has always been 5%
|
|
64
|
+
* from one that changed.
|
|
65
|
+
*/
|
|
66
|
+
effectiveFrom?: Date;
|
|
67
|
+
isActive: boolean;
|
|
68
|
+
/** Why this rate, and what it covers — the thing a future admin will want. */
|
|
69
|
+
notes?: string;
|
|
70
|
+
}
|
|
71
|
+
export type TaxCodeCreateInput = Omit<TaxCodeDocument, "id" | "createdAt" | "updatedAt">;
|
|
72
|
+
export declare const TAX_CODE_UPDATABLE_FIELDS: readonly ["hsnCode", "label", "description", "gstRate", "chapter", "effectiveFrom", "isActive", "notes"];
|
|
73
|
+
export type TaxCodeUpdateInput = Partial<Pick<TaxCodeDocument, (typeof TAX_CODE_UPDATABLE_FIELDS)[number]>>;
|
|
74
|
+
export declare const TAX_CODE_FIELDS: {
|
|
75
|
+
readonly ID: "id";
|
|
76
|
+
readonly HSN_CODE: "hsnCode";
|
|
77
|
+
readonly LABEL: "label";
|
|
78
|
+
readonly GST_RATE: "gstRate";
|
|
79
|
+
readonly CHAPTER: "chapter";
|
|
80
|
+
readonly IS_ACTIVE: "isActive";
|
|
81
|
+
readonly EFFECTIVE_FROM: "effectiveFrom";
|
|
82
|
+
readonly CREATED_AT: "createdAt";
|
|
83
|
+
readonly UPDATED_AT: "updatedAt";
|
|
84
|
+
};
|
|
85
|
+
export declare const TAX_CODE_SIEVE_FIELDS: {
|
|
86
|
+
readonly label: {
|
|
87
|
+
readonly canFilter: true;
|
|
88
|
+
readonly canSort: true;
|
|
89
|
+
};
|
|
90
|
+
readonly hsnCode: {
|
|
91
|
+
readonly canFilter: true;
|
|
92
|
+
readonly canSort: true;
|
|
93
|
+
};
|
|
94
|
+
readonly gstRate: {
|
|
95
|
+
readonly canFilter: true;
|
|
96
|
+
readonly canSort: true;
|
|
97
|
+
};
|
|
98
|
+
readonly chapter: {
|
|
99
|
+
readonly canFilter: true;
|
|
100
|
+
readonly canSort: false;
|
|
101
|
+
};
|
|
102
|
+
readonly isActive: {
|
|
103
|
+
readonly canFilter: true;
|
|
104
|
+
readonly canSort: false;
|
|
105
|
+
};
|
|
106
|
+
readonly createdAt: {
|
|
107
|
+
readonly canFilter: false;
|
|
108
|
+
readonly canSort: true;
|
|
109
|
+
};
|
|
110
|
+
};
|
|
111
|
+
/**
|
|
112
|
+
* Derive the document id from the HSN code.
|
|
113
|
+
*
|
|
114
|
+
* Deterministic, so a seed run is idempotent (Root Cause #25) and so the same
|
|
115
|
+
* HSN cannot be entered twice under two ids — which would give two rows one
|
|
116
|
+
* admin edits and the other serves.
|
|
117
|
+
*/
|
|
118
|
+
export declare function createTaxCodeId(hsnCode: string): string;
|
|
119
|
+
/**
|
|
120
|
+
* India's HSN codes are 4, 6 or 8 digits. Checked rather than trusted because
|
|
121
|
+
* the inline-create path in the seller form reaches the same route as the
|
|
122
|
+
* admin editor, and a malformed code propagates onto every product derived
|
|
123
|
+
* from the category that points at it.
|
|
124
|
+
*/
|
|
125
|
+
export declare function isValidHsnCode(hsnCode: string): boolean;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tax codes — Firestore schema
|
|
3
|
+
*
|
|
4
|
+
* An HSN code and its GST rate, as an ENTITY rather than two fields repeated
|
|
5
|
+
* across every category. Changing a rate becomes one edit instead of ~330.
|
|
6
|
+
*
|
|
7
|
+
* ## 🛑 The reference is for AUTHORING. The scalars stay for CORRECTNESS.
|
|
8
|
+
*
|
|
9
|
+
* `CategoryProductDefaults.taxCodeId` points here, and `deriveTaxonomy`
|
|
10
|
+
* resolves it to `ProductDocument.gstRate` / `.hsnCode` **on the product**;
|
|
11
|
+
* the order item then snapshots from the product, exactly as it already does.
|
|
12
|
+
*
|
|
13
|
+
* That layering is not redundancy. Without it, editing a tax code would
|
|
14
|
+
* retroactively change the GST on orders already invoiced — a compliance
|
|
15
|
+
* problem, not a modelling preference. It is the same reason
|
|
16
|
+
* `OrderDocumentItem` already snapshots `hsnCode`/`gstRate`.
|
|
17
|
+
*
|
|
18
|
+
* a category edit -> changes future listings
|
|
19
|
+
* a tax-code edit -> changes future derivations
|
|
20
|
+
* nothing -> rewrites a past sale
|
|
21
|
+
*
|
|
22
|
+
* ## Why this exists at all
|
|
23
|
+
*
|
|
24
|
+
* GST has never been charged on goods here. Recorded in our own source
|
|
25
|
+
* (`_internal/server/features/checkout/actions.ts`): *"0 of 40 live orders had
|
|
26
|
+
* a `gstAmount`, and the order page's Tax row had never rendered."* And
|
|
27
|
+
* `gstRate` is set on zero of the current products.
|
|
28
|
+
*
|
|
29
|
+
* id === slug, prefix "tax-".
|
|
30
|
+
*/
|
|
31
|
+
export const TAX_CODES_COLLECTION = "taxCodes";
|
|
32
|
+
export const TAX_CODE_PREFIX = "tax-";
|
|
33
|
+
export const GST_RATE_VALUES = [0, 5, 12, 18, 28];
|
|
34
|
+
export const TAX_CODE_UPDATABLE_FIELDS = [
|
|
35
|
+
"hsnCode",
|
|
36
|
+
"label",
|
|
37
|
+
"description",
|
|
38
|
+
"gstRate",
|
|
39
|
+
"chapter",
|
|
40
|
+
"effectiveFrom",
|
|
41
|
+
"isActive",
|
|
42
|
+
"notes",
|
|
43
|
+
];
|
|
44
|
+
export const TAX_CODE_FIELDS = {
|
|
45
|
+
ID: "id",
|
|
46
|
+
HSN_CODE: "hsnCode",
|
|
47
|
+
LABEL: "label",
|
|
48
|
+
GST_RATE: "gstRate",
|
|
49
|
+
CHAPTER: "chapter",
|
|
50
|
+
IS_ACTIVE: "isActive",
|
|
51
|
+
EFFECTIVE_FROM: "effectiveFrom",
|
|
52
|
+
CREATED_AT: "createdAt",
|
|
53
|
+
UPDATED_AT: "updatedAt",
|
|
54
|
+
};
|
|
55
|
+
export const TAX_CODE_SIEVE_FIELDS = {
|
|
56
|
+
label: { canFilter: true, canSort: true },
|
|
57
|
+
hsnCode: { canFilter: true, canSort: true },
|
|
58
|
+
gstRate: { canFilter: true, canSort: true },
|
|
59
|
+
chapter: { canFilter: true, canSort: false },
|
|
60
|
+
isActive: { canFilter: true, canSort: false },
|
|
61
|
+
createdAt: { canFilter: false, canSort: true },
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* Derive the document id from the HSN code.
|
|
65
|
+
*
|
|
66
|
+
* Deterministic, so a seed run is idempotent (Root Cause #25) and so the same
|
|
67
|
+
* HSN cannot be entered twice under two ids — which would give two rows one
|
|
68
|
+
* admin edits and the other serves.
|
|
69
|
+
*/
|
|
70
|
+
export function createTaxCodeId(hsnCode) {
|
|
71
|
+
const digits = hsnCode.replace(/\D/g, "");
|
|
72
|
+
return digits ? `${TAX_CODE_PREFIX}hsn-${digits}` : `${TAX_CODE_PREFIX}exempt-0`;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* India's HSN codes are 4, 6 or 8 digits. Checked rather than trusted because
|
|
76
|
+
* the inline-create path in the seller form reaches the same route as the
|
|
77
|
+
* admin editor, and a malformed code propagates onto every product derived
|
|
78
|
+
* from the category that points at it.
|
|
79
|
+
*/
|
|
80
|
+
export function isValidHsnCode(hsnCode) {
|
|
81
|
+
return /^(\d{4}|\d{6}|\d{8})$/.test(hsnCode.trim());
|
|
82
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/** The GST slabs, as the rate select offers them. */
|
|
3
|
+
export declare const GST_RATE_OPTIONS: {
|
|
4
|
+
value: string;
|
|
5
|
+
label: string;
|
|
6
|
+
}[];
|
|
7
|
+
/**
|
|
8
|
+
* Chapter options.
|
|
9
|
+
*
|
|
10
|
+
* 🛑 The two non-9503 chapters are offered because the merchandise exists
|
|
11
|
+
* (printed cards and stickers; playing cards), and their labels say the rate is
|
|
12
|
+
* unverified rather than implying one. Picking a chapter does not set a rate.
|
|
13
|
+
*/
|
|
14
|
+
export declare const HSN_CHAPTER_OPTIONS: {
|
|
15
|
+
value: string;
|
|
16
|
+
label: string;
|
|
17
|
+
}[];
|
|
18
|
+
/**
|
|
19
|
+
* 🛑 `annotate()` must be the OUTERMOST call on each field — it keys a WeakMap
|
|
20
|
+
* by schema instance and every zod wrapper returns a new one.
|
|
21
|
+
*/
|
|
22
|
+
export declare const taxCodeFormSchema: z.ZodEffects<z.ZodObject<{
|
|
23
|
+
label: z.ZodString;
|
|
24
|
+
hsnCode: z.ZodOptional<z.ZodString>;
|
|
25
|
+
gstRate: z.ZodEffects<z.ZodNumber, number, number>;
|
|
26
|
+
chapter: z.ZodOptional<z.ZodEnum<["9503", "4911", "9504", "exempt"]>>;
|
|
27
|
+
description: z.ZodOptional<z.ZodString>;
|
|
28
|
+
notes: z.ZodOptional<z.ZodString>;
|
|
29
|
+
isActive: z.ZodBoolean;
|
|
30
|
+
}, "strip", z.ZodTypeAny, {
|
|
31
|
+
label: string;
|
|
32
|
+
isActive: boolean;
|
|
33
|
+
gstRate: number;
|
|
34
|
+
description?: string | undefined;
|
|
35
|
+
notes?: string | undefined;
|
|
36
|
+
hsnCode?: string | undefined;
|
|
37
|
+
chapter?: "9503" | "4911" | "9504" | "exempt" | undefined;
|
|
38
|
+
}, {
|
|
39
|
+
label: string;
|
|
40
|
+
isActive: boolean;
|
|
41
|
+
gstRate: number;
|
|
42
|
+
description?: string | undefined;
|
|
43
|
+
notes?: string | undefined;
|
|
44
|
+
hsnCode?: string | undefined;
|
|
45
|
+
chapter?: "9503" | "4911" | "9504" | "exempt" | undefined;
|
|
46
|
+
}>, {
|
|
47
|
+
label: string;
|
|
48
|
+
isActive: boolean;
|
|
49
|
+
gstRate: number;
|
|
50
|
+
description?: string | undefined;
|
|
51
|
+
notes?: string | undefined;
|
|
52
|
+
hsnCode?: string | undefined;
|
|
53
|
+
chapter?: "9503" | "4911" | "9504" | "exempt" | undefined;
|
|
54
|
+
}, {
|
|
55
|
+
label: string;
|
|
56
|
+
isActive: boolean;
|
|
57
|
+
gstRate: number;
|
|
58
|
+
description?: string | undefined;
|
|
59
|
+
notes?: string | undefined;
|
|
60
|
+
hsnCode?: string | undefined;
|
|
61
|
+
chapter?: "9503" | "4911" | "9504" | "exempt" | undefined;
|
|
62
|
+
}>;
|
|
63
|
+
export type TaxCodeFormValues = z.infer<typeof taxCodeFormSchema>;
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* WHAT: The tax-code editor's schema — the field set, annotated for
|
|
3
|
+
* `buildSectionsFromSchema`, with the HSN-format rule as a refinement
|
|
4
|
+
* rather than a hint under the input.
|
|
5
|
+
*
|
|
6
|
+
* ## 🛑 Why the HSN rule is a refinement and not placeholder text
|
|
7
|
+
*
|
|
8
|
+
* A malformed HSN does not fail loudly. It propagates: a category points at
|
|
9
|
+
* the tax code, `deriveTaxonomy` writes `hsnCode` onto every product derived
|
|
10
|
+
* from that category, and the order item snapshots it onto a GST invoice.
|
|
11
|
+
* Nothing in that chain validates the string again, and the invoice is the
|
|
12
|
+
* first place a human might notice — by which point the sale is recorded.
|
|
13
|
+
*
|
|
14
|
+
* So the format is checked at the one place it enters the system. The same
|
|
15
|
+
* route serves the inline-create path in the seller form, which is the busiest
|
|
16
|
+
* surface and the one most likely to be used in a hurry.
|
|
17
|
+
*
|
|
18
|
+
* ## The exemption row is the reason `hsnCode` is not simply required
|
|
19
|
+
*
|
|
20
|
+
* A 0% row (live plants, live animals) has no HSN. Requiring one would force
|
|
21
|
+
* an author to invent a code to express an exemption, and an invented HSN on
|
|
22
|
+
* an invoice is exactly the failure above. So the refinement is conditional:
|
|
23
|
+
* blank is legal only at rate 0.
|
|
24
|
+
*
|
|
25
|
+
* 🛑 The two option arrays are exported for the VIEW to pass as
|
|
26
|
+
* `options: { gstRate: GST_RATE_OPTIONS, chapter: HSN_CHAPTER_OPTIONS }` —
|
|
27
|
+
* `FieldUiMeta` has no `options` key, and `annotate` would silently accept
|
|
28
|
+
* neither. That split follows `AdminBundleEditorView`, which supplies
|
|
29
|
+
* `BUNDLE_RULE_TYPE_OPTIONS` the same way: a select's VALUES are schema, its
|
|
30
|
+
* option list is often runtime data (brands, categories) and so belongs to
|
|
31
|
+
* whoever can fetch it.
|
|
32
|
+
*
|
|
33
|
+
* EXPORTS:
|
|
34
|
+
* taxCodeFormSchema, type TaxCodeFormValues, GST_RATE_OPTIONS,
|
|
35
|
+
* HSN_CHAPTER_OPTIONS
|
|
36
|
+
*
|
|
37
|
+
* @tag domain:tax-codes
|
|
38
|
+
* @tag layer:schema
|
|
39
|
+
* @tag pattern:none
|
|
40
|
+
* @tag access:isomorphic
|
|
41
|
+
* @tag consumers:AdminTaxCodeEditorView
|
|
42
|
+
* @tag sideEffects:none
|
|
43
|
+
*/
|
|
44
|
+
import { z } from "zod";
|
|
45
|
+
import { annotate } from "../../shell/field-ui-meta";
|
|
46
|
+
import { GST_RATE_VALUES, isValidHsnCode } from "./firestore";
|
|
47
|
+
/** The GST slabs, as the rate select offers them. */
|
|
48
|
+
export const GST_RATE_OPTIONS = GST_RATE_VALUES.map((r) => ({
|
|
49
|
+
value: String(r),
|
|
50
|
+
label: r === 0 ? "0% — exempt" : `${r}%`,
|
|
51
|
+
}));
|
|
52
|
+
/**
|
|
53
|
+
* Chapter options.
|
|
54
|
+
*
|
|
55
|
+
* 🛑 The two non-9503 chapters are offered because the merchandise exists
|
|
56
|
+
* (printed cards and stickers; playing cards), and their labels say the rate is
|
|
57
|
+
* unverified rather than implying one. Picking a chapter does not set a rate.
|
|
58
|
+
*/
|
|
59
|
+
export const HSN_CHAPTER_OPTIONS = [
|
|
60
|
+
{ value: "9503", label: "9503 — Toys (verified)" },
|
|
61
|
+
{ value: "4911", label: "4911 — Printed matter: cards, stickers, posters (rate unverified)" },
|
|
62
|
+
{ value: "9504", label: "9504 — Playing cards, board games (rate unverified)" },
|
|
63
|
+
{ value: "exempt", label: "Exempt — no HSN" },
|
|
64
|
+
];
|
|
65
|
+
/**
|
|
66
|
+
* 🛑 `annotate()` must be the OUTERMOST call on each field — it keys a WeakMap
|
|
67
|
+
* by schema instance and every zod wrapper returns a new one.
|
|
68
|
+
*/
|
|
69
|
+
export const taxCodeFormSchema = z
|
|
70
|
+
.object({
|
|
71
|
+
label: annotate(z
|
|
72
|
+
.string()
|
|
73
|
+
.min(1, "Give the code a label an admin will recognise in a picker.")
|
|
74
|
+
.max(150, "Keep the label under 150 characters."), {
|
|
75
|
+
section: "basics",
|
|
76
|
+
sectionLabel: "Tax code",
|
|
77
|
+
sectionRequired: true,
|
|
78
|
+
quick: true,
|
|
79
|
+
order: 1,
|
|
80
|
+
label: "Label",
|
|
81
|
+
help: 'What the picker shows, e.g. "Toys, non-electronic (HSN 9503 0020) — 5%".',
|
|
82
|
+
}),
|
|
83
|
+
/*
|
|
84
|
+
* A STRING, not a number: leading zeros are significant in an HSN code
|
|
85
|
+
* (`04` is not `4`) and a numeric input would eat them. Digits-only is
|
|
86
|
+
* enforced by the refinement below, against the real 4/6/8-digit rule.
|
|
87
|
+
*/
|
|
88
|
+
hsnCode: annotate(z.string().trim().max(8, "An HSN code is at most 8 digits.").optional(), {
|
|
89
|
+
section: "basics",
|
|
90
|
+
quick: true,
|
|
91
|
+
order: 2,
|
|
92
|
+
row: "pair",
|
|
93
|
+
label: "HSN code",
|
|
94
|
+
help: "4, 6 or 8 digits. Leave blank only for a 0% exemption.",
|
|
95
|
+
}),
|
|
96
|
+
gstRate: annotate(z.coerce
|
|
97
|
+
.number()
|
|
98
|
+
.refine((v) => GST_RATE_VALUES.includes(v), {
|
|
99
|
+
message: "Pick one of the GST slabs: 0, 5, 12, 18 or 28.",
|
|
100
|
+
}), {
|
|
101
|
+
section: "basics",
|
|
102
|
+
sectionRequired: true,
|
|
103
|
+
quick: true,
|
|
104
|
+
order: 3,
|
|
105
|
+
row: "pair",
|
|
106
|
+
kind: "select",
|
|
107
|
+
label: "GST rate",
|
|
108
|
+
}),
|
|
109
|
+
chapter: annotate(z.enum(["9503", "4911", "9504", "exempt"]).optional(), {
|
|
110
|
+
section: "basics",
|
|
111
|
+
order: 4,
|
|
112
|
+
kind: "select",
|
|
113
|
+
label: "Chapter",
|
|
114
|
+
help: "Groups the picker. Only chapter 9503's rates are verified.",
|
|
115
|
+
}),
|
|
116
|
+
description: annotate(z.string().max(500).optional(), { section: "detail", sectionLabel: "Detail", order: 1, kind: "textarea", label: "Description" }),
|
|
117
|
+
notes: annotate(z.string().max(1000).optional(), {
|
|
118
|
+
section: "detail",
|
|
119
|
+
order: 2,
|
|
120
|
+
kind: "textarea",
|
|
121
|
+
label: "Notes",
|
|
122
|
+
help: "Why this rate, and what it covers. The thing a future admin will want.",
|
|
123
|
+
}),
|
|
124
|
+
isActive: annotate(z.boolean(), {
|
|
125
|
+
section: "detail",
|
|
126
|
+
order: 3,
|
|
127
|
+
label: "Active",
|
|
128
|
+
help: "An inactive code is never resolved onto a product, and disappears from the picker.",
|
|
129
|
+
}),
|
|
130
|
+
})
|
|
131
|
+
.superRefine((values, ctx) => {
|
|
132
|
+
const hsn = values.hsnCode?.trim() ?? "";
|
|
133
|
+
// Blank is legal ONLY at 0%. See the header: forcing an HSN on an exemption
|
|
134
|
+
// is what gets a code invented, and an invented code reaches an invoice.
|
|
135
|
+
if (hsn === "") {
|
|
136
|
+
if (values.gstRate !== 0) {
|
|
137
|
+
ctx.addIssue({
|
|
138
|
+
code: z.ZodIssueCode.custom,
|
|
139
|
+
path: ["hsnCode"],
|
|
140
|
+
message: "An HSN code is required for any rate above 0%. Leave it blank only to record an exemption.",
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (!isValidHsnCode(hsn)) {
|
|
146
|
+
ctx.addIssue({
|
|
147
|
+
code: z.ZodIssueCode.custom,
|
|
148
|
+
path: ["hsnCode"],
|
|
149
|
+
message: "An HSN code must be exactly 4, 6 or 8 digits — no spaces or dots.",
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zod validators for the `/api/admin/tax-codes` route handlers.
|
|
3
|
+
*
|
|
4
|
+
* Separate from `tax-code-form.ts`, which is the EDITOR's schema: that one is
|
|
5
|
+
* annotated for `buildSectionsFromSchema`, takes strings from inputs, and
|
|
6
|
+
* coerces. These are the wire contracts, and they are `.strict()`.
|
|
7
|
+
*
|
|
8
|
+
* ## 🛑 `.strict()`, not `z.object()` and not `.passthrough()`
|
|
9
|
+
*
|
|
10
|
+
* `z.object()` STRIPS unknown keys, which is how a PATCH returns 200 having
|
|
11
|
+
* written nothing (Root Cause #40's shape, and the admin grouped-listings
|
|
12
|
+
* route's documented bug). `.passthrough()` writes whatever arrives, which is
|
|
13
|
+
* how a form overwrote booked lottery slots. `.strict()` 400s on a field the
|
|
14
|
+
* contract does not name — the caller is told, and nothing silently vanishes.
|
|
15
|
+
*
|
|
16
|
+
* ## The id is NOT accepted on update
|
|
17
|
+
*
|
|
18
|
+
* `createTaxCodeId` derives the id from the HSN code, so accepting an id (or
|
|
19
|
+
* re-deriving one on update) would let an edit orphan every
|
|
20
|
+
* `CategoryProductDefaults.taxCodeId` pointing at the old one — with a GST
|
|
21
|
+
* invoice at the end of that chain. To correct a wrong HSN: deactivate, create
|
|
22
|
+
* the right row, repoint the categories.
|
|
23
|
+
*/
|
|
24
|
+
import { z } from "zod";
|
|
25
|
+
/**
|
|
26
|
+
* 🛑 `z.custom<GstRate>`, not `z.number().refine(...)`.
|
|
27
|
+
*
|
|
28
|
+
* A `.refine()` validates at runtime and does NOT narrow the inferred type —
|
|
29
|
+
* the payload still types as `number`, so `repository.create(body)` fails to
|
|
30
|
+
* compile against `gstRate: GstRate`, and the obvious unblock is a cast. A
|
|
31
|
+
* cast at a request boundary is where this codebase's worst bugs live (an
|
|
32
|
+
* `ActionResult` envelope spread as a product, a raw `StoreDocument` published
|
|
33
|
+
* as HTML), so the type is narrowed here instead.
|
|
34
|
+
*
|
|
35
|
+
* And `z.custom` rather than a hand-written `z.union([z.literal(0), …])`
|
|
36
|
+
* because the slab list then has ONE definition — `GST_RATE_VALUES`, which
|
|
37
|
+
* `ProductDocument.gstRate` and the form's option list also derive from. A
|
|
38
|
+
* literal union here would be a second enumeration of the same union, which is
|
|
39
|
+
* Root Cause #61's shape.
|
|
40
|
+
*/
|
|
41
|
+
export declare const GST_RATE_ENUM: z.ZodType<0 | 5 | 12 | 18 | 28, z.ZodTypeDef, 0 | 5 | 12 | 18 | 28>;
|
|
42
|
+
export declare const HSN_CHAPTER_ENUM: z.ZodEnum<["9503", "4911", "9504", "exempt"]>;
|
|
43
|
+
/**
|
|
44
|
+
* 🛑 `hsnCode` is OPTIONAL here and the blank-only-at-0% rule lives in the
|
|
45
|
+
* repository, not this schema. Deliberate: the same rule has to hold for the
|
|
46
|
+
* seed path and any future importer, neither of which goes through a Zod
|
|
47
|
+
* request schema. A rule enforced only at the HTTP edge is a rule with a side
|
|
48
|
+
* door.
|
|
49
|
+
*/
|
|
50
|
+
export declare const taxCodeCreateSchema: z.ZodObject<{
|
|
51
|
+
hsnCode: z.ZodDefault<z.ZodString>;
|
|
52
|
+
label: z.ZodString;
|
|
53
|
+
description: z.ZodOptional<z.ZodString>;
|
|
54
|
+
gstRate: z.ZodType<0 | 5 | 12 | 18 | 28, z.ZodTypeDef, 0 | 5 | 12 | 18 | 28>;
|
|
55
|
+
chapter: z.ZodOptional<z.ZodEnum<["9503", "4911", "9504", "exempt"]>>;
|
|
56
|
+
effectiveFrom: z.ZodOptional<z.ZodDate>;
|
|
57
|
+
isActive: z.ZodDefault<z.ZodBoolean>;
|
|
58
|
+
notes: z.ZodOptional<z.ZodString>;
|
|
59
|
+
}, "strict", z.ZodTypeAny, {
|
|
60
|
+
label: string;
|
|
61
|
+
isActive: boolean;
|
|
62
|
+
gstRate: 0 | 5 | 12 | 18 | 28;
|
|
63
|
+
hsnCode: string;
|
|
64
|
+
description?: string | undefined;
|
|
65
|
+
notes?: string | undefined;
|
|
66
|
+
chapter?: "9503" | "4911" | "9504" | "exempt" | undefined;
|
|
67
|
+
effectiveFrom?: Date | undefined;
|
|
68
|
+
}, {
|
|
69
|
+
label: string;
|
|
70
|
+
gstRate: 0 | 5 | 12 | 18 | 28;
|
|
71
|
+
description?: string | undefined;
|
|
72
|
+
isActive?: boolean | undefined;
|
|
73
|
+
notes?: string | undefined;
|
|
74
|
+
hsnCode?: string | undefined;
|
|
75
|
+
chapter?: "9503" | "4911" | "9504" | "exempt" | undefined;
|
|
76
|
+
effectiveFrom?: Date | undefined;
|
|
77
|
+
}>;
|
|
78
|
+
export type TaxCodeCreatePayload = z.infer<typeof taxCodeCreateSchema>;
|
|
79
|
+
export declare const taxCodeUpdateSchema: z.ZodObject<{
|
|
80
|
+
hsnCode: z.ZodOptional<z.ZodString>;
|
|
81
|
+
label: z.ZodOptional<z.ZodString>;
|
|
82
|
+
description: z.ZodOptional<z.ZodString>;
|
|
83
|
+
gstRate: z.ZodOptional<z.ZodType<0 | 5 | 12 | 18 | 28, z.ZodTypeDef, 0 | 5 | 12 | 18 | 28>>;
|
|
84
|
+
chapter: z.ZodOptional<z.ZodEnum<["9503", "4911", "9504", "exempt"]>>;
|
|
85
|
+
effectiveFrom: z.ZodOptional<z.ZodDate>;
|
|
86
|
+
isActive: z.ZodOptional<z.ZodBoolean>;
|
|
87
|
+
notes: z.ZodOptional<z.ZodString>;
|
|
88
|
+
}, "strict", z.ZodTypeAny, {
|
|
89
|
+
label?: string | undefined;
|
|
90
|
+
description?: string | undefined;
|
|
91
|
+
isActive?: boolean | undefined;
|
|
92
|
+
notes?: string | undefined;
|
|
93
|
+
gstRate?: 0 | 5 | 12 | 18 | 28 | undefined;
|
|
94
|
+
hsnCode?: string | undefined;
|
|
95
|
+
chapter?: "9503" | "4911" | "9504" | "exempt" | undefined;
|
|
96
|
+
effectiveFrom?: Date | undefined;
|
|
97
|
+
}, {
|
|
98
|
+
label?: string | undefined;
|
|
99
|
+
description?: string | undefined;
|
|
100
|
+
isActive?: boolean | undefined;
|
|
101
|
+
notes?: string | undefined;
|
|
102
|
+
gstRate?: 0 | 5 | 12 | 18 | 28 | undefined;
|
|
103
|
+
hsnCode?: string | undefined;
|
|
104
|
+
chapter?: "9503" | "4911" | "9504" | "exempt" | undefined;
|
|
105
|
+
effectiveFrom?: Date | undefined;
|
|
106
|
+
}>;
|
|
107
|
+
export type TaxCodeUpdatePayload = z.infer<typeof taxCodeUpdateSchema>;
|