@mohasinac/appkit 4.62.0 → 4.62.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/.build-stamp.json +3 -3
- package/dist/_internal/server/features/checkout/actions.d.ts +80 -0
- package/dist/_internal/server/features/checkout/actions.js +542 -26
- package/dist/_internal/server/features/invoicing/allocate.d.ts +63 -0
- package/dist/_internal/server/features/invoicing/allocate.js +162 -0
- package/dist/_internal/server/features/invoicing/credit-note.d.ts +57 -0
- package/dist/_internal/server/features/invoicing/credit-note.js +126 -0
- package/dist/_internal/server/features/invoicing/gstr1.d.ts +74 -0
- package/dist/_internal/server/features/invoicing/gstr1.js +251 -0
- package/dist/_internal/server/features/invoicing/issue.d.ts +96 -0
- package/dist/_internal/server/features/invoicing/issue.js +202 -0
- package/dist/_internal/server/features/orders/actions.d.ts +8 -1
- package/dist/_internal/server/features/orders/actions.js +81 -8
- package/dist/_internal/server/features/orders/adapters.js +33 -0
- package/dist/_internal/server/features/orders/cancellation-refund.d.ts +15 -9
- package/dist/_internal/server/features/orders/cancellation-refund.js +15 -9
- package/dist/_internal/server/features/points/award.js +46 -4
- package/dist/_internal/server/features/refunds/actions.js +26 -0
- package/dist/_internal/server/features/shipping/courier-block.d.ts +69 -0
- package/dist/_internal/server/features/shipping/courier-block.js +239 -0
- package/dist/_internal/server/features/site-settings/adapters.d.ts +1 -1
- package/dist/_internal/server/features/site-settings/adapters.js +15 -0
- package/dist/_internal/server/features/stores/adapters.d.ts +2 -2
- package/dist/_internal/server/features/stores/adapters.js +51 -0
- package/dist/_internal/server/functions/scheduled.js +38 -1
- package/dist/_internal/server/jobs/core/autoPayoutEligibility.js +9 -2
- package/dist/_internal/server/jobs/core/dailyStatusDigest.d.ts +4 -0
- package/dist/_internal/server/jobs/core/dailyStatusDigest.js +19 -2
- package/dist/_internal/server/jobs/core/index.d.ts +2 -1
- package/dist/_internal/server/jobs/core/index.js +2 -1
- package/dist/_internal/server/jobs/core/onJobCreated.d.ts +30 -7
- package/dist/_internal/server/jobs/core/onJobCreated.js +78 -45
- package/dist/_internal/server/jobs/core/onOrderStatusChange.d.ts +7 -0
- package/dist/_internal/server/jobs/core/onOrderStatusChange.js +62 -0
- package/dist/_internal/server/jobs/core/paymentWindowTimeout.js +33 -0
- package/dist/_internal/server/jobs/core/runJobOnce.d.ts +48 -0
- package/dist/_internal/server/jobs/core/runJobOnce.js +195 -0
- package/dist/_internal/server/jobs/core/weeklyPayoutEligibility.js +10 -2
- package/dist/_internal/server/jobs/handlers/onOrderStatusChange.js +17 -0
- package/dist/_internal/server/notifications/channel-health.d.ts +26 -3
- package/dist/_internal/shared/actions/action-registry.js +28 -0
- package/dist/_internal/shared/checkout/coupon-discount.d.ts +43 -1
- package/dist/_internal/shared/checkout/coupon-discount.js +26 -1
- package/dist/_internal/shared/checkout/order-math.d.ts +145 -0
- package/dist/_internal/shared/checkout/order-math.js +173 -0
- package/dist/_internal/shared/checkout/refundable-base.d.ts +25 -0
- package/dist/_internal/shared/checkout/refundable-base.js +13 -1
- package/dist/_internal/shared/checkout/split.d.ts +87 -0
- package/dist/_internal/shared/checkout/split.js +100 -0
- package/dist/_internal/shared/content/disclaimers.d.ts +88 -0
- package/dist/_internal/shared/content/disclaimers.js +217 -0
- package/dist/_internal/shared/content/published-values.d.ts +88 -0
- package/dist/_internal/shared/content/published-values.js +198 -0
- package/dist/_internal/shared/features/admin/integrations.d.ts +109 -0
- package/dist/_internal/shared/features/admin/integrations.js +253 -0
- package/dist/_internal/shared/features/orders/dispute-reasons.d.ts +113 -0
- package/dist/_internal/shared/features/orders/dispute-reasons.js +181 -0
- package/dist/_internal/shared/features/orders/refund-copy.d.ts +1 -1
- package/dist/_internal/shared/features/orders/refund-copy.js +18 -1
- package/dist/_internal/shared/features/points/accepts.d.ts +36 -0
- package/dist/_internal/shared/features/points/accepts.js +43 -0
- package/dist/_internal/shared/features/shipping/shared-purchase.d.ts +91 -0
- package/dist/_internal/shared/features/shipping/shared-purchase.js +108 -0
- package/dist/_internal/shared/features/site-settings/types.d.ts +11 -0
- package/dist/_internal/shared/fees/calculator.d.ts +33 -1
- package/dist/_internal/shared/fees/calculator.js +32 -2
- package/dist/client.d.ts +11 -0
- package/dist/client.js +30 -0
- package/dist/constants/api-endpoints.d.ts +42 -0
- package/dist/constants/api-endpoints.js +14 -0
- package/dist/constants/field-names.d.ts +1 -0
- package/dist/constants/field-names.js +5 -0
- package/dist/contracts/index.d.ts +1 -1
- package/dist/contracts/shipping.d.ts +45 -0
- package/dist/contracts/shipping.js +15 -0
- package/dist/features/about/components/AboutView.js +1 -1
- package/dist/features/account/components/BuyerAuctionsGuideView.js +4 -1
- package/dist/features/account/components/UserOrdersView.js +19 -33
- package/dist/features/admin/components/AdminCouponEditorView.js +17 -0
- package/dist/features/admin/components/AdminInvoicingView.d.ts +2 -0
- package/dist/features/admin/components/AdminInvoicingView.js +65 -0
- package/dist/features/admin/components/AdminOrderEditorView.d.ts +18 -1
- package/dist/features/admin/components/AdminOrderEditorView.js +11 -2
- package/dist/features/admin/components/AdminOrdersGuideView.js +23 -2
- package/dist/features/admin/components/AdminOrdersView.js +67 -5
- package/dist/features/admin/components/OwnerQuickActions.js +24 -2
- package/dist/features/admin/constants/filter-tabs.d.ts +6 -0
- package/dist/features/admin/constants/filter-tabs.js +6 -0
- package/dist/features/admin/constants/site-settings-tabs.d.ts +1 -1
- package/dist/features/admin/hooks/useAdminListingData.js +16 -0
- package/dist/features/admin/schemas/admin-user-form.d.ts +2 -2
- package/dist/features/admin/schemas/firestore.d.ts +30 -0
- package/dist/features/admin/schemas/firestore.js +5 -0
- package/dist/features/audit-log/schemas/firestore.d.ts +30 -0
- package/dist/features/audit-log/schemas/firestore.js +15 -0
- package/dist/features/auth/permissions/constants.js +13 -0
- package/dist/features/cart/components/CartPriceBreakdown.d.ts +13 -0
- package/dist/features/cart/components/CartPriceBreakdown.js +6 -2
- package/dist/features/cart/components/PointsTenderPicker.d.ts +61 -0
- package/dist/features/cart/components/PointsTenderPicker.js +15 -0
- package/dist/features/cart/components/StoreAddonsPicker.js +2 -1
- package/dist/features/cart/components/index.d.ts +2 -0
- package/dist/features/cart/components/index.js +1 -0
- package/dist/features/cart/repository/cart.repository.d.ts +19 -0
- package/dist/features/cart/repository/cart.repository.js +27 -0
- package/dist/features/cart/schemas/firestore.d.ts +92 -0
- package/dist/features/contact/email.d.ts +27 -10
- package/dist/features/contact/email.js +20 -3
- package/dist/features/dispute-reasons/repository/dispute-reasons.repository.d.ts +38 -0
- package/dist/features/dispute-reasons/repository/dispute-reasons.repository.js +80 -0
- package/dist/features/dispute-reasons/schemas/firestore.d.ts +64 -0
- package/dist/features/dispute-reasons/schemas/firestore.js +40 -0
- package/dist/features/faq/components/FAQAccordion.d.ts +13 -1
- package/dist/features/faq/components/FAQAccordion.js +29 -4
- package/dist/features/invoicing/schemas/firestore.d.ts +112 -0
- package/dist/features/invoicing/schemas/firestore.js +45 -0
- package/dist/features/invoicing/utils/financial-year.d.ts +43 -0
- package/dist/features/invoicing/utils/financial-year.js +53 -0
- package/dist/features/jobs/actions/enqueue-job.js +18 -1
- package/dist/features/jobs/repository/jobs.repository.d.ts +62 -1
- package/dist/features/jobs/repository/jobs.repository.js +84 -1
- package/dist/features/jobs/schemas/firestore.d.ts +80 -0
- package/dist/features/jobs/schemas/firestore.js +37 -0
- package/dist/features/lottery/schemas/zod.d.ts +6 -6
- package/dist/features/media/MediaImage.js +22 -1
- package/dist/features/media/upload/ImageUpload.js +1 -1
- package/dist/features/obligations/constants/policies.js +24 -1
- package/dist/features/orders/components/OrderPaymentSummary.js +10 -1
- package/dist/features/orders/components/OrderProvenanceCard.d.ts +8 -0
- package/dist/features/orders/components/OrderProvenanceCard.js +185 -0
- package/dist/features/orders/components/RaiseDisputeForm.d.ts +14 -0
- package/dist/features/orders/components/RaiseDisputeForm.js +111 -0
- package/dist/features/orders/components/RefundRequestView.js +19 -1
- package/dist/features/orders/components/index.d.ts +4 -0
- package/dist/features/orders/components/index.js +2 -0
- package/dist/features/orders/constants/order-lane-tabs.d.ts +69 -0
- package/dist/features/orders/constants/order-lane-tabs.js +88 -0
- package/dist/features/orders/repository/orders.repository.d.ts +57 -0
- package/dist/features/orders/repository/orders.repository.js +145 -0
- package/dist/features/orders/schemas/firestore.d.ts +196 -1
- package/dist/features/orders/schemas/index.d.ts +54 -6
- package/dist/features/orders/schemas/index.js +12 -0
- package/dist/features/orders/types/index.d.ts +26 -0
- package/dist/features/orders/utils/courier-block.d.ts +25 -0
- package/dist/features/orders/utils/courier-block.js +23 -0
- package/dist/features/payment-cases/schemas/firestore.d.ts +26 -1
- package/dist/features/payment-cases/schemas/firestore.js +1 -0
- package/dist/features/products/components/PrizeDrawDetailPageView.js +2 -2
- package/dist/features/products/components/ProductDetailPageView.d.ts +17 -1
- package/dist/features/products/components/ProductDetailPageView.js +2 -2
- package/dist/features/products/schemas/product-features.validators.d.ts +10 -10
- package/dist/features/promotions/actions/coupon-stacking.d.ts +30 -3
- package/dist/features/promotions/actions/coupon-stacking.js +21 -2
- package/dist/features/promotions/components/CouponCard.js +13 -0
- package/dist/features/promotions/constants/coupon-help.d.ts +2 -2
- package/dist/features/promotions/constants/coupon-help.js +9 -1
- package/dist/features/promotions/schemas/firestore.d.ts +24 -0
- package/dist/features/promotions/schemas/firestore.js +4 -0
- package/dist/features/promotions/types/index.d.ts +19 -1
- package/dist/features/scams/components/ScamProfileView.js +2 -2
- package/dist/features/scams/components/ScamRegistryView.js +2 -2
- package/dist/features/search/action-index/tab-entries.js +1 -1
- package/dist/features/seller/actions/seller-actions.js +33 -3
- package/dist/features/seller/components/SellerOrdersView.js +129 -27
- package/dist/features/seller/hooks/useSellerListingData.js +25 -0
- package/dist/features/shipments/schemas/validation.d.ts +4 -4
- package/dist/features/shipping-boxes/repository/shipping-boxes.repository.d.ts +43 -0
- package/dist/features/shipping-boxes/repository/shipping-boxes.repository.js +90 -0
- package/dist/features/shipping-boxes/schemas/firestore.d.ts +82 -0
- package/dist/features/shipping-boxes/schemas/firestore.js +53 -0
- package/dist/features/store-extensions/schemas/report-create-form.d.ts +2 -2
- package/dist/features/store-extensions/schemas/shipping-config-form.d.ts +2 -2
- package/dist/features/stores/components/StoreOrdersGuideView.js +6 -1
- package/dist/features/stores/schemas/firestore.d.ts +71 -0
- package/dist/features/stores/types/index.d.ts +19 -0
- package/dist/features/support/schemas/index.d.ts +6 -6
- package/dist/features/wishlist/hooks/useWishlistWithGuest.d.ts +1 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.js +14 -1
- package/dist/next/routing/route-map.d.ts +20 -0
- package/dist/next/routing/route-map.js +10 -0
- package/dist/providers/shipping-shiprocket/index.d.ts +27 -1
- package/dist/providers/shipping-shiprocket/index.js +84 -1
- package/dist/schemas/registry.d.ts +47 -18
- package/dist/schemas/sieve.d.ts +2 -2
- package/dist/server-entry.d.ts +2 -2
- package/dist/server-entry.js +8 -1
- package/dist/server.d.ts +10 -1
- package/dist/server.js +39 -1
- package/dist/styles.css +64 -80
- package/dist/tailwind-utilities.css +1 -1
- package/dist/tokens/themes/default-dark.js +1 -0
- package/dist/tokens/themes/default-light.js +1 -0
- package/dist/tokens/themes/required.d.ts +1 -1
- package/dist/tokens/themes/required.js +1 -0
- package/dist/tokens/tokens.css +15 -0
- package/dist/ui/components/Disclaimer.d.ts +19 -0
- package/dist/ui/components/Disclaimer.js +74 -0
- package/dist/ui/components/tabs-overflow.js +64 -11
- package/dist/ui/index.d.ts +2 -0
- package/dist/ui/index.js +6 -0
- package/dist/ui/rich-text/RichText.style.css +48 -79
- package/package.json +1 -1
- package/scripts/seed-cli.mjs +1831 -1822
|
@@ -32,12 +32,54 @@ export interface GroupCouponDiscountResult {
|
|
|
32
32
|
appliedDiscounts: {
|
|
33
33
|
code: string;
|
|
34
34
|
couponId?: string;
|
|
35
|
+
/**
|
|
36
|
+
* 🛑 This is NOT `CouponType`, and the two are easy to confuse now that
|
|
37
|
+
* both live in this file. This one records WHERE the discount came from —
|
|
38
|
+
* a coupon the buyer typed, a deal, or an automatic rule — and it is what
|
|
39
|
+
* the order document stores in `appliedDiscounts[]`.
|
|
40
|
+
*
|
|
41
|
+
* `CartAppliedCoupon.type` is the coupon's own kind (`percentage`,
|
|
42
|
+
* `points_earn`, …) and is what the loop below skips on. Reading one where
|
|
43
|
+
* the other is meant would compile cleanly and be wrong: every value of
|
|
44
|
+
* this union is absent from that one, so the `points_earn` skip would
|
|
45
|
+
* never fire and a points coupon would discount the order.
|
|
46
|
+
*/
|
|
35
47
|
type: "coupon" | "deal" | "auto";
|
|
36
48
|
discountAmount: number;
|
|
37
49
|
scope?: "admin" | "seller";
|
|
38
50
|
storeId?: string;
|
|
39
51
|
}[];
|
|
40
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* What the CART-level call returns: the same totals, plus the per-group
|
|
55
|
+
* breakdown.
|
|
56
|
+
*
|
|
57
|
+
* 🛑 A separate interface rather than one optional field on the result above.
|
|
58
|
+
* `computeGroupCouponDiscount` scores ONE group, so it has no per-group list
|
|
59
|
+
* to report and never will — giving it an empty `perGroup` would be a value
|
|
60
|
+
* that type-checks and means nothing, and the capture route's points tender
|
|
61
|
+
* reads this array positionally. A caller that gets `[]` where it expected one
|
|
62
|
+
* entry per group apportions every point against group zero.
|
|
63
|
+
*/
|
|
64
|
+
export interface CartCouponDiscountResult extends GroupCouponDiscountResult {
|
|
65
|
+
/**
|
|
66
|
+
* The per-group discount, in the order the groups were passed.
|
|
67
|
+
*
|
|
68
|
+
* 🛑 Surfaced rather than aggregated-and-forgotten. It was already being
|
|
69
|
+
* computed inside the loop below and then summed away — the same shape as
|
|
70
|
+
* `CheckoutPricingPreviewStore`'s per-store fees, which existed only as an
|
|
71
|
+
* aggregate until a seller card needed them.
|
|
72
|
+
*
|
|
73
|
+
* The caller that needs it is the capture route's points tender: the
|
|
74
|
+
* per-group base includes that group's own discount, and re-deriving it
|
|
75
|
+
* there would be a second copy of the apportionment rule (Root Cause #75) on
|
|
76
|
+
* the one path D37 proves gets left behind.
|
|
77
|
+
*/
|
|
78
|
+
perGroup: {
|
|
79
|
+
storeId: string | undefined;
|
|
80
|
+
couponDiscount: number;
|
|
81
|
+
}[];
|
|
82
|
+
}
|
|
41
83
|
/**
|
|
42
84
|
* Prorates every buyer-applied coupon against one seller-group's total.
|
|
43
85
|
*
|
|
@@ -70,4 +112,4 @@ export declare function computeCartCouponDiscount(appliedCoupons: CartAppliedCou
|
|
|
70
112
|
itemId: string;
|
|
71
113
|
lineTotal: number;
|
|
72
114
|
}[];
|
|
73
|
-
}[]):
|
|
115
|
+
}[]): CartCouponDiscountResult;
|
|
@@ -42,6 +42,20 @@ export function computeGroupCouponDiscount(appliedCoupons, groupLines, groupTota
|
|
|
42
42
|
let couponDiscount = 0;
|
|
43
43
|
const appliedDiscounts = [];
|
|
44
44
|
for (const coupon of appliedCoupons) {
|
|
45
|
+
/*
|
|
46
|
+
* 🛑 D51 — a `points_earn` coupon DISCOUNTS NOTHING. It awards points on
|
|
47
|
+
* the post-discount amount paid, and it carries `pointsPercent` rather than
|
|
48
|
+
* a discount value.
|
|
49
|
+
*
|
|
50
|
+
* Skipped here rather than relied on to carry `discountAmount: 0`. The
|
|
51
|
+
* apply route writes whatever `validateCouponForCart` returned, and a
|
|
52
|
+
* points coupon that ever came back with a non-zero `discountAmount` —
|
|
53
|
+
* through a mis-seeded `discount.value`, or an admin editing a coupon's
|
|
54
|
+
* type after creation — would silently discount the order AND award points.
|
|
55
|
+
* A type check cannot be bypassed by bad data; a zero cannot be relied on.
|
|
56
|
+
*/
|
|
57
|
+
if (coupon.type === "points_earn")
|
|
58
|
+
continue;
|
|
45
59
|
let couponGroupDiscount = 0;
|
|
46
60
|
const isSellerScoped = coupon.scope === "seller" && coupon.storeId;
|
|
47
61
|
if (isSellerScoped) {
|
|
@@ -93,11 +107,22 @@ export function computeCartCouponDiscount(appliedCoupons, groups) {
|
|
|
93
107
|
const cartSubtotal = groups.reduce((sum, g) => sum + g.lines.reduce((s, l) => s + l.lineTotal, 0), 0);
|
|
94
108
|
let couponDiscount = 0;
|
|
95
109
|
const appliedDiscounts = [];
|
|
110
|
+
const perGroup = [];
|
|
96
111
|
for (const group of groups) {
|
|
97
112
|
const groupTotal = group.lines.reduce((s, l) => s + l.lineTotal, 0);
|
|
98
113
|
const res = computeGroupCouponDiscount(appliedCoupons, group.lines, groupTotal, cartSubtotal, group.storeId);
|
|
99
114
|
couponDiscount += res.couponDiscount;
|
|
100
115
|
appliedDiscounts.push(...res.appliedDiscounts);
|
|
116
|
+
/*
|
|
117
|
+
* 🛑 NOT rounded here, and the aggregate below IS. Rounding each group
|
|
118
|
+
* then summing can differ from summing then rounding by a paisa per group,
|
|
119
|
+
* and the aggregate is what the gateway captures — so the group figures
|
|
120
|
+
* stay raw and the caller rounds its own composed base once, exactly as
|
|
121
|
+
* `createOrderForGroup` does with a single `roundRupees` on `orderTotal`
|
|
122
|
+
* (D42). A rounded-then-summed discount is how a total ends up a paisa
|
|
123
|
+
* short of its own lines.
|
|
124
|
+
*/
|
|
125
|
+
perGroup.push({ storeId: group.storeId, couponDiscount: res.couponDiscount });
|
|
101
126
|
}
|
|
102
|
-
return { couponDiscount: roundRupees(couponDiscount), appliedDiscounts };
|
|
127
|
+
return { couponDiscount: roundRupees(couponDiscount), appliedDiscounts, perGroup };
|
|
103
128
|
}
|
|
@@ -377,12 +377,157 @@ export declare function orderShippingReimbursement(order: ReimbursableOrderLike)
|
|
|
377
377
|
export declare function orderPointsFunded(order: {
|
|
378
378
|
pointsAwarded?: number;
|
|
379
379
|
}): number;
|
|
380
|
+
/**
|
|
381
|
+
* Points this order CONSUMED, which the accepting store also funds (D46/D47).
|
|
382
|
+
*
|
|
383
|
+
* Decision 3's other end. The earn above is borne by the originating store;
|
|
384
|
+
* the redemption is absorbed by the store that agreed to accept points
|
|
385
|
+
* (`StoreDocument.acceptsPoints`, defaulting to NOT accepted precisely so no
|
|
386
|
+
* seller is enrolled in funding discounts they never agreed to). The platform
|
|
387
|
+
* funds neither end.
|
|
388
|
+
*
|
|
389
|
+
* Reads `pointsApplied` off the order for the same reason the earn reads
|
|
390
|
+
* `pointsAwarded`: it must be what was actually spent. Recomputing the cap from
|
|
391
|
+
* today's total would deduct a figure the buyer never received the benefit of —
|
|
392
|
+
* and on a partially-cancelled order the total has moved.
|
|
393
|
+
*/
|
|
394
|
+
export declare function orderPointsRedeemed(order: {
|
|
395
|
+
pointsApplied?: number;
|
|
396
|
+
}): number;
|
|
380
397
|
/** Sum of `orderPointsFunded` across a batch. */
|
|
381
398
|
export declare function ordersPointsFunded(orders: readonly {
|
|
382
399
|
pointsAwarded?: number;
|
|
383
400
|
}[]): number;
|
|
401
|
+
/**
|
|
402
|
+
* Everything the store funds through the points programme, for one batch:
|
|
403
|
+
* points ISSUED on these orders plus points SPENT on them.
|
|
404
|
+
*
|
|
405
|
+
* ## 🛑 One figure, because they are one obligation
|
|
406
|
+
*
|
|
407
|
+
* Both ends are seller-funded (decisions 3 and 17) and both are deducted from
|
|
408
|
+
* the same payout at the same moment, so splitting them across two parameters
|
|
409
|
+
* of `computePayoutDeduction` would be two chances for a call site to pass one
|
|
410
|
+
* and forget the other — and a payout that deducts the earn but not the
|
|
411
|
+
* redemption is short by exactly the amount nobody noticed.
|
|
412
|
+
*
|
|
413
|
+
* 🛑 This helper exists because `ordersPointsFunded` had **ZERO consumers**
|
|
414
|
+
* from the day it was written — the earn deduction was built, documented at
|
|
415
|
+
* length, and never passed to the function it was built for, so every payout
|
|
416
|
+
* since has quietly been funded by the platform. That is the dead-field
|
|
417
|
+
* pattern (Rule: every declared field has both a writer and a reader) occurring
|
|
418
|
+
* inside the work that catalogues it. Wiring the combined figure at all four
|
|
419
|
+
* call sites is what closes it.
|
|
420
|
+
*/
|
|
421
|
+
export declare function ordersPointsBorneByStore(orders: readonly {
|
|
422
|
+
pointsAwarded?: number;
|
|
423
|
+
pointsApplied?: number;
|
|
424
|
+
}[]): number;
|
|
384
425
|
/** Sum of `orderShippingReimbursement` across a batch. */
|
|
385
426
|
export declare function ordersShippingReimbursement(orders: readonly ReimbursableOrderLike[]): number;
|
|
427
|
+
/**
|
|
428
|
+
* How many points may be spent on ONE order group, and what is left to charge.
|
|
429
|
+
*
|
|
430
|
+
* ## 🛑 A PIPELINE STAGE, after the final rounding — never a post-order debit
|
|
431
|
+
*
|
|
432
|
+
* This is the whole lesson of D46. The deleted code redeemed points AFTER the
|
|
433
|
+
* orders existed and the gateway had captured `orderTotal`, so a successful
|
|
434
|
+
* redemption took points from a buyer who had just paid full price: nothing in
|
|
435
|
+
* the pipeline knew about the spend, so nothing reduced the charge.
|
|
436
|
+
*
|
|
437
|
+
* So the tender is a STAGE. `amountDue = orderTotal − pointsApplied` is
|
|
438
|
+
* computed here, and `amountDue` is what the gateway captures and what the
|
|
439
|
+
* post-capture guard compares. A sixth way for the captured amount to diverge
|
|
440
|
+
* from the recorded total is the D37/D38/D40 family; this is how it is avoided.
|
|
441
|
+
*
|
|
442
|
+
* ## 🛑 Applied AFTER rounding, deliberately
|
|
443
|
+
*
|
|
444
|
+
* `orderTotal` is rounded ONCE at the end of its own nine-term sum (D42).
|
|
445
|
+
* Folding points in before that rounding would put a discount inside the sum
|
|
446
|
+
* being rounded, so the figure shown to the buyer and the figure captured could
|
|
447
|
+
* differ by a paisa. They must be provably equal, which is why `amountDue` is
|
|
448
|
+
* derived here and never recomputed at the gateway.
|
|
449
|
+
*
|
|
450
|
+
* ## Per order group, because the ACCEPTING STORE absorbs it
|
|
451
|
+
*
|
|
452
|
+
* Decision 3: a redeemed point reduces THAT store's payout for THAT order. A
|
|
453
|
+
* cart spanning an accepting and a non-accepting store must not pool the
|
|
454
|
+
* discount — each group gets its own cap and its own figure. That is the same
|
|
455
|
+
* granularity `splitCartIntoOrderGroups` and `allocateCheckoutFees` already
|
|
456
|
+
* use, and its absence is what caused Root Cause #65's per-store
|
|
457
|
+
* multiplication bug.
|
|
458
|
+
*
|
|
459
|
+
* ## All-or-nothing, and the remainder STAYS
|
|
460
|
+
*
|
|
461
|
+
* Owner's rule: *"if use point it is all and then the balance is returned if
|
|
462
|
+
* excess"*. There is no requested amount — the balance is spent down as far as
|
|
463
|
+
* the cap allows, and whatever will not fit is simply not spent. It stays in
|
|
464
|
+
* the ledger for a later order; nothing is forfeited, and no caller can ask for
|
|
465
|
+
* a partial spend.
|
|
466
|
+
*
|
|
467
|
+
* 🛑 The 50% cap is not optional, and removing it was explicitly declined.
|
|
468
|
+
* `POINTS_MAX_REDEMPTION_PERCENT` exists because a ₹0 order breaks the gateway
|
|
469
|
+
* (a ₹0 Razorpay order is not a thing) and because a balance that can settle a
|
|
470
|
+
* whole purchase behaves like the stored value the points schema is written to
|
|
471
|
+
* avoid.
|
|
472
|
+
*/
|
|
473
|
+
export declare function resolvePointsTender(input: {
|
|
474
|
+
/** The group's FINAL rounded total. */
|
|
475
|
+
orderTotal: number;
|
|
476
|
+
/** The buyer's live balance, in points (1 point = ₹1). */
|
|
477
|
+
balance: number;
|
|
478
|
+
/** Whether this group's store accepts points. */
|
|
479
|
+
accepted: boolean;
|
|
480
|
+
/** Whether the buyer ticked the cart's `useAllPoints`. */
|
|
481
|
+
optedIn: boolean;
|
|
482
|
+
}): {
|
|
483
|
+
pointsApplied: number;
|
|
484
|
+
amountDue: number;
|
|
485
|
+
};
|
|
486
|
+
/**
|
|
487
|
+
* Draw one balance down across a checkout's order groups, in group order.
|
|
488
|
+
*
|
|
489
|
+
* ## Why this exists rather than each caller looping
|
|
490
|
+
*
|
|
491
|
+
* The tender is applied PER GROUP (decision 3 — the accepting store absorbs
|
|
492
|
+
* its own redemption), so "how many points does this checkout use" is a
|
|
493
|
+
* sequential draw-down, not a single `min`. Four surfaces need the same answer
|
|
494
|
+
* and must not each write the loop:
|
|
495
|
+
*
|
|
496
|
+
* | surface | why it needs the total |
|
|
497
|
+
* |---|---|
|
|
498
|
+
* | `previewCheckoutPricing` | the figure shown to the buyer |
|
|
499
|
+
* | `POST /api/payment/create-order` | the figure CAPTURED |
|
|
500
|
+
* | the post-capture guard | the figure compared |
|
|
501
|
+
* | the two order paths | the per-group figure recorded |
|
|
502
|
+
*
|
|
503
|
+
* D37 is the record of what happens when one of those four is left out: the
|
|
504
|
+
* coupon discount reached three of them and not the capture route, so the
|
|
505
|
+
* buyer was shown one number, charged another, and the order recorded the
|
|
506
|
+
* first — invisible in our own data because the guard was one-sided.
|
|
507
|
+
*
|
|
508
|
+
* 🛑 The order paths deliberately do NOT call this. They apply
|
|
509
|
+
* `resolvePointsTender` to their own group as they create it, drawing down a
|
|
510
|
+
* shared remainder — which is the same function applied in the same order, so
|
|
511
|
+
* the sum is identical by construction. Making them call this instead would
|
|
512
|
+
* mean computing every group's final total before creating any of them, which
|
|
513
|
+
* is a restructure of two 200-line functions for an answer they already have.
|
|
514
|
+
*
|
|
515
|
+
* ## The cap is per group, and the ORDER of the groups therefore matters
|
|
516
|
+
*
|
|
517
|
+
* A balance smaller than the whole checkout's 50% allowance is consumed by the
|
|
518
|
+
* earliest groups. That is arbitrary but it must be *consistently* arbitrary:
|
|
519
|
+
* every caller iterates `orderGroups` in the order `splitCartIntoOrderGroups`
|
|
520
|
+
* produced, so all four agree. Sorting differently anywhere would make the
|
|
521
|
+
* shown and charged figures diverge on exactly the carts where the balance is
|
|
522
|
+
* tight.
|
|
523
|
+
*/
|
|
524
|
+
export declare function allocatePointsAcrossGroups(groups: readonly {
|
|
525
|
+
total: number;
|
|
526
|
+
accepted: boolean;
|
|
527
|
+
}[], balance: number, optedIn: boolean): {
|
|
528
|
+
perGroup: number[];
|
|
529
|
+
pointsApplied: number;
|
|
530
|
+
};
|
|
386
531
|
/**
|
|
387
532
|
* P-6 — pre-order groups charge each product's own `preOrderDepositPercent`
|
|
388
533
|
* (falling back to the generic COD deposit % when a product doesn't have one
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { roundRupees } from "../../../utils/number.formatter.js";
|
|
2
2
|
import { calculateGstInclusive, taxableFromInclusive } from "../fees/calculator.js";
|
|
3
|
+
/*
|
|
4
|
+
* Points: the CAP arithmetic only. `maxRedeemablePoints` lives beside
|
|
5
|
+
* `POINTS_MAX_REDEMPTION_PERCENT`, which is the constant that defines it, so
|
|
6
|
+
* importing it here keeps one owner for the rule rather than a second copy of
|
|
7
|
+
* the percentage (Root Cause #75). This file stays pure — the points SCHEMA is
|
|
8
|
+
* plain data with no Firestore import.
|
|
9
|
+
*/
|
|
10
|
+
import { maxRedeemablePoints } from "../../../features/points/schemas/firestore.js";
|
|
3
11
|
/**
|
|
4
12
|
* The single source of truth for "what do we charge for ONE COPY of this line".
|
|
5
13
|
*
|
|
@@ -593,14 +601,179 @@ export function orderPointsFunded(order) {
|
|
|
593
601
|
? roundRupees(Math.max(0, awarded))
|
|
594
602
|
: 0;
|
|
595
603
|
}
|
|
604
|
+
/**
|
|
605
|
+
* Points this order CONSUMED, which the accepting store also funds (D46/D47).
|
|
606
|
+
*
|
|
607
|
+
* Decision 3's other end. The earn above is borne by the originating store;
|
|
608
|
+
* the redemption is absorbed by the store that agreed to accept points
|
|
609
|
+
* (`StoreDocument.acceptsPoints`, defaulting to NOT accepted precisely so no
|
|
610
|
+
* seller is enrolled in funding discounts they never agreed to). The platform
|
|
611
|
+
* funds neither end.
|
|
612
|
+
*
|
|
613
|
+
* Reads `pointsApplied` off the order for the same reason the earn reads
|
|
614
|
+
* `pointsAwarded`: it must be what was actually spent. Recomputing the cap from
|
|
615
|
+
* today's total would deduct a figure the buyer never received the benefit of —
|
|
616
|
+
* and on a partially-cancelled order the total has moved.
|
|
617
|
+
*/
|
|
618
|
+
export function orderPointsRedeemed(order) {
|
|
619
|
+
const applied = order.pointsApplied;
|
|
620
|
+
return typeof applied === "number" && Number.isFinite(applied)
|
|
621
|
+
? roundRupees(Math.max(0, applied))
|
|
622
|
+
: 0;
|
|
623
|
+
}
|
|
596
624
|
/** Sum of `orderPointsFunded` across a batch. */
|
|
597
625
|
export function ordersPointsFunded(orders) {
|
|
598
626
|
return roundRupees(orders.reduce((sum, o) => sum + orderPointsFunded(o), 0));
|
|
599
627
|
}
|
|
628
|
+
/**
|
|
629
|
+
* Everything the store funds through the points programme, for one batch:
|
|
630
|
+
* points ISSUED on these orders plus points SPENT on them.
|
|
631
|
+
*
|
|
632
|
+
* ## 🛑 One figure, because they are one obligation
|
|
633
|
+
*
|
|
634
|
+
* Both ends are seller-funded (decisions 3 and 17) and both are deducted from
|
|
635
|
+
* the same payout at the same moment, so splitting them across two parameters
|
|
636
|
+
* of `computePayoutDeduction` would be two chances for a call site to pass one
|
|
637
|
+
* and forget the other — and a payout that deducts the earn but not the
|
|
638
|
+
* redemption is short by exactly the amount nobody noticed.
|
|
639
|
+
*
|
|
640
|
+
* 🛑 This helper exists because `ordersPointsFunded` had **ZERO consumers**
|
|
641
|
+
* from the day it was written — the earn deduction was built, documented at
|
|
642
|
+
* length, and never passed to the function it was built for, so every payout
|
|
643
|
+
* since has quietly been funded by the platform. That is the dead-field
|
|
644
|
+
* pattern (Rule: every declared field has both a writer and a reader) occurring
|
|
645
|
+
* inside the work that catalogues it. Wiring the combined figure at all four
|
|
646
|
+
* call sites is what closes it.
|
|
647
|
+
*/
|
|
648
|
+
export function ordersPointsBorneByStore(orders) {
|
|
649
|
+
return roundRupees(orders.reduce((sum, o) => sum + orderPointsFunded(o) + orderPointsRedeemed(o), 0));
|
|
650
|
+
}
|
|
600
651
|
/** Sum of `orderShippingReimbursement` across a batch. */
|
|
601
652
|
export function ordersShippingReimbursement(orders) {
|
|
602
653
|
return roundRupees(orders.reduce((sum, o) => sum + orderShippingReimbursement(o), 0));
|
|
603
654
|
}
|
|
655
|
+
/**
|
|
656
|
+
* How many points may be spent on ONE order group, and what is left to charge.
|
|
657
|
+
*
|
|
658
|
+
* ## 🛑 A PIPELINE STAGE, after the final rounding — never a post-order debit
|
|
659
|
+
*
|
|
660
|
+
* This is the whole lesson of D46. The deleted code redeemed points AFTER the
|
|
661
|
+
* orders existed and the gateway had captured `orderTotal`, so a successful
|
|
662
|
+
* redemption took points from a buyer who had just paid full price: nothing in
|
|
663
|
+
* the pipeline knew about the spend, so nothing reduced the charge.
|
|
664
|
+
*
|
|
665
|
+
* So the tender is a STAGE. `amountDue = orderTotal − pointsApplied` is
|
|
666
|
+
* computed here, and `amountDue` is what the gateway captures and what the
|
|
667
|
+
* post-capture guard compares. A sixth way for the captured amount to diverge
|
|
668
|
+
* from the recorded total is the D37/D38/D40 family; this is how it is avoided.
|
|
669
|
+
*
|
|
670
|
+
* ## 🛑 Applied AFTER rounding, deliberately
|
|
671
|
+
*
|
|
672
|
+
* `orderTotal` is rounded ONCE at the end of its own nine-term sum (D42).
|
|
673
|
+
* Folding points in before that rounding would put a discount inside the sum
|
|
674
|
+
* being rounded, so the figure shown to the buyer and the figure captured could
|
|
675
|
+
* differ by a paisa. They must be provably equal, which is why `amountDue` is
|
|
676
|
+
* derived here and never recomputed at the gateway.
|
|
677
|
+
*
|
|
678
|
+
* ## Per order group, because the ACCEPTING STORE absorbs it
|
|
679
|
+
*
|
|
680
|
+
* Decision 3: a redeemed point reduces THAT store's payout for THAT order. A
|
|
681
|
+
* cart spanning an accepting and a non-accepting store must not pool the
|
|
682
|
+
* discount — each group gets its own cap and its own figure. That is the same
|
|
683
|
+
* granularity `splitCartIntoOrderGroups` and `allocateCheckoutFees` already
|
|
684
|
+
* use, and its absence is what caused Root Cause #65's per-store
|
|
685
|
+
* multiplication bug.
|
|
686
|
+
*
|
|
687
|
+
* ## All-or-nothing, and the remainder STAYS
|
|
688
|
+
*
|
|
689
|
+
* Owner's rule: *"if use point it is all and then the balance is returned if
|
|
690
|
+
* excess"*. There is no requested amount — the balance is spent down as far as
|
|
691
|
+
* the cap allows, and whatever will not fit is simply not spent. It stays in
|
|
692
|
+
* the ledger for a later order; nothing is forfeited, and no caller can ask for
|
|
693
|
+
* a partial spend.
|
|
694
|
+
*
|
|
695
|
+
* 🛑 The 50% cap is not optional, and removing it was explicitly declined.
|
|
696
|
+
* `POINTS_MAX_REDEMPTION_PERCENT` exists because a ₹0 order breaks the gateway
|
|
697
|
+
* (a ₹0 Razorpay order is not a thing) and because a balance that can settle a
|
|
698
|
+
* whole purchase behaves like the stored value the points schema is written to
|
|
699
|
+
* avoid.
|
|
700
|
+
*/
|
|
701
|
+
export function resolvePointsTender(input) {
|
|
702
|
+
const total = roundRupees(Math.max(0, input.orderTotal));
|
|
703
|
+
if (!input.optedIn || !input.accepted)
|
|
704
|
+
return { pointsApplied: 0, amountDue: total };
|
|
705
|
+
const balance = Math.floor(Math.max(0, input.balance));
|
|
706
|
+
if (!Number.isFinite(balance) || balance <= 0)
|
|
707
|
+
return { pointsApplied: 0, amountDue: total };
|
|
708
|
+
/*
|
|
709
|
+
* `maxRedeemablePoints` owns the cap arithmetic — it floors to whole points
|
|
710
|
+
* and applies `POINTS_MAX_REDEMPTION_PERCENT`. Re-deriving the percentage
|
|
711
|
+
* here would be a second copy of a money rule (Root Cause #75), and the one
|
|
712
|
+
* place it already lives sits beside the constant that defines it.
|
|
713
|
+
*/
|
|
714
|
+
const pointsApplied = maxRedeemablePoints(total, balance);
|
|
715
|
+
return { pointsApplied, amountDue: roundRupees(total - pointsApplied) };
|
|
716
|
+
}
|
|
717
|
+
/**
|
|
718
|
+
* Draw one balance down across a checkout's order groups, in group order.
|
|
719
|
+
*
|
|
720
|
+
* ## Why this exists rather than each caller looping
|
|
721
|
+
*
|
|
722
|
+
* The tender is applied PER GROUP (decision 3 — the accepting store absorbs
|
|
723
|
+
* its own redemption), so "how many points does this checkout use" is a
|
|
724
|
+
* sequential draw-down, not a single `min`. Four surfaces need the same answer
|
|
725
|
+
* and must not each write the loop:
|
|
726
|
+
*
|
|
727
|
+
* | surface | why it needs the total |
|
|
728
|
+
* |---|---|
|
|
729
|
+
* | `previewCheckoutPricing` | the figure shown to the buyer |
|
|
730
|
+
* | `POST /api/payment/create-order` | the figure CAPTURED |
|
|
731
|
+
* | the post-capture guard | the figure compared |
|
|
732
|
+
* | the two order paths | the per-group figure recorded |
|
|
733
|
+
*
|
|
734
|
+
* D37 is the record of what happens when one of those four is left out: the
|
|
735
|
+
* coupon discount reached three of them and not the capture route, so the
|
|
736
|
+
* buyer was shown one number, charged another, and the order recorded the
|
|
737
|
+
* first — invisible in our own data because the guard was one-sided.
|
|
738
|
+
*
|
|
739
|
+
* 🛑 The order paths deliberately do NOT call this. They apply
|
|
740
|
+
* `resolvePointsTender` to their own group as they create it, drawing down a
|
|
741
|
+
* shared remainder — which is the same function applied in the same order, so
|
|
742
|
+
* the sum is identical by construction. Making them call this instead would
|
|
743
|
+
* mean computing every group's final total before creating any of them, which
|
|
744
|
+
* is a restructure of two 200-line functions for an answer they already have.
|
|
745
|
+
*
|
|
746
|
+
* ## The cap is per group, and the ORDER of the groups therefore matters
|
|
747
|
+
*
|
|
748
|
+
* A balance smaller than the whole checkout's 50% allowance is consumed by the
|
|
749
|
+
* earliest groups. That is arbitrary but it must be *consistently* arbitrary:
|
|
750
|
+
* every caller iterates `orderGroups` in the order `splitCartIntoOrderGroups`
|
|
751
|
+
* produced, so all four agree. Sorting differently anywhere would make the
|
|
752
|
+
* shown and charged figures diverge on exactly the carts where the balance is
|
|
753
|
+
* tight.
|
|
754
|
+
*/
|
|
755
|
+
export function allocatePointsAcrossGroups(groups, balance, optedIn) {
|
|
756
|
+
const perGroup = groups.map(() => 0);
|
|
757
|
+
if (!optedIn)
|
|
758
|
+
return { perGroup, pointsApplied: 0 };
|
|
759
|
+
let remaining = Math.floor(Math.max(0, balance));
|
|
760
|
+
if (!Number.isFinite(remaining) || remaining <= 0)
|
|
761
|
+
return { perGroup, pointsApplied: 0 };
|
|
762
|
+
let applied = 0;
|
|
763
|
+
for (let i = 0; i < groups.length; i += 1) {
|
|
764
|
+
const g = groups[i];
|
|
765
|
+
const { pointsApplied } = resolvePointsTender({
|
|
766
|
+
orderTotal: g.total,
|
|
767
|
+
balance: g.accepted ? remaining : 0,
|
|
768
|
+
accepted: g.accepted,
|
|
769
|
+
optedIn: true,
|
|
770
|
+
});
|
|
771
|
+
perGroup[i] = pointsApplied;
|
|
772
|
+
remaining -= pointsApplied;
|
|
773
|
+
applied += pointsApplied;
|
|
774
|
+
}
|
|
775
|
+
return { perGroup, pointsApplied: applied };
|
|
776
|
+
}
|
|
604
777
|
/**
|
|
605
778
|
* P-6 — pre-order groups charge each product's own `preOrderDepositPercent`
|
|
606
779
|
* (falling back to the generic COD deposit % when a product doesn't have one
|
|
@@ -36,6 +36,21 @@ export interface RefundableOrderLike {
|
|
|
36
36
|
amount: number;
|
|
37
37
|
status?: string;
|
|
38
38
|
}>;
|
|
39
|
+
/**
|
|
40
|
+
* D46/D47 — points applied at checkout, in points (1 point = ₹1).
|
|
41
|
+
*
|
|
42
|
+
* 🛑 Subtracted from the refundable base, because **a refund can only return
|
|
43
|
+
* money that was collected.** On a ₹1,000 order settled with 250 points the
|
|
44
|
+
* card was charged ₹750; refunding the full base would hand the buyer ₹1,000
|
|
45
|
+
* and then `reverseRedemptionForOrder` would hand back 250 points on top —
|
|
46
|
+
* ₹250 of real money created out of a discount.
|
|
47
|
+
*
|
|
48
|
+
* The points themselves come back as POINTS, never as cash. That is not a
|
|
49
|
+
* stylistic choice: `pointsLedger`'s header states it as a legal boundary,
|
|
50
|
+
* because paying a balance out in rupees is what makes it stored value, which
|
|
51
|
+
* in India is PPI territory with RBI authorisation attached.
|
|
52
|
+
*/
|
|
53
|
+
pointsApplied?: number;
|
|
39
54
|
}
|
|
40
55
|
/**
|
|
41
56
|
* The platform fee actually withheld on a refund: the amount CHARGED, never a
|
|
@@ -77,6 +92,16 @@ legacyGstPercent?: number): number;
|
|
|
77
92
|
* has to compare against THIS, or the largest possible refund is strictly less
|
|
78
93
|
* than `totalPrice` and **an order can never reach `status: "refunded"`** —
|
|
79
94
|
* which is what the old `amount >= totalPrice` test guaranteed.
|
|
95
|
+
*
|
|
96
|
+
* 🛑 And `pointsApplied` comes off too (D46/D47). A refund returns MONEY, and
|
|
97
|
+
* the money collected was `totalPrice − pointsApplied`; the points go back
|
|
98
|
+
* through `reverseRedemptionForOrder` as points. Leaving them in the base would
|
|
99
|
+
* refund them twice — once in rupees here and once in points there — which is
|
|
100
|
+
* the one way this feature could manufacture real money from a discount.
|
|
101
|
+
*
|
|
102
|
+
* Absent means zero, so every order written before the tender existed is
|
|
103
|
+
* unaffected, and `isFull` still reaches the exhausted state on them exactly as
|
|
104
|
+
* before.
|
|
80
105
|
*/
|
|
81
106
|
export declare function refundableBase(order: RefundableOrderLike, legacyGstPercent?: number): number;
|
|
82
107
|
/**
|
|
@@ -75,9 +75,21 @@ legacyGstPercent) {
|
|
|
75
75
|
* has to compare against THIS, or the largest possible refund is strictly less
|
|
76
76
|
* than `totalPrice` and **an order can never reach `status: "refunded"`** —
|
|
77
77
|
* which is what the old `amount >= totalPrice` test guaranteed.
|
|
78
|
+
*
|
|
79
|
+
* 🛑 And `pointsApplied` comes off too (D46/D47). A refund returns MONEY, and
|
|
80
|
+
* the money collected was `totalPrice − pointsApplied`; the points go back
|
|
81
|
+
* through `reverseRedemptionForOrder` as points. Leaving them in the base would
|
|
82
|
+
* refund them twice — once in rupees here and once in points there — which is
|
|
83
|
+
* the one way this feature could manufacture real money from a discount.
|
|
84
|
+
*
|
|
85
|
+
* Absent means zero, so every order written before the tender existed is
|
|
86
|
+
* unaffected, and `isFull` still reaches the exhausted state on them exactly as
|
|
87
|
+
* before.
|
|
78
88
|
*/
|
|
79
89
|
export function refundableBase(order, legacyGstPercent) {
|
|
80
|
-
return roundRupees(Math.max(0, order.totalPrice -
|
|
90
|
+
return roundRupees(Math.max(0, order.totalPrice -
|
|
91
|
+
platformFeeWithheld(order, legacyGstPercent) -
|
|
92
|
+
Math.max(0, order.pointsApplied ?? 0)));
|
|
81
93
|
}
|
|
82
94
|
/**
|
|
83
95
|
* Sum of refunds that have COMMITTED money, i.e. everything not `failed`.
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Group-purchase contribution splits — the ONE owner of the invariant.
|
|
3
|
+
*
|
|
4
|
+
* A coordinator divides what a grouped line costs among its members. They may
|
|
5
|
+
* type absolute amounts or percentages, and the total may come out at or ABOVE
|
|
6
|
+
* the landed cost; the excess belongs to the seller.
|
|
7
|
+
*
|
|
8
|
+
* ## The invariant
|
|
9
|
+
*
|
|
10
|
+
* ```
|
|
11
|
+
* landedCost = goods + shipping + fees + add-ons (per order group)
|
|
12
|
+
*
|
|
13
|
+
* contribution(member) = percent × landedCost OR a typed amount
|
|
14
|
+
* require: Σ(contributions) ≥ landedCost
|
|
15
|
+
* surplus: Σ(contributions) − landedCost
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* Percentages summing above 100% and typed amounts summing above the landed
|
|
19
|
+
* cost are **the same case**, so one rule covers both and there is no
|
|
20
|
+
* mode-specific branch to get wrong.
|
|
21
|
+
*
|
|
22
|
+
* ## Why the surplus becomes `lockedPrice`
|
|
23
|
+
*
|
|
24
|
+
* `unitPriceFor` already honours `lockedPrice` ahead of everything else — that
|
|
25
|
+
* is how an accepted offer BELOW list is charged correctly. An agreed total
|
|
26
|
+
* ABOVE list is the same mechanism in the other direction, so GST is charged on
|
|
27
|
+
* what was actually paid, the payout base rises by the surplus,
|
|
28
|
+
* `refundableBase` ceilings at the agreed amount, and the invoice needs no new
|
|
29
|
+
* line. No new money term anywhere.
|
|
30
|
+
*/
|
|
31
|
+
/** A member's share, as the coordinator expressed it. */
|
|
32
|
+
export interface SplitContribution {
|
|
33
|
+
productId: string;
|
|
34
|
+
/** `"percent"` reads `percent`; `"amount"` reads `amount`. */
|
|
35
|
+
splitMode: "amount" | "percent";
|
|
36
|
+
/** 0–100. Required when `splitMode === "percent"`. */
|
|
37
|
+
percent?: number;
|
|
38
|
+
/** Decimal rupees. Required when `splitMode === "amount"`. */
|
|
39
|
+
amount?: number;
|
|
40
|
+
}
|
|
41
|
+
export interface SplitResolution {
|
|
42
|
+
/** Per member, in input order, after rounding. */
|
|
43
|
+
resolved: Array<{
|
|
44
|
+
productId: string;
|
|
45
|
+
amount: number;
|
|
46
|
+
}>;
|
|
47
|
+
/** What the members together put in. */
|
|
48
|
+
contributed: number;
|
|
49
|
+
/** `contributed − landedCost`, never negative (see `shortfall`). */
|
|
50
|
+
surplus: number;
|
|
51
|
+
/** `landedCost − contributed` when the split is short, else 0. */
|
|
52
|
+
shortfall: number;
|
|
53
|
+
/** The invariant holds. A caller must refuse to commit when false. */
|
|
54
|
+
valid: boolean;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Resolve contributions against a landed cost.
|
|
58
|
+
*
|
|
59
|
+
* Pure. No I/O, no clock — the landed cost is computed by the caller from the
|
|
60
|
+
* order group it already has.
|
|
61
|
+
*
|
|
62
|
+
* 🛑 ROUNDS ONCE, at the end of each member's amount, and then puts the
|
|
63
|
+
* ROUNDING REMAINDER on the last member. Rounding each percentage
|
|
64
|
+
* independently and summing is how a 3-way 33.33% split charges a rupee less
|
|
65
|
+
* than the agreed total and the surplus silently goes negative — the same
|
|
66
|
+
* remainder discipline `allocateAcrossMembers` uses for bundle tax.
|
|
67
|
+
*/
|
|
68
|
+
export declare function resolveSplit(contributions: readonly SplitContribution[], landedCost: number): SplitResolution;
|
|
69
|
+
/**
|
|
70
|
+
* The price to lock on the line, given the goods subtotal and a surplus.
|
|
71
|
+
*
|
|
72
|
+
* ## 🛑 THE TRAP THIS FUNCTION EXISTS TO PREVENT
|
|
73
|
+
*
|
|
74
|
+
* The percentage BASE is the landed cost — goods **plus shipping plus fees** —
|
|
75
|
+
* because that is what the coordinator is really dividing. But `lockedPrice` is
|
|
76
|
+
* the GOODS price of the line: the checkout adds shipping, the platform fee,
|
|
77
|
+
* GST and the add-ons on top of it, independently.
|
|
78
|
+
*
|
|
79
|
+
* So locking the landed cost would charge shipping and every fee **TWICE** —
|
|
80
|
+
* once inside `lockedPrice` and once again as the checkout's own terms. The
|
|
81
|
+
* surplus rides on the goods price alone.
|
|
82
|
+
*
|
|
83
|
+
* That is the whole reason this is a named function rather than an expression
|
|
84
|
+
* at a call site: `goods + surplus` and `landedCost + surplus` are both
|
|
85
|
+
* plausible-looking one-liners and only one of them is a correct charge.
|
|
86
|
+
*/
|
|
87
|
+
export declare function lockedPriceWithSurplus(goodsSubtotal: number, surplus: number): number;
|