@mohasinac/appkit 4.61.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/client/features/lottery/LotteryAdminEditView.js +19 -3
- package/dist/_internal/client/features/maintenance/views/AnalysisRunnerView.js +1 -1
- package/dist/_internal/client/features/maintenance/views/CloudLogsListView.js +1 -1
- 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/paymentReconcile.js +87 -97
- 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 +177 -9
- 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/_internal/shared/media/limits.d.ts +10 -0
- package/dist/_internal/shared/media/limits.js +10 -0
- package/dist/_internal/shared/types/action-result.js +15 -1
- package/dist/client/api/surface-error.js +16 -2
- package/dist/client/api/useApiMutation.js +17 -1
- package/dist/client.d.ts +12 -1
- package/dist/client.js +31 -1
- package/dist/constants/api-endpoints.d.ts +45 -0
- package/dist/constants/api-endpoints.js +15 -0
- package/dist/constants/field-names.d.ts +18 -0
- package/dist/constants/field-names.js +28 -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/core/server-action.js +19 -3
- package/dist/errors/error-mapping.js +26 -1
- package/dist/features/about/components/AboutView.js +1 -1
- package/dist/features/account/components/BuyerAuctionsGuideView.js +4 -1
- package/dist/features/account/components/UserOffersPanel.js +5 -3
- package/dist/features/account/components/UserOrdersView.js +19 -33
- package/dist/features/admin/components/AdminActionIndexView.js +100 -5
- package/dist/features/admin/components/AdminAddressClustersView.js +21 -2
- package/dist/features/admin/components/AdminBlogEditorView.js +8 -8
- package/dist/features/admin/components/AdminCarouselEditorView.js +1 -1
- package/dist/features/admin/components/AdminCarouselView.js +96 -4
- package/dist/features/admin/components/AdminCouponEditorView.js +17 -0
- package/dist/features/admin/components/AdminFulfillmentView.js +3 -2
- package/dist/features/admin/components/AdminInvoicingView.d.ts +2 -0
- package/dist/features/admin/components/AdminInvoicingView.js +65 -0
- package/dist/features/admin/components/AdminMediaView.js +36 -3
- package/dist/features/admin/components/AdminNavigationView.js +4 -1
- package/dist/features/admin/components/AdminOffersView.js +2 -1
- 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/AdminPaymentClustersView.js +19 -2
- package/dist/features/admin/components/AdminProductEditorView.js +2 -2
- package/dist/features/admin/components/AdminSectionsView.d.ts +10 -0
- package/dist/features/admin/components/AdminSectionsView.js +486 -224
- package/dist/features/admin/components/OwnerQuickActions.js +24 -2
- package/dist/features/admin/components/ReferenceRecordEditor.js +1 -1
- 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/hooks/useOptimisticRowFlag.d.ts +33 -0
- package/dist/features/admin/hooks/useOptimisticRowFlag.js +87 -0
- package/dist/features/admin/repository/notification.repository.d.ts +15 -0
- package/dist/features/admin/repository/notification.repository.js +32 -0
- package/dist/features/admin/schemas/action-index-form.d.ts +34 -0
- package/dist/features/admin/schemas/action-index-form.js +115 -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/components/RegisterForm.js +2 -1
- 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/catalogue/components/CatalogueItemEditorView.js +3 -3
- 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/events/components/AdminEventEditorView.js +8 -6
- 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/layout/hooks/useFormBottomActions.js +20 -1
- 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/EmiInstallmentsPanel.js +25 -7
- 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 +22 -4
- 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 +60 -12
- 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/GroupMemberPicker.js +4 -1
- package/dist/features/products/components/PrizeDrawDetailPageView.js +2 -2
- package/dist/features/products/components/PrizeDrawEntryActions.js +2 -1
- package/dist/features/products/components/ProductDetailActions.js +2 -1
- 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/FulfillmentView.js +3 -2
- package/dist/features/seller/components/SellerAddressesView.js +32 -4
- package/dist/features/seller/components/SellerAnalyticsAlertsView.js +62 -3
- package/dist/features/seller/components/SellerBundlesView.js +53 -14
- package/dist/features/seller/components/SellerGroupedListingsView.js +23 -3
- package/dist/features/seller/components/SellerListingTypeView.js +56 -10
- package/dist/features/seller/components/SellerOrdersView.js +129 -27
- package/dist/features/seller/components/SellerPayoutMethodsView.js +33 -1
- package/dist/features/seller/components/SellerPayoutSettingsView.js +48 -9
- package/dist/features/seller/components/SellerProductsView.js +5 -1
- package/dist/features/seller/components/SellerShippingConfigsView.js +33 -1
- package/dist/features/seller/components/SellerShippingView.js +2 -1
- package/dist/features/seller/components/SellerStoreCategoriesView.js +33 -1
- package/dist/features/seller/components/SellerSupportView.js +69 -2
- package/dist/features/seller/hooks/useSellerListingData.js +25 -0
- package/dist/features/shell/SectionForm.js +20 -16
- package/dist/features/shell/build-sections.js +14 -1
- package/dist/features/shipments/components/AdminShipmentLotItemsView.js +54 -1
- package/dist/features/shipments/components/AdminShipmentProjectionsView.js +3 -2
- 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/sticker-trace/components/StickerCommitPanel.js +2 -1
- package/dist/features/sticker-trace/components/StickerPrintPanel.js +2 -1
- package/dist/features/sticker-trace/components/StickerRetryPanel.js +24 -4
- package/dist/features/sticker-trace/components/StickerTraceStudio.js +2 -1
- 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/tester/components/AdminTesterFeedbackIssuesView.js +3 -2
- package/dist/features/wishlist/hooks/useWishlistWithGuest.d.ts +1 -1
- package/dist/index.d.ts +11 -2
- package/dist/index.js +17 -2
- package/dist/next/routing/route-map.d.ts +24 -0
- package/dist/next/routing/route-map.js +19 -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 +51 -22
- package/dist/schemas/sieve.d.ts +5 -5
- package/dist/server-entry.d.ts +2 -2
- package/dist/server-entry.js +8 -1
- package/dist/server.d.ts +11 -2
- package/dist/server.js +40 -2
- 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/Checkbox.js +10 -2
- package/dist/ui/components/DateInput.js +8 -1
- package/dist/ui/components/Disclaimer.d.ts +19 -0
- package/dist/ui/components/Disclaimer.js +74 -0
- package/dist/ui/components/Input.js +22 -2
- package/dist/ui/components/PaginatedSelect.d.ts +8 -19
- package/dist/ui/components/PaginatedSelect.js +43 -9
- package/dist/ui/components/Radio.js +8 -1
- package/dist/ui/components/Select.js +10 -2
- package/dist/ui/components/TagInput.d.ts +8 -1
- package/dist/ui/components/TagInput.js +4 -3
- package/dist/ui/components/Textarea.js +10 -2
- package/dist/ui/components/tabs-overflow.js +64 -11
- package/dist/ui/forms/FieldCheckbox.js +1 -1
- package/dist/ui/forms/FormErrorList.d.ts +0 -16
- package/dist/ui/forms/FormErrorList.js +34 -1
- package/dist/ui/forms/apply-zod-issues.d.ts +19 -1
- package/dist/ui/forms/apply-zod-issues.js +23 -1
- 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
|
@@ -16,7 +16,18 @@ import { roundShippingUp, } from "../../../shared/shipping/quote.js";
|
|
|
16
16
|
import { unitOfWork, adminNotificationsRepository, siteSettingsRepository, userRepository, storeRepository, couponsRepository, claimedCouponsRepository, addressesRepository, productRepository, } from "../../../../repositories/index.js";
|
|
17
17
|
import { isIntraStateSupply } from "../../../shared/fees/calculator.js";
|
|
18
18
|
import { resolveStateKey, INDIA_STATES } from "../../../shared/fees/india-states.js";
|
|
19
|
-
import { computePreOrderDepositAmount, lineTotalFor, sumGroupGst, unitPriceFor, } from "../../../shared/checkout/order-math.js";
|
|
19
|
+
import { allocatePointsAcrossGroups, computePreOrderDepositAmount, lineTotalFor, resolvePointsTender, sumGroupGst, unitPriceFor, } from "../../../shared/checkout/order-math.js";
|
|
20
|
+
/*
|
|
21
|
+
* D46/D47 — points as a checkout TENDER.
|
|
22
|
+
*
|
|
23
|
+
* `storeAcceptsPoints` resolves decision 22's per-store opt-in (and the
|
|
24
|
+
* platform store's always-accepts, which is decided in code); the repository
|
|
25
|
+
* supplies the balance and owns the ledger write. The ARITHMETIC lives in
|
|
26
|
+
* `resolvePointsTender` above, beside every other money rule, so the tender
|
|
27
|
+
* cannot become a sixth disagreeing total (D37/D38/D40).
|
|
28
|
+
*/
|
|
29
|
+
import { storeAcceptsPoints } from "../../../shared/features/points/accepts.js";
|
|
30
|
+
import { pointsRepository } from "../../../../features/points/repository/points.repository.js";
|
|
20
31
|
import { COD_MAX_ORDER_VALUE_FALLBACK } from "../../../../features/admin/schemas/firestore.js";
|
|
21
32
|
import { CHECKOUT_ERROR_CODES } from "../../../../errors/error-codes.js";
|
|
22
33
|
import { failedCheckoutRepository } from "../../../../features/checkout/repository/failed-checkout.repository.js";
|
|
@@ -31,6 +42,9 @@ import { DEFAULT_STORE_SHIPPING_POLICY, quoteShipping, toStoreShippingPolicy, }
|
|
|
31
42
|
*/
|
|
32
43
|
import { paymentCasesRepository } from "../../../../features/payment-cases/repository/payment-cases.repository.js";
|
|
33
44
|
import { computeProtectionFees } from "../../../shared/features/shipping/protections.js";
|
|
45
|
+
/* S5a — one payer, N destinations. Pure: the rates come in snapshotted, and
|
|
46
|
+
* an UNPRICED arrangement is refused rather than quoted at ₹0 (D44's lesson). */
|
|
47
|
+
import { countDestinations, quoteSharedPurchase, } from "../../../shared/features/shipping/shared-purchase.js";
|
|
34
48
|
import { resolveCouponCategorySlugs } from "../../../../features/promotions/actions/coupon-actions.js";
|
|
35
49
|
import { assertCheckoutLane, assertLockedLinesStillValid, finalizeLockedLines, } from "./locked-lines.js";
|
|
36
50
|
import { activeLane, defaultCheckoutItems, resolveCheckoutItems, laneOf, } from "../../../shared/checkout/lanes.js";
|
|
@@ -262,6 +276,10 @@ opts) {
|
|
|
262
276
|
shippingPolicy: DEFAULT_STORE_SHIPPING_POLICY,
|
|
263
277
|
shippingUnavailable: false,
|
|
264
278
|
storeProtections: { shipping: false, sale: false },
|
|
279
|
+
// No store, so no seller to absorb a redemption.
|
|
280
|
+
acceptsPoints: false,
|
|
281
|
+
// No store, so nobody to pack N parcels.
|
|
282
|
+
sharedPurchaseRates: { allowSharedPurchase: false },
|
|
265
283
|
// No store, so no seller registration — the caller falls back to the
|
|
266
284
|
// platform's via resolveSupplierIdentity.
|
|
267
285
|
storeTaxIdentity: undefined,
|
|
@@ -381,6 +399,13 @@ opts) {
|
|
|
381
399
|
shipping: store?.supportsShippingProtection === true,
|
|
382
400
|
sale: store?.supportsSaleProtection === true,
|
|
383
401
|
},
|
|
402
|
+
acceptsPoints: storeAcceptsPoints(store, storeId),
|
|
403
|
+
// S5a — raw, so the caller can tell "not configured" from ₹0.
|
|
404
|
+
sharedPurchaseRates: {
|
|
405
|
+
allowSharedPurchase: store?.allowSharedPurchase === true,
|
|
406
|
+
sharedPurchaseHandlingFee: store?.sharedPurchaseHandlingFee,
|
|
407
|
+
sharedPurchaseExtraShippingPerDestination: store?.sharedPurchaseExtraShippingPerDestination,
|
|
408
|
+
},
|
|
384
409
|
/*
|
|
385
410
|
* The seller's own tax registration, returned from the store read this
|
|
386
411
|
* function ALREADY does.
|
|
@@ -594,6 +619,112 @@ function dispatchOrderConfirmationEmails(emailsToSend) {
|
|
|
594
619
|
return;
|
|
595
620
|
Promise.all(emailsToSend.map((e) => sendOrderConfirmationEmail(e))).catch((err) => serverLogger.error("Order confirmation email error:", err));
|
|
596
621
|
}
|
|
622
|
+
/**
|
|
623
|
+
* The buyer's points balance, or **0** if it cannot be read.
|
|
624
|
+
*
|
|
625
|
+
* 🛑 Failing OPEN to zero is the safe direction and the only safe direction: a
|
|
626
|
+
* balance we could not read becomes "no discount applied", so the buyer is
|
|
627
|
+
* charged the full amount and their points are untouched. The inverse — guessing
|
|
628
|
+
* a balance — would reduce the charge against points that may not exist, which
|
|
629
|
+
* is the shape of the deleted D46 block (it debited points and reduced nothing;
|
|
630
|
+
* this is the same mistake with the signs swapped).
|
|
631
|
+
*
|
|
632
|
+
* A buyer who opted in and sees no discount can retry. There is no recoverable
|
|
633
|
+
* outcome on the other side.
|
|
634
|
+
*/
|
|
635
|
+
async function safePointsBalance(userId) {
|
|
636
|
+
try {
|
|
637
|
+
return await pointsRepository.getBalance(userId);
|
|
638
|
+
}
|
|
639
|
+
catch (err) {
|
|
640
|
+
const normalized = normalizeError(err);
|
|
641
|
+
serverLogger.warn("[checkout] points balance unreadable — charging full amount", {
|
|
642
|
+
userId,
|
|
643
|
+
error: normalized.message,
|
|
644
|
+
});
|
|
645
|
+
return 0;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
/**
|
|
649
|
+
* Debit the points an order was discounted by — the second half of the tender.
|
|
650
|
+
*
|
|
651
|
+
* 🛑 ONE implementation, called from BOTH order-creation paths. They are
|
|
652
|
+
* ~200-line twins that have already drifted once (add-on fees, Root Cause
|
|
653
|
+
* #65), and a money rule reachable by copy-paste gets copied (Root Cause #75).
|
|
654
|
+
*
|
|
655
|
+
* Runs AFTER the order exists and BEFORE the gateway is asked for anything.
|
|
656
|
+
* That ordering is the whole D46 fix: the deleted block ran after capture, so a
|
|
657
|
+
* redemption that succeeded took points from a buyer who had just paid full
|
|
658
|
+
* price while its own comment claimed "the balance intact".
|
|
659
|
+
*
|
|
660
|
+
* 🛑 A REFUSED REDEEM REVERTS THE DISCOUNT; it is never swallowed. `redeem`
|
|
661
|
+
* re-reads the balance inside its own transaction and refuses rather than going
|
|
662
|
+
* negative — so a refusal means the budget was wrong and the order is recording
|
|
663
|
+
* a discount nobody funded. Reverting is the only honest response: the buyer
|
|
664
|
+
* pays full price, which they can see and query, where an unfunded discount is
|
|
665
|
+
* a silent under-collection on every order that hits it.
|
|
666
|
+
*
|
|
667
|
+
* `orderId` is the natural idempotency key — this runs once per created order,
|
|
668
|
+
* and an order is created once.
|
|
669
|
+
*/
|
|
670
|
+
async function settlePointsTender(input) {
|
|
671
|
+
if (input.pointsApplied <= 0)
|
|
672
|
+
return;
|
|
673
|
+
try {
|
|
674
|
+
await pointsRepository.redeem({
|
|
675
|
+
userId: input.userId,
|
|
676
|
+
points: input.pointsApplied,
|
|
677
|
+
orderId: input.orderId,
|
|
678
|
+
description: `Points applied to order ${input.orderId}`,
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
catch (err) {
|
|
682
|
+
const normalized = normalizeError(err);
|
|
683
|
+
serverLogger.error("[checkout] points redemption refused — reverting the discount", {
|
|
684
|
+
orderId: input.orderId,
|
|
685
|
+
userId: input.userId,
|
|
686
|
+
pointsApplied: input.pointsApplied,
|
|
687
|
+
error: normalized.message,
|
|
688
|
+
});
|
|
689
|
+
// Hand the budget back, so a later group in the same checkout is not
|
|
690
|
+
// charged for a spend that did not happen.
|
|
691
|
+
input.pointsBudget.remaining += input.pointsApplied;
|
|
692
|
+
await unitOfWork.orders
|
|
693
|
+
.update(input.orderId, { pointsApplied: undefined, amountDue: undefined })
|
|
694
|
+
.catch((e) => {
|
|
695
|
+
/*
|
|
696
|
+
* 🛑 The one genuinely bad outcome, so it is reported rather than
|
|
697
|
+
* logged: the order still says `amountDue`, the points were never
|
|
698
|
+
* taken, and the gateway is about to be asked for the discounted
|
|
699
|
+
* figure. A case names the amount and the order; `safeRead`'s silent
|
|
700
|
+
* degrade would be wrong here because nothing downstream can detect it.
|
|
701
|
+
*/
|
|
702
|
+
const inner = normalizeError(e);
|
|
703
|
+
serverLogger.error("[checkout] points revert FAILED — order under-collects", {
|
|
704
|
+
orderId: input.orderId,
|
|
705
|
+
amountUnfunded: input.pointsApplied,
|
|
706
|
+
error: inner.message,
|
|
707
|
+
});
|
|
708
|
+
void paymentCasesRepository()
|
|
709
|
+
.openOrObserve({
|
|
710
|
+
kind: "amount_mismatch",
|
|
711
|
+
// The problem is this order's unfunded discount, so the subject is
|
|
712
|
+
// the order — one case however many times it is observed.
|
|
713
|
+
subjectId: input.orderId,
|
|
714
|
+
severity: "error",
|
|
715
|
+
entityType: "order",
|
|
716
|
+
entityId: input.orderId,
|
|
717
|
+
orderIds: [input.orderId],
|
|
718
|
+
title: "Order carries a points discount that was never funded",
|
|
719
|
+
detail: `Redemption of ${input.pointsApplied} points was refused and the ` +
|
|
720
|
+
`revert also failed, so this order will be captured at its ` +
|
|
721
|
+
`discounted amountDue with no matching ledger debit.`,
|
|
722
|
+
amountAtRisk: input.pointsApplied,
|
|
723
|
+
})
|
|
724
|
+
.catch(() => undefined);
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
}
|
|
597
728
|
/**
|
|
598
729
|
* Places one order for one seller-group of the checkout cart — fee math
|
|
599
730
|
* (COD deposit/handling, EMI schedule), coupon apportionment, order-doc
|
|
@@ -602,7 +733,7 @@ function dispatchOrderConfirmationEmails(emailsToSend) {
|
|
|
602
733
|
* so the caller can accumulate the checkout-wide `total`.
|
|
603
734
|
*/
|
|
604
735
|
async function createOrderForGroup(group, orderType, ctx) {
|
|
605
|
-
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, selectedShippingQuote, } = ctx;
|
|
736
|
+
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, selectedShippingQuote, useAllPoints, pointsBudget, } = ctx;
|
|
606
737
|
const firstItem = group[0].item;
|
|
607
738
|
const firstProduct = group[0].product;
|
|
608
739
|
// Add-ons are this store's choice, not the cart's — see OrderGroupContext.
|
|
@@ -650,7 +781,7 @@ async function createOrderForGroup(group, orderType, ctx) {
|
|
|
650
781
|
* grouped line the two differ.
|
|
651
782
|
*/
|
|
652
783
|
const groupGoodsValue = group.reduce((sum, { item, product }) => sum + lineTotalFor(item, product), 0);
|
|
653
|
-
const { shippingFee: resolvedShippingFee, storeOwnerId, storeEmiEnabled, storeState, storeProtections, storeTaxIdentity } = await resolveShippingCost(firstItem.storeId, { goodsValue: groupGoodsValue });
|
|
784
|
+
const { shippingFee: resolvedShippingFee, storeOwnerId, storeEmiEnabled, storeState, storeProtections, storeTaxIdentity, acceptsPoints, sharedPurchaseRates } = await resolveShippingCost(firstItem.storeId, { goodsValue: groupGoodsValue });
|
|
654
785
|
/*
|
|
655
786
|
* 🛑 The buyer's quote WINS over the seller's configured rate, and it is
|
|
656
787
|
* re-rounded here rather than trusted as sent.
|
|
@@ -818,6 +949,47 @@ async function createOrderForGroup(group, orderType, ctx) {
|
|
|
818
949
|
groupCouponCodes.add(discount.code);
|
|
819
950
|
}
|
|
820
951
|
}
|
|
952
|
+
/*
|
|
953
|
+
* ── S5a: a shared purchase — one payer, N destinations ────────────────────
|
|
954
|
+
*
|
|
955
|
+
* Computed BEFORE `orderTotal` because it is a term in it. The rates come
|
|
956
|
+
* off the store document and are SNAPSHOTTED onto the order below: a
|
|
957
|
+
* coordinator commits against a quoted total, and re-reading a rate at
|
|
958
|
+
* settle would let a seller raise it afterwards. That is D61's rule, and the
|
|
959
|
+
* precedent is the refund path reading `finalSale` from the order snapshot
|
|
960
|
+
* for exactly this reason.
|
|
961
|
+
*
|
|
962
|
+
* 🛑 `countDestinations` counts DISTINCT addresses and collapses every line
|
|
963
|
+
* with no `destinationAddressId` into one bucket — the order's own address.
|
|
964
|
+
* Counting items instead would charge an ordinary three-item order for two
|
|
965
|
+
* extra legs it never asked for.
|
|
966
|
+
*/
|
|
967
|
+
const sharedQuote = quoteSharedPurchase({
|
|
968
|
+
destinationCount: countDestinations(group.map(({ item }) => item)),
|
|
969
|
+
rates: sharedPurchaseRates,
|
|
970
|
+
});
|
|
971
|
+
/*
|
|
972
|
+
* 🛑 An UNPRICED arrangement is refused, not given away. The seller opted in
|
|
973
|
+
* without setting a rate, which is not consent to do the extra packing for
|
|
974
|
+
* free — and quoting ₹0 is exactly D44: `shippingUnavailable` carried a
|
|
975
|
+
* docstring saying "the caller MUST act on this rather than trusting
|
|
976
|
+
* `shippingFee: 0`", had zero consumers, and every `estimate`-mode store
|
|
977
|
+
* shipped free on every path as a result.
|
|
978
|
+
*/
|
|
979
|
+
if (sharedQuote.unpriced) {
|
|
980
|
+
throw new ValidationError("This seller has not set a price for splitting an order across addresses. " +
|
|
981
|
+
"Send everything to one address, or contact the seller.");
|
|
982
|
+
}
|
|
983
|
+
/*
|
|
984
|
+
* A multi-destination order from a store that has NOT opted in. The quote
|
|
985
|
+
* prices; it does not authorise — so the refusal is here, where the buyer
|
|
986
|
+
* can be told something true and specific.
|
|
987
|
+
*/
|
|
988
|
+
if (!sharedQuote.isShared &&
|
|
989
|
+
countDestinations(group.map(({ item }) => item)) > 1) {
|
|
990
|
+
throw new ValidationError("This seller ships one order to one address. Remove the extra delivery addresses, " +
|
|
991
|
+
"or order the items separately.");
|
|
992
|
+
}
|
|
821
993
|
// The platform commission is now charged on EVERY payment method. It used to
|
|
822
994
|
// be added only on the online-payment path, so COD / UPI-manual / cash / EMI
|
|
823
995
|
// buyers silently never paid it.
|
|
@@ -848,7 +1020,7 @@ async function createOrderForGroup(group, orderType, ctx) {
|
|
|
848
1020
|
* compounds nine half-paisa errors instead of one, which is how a total
|
|
849
1021
|
* drifts from the sum of the lines shown beside it.
|
|
850
1022
|
*/
|
|
851
|
-
roundRupees(Math.max(0, groupTotal - couponDiscount) + shippingFee + codHandlingFee + giftWrapFee + shipmentProtectionFee + saleProtectionFee + platformFee + platformFeeGst + (emiSchedule?.surchargeAmount ?? 0));
|
|
1023
|
+
roundRupees(Math.max(0, groupTotal - couponDiscount) + shippingFee + codHandlingFee + giftWrapFee + shipmentProtectionFee + saleProtectionFee + platformFee + platformFeeGst + (emiSchedule?.surchargeAmount ?? 0) + sharedQuote.total);
|
|
852
1024
|
/*
|
|
853
1025
|
* ── D43: the COD deposit/remaining split, on the ORDER TOTAL ──────────────
|
|
854
1026
|
*
|
|
@@ -901,6 +1073,44 @@ async function createOrderForGroup(group, orderType, ctx) {
|
|
|
901
1073
|
const displayedUpiId = hasPaymentWindow
|
|
902
1074
|
? await resolveDisplayedUpiId(firstItem.storeId, siteContactUpiVpa)
|
|
903
1075
|
: undefined;
|
|
1076
|
+
/*
|
|
1077
|
+
* ── POINTS TENDER (D46/D47) — a STAGE, after `orderTotal` is rounded ──────
|
|
1078
|
+
*
|
|
1079
|
+
* The deleted code redeemed points AFTER the orders existed and after the
|
|
1080
|
+
* gateway had captured `orderTotal`, so a successful redemption took points
|
|
1081
|
+
* from a buyer who had just paid full price — while its own comment claimed
|
|
1082
|
+
* "the balance intact". This is the same feature done as a pipeline stage:
|
|
1083
|
+
* `amountDue` is derived here, and `amountDue` is what the gateway captures
|
|
1084
|
+
* and what the post-capture guard compares.
|
|
1085
|
+
*
|
|
1086
|
+
* 🛑 `useAllPoints` is the only client input and it is a BOOLEAN (decision
|
|
1087
|
+
* 24). The amount comes from the live balance and the 50% cap; a
|
|
1088
|
+
* client-supplied money figure is a client-supplied discount.
|
|
1089
|
+
*
|
|
1090
|
+
* 🛑 Per group, because the ACCEPTING store absorbs the redemption against
|
|
1091
|
+
* its own payout (decision 3). A cart spanning an accepting and a
|
|
1092
|
+
* non-accepting store must not pool the discount — so this reads
|
|
1093
|
+
* `acceptsPoints`, which `resolveShippingCost` resolved from the store
|
|
1094
|
+
* document it had already fetched.
|
|
1095
|
+
*
|
|
1096
|
+
* 🛑 `pointsBudget` is a SHARED MUTABLE REMAINDER, read once per checkout by
|
|
1097
|
+
* the caller and decremented here — the same shape as
|
|
1098
|
+
* `couponUsageAccumulator` and `platformFeeByStore`. Reading the balance
|
|
1099
|
+
* per group would let two groups each spend the whole balance: `redeem`
|
|
1100
|
+
* would then refuse the second one inside its transaction, leaving an order
|
|
1101
|
+
* recorded at a discount that was never funded. One budget, drawn down in
|
|
1102
|
+
* order, is what makes the sum of the groups equal what the buyer has.
|
|
1103
|
+
*/
|
|
1104
|
+
const pointsOptedIn = useAllPoints === true;
|
|
1105
|
+
const pointsAccepted = pointsOptedIn && acceptsPoints;
|
|
1106
|
+
const { pointsApplied, amountDue } = resolvePointsTender({
|
|
1107
|
+
orderTotal,
|
|
1108
|
+
balance: pointsAccepted ? pointsBudget.remaining : 0,
|
|
1109
|
+
accepted: pointsAccepted,
|
|
1110
|
+
optedIn: pointsOptedIn,
|
|
1111
|
+
});
|
|
1112
|
+
if (pointsApplied > 0)
|
|
1113
|
+
pointsBudget.remaining -= pointsApplied;
|
|
904
1114
|
// Both creation paths call the SAME builder — two hand-written copies is the
|
|
905
1115
|
// create-vs-create axis of Root Cause #39, and these two functions have
|
|
906
1116
|
// already drifted once before (add-on fees, Root Cause #65).
|
|
@@ -914,6 +1124,23 @@ async function createOrderForGroup(group, orderType, ctx) {
|
|
|
914
1124
|
quantity: totalQuantity,
|
|
915
1125
|
unitPrice: unitPriceFor(firstItem, firstProduct),
|
|
916
1126
|
totalPrice: orderTotal,
|
|
1127
|
+
/*
|
|
1128
|
+
* D46/D47 — what the points paid for, and what is left to collect.
|
|
1129
|
+
*
|
|
1130
|
+
* 🛑 `totalPrice` stays the FULL order value and is not reduced. It is the
|
|
1131
|
+
* value of the supply, it is what the GST breakdown above was computed on,
|
|
1132
|
+
* and it is what `refundableBase` measures against — netting the points
|
|
1133
|
+
* off it would silently shrink the refundable base and the taxable value
|
|
1134
|
+
* at once. Points are a DISCOUNT settled against goods (decision 5), so
|
|
1135
|
+
* they reduce the amount COLLECTED, not the amount supplied.
|
|
1136
|
+
*
|
|
1137
|
+
* 🛑 Both are written only when non-zero, so an order placed without
|
|
1138
|
+
* points carries neither field and every reader's `amountDue ?? totalPrice`
|
|
1139
|
+
* fallback resolves to the right number on the 100% of historical orders
|
|
1140
|
+
* that predate this.
|
|
1141
|
+
*/
|
|
1142
|
+
pointsApplied: pointsApplied > 0 ? pointsApplied : undefined,
|
|
1143
|
+
amountDue: pointsApplied > 0 ? amountDue : undefined,
|
|
917
1144
|
currency: firstItem.currency ?? getDefaultCurrency(),
|
|
918
1145
|
storeId: firstItem.storeId || undefined,
|
|
919
1146
|
storeName: firstItem.storeName || undefined,
|
|
@@ -971,11 +1198,34 @@ async function createOrderForGroup(group, orderType, ctx) {
|
|
|
971
1198
|
shipmentProtectionFee: !adminBypass && shipmentProtectionFee > 0 ? shipmentProtectionFee : undefined,
|
|
972
1199
|
saleProtectionAddon: !adminBypass && saleProtectionFee > 0 ? true : undefined,
|
|
973
1200
|
saleProtectionFee: !adminBypass && saleProtectionFee > 0 ? saleProtectionFee : undefined,
|
|
1201
|
+
/*
|
|
1202
|
+
* S5a — the arrangement, snapshotted. Written only when it IS a shared
|
|
1203
|
+
* purchase, so an ordinary order carries no field at all and every reader
|
|
1204
|
+
* can test presence rather than compare a count to 1.
|
|
1205
|
+
*
|
|
1206
|
+
* 🛑 The FEES AS CHARGED, not the store's current rates. That is the whole
|
|
1207
|
+
* point of a snapshot: an order refunded in six months must reverse what
|
|
1208
|
+
* was collected, not what the seller charges by then.
|
|
1209
|
+
*/
|
|
1210
|
+
sharedPurchase: sharedQuote.isShared
|
|
1211
|
+
? {
|
|
1212
|
+
destinationCount: countDestinations(group.map(({ item }) => item)),
|
|
1213
|
+
handlingFee: sharedQuote.handlingFee,
|
|
1214
|
+
extraShippingFee: sharedQuote.extraShippingFee,
|
|
1215
|
+
}
|
|
1216
|
+
: undefined,
|
|
974
1217
|
taxableAmount: gstBreakdown?.taxableAmount,
|
|
975
1218
|
/* Exempt goods are a reportable nil-rated supply, kept OUT of
|
|
976
1219
|
* `taxableAmount` so the taxed base stays the base the tax was charged on,
|
|
977
1220
|
* and recorded here so the invoice's columns can reconcile. */
|
|
978
1221
|
exemptAmount: gstBreakdown?.exemptAmount || undefined,
|
|
1222
|
+
/*
|
|
1223
|
+
* Phase 7 — the place of supply, which is the INPUT the CGST/SGST-vs-IGST
|
|
1224
|
+
* split above was computed from. Only the split's RESULT was recorded
|
|
1225
|
+
* before, so an order could not state why it was taxed the way it was, and
|
|
1226
|
+
* GSTR-1's mandatory place-of-supply column had nothing to read.
|
|
1227
|
+
*/
|
|
1228
|
+
placeOfSupply: buyerState || undefined,
|
|
979
1229
|
/* The supplier of record AS IT STANDS NOW. A tax invoice without the
|
|
980
1230
|
* supplier's GSTIN is not a tax invoice, and the invoice page cannot read
|
|
981
1231
|
* it from site settings — `gst` is in PRIVATE_SITE_FIELDS. See the schema.
|
|
@@ -1039,6 +1289,9 @@ async function createOrderForGroup(group, orderType, ctx) {
|
|
|
1039
1289
|
...extraOrderFields,
|
|
1040
1290
|
});
|
|
1041
1291
|
orderIds.push(order.id);
|
|
1292
|
+
// The second half of the points tender — see `settlePointsTender`. Runs
|
|
1293
|
+
// before the caller asks the gateway for anything.
|
|
1294
|
+
await settlePointsTender({ orderId: order.id, userId: uid, pointsApplied, pointsBudget });
|
|
1042
1295
|
// Close the loop on any locked line this order settled: flip the offer to
|
|
1043
1296
|
// "paid" (its terminal status had NO server-side writer before 2026-08-21,
|
|
1044
1297
|
// so an accepted offer could be re-added and re-ordered) and back-link the
|
|
@@ -1388,6 +1641,24 @@ export async function createCheckoutOrderAction(input) {
|
|
|
1388
1641
|
g[0].item.storeId,
|
|
1389
1642
|
g.reduce((sum, { item, product }) => sum + lineTotalFor(item, product), 0),
|
|
1390
1643
|
]), computeCheckoutFees(cartSubtotal, commissions));
|
|
1644
|
+
/*
|
|
1645
|
+
* ── The checkout's points budget, read ONCE (D46/D47) ─────────────────────
|
|
1646
|
+
*
|
|
1647
|
+
* 🛑 One read for the whole checkout, not one per order group. Rule #6
|
|
1648
|
+
* budgets ~3 sequential Firestore round-trips per request, and a cart
|
|
1649
|
+
* spanning four sellers would otherwise pay four reads to learn one number.
|
|
1650
|
+
*
|
|
1651
|
+
* 🛑 And ZERO when the buyer did not opt in, so an ordinary checkout performs
|
|
1652
|
+
* no points read at all — the common case must not pay for a feature it is
|
|
1653
|
+
* not using.
|
|
1654
|
+
*
|
|
1655
|
+
* 🛑 `adminBypass` is deliberately excluded: that path takes no payment, so
|
|
1656
|
+
* there is nothing for points to reduce, and debiting a buyer's balance for
|
|
1657
|
+
* an order the admin placed free would be a theft with an audit-log entry
|
|
1658
|
+
* saying otherwise.
|
|
1659
|
+
*/
|
|
1660
|
+
const useAllPoints = cart.useAllPoints === true && !adminBypass;
|
|
1661
|
+
const pointsBudget = { remaining: useAllPoints ? await safePointsBalance(uid) : 0 };
|
|
1391
1662
|
const groupCtx = {
|
|
1392
1663
|
paymentMethod,
|
|
1393
1664
|
emiTenureMonths,
|
|
@@ -1415,6 +1686,8 @@ export async function createCheckoutOrderAction(input) {
|
|
|
1415
1686
|
platformFeeByStore,
|
|
1416
1687
|
selectedShippingQuote: input.selectedShippingQuote,
|
|
1417
1688
|
productById: checkoutProductById,
|
|
1689
|
+
useAllPoints,
|
|
1690
|
+
pointsBudget,
|
|
1418
1691
|
};
|
|
1419
1692
|
for (const { items: group, orderType } of orderGroups) {
|
|
1420
1693
|
total += await createOrderForGroup(group, orderType, groupCtx);
|
|
@@ -1448,18 +1721,26 @@ export async function createCheckoutOrderAction(input) {
|
|
|
1448
1721
|
* Deleting it leaves a promise with no mechanism, which is a disappointment.
|
|
1449
1722
|
* Keeping it left a mechanism that silently takes points, which is a theft.
|
|
1450
1723
|
*
|
|
1451
|
-
* ##
|
|
1452
|
-
*
|
|
1453
|
-
* Points are a TENDER, so they
|
|
1454
|
-
*
|
|
1455
|
-
*
|
|
1456
|
-
*
|
|
1457
|
-
*
|
|
1458
|
-
*
|
|
1459
|
-
*
|
|
1460
|
-
*
|
|
1461
|
-
*
|
|
1462
|
-
*
|
|
1724
|
+
* ## ✅ The real tender was BUILT 2026-10-22 — and it is not here
|
|
1725
|
+
*
|
|
1726
|
+
* 🛑 Do not look for it at this line. Points are a TENDER, so they are a
|
|
1727
|
+
* STAGE inside each order-creation path, immediately after that path's single
|
|
1728
|
+
* `roundRupees` and BEFORE anything is written or captured:
|
|
1729
|
+
*
|
|
1730
|
+
* - `resolvePointsTender` (`_internal/shared/checkout/order-math.ts`) —
|
|
1731
|
+
* the arithmetic, beside every other money rule.
|
|
1732
|
+
* - the stage in `createOrderForGroup` and `createPhonePeGroupOrder`,
|
|
1733
|
+
* which write `pointsApplied` + `amountDue` onto the order.
|
|
1734
|
+
* - `settlePointsTender` — ONE debit implementation shared by both paths,
|
|
1735
|
+
* running after the order exists so the discount and the debit are two
|
|
1736
|
+
* halves of one recorded fact, and reverting the discount if `redeem`
|
|
1737
|
+
* refuses.
|
|
1738
|
+
*
|
|
1739
|
+
* This note stays because the mistake is worth keeping, but a comment saying
|
|
1740
|
+
* a feature is "missing" while it sits 800 lines above is its own trap — it
|
|
1741
|
+
* is how `assertReturnWindowOpen` got reported as absent when my own comment
|
|
1742
|
+
* was shadowing the real answer. The accurate statement is: the tender is
|
|
1743
|
+
* built, and this is the wrong place for it.
|
|
1463
1744
|
*/
|
|
1464
1745
|
dispatchOrderConfirmationEmails(emailsToSend);
|
|
1465
1746
|
serverLogger.info(`createCheckoutOrderAction: ${orderIds.length} order(s) placed for uid=${uid}` +
|
|
@@ -1493,7 +1774,11 @@ const EMPTY_PRICING_PREVIEW = {
|
|
|
1493
1774
|
couponDiscount: 0,
|
|
1494
1775
|
platformFee: 0,
|
|
1495
1776
|
gstOnFee: 0,
|
|
1777
|
+
sharedPurchaseFee: 0,
|
|
1496
1778
|
total: 0,
|
|
1779
|
+
pointsApplied: 0,
|
|
1780
|
+
amountDue: 0,
|
|
1781
|
+
pointsBalance: 0,
|
|
1497
1782
|
stores: [],
|
|
1498
1783
|
};
|
|
1499
1784
|
/**
|
|
@@ -1566,7 +1851,7 @@ export async function previewCheckoutPricing(input) {
|
|
|
1566
1851
|
* and then the order path (equally unpassed) charged it. Preview and charge
|
|
1567
1852
|
* agreed, and both were wrong in the same direction.
|
|
1568
1853
|
*/
|
|
1569
|
-
const { shippingFee: groupShippingFee, storeState, storeProtections, storeEmiEnabled } = await resolveShippingCost(firstItem.storeId, { goodsValue: groupTotal });
|
|
1854
|
+
const { shippingFee: groupShippingFee, storeState, storeProtections, storeEmiEnabled, acceptsPoints, sharedPurchaseRates: previewSharedRates } = await resolveShippingCost(firstItem.storeId, { goodsValue: groupTotal });
|
|
1570
1855
|
// Add-ons are per store, read off the cart document — the same entry
|
|
1571
1856
|
// createOrderForGroup will read when the order is actually placed.
|
|
1572
1857
|
const addons = cart.storeAddons?.[firstItem.storeId] ?? {};
|
|
@@ -1617,6 +1902,28 @@ export async function previewCheckoutPricing(input) {
|
|
|
1617
1902
|
* seller cannot flip the switch after the sale).
|
|
1618
1903
|
*/
|
|
1619
1904
|
const groupHasFinalSale = group.some(({ product }) => product?.finalSale === true);
|
|
1905
|
+
/*
|
|
1906
|
+
* ── S5a in the PREVIEW ───────────────────────────────────────────────────
|
|
1907
|
+
*
|
|
1908
|
+
* 🛑 Here because a coordinator commits against this figure. The plan is
|
|
1909
|
+
* explicit that the fees "must be shown before the coordinator commits" —
|
|
1910
|
+
* a handling fee plus N−1 shipping legs discovered afterwards is exactly
|
|
1911
|
+
* the surprise the arrangement makes expensive.
|
|
1912
|
+
*
|
|
1913
|
+
* The same function and the same `countDestinations` as both order paths,
|
|
1914
|
+
* so the shown figure and the charged one cannot diverge. D37 is the record
|
|
1915
|
+
* of what ONE money term reaching three of four paths costs: the buyer was
|
|
1916
|
+
* shown one number, charged another, and the order recorded the first.
|
|
1917
|
+
*
|
|
1918
|
+
* 🛑 A preview NEVER throws, so an unpriced arrangement contributes 0 here
|
|
1919
|
+
* rather than raising — `quoteSharedPurchase` reports it on `unpriced` and
|
|
1920
|
+
* the ORDER path is what refuses. A preview that threw would render the
|
|
1921
|
+
* whole cart as an error page for a seller's missing config.
|
|
1922
|
+
*/
|
|
1923
|
+
const groupSharedPurchaseFee = quoteSharedPurchase({
|
|
1924
|
+
destinationCount: countDestinations(group.map(({ item }) => item)),
|
|
1925
|
+
rates: previewSharedRates,
|
|
1926
|
+
}).total;
|
|
1620
1927
|
const { shippingProtectionFee: groupShipmentProtectionFee, saleProtectionFee: groupSaleProtectionFee, } = computeProtectionFees({
|
|
1621
1928
|
subtotal: groupTotal,
|
|
1622
1929
|
selected: {
|
|
@@ -1701,10 +2008,19 @@ export async function previewCheckoutPricing(input) {
|
|
|
1701
2008
|
groupEmiSurcharge +
|
|
1702
2009
|
groupGiftWrapFee +
|
|
1703
2010
|
groupShipmentProtectionFee +
|
|
1704
|
-
groupSaleProtectionFee
|
|
2011
|
+
groupSaleProtectionFee +
|
|
2012
|
+
// S5a — a term in the per-store total, so the seller card and the
|
|
2013
|
+
// order it previews agree.
|
|
2014
|
+
groupSharedPurchaseFee,
|
|
1705
2015
|
supportsShippingProtection: storeProtections.shipping,
|
|
1706
2016
|
supportsSaleProtection: storeProtections.sale,
|
|
1707
2017
|
hasFinalSaleItem: groupHasFinalSale,
|
|
2018
|
+
acceptsPoints,
|
|
2019
|
+
sharedPurchaseFee: groupSharedPurchaseFee,
|
|
2020
|
+
// Filled by the tender pass below — it needs each store's share of the
|
|
2021
|
+
// one capped commission, which is only allocatable once every group is
|
|
2022
|
+
// known.
|
|
2023
|
+
pointsApplied: 0,
|
|
1708
2024
|
});
|
|
1709
2025
|
}
|
|
1710
2026
|
const sum = (pick) => stores.reduce((acc, s) => acc + pick(s), 0);
|
|
@@ -1714,11 +2030,21 @@ export async function previewCheckoutPricing(input) {
|
|
|
1714
2030
|
const giftWrapFeeTotal = sum((s) => s.giftWrapFee);
|
|
1715
2031
|
const shipmentProtectionFeeTotal = sum((s) => s.shipmentProtectionFee);
|
|
1716
2032
|
const saleProtectionFeeTotal = sum((s) => s.saleProtectionFee);
|
|
2033
|
+
// S5a — summed from the same per-store figures, so the aggregate cannot
|
|
2034
|
+
// disagree with the seller cards it is built from.
|
|
2035
|
+
const sharedPurchaseFeeTotal = sum((s) => s.sharedPurchaseFee);
|
|
1717
2036
|
const gstAmount = sum((s) => s.gstAmount);
|
|
1718
2037
|
const couponDiscount = sum((s) => s.couponDiscount);
|
|
1719
2038
|
// One capped commission for the whole checkout, on every payment method —
|
|
1720
2039
|
// deliberately not per store, so it is not added to the per-store rows above.
|
|
1721
|
-
|
|
2040
|
+
// The whole object is kept (not just the two fields) because the points
|
|
2041
|
+
// tender below re-allocates it per store through `allocateCheckoutFees`,
|
|
2042
|
+
// which takes a `CheckoutFees` — and `totalFee` carries the
|
|
2043
|
+
// `minimumTransactionFee` floor, which is exactly the field D41 found had no
|
|
2044
|
+
// consumer anywhere. Destructuring it away here would be how it loses its
|
|
2045
|
+
// second one.
|
|
2046
|
+
const checkoutFees = computeCheckoutFees(cartSubtotal, commissions);
|
|
2047
|
+
const { platformFee, gstOnFee } = checkoutFees;
|
|
1722
2048
|
/*
|
|
1723
2049
|
* 🛑 `gstAmount` is NOT a term, and leaving it in was a shown-vs-charged
|
|
1724
2050
|
* drift waiting on a settings flag.
|
|
@@ -1751,8 +2077,52 @@ export async function previewCheckoutPricing(input) {
|
|
|
1751
2077
|
giftWrapFeeTotal +
|
|
1752
2078
|
shipmentProtectionFeeTotal +
|
|
1753
2079
|
saleProtectionFeeTotal +
|
|
2080
|
+
// S5a — a real additive charge, so it belongs in the column with the other
|
|
2081
|
+
// fees rather than below the total like the tax-inclusive `gstAmount`.
|
|
2082
|
+
sharedPurchaseFeeTotal +
|
|
1754
2083
|
platformFee +
|
|
1755
2084
|
gstOnFee;
|
|
2085
|
+
/*
|
|
2086
|
+
* ── THE POINTS TENDER, PREVIEWED (D46/D47) ────────────────────────────────
|
|
2087
|
+
*
|
|
2088
|
+
* 🛑 Applied PER STORE against the SAME base the order path uses, which is
|
|
2089
|
+
* why this runs here and not inside the loop above: the base includes each
|
|
2090
|
+
* store's share of the one capped commission, and that share is only
|
|
2091
|
+
* allocatable once every group is known.
|
|
2092
|
+
*
|
|
2093
|
+
* 🛑 Per store is not a detail — it is the only shape that can agree with
|
|
2094
|
+
* what gets charged. A cart-level application gives the same answer as the
|
|
2095
|
+
* per-group one whenever every store accepts (the sum of per-group 50% caps
|
|
2096
|
+
* equals 50% of the sum), and a DIFFERENT answer the moment one store does
|
|
2097
|
+
* not: with ₹500 at an accepting store, ₹500 at a declining one and a 600
|
|
2098
|
+
* balance, per-store applies ₹250 and cart-level would promise ₹500. The
|
|
2099
|
+
* buyer would be shown a discount half of which could never be honoured.
|
|
2100
|
+
*
|
|
2101
|
+
* The iteration order matches `orderGroups`, and so does the order-creation
|
|
2102
|
+
* loop, so the budget is drawn down in the same sequence in both — which is
|
|
2103
|
+
* what makes the two sums identical rather than merely close.
|
|
2104
|
+
*/
|
|
2105
|
+
const previewUseAllPoints = cart.useAllPoints === true;
|
|
2106
|
+
const previewBalance = previewUseAllPoints ? await safePointsBalance(uid) : 0;
|
|
2107
|
+
if (previewUseAllPoints && previewBalance > 0) {
|
|
2108
|
+
const feeByStore = allocateCheckoutFees(stores.map((s) => [s.storeId, s.subtotal]), checkoutFees);
|
|
2109
|
+
const { perGroup } = allocatePointsAcrossGroups(stores.map((s) => {
|
|
2110
|
+
const share = feeByStore.get(s.storeId);
|
|
2111
|
+
return {
|
|
2112
|
+
// The SAME base `createOrderForGroup` applies the tender to: the
|
|
2113
|
+
// store's charged total plus its share of the one capped commission.
|
|
2114
|
+
// `store.total` deliberately excludes that share (it is a checkout
|
|
2115
|
+
// charge, not a per-store one), so it is added back here rather than
|
|
2116
|
+
// by widening a published field.
|
|
2117
|
+
total: roundRupees(s.total + (share?.platformFee ?? 0) + (share?.gstOnFee ?? 0)),
|
|
2118
|
+
accepted: s.acceptsPoints,
|
|
2119
|
+
};
|
|
2120
|
+
}), previewBalance, true);
|
|
2121
|
+
perGroup.forEach((applied, i) => {
|
|
2122
|
+
stores[i].pointsApplied = applied;
|
|
2123
|
+
});
|
|
2124
|
+
}
|
|
2125
|
+
const pointsApplied = sum((s) => s.pointsApplied);
|
|
1756
2126
|
return {
|
|
1757
2127
|
subtotal: cartSubtotal,
|
|
1758
2128
|
shippingFee,
|
|
@@ -1761,11 +2131,17 @@ export async function previewCheckoutPricing(input) {
|
|
|
1761
2131
|
giftWrapFee: giftWrapFeeTotal,
|
|
1762
2132
|
shipmentProtectionFee: shipmentProtectionFeeTotal,
|
|
1763
2133
|
saleProtectionFee: saleProtectionFeeTotal,
|
|
2134
|
+
sharedPurchaseFee: sharedPurchaseFeeTotal,
|
|
1764
2135
|
gstAmount,
|
|
1765
2136
|
couponDiscount,
|
|
1766
2137
|
platformFee,
|
|
1767
2138
|
gstOnFee,
|
|
1768
2139
|
total,
|
|
2140
|
+
pointsApplied,
|
|
2141
|
+
// Equal to `total` when no points apply, so every existing reader of
|
|
2142
|
+
// either figure keeps getting the right number.
|
|
2143
|
+
amountDue: roundRupees(total - pointsApplied),
|
|
2144
|
+
pointsBalance: previewBalance,
|
|
1769
2145
|
stores,
|
|
1770
2146
|
};
|
|
1771
2147
|
}
|
|
@@ -1916,7 +2292,7 @@ async function refundDroppedItemsForPhonePeCheckout(input) {
|
|
|
1916
2292
|
* from the inline version this replaced.
|
|
1917
2293
|
*/
|
|
1918
2294
|
async function createPhonePeGroupOrder(group, orderType, ctx) {
|
|
1919
|
-
const { appliedCoupons, cartSubtotal, storeAddons, platformFeeByStore, siteSettings, uid, userName, userEmail, merchantOrderId, phonepeOrderId, transactionId, shippingAddress, notes, outOfStockPolicy, unavailablePaid, orderIds, emailsToSend, couponUsageAccumulator, productById: phonepeProductById, buyerState, } = ctx;
|
|
2295
|
+
const { appliedCoupons, cartSubtotal, storeAddons, platformFeeByStore, siteSettings, uid, userName, userEmail, merchantOrderId, phonepeOrderId, transactionId, shippingAddress, notes, outOfStockPolicy, unavailablePaid, orderIds, emailsToSend, couponUsageAccumulator, productById: phonepeProductById, buyerState, useAllPoints, pointsBudget, } = ctx;
|
|
1920
2296
|
/*
|
|
1921
2297
|
* Which online gateway actually took the money, read off the registered
|
|
1922
2298
|
* provider rather than passed in — exactly one is registered per process
|
|
@@ -1932,7 +2308,7 @@ async function createPhonePeGroupOrder(group, orderType, ctx) {
|
|
|
1932
2308
|
// seller group here, duplicated from resolveShippingCost.
|
|
1933
2309
|
// D44: `groupTotal` is computed just above — pass it so a free-above
|
|
1934
2310
|
// threshold can fire and an unpriceable store is recorded.
|
|
1935
|
-
const { shippingFee, storeOwnerId, storeState, storeTaxIdentity, storeProtections } = await resolveShippingCost(firstItem.storeId, { goodsValue: groupTotal });
|
|
2311
|
+
const { shippingFee, storeOwnerId, storeState, storeTaxIdentity, storeProtections, acceptsPoints, sharedPurchaseRates } = await resolveShippingCost(firstItem.storeId, { goodsValue: groupTotal });
|
|
1936
2312
|
/*
|
|
1937
2313
|
* Same supplier-of-record rule as `createOrderForGroup`, through the same
|
|
1938
2314
|
* function. This path (PhonePe verify / webhook) is the SECOND place an
|
|
@@ -2050,9 +2426,51 @@ async function createPhonePeGroupOrder(group, orderType, ctx) {
|
|
|
2050
2426
|
/* Product GST is not a term — prices are tax-inclusive, so it is already
|
|
2051
2427
|
* inside `groupTotal`. Matches the COD/UPI path exactly; `platformFeeGst`
|
|
2052
2428
|
* stays because that is GST on our fee, charged on top. */
|
|
2429
|
+
/*
|
|
2430
|
+
* ── S5a: the shared-purchase arrangement, matching the COD/manual path ────
|
|
2431
|
+
*
|
|
2432
|
+
* Same function, same inputs, same position — before `orderTotal`, because
|
|
2433
|
+
* it is a term in it. See the fuller 🛑 at the COD/manual site for why the
|
|
2434
|
+
* rates are snapshotted and why an unpriced arrangement is refused rather
|
|
2435
|
+
* than quoted at ₹0.
|
|
2436
|
+
*
|
|
2437
|
+
* 🛑 On THIS path the money is already captured by the time this runs, so a
|
|
2438
|
+
* refusal here means a charged-with-no-order — the D64 class. That is why
|
|
2439
|
+
* `/api/payment/create-order` must apply the same term: the pre-payment
|
|
2440
|
+
* amount and this total have to agree, which is the parity C0c defect 1 was
|
|
2441
|
+
* about and the reason every addition here has to be made in three places.
|
|
2442
|
+
*/
|
|
2443
|
+
const phonepeSharedQuote = quoteSharedPurchase({
|
|
2444
|
+
destinationCount: countDestinations(group.map(({ item }) => item)),
|
|
2445
|
+
rates: sharedPurchaseRates,
|
|
2446
|
+
});
|
|
2053
2447
|
// D42: rounded once on the sum — see the COD/UPI path's 🛑 for why a raw
|
|
2054
2448
|
// float sum of seven 2dp terms reached Firestore as 1011.8000000000001.
|
|
2055
|
-
const orderTotal = roundRupees(Math.max(0, groupTotal - couponDiscount) + shippingFee + giftWrapFee + shipmentProtectionFee + saleProtectionFee + platformFee + platformFeeGst);
|
|
2449
|
+
const orderTotal = roundRupees(Math.max(0, groupTotal - couponDiscount) + shippingFee + giftWrapFee + shipmentProtectionFee + saleProtectionFee + platformFee + platformFeeGst + phonepeSharedQuote.total);
|
|
2450
|
+
/*
|
|
2451
|
+
* ── POINTS TENDER (D46/D47) — the same STAGE as the COD/manual path ───────
|
|
2452
|
+
*
|
|
2453
|
+
* Same four inputs, same function, same point in the sequence: immediately
|
|
2454
|
+
* after `orderTotal`'s single `roundRupees` and before anything is written.
|
|
2455
|
+
* Computing it here rather than in a shared builder is unavoidable — the two
|
|
2456
|
+
* paths derive `orderTotal` separately — but the ARITHMETIC is
|
|
2457
|
+
* `resolvePointsTender` in both, so they cannot disagree about the amount
|
|
2458
|
+
* the way the add-on fees once did (Root Cause #65).
|
|
2459
|
+
*
|
|
2460
|
+
* 🛑 This is the path whose capture matters most: `amountDue` is what the
|
|
2461
|
+
* Razorpay order is created for, so a divergence here is money taken that
|
|
2462
|
+
* the order does not record.
|
|
2463
|
+
*/
|
|
2464
|
+
const pointsOptedIn = useAllPoints === true;
|
|
2465
|
+
const pointsAccepted = pointsOptedIn && acceptsPoints;
|
|
2466
|
+
const { pointsApplied, amountDue } = resolvePointsTender({
|
|
2467
|
+
orderTotal,
|
|
2468
|
+
balance: pointsAccepted ? pointsBudget.remaining : 0,
|
|
2469
|
+
accepted: pointsAccepted,
|
|
2470
|
+
optedIn: pointsOptedIn,
|
|
2471
|
+
});
|
|
2472
|
+
if (pointsApplied > 0)
|
|
2473
|
+
pointsBudget.remaining -= pointsApplied;
|
|
2056
2474
|
// S-SBUNI-RULES 2026-05-13 — order-item decoration via rule registry.
|
|
2057
2475
|
// Same expansion as the COD/UPI path. This was a THIRD hand-rolled copy of
|
|
2058
2476
|
// the price rule (`isBundle ? item.price : product.price`) that — like the
|
|
@@ -2089,6 +2507,10 @@ async function createPhonePeGroupOrder(group, orderType, ctx) {
|
|
|
2089
2507
|
quantity: totalQuantity,
|
|
2090
2508
|
unitPrice: group[0].product.price,
|
|
2091
2509
|
totalPrice: orderTotal,
|
|
2510
|
+
// D46/D47 — see the identical pair in `createOrderForGroup`. `totalPrice`
|
|
2511
|
+
// stays the full value of the supply; points reduce what is COLLECTED.
|
|
2512
|
+
pointsApplied: pointsApplied > 0 ? pointsApplied : undefined,
|
|
2513
|
+
amountDue: pointsApplied > 0 ? amountDue : undefined,
|
|
2092
2514
|
currency: firstItem.currency ?? getDefaultCurrency(),
|
|
2093
2515
|
storeId: firstItem.storeId || undefined,
|
|
2094
2516
|
storeName: firstItem.storeName || undefined,
|
|
@@ -2120,7 +2542,15 @@ async function createPhonePeGroupOrder(group, orderType, ctx) {
|
|
|
2120
2542
|
paymentRecord: {
|
|
2121
2543
|
method: gatewayName === "razorpay" ? "razorpay" : "phonepe",
|
|
2122
2544
|
transactionId: transactionId ?? merchantOrderId,
|
|
2123
|
-
|
|
2545
|
+
/*
|
|
2546
|
+
* 🛑 What the gateway actually CAPTURED, which is `amountDue` once points
|
|
2547
|
+
* are applied — not the order's value. This record is the evidence a
|
|
2548
|
+
* reconciler compares against the gateway's own figure (Phase 4 R2), so
|
|
2549
|
+
* writing `orderTotal` here would open an `amount_mismatch` case on every
|
|
2550
|
+
* points-funded order: the discount would read as money we failed to
|
|
2551
|
+
* collect.
|
|
2552
|
+
*/
|
|
2553
|
+
amount: amountDue,
|
|
2124
2554
|
paidAt: new Date(),
|
|
2125
2555
|
verifiedBy: `${gatewayName}-order-status`,
|
|
2126
2556
|
verificationMethod: "order-status",
|
|
@@ -2148,6 +2578,14 @@ async function createPhonePeGroupOrder(group, orderType, ctx) {
|
|
|
2148
2578
|
// Recorded as well as charged — an order that collects a protection fee
|
|
2149
2579
|
// without a record of it cannot be reconciled against a later claim.
|
|
2150
2580
|
saleProtectionFee: saleProtectionFee > 0 ? saleProtectionFee : undefined,
|
|
2581
|
+
// S5a — snapshotted, matching the COD/manual path field for field.
|
|
2582
|
+
sharedPurchase: phonepeSharedQuote.isShared
|
|
2583
|
+
? {
|
|
2584
|
+
destinationCount: countDestinations(group.map(({ item }) => item)),
|
|
2585
|
+
handlingFee: phonepeSharedQuote.handlingFee,
|
|
2586
|
+
extraShippingFee: phonepeSharedQuote.extraShippingFee,
|
|
2587
|
+
}
|
|
2588
|
+
: undefined,
|
|
2151
2589
|
/*
|
|
2152
2590
|
* The tax breakdown, matching `createOrderForGroup` field for field.
|
|
2153
2591
|
* This path recorded NONE of it before C0c defect 1 — so an online order
|
|
@@ -2158,6 +2596,10 @@ async function createPhonePeGroupOrder(group, orderType, ctx) {
|
|
|
2158
2596
|
*/
|
|
2159
2597
|
taxableAmount: phonepeGst?.taxableAmount,
|
|
2160
2598
|
exemptAmount: phonepeGst?.exemptAmount || undefined,
|
|
2599
|
+
// Phase 7 — the place of supply, matching `createOrderForGroup` field for
|
|
2600
|
+
// field. This path recorded NONE of the tax breakdown until C0c defect 1,
|
|
2601
|
+
// which is precisely why every addition here has to be made twice.
|
|
2602
|
+
placeOfSupply: buyerState || undefined,
|
|
2161
2603
|
// Our registration at the moment of supply — see the schema. Mirrors the
|
|
2162
2604
|
// COD/UPI path so both produce an invoiceable order.
|
|
2163
2605
|
supplierGstin: supplier.gstin,
|
|
@@ -2177,6 +2619,9 @@ async function createPhonePeGroupOrder(group, orderType, ctx) {
|
|
|
2177
2619
|
...extraOrderFields,
|
|
2178
2620
|
});
|
|
2179
2621
|
orderIds.push(order.id);
|
|
2622
|
+
// The second half of the points tender — one implementation, shared with the
|
|
2623
|
+
// COD/manual path. See `settlePointsTender`.
|
|
2624
|
+
await settlePointsTender({ orderId: order.id, userId: uid, pointsApplied, pointsBudget });
|
|
2180
2625
|
// Close the loop on any locked line this order settled: flip the offer to
|
|
2181
2626
|
// "paid" (its terminal status had NO server-side writer before 2026-08-21,
|
|
2182
2627
|
// so an accepted offer could be re-added and re-ordered) and back-link the
|
|
@@ -2538,7 +2983,12 @@ export async function verifyAndPlaceGatewayOrderAction(input) {
|
|
|
2538
2983
|
// percentages, so the moment the cap landed it would have expected more
|
|
2539
2984
|
// than was actually collected and rejected every legitimate payment.
|
|
2540
2985
|
const commissionRates = siteSettings?.commissions ?? CHECKOUT_DEFAULT_COMMISSIONS;
|
|
2541
|
-
|
|
2986
|
+
// The whole object is kept so the points tender below can re-allocate it
|
|
2987
|
+
// per group — see the same note in `previewCheckoutPricing`. `totalFee`
|
|
2988
|
+
// carries the `minimumTransactionFee` floor, and destructuring it away is
|
|
2989
|
+
// how it lost every consumer once already (D41).
|
|
2990
|
+
const expectedFees = computeCheckoutFees(cartSubtotalRs, commissionRates);
|
|
2991
|
+
const { platformFee: expectedPlatformFee, gstOnFee: expectedGstOnFee } = expectedFees;
|
|
2542
2992
|
// Same per-seller-group shipping sum /api/payment/create-order charged
|
|
2543
2993
|
// upfront (and the same resolveShippingCost createPhonePeGroupOrder
|
|
2544
2994
|
// below will use to build each order's recorded totalPrice) — keeps this
|
|
@@ -2624,11 +3074,62 @@ export async function verifyAndPlaceGatewayOrderAction(input) {
|
|
|
2624
3074
|
* would make this guard demand ~₹580 more than the buyer owes on a
|
|
2625
3075
|
* ₹5,547 cart — rejecting every correctly-charged order.
|
|
2626
3076
|
*/
|
|
2627
|
-
|
|
3077
|
+
/*
|
|
3078
|
+
* ── D46/D47 — the guard must expect the amount NET OF POINTS ─────────────
|
|
3079
|
+
*
|
|
3080
|
+
* 🛑 Omitting this term would make the guard reject every correctly-charged
|
|
3081
|
+
* points order, which is worse than the overcharge it exists to catch: the
|
|
3082
|
+
* money is already captured by the time this runs, so a false rejection is
|
|
3083
|
+
* a charged-with-no-order — the D64 class.
|
|
3084
|
+
*
|
|
3085
|
+
* The bases are rebuilt here from the same terms in the same order, and the
|
|
3086
|
+
* draw-down is the same shared allocator, so the only way this can diverge
|
|
3087
|
+
* from the capture route is a change made to one and not the other. That is
|
|
3088
|
+
* precisely the drift the four-way export comment on
|
|
3089
|
+
* `allocatePointsAcrossGroups` exists to make visible.
|
|
3090
|
+
*/
|
|
3091
|
+
const guardUseAllPoints = cart.useAllPoints === true;
|
|
3092
|
+
let expectedPointsApplied = 0;
|
|
3093
|
+
if (guardUseAllPoints) {
|
|
3094
|
+
const guardBalance = await safePointsBalance(uid);
|
|
3095
|
+
if (guardBalance > 0) {
|
|
3096
|
+
const guardSubtotals = expectedShippingGroups.map((g) => [
|
|
3097
|
+
g.items[0].item.storeId,
|
|
3098
|
+
g.items.reduce((gs, { item, product }) => gs + lineTotalFor(item, product), 0),
|
|
3099
|
+
]);
|
|
3100
|
+
const guardFeeByGroup = allocateCheckoutFees(guardSubtotals, expectedFees);
|
|
3101
|
+
const guardCouponByGroup = computeCartCouponDiscount(cart.appliedCoupons ?? [], expectedShippingGroups.map((g) => ({
|
|
3102
|
+
storeId: g.items[0].item.storeId,
|
|
3103
|
+
lines: g.items.map(({ item, product }) => ({
|
|
3104
|
+
itemId: item.itemId,
|
|
3105
|
+
lineTotal: lineTotalFor(item, product),
|
|
3106
|
+
})),
|
|
3107
|
+
}))).perGroup;
|
|
3108
|
+
const guardBases = expectedShippingGroups.map((g, i) => {
|
|
3109
|
+
const storeId = g.items[0].item.storeId;
|
|
3110
|
+
const addons = cart.storeAddons?.[storeId] ?? {};
|
|
3111
|
+
const goods = guardSubtotals[i][1];
|
|
3112
|
+
const share = guardFeeByGroup.get(storeId);
|
|
3113
|
+
const groupAddons = computeGiftWrapFee(addons.giftWrapAddon ?? false, commissionRates) +
|
|
3114
|
+
computeShipmentProtectionFee(goods, addons.shipmentProtectionAddon ?? false, commissionRates);
|
|
3115
|
+
return {
|
|
3116
|
+
total: roundRupees(Math.max(0, goods - (guardCouponByGroup[i]?.couponDiscount ?? 0)) +
|
|
3117
|
+
(expectedShippingFees[i]?.shippingFee ?? 0) +
|
|
3118
|
+
groupAddons +
|
|
3119
|
+
(share?.platformFee ?? 0) +
|
|
3120
|
+
(share?.gstOnFee ?? 0)),
|
|
3121
|
+
accepted: expectedShippingFees[i]?.acceptsPoints === true,
|
|
3122
|
+
};
|
|
3123
|
+
});
|
|
3124
|
+
expectedPointsApplied = allocatePointsAcrossGroups(guardBases, guardBalance, true).pointsApplied;
|
|
3125
|
+
}
|
|
3126
|
+
}
|
|
3127
|
+
const expectedPaymentAmountRs = roundRupees(Math.max(0, expectedDiscountedSubtotal +
|
|
2628
3128
|
expectedPlatformFee +
|
|
2629
3129
|
expectedGstOnFee +
|
|
2630
3130
|
expectedAddonFees +
|
|
2631
|
-
expectedShippingFee
|
|
3131
|
+
expectedShippingFee -
|
|
3132
|
+
expectedPointsApplied));
|
|
2632
3133
|
// Reuses the order already fetched above to confirm COMPLETED — PhonePe's
|
|
2633
3134
|
// own amount is in its wire-format smallest unit. // audit-money-units-ok: describes the PhonePe boundary conversion this line performs, not our storage convention
|
|
2634
3135
|
const paidAmountRs = paiseToRupees(gatewayOrder.amount);
|
|
@@ -2703,6 +3204,19 @@ export async function verifyAndPlaceGatewayOrderAction(input) {
|
|
|
2703
3204
|
g[0].item.storeId,
|
|
2704
3205
|
g.reduce((sum, { item, product }) => sum + lineTotalFor(item, product), 0),
|
|
2705
3206
|
]), computeCheckoutFees(cartSubtotal, siteSettings?.commissions ?? CHECKOUT_DEFAULT_COMMISSIONS));
|
|
3207
|
+
/*
|
|
3208
|
+
* The points budget for this checkout — one read, zero when not opted in.
|
|
3209
|
+
* See the identical pair in the COD/manual path for why it is a shared
|
|
3210
|
+
* remainder rather than a per-group balance read.
|
|
3211
|
+
*
|
|
3212
|
+
* 🛑 On THIS path the money has already been captured by the time these
|
|
3213
|
+
* orders are created, so the budget must match whatever the pre-payment
|
|
3214
|
+
* amount was computed against. That is why `useAllPoints` is read off the
|
|
3215
|
+
* cart here too and not re-decided: the figure quoted to the gateway and the
|
|
3216
|
+
* figure recorded on the orders come from the same cart flag.
|
|
3217
|
+
*/
|
|
3218
|
+
const useAllPoints = cart.useAllPoints === true;
|
|
3219
|
+
const pointsBudget = { remaining: useAllPoints ? await safePointsBalance(uid) : 0 };
|
|
2706
3220
|
for (const { items: group, orderType } of orderGroups) {
|
|
2707
3221
|
total += await createPhonePeGroupOrder(group, orderType, {
|
|
2708
3222
|
appliedCoupons,
|
|
@@ -2728,6 +3242,8 @@ export async function verifyAndPlaceGatewayOrderAction(input) {
|
|
|
2728
3242
|
emailsToSend,
|
|
2729
3243
|
couponUsageAccumulator,
|
|
2730
3244
|
productById: productByIdPaid,
|
|
3245
|
+
useAllPoints,
|
|
3246
|
+
pointsBudget,
|
|
2731
3247
|
});
|
|
2732
3248
|
}
|
|
2733
3249
|
await flushCouponUsageAccumulator(couponUsageAccumulator, uid);
|