@mohasinac/appkit 4.11.2 → 4.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_internal/server/features/checkout/actions.d.ts +40 -3
- package/dist/_internal/server/features/checkout/actions.js +232 -47
- package/dist/_internal/server/features/checkout/data.d.ts +10 -0
- package/dist/_internal/server/features/orders/adapters.js +20 -0
- package/dist/_internal/server/features/products/data.d.ts +12 -3
- package/dist/_internal/server/features/products/data.js +55 -4
- package/dist/_internal/server/features/products/index.d.ts +1 -1
- package/dist/_internal/server/features/products/index.js +1 -1
- package/dist/_internal/server/features/products/list-public.d.ts +47 -0
- package/dist/_internal/server/features/products/list-public.js +131 -6
- package/dist/_internal/server/features/reviews/data.d.ts +1 -1
- package/dist/_internal/server/features/reviews/data.js +3 -2
- package/dist/_internal/server/features/tester/visibility.js +3 -3
- package/dist/_internal/server/jobs/core/auctionSettlement.js +5 -2
- package/dist/_internal/server/jobs/core/testerSandboxCleanup.d.ts +1 -1
- package/dist/_internal/server/jobs/core/testerSandboxCleanup.js +4 -1
- package/dist/_internal/server/jobs/core/testerSandboxRefresh.js +2 -0
- package/dist/_internal/server/jobs/handlers/_helpers.js +4 -1
- package/dist/_internal/shared/checkout/lanes.d.ts +20 -0
- package/dist/_internal/shared/checkout/lanes.js +28 -0
- 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/features/promotions/schema.d.ts +0 -16
- package/dist/_internal/shared/features/promotions/schema.js +0 -2
- package/dist/_internal/shared/features/reviews/config.d.ts +8 -1
- package/dist/_internal/shared/features/reviews/config.js +8 -1
- 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/_registry.d.ts +88 -1
- package/dist/_internal/shared/listing-types/_registry.js +63 -1
- package/dist/_internal/shared/listing-types/art/config.d.ts +59 -0
- package/dist/_internal/shared/listing-types/art/config.js +12 -0
- package/dist/_internal/shared/listing-types/auction/config.d.ts +53 -0
- package/dist/_internal/shared/listing-types/auction/config.js +17 -0
- package/dist/_internal/shared/listing-types/classified/config.d.ts +59 -0
- package/dist/_internal/shared/listing-types/classified/config.js +14 -0
- package/dist/_internal/shared/listing-types/digital-code/config.d.ts +59 -0
- package/dist/_internal/shared/listing-types/digital-code/config.js +12 -0
- package/dist/_internal/shared/listing-types/live/config.d.ts +59 -0
- package/dist/_internal/shared/listing-types/live/config.js +14 -0
- package/dist/_internal/shared/listing-types/pre-order/config.d.ts +110 -0
- package/dist/_internal/shared/listing-types/pre-order/config.js +14 -0
- package/dist/_internal/shared/listing-types/prize-draw/config.d.ts +44 -0
- package/dist/_internal/shared/listing-types/prize-draw/config.js +10 -0
- package/dist/_internal/shared/listing-types/standard/config.d.ts +59 -0
- package/dist/_internal/shared/listing-types/standard/config.js +9 -0
- package/dist/_internal/shared/listing-types/stickers/config.d.ts +59 -0
- package/dist/_internal/shared/listing-types/stickers/config.js +12 -0
- package/dist/client.d.ts +12 -2
- package/dist/client.js +8 -2
- package/dist/constants/api-endpoints.d.ts +6 -0
- package/dist/constants/api-endpoints.js +2 -0
- package/dist/constants/field-names.d.ts +12 -0
- package/dist/constants/field-names.js +12 -0
- package/dist/constants/table-keys.d.ts +2 -0
- package/dist/constants/table-keys.js +2 -0
- package/dist/features/account/components/UserOrdersView.js +27 -5
- package/dist/features/admin/components/AdminAddressesView.js +21 -11
- 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/AdminCouponEditorView.js +1 -4
- 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/AdminOrderEditorView.d.ts +8 -1
- package/dist/features/admin/components/AdminOrderEditorView.js +4 -3
- package/dist/features/admin/components/AdminOrdersView.js +14 -1
- package/dist/features/admin/components/AdminPaymentMethodsView.js +18 -11
- package/dist/features/admin/components/AdminProductsView.js +34 -18
- 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/constants/filter-tabs.d.ts +26 -9
- package/dist/features/admin/constants/filter-tabs.js +22 -9
- 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 +2 -2
- package/dist/features/auctions/components/AuctionBidsTable.js +1 -1
- package/dist/features/auctions/components/AuctionBottomActions.js +1 -0
- package/dist/features/auctions/components/AuctionDetailPageView.js +13 -11
- 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 -2
- package/dist/features/categories/components/BrandDetailTabs.js +31 -11
- package/dist/features/categories/components/CategoryDetailTabs.js +27 -7
- package/dist/features/categories/components/CategoryProductsListing.d.ts +9 -1
- package/dist/features/categories/components/CategoryProductsListing.js +5 -2
- package/dist/features/classified/components/ClassifiedDetailPageView.js +2 -1
- package/dist/features/contact/email.js +5 -1
- package/dist/features/digital-codes/components/DigitalCodeDetailPageView.js +17 -16
- package/dist/features/homepage/components/FeaturedResultsSection.js +1 -1
- package/dist/features/homepage/components/SectionCarousel.js +1 -1
- package/dist/features/homepage/components/ShopByCategorySection.js +1 -1
- package/dist/features/layout/AppLayoutShell.js +9 -2
- package/dist/features/layout/BottomActions.js +62 -6
- package/dist/features/layout/BottomActionsContext.d.ts +26 -0
- package/dist/features/layout/BottomActionsContext.js +18 -1
- package/dist/features/layout/hooks/useBottomActions.d.ts +12 -1
- package/dist/features/layout/hooks/useBottomActions.js +14 -1
- package/dist/features/layout/index.d.ts +1 -1
- package/dist/features/live/components/LiveItemDetailPageView.js +7 -6
- 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 +36 -0
- package/dist/features/orders/repository/orders.repository.js +18 -0
- package/dist/features/orders/types/index.d.ts +38 -0
- package/dist/features/pre-orders/components/PreOrderBottomActions.d.ts +1 -1
- package/dist/features/pre-orders/components/PreOrderBottomActions.js +3 -19
- package/dist/features/products/components/ArtStickersListView.js +5 -4
- package/dist/features/products/components/AuctionsIndexListing.js +1 -2
- package/dist/features/products/components/ListingBottomActions.d.ts +28 -0
- package/dist/features/products/components/ListingBottomActions.js +25 -0
- package/dist/features/products/components/PrizeDrawBottomActions.d.ts +1 -1
- package/dist/features/products/components/PrizeDrawBottomActions.js +3 -21
- package/dist/features/products/components/PrizeDrawsListingView.js +18 -33
- package/dist/features/products/components/ProductDetailActions.js +4 -0
- package/dist/features/products/components/ProductDetailPageView.js +7 -7
- 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/constants/listing-tabs.d.ts +43 -198
- package/dist/features/products/constants/listing-tabs.js +71 -58
- package/dist/features/products/constants/sieve.d.ts +104 -1
- package/dist/features/products/constants/sieve.js +27 -7
- package/dist/features/products/hooks/useProducts.js +14 -0
- package/dist/features/products/repository/products.repository.d.ts +45 -1
- package/dist/features/products/repository/products.repository.js +26 -3
- package/dist/features/products/types/index.d.ts +15 -0
- package/dist/features/products/utils/listing-badge-variant.d.ts +20 -1
- package/dist/features/products/utils/listing-badge-variant.js +26 -1
- package/dist/features/products/utils/listing-type.d.ts +24 -0
- package/dist/features/products/utils/listing-type.js +48 -0
- package/dist/features/promotions/actions/coupon-actions.d.ts +7 -0
- package/dist/features/promotions/actions/coupon-actions.js +12 -1
- package/dist/features/promotions/actions/coupon-stacking.d.ts +24 -0
- package/dist/features/promotions/actions/coupon-stacking.js +21 -0
- package/dist/features/promotions/actions/index.d.ts +1 -0
- package/dist/features/promotions/actions/index.js +1 -0
- package/dist/features/promotions/actions/seller-coupon-actions.d.ts +0 -1
- package/dist/features/promotions/actions/seller-coupon-actions.js +0 -1
- package/dist/features/promotions/components/CouponHelpDetails.d.ts +20 -0
- package/dist/features/promotions/components/CouponHelpDetails.js +17 -0
- package/dist/features/promotions/components/CouponsIndexListing.js +2 -1
- package/dist/features/promotions/components/index.d.ts +2 -0
- package/dist/features/promotions/components/index.js +1 -0
- package/dist/features/promotions/constants/coupon-help.d.ts +27 -0
- package/dist/features/promotions/constants/coupon-help.js +42 -0
- package/dist/features/promotions/repository/coupons.repository.d.ts +10 -1
- package/dist/features/promotions/repository/coupons.repository.js +22 -8
- package/dist/features/promotions/schemas/firestore.d.ts +0 -1
- package/dist/features/promotions/schemas/firestore.js +1 -1
- package/dist/features/promotions/schemas/index.d.ts +0 -20
- package/dist/features/promotions/schemas/index.js +0 -1
- package/dist/features/reviews/actions/review-actions.js +5 -1
- package/dist/features/reviews/components/ReviewDetailPageView.js +2 -1
- package/dist/features/reviews/components/ReviewFilters.d.ts +7 -1
- package/dist/features/reviews/components/ReviewFilters.js +4 -3
- package/dist/features/reviews/components/ReviewsIndexListing.d.ts +6 -0
- package/dist/features/reviews/components/ReviewsIndexListing.js +9 -109
- package/dist/features/reviews/components/ReviewsListingPanel.d.ts +41 -0
- package/dist/features/reviews/components/ReviewsListingPanel.js +226 -0
- package/dist/features/reviews/components/index.d.ts +2 -0
- package/dist/features/reviews/components/index.js +1 -0
- package/dist/features/reviews/hooks/useReviews.d.ts +8 -0
- package/dist/features/reviews/hooks/useReviews.js +14 -6
- package/dist/features/reviews/repository/reviews.repository.d.ts +29 -1
- package/dist/features/reviews/repository/reviews.repository.js +77 -41
- package/dist/features/reviews/types/index.d.ts +8 -1
- package/dist/features/seller/components/SellerOrdersView.js +2 -1
- package/dist/features/seller/components/SellerProductsCards.js +2 -2
- package/dist/features/seller/components/SellerProductsView.js +21 -18
- package/dist/features/stores/actions/store-query-actions.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/StoreAuctionsListing.js +5 -11
- 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/StorePreOrdersListing.js +6 -11
- 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/stores/components/StoreReviewsListing.d.ts +4 -0
- package/dist/features/stores/components/StoreReviewsListing.js +7 -108
- package/dist/features/stores/schemas/index.d.ts +2 -2
- package/dist/features/tester/seed-data/coupons-tester-seed-data.js +3 -3
- package/dist/features/tester/seed-data/index.d.ts +1 -0
- package/dist/features/tester/seed-data/index.js +1 -0
- package/dist/features/tester/seed-data/offers-tester-seed-data.d.ts +2 -0
- package/dist/features/tester/seed-data/offers-tester-seed-data.js +61 -0
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +939 -1
- package/dist/index.d.ts +9 -3
- package/dist/index.js +11 -2
- package/dist/next/routing/route-map.d.ts +2 -0
- package/dist/next/routing/route-map.js +5 -0
- package/dist/react/hooks/useListingTypeFlags.d.ts +12 -19
- package/dist/react/hooks/useListingTypeFlags.js +4 -9
- package/dist/schemas/registry.d.ts +0 -12
- package/dist/seed/claimed-coupons-seed-data.js +3 -3
- package/dist/seed/coupons-seed-data.js +29 -10
- package/dist/seed/faq-seed-data.js +2 -2
- package/dist/seed/manifest.js +2 -2
- package/dist/seed/reviews-seed-data.js +52 -0
- package/dist/server-entry.d.ts +1 -0
- package/dist/server-entry.js +1 -0
- package/dist/styles.css +55 -39
- package/dist/tailwind-utilities.css +1 -1
- 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/FilterChipGroup.d.ts +21 -3
- package/dist/ui/components/FilterChipGroup.js +33 -4
- package/dist/ui/components/HorizontalScroller.d.ts +1 -2
- package/dist/ui/components/HorizontalScroller.js +9 -5
- package/dist/ui/components/HorizontalScroller.style.css +43 -38
- package/package.json +1 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type CartLane } from "../../../shared/checkout/lanes";
|
|
1
2
|
import type { OutOfStockPolicy } from "../../../../features/orders/schemas/index";
|
|
2
3
|
import { type CheckoutPaymentMethod } from "../../../shared/features/checkout/config";
|
|
3
4
|
import { type CheckoutOrderResult } from "./data";
|
|
@@ -44,6 +45,10 @@ export declare function resolveShippingCost(storeId: string | undefined): Promis
|
|
|
44
45
|
storeEmiEnabled: boolean;
|
|
45
46
|
storeState: string | undefined;
|
|
46
47
|
}>;
|
|
48
|
+
export interface DroppedCoupon {
|
|
49
|
+
code: string;
|
|
50
|
+
reason: string;
|
|
51
|
+
}
|
|
47
52
|
/**
|
|
48
53
|
* Place order(s) from the user's cart in a single Firestore transaction.
|
|
49
54
|
*
|
|
@@ -57,9 +62,29 @@ export interface CheckoutPricingPreviewInput {
|
|
|
57
62
|
addressId?: string;
|
|
58
63
|
paymentMethod: CheckoutPaymentMethod;
|
|
59
64
|
excludedProductIds?: string[];
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
65
|
+
/**
|
|
66
|
+
* Which cart lane to price. Only one lane is payable in a single checkout, so
|
|
67
|
+
* a preview spanning lanes would be a number the buyer can never be charged.
|
|
68
|
+
*
|
|
69
|
+
* Passed in rather than derived from `activeLane` server-side because the cart
|
|
70
|
+
* page previews whichever tab the buyer is looking at — including a lane that
|
|
71
|
+
* is currently gated off.
|
|
72
|
+
*/
|
|
73
|
+
lane?: CartLane;
|
|
74
|
+
}
|
|
75
|
+
/** One store's slice of the preview — what that seller's order will cost. */
|
|
76
|
+
export interface CheckoutPricingPreviewStore {
|
|
77
|
+
storeId: string;
|
|
78
|
+
storeName: string;
|
|
79
|
+
subtotal: number;
|
|
80
|
+
shippingFee: number;
|
|
81
|
+
codHandlingFee: number;
|
|
82
|
+
whatsappNotifyFee: number;
|
|
83
|
+
giftWrapFee: number;
|
|
84
|
+
shipmentProtectionFee: number;
|
|
85
|
+
gstAmount: number;
|
|
86
|
+
couponDiscount: number;
|
|
87
|
+
total: number;
|
|
63
88
|
}
|
|
64
89
|
export interface CheckoutPricingPreview {
|
|
65
90
|
subtotal: number;
|
|
@@ -70,7 +95,19 @@ export interface CheckoutPricingPreview {
|
|
|
70
95
|
shipmentProtectionFee: number;
|
|
71
96
|
gstAmount: number;
|
|
72
97
|
couponDiscount: number;
|
|
98
|
+
/** Buyer-facing platform commission, capped, charged once for this checkout. */
|
|
99
|
+
platformFee: number;
|
|
100
|
+
/** GST on the (capped) platform commission. */
|
|
101
|
+
gstOnFee: number;
|
|
73
102
|
total: number;
|
|
103
|
+
/**
|
|
104
|
+
* Per-store breakdown, in group order. Every figure here was already being
|
|
105
|
+
* computed inside the per-group loop and then discarded into an aggregate —
|
|
106
|
+
* surfacing it is what lets each seller card show its own fees.
|
|
107
|
+
*
|
|
108
|
+
* A store with no selected items forms no group and therefore never appears.
|
|
109
|
+
*/
|
|
110
|
+
stores: CheckoutPricingPreviewStore[];
|
|
74
111
|
}
|
|
75
112
|
/**
|
|
76
113
|
* Read-only "what will I actually be charged" preview for the checkout
|
|
@@ -17,9 +17,10 @@ import { computePreOrderDepositAmount, unitPriceFor } from "../../../shared/chec
|
|
|
17
17
|
import { failedCheckoutRepository } from "../../../../features/checkout/repository/failed-checkout.repository";
|
|
18
18
|
import { sendOrderConfirmationEmail } from "../../../../features/contact/server";
|
|
19
19
|
import { splitCartIntoOrderGroups } from "../../../../features/orders/index";
|
|
20
|
+
import { resolveCouponCategorySlugs } from "../../../../features/promotions/actions/coupon-actions";
|
|
20
21
|
import { assertCheckoutLane, assertLockedLinesStillValid, finalizeLockedLines, } from "./locked-lines";
|
|
21
22
|
import { activeLane, laneOf } from "../../../shared/checkout/lanes";
|
|
22
|
-
import { computeCodHandlingFee, computeWhatsAppNotifyFee, computeGiftWrapFee, computeShipmentProtectionFee, } from "../../../shared/fees/calculator";
|
|
23
|
+
import { computeCodHandlingFee, computeWhatsAppNotifyFee, computeGiftWrapFee, computeShipmentProtectionFee, computeCheckoutFees, allocateCheckoutFees, } from "../../../shared/fees/calculator";
|
|
23
24
|
import { getAdminDb, getAdminRealtimeDb, RTDB_PATHS, } from "../../../../providers/db-firebase";
|
|
24
25
|
import { PRODUCT_COLLECTION, PRODUCT_CODES_SUBCOLLECTION } from "../../../../features/products/schemas/firestore";
|
|
25
26
|
import { CART_COLLECTION } from "../../../../features/cart/schemas/index";
|
|
@@ -238,6 +239,90 @@ function buildStockUpdatePayload(product, qtyDelta) {
|
|
|
238
239
|
...rule.stockDecrementExtras(product, qtyDelta),
|
|
239
240
|
};
|
|
240
241
|
}
|
|
242
|
+
/**
|
|
243
|
+
* Re-checks every coupon persisted on the cart against the items actually
|
|
244
|
+
* being ordered, immediately before pricing.
|
|
245
|
+
*
|
|
246
|
+
* A cart can sit for days, so the `discountAmount` frozen onto
|
|
247
|
+
* `cart.appliedCoupons` at apply-time may since have become wrong or
|
|
248
|
+
* unearned — the coupon may have expired, hit its total/per-user limit, been
|
|
249
|
+
* deactivated, or had its eligible items removed from the cart. Trusting the
|
|
250
|
+
* stored amount let all of those redeem anyway.
|
|
251
|
+
*
|
|
252
|
+
* Invalid coupons are DROPPED (and reported back to the caller) rather than
|
|
253
|
+
* failing the whole checkout: the buyer is on the payment step and cannot fix
|
|
254
|
+
* a lapsed coupon from there, so killing an otherwise-valid order is hostile.
|
|
255
|
+
*
|
|
256
|
+
* One parallel round of reads for at most a handful of coupons (the stacking
|
|
257
|
+
* rule caps it at one per store plus one platform-wide), so this stays inside
|
|
258
|
+
* the Vercel Hobby round-trip budget.
|
|
259
|
+
*/
|
|
260
|
+
async function revalidateAppliedCoupons(appliedCoupons, available, uid) {
|
|
261
|
+
if (appliedCoupons.length === 0)
|
|
262
|
+
return { coupons: [], dropped: [] };
|
|
263
|
+
const cartItems = available.map(({ item, product }) => ({
|
|
264
|
+
productId: item.productId,
|
|
265
|
+
storeId: item.storeId,
|
|
266
|
+
price: unitPriceFor(item, product),
|
|
267
|
+
quantity: item.quantity,
|
|
268
|
+
listingType: (item.listingType ?? "standard"),
|
|
269
|
+
}));
|
|
270
|
+
const results = await Promise.all(appliedCoupons.map(async (coupon) => {
|
|
271
|
+
try {
|
|
272
|
+
const check = await couponsRepository.validateCouponForCart(coupon.code, uid, cartItems, { resolveCategorySlugs: resolveCouponCategorySlugs });
|
|
273
|
+
if (!check.valid) {
|
|
274
|
+
return {
|
|
275
|
+
dropped: {
|
|
276
|
+
code: coupon.code,
|
|
277
|
+
reason: check.message ?? "This coupon is no longer valid",
|
|
278
|
+
},
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
// Re-derive the amount and the eligible lines from the CURRENT cart —
|
|
282
|
+
// the stored values were computed against whatever was in the cart at
|
|
283
|
+
// apply-time.
|
|
284
|
+
const applicableItemIds = check.eligibleProductIds
|
|
285
|
+
? available
|
|
286
|
+
.filter(({ item }) => check.eligibleProductIds.includes(item.productId))
|
|
287
|
+
.map(({ item }) => item.itemId)
|
|
288
|
+
: undefined;
|
|
289
|
+
return {
|
|
290
|
+
coupon: {
|
|
291
|
+
...coupon,
|
|
292
|
+
discountAmount: check.discountAmount ?? 0,
|
|
293
|
+
scope: check.scope ?? coupon.scope,
|
|
294
|
+
storeId: check.storeId ?? coupon.storeId,
|
|
295
|
+
applicableItemIds,
|
|
296
|
+
},
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
catch (err) {
|
|
300
|
+
void normalizeError(err);
|
|
301
|
+
// A lookup failure must not silently grant the discount.
|
|
302
|
+
return {
|
|
303
|
+
dropped: {
|
|
304
|
+
code: coupon.code,
|
|
305
|
+
reason: "This coupon could not be verified",
|
|
306
|
+
},
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
}));
|
|
310
|
+
const coupons = [];
|
|
311
|
+
const dropped = [];
|
|
312
|
+
for (const r of results) {
|
|
313
|
+
if (r.coupon)
|
|
314
|
+
coupons.push(r.coupon);
|
|
315
|
+
else if (r.dropped)
|
|
316
|
+
dropped.push(r.dropped);
|
|
317
|
+
}
|
|
318
|
+
if (dropped.length > 0) {
|
|
319
|
+
serverLogger.info("Dropped invalid coupons at checkout", {
|
|
320
|
+
uid,
|
|
321
|
+
codes: dropped.map((d) => d.code).join(","),
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
return { coupons, dropped };
|
|
325
|
+
}
|
|
241
326
|
async function flushCouponUsageAccumulator(accumulator, uid) {
|
|
242
327
|
if (accumulator.size === 0)
|
|
243
328
|
return;
|
|
@@ -282,9 +367,12 @@ function dispatchOrderConfirmationEmails(emailsToSend) {
|
|
|
282
367
|
* so the caller can accumulate the checkout-wide `total`.
|
|
283
368
|
*/
|
|
284
369
|
async function createOrderForGroup(group, orderType, ctx) {
|
|
285
|
-
const { paymentMethod, emiTenureMonths, emiSettings, commissions, appliedCoupons, cartSubtotal, couponUsageAccumulator, uid, userName, userEmail, shippingAddress, notes, adminBypass, adminBypassBy, adminBatchId, orderIds, emailsToSend, outOfStockPolicy, droppedItems, buyerState, gstSettings, siteContactUpiVpa,
|
|
370
|
+
const { paymentMethod, emiTenureMonths, emiSettings, commissions, appliedCoupons, cartSubtotal, couponUsageAccumulator, uid, userName, userEmail, shippingAddress, notes, adminBypass, adminBypassBy, adminBatchId, orderIds, emailsToSend, outOfStockPolicy, droppedItems, buyerState, gstSettings, siteContactUpiVpa, storeAddons, platformFeeByStore, } = ctx;
|
|
286
371
|
const firstItem = group[0].item;
|
|
287
372
|
const firstProduct = group[0].product;
|
|
373
|
+
// Add-ons are this store's choice, not the cart's — see OrderGroupContext.
|
|
374
|
+
const { whatsappNotifyAddon = false, giftWrapAddon = false, giftWrapMessage, shipmentProtectionAddon = false, } = storeAddons?.[firstItem.storeId] ?? {};
|
|
375
|
+
const { platformFee = 0, gstOnFee: platformFeeGst = 0 } = platformFeeByStore.get(firstItem.storeId) ?? {};
|
|
288
376
|
const groupTotal = group.reduce((sum, { item, product }) => sum + unitPriceFor(item, product) * item.quantity, 0);
|
|
289
377
|
// S-SBUNI-RULES 2026-05-13 — order-item decoration via rule registry.
|
|
290
378
|
const orderItems = group.map(({ item, product }) => {
|
|
@@ -371,7 +459,10 @@ async function createOrderForGroup(group, orderType, ctx) {
|
|
|
371
459
|
groupCouponCodes.add(discount.code);
|
|
372
460
|
}
|
|
373
461
|
}
|
|
374
|
-
|
|
462
|
+
// The platform commission is now charged on EVERY payment method. It used to
|
|
463
|
+
// be added only on the Razorpay path, so COD / UPI-manual / cash / EMI buyers
|
|
464
|
+
// silently never paid it.
|
|
465
|
+
const orderTotal = Math.max(0, groupTotal - couponDiscount) + shippingFee + codHandlingFee + whatsappNotifyFee + giftWrapFee + shipmentProtectionFee + platformFee + platformFeeGst + (emiSchedule?.surchargeAmount ?? 0) + (gstBreakdown?.gstAmount ?? 0);
|
|
375
466
|
const imageUrls = [
|
|
376
467
|
...new Set(group
|
|
377
468
|
.map(({ product }) => product.mainImage)
|
|
@@ -425,6 +516,9 @@ async function createOrderForGroup(group, orderType, ctx) {
|
|
|
425
516
|
depositAmount: adminBypass ? undefined : depositAmount,
|
|
426
517
|
codRemainingAmount: adminBypass ? undefined : codRemainingAmount,
|
|
427
518
|
codHandlingFee: !adminBypass && codHandlingFee > 0 ? codHandlingFee : undefined,
|
|
519
|
+
// This group's pro-rata share of the checkout's single capped commission —
|
|
520
|
+
// the shares across a multi-store checkout sum to exactly what was charged.
|
|
521
|
+
platformFee: !adminBypass && platformFee > 0 ? platformFee : undefined,
|
|
428
522
|
whatsappNotifyAddon: !adminBypass && whatsappNotifyFee > 0 ? true : undefined,
|
|
429
523
|
whatsappNotifyFee: !adminBypass && whatsappNotifyFee > 0 ? whatsappNotifyFee : undefined,
|
|
430
524
|
giftWrapAddon: !adminBypass && giftWrapFee > 0 ? true : undefined,
|
|
@@ -506,7 +600,7 @@ async function createOrderForGroup(group, orderType, ctx) {
|
|
|
506
600
|
* auth + rate-limiting before calling this and supply the resolved user fields.
|
|
507
601
|
*/
|
|
508
602
|
export async function createCheckoutOrderAction(input) {
|
|
509
|
-
const { userId: uid, userName, userEmail, addressId, paymentMethod, emiTenureMonths, notes, excludedProductIds = [], adminBypass = false, adminBypassBy, outOfStockPolicy = OutOfStockPolicyValues.SKIP_ITEMS,
|
|
603
|
+
const { userId: uid, userName, userEmail, addressId, paymentMethod, emiTenureMonths, notes, excludedProductIds = [], adminBypass = false, adminBypassBy, outOfStockPolicy = OutOfStockPolicyValues.SKIP_ITEMS, } = input;
|
|
510
604
|
const siteSettings = await siteSettingsRepository.getSingleton();
|
|
511
605
|
const commissions = siteSettings?.commissions ?? CHECKOUT_DEFAULT_COMMISSIONS;
|
|
512
606
|
const emiSettings = siteSettings?.emi ?? CHECKOUT_DEFAULT_EMI_SETTINGS;
|
|
@@ -701,7 +795,7 @@ export async function createCheckoutOrderAction(input) {
|
|
|
701
795
|
.catch((logErr) => { void normalizeError(logErr); serverLogger.warn(AUDIT_LOG_FAIL_MSG, { error: logErr instanceof Error ? logErr.message : String(logErr) }); });
|
|
702
796
|
throw new ValidationError(ERROR_MESSAGES.CHECKOUT.INSUFFICIENT_STOCK);
|
|
703
797
|
}
|
|
704
|
-
const appliedCoupons = cart.appliedCoupons ?? [];
|
|
798
|
+
const { coupons: appliedCoupons, dropped: droppedCoupons } = await revalidateAppliedCoupons(cart.appliedCoupons ?? [], available, uid);
|
|
705
799
|
const orderGroups = splitCartIntoOrderGroups(available);
|
|
706
800
|
// Admin-bypass orders are immediately paid and processing; generate a shared
|
|
707
801
|
// transaction reference for all orders in this checkout batch.
|
|
@@ -713,6 +807,12 @@ export async function createCheckoutOrderAction(input) {
|
|
|
713
807
|
const emailsToSend = [];
|
|
714
808
|
const cartSubtotal = orderGroups.reduce((s, { items: g }) => s + g.reduce((gs, { item, product }) => gs + unitPriceFor(item, product) * item.quantity, 0), 0);
|
|
715
809
|
const couponUsageAccumulator = new Map();
|
|
810
|
+
// One commission on one transaction, capped, then apportioned across the
|
|
811
|
+
// orders this cart splits into — see allocateCheckoutFees.
|
|
812
|
+
const platformFeeByStore = allocateCheckoutFees(orderGroups.map(({ items: g }) => [
|
|
813
|
+
g[0].item.storeId,
|
|
814
|
+
g.reduce((sum, { item, product }) => sum + unitPriceFor(item, product) * item.quantity, 0),
|
|
815
|
+
]), computeCheckoutFees(cartSubtotal, commissions));
|
|
716
816
|
const groupCtx = {
|
|
717
817
|
paymentMethod,
|
|
718
818
|
emiTenureMonths,
|
|
@@ -736,10 +836,8 @@ export async function createCheckoutOrderAction(input) {
|
|
|
736
836
|
buyerState: resolvedAddress?.state,
|
|
737
837
|
gstSettings: siteSettings?.gst,
|
|
738
838
|
siteContactUpiVpa: siteSettings?.contact?.upiVpa,
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
giftWrapMessage,
|
|
742
|
-
shipmentProtectionAddon,
|
|
839
|
+
storeAddons: cart.storeAddons,
|
|
840
|
+
platformFeeByStore,
|
|
743
841
|
};
|
|
744
842
|
for (const { items: group, orderType } of orderGroups) {
|
|
745
843
|
total += await createOrderForGroup(group, orderType, groupCtx);
|
|
@@ -753,6 +851,7 @@ export async function createCheckoutOrderAction(input) {
|
|
|
753
851
|
total,
|
|
754
852
|
itemCount: orderIds.length,
|
|
755
853
|
...(unavailable.length > 0 ? { unavailableItems: unavailable } : {}),
|
|
854
|
+
...(droppedCoupons.length > 0 ? { droppedCoupons } : {}),
|
|
756
855
|
};
|
|
757
856
|
}
|
|
758
857
|
const EMPTY_PRICING_PREVIEW = {
|
|
@@ -764,7 +863,10 @@ const EMPTY_PRICING_PREVIEW = {
|
|
|
764
863
|
shipmentProtectionFee: 0,
|
|
765
864
|
gstAmount: 0,
|
|
766
865
|
couponDiscount: 0,
|
|
866
|
+
platformFee: 0,
|
|
867
|
+
gstOnFee: 0,
|
|
767
868
|
total: 0,
|
|
869
|
+
stores: [],
|
|
768
870
|
};
|
|
769
871
|
/**
|
|
770
872
|
* Read-only "what will I actually be charged" preview for the checkout
|
|
@@ -776,13 +878,16 @@ const EMPTY_PRICING_PREVIEW = {
|
|
|
776
878
|
* stock, creates an order, or writes coupon usage.
|
|
777
879
|
*/
|
|
778
880
|
export async function previewCheckoutPricing(input) {
|
|
779
|
-
const { userId: uid, addressId, paymentMethod, excludedProductIds = [],
|
|
881
|
+
const { userId: uid, addressId, paymentMethod, excludedProductIds = [], lane, } = input;
|
|
780
882
|
const siteSettings = await siteSettingsRepository.getSingleton();
|
|
781
883
|
const commissions = siteSettings?.commissions ?? CHECKOUT_DEFAULT_COMMISSIONS;
|
|
782
884
|
const cart = await unitOfWork.carts.getOrCreate(uid);
|
|
783
885
|
const excludedSet = new Set(excludedProductIds);
|
|
784
886
|
const selectedSet = cart.selectedItemIds?.length ? new Set(cart.selectedItemIds) : null;
|
|
785
|
-
|
|
887
|
+
// An explicit lane wins: the cart page prices whichever tab is open, which may
|
|
888
|
+
// not be the one that is currently payable. Falling back to activeLane keeps
|
|
889
|
+
// the checkout page's existing behaviour unchanged.
|
|
890
|
+
const previewLane = lane ?? activeLane(cart.items ?? []);
|
|
786
891
|
const cartItems = (cart.items ?? []).filter((item) => !excludedSet.has(item.productId) &&
|
|
787
892
|
(!selectedSet || selectedSet.has(item.itemId)) &&
|
|
788
893
|
// A preview never throws — it just shows the lane the buyer can actually
|
|
@@ -805,46 +910,78 @@ export async function previewCheckoutPricing(input) {
|
|
|
805
910
|
const checks = cartItems.map((item) => ({ item, product: productById.get(item.productId) ?? null }));
|
|
806
911
|
const orderGroups = splitCartIntoOrderGroups(checks);
|
|
807
912
|
const cartSubtotal = orderGroups.reduce((s, { items: g }) => s + g.reduce((gs, { item, product }) => gs + unitPriceFor(item, product) * item.quantity, 0), 0);
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
let couponDiscount = 0;
|
|
913
|
+
// Same re-validation the real order placement runs, so the preview never
|
|
914
|
+
// shows a discount the order won't actually honour.
|
|
915
|
+
const { coupons: appliedCoupons } = await revalidateAppliedCoupons(cart.appliedCoupons ?? [], checks, uid);
|
|
916
|
+
// Each group's figures are kept, not just summed away — the aggregates below
|
|
917
|
+
// are derived FROM this array, so a store card and the summary panel can never
|
|
918
|
+
// disagree about the same fee.
|
|
919
|
+
const stores = [];
|
|
816
920
|
for (const { items: group } of orderGroups) {
|
|
817
921
|
const firstItem = group[0].item;
|
|
818
922
|
const groupTotal = group.reduce((sum, { item, product }) => sum + unitPriceFor(item, product) * item.quantity, 0);
|
|
819
923
|
const { shippingFee: groupShippingFee, storeState } = await resolveShippingCost(firstItem.storeId);
|
|
820
|
-
|
|
924
|
+
// Add-ons are per store, read off the cart document — the same entry
|
|
925
|
+
// createOrderForGroup will read when the order is actually placed.
|
|
926
|
+
const addons = cart.storeAddons?.[firstItem.storeId] ?? {};
|
|
821
927
|
// Mirrors createOrderForGroup exactly — the COD handling fee is only
|
|
822
928
|
// ever charged for literal "cod" (pay the courier), never upi_manual or
|
|
823
929
|
// cash (both pre-paid via UPI/bank transfer before dispatch).
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
930
|
+
const groupCodHandlingFee = paymentMethod === "cod" ? computeCodHandlingFee(groupTotal, commissions) : 0;
|
|
931
|
+
const groupWhatsappFee = computeWhatsAppNotifyFee(addons.whatsappNotifyAddon ?? false, commissions);
|
|
932
|
+
const groupGiftWrapFee = computeGiftWrapFee(addons.giftWrapAddon ?? false, commissions);
|
|
933
|
+
const groupShipmentProtectionFee = computeShipmentProtectionFee(groupTotal, addons.shipmentProtectionAddon ?? false, commissions);
|
|
934
|
+
let groupGstAmount = 0;
|
|
829
935
|
if (siteSettings?.gst?.enabled && resolvedAddress?.state) {
|
|
830
936
|
const intraState = !!storeState && storeState === resolvedAddress.state;
|
|
831
937
|
for (const { item, product } of group) {
|
|
832
938
|
const lineTotal = unitPriceFor(item, product) * item.quantity;
|
|
833
939
|
const rate = product?.gstRate ?? 0;
|
|
834
940
|
if (rate > 0)
|
|
835
|
-
|
|
941
|
+
groupGstAmount += calculateGst(rate, intraState, lineTotal).gstAmount;
|
|
836
942
|
}
|
|
837
943
|
}
|
|
838
944
|
const groupLines = group.map(({ item, product }) => ({ itemId: item.itemId, lineTotal: unitPriceFor(item, product) * item.quantity }));
|
|
839
945
|
const { couponDiscount: groupCouponDiscount } = computeGroupCouponDiscount(appliedCoupons, groupLines, groupTotal, cartSubtotal, firstItem.storeId);
|
|
840
|
-
|
|
946
|
+
stores.push({
|
|
947
|
+
storeId: firstItem.storeId,
|
|
948
|
+
storeName: firstItem.storeName || firstItem.storeId,
|
|
949
|
+
subtotal: groupTotal,
|
|
950
|
+
shippingFee: groupShippingFee,
|
|
951
|
+
codHandlingFee: groupCodHandlingFee,
|
|
952
|
+
whatsappNotifyFee: groupWhatsappFee,
|
|
953
|
+
giftWrapFee: groupGiftWrapFee,
|
|
954
|
+
shipmentProtectionFee: groupShipmentProtectionFee,
|
|
955
|
+
gstAmount: groupGstAmount,
|
|
956
|
+
couponDiscount: groupCouponDiscount,
|
|
957
|
+
total: Math.max(0, groupTotal - groupCouponDiscount) +
|
|
958
|
+
groupShippingFee +
|
|
959
|
+
groupCodHandlingFee +
|
|
960
|
+
groupWhatsappFee +
|
|
961
|
+
groupGiftWrapFee +
|
|
962
|
+
groupShipmentProtectionFee +
|
|
963
|
+
groupGstAmount,
|
|
964
|
+
});
|
|
841
965
|
}
|
|
966
|
+
const sum = (pick) => stores.reduce((acc, s) => acc + pick(s), 0);
|
|
967
|
+
const shippingFee = sum((s) => s.shippingFee);
|
|
968
|
+
const codHandlingFee = sum((s) => s.codHandlingFee);
|
|
969
|
+
const whatsappNotifyFeeTotal = sum((s) => s.whatsappNotifyFee);
|
|
970
|
+
const giftWrapFeeTotal = sum((s) => s.giftWrapFee);
|
|
971
|
+
const shipmentProtectionFeeTotal = sum((s) => s.shipmentProtectionFee);
|
|
972
|
+
const gstAmount = sum((s) => s.gstAmount);
|
|
973
|
+
const couponDiscount = sum((s) => s.couponDiscount);
|
|
974
|
+
// One capped commission for the whole checkout, on every payment method —
|
|
975
|
+
// deliberately not per store, so it is not added to the per-store rows above.
|
|
976
|
+
const { platformFee, gstOnFee } = computeCheckoutFees(cartSubtotal, commissions);
|
|
842
977
|
const total = Math.max(0, cartSubtotal - couponDiscount) +
|
|
843
978
|
shippingFee +
|
|
844
979
|
codHandlingFee +
|
|
845
980
|
whatsappNotifyFeeTotal +
|
|
846
981
|
giftWrapFeeTotal +
|
|
847
982
|
shipmentProtectionFeeTotal +
|
|
983
|
+
platformFee +
|
|
984
|
+
gstOnFee +
|
|
848
985
|
gstAmount;
|
|
849
986
|
return {
|
|
850
987
|
subtotal: cartSubtotal,
|
|
@@ -855,7 +992,10 @@ export async function previewCheckoutPricing(input) {
|
|
|
855
992
|
shipmentProtectionFee: shipmentProtectionFeeTotal,
|
|
856
993
|
gstAmount,
|
|
857
994
|
couponDiscount,
|
|
995
|
+
platformFee,
|
|
996
|
+
gstOnFee,
|
|
858
997
|
total,
|
|
998
|
+
stores,
|
|
859
999
|
};
|
|
860
1000
|
}
|
|
861
1001
|
/**
|
|
@@ -976,7 +1116,7 @@ async function refundDroppedItemsForRazorpayCheckout(input) {
|
|
|
976
1116
|
* from the inline version this replaced.
|
|
977
1117
|
*/
|
|
978
1118
|
async function createRazorpayGroupOrder(group, orderType, ctx) {
|
|
979
|
-
const { appliedCoupons, cartSubtotal,
|
|
1119
|
+
const { appliedCoupons, cartSubtotal, storeAddons, platformFeeByStore, siteSettings, uid, userName, userEmail, razorpay_order_id, razorpay_payment_id, razorpay_signature, shippingAddress, notes, outOfStockPolicy, unavailablePaid, orderIds, emailsToSend, couponUsageAccumulator, } = ctx;
|
|
980
1120
|
const firstItem = group[0].item;
|
|
981
1121
|
const groupTotal = group.reduce((sum, { item, product }) => sum + unitPriceFor(item, product) * item.quantity, 0);
|
|
982
1122
|
// Reuses the same store/seller lookup as the COD/UPI path above instead
|
|
@@ -985,11 +1125,31 @@ async function createRazorpayGroupOrder(group, orderType, ctx) {
|
|
|
985
1125
|
const { shippingFee, storeOwnerId } = await resolveShippingCost(firstItem.storeId);
|
|
986
1126
|
const groupLines = group.map(({ item, product }) => ({ itemId: item.itemId, lineTotal: unitPriceFor(item, product) * item.quantity }));
|
|
987
1127
|
const { couponDiscount, appliedDiscounts } = computeGroupCouponDiscount(appliedCoupons, groupLines, groupTotal, cartSubtotal, firstItem.storeId);
|
|
988
|
-
|
|
989
|
-
|
|
1128
|
+
// Mirrors createOrderForGroup — without this the online-payment path wrote
|
|
1129
|
+
// the discount onto the order but never incremented usage.currentUsage or
|
|
1130
|
+
// the per-user counter, so limits were unenforceable for Razorpay orders.
|
|
1131
|
+
const groupCouponCodes = new Set();
|
|
1132
|
+
for (const discount of appliedDiscounts) {
|
|
1133
|
+
if (discount.couponId) {
|
|
1134
|
+
accumulateCouponUsage(couponUsageAccumulator, discount.code, discount.couponId, discount.discountAmount);
|
|
1135
|
+
groupCouponCodes.add(discount.code);
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
// Was a per-group, uncapped `groupTotal × platformFeePercent` computed inline
|
|
1139
|
+
// here — the only path that charged the commission at all, and it charged a
|
|
1140
|
+
// different (larger, multiplied-per-store) number than the cap now allows.
|
|
1141
|
+
// Both the amount and its allocation are now decided once for the checkout.
|
|
1142
|
+
const { platformFee: rawPlatformFee = 0, gstOnFee = 0 } = platformFeeByStore.get(firstItem.storeId) ?? {};
|
|
990
1143
|
const platformFee = rawPlatformFee + gstOnFee;
|
|
991
|
-
|
|
992
|
-
|
|
1144
|
+
// Add-ons are this store's choice, read off the cart doc — previously one
|
|
1145
|
+
// cart-wide fee was passed in and billed against every group.
|
|
1146
|
+
const commissionRates = siteSettings?.commissions ?? CHECKOUT_DEFAULT_COMMISSIONS;
|
|
1147
|
+
const addons = storeAddons?.[firstItem.storeId] ?? {};
|
|
1148
|
+
const whatsappNotifyFee = computeWhatsAppNotifyFee(addons.whatsappNotifyAddon ?? false, commissionRates);
|
|
1149
|
+
const giftWrapFee = computeGiftWrapFee(addons.giftWrapAddon ?? false, commissionRates);
|
|
1150
|
+
const giftWrapMessage = addons.giftWrapMessage;
|
|
1151
|
+
const shipmentProtectionFee = computeShipmentProtectionFee(groupTotal, addons.shipmentProtectionAddon ?? false, commissionRates);
|
|
1152
|
+
const orderTotal = Math.max(0, groupTotal - couponDiscount) + shippingFee + whatsappNotifyFee + giftWrapFee + shipmentProtectionFee + platformFee;
|
|
993
1153
|
// P-8 GST — deliberately NOT wired into this Razorpay-verify path. The
|
|
994
1154
|
// amount-mismatch check above (expectedPaymentAmountRs) compares against
|
|
995
1155
|
// what the buyer already paid via the Razorpay order created earlier in
|
|
@@ -1103,6 +1263,11 @@ async function createRazorpayGroupOrder(group, orderType, ctx) {
|
|
|
1103
1263
|
productTitle: firstItem.productTitle,
|
|
1104
1264
|
}).catch((e) => serverLogger.error("claimDigitalCode", e));
|
|
1105
1265
|
}
|
|
1266
|
+
for (const code of groupCouponCodes) {
|
|
1267
|
+
const entry = couponUsageAccumulator.get(code);
|
|
1268
|
+
if (entry)
|
|
1269
|
+
entry.orderIds.push(order.id);
|
|
1270
|
+
}
|
|
1106
1271
|
if (userEmail) {
|
|
1107
1272
|
emailsToSend.push({
|
|
1108
1273
|
to: userEmail,
|
|
@@ -1129,12 +1294,11 @@ async function createRazorpayGroupOrder(group, orderType, ctx) {
|
|
|
1129
1294
|
return orderTotal;
|
|
1130
1295
|
}
|
|
1131
1296
|
export async function verifyAndPlaceRazorpayOrderAction(input) {
|
|
1132
|
-
const { userId: uid, userName, userEmail, razorpay_order_id, razorpay_payment_id, razorpay_signature, addressId, notes, outOfStockPolicy = OutOfStockPolicyValues.CANCEL_ORDER,
|
|
1297
|
+
const { userId: uid, userName, userEmail, razorpay_order_id, razorpay_payment_id, razorpay_signature, addressId, notes, outOfStockPolicy = OutOfStockPolicyValues.CANCEL_ORDER, } = input;
|
|
1298
|
+
// Add-on fees are no longer resolved here. They are per store, and this
|
|
1299
|
+
// function has no single store — each group reads its own selection off
|
|
1300
|
+
// `cart.storeAddons` inside createRazorpayGroupOrder.
|
|
1133
1301
|
const siteSettings = await siteSettingsRepository.getSingleton();
|
|
1134
|
-
const platformFeePercent = siteSettings?.commissions?.platformFeePercent ?? 5;
|
|
1135
|
-
const gstPercent = siteSettings?.commissions?.gstPercent ?? 18;
|
|
1136
|
-
const whatsappNotifyFee = computeWhatsAppNotifyFee(whatsappNotifyAddon, siteSettings?.commissions ?? CHECKOUT_DEFAULT_COMMISSIONS);
|
|
1137
|
-
const giftWrapFee = computeGiftWrapFee(giftWrapAddon, siteSettings?.commissions ?? CHECKOUT_DEFAULT_COMMISSIONS);
|
|
1138
1302
|
const isValid = await verifyPaymentSignatureWithKeys({
|
|
1139
1303
|
razorpay_order_id,
|
|
1140
1304
|
razorpay_payment_id,
|
|
@@ -1257,8 +1421,12 @@ export async function verifyAndPlaceRazorpayOrderAction(input) {
|
|
|
1257
1421
|
const unit = isBundle ? item.price : product.price;
|
|
1258
1422
|
return sum + unit * item.quantity;
|
|
1259
1423
|
}, 0);
|
|
1260
|
-
|
|
1261
|
-
|
|
1424
|
+
// Must use the SAME helper /api/payment/create-order used to compute what
|
|
1425
|
+
// it charged. This block used to re-derive the fee inline from the raw
|
|
1426
|
+
// percentages, so the moment the cap landed it would have expected more
|
|
1427
|
+
// than was actually collected and rejected every legitimate payment.
|
|
1428
|
+
const commissionRates = siteSettings?.commissions ?? CHECKOUT_DEFAULT_COMMISSIONS;
|
|
1429
|
+
const { platformFee: expectedPlatformFee, gstOnFee: expectedGstOnFee } = computeCheckoutFees(cartSubtotalRs, commissionRates);
|
|
1262
1430
|
// Same per-seller-group shipping sum /api/payment/create-order charged
|
|
1263
1431
|
// upfront (and the same resolveShippingCost createRazorpayGroupOrder
|
|
1264
1432
|
// below will use to build each order's recorded totalPrice) — keeps this
|
|
@@ -1266,7 +1434,19 @@ export async function verifyAndPlaceRazorpayOrderAction(input) {
|
|
|
1266
1434
|
const expectedShippingGroups = splitCartIntoOrderGroups(bucketedPaid.available);
|
|
1267
1435
|
const expectedShippingFees = await Promise.all(expectedShippingGroups.map((g) => resolveShippingCost(g.items[0].item.storeId)));
|
|
1268
1436
|
const expectedShippingFee = expectedShippingFees.reduce((sum, r) => sum + r.shippingFee, 0);
|
|
1269
|
-
|
|
1437
|
+
// Add-ons are per store, so the expected total sums each group's own
|
|
1438
|
+
// selection — a single cart-wide figure would over- or under-count as soon
|
|
1439
|
+
// as the buyer picked add-ons for only some sellers.
|
|
1440
|
+
const expectedAddonFees = expectedShippingGroups.reduce((sum, g) => {
|
|
1441
|
+
const storeId = g.items[0].item.storeId;
|
|
1442
|
+
const addons = cart.storeAddons?.[storeId] ?? {};
|
|
1443
|
+
const groupSubtotal = g.items.reduce((gs, { item, product }) => gs + unitPriceFor(item, product) * item.quantity, 0);
|
|
1444
|
+
return (sum +
|
|
1445
|
+
computeWhatsAppNotifyFee(addons.whatsappNotifyAddon ?? false, commissionRates) +
|
|
1446
|
+
computeGiftWrapFee(addons.giftWrapAddon ?? false, commissionRates) +
|
|
1447
|
+
computeShipmentProtectionFee(groupSubtotal, addons.shipmentProtectionAddon ?? false, commissionRates));
|
|
1448
|
+
}, 0);
|
|
1449
|
+
const expectedPaymentAmountRs = cartSubtotalRs + expectedPlatformFee + expectedGstOnFee + expectedAddonFees + expectedShippingFee;
|
|
1270
1450
|
const rzpOrderRecord = await fetchRazorpayOrder(razorpay_order_id);
|
|
1271
1451
|
const paidAmountRs = paiseToRupees(rzpOrderRecord.amount);
|
|
1272
1452
|
if (paidAmountRs < expectedPaymentAmountRs - 1) {
|
|
@@ -1282,7 +1462,7 @@ export async function verifyAndPlaceRazorpayOrderAction(input) {
|
|
|
1282
1462
|
throw new ValidationError(ERROR_MESSAGES.CHECKOUT.PAYMENT_FAILED);
|
|
1283
1463
|
}
|
|
1284
1464
|
}
|
|
1285
|
-
const appliedCoupons = cart.appliedCoupons ?? [];
|
|
1465
|
+
const { coupons: appliedCoupons, dropped: droppedCoupons } = await revalidateAppliedCoupons(cart.appliedCoupons ?? [], bucketedPaid.available, uid);
|
|
1286
1466
|
// "skip_items" (or an all-available cart under any policy) — build order
|
|
1287
1467
|
// groups from the available bucket only. Dropped items are never ordered;
|
|
1288
1468
|
// their value is refunded below since Razorpay already captured payment
|
|
@@ -1293,16 +1473,18 @@ export async function verifyAndPlaceRazorpayOrderAction(input) {
|
|
|
1293
1473
|
const emailsToSend = [];
|
|
1294
1474
|
const cartSubtotal = orderGroups.reduce((s, { items: g }) => s +
|
|
1295
1475
|
g.reduce((gs, { item, product }) => gs + unitPriceFor(item, product) * item.quantity, 0), 0);
|
|
1476
|
+
const couponUsageAccumulator = new Map();
|
|
1477
|
+
// Same single capped commission + pro-rata split as the COD/UPI path.
|
|
1478
|
+
const platformFeeByStore = allocateCheckoutFees(orderGroups.map(({ items: g }) => [
|
|
1479
|
+
g[0].item.storeId,
|
|
1480
|
+
g.reduce((sum, { item, product }) => sum + unitPriceFor(item, product) * item.quantity, 0),
|
|
1481
|
+
]), computeCheckoutFees(cartSubtotal, siteSettings?.commissions ?? CHECKOUT_DEFAULT_COMMISSIONS));
|
|
1296
1482
|
for (const { items: group, orderType } of orderGroups) {
|
|
1297
1483
|
total += await createRazorpayGroupOrder(group, orderType, {
|
|
1298
1484
|
appliedCoupons,
|
|
1299
1485
|
cartSubtotal,
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
whatsappNotifyFee,
|
|
1303
|
-
giftWrapFee,
|
|
1304
|
-
giftWrapMessage,
|
|
1305
|
-
shipmentProtectionAddon,
|
|
1486
|
+
storeAddons: cart.storeAddons,
|
|
1487
|
+
platformFeeByStore,
|
|
1306
1488
|
siteSettings,
|
|
1307
1489
|
uid,
|
|
1308
1490
|
userName,
|
|
@@ -1316,8 +1498,10 @@ export async function verifyAndPlaceRazorpayOrderAction(input) {
|
|
|
1316
1498
|
unavailablePaid,
|
|
1317
1499
|
orderIds,
|
|
1318
1500
|
emailsToSend,
|
|
1501
|
+
couponUsageAccumulator,
|
|
1319
1502
|
});
|
|
1320
1503
|
}
|
|
1504
|
+
await flushCouponUsageAccumulator(couponUsageAccumulator, uid);
|
|
1321
1505
|
// SB-UNI-5 2026-05-13 — bundle-aware batch decrement. We iterate the
|
|
1322
1506
|
// cumulative `decrements` map (one entry per unique member product) so a
|
|
1323
1507
|
// bundle's members + any sibling cart line touching the same product
|
|
@@ -1366,5 +1550,6 @@ export async function verifyAndPlaceRazorpayOrderAction(input) {
|
|
|
1366
1550
|
total,
|
|
1367
1551
|
itemCount: orderIds.length,
|
|
1368
1552
|
...(unavailablePaid.length > 0 ? { unavailableItems: unavailablePaid } : {}),
|
|
1553
|
+
...(droppedCoupons.length > 0 ? { droppedCoupons } : {}),
|
|
1369
1554
|
};
|
|
1370
1555
|
}
|
|
@@ -10,4 +10,14 @@ export interface CheckoutOrderResult {
|
|
|
10
10
|
requestedQty: number;
|
|
11
11
|
availableQty: number;
|
|
12
12
|
}[];
|
|
13
|
+
/**
|
|
14
|
+
* Coupons that were on the cart but failed re-validation at placement time
|
|
15
|
+
* (expired, limit reached, no longer any eligible items). They were removed
|
|
16
|
+
* and the order priced without them — surfaced so the UI can tell the buyer
|
|
17
|
+
* rather than silently charging more than the cart showed.
|
|
18
|
+
*/
|
|
19
|
+
droppedCoupons?: {
|
|
20
|
+
code: string;
|
|
21
|
+
reason: string;
|
|
22
|
+
}[];
|
|
13
23
|
}
|
|
@@ -50,12 +50,32 @@ export function orderDocumentToOrder(doc) {
|
|
|
50
50
|
address,
|
|
51
51
|
orderStatus: doc.status,
|
|
52
52
|
paymentStatus: doc.paymentStatus,
|
|
53
|
+
// Which checkout lane produced this order (standard / auction / offer /
|
|
54
|
+
// preorder / prize-draw). Drives the lane tabs on /user/orders. Absent on
|
|
55
|
+
// orders written before `orderType` existed — every reader must treat a
|
|
56
|
+
// missing value as "standard" rather than filtering it out.
|
|
57
|
+
orderType: doc.orderType,
|
|
58
|
+
offerId: doc.offerId,
|
|
53
59
|
subtotal,
|
|
54
60
|
shippingCost: shippingCost || undefined,
|
|
55
61
|
discount: discount || undefined,
|
|
56
62
|
total: doc.totalPrice,
|
|
57
63
|
currency: doc.currency,
|
|
58
64
|
couponCode: doc.couponCode,
|
|
65
|
+
couponDiscount: doc.couponDiscount,
|
|
66
|
+
appliedDiscounts: doc.appliedDiscounts,
|
|
67
|
+
// Add-ons are operational, not decorative: the status-change notifier reads
|
|
68
|
+
// whatsappNotifyAddon, and the packer needs giftWrapMessage in hand. An
|
|
69
|
+
// adapter that drops them makes every downstream surface render its
|
|
70
|
+
// "not applicable" fallback instead (Root Cause #57).
|
|
71
|
+
whatsappNotifyAddon: doc.whatsappNotifyAddon,
|
|
72
|
+
whatsappNotifyFee: doc.whatsappNotifyFee,
|
|
73
|
+
giftWrapAddon: doc.giftWrapAddon,
|
|
74
|
+
giftWrapFee: doc.giftWrapFee,
|
|
75
|
+
giftWrapMessage: doc.giftWrapMessage,
|
|
76
|
+
shipmentProtectionAddon: doc.shipmentProtectionAddon,
|
|
77
|
+
shipmentProtectionFee: doc.shipmentProtectionFee,
|
|
78
|
+
platformFee: doc.platformFee,
|
|
59
79
|
trackingNumber: doc.trackingNumber,
|
|
60
80
|
shippingCarrier: doc.shippingCarrier,
|
|
61
81
|
trackingUrl: doc.trackingUrl,
|
|
@@ -4,15 +4,24 @@ import type { ProductDocument } from "../../../../features/products/schemas/fire
|
|
|
4
4
|
import type { ListingType } from "../../../../features/products/types";
|
|
5
5
|
import type { ProductItem } from "../../../../features/products/types";
|
|
6
6
|
import type { FirestoreDocument } from "../../../../schemas/types";
|
|
7
|
-
import type { Review } from "../../../../features/reviews/types";
|
|
7
|
+
import type { Review, ReviewListResponse } from "../../../../features/reviews/types";
|
|
8
8
|
/** Fetch a single product by slug or id, deduped per request. */
|
|
9
9
|
export declare const getProductForDetail: (slugOrId: string) => Promise<ProductDocument | null>;
|
|
10
10
|
/** Fetch the first page of approved reviews for a product, deduped per request. */
|
|
11
|
-
export declare const getReviewsForProduct: (productId: string) => Promise<unknown[]>;
|
|
11
|
+
export declare const getReviewsForProduct: (productId: string, limit?: number) => Promise<unknown[]>;
|
|
12
12
|
/** Firestore review doc → client Review shape. Shared by every listing-type detail page's reviews tab. */
|
|
13
13
|
export declare function toReview(doc: FirestoreDocument): Review;
|
|
14
14
|
/** Fetch + map approved reviews for a product straight to the client Review shape, deduped per request. */
|
|
15
|
-
export declare const getReviewItemsForProduct: (productId: string) => Promise<Review[]>;
|
|
15
|
+
export declare const getReviewItemsForProduct: (productId: string, limit?: number) => Promise<Review[]>;
|
|
16
|
+
/**
|
|
17
|
+
* Page 1 of a product's approved reviews, shaped exactly like `GET /api/reviews?productId=`
|
|
18
|
+
* so it can seed `<ReviewsListingPanel initialData>` without a client round-trip.
|
|
19
|
+
*
|
|
20
|
+
* The defaults here MUST match the panel's bare (unfiltered, page-1) query defaults —
|
|
21
|
+
* `staleTime: Infinity` freezes whatever `initialData` is handed over, so a mismatch would
|
|
22
|
+
* pin the wrong rows into the cache (Recurrent Root Cause #30).
|
|
23
|
+
*/
|
|
24
|
+
export declare const getReviewPageForProduct: (productId: string, pageSize?: number) => Promise<ReviewListResponse>;
|
|
16
25
|
/** Minimal product fields needed for sitemap generation. */
|
|
17
26
|
export interface SitemapProduct {
|
|
18
27
|
slugOrId: string;
|