@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
|
@@ -528,6 +528,40 @@ export const ACTIONS = {
|
|
|
528
528
|
description: "Navigate to the full product listing for this store.",
|
|
529
529
|
kind: "link",
|
|
530
530
|
},
|
|
531
|
+
// ── Category description templates (C2c) ─────────────────────────────
|
|
532
|
+
"apply-template": {
|
|
533
|
+
iconKey: "edit",
|
|
534
|
+
id: "store.apply-template",
|
|
535
|
+
label: "Use this description",
|
|
536
|
+
ariaLabel: "Fill the description from this category template",
|
|
537
|
+
description: "Fill the description field from the category's description template, with the listing's own details interpolated.",
|
|
538
|
+
kind: "secondary",
|
|
539
|
+
permissions: ["seller", "admin"],
|
|
540
|
+
},
|
|
541
|
+
"replace-description": {
|
|
542
|
+
iconKey: "edit",
|
|
543
|
+
id: "store.replace-description",
|
|
544
|
+
label: "Replace description",
|
|
545
|
+
ariaLabel: "Replace the description you have written with the category template",
|
|
546
|
+
description: "Overwrite the description with the category's template. The text currently in the field is lost.",
|
|
547
|
+
/*
|
|
548
|
+
* 🛑 `danger` with a confirmation, because it destroys authored text.
|
|
549
|
+
*
|
|
550
|
+
* The picker only ever offers this when `isDescriptionDirty` — i.e. the
|
|
551
|
+
* seller has typed something that is not a body we filled in. Rule #7:
|
|
552
|
+
* an action that cannot be undone needs the confirmation ON the
|
|
553
|
+
* ActionDef, not a `window.confirm` at the call site, so the copy lives
|
|
554
|
+
* with the action and is translatable.
|
|
555
|
+
*/
|
|
556
|
+
kind: "danger",
|
|
557
|
+
permissions: ["seller", "admin"],
|
|
558
|
+
confirmation: {
|
|
559
|
+
title: "Replace what you have written?",
|
|
560
|
+
body: "The description you typed will be overwritten by the category template. This cannot be undone.",
|
|
561
|
+
confirmLabel: "Replace it",
|
|
562
|
+
confirmKind: "danger",
|
|
563
|
+
},
|
|
564
|
+
},
|
|
531
565
|
// ── Offer management ─────────────────────────────────────────────────
|
|
532
566
|
"accept-offer": {
|
|
533
567
|
iconKey: "approve",
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which PAGE paths a write to a collection makes stale.
|
|
3
|
+
*
|
|
4
|
+
* ## Why this exists
|
|
5
|
+
*
|
|
6
|
+
* Detail routes used to carry very short `revalidate` windows — 30s on
|
|
7
|
+
* `auctions/[id]`, 60s on `products/[slug]` — not because anyone wanted that
|
|
8
|
+
* freshness, but because **the window was the only freshness mechanism in the
|
|
9
|
+
* app**. Measured 2026-10-09: `revalidatePath` appeared three times in the whole
|
|
10
|
+
* repository, all `revalidatePath("/")`, and `revalidateTag` zero times.
|
|
11
|
+
*
|
|
12
|
+
* Short windows × many unique paths is how you manufacture ISR writes. 95
|
|
13
|
+
* products at 60s is up to 137K regenerations/day from one route, and the
|
|
14
|
+
* project was suspended for exceeding 200K ISR writes with 3.1M — the signature
|
|
15
|
+
* being writes outnumbering reads 3:1, i.e. entries regenerating before anyone
|
|
16
|
+
* read them twice.
|
|
17
|
+
*
|
|
18
|
+
* Invalidation keyed on the WRITE is both cheaper and more correct than any
|
|
19
|
+
* clock: a product that changed is stale immediately, and one that did not never
|
|
20
|
+
* regenerates at all.
|
|
21
|
+
*
|
|
22
|
+
* ## Contract
|
|
23
|
+
*
|
|
24
|
+
* Returns **locale-less** paths. The caller is responsible for prefixing each
|
|
25
|
+
* with the locales it serves — see the revalidate route, which does that from
|
|
26
|
+
* `routing.locales`, because prerendered entries live at `/en/...`.
|
|
27
|
+
*
|
|
28
|
+
* Paths are returned on a best-effort basis: a caller that cannot supply hints
|
|
29
|
+
* still gets the always-stale list, which is the part that matters for a
|
|
30
|
+
* listing. Over-revalidating is cheap (it marks an entry stale; it does not
|
|
31
|
+
* render). Under-revalidating is the failure that shows a buyer a sold-out
|
|
32
|
+
* listing as in stock, so when in doubt this returns MORE paths, not fewer.
|
|
33
|
+
*/
|
|
34
|
+
/** What the caller knows about the written document, when it knows anything. */
|
|
35
|
+
export interface RevalidateHints {
|
|
36
|
+
/** `standard` | `auction` | `pre-order` | … — picks the right detail route. */
|
|
37
|
+
listingType?: string;
|
|
38
|
+
/** Full ancestor chain; every category page listing this product goes stale. */
|
|
39
|
+
categorySlugs?: string[];
|
|
40
|
+
brandSlug?: string;
|
|
41
|
+
storeId?: string;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Page paths made stale by a write to `collection`.
|
|
45
|
+
*
|
|
46
|
+
* `id` and `hints` are optional; omitting them yields only the always-stale
|
|
47
|
+
* listing pages, which is the correct answer for a bulk or unknown write.
|
|
48
|
+
*/
|
|
49
|
+
export declare function revalidateTargetsFor(collection: string, id?: string, hints?: RevalidateHints): string[];
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which PAGE paths a write to a collection makes stale.
|
|
3
|
+
*
|
|
4
|
+
* ## Why this exists
|
|
5
|
+
*
|
|
6
|
+
* Detail routes used to carry very short `revalidate` windows — 30s on
|
|
7
|
+
* `auctions/[id]`, 60s on `products/[slug]` — not because anyone wanted that
|
|
8
|
+
* freshness, but because **the window was the only freshness mechanism in the
|
|
9
|
+
* app**. Measured 2026-10-09: `revalidatePath` appeared three times in the whole
|
|
10
|
+
* repository, all `revalidatePath("/")`, and `revalidateTag` zero times.
|
|
11
|
+
*
|
|
12
|
+
* Short windows × many unique paths is how you manufacture ISR writes. 95
|
|
13
|
+
* products at 60s is up to 137K regenerations/day from one route, and the
|
|
14
|
+
* project was suspended for exceeding 200K ISR writes with 3.1M — the signature
|
|
15
|
+
* being writes outnumbering reads 3:1, i.e. entries regenerating before anyone
|
|
16
|
+
* read them twice.
|
|
17
|
+
*
|
|
18
|
+
* Invalidation keyed on the WRITE is both cheaper and more correct than any
|
|
19
|
+
* clock: a product that changed is stale immediately, and one that did not never
|
|
20
|
+
* regenerates at all.
|
|
21
|
+
*
|
|
22
|
+
* ## Contract
|
|
23
|
+
*
|
|
24
|
+
* Returns **locale-less** paths. The caller is responsible for prefixing each
|
|
25
|
+
* with the locales it serves — see the revalidate route, which does that from
|
|
26
|
+
* `routing.locales`, because prerendered entries live at `/en/...`.
|
|
27
|
+
*
|
|
28
|
+
* Paths are returned on a best-effort basis: a caller that cannot supply hints
|
|
29
|
+
* still gets the always-stale list, which is the part that matters for a
|
|
30
|
+
* listing. Over-revalidating is cheap (it marks an entry stale; it does not
|
|
31
|
+
* render). Under-revalidating is the failure that shows a buyer a sold-out
|
|
32
|
+
* listing as in stock, so when in doubt this returns MORE paths, not fewer.
|
|
33
|
+
*/
|
|
34
|
+
/** Detail route per listing type. Mirrors `ListingTypePlugin.detailRoute`. */
|
|
35
|
+
const PRODUCT_DETAIL_BY_TYPE = {
|
|
36
|
+
standard: (id) => `/products/${id}`,
|
|
37
|
+
art: (id) => `/products/${id}`,
|
|
38
|
+
stickers: (id) => `/products/${id}`,
|
|
39
|
+
auction: (id) => `/auctions/${id}`,
|
|
40
|
+
"pre-order": (id) => `/pre-orders/${id}`,
|
|
41
|
+
"prize-draw": (id) => `/prize-draws/${id}`,
|
|
42
|
+
classified: (id) => `/classified/${id}`,
|
|
43
|
+
"digital-code": (id) => `/digital-codes/${id}`,
|
|
44
|
+
live: (id) => `/live/${id}`,
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Infer the listing type from the id prefix when no hint was supplied.
|
|
48
|
+
*
|
|
49
|
+
* The slug prefix system (CLAUDE.md § Slug Prefix System) makes this reliable
|
|
50
|
+
* for seeded and generated ids — `auction-…`, `preorder-…`, `prizedraw-…` — and
|
|
51
|
+
* it is only a fallback: the Firestore trigger reads the document and passes
|
|
52
|
+
* `listingType` explicitly.
|
|
53
|
+
*/
|
|
54
|
+
function listingTypeFromId(id) {
|
|
55
|
+
if (id.startsWith("auction-"))
|
|
56
|
+
return "auction";
|
|
57
|
+
if (id.startsWith("preorder-"))
|
|
58
|
+
return "pre-order";
|
|
59
|
+
if (id.startsWith("prizedraw-"))
|
|
60
|
+
return "prize-draw";
|
|
61
|
+
if (id.startsWith("classified-"))
|
|
62
|
+
return "classified";
|
|
63
|
+
if (id.startsWith("digitalcode-"))
|
|
64
|
+
return "digital-code";
|
|
65
|
+
if (id.startsWith("live-"))
|
|
66
|
+
return "live";
|
|
67
|
+
if (id.startsWith("art-"))
|
|
68
|
+
return "art";
|
|
69
|
+
if (id.startsWith("sticker-"))
|
|
70
|
+
return "stickers";
|
|
71
|
+
if (id.startsWith("product-"))
|
|
72
|
+
return "standard";
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
function productTargets(id, hints) {
|
|
76
|
+
const out = ["/", "/products"];
|
|
77
|
+
if (!id)
|
|
78
|
+
return out;
|
|
79
|
+
const type = hints?.listingType ?? listingTypeFromId(id);
|
|
80
|
+
if (type && PRODUCT_DETAIL_BY_TYPE[type]) {
|
|
81
|
+
out.push(PRODUCT_DETAIL_BY_TYPE[type](id));
|
|
82
|
+
// The type's own browse page, when it has one distinct from /products.
|
|
83
|
+
const browse = PRODUCT_DETAIL_BY_TYPE[type](id).replace(/\/[^/]+$/, "");
|
|
84
|
+
if (browse && browse !== "/products")
|
|
85
|
+
out.push(browse);
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
/*
|
|
89
|
+
* Unknown type: revalidate EVERY detail route for this id rather than
|
|
90
|
+
* guessing one. At most nine entries marked stale, none of which renders
|
|
91
|
+
* until requested — strictly cheaper than leaving the real one live.
|
|
92
|
+
*/
|
|
93
|
+
for (const build of Object.values(PRODUCT_DETAIL_BY_TYPE))
|
|
94
|
+
out.push(build(id));
|
|
95
|
+
}
|
|
96
|
+
// A product's price/stock/status is rendered on every taxonomy page that
|
|
97
|
+
// lists it. `categorySlugs` is the full ancestor chain, so this covers the
|
|
98
|
+
// leaf and every parent up to the root.
|
|
99
|
+
for (const slug of hints?.categorySlugs ?? [])
|
|
100
|
+
out.push(`/categories/${slug}`);
|
|
101
|
+
if (hints?.brandSlug)
|
|
102
|
+
out.push(`/brands/${hints.brandSlug}`);
|
|
103
|
+
if (hints?.storeId)
|
|
104
|
+
out.push(`/stores/${hints.storeId}`);
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Page paths made stale by a write to `collection`.
|
|
109
|
+
*
|
|
110
|
+
* `id` and `hints` are optional; omitting them yields only the always-stale
|
|
111
|
+
* listing pages, which is the correct answer for a bulk or unknown write.
|
|
112
|
+
*/
|
|
113
|
+
export function revalidateTargetsFor(collection, id, hints) {
|
|
114
|
+
let paths;
|
|
115
|
+
switch (collection) {
|
|
116
|
+
case "products":
|
|
117
|
+
paths = productTargets(id, hints);
|
|
118
|
+
break;
|
|
119
|
+
case "categories":
|
|
120
|
+
// Brands and pricing bundles are `categories` rows discriminated by
|
|
121
|
+
// `categoryType`, so all three detail shapes are marked.
|
|
122
|
+
paths = ["/", "/categories"];
|
|
123
|
+
if (id)
|
|
124
|
+
paths.push(`/categories/${id}`, `/brands/${id}`, `/bundles/${id}`);
|
|
125
|
+
break;
|
|
126
|
+
case "blogPosts":
|
|
127
|
+
paths = ["/", "/blog"];
|
|
128
|
+
if (id)
|
|
129
|
+
paths.push(`/blog/${id}`);
|
|
130
|
+
break;
|
|
131
|
+
case "events":
|
|
132
|
+
paths = ["/", "/events"];
|
|
133
|
+
if (id)
|
|
134
|
+
paths.push(`/events/${id}`);
|
|
135
|
+
break;
|
|
136
|
+
case "reviews":
|
|
137
|
+
// A new review changes the product's rating and the homepage stat.
|
|
138
|
+
paths = ["/", "/reviews"];
|
|
139
|
+
break;
|
|
140
|
+
case "stores":
|
|
141
|
+
paths = ["/", "/stores"];
|
|
142
|
+
if (id)
|
|
143
|
+
paths.push(`/stores/${id}`);
|
|
144
|
+
break;
|
|
145
|
+
case "carouselSlides":
|
|
146
|
+
case "homepageSections":
|
|
147
|
+
case "siteSettings":
|
|
148
|
+
paths = ["/"];
|
|
149
|
+
break;
|
|
150
|
+
case "faqs":
|
|
151
|
+
paths = ["/", "/faqs"];
|
|
152
|
+
break;
|
|
153
|
+
default:
|
|
154
|
+
paths = [];
|
|
155
|
+
}
|
|
156
|
+
return Array.from(new Set(paths));
|
|
157
|
+
}
|
|
@@ -101,7 +101,7 @@ export interface LineTaxComponent {
|
|
|
101
101
|
* `productById` supplies the live `gstRate`; each member's snapshotted rate is
|
|
102
102
|
* the fallback for a member whose listing has since been archived.
|
|
103
103
|
*/
|
|
104
|
-
export declare function lineTaxComponentsFor(item: CartItemDocument, productById: Map<string, ProductDocument>): LineTaxComponent[];
|
|
104
|
+
export declare function lineTaxComponentsFor(item: CartItemDocument, productById: Map<string, ProductDocument | null>): LineTaxComponent[];
|
|
105
105
|
/**
|
|
106
106
|
* Total GST for one order group, and the base it was charged on.
|
|
107
107
|
*
|
|
@@ -111,10 +111,34 @@ export declare function lineTaxComponentsFor(item: CartItemDocument, productById
|
|
|
111
111
|
* one and not the other — here that would mean quoting one tax figure and
|
|
112
112
|
* charging another.
|
|
113
113
|
*/
|
|
114
|
-
export declare function sumGroupGst(items: readonly CartItemDocument[], productById: Map<string, ProductDocument>, intraState: boolean):
|
|
114
|
+
export declare function sumGroupGst(items: readonly CartItemDocument[], productById: Map<string, ProductDocument | null>, intraState: boolean): GroupGstSummary;
|
|
115
|
+
/** One rate's rollup within an order group — what Rule 46 asks an invoice to show. */
|
|
116
|
+
export interface GstRateSlice {
|
|
117
|
+
gstRate: number;
|
|
118
|
+
taxableAmount: number;
|
|
119
|
+
cgst: number;
|
|
120
|
+
sgst: number;
|
|
121
|
+
igst: number;
|
|
122
|
+
gstAmount: number;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* What one order group owes, with the split and the rate dimension intact.
|
|
126
|
+
*
|
|
127
|
+
* Widened from `{ taxableAmount, gstAmount }` — additive, so both existing
|
|
128
|
+
* callers keep working while neither has to re-derive a split the per-slice
|
|
129
|
+
* `calculateGst` calls above already computed correctly.
|
|
130
|
+
*/
|
|
131
|
+
export interface GroupGstSummary {
|
|
132
|
+
/** Taxable value of slices carrying a POSITIVE rate. */
|
|
115
133
|
taxableAmount: number;
|
|
134
|
+
/** Taxable value of 0%-rated (exempt) slices. Not part of `taxableAmount`. */
|
|
135
|
+
exemptAmount: number;
|
|
136
|
+
cgst: number;
|
|
137
|
+
sgst: number;
|
|
138
|
+
igst: number;
|
|
116
139
|
gstAmount: number;
|
|
117
|
-
|
|
140
|
+
byRate: GstRateSlice[];
|
|
141
|
+
}
|
|
118
142
|
/**
|
|
119
143
|
* Split `total` across members in proportion to their list prices, with the
|
|
120
144
|
* rounding remainder pushed onto the LAST member so the parts sum to exactly
|
|
@@ -125,6 +149,92 @@ export declare function sumGroupGst(items: readonly CartItemDocument[], productB
|
|
|
125
149
|
* items), which is the only case where proportion is undefined.
|
|
126
150
|
*/
|
|
127
151
|
export declare function allocateAcrossMembers(total: number, weights: number[]): number[];
|
|
152
|
+
/** Exactly the fields `orderGoodsValue` / `orderTaxableValue` read. */
|
|
153
|
+
export interface PayableOrderLike {
|
|
154
|
+
items?: Array<{
|
|
155
|
+
quantity: number;
|
|
156
|
+
unitPrice: number;
|
|
157
|
+
cancelledQuantity?: number;
|
|
158
|
+
/** Snapshotted at purchase. Absent or 0 means no tax to back out. */
|
|
159
|
+
gstRate?: number;
|
|
160
|
+
}>;
|
|
161
|
+
/** The flat shape, authoritative when `items` is absent. */
|
|
162
|
+
quantity?: number;
|
|
163
|
+
unitPrice?: number;
|
|
164
|
+
gstRate?: number;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* The GOODS VALUE of an order — the only correct base for a seller payout.
|
|
168
|
+
*
|
|
169
|
+
* 🛑 This is NOT `OrderDocument.totalPrice`, and conflating the two was a live
|
|
170
|
+
* money bug in all four payout paths (`autoPayoutEligibility`,
|
|
171
|
+
* `weeklyPayoutEligibility`, and both `seller-actions` entry points). That
|
|
172
|
+
* field is the GRAND TOTAL — `checkout/actions.ts` builds it as
|
|
173
|
+
* `goods − coupon + shipping + codHandling + whatsappNotify + giftWrap +
|
|
174
|
+
* shipmentProtection + platformFee + platformFeeGst + emiSurcharge + gstAmount`
|
|
175
|
+
* — so using it as the payout base did three wrong things at once:
|
|
176
|
+
*
|
|
177
|
+
* 1. **paid the seller the GST we collected** and must remit ourselves;
|
|
178
|
+
* 2. **took the platform fee twice** — the buyer already paid it as a term of
|
|
179
|
+
* `totalPrice`, and `computePayoutDeduction` then charged the seller a
|
|
180
|
+
* percentage of a figure that already included it;
|
|
181
|
+
* 3. **computed every percentage on an inflated base**, since shipping, COD
|
|
182
|
+
* handling, add-on fees and the EMI surcharge were all inside it.
|
|
183
|
+
*
|
|
184
|
+
* The payout base is the **taxable value**, never the gross. That one sentence
|
|
185
|
+
* also covers GST-inclusive pricing: an inclusive ₹999 @5% backs out to a
|
|
186
|
+
* ₹951.43 taxable value and that is what is paid, while an exclusive ₹999
|
|
187
|
+
* charges the buyer ₹1,048.95 and pays ₹999.
|
|
188
|
+
*
|
|
189
|
+
* 🛑 Computed from `quantity − cancelledQuantity`, NOT from `item.totalPrice`.
|
|
190
|
+
* A partial cancellation sets `cancelledQuantity: item.quantity` and
|
|
191
|
+
* **deliberately leaves `totalPrice` untouched** (`order-actions.ts:97-106`),
|
|
192
|
+
* so summing `totalPrice` would pay the seller in full for lines the buyer
|
|
193
|
+
* cancelled. `stock-restore.ts:20` already derives the live quantity this way.
|
|
194
|
+
*
|
|
195
|
+
* 🛑 Falls back to the FLAT `quantity × unitPrice` when `items` is absent.
|
|
196
|
+
* `items?` is optional on `OrderDocument` and the flat fields are the primary
|
|
197
|
+
* shape, not legacy residue — a bare `Σ items[]` would silently pay **zero**
|
|
198
|
+
* on any flat-shaped order, which is indistinguishable from "this seller is
|
|
199
|
+
* owed nothing".
|
|
200
|
+
*/
|
|
201
|
+
export declare function orderGoodsValue(order: PayableOrderLike): number;
|
|
202
|
+
/** Sum of `orderGoodsValue` across orders — the GROSS, not the payout base. */
|
|
203
|
+
export declare function ordersGoodsValue(orders: readonly PayableOrderLike[]): number;
|
|
204
|
+
/**
|
|
205
|
+
* The TAXABLE VALUE of an order — **the payout base.**
|
|
206
|
+
*
|
|
207
|
+
* 🛑 This is the second of two narrowings, and the distinction between them is
|
|
208
|
+
* worth keeping straight:
|
|
209
|
+
*
|
|
210
|
+
* - C0 established the base is the **goods value**, not the order grand total
|
|
211
|
+
* (which folded in GST, the platform fee the buyer already paid, shipping,
|
|
212
|
+
* COD handling, add-ons and the EMI surcharge).
|
|
213
|
+
* - This establishes it is the **taxable value**, not the gross goods value —
|
|
214
|
+
* because every price here is tax-INCLUSIVE, so the goods value still has
|
|
215
|
+
* our GST sitting inside it, and paying that out is the very thing C0 set
|
|
216
|
+
* out to stop.
|
|
217
|
+
*
|
|
218
|
+
* The second narrowing was not possible until the pricing model was settled:
|
|
219
|
+
* under the old add-on-top math the goods value WAS the taxable value, so the
|
|
220
|
+
* two were indistinguishable and C0's fix was complete as written.
|
|
221
|
+
*
|
|
222
|
+
* One sentence covers both seller modes, which is the reason the flag is an
|
|
223
|
+
* authoring convenience and nothing more:
|
|
224
|
+
*
|
|
225
|
+
* **the payout base is the taxable value, never the gross.**
|
|
226
|
+
*
|
|
227
|
+
* - inclusive seller, ₹999 @5% → buyer pays ₹999, taxable ₹951.43, payout ₹951.43
|
|
228
|
+
* - exclusive seller types ₹999 → grossed up on write to ₹1,048.95, buyer
|
|
229
|
+
* pays ₹1,048.95, taxable ₹999.00, payout ₹999.00
|
|
230
|
+
*
|
|
231
|
+
* Per ITEM, using each line's own snapshotted `gstRate`: a mixed 5% + 18%
|
|
232
|
+
* order cannot be backed out with a single blended rate, and an order-level
|
|
233
|
+
* rate does not exist.
|
|
234
|
+
*/
|
|
235
|
+
export declare function orderTaxableValue(order: PayableOrderLike): number;
|
|
236
|
+
/** Sum of `orderTaxableValue` across orders — the payout base for a batch. */
|
|
237
|
+
export declare function ordersTaxableValue(orders: readonly PayableOrderLike[]): number;
|
|
128
238
|
/**
|
|
129
239
|
* P-6 — pre-order groups charge each product's own `preOrderDepositPercent`
|
|
130
240
|
* (falling back to the generic COD deposit % when a product doesn't have one
|
|
@@ -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
|