@mohasinac/appkit 4.11.3 → 4.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_internal/server/features/auctions/service.d.ts +7 -1
- package/dist/_internal/server/features/auctions/service.js +13 -5
- package/dist/_internal/server/features/checkout/actions.d.ts +36 -18
- package/dist/_internal/server/features/checkout/actions.js +127 -45
- package/dist/_internal/server/features/orders/actions.js +5 -3
- package/dist/_internal/server/features/orders/adapters.js +13 -0
- package/dist/_internal/server/features/products/list-public.d.ts +28 -0
- package/dist/_internal/server/features/products/list-public.js +92 -0
- package/dist/_internal/server/functions/firestore.d.ts +1 -2
- package/dist/_internal/server/functions/firestore.js +1 -9
- package/dist/_internal/server/jobs/core/auctionSettlement.js +5 -2
- package/dist/_internal/server/jobs/core/index.d.ts +0 -1
- package/dist/_internal/server/jobs/core/index.js +0 -1
- package/dist/_internal/server/jobs/core/offerExpiry.js +33 -1
- package/dist/_internal/server/jobs/handlers/index.d.ts +1 -2
- package/dist/_internal/server/jobs/handlers/index.js +1 -2
- package/dist/_internal/shared/checkout/lanes.d.ts +20 -0
- package/dist/_internal/shared/checkout/lanes.js +28 -0
- package/dist/_internal/shared/features/auctions/config.d.ts +34 -0
- package/dist/_internal/shared/features/auctions/config.js +47 -3
- package/dist/_internal/shared/features/checkout/config.d.ts +4 -0
- package/dist/_internal/shared/features/checkout/config.js +8 -0
- package/dist/_internal/shared/fees/calculator.d.ts +34 -2
- package/dist/_internal/shared/fees/calculator.js +45 -1
- package/dist/_internal/shared/listing-types/auction/config.js +1 -1
- package/dist/_internal/shared/listing-types/digital-code/config.js +1 -1
- package/dist/_internal/shared/listing-types/live/config.js +1 -1
- package/dist/_internal/shared/listing-types/pre-order/config.js +1 -1
- package/dist/client.d.ts +7 -1
- package/dist/client.js +4 -1
- package/dist/constants/api-endpoints.d.ts +6 -0
- package/dist/constants/api-endpoints.js +2 -0
- package/dist/features/account/components/NotificationBell.js +1 -1
- package/dist/features/admin/components/AdminAddressesView.js +18 -10
- package/dist/features/admin/components/AdminArtView.d.ts +9 -0
- package/dist/features/admin/components/AdminArtView.js +11 -35
- package/dist/features/admin/components/AdminClassifiedView.d.ts +9 -0
- package/dist/features/admin/components/AdminClassifiedView.js +11 -35
- package/dist/features/admin/components/AdminDigitalCodesView.d.ts +9 -0
- package/dist/features/admin/components/AdminDigitalCodesView.js +11 -42
- package/dist/features/admin/components/AdminLiveView.d.ts +9 -0
- package/dist/features/admin/components/AdminLiveView.js +11 -41
- package/dist/features/admin/components/AdminNotificationsView.js +4 -1
- package/dist/features/admin/components/AdminOrderEditorView.d.ts +14 -1
- package/dist/features/admin/components/AdminOrderEditorView.js +18 -5
- package/dist/features/admin/components/AdminOrdersView.js +15 -1
- package/dist/features/admin/components/AdminPaymentMethodsView.js +15 -10
- package/dist/features/admin/components/AdminSiteConfigGuideView.js +1 -1
- package/dist/features/admin/components/AdminSiteSettingsView.js +4 -2
- package/dist/features/admin/components/AdminStickersView.d.ts +9 -0
- package/dist/features/admin/components/AdminStickersView.js +11 -35
- package/dist/features/admin/hooks/useAdminListing.js +7 -2
- package/dist/features/admin/schemas/firestore.d.ts +5 -0
- package/dist/features/admin/schemas/firestore.js +1 -0
- package/dist/features/auctions/actions/bid-actions.js +73 -11
- package/dist/features/auctions/components/AuctionDetailPageView.js +8 -2
- package/dist/features/auctions/components/MarketplaceAuctionCard.js +3 -3
- package/dist/features/auctions/components/PlaceBidFormClient.js +89 -14
- package/dist/features/auctions/schemas/bid-input.d.ts +19 -6
- package/dist/features/auctions/schemas/bid-input.js +19 -7
- package/dist/features/auth/repository/user.repository.js +6 -0
- package/dist/features/cart/components/CartDrawer.d.ts +9 -1
- package/dist/features/cart/components/CartDrawer.js +4 -3
- package/dist/features/cart/components/CartPriceBreakdown.d.ts +61 -0
- package/dist/features/cart/components/CartPriceBreakdown.js +25 -0
- package/dist/features/cart/components/StoreAddonsPicker.d.ts +47 -0
- package/dist/features/cart/components/StoreAddonsPicker.js +21 -0
- package/dist/features/cart/components/index.d.ts +4 -0
- package/dist/features/cart/components/index.js +2 -0
- package/dist/features/cart/repository/cart.repository.d.ts +20 -2
- package/dist/features/cart/repository/cart.repository.js +49 -0
- package/dist/features/cart/schemas/firestore.d.ts +26 -0
- package/dist/features/faq/components/FAQHelpfulButtons.js +2 -2
- package/dist/features/filters/FilterFacetSection.js +1 -1
- package/dist/features/homepage/components/WhatsAppCommunitySection.js +1 -1
- package/dist/features/layout/BackToTop.js +1 -1
- package/dist/features/layout/BottomActions.js +38 -5
- package/dist/features/layout/BottomActionsContext.d.ts +12 -0
- package/dist/features/layout/BottomActionsContext.js +5 -0
- package/dist/features/layout/FooterLayout.js +2 -2
- package/dist/features/layout/TitleBarLayout.js +1 -1
- package/dist/features/layout/hooks/useBottomActions.d.ts +6 -0
- package/dist/features/layout/hooks/useBottomActions.js +11 -1
- package/dist/features/orders/components/OrderAddonBadges.d.ts +36 -0
- package/dist/features/orders/components/OrderAddonBadges.js +28 -0
- package/dist/features/orders/components/index.d.ts +1 -0
- package/dist/features/orders/components/index.js +1 -0
- package/dist/features/orders/repository/orders.repository.d.ts +32 -0
- package/dist/features/orders/repository/orders.repository.js +14 -0
- package/dist/features/orders/types/index.d.ts +22 -0
- package/dist/features/pre-orders/components/MarketplacePreorderCard.js +3 -3
- package/dist/features/products/components/ArtStickersListView.js +5 -4
- package/dist/features/products/components/MarketplaceBundleCard.js +2 -2
- package/dist/features/products/components/MarketplacePrizeDrawCard.js +2 -2
- package/dist/features/products/components/PrizeDrawCollage.js +1 -1
- package/dist/features/products/components/PrizeDrawItemsEditor.js +1 -1
- package/dist/features/products/components/PrizeDrawsListingView.js +18 -33
- package/dist/features/products/components/ProductDetailPageView.js +1 -1
- package/dist/features/products/components/ProductGrid.js +3 -3
- package/dist/features/products/components/ProductsIndexListing.js +87 -11
- package/dist/features/products/components/ProductsIndexPageView.js +14 -2
- package/dist/features/products/config/listing-type-listing-config.d.ts +31 -0
- package/dist/features/products/config/listing-type-listing-config.js +74 -0
- package/dist/features/products/hooks/useProducts.js +14 -0
- package/dist/features/products/repository/products.repository.d.ts +0 -4
- package/dist/features/products/repository/products.repository.js +0 -15
- package/dist/features/products/types/index.d.ts +15 -0
- package/dist/features/promotions/repository/coupons.repository.d.ts +12 -0
- package/dist/features/promotions/repository/coupons.repository.js +12 -0
- package/dist/features/seller/components/SellerOrdersView.js +2 -1
- package/dist/features/seller/components/SellerSidebar.js +1 -1
- package/dist/features/stores/components/StoreArtStickersPageView.d.ts +21 -0
- package/dist/features/stores/components/StoreArtStickersPageView.js +30 -0
- package/dist/features/stores/components/StoreAuctionsPageView.d.ts +4 -1
- package/dist/features/stores/components/StoreAuctionsPageView.js +17 -17
- package/dist/features/stores/components/StoreClassifiedsListing.js +7 -0
- package/dist/features/stores/components/StoreClassifiedsPageView.d.ts +4 -1
- package/dist/features/stores/components/StoreClassifiedsPageView.js +16 -12
- package/dist/features/stores/components/StoreDetailLayoutView.js +4 -3
- package/dist/features/stores/components/StoreDigitalCodesListing.js +5 -0
- package/dist/features/stores/components/StoreDigitalCodesPageView.d.ts +4 -1
- package/dist/features/stores/components/StoreDigitalCodesPageView.js +16 -12
- package/dist/features/stores/components/StoreLiveItemsListing.js +7 -0
- package/dist/features/stores/components/StoreLiveItemsPageView.d.ts +4 -1
- package/dist/features/stores/components/StoreLiveItemsPageView.js +16 -12
- package/dist/features/stores/components/StorePreOrdersPageView.d.ts +4 -1
- package/dist/features/stores/components/StorePreOrdersPageView.js +17 -17
- package/dist/features/stores/components/StorePrizeDrawsPageView.d.ts +4 -8
- package/dist/features/stores/components/StorePrizeDrawsPageView.js +17 -22
- package/dist/features/stores/components/StoreProductsListing.d.ts +7 -1
- package/dist/features/stores/components/StoreProductsListing.js +3 -2
- package/dist/features/stores/components/StoreProductsPageView.d.ts +1 -1
- package/dist/features/stores/components/StoreProductsPageView.js +16 -21
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +875 -0
- package/dist/features/whatsapp-bot/components/WhatsAppChatButton.js +1 -1
- package/dist/jobs.d.ts +1 -1
- package/dist/jobs.js +1 -1
- package/dist/next/routing/route-map.d.ts +2 -0
- package/dist/next/routing/route-map.js +5 -0
- package/dist/seed/homepage-sections-seed-data.js +5 -1
- package/dist/seed/reviews-seed-data.js +52 -0
- package/dist/seed/site-settings-seed-data.js +18 -0
- package/dist/server-entry.d.ts +1 -0
- package/dist/server-entry.js +1 -0
- package/dist/styles.css +66 -1
- package/dist/tailwind-utilities.css +1 -1
- package/dist/tokens/color-pairs.js +1 -0
- package/dist/tokens/tokens.css +33 -0
- package/dist/ui/components/Button.style.css +21 -0
- package/dist/ui/components/Checkbox.d.ts +10 -1
- package/dist/ui/components/Checkbox.js +14 -2
- package/dist/ui/components/Checkbox.style.css +11 -0
- package/dist/ui/components/ImageLightbox.js +7 -4
- package/dist/ui/components/surface-tokens.d.ts +7 -0
- package/dist/ui/components/surface-tokens.js +7 -0
- package/package.json +1 -1
- package/dist/_internal/server/jobs/core/onBidPlaced.d.ts +0 -15
- package/dist/_internal/server/jobs/core/onBidPlaced.js +0 -63
- package/dist/_internal/server/jobs/handlers/onBidPlaced.d.ts +0 -3
- package/dist/_internal/server/jobs/handlers/onBidPlaced.js +0 -9
|
@@ -80,6 +80,40 @@ function num(raw) {
|
|
|
80
80
|
const n = Number(raw);
|
|
81
81
|
return Number.isFinite(n) ? n : undefined;
|
|
82
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* Per-type facet URL key → the Firestore field path it filters.
|
|
85
|
+
*
|
|
86
|
+
* Every target is already allowlisted in the products repository's
|
|
87
|
+
* SIEVE_FIELDS — the nested `classified.*` / `digitalCode.*` / `liveItem.*`
|
|
88
|
+
* entries have been there since those types shipped. What was missing was any
|
|
89
|
+
* code path that actually emitted a clause for them: the store listings
|
|
90
|
+
* declared these keys in their FILTER_KEYS (so they counted toward the
|
|
91
|
+
* filter badge) and rendered the controls, but never put them on the wire.
|
|
92
|
+
*/
|
|
93
|
+
const TYPE_FACET_FIELD = {
|
|
94
|
+
[TABLE_KEYS.CITY]: "classified.meetupArea.city",
|
|
95
|
+
[TABLE_KEYS.NEGOTIABLE]: "classified.negotiable",
|
|
96
|
+
[TABLE_KEYS.ACCEPTS_SHIPPING]: "classified.acceptsShipping",
|
|
97
|
+
[TABLE_KEYS.DELIVERY_METHOD]: "digitalCode.codeDeliveryMethod",
|
|
98
|
+
[TABLE_KEYS.SPECIES]: "liveItem.species",
|
|
99
|
+
[TABLE_KEYS.LIVE_SEX]: "liveItem.sex",
|
|
100
|
+
[TABLE_KEYS.JURISDICTION]: "liveItem.jurisdictionAllowed",
|
|
101
|
+
};
|
|
102
|
+
/** Read every known per-type facet present in the params. */
|
|
103
|
+
function readTypeFacets(get) {
|
|
104
|
+
const out = {};
|
|
105
|
+
for (const key of Object.keys(TYPE_FACET_FIELD)) {
|
|
106
|
+
const value = get(key);
|
|
107
|
+
if (value)
|
|
108
|
+
out[key] = value;
|
|
109
|
+
}
|
|
110
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
111
|
+
}
|
|
112
|
+
/** Split a pipe-joined multi-select value; `undefined` when empty. */
|
|
113
|
+
function pipeList(raw) {
|
|
114
|
+
const parts = raw.split("|").map((s) => s.trim()).filter(Boolean);
|
|
115
|
+
return parts.length > 0 ? parts : undefined;
|
|
116
|
+
}
|
|
83
117
|
/**
|
|
84
118
|
* Read the shared public-listing query params. SSR views hand in Next's
|
|
85
119
|
* `searchParams` record; the API route hands in `url.searchParams`. Both produce
|
|
@@ -118,6 +152,12 @@ export function parsePublicProductParams(params, defaults) {
|
|
|
118
152
|
freeShipping: get(TABLE_KEYS.FREE_SHIPPING) === "true" || undefined,
|
|
119
153
|
isPartOfBundle: get(TABLE_KEYS.IS_PART_OF_BUNDLE) === "true" || undefined,
|
|
120
154
|
features: featuresRaw ? featuresRaw.split("|").filter(Boolean) : undefined,
|
|
155
|
+
// Tags and Sublisting Type were rendered by <ProductFilters> for all three
|
|
156
|
+
// variants and counted toward the active-filter badge, but were never
|
|
157
|
+
// parsed here — so ticking one inflated the badge and changed nothing.
|
|
158
|
+
tags: pipeList(get(TABLE_KEYS.TAGS)),
|
|
159
|
+
sublistingCategoryIds: pipeList(get(TABLE_KEYS.SUBLISTING_CATEGORY)),
|
|
160
|
+
typeFacets: readTypeFacets(get),
|
|
121
161
|
preOrderProductionStatus: get(TABLE_KEYS.PREORDER_STATUS) || get("preOrderStatus") || undefined,
|
|
122
162
|
prizeRevealStatus: get(TABLE_KEYS.PRIZE_REVEAL_STATUS) || undefined,
|
|
123
163
|
// Three spellings of one intent — /products and /art call it "Show sold",
|
|
@@ -211,6 +251,31 @@ function buildFirestoreSafeFilters(input) {
|
|
|
211
251
|
if (input.features?.length === 1) {
|
|
212
252
|
parts.push(sieveFilter(PRODUCT_FIELDS.FEATURES, SIEVE_OP.CONTAINS, input.features[0]));
|
|
213
253
|
}
|
|
254
|
+
// `tags` is an array field, so the same one-value limit applies.
|
|
255
|
+
if (input.tags?.length === 1) {
|
|
256
|
+
parts.push(sieveFilter(PRODUCT_FIELDS.TAGS, SIEVE_OP.CONTAINS, input.tags[0]));
|
|
257
|
+
}
|
|
258
|
+
// A scalar field, so several values ARE expressible as an OR-group (which
|
|
259
|
+
// the Firebase adapter upgrades to `in`).
|
|
260
|
+
if (input.sublistingCategoryIds?.length) {
|
|
261
|
+
parts.push(sieveFilter(PRODUCT_FIELDS.SUBLISTING_CATEGORY_ID, SIEVE_OP.EQ, input.sublistingCategoryIds.join("|")));
|
|
262
|
+
}
|
|
263
|
+
// Per-type facets. Unknown keys can't reach here — `readTypeFacets` only
|
|
264
|
+
// reads the ones in TYPE_FACET_FIELD, so an arbitrary query param can never
|
|
265
|
+
// be turned into a Firestore field path.
|
|
266
|
+
for (const [key, raw] of Object.entries(input.typeFacets ?? {})) {
|
|
267
|
+
const field = TYPE_FACET_FIELD[key];
|
|
268
|
+
if (!field || !raw)
|
|
269
|
+
continue;
|
|
270
|
+
// `jurisdictionAllowed` is an array field — array-contains, not equality.
|
|
271
|
+
const op = field.endsWith("jurisdictionAllowed") ? SIEVE_OP.CONTAINS : SIEVE_OP.EQ;
|
|
272
|
+
const values = raw.split("|").filter(Boolean);
|
|
273
|
+
if (values.length === 0)
|
|
274
|
+
continue;
|
|
275
|
+
parts.push(op === SIEVE_OP.CONTAINS
|
|
276
|
+
? sieveFilter(field, op, values[0])
|
|
277
|
+
: sieveFilter(field, op, values.join("|")));
|
|
278
|
+
}
|
|
214
279
|
if (input.rawFilters)
|
|
215
280
|
parts.push(input.rawFilters);
|
|
216
281
|
return sieveAnd(...parts.filter(Boolean));
|
|
@@ -353,6 +418,33 @@ export async function listPublicProducts(input, opts) {
|
|
|
353
418
|
sorts,
|
|
354
419
|
};
|
|
355
420
|
}
|
|
421
|
+
/**
|
|
422
|
+
* One store tab's worth of listings.
|
|
423
|
+
*
|
|
424
|
+
* Every `Store*PageView` used to hand-roll this: `productRepository.list()`
|
|
425
|
+
* with an inline `sieveAnd(...)` and a `.catch(() => null)`. That is the exact
|
|
426
|
+
* shape that made `/art` render empty — an unsafe inequality (or any missing
|
|
427
|
+
* index) becomes FAILED_PRECONDITION, the catch turns it into `null`, and the
|
|
428
|
+
* page shows "no results" with nothing logged. Eight views carried it, and
|
|
429
|
+
* `audit-listing-filter-parity` had none of them registered.
|
|
430
|
+
*
|
|
431
|
+
* Routing them through `listPublicProducts` also gives every store tab
|
|
432
|
+
* something it never had: the URL's own sort/filter/page params reach the SSR
|
|
433
|
+
* fetch, so the first paint matches what the toolbar says is selected.
|
|
434
|
+
*/
|
|
435
|
+
export async function listStoreProducts(storeId, listingTypes, searchParams = {}, defaults) {
|
|
436
|
+
return listPublicProducts({
|
|
437
|
+
...parsePublicProductParams(searchParams, {
|
|
438
|
+
listingTypes,
|
|
439
|
+
pageSize: defaults?.pageSize ?? DEFAULT_PAGE_SIZE,
|
|
440
|
+
sorts: defaults?.sorts,
|
|
441
|
+
...defaultTogglesForListingTypes(listingTypes),
|
|
442
|
+
}),
|
|
443
|
+
// The route owns the store identity — a `?storeId=` in the URL must not be
|
|
444
|
+
// able to make one store's tab render another store's inventory.
|
|
445
|
+
storeId,
|
|
446
|
+
});
|
|
447
|
+
}
|
|
356
448
|
function sortRows(rows, field, desc) {
|
|
357
449
|
return [...rows].sort((a, b) => {
|
|
358
450
|
const av = a[field];
|
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
* The path patterns mirror the original `functions/src/index.ts` declarations
|
|
7
7
|
* to preserve identical deploy behavior.
|
|
8
8
|
*/
|
|
9
|
-
export declare const onBidPlaced: import("./types").DocumentTriggerFunctionDefinition<null, import("../jobs").NewBid>;
|
|
10
9
|
export declare const onOrderCreate: import("./types").DocumentTriggerFunctionDefinition<null, import("../jobs").NewOrder>;
|
|
11
10
|
export declare const onOrderStatusChange: import("./types").DocumentTriggerFunctionDefinition<import("../jobs").OrderBefore, import("../jobs").OrderAfter>;
|
|
12
11
|
export declare const onProductWrite: import("./types").DocumentTriggerFunctionDefinition<import("../jobs").ProductDoc, import("../jobs").ProductDoc>;
|
|
@@ -51,7 +50,7 @@ export declare const onCatalogueSubmittedForApproval: import("./types").Document
|
|
|
51
50
|
[x: string]: import("../../..").JsonValue;
|
|
52
51
|
}>;
|
|
53
52
|
export declare const onJobCreated: import("./types").DocumentTriggerFunctionDefinition<null, import("../../../server").JobDocument>;
|
|
54
|
-
export declare const FIRESTORE_TRIGGER_FUNCTIONS: readonly [import("./types").DocumentTriggerFunctionDefinition<null, import("../jobs").
|
|
53
|
+
export declare const FIRESTORE_TRIGGER_FUNCTIONS: readonly [import("./types").DocumentTriggerFunctionDefinition<null, import("../jobs").NewOrder>, import("./types").DocumentTriggerFunctionDefinition<import("../jobs").OrderBefore, import("../jobs").OrderAfter>, import("./types").DocumentTriggerFunctionDefinition<import("../jobs").ProductDoc, import("../jobs").ProductDoc>, import("./types").DocumentTriggerFunctionDefinition<{
|
|
55
54
|
[x: string]: import("../../..").JsonValue;
|
|
56
55
|
}, {
|
|
57
56
|
[x: string]: import("../../..").JsonValue;
|
|
@@ -6,16 +6,9 @@
|
|
|
6
6
|
* The path patterns mirror the original `functions/src/index.ts` declarations
|
|
7
7
|
* to preserve identical deploy behavior.
|
|
8
8
|
*/
|
|
9
|
-
import {
|
|
9
|
+
import { onCategoryWriteHandler, onOrderCreateHandler, onOrderStatusChangeHandler, onProductStockChangeHandler, onProductWriteHandler, onReviewWriteHandler, onScamReportCreateHandler, onScamReportUpdateHandler, onStoreWriteHandler, onSupportTicketCreateHandler, onSupportTicketUpdateHandler, onUserBanChangeHandler, onShipmentItemWriteHandler, onShipmentLotWriteHandler, onShipmentHeaderWriteHandler, onShipmentDeletedHandler, onCatalogueSubmittedForApprovalHandler, onJobCreatedHandler, onPrizeDrawPaymentConfirmedHandler, prizeDrawSoldOutRevealHandler, } from "../jobs/handlers";
|
|
10
10
|
import { defineFunction } from "./define";
|
|
11
11
|
const REGION = "asia-south1";
|
|
12
|
-
export const onBidPlaced = defineFunction({
|
|
13
|
-
name: "onBidPlaced",
|
|
14
|
-
description: "Side-effects on bid creation (notifications, outbid emails).",
|
|
15
|
-
trigger: { kind: "documentCreated", pathPattern: "bids/{bidId}" },
|
|
16
|
-
handler: onBidPlacedHandler,
|
|
17
|
-
options: { region: REGION },
|
|
18
|
-
});
|
|
19
12
|
export const onOrderCreate = defineFunction({
|
|
20
13
|
name: "onOrderCreate",
|
|
21
14
|
description: "Side-effects on order creation (notify store, decrement stock).",
|
|
@@ -164,7 +157,6 @@ export const onJobCreated = defineFunction({
|
|
|
164
157
|
options: { region: REGION, timeoutSeconds: 300, memory: "512MiB" },
|
|
165
158
|
});
|
|
166
159
|
export const FIRESTORE_TRIGGER_FUNCTIONS = [
|
|
167
|
-
onBidPlaced,
|
|
168
160
|
onOrderCreate,
|
|
169
161
|
onOrderStatusChange,
|
|
170
162
|
onProductWrite,
|
|
@@ -2,12 +2,15 @@ import { bidRepository, cartRepository, productRepository, storeRepository, } fr
|
|
|
2
2
|
import { sendNotification } from "../../../../features/admin/actions/notification-actions";
|
|
3
3
|
import { AUCTION_MESSAGES } from "../handlers/messages";
|
|
4
4
|
import { ROUTES } from "../../../../next/routing/route-map";
|
|
5
|
-
import { CART_LANE } from "../../../shared/checkout/lanes";
|
|
5
|
+
import { CART_LANE, LANE_CHECKOUT_WINDOW_MS } from "../../../shared/checkout/lanes";
|
|
6
6
|
/**
|
|
7
7
|
* How long the winner has to pay before the locked cart line lapses. Mirrors
|
|
8
8
|
* the accepted-offer window so both locked lanes behave the same.
|
|
9
9
|
*/
|
|
10
|
-
|
|
10
|
+
// Was a local `48 * 60 * 60 * 1000` here AND an identical one in bid-actions.ts
|
|
11
|
+
// (the Buy-Now path), with nothing linking them — changing one would have given
|
|
12
|
+
// the same kind of win two different deadlines depending on how it was won.
|
|
13
|
+
const AUCTION_CHECKOUT_WINDOW_MS = LANE_CHECKOUT_WINDOW_MS.auction;
|
|
11
14
|
/** Deep link the winner straight into the auction checkout lane. */
|
|
12
15
|
const AUCTION_CHECKOUT_URL = `${String(ROUTES.USER.CHECKOUT)}?lane=${CART_LANE.AUCTION}`;
|
|
13
16
|
async function settleAuction(ctx, product) {
|
|
@@ -36,7 +36,6 @@ export { runStoreAnalytics, type StoreAnalyticsInput, type StoreAnalyticsResult,
|
|
|
36
36
|
export { runAdminAnalytics, type AdminAnalyticsResult } from "./adminAnalytics";
|
|
37
37
|
export { runTriggerEventRaffle, type TriggerEventRaffleInput, type TriggerEventRaffleResult, } from "./triggerEventRaffle";
|
|
38
38
|
export { runAssignSpinPrize, type AssignSpinPrizeInput, type AssignSpinPrizeResult, } from "./assignSpinPrize";
|
|
39
|
-
export { handleBidPlaced, type HandleBidPlacedInput, type NewBid } from "./onBidPlaced";
|
|
40
39
|
export { handleOrderCreate, type HandleOrderCreateInput, type NewOrder, } from "./onOrderCreate";
|
|
41
40
|
export { handleOrderStatusChange, type HandleOrderStatusChangeInput, type OrderAfter, type OrderBefore, } from "./onOrderStatusChange";
|
|
42
41
|
export { handleProductWrite, type HandleProductWriteInput, type ProductDoc, } from "./onProductWrite";
|
|
@@ -39,7 +39,6 @@ export { runAdminAnalytics } from "./adminAnalytics";
|
|
|
39
39
|
export { runTriggerEventRaffle, } from "./triggerEventRaffle";
|
|
40
40
|
export { runAssignSpinPrize, } from "./assignSpinPrize";
|
|
41
41
|
// Trigger reactors — call with the meaningful payload, no Firestore event needed
|
|
42
|
-
export { handleBidPlaced } from "./onBidPlaced";
|
|
43
42
|
export { handleOrderCreate, } from "./onOrderCreate";
|
|
44
43
|
export { handleOrderStatusChange, } from "./onOrderStatusChange";
|
|
45
44
|
export { handleProductWrite, } from "./onProductWrite";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { normalizeError } from "../../../../errors/normalize";
|
|
2
|
-
import { bidRepository, cartRepository, offerRepository } from "../../../../repositories";
|
|
2
|
+
import { bidRepository, cartRepository, offerRepository, storeRepository } from "../../../../repositories";
|
|
3
3
|
import { sendNotification } from "../../../../features/admin/actions/notification-actions";
|
|
4
4
|
export async function runOfferExpiry(ctx) {
|
|
5
5
|
ctx.logger.info("Starting offer expiry sweep");
|
|
@@ -141,6 +141,37 @@ async function lapseUnpaidAuctionWins(ctx) {
|
|
|
141
141
|
if (auctionLines.length === 0)
|
|
142
142
|
return;
|
|
143
143
|
ctx.logger.info(`Found ${auctionLines.length} unpaid auction win(s) past deadline`);
|
|
144
|
+
/**
|
|
145
|
+
* Tell the seller their item is unsold again so they can relist it or approach
|
|
146
|
+
* the runner-up. Only the buyer was ever notified, so to a seller a forfeited
|
|
147
|
+
* win looked like a completed sale that simply never paid out.
|
|
148
|
+
*
|
|
149
|
+
* Best-effort by design: the forfeiture itself (cart line cleared, bid marked
|
|
150
|
+
* forfeited) is already committed by the time this runs, and a missing store or
|
|
151
|
+
* a notification failure must not roll that back or abort the sweep.
|
|
152
|
+
*/
|
|
153
|
+
async function notifySellerOfForfeitedWin(item, jobCtx) {
|
|
154
|
+
try {
|
|
155
|
+
const store = item.storeId ? await storeRepository.findById(item.storeId) : null;
|
|
156
|
+
if (!store?.ownerId)
|
|
157
|
+
return;
|
|
158
|
+
await sendNotification({
|
|
159
|
+
userId: store.ownerId,
|
|
160
|
+
type: "auction_ended",
|
|
161
|
+
priority: "normal",
|
|
162
|
+
title: "Auction win forfeited — item unsold",
|
|
163
|
+
message: `The winning bidder for "${item.productTitle}" did not pay within the deadline, so the win was forfeited. You can relist the item or approach the next highest bidder.`,
|
|
164
|
+
relatedId: item.auctionId ?? item.productId,
|
|
165
|
+
relatedType: "product",
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
catch (sellerErr) {
|
|
169
|
+
void normalizeError(sellerErr);
|
|
170
|
+
jobCtx.logger.warn(`Failed to notify seller of forfeited win ${item.bidId}`, {
|
|
171
|
+
error: sellerErr instanceof Error ? sellerErr.message : String(sellerErr),
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
144
175
|
for (const { userId, item } of auctionLines) {
|
|
145
176
|
try {
|
|
146
177
|
await cartRepository.removeItemsByBidId(userId, item.bidId);
|
|
@@ -154,6 +185,7 @@ async function lapseUnpaidAuctionWins(ctx) {
|
|
|
154
185
|
relatedId: item.auctionId ?? item.productId,
|
|
155
186
|
relatedType: "product",
|
|
156
187
|
});
|
|
188
|
+
await notifySellerOfForfeitedWin(item, ctx);
|
|
157
189
|
}
|
|
158
190
|
catch (err) {
|
|
159
191
|
void normalizeError(err);
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* S4 batch 1: promotions, onOrderCreate, onOrderStatusChange,
|
|
6
6
|
* auctionSettlement, autoPayoutEligibility, couponExpiry, offerExpiry
|
|
7
|
-
* S5 batch 2: onReviewWrite,
|
|
7
|
+
* S5 batch 2: onReviewWrite, cartPrune, notificationPrune,
|
|
8
8
|
* dailyDataCleanup, countersReconcile, cleanupRtdbEvents
|
|
9
9
|
*/
|
|
10
10
|
export { couponExpiryHandler } from "./couponExpiry";
|
|
@@ -18,7 +18,6 @@ export { autoPayoutEligibilityHandler } from "./autoPayoutEligibility";
|
|
|
18
18
|
export { countersReconcileHandler } from "./countersReconcile";
|
|
19
19
|
export { onOrderCreateHandler } from "./onOrderCreate";
|
|
20
20
|
export { onOrderStatusChangeHandler } from "./onOrderStatusChange";
|
|
21
|
-
export { onBidPlacedHandler } from "./onBidPlaced";
|
|
22
21
|
export { onJobCreatedHandler } from "./onJobCreated";
|
|
23
22
|
export { onReviewWriteHandler } from "./onReviewWrite";
|
|
24
23
|
export { promotionsHandler, type PromotionsCallableResult } from "./promotions";
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* S4 batch 1: promotions, onOrderCreate, onOrderStatusChange,
|
|
6
6
|
* auctionSettlement, autoPayoutEligibility, couponExpiry, offerExpiry
|
|
7
|
-
* S5 batch 2: onReviewWrite,
|
|
7
|
+
* S5 batch 2: onReviewWrite, cartPrune, notificationPrune,
|
|
8
8
|
* dailyDataCleanup, countersReconcile, cleanupRtdbEvents
|
|
9
9
|
*/
|
|
10
10
|
export { couponExpiryHandler } from "./couponExpiry";
|
|
@@ -18,7 +18,6 @@ export { autoPayoutEligibilityHandler } from "./autoPayoutEligibility";
|
|
|
18
18
|
export { countersReconcileHandler } from "./countersReconcile";
|
|
19
19
|
export { onOrderCreateHandler } from "./onOrderCreate";
|
|
20
20
|
export { onOrderStatusChangeHandler } from "./onOrderStatusChange";
|
|
21
|
-
export { onBidPlacedHandler } from "./onBidPlaced";
|
|
22
21
|
export { onJobCreatedHandler } from "./onJobCreated";
|
|
23
22
|
export { onReviewWriteHandler } from "./onReviewWrite";
|
|
24
23
|
export { promotionsHandler } from "./promotions";
|
|
@@ -11,6 +11,26 @@ export type CartLane = (typeof CART_LANE)[keyof typeof CART_LANE];
|
|
|
11
11
|
*/
|
|
12
12
|
export declare const CART_LANE_PRIORITY: readonly CartLane[];
|
|
13
13
|
export declare const CART_LANE_LABELS: Record<CartLane, string>;
|
|
14
|
+
/**
|
|
15
|
+
* How long a locked line survives before it lapses, per lane.
|
|
16
|
+
*
|
|
17
|
+
* Lives here, with the lane model, because the deadline is a property OF the
|
|
18
|
+
* lane — and because `AUCTION_CHECKOUT_WINDOW_MS` was previously declared twice
|
|
19
|
+
* (`auctionSettlement.ts` and `bid-actions.ts`, i.e. the settlement path and the
|
|
20
|
+
* Buy-Now path) with no link between them: changing one would silently give the
|
|
21
|
+
* same kind of win two different deadlines depending on how it was won.
|
|
22
|
+
*
|
|
23
|
+
* The two windows are equal today but mean different things, so they stay
|
|
24
|
+
* separate names rather than one shared constant:
|
|
25
|
+
* - **auction** — payment is MANDATORY. You bid, you won, you owe it. Past the
|
|
26
|
+
* deadline the bid is forfeited and the buyer is warned that repeated
|
|
27
|
+
* non-payment restricts their account.
|
|
28
|
+
* - **offer** — payment is OPTIONAL. Declining to buy at your own offer price
|
|
29
|
+
* is the buyer's right; the offer simply lapses with no penalty.
|
|
30
|
+
*/
|
|
31
|
+
export declare const LANE_CHECKOUT_WINDOW_MS: Record<"auction" | "offer", number>;
|
|
32
|
+
/** 2 days, in hours — for buyer-facing copy ("pay within 48 hours"). */
|
|
33
|
+
export declare const LANE_CHECKOUT_WINDOW_HOURS: Record<"auction" | "offer", number>;
|
|
14
34
|
/** The two lanes whose price is fixed outside the listing. */
|
|
15
35
|
export declare function isLockedLane(lane: CartLane): boolean;
|
|
16
36
|
/** Minimal shape this module needs — works on cart documents and client rows alike. */
|
|
@@ -44,6 +44,34 @@ export const CART_LANE_LABELS = {
|
|
|
44
44
|
offer: "Accepted offers",
|
|
45
45
|
standard: "Cart",
|
|
46
46
|
};
|
|
47
|
+
/**
|
|
48
|
+
* How long a locked line survives before it lapses, per lane.
|
|
49
|
+
*
|
|
50
|
+
* Lives here, with the lane model, because the deadline is a property OF the
|
|
51
|
+
* lane — and because `AUCTION_CHECKOUT_WINDOW_MS` was previously declared twice
|
|
52
|
+
* (`auctionSettlement.ts` and `bid-actions.ts`, i.e. the settlement path and the
|
|
53
|
+
* Buy-Now path) with no link between them: changing one would silently give the
|
|
54
|
+
* same kind of win two different deadlines depending on how it was won.
|
|
55
|
+
*
|
|
56
|
+
* The two windows are equal today but mean different things, so they stay
|
|
57
|
+
* separate names rather than one shared constant:
|
|
58
|
+
* - **auction** — payment is MANDATORY. You bid, you won, you owe it. Past the
|
|
59
|
+
* deadline the bid is forfeited and the buyer is warned that repeated
|
|
60
|
+
* non-payment restricts their account.
|
|
61
|
+
* - **offer** — payment is OPTIONAL. Declining to buy at your own offer price
|
|
62
|
+
* is the buyer's right; the offer simply lapses with no penalty.
|
|
63
|
+
*/
|
|
64
|
+
export const LANE_CHECKOUT_WINDOW_MS = {
|
|
65
|
+
/** 2 days. Mandatory — enforced by the expiry sweep, which forfeits the bid. */
|
|
66
|
+
auction: 48 * 60 * 60 * 1000,
|
|
67
|
+
/** 2 days. Optional — lapses quietly, no penalty. */
|
|
68
|
+
offer: 48 * 60 * 60 * 1000,
|
|
69
|
+
};
|
|
70
|
+
/** 2 days, in hours — for buyer-facing copy ("pay within 48 hours"). */
|
|
71
|
+
export const LANE_CHECKOUT_WINDOW_HOURS = {
|
|
72
|
+
auction: LANE_CHECKOUT_WINDOW_MS.auction / (60 * 60 * 1000),
|
|
73
|
+
offer: LANE_CHECKOUT_WINDOW_MS.offer / (60 * 60 * 1000),
|
|
74
|
+
};
|
|
47
75
|
/** The two lanes whose price is fixed outside the listing. */
|
|
48
76
|
export function isLockedLane(lane) {
|
|
49
77
|
return lane === CART_LANE.AUCTION || lane === CART_LANE.OFFER;
|
|
@@ -24,3 +24,37 @@ export declare function resolveTieredBidIncrement(currentBidAmount: number, tier
|
|
|
24
24
|
* MORE than the platform tier, but can never undercut it.
|
|
25
25
|
*/
|
|
26
26
|
export declare function resolveMinBidIncrement(currentBidAmount: number, tiers: BidIncrementTier[], override?: number | null): number;
|
|
27
|
+
export interface ResolveMinBidOptions {
|
|
28
|
+
/**
|
|
29
|
+
* Whether the auction already has at least one bid. Defaults to `true`.
|
|
30
|
+
*
|
|
31
|
+
* When `false` the starting bid is itself acceptable — a seller's opening
|
|
32
|
+
* price is a price, not a floor that must be beaten. Requiring
|
|
33
|
+
* `startingBid + increment` for the very first bid (which is what this
|
|
34
|
+
* function did unconditionally before) meant a ₹100 auction could never be
|
|
35
|
+
* opened at ₹100, only at ₹110, contradicting both the "Starting bid ₹100"
|
|
36
|
+
* label shown next to the field and the eBay convention buyers expect.
|
|
37
|
+
*/
|
|
38
|
+
hasBids?: boolean;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The single definition of "the lowest amount this auction will accept next".
|
|
42
|
+
*
|
|
43
|
+
* Both the bid form and `placeBid` must agree on this exactly, or the form
|
|
44
|
+
* seeds an amount the server then rejects. Deriving it in two places is the
|
|
45
|
+
* drift shape Root Cause #30 describes, so both call this instead.
|
|
46
|
+
*
|
|
47
|
+
* `currentBidAmount` is the auction's live visible price — for an auction with
|
|
48
|
+
* no bids yet, callers pass `startingBid` (mirroring `placeBid`'s `baseBid`)
|
|
49
|
+
* together with `{ hasBids: false }`.
|
|
50
|
+
*/
|
|
51
|
+
export declare function resolveMinBid(currentBidAmount: number, tiers: BidIncrementTier[], override?: number | null, opts?: ResolveMinBidOptions): number;
|
|
52
|
+
/**
|
|
53
|
+
* Preset step multipliers offered by the bid form, as multiples of the
|
|
54
|
+
* effective minimum increment. With a ₹100 increment this renders
|
|
55
|
+
* +₹100 / +₹500 / +₹1,000; with a ₹1,000 increment, +₹1,000 / +₹5,000 /
|
|
56
|
+
* +₹10,000. Never hardcode the rupee amounts at the call site — the whole
|
|
57
|
+
* point is that the steps track the tier.
|
|
58
|
+
*/
|
|
59
|
+
export declare const BID_PRESET_MULTIPLIERS: readonly [1, 5, 10];
|
|
60
|
+
export type BidPresetMultiplier = (typeof BID_PRESET_MULTIPLIERS)[number];
|
|
@@ -12,6 +12,23 @@ export const DEFAULT_AUCTION_BID_INCREMENT_TIERS = [
|
|
|
12
12
|
{ upTo: 10000, increment: 500 },
|
|
13
13
|
{ upTo: null, increment: 1000 },
|
|
14
14
|
];
|
|
15
|
+
/**
|
|
16
|
+
* Drop malformed rows and fall back to the seed table when nothing usable is
|
|
17
|
+
* left.
|
|
18
|
+
*
|
|
19
|
+
* An empty/missing tier array does NOT mean "this auction has no increment
|
|
20
|
+
* rule" — it means the `siteSettings/global` singleton predates
|
|
21
|
+
* `auctionConfig.bidIncrementTiers`, or an admin cleared every row. Every
|
|
22
|
+
* caller reaches this via `settings.auctionConfig?.bidIncrementTiers ?? []`,
|
|
23
|
+
* so before this guard existed that case fell through to the ₹1
|
|
24
|
+
* `AUCTION_MIN_BID_INCREMENT` last-resort constant, which made the effective
|
|
25
|
+
* minimum step ₹1 on every auction on the site — that is what rendered the
|
|
26
|
+
* bid presets as "+₹1 / +₹5 / +₹10" instead of tier-derived steps.
|
|
27
|
+
*/
|
|
28
|
+
function usableTiers(tiers) {
|
|
29
|
+
const valid = (tiers ?? []).filter((t) => t && Number.isFinite(t.increment) && t.increment > 0);
|
|
30
|
+
return valid.length > 0 ? valid : DEFAULT_AUCTION_BID_INCREMENT_TIERS;
|
|
31
|
+
}
|
|
15
32
|
/**
|
|
16
33
|
* Resolve the tiered minimum bid increment for a given current bid amount.
|
|
17
34
|
* Tier lookup is inclusive of each band's upper bound — e.g. with the
|
|
@@ -19,11 +36,12 @@ export const DEFAULT_AUCTION_BID_INCREMENT_TIERS = [
|
|
|
19
36
|
* increment (the 100-1000 tier), not the 200 increment of the next band.
|
|
20
37
|
*/
|
|
21
38
|
export function resolveTieredBidIncrement(currentBidAmount, tiers) {
|
|
22
|
-
|
|
39
|
+
const table = usableTiers(tiers);
|
|
40
|
+
for (const tier of table) {
|
|
23
41
|
if (tier.upTo === null || currentBidAmount <= tier.upTo)
|
|
24
42
|
return tier.increment;
|
|
25
43
|
}
|
|
26
|
-
return
|
|
44
|
+
return table[table.length - 1]?.increment ?? AUCTION_MIN_BID_INCREMENT;
|
|
27
45
|
}
|
|
28
46
|
/**
|
|
29
47
|
* Effective minimum bid increment = max(admin tier floor, per-listing
|
|
@@ -32,5 +50,31 @@ export function resolveTieredBidIncrement(currentBidAmount, tiers) {
|
|
|
32
50
|
*/
|
|
33
51
|
export function resolveMinBidIncrement(currentBidAmount, tiers, override) {
|
|
34
52
|
const tierValue = resolveTieredBidIncrement(currentBidAmount, tiers);
|
|
35
|
-
return typeof override === "number" && override
|
|
53
|
+
return typeof override === "number" && Number.isFinite(override) && override > tierValue
|
|
54
|
+
? override
|
|
55
|
+
: tierValue;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The single definition of "the lowest amount this auction will accept next".
|
|
59
|
+
*
|
|
60
|
+
* Both the bid form and `placeBid` must agree on this exactly, or the form
|
|
61
|
+
* seeds an amount the server then rejects. Deriving it in two places is the
|
|
62
|
+
* drift shape Root Cause #30 describes, so both call this instead.
|
|
63
|
+
*
|
|
64
|
+
* `currentBidAmount` is the auction's live visible price — for an auction with
|
|
65
|
+
* no bids yet, callers pass `startingBid` (mirroring `placeBid`'s `baseBid`)
|
|
66
|
+
* together with `{ hasBids: false }`.
|
|
67
|
+
*/
|
|
68
|
+
export function resolveMinBid(currentBidAmount, tiers, override, opts) {
|
|
69
|
+
if (opts?.hasBids === false)
|
|
70
|
+
return currentBidAmount;
|
|
71
|
+
return currentBidAmount + resolveMinBidIncrement(currentBidAmount, tiers, override);
|
|
36
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* Preset step multipliers offered by the bid form, as multiples of the
|
|
75
|
+
* effective minimum increment. With a ₹100 increment this renders
|
|
76
|
+
* +₹100 / +₹500 / +₹1,000; with a ₹1,000 increment, +₹1,000 / +₹5,000 /
|
|
77
|
+
* +₹10,000. Never hardcode the rupee amounts at the call site — the whole
|
|
78
|
+
* point is that the steps track the tier.
|
|
79
|
+
*/
|
|
80
|
+
export const BID_PRESET_MULTIPLIERS = [1, 5, 10];
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
export declare const CHECKOUT_DEFAULT_COMMISSIONS: {
|
|
2
|
+
readonly platformFeePercent: 5;
|
|
3
|
+
readonly gstPercent: 18;
|
|
4
|
+
readonly minimumTransactionFee: 0;
|
|
5
|
+
readonly platformFeeMax: 10;
|
|
2
6
|
readonly codDepositPercent: 10;
|
|
3
7
|
readonly codHandlingFeeMin: 200;
|
|
4
8
|
readonly codHandlingFeePercent: 10;
|
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
export const CHECKOUT_DEFAULT_COMMISSIONS = {
|
|
2
|
+
// Platform-commission rates. Previously absent here because no checkout path
|
|
3
|
+
// computed a buyer-facing platform fee — the moment one did, a siteSettings
|
|
4
|
+
// document that failed to load would have produced NaN fees rather than a
|
|
5
|
+
// sane fallback. Mirrors the SiteSettingsDocument defaults.
|
|
6
|
+
platformFeePercent: 5,
|
|
7
|
+
gstPercent: 18,
|
|
8
|
+
minimumTransactionFee: 0,
|
|
9
|
+
platformFeeMax: 10,
|
|
2
10
|
codDepositPercent: 10,
|
|
3
11
|
codHandlingFeeMin: 200,
|
|
4
12
|
codHandlingFeePercent: 10,
|
|
@@ -18,17 +18,49 @@ export interface FeeCommissionRates {
|
|
|
18
18
|
platformFeePercent: number;
|
|
19
19
|
gstPercent: number;
|
|
20
20
|
/** Rupee floor (not paise) — matches the existing `minimumTransactionFee` doc convention. */ minimumTransactionFee?: number;
|
|
21
|
+
/**
|
|
22
|
+
* Rupee ceiling on the buyer-facing platform commission. Optional so
|
|
23
|
+
* siteSettings documents saved before this field existed keep working —
|
|
24
|
+
* falls back to DEFAULT_PLATFORM_FEE_MAX (₹10).
|
|
25
|
+
*
|
|
26
|
+
* Applies to `computeCheckoutFees` (what the BUYER pays) only. The seller-side
|
|
27
|
+
* `computePayoutDeduction` is deliberately uncapped — that is a separate
|
|
28
|
+
* commercial decision, not the same number.
|
|
29
|
+
*/ platformFeeMax?: number;
|
|
21
30
|
gatewayFeePercent?: number;
|
|
22
31
|
}
|
|
23
32
|
export interface CheckoutFees {
|
|
24
|
-
/** Platform commission in decimal rupees
|
|
33
|
+
/** Platform commission in decimal rupees, capped at `platformFeeMax`. */
|
|
25
34
|
platformFee: number;
|
|
26
|
-
/** GST on the platform fee, in decimal rupees. */
|
|
35
|
+
/** GST on the (already capped) platform fee, in decimal rupees. */
|
|
27
36
|
gstOnFee: number;
|
|
28
37
|
/** platformFee + gstOnFee, floored at minimumTransactionFee. */
|
|
29
38
|
totalFee: number;
|
|
30
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* What the buyer pays on top of the subtotal.
|
|
42
|
+
*
|
|
43
|
+
* The cap is applied to the commission BEFORE GST is computed, because GST is
|
|
44
|
+
* levied on the commission actually charged — capping afterwards would bill tax
|
|
45
|
+
* on money that was never collected.
|
|
46
|
+
*/
|
|
31
47
|
export declare function computeCheckoutFees(subtotal: number, commissions: FeeCommissionRates): CheckoutFees;
|
|
48
|
+
/**
|
|
49
|
+
* Split one checkout's platform commission across the orders it produces.
|
|
50
|
+
*
|
|
51
|
+
* The buyer is charged a single capped commission for the transaction, but the
|
|
52
|
+
* cart splits into one order per store, and each `OrderDocument` records its own
|
|
53
|
+
* `platformFee` (invoices, reconciliation). Apportioning pro-rata by group
|
|
54
|
+
* subtotal keeps those per-order figures meaningful; giving the last group the
|
|
55
|
+
* rounding remainder guarantees they sum to exactly what was charged, so the
|
|
56
|
+
* orders can never quietly total a rupee more or less than the buyer paid.
|
|
57
|
+
*
|
|
58
|
+
* @param groupSubtotals `[storeId, subtotal]` pairs, in stable order.
|
|
59
|
+
*/
|
|
60
|
+
export declare function allocateCheckoutFees(groupSubtotals: readonly (readonly [string, number])[], fees: CheckoutFees): Map<string, {
|
|
61
|
+
platformFee: number;
|
|
62
|
+
gstOnFee: number;
|
|
63
|
+
}>;
|
|
32
64
|
export interface PayoutDeduction {
|
|
33
65
|
/** Platform commission in decimal rupees. */
|
|
34
66
|
platformFee: number;
|
|
@@ -15,12 +15,56 @@
|
|
|
15
15
|
* All money amounts are in decimal rupees.
|
|
16
16
|
*/
|
|
17
17
|
import { roundRupees } from "../../../utils/number.formatter";
|
|
18
|
+
/** Buyer-facing platform commission ceiling, in decimal rupees. */
|
|
19
|
+
const DEFAULT_PLATFORM_FEE_MAX = 10;
|
|
20
|
+
/**
|
|
21
|
+
* What the buyer pays on top of the subtotal.
|
|
22
|
+
*
|
|
23
|
+
* The cap is applied to the commission BEFORE GST is computed, because GST is
|
|
24
|
+
* levied on the commission actually charged — capping afterwards would bill tax
|
|
25
|
+
* on money that was never collected.
|
|
26
|
+
*/
|
|
18
27
|
export function computeCheckoutFees(subtotal, commissions) {
|
|
19
|
-
const
|
|
28
|
+
const uncapped = roundRupees(subtotal * (commissions.platformFeePercent / 100));
|
|
29
|
+
const cap = commissions.platformFeeMax ?? DEFAULT_PLATFORM_FEE_MAX;
|
|
30
|
+
const platformFee = Math.min(uncapped, cap);
|
|
20
31
|
const gstOnFee = roundRupees(platformFee * (commissions.gstPercent / 100));
|
|
21
32
|
const totalFee = Math.max(platformFee + gstOnFee, commissions.minimumTransactionFee ?? 0);
|
|
22
33
|
return { platformFee, gstOnFee, totalFee };
|
|
23
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* Split one checkout's platform commission across the orders it produces.
|
|
37
|
+
*
|
|
38
|
+
* The buyer is charged a single capped commission for the transaction, but the
|
|
39
|
+
* cart splits into one order per store, and each `OrderDocument` records its own
|
|
40
|
+
* `platformFee` (invoices, reconciliation). Apportioning pro-rata by group
|
|
41
|
+
* subtotal keeps those per-order figures meaningful; giving the last group the
|
|
42
|
+
* rounding remainder guarantees they sum to exactly what was charged, so the
|
|
43
|
+
* orders can never quietly total a rupee more or less than the buyer paid.
|
|
44
|
+
*
|
|
45
|
+
* @param groupSubtotals `[storeId, subtotal]` pairs, in stable order.
|
|
46
|
+
*/
|
|
47
|
+
export function allocateCheckoutFees(groupSubtotals, fees) {
|
|
48
|
+
const allocation = new Map();
|
|
49
|
+
if (groupSubtotals.length === 0)
|
|
50
|
+
return allocation;
|
|
51
|
+
const total = groupSubtotals.reduce((sum, [, subtotal]) => sum + subtotal, 0);
|
|
52
|
+
let platformRemaining = fees.platformFee;
|
|
53
|
+
let gstRemaining = fees.gstOnFee;
|
|
54
|
+
groupSubtotals.forEach(([storeId, subtotal], index) => {
|
|
55
|
+
const isLast = index === groupSubtotals.length - 1;
|
|
56
|
+
// An all-zero cart can't be apportioned by share — fall back to giving the
|
|
57
|
+
// whole (necessarily zero-ish) fee to the last group rather than dividing
|
|
58
|
+
// by zero.
|
|
59
|
+
const share = total > 0 ? subtotal / total : 0;
|
|
60
|
+
const platformFee = isLast ? roundRupees(platformRemaining) : roundRupees(fees.platformFee * share);
|
|
61
|
+
const gstOnFee = isLast ? roundRupees(gstRemaining) : roundRupees(fees.gstOnFee * share);
|
|
62
|
+
platformRemaining = roundRupees(platformRemaining - platformFee);
|
|
63
|
+
gstRemaining = roundRupees(gstRemaining - gstOnFee);
|
|
64
|
+
allocation.set(storeId, { platformFee, gstOnFee });
|
|
65
|
+
});
|
|
66
|
+
return allocation;
|
|
67
|
+
}
|
|
24
68
|
export function computePayoutDeduction(grossAmount, commissions) {
|
|
25
69
|
const platformFee = roundRupees(grossAmount * (commissions.platformFeePercent / 100));
|
|
26
70
|
const gatewayFee = roundRupees(grossAmount * ((commissions.gatewayFeePercent ?? 0) / 100));
|
|
@@ -9,7 +9,7 @@ export const config = {
|
|
|
9
9
|
slugPrefix: "auction-",
|
|
10
10
|
cartLine: "blocked",
|
|
11
11
|
detailRoute: (idOrSlug) => String(ROUTES.PUBLIC.AUCTION_DETAIL(idOrSlug)),
|
|
12
|
-
badge: { label: "Auction", className: "bg-warning-
|
|
12
|
+
badge: { label: "Auction", className: "bg-warning-solid text-warning-on-solid" },
|
|
13
13
|
priceLabel: "Suggested Retail Price (₹)",
|
|
14
14
|
typeLabel: "Auction",
|
|
15
15
|
showsStockQuantity: false,
|
|
@@ -9,7 +9,7 @@ export const config = {
|
|
|
9
9
|
slugPrefix: "digitalcode-",
|
|
10
10
|
cartLine: "single-product",
|
|
11
11
|
detailRoute: (idOrSlug) => String(ROUTES.PUBLIC.DIGITAL_CODE_DETAIL(idOrSlug)),
|
|
12
|
-
badge: { label: "Digital Code", className: "bg-success-
|
|
12
|
+
badge: { label: "Digital Code", className: "bg-success-solid text-success-on-solid" },
|
|
13
13
|
priceLabel: "Price per Code (₹)",
|
|
14
14
|
typeLabel: "Digital Code",
|
|
15
15
|
showsStockQuantity: false,
|
|
@@ -9,7 +9,7 @@ export const config = {
|
|
|
9
9
|
slugPrefix: "live-",
|
|
10
10
|
cartLine: "single-product",
|
|
11
11
|
detailRoute: (idOrSlug) => String(ROUTES.PUBLIC.LIVE_DETAIL(idOrSlug)),
|
|
12
|
-
badge: { label: "Live Item", className: "bg-
|
|
12
|
+
badge: { label: "Live Item", className: "bg-error-solid text-error-on-solid" },
|
|
13
13
|
priceLabel: "Price (₹)",
|
|
14
14
|
typeLabel: "Live Item",
|
|
15
15
|
showsStockQuantity: true,
|
|
@@ -9,7 +9,7 @@ export const config = {
|
|
|
9
9
|
slugPrefix: "preorder-",
|
|
10
10
|
cartLine: "single-product",
|
|
11
11
|
detailRoute: (idOrSlug) => String(ROUTES.PUBLIC.PRE_ORDER_DETAIL(idOrSlug)),
|
|
12
|
-
badge: { label: "Pre-Order", className: "bg-info-
|
|
12
|
+
badge: { label: "Pre-Order", className: "bg-info-solid text-info-on-solid" },
|
|
13
13
|
priceLabel: "Pre-Order Price (₹)",
|
|
14
14
|
typeLabel: "Pre-Order",
|
|
15
15
|
showsStockQuantity: false,
|