@mohasinac/appkit 4.11.3 → 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 +36 -3
- package/dist/_internal/server/features/checkout/actions.js +122 -44
- package/dist/_internal/server/features/orders/adapters.js +13 -0
- package/dist/_internal/server/features/products/list-public.d.ts +28 -0
- package/dist/_internal/server/features/products/list-public.js +92 -0
- package/dist/_internal/server/jobs/core/auctionSettlement.js +5 -2
- package/dist/_internal/shared/checkout/lanes.d.ts +20 -0
- package/dist/_internal/shared/checkout/lanes.js +28 -0
- package/dist/_internal/shared/features/checkout/config.d.ts +4 -0
- package/dist/_internal/shared/features/checkout/config.js +8 -0
- package/dist/_internal/shared/fees/calculator.d.ts +34 -2
- package/dist/_internal/shared/fees/calculator.js +45 -1
- package/dist/client.d.ts +7 -1
- package/dist/client.js +4 -1
- package/dist/constants/api-endpoints.d.ts +6 -0
- package/dist/constants/api-endpoints.js +2 -0
- package/dist/features/admin/components/AdminAddressesView.js +18 -10
- package/dist/features/admin/components/AdminArtView.d.ts +9 -0
- package/dist/features/admin/components/AdminArtView.js +11 -35
- package/dist/features/admin/components/AdminClassifiedView.d.ts +9 -0
- package/dist/features/admin/components/AdminClassifiedView.js +11 -35
- package/dist/features/admin/components/AdminDigitalCodesView.d.ts +9 -0
- package/dist/features/admin/components/AdminDigitalCodesView.js +11 -42
- package/dist/features/admin/components/AdminLiveView.d.ts +9 -0
- package/dist/features/admin/components/AdminLiveView.js +11 -41
- package/dist/features/admin/components/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 +15 -10
- package/dist/features/admin/components/AdminSiteConfigGuideView.js +1 -1
- package/dist/features/admin/components/AdminSiteSettingsView.js +4 -2
- package/dist/features/admin/components/AdminStickersView.d.ts +9 -0
- package/dist/features/admin/components/AdminStickersView.js +11 -35
- package/dist/features/admin/hooks/useAdminListing.js +7 -2
- package/dist/features/admin/schemas/firestore.d.ts +5 -0
- package/dist/features/admin/schemas/firestore.js +1 -0
- package/dist/features/auctions/actions/bid-actions.js +2 -2
- package/dist/features/cart/components/CartDrawer.d.ts +9 -1
- package/dist/features/cart/components/CartDrawer.js +4 -3
- package/dist/features/cart/components/CartPriceBreakdown.d.ts +61 -0
- package/dist/features/cart/components/CartPriceBreakdown.js +25 -0
- package/dist/features/cart/components/StoreAddonsPicker.d.ts +47 -0
- package/dist/features/cart/components/StoreAddonsPicker.js +21 -0
- package/dist/features/cart/components/index.d.ts +4 -0
- package/dist/features/cart/components/index.js +2 -0
- package/dist/features/cart/repository/cart.repository.d.ts +20 -2
- package/dist/features/cart/repository/cart.repository.js +49 -0
- package/dist/features/cart/schemas/firestore.d.ts +26 -0
- package/dist/features/layout/BottomActions.js +37 -4
- package/dist/features/layout/BottomActionsContext.d.ts +12 -0
- package/dist/features/layout/BottomActionsContext.js +5 -0
- package/dist/features/layout/hooks/useBottomActions.d.ts +6 -0
- package/dist/features/layout/hooks/useBottomActions.js +11 -1
- package/dist/features/orders/components/OrderAddonBadges.d.ts +36 -0
- package/dist/features/orders/components/OrderAddonBadges.js +28 -0
- package/dist/features/orders/components/index.d.ts +1 -0
- package/dist/features/orders/components/index.js +1 -0
- package/dist/features/orders/repository/orders.repository.d.ts +32 -0
- package/dist/features/orders/repository/orders.repository.js +14 -0
- package/dist/features/orders/types/index.d.ts +22 -0
- package/dist/features/products/components/ArtStickersListView.js +5 -4
- package/dist/features/products/components/PrizeDrawsListingView.js +18 -33
- package/dist/features/products/components/ProductsIndexListing.js +87 -11
- package/dist/features/products/components/ProductsIndexPageView.js +14 -2
- package/dist/features/products/config/listing-type-listing-config.d.ts +31 -0
- package/dist/features/products/config/listing-type-listing-config.js +74 -0
- package/dist/features/products/hooks/useProducts.js +14 -0
- package/dist/features/products/types/index.d.ts +15 -0
- package/dist/features/seller/components/SellerOrdersView.js +2 -1
- package/dist/features/stores/components/StoreArtStickersPageView.d.ts +21 -0
- package/dist/features/stores/components/StoreArtStickersPageView.js +30 -0
- package/dist/features/stores/components/StoreAuctionsPageView.d.ts +4 -1
- package/dist/features/stores/components/StoreAuctionsPageView.js +17 -17
- package/dist/features/stores/components/StoreClassifiedsListing.js +7 -0
- package/dist/features/stores/components/StoreClassifiedsPageView.d.ts +4 -1
- package/dist/features/stores/components/StoreClassifiedsPageView.js +16 -12
- package/dist/features/stores/components/StoreDetailLayoutView.js +4 -3
- package/dist/features/stores/components/StoreDigitalCodesListing.js +5 -0
- package/dist/features/stores/components/StoreDigitalCodesPageView.d.ts +4 -1
- package/dist/features/stores/components/StoreDigitalCodesPageView.js +16 -12
- package/dist/features/stores/components/StoreLiveItemsListing.js +7 -0
- package/dist/features/stores/components/StoreLiveItemsPageView.d.ts +4 -1
- package/dist/features/stores/components/StoreLiveItemsPageView.js +16 -12
- package/dist/features/stores/components/StorePreOrdersPageView.d.ts +4 -1
- package/dist/features/stores/components/StorePreOrdersPageView.js +17 -17
- package/dist/features/stores/components/StorePrizeDrawsPageView.d.ts +4 -8
- package/dist/features/stores/components/StorePrizeDrawsPageView.js +17 -22
- package/dist/features/stores/components/StoreProductsListing.d.ts +7 -1
- package/dist/features/stores/components/StoreProductsListing.js +3 -2
- package/dist/features/stores/components/StoreProductsPageView.d.ts +1 -1
- package/dist/features/stores/components/StoreProductsPageView.js +16 -21
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +602 -0
- package/dist/next/routing/route-map.d.ts +2 -0
- package/dist/next/routing/route-map.js +5 -0
- 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 +12 -1
- 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/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";
|
|
@@ -61,9 +62,29 @@ export interface CheckoutPricingPreviewInput {
|
|
|
61
62
|
addressId?: string;
|
|
62
63
|
paymentMethod: CheckoutPaymentMethod;
|
|
63
64
|
excludedProductIds?: string[];
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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;
|
|
67
88
|
}
|
|
68
89
|
export interface CheckoutPricingPreview {
|
|
69
90
|
subtotal: number;
|
|
@@ -74,7 +95,19 @@ export interface CheckoutPricingPreview {
|
|
|
74
95
|
shipmentProtectionFee: number;
|
|
75
96
|
gstAmount: number;
|
|
76
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;
|
|
77
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[];
|
|
78
111
|
}
|
|
79
112
|
/**
|
|
80
113
|
* Read-only "what will I actually be charged" preview for the checkout
|
|
@@ -20,7 +20,7 @@ import { splitCartIntoOrderGroups } from "../../../../features/orders/index";
|
|
|
20
20
|
import { resolveCouponCategorySlugs } from "../../../../features/promotions/actions/coupon-actions";
|
|
21
21
|
import { assertCheckoutLane, assertLockedLinesStillValid, finalizeLockedLines, } from "./locked-lines";
|
|
22
22
|
import { activeLane, laneOf } from "../../../shared/checkout/lanes";
|
|
23
|
-
import { computeCodHandlingFee, computeWhatsAppNotifyFee, computeGiftWrapFee, computeShipmentProtectionFee, } from "../../../shared/fees/calculator";
|
|
23
|
+
import { computeCodHandlingFee, computeWhatsAppNotifyFee, computeGiftWrapFee, computeShipmentProtectionFee, computeCheckoutFees, allocateCheckoutFees, } from "../../../shared/fees/calculator";
|
|
24
24
|
import { getAdminDb, getAdminRealtimeDb, RTDB_PATHS, } from "../../../../providers/db-firebase";
|
|
25
25
|
import { PRODUCT_COLLECTION, PRODUCT_CODES_SUBCOLLECTION } from "../../../../features/products/schemas/firestore";
|
|
26
26
|
import { CART_COLLECTION } from "../../../../features/cart/schemas/index";
|
|
@@ -367,9 +367,12 @@ function dispatchOrderConfirmationEmails(emailsToSend) {
|
|
|
367
367
|
* so the caller can accumulate the checkout-wide `total`.
|
|
368
368
|
*/
|
|
369
369
|
async function createOrderForGroup(group, orderType, ctx) {
|
|
370
|
-
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;
|
|
371
371
|
const firstItem = group[0].item;
|
|
372
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) ?? {};
|
|
373
376
|
const groupTotal = group.reduce((sum, { item, product }) => sum + unitPriceFor(item, product) * item.quantity, 0);
|
|
374
377
|
// S-SBUNI-RULES 2026-05-13 — order-item decoration via rule registry.
|
|
375
378
|
const orderItems = group.map(({ item, product }) => {
|
|
@@ -456,7 +459,10 @@ async function createOrderForGroup(group, orderType, ctx) {
|
|
|
456
459
|
groupCouponCodes.add(discount.code);
|
|
457
460
|
}
|
|
458
461
|
}
|
|
459
|
-
|
|
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);
|
|
460
466
|
const imageUrls = [
|
|
461
467
|
...new Set(group
|
|
462
468
|
.map(({ product }) => product.mainImage)
|
|
@@ -510,6 +516,9 @@ async function createOrderForGroup(group, orderType, ctx) {
|
|
|
510
516
|
depositAmount: adminBypass ? undefined : depositAmount,
|
|
511
517
|
codRemainingAmount: adminBypass ? undefined : codRemainingAmount,
|
|
512
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,
|
|
513
522
|
whatsappNotifyAddon: !adminBypass && whatsappNotifyFee > 0 ? true : undefined,
|
|
514
523
|
whatsappNotifyFee: !adminBypass && whatsappNotifyFee > 0 ? whatsappNotifyFee : undefined,
|
|
515
524
|
giftWrapAddon: !adminBypass && giftWrapFee > 0 ? true : undefined,
|
|
@@ -591,7 +600,7 @@ async function createOrderForGroup(group, orderType, ctx) {
|
|
|
591
600
|
* auth + rate-limiting before calling this and supply the resolved user fields.
|
|
592
601
|
*/
|
|
593
602
|
export async function createCheckoutOrderAction(input) {
|
|
594
|
-
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;
|
|
595
604
|
const siteSettings = await siteSettingsRepository.getSingleton();
|
|
596
605
|
const commissions = siteSettings?.commissions ?? CHECKOUT_DEFAULT_COMMISSIONS;
|
|
597
606
|
const emiSettings = siteSettings?.emi ?? CHECKOUT_DEFAULT_EMI_SETTINGS;
|
|
@@ -798,6 +807,12 @@ export async function createCheckoutOrderAction(input) {
|
|
|
798
807
|
const emailsToSend = [];
|
|
799
808
|
const cartSubtotal = orderGroups.reduce((s, { items: g }) => s + g.reduce((gs, { item, product }) => gs + unitPriceFor(item, product) * item.quantity, 0), 0);
|
|
800
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));
|
|
801
816
|
const groupCtx = {
|
|
802
817
|
paymentMethod,
|
|
803
818
|
emiTenureMonths,
|
|
@@ -821,10 +836,8 @@ export async function createCheckoutOrderAction(input) {
|
|
|
821
836
|
buyerState: resolvedAddress?.state,
|
|
822
837
|
gstSettings: siteSettings?.gst,
|
|
823
838
|
siteContactUpiVpa: siteSettings?.contact?.upiVpa,
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
giftWrapMessage,
|
|
827
|
-
shipmentProtectionAddon,
|
|
839
|
+
storeAddons: cart.storeAddons,
|
|
840
|
+
platformFeeByStore,
|
|
828
841
|
};
|
|
829
842
|
for (const { items: group, orderType } of orderGroups) {
|
|
830
843
|
total += await createOrderForGroup(group, orderType, groupCtx);
|
|
@@ -850,7 +863,10 @@ const EMPTY_PRICING_PREVIEW = {
|
|
|
850
863
|
shipmentProtectionFee: 0,
|
|
851
864
|
gstAmount: 0,
|
|
852
865
|
couponDiscount: 0,
|
|
866
|
+
platformFee: 0,
|
|
867
|
+
gstOnFee: 0,
|
|
853
868
|
total: 0,
|
|
869
|
+
stores: [],
|
|
854
870
|
};
|
|
855
871
|
/**
|
|
856
872
|
* Read-only "what will I actually be charged" preview for the checkout
|
|
@@ -862,13 +878,16 @@ const EMPTY_PRICING_PREVIEW = {
|
|
|
862
878
|
* stock, creates an order, or writes coupon usage.
|
|
863
879
|
*/
|
|
864
880
|
export async function previewCheckoutPricing(input) {
|
|
865
|
-
const { userId: uid, addressId, paymentMethod, excludedProductIds = [],
|
|
881
|
+
const { userId: uid, addressId, paymentMethod, excludedProductIds = [], lane, } = input;
|
|
866
882
|
const siteSettings = await siteSettingsRepository.getSingleton();
|
|
867
883
|
const commissions = siteSettings?.commissions ?? CHECKOUT_DEFAULT_COMMISSIONS;
|
|
868
884
|
const cart = await unitOfWork.carts.getOrCreate(uid);
|
|
869
885
|
const excludedSet = new Set(excludedProductIds);
|
|
870
886
|
const selectedSet = cart.selectedItemIds?.length ? new Set(cart.selectedItemIds) : null;
|
|
871
|
-
|
|
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 ?? []);
|
|
872
891
|
const cartItems = (cart.items ?? []).filter((item) => !excludedSet.has(item.productId) &&
|
|
873
892
|
(!selectedSet || selectedSet.has(item.itemId)) &&
|
|
874
893
|
// A preview never throws — it just shows the lane the buyer can actually
|
|
@@ -894,45 +913,75 @@ export async function previewCheckoutPricing(input) {
|
|
|
894
913
|
// Same re-validation the real order placement runs, so the preview never
|
|
895
914
|
// shows a discount the order won't actually honour.
|
|
896
915
|
const { coupons: appliedCoupons } = await revalidateAppliedCoupons(cart.appliedCoupons ?? [], checks, uid);
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
let shipmentProtectionFeeTotal = 0;
|
|
902
|
-
let gstAmount = 0;
|
|
903
|
-
let couponDiscount = 0;
|
|
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 = [];
|
|
904
920
|
for (const { items: group } of orderGroups) {
|
|
905
921
|
const firstItem = group[0].item;
|
|
906
922
|
const groupTotal = group.reduce((sum, { item, product }) => sum + unitPriceFor(item, product) * item.quantity, 0);
|
|
907
923
|
const { shippingFee: groupShippingFee, storeState } = await resolveShippingCost(firstItem.storeId);
|
|
908
|
-
|
|
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] ?? {};
|
|
909
927
|
// Mirrors createOrderForGroup exactly — the COD handling fee is only
|
|
910
928
|
// ever charged for literal "cod" (pay the courier), never upi_manual or
|
|
911
929
|
// cash (both pre-paid via UPI/bank transfer before dispatch).
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
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;
|
|
917
935
|
if (siteSettings?.gst?.enabled && resolvedAddress?.state) {
|
|
918
936
|
const intraState = !!storeState && storeState === resolvedAddress.state;
|
|
919
937
|
for (const { item, product } of group) {
|
|
920
938
|
const lineTotal = unitPriceFor(item, product) * item.quantity;
|
|
921
939
|
const rate = product?.gstRate ?? 0;
|
|
922
940
|
if (rate > 0)
|
|
923
|
-
|
|
941
|
+
groupGstAmount += calculateGst(rate, intraState, lineTotal).gstAmount;
|
|
924
942
|
}
|
|
925
943
|
}
|
|
926
944
|
const groupLines = group.map(({ item, product }) => ({ itemId: item.itemId, lineTotal: unitPriceFor(item, product) * item.quantity }));
|
|
927
945
|
const { couponDiscount: groupCouponDiscount } = computeGroupCouponDiscount(appliedCoupons, groupLines, groupTotal, cartSubtotal, firstItem.storeId);
|
|
928
|
-
|
|
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
|
+
});
|
|
929
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);
|
|
930
977
|
const total = Math.max(0, cartSubtotal - couponDiscount) +
|
|
931
978
|
shippingFee +
|
|
932
979
|
codHandlingFee +
|
|
933
980
|
whatsappNotifyFeeTotal +
|
|
934
981
|
giftWrapFeeTotal +
|
|
935
982
|
shipmentProtectionFeeTotal +
|
|
983
|
+
platformFee +
|
|
984
|
+
gstOnFee +
|
|
936
985
|
gstAmount;
|
|
937
986
|
return {
|
|
938
987
|
subtotal: cartSubtotal,
|
|
@@ -943,7 +992,10 @@ export async function previewCheckoutPricing(input) {
|
|
|
943
992
|
shipmentProtectionFee: shipmentProtectionFeeTotal,
|
|
944
993
|
gstAmount,
|
|
945
994
|
couponDiscount,
|
|
995
|
+
platformFee,
|
|
996
|
+
gstOnFee,
|
|
946
997
|
total,
|
|
998
|
+
stores,
|
|
947
999
|
};
|
|
948
1000
|
}
|
|
949
1001
|
/**
|
|
@@ -1064,7 +1116,7 @@ async function refundDroppedItemsForRazorpayCheckout(input) {
|
|
|
1064
1116
|
* from the inline version this replaced.
|
|
1065
1117
|
*/
|
|
1066
1118
|
async function createRazorpayGroupOrder(group, orderType, ctx) {
|
|
1067
|
-
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;
|
|
1068
1120
|
const firstItem = group[0].item;
|
|
1069
1121
|
const groupTotal = group.reduce((sum, { item, product }) => sum + unitPriceFor(item, product) * item.quantity, 0);
|
|
1070
1122
|
// Reuses the same store/seller lookup as the COD/UPI path above instead
|
|
@@ -1083,11 +1135,21 @@ async function createRazorpayGroupOrder(group, orderType, ctx) {
|
|
|
1083
1135
|
groupCouponCodes.add(discount.code);
|
|
1084
1136
|
}
|
|
1085
1137
|
}
|
|
1086
|
-
|
|
1087
|
-
|
|
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) ?? {};
|
|
1088
1143
|
const platformFee = rawPlatformFee + gstOnFee;
|
|
1089
|
-
|
|
1090
|
-
|
|
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;
|
|
1091
1153
|
// P-8 GST — deliberately NOT wired into this Razorpay-verify path. The
|
|
1092
1154
|
// amount-mismatch check above (expectedPaymentAmountRs) compares against
|
|
1093
1155
|
// what the buyer already paid via the Razorpay order created earlier in
|
|
@@ -1232,12 +1294,11 @@ async function createRazorpayGroupOrder(group, orderType, ctx) {
|
|
|
1232
1294
|
return orderTotal;
|
|
1233
1295
|
}
|
|
1234
1296
|
export async function verifyAndPlaceRazorpayOrderAction(input) {
|
|
1235
|
-
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.
|
|
1236
1301
|
const siteSettings = await siteSettingsRepository.getSingleton();
|
|
1237
|
-
const platformFeePercent = siteSettings?.commissions?.platformFeePercent ?? 5;
|
|
1238
|
-
const gstPercent = siteSettings?.commissions?.gstPercent ?? 18;
|
|
1239
|
-
const whatsappNotifyFee = computeWhatsAppNotifyFee(whatsappNotifyAddon, siteSettings?.commissions ?? CHECKOUT_DEFAULT_COMMISSIONS);
|
|
1240
|
-
const giftWrapFee = computeGiftWrapFee(giftWrapAddon, siteSettings?.commissions ?? CHECKOUT_DEFAULT_COMMISSIONS);
|
|
1241
1302
|
const isValid = await verifyPaymentSignatureWithKeys({
|
|
1242
1303
|
razorpay_order_id,
|
|
1243
1304
|
razorpay_payment_id,
|
|
@@ -1360,8 +1421,12 @@ export async function verifyAndPlaceRazorpayOrderAction(input) {
|
|
|
1360
1421
|
const unit = isBundle ? item.price : product.price;
|
|
1361
1422
|
return sum + unit * item.quantity;
|
|
1362
1423
|
}, 0);
|
|
1363
|
-
|
|
1364
|
-
|
|
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);
|
|
1365
1430
|
// Same per-seller-group shipping sum /api/payment/create-order charged
|
|
1366
1431
|
// upfront (and the same resolveShippingCost createRazorpayGroupOrder
|
|
1367
1432
|
// below will use to build each order's recorded totalPrice) — keeps this
|
|
@@ -1369,7 +1434,19 @@ export async function verifyAndPlaceRazorpayOrderAction(input) {
|
|
|
1369
1434
|
const expectedShippingGroups = splitCartIntoOrderGroups(bucketedPaid.available);
|
|
1370
1435
|
const expectedShippingFees = await Promise.all(expectedShippingGroups.map((g) => resolveShippingCost(g.items[0].item.storeId)));
|
|
1371
1436
|
const expectedShippingFee = expectedShippingFees.reduce((sum, r) => sum + r.shippingFee, 0);
|
|
1372
|
-
|
|
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;
|
|
1373
1450
|
const rzpOrderRecord = await fetchRazorpayOrder(razorpay_order_id);
|
|
1374
1451
|
const paidAmountRs = paiseToRupees(rzpOrderRecord.amount);
|
|
1375
1452
|
if (paidAmountRs < expectedPaymentAmountRs - 1) {
|
|
@@ -1397,16 +1474,17 @@ export async function verifyAndPlaceRazorpayOrderAction(input) {
|
|
|
1397
1474
|
const cartSubtotal = orderGroups.reduce((s, { items: g }) => s +
|
|
1398
1475
|
g.reduce((gs, { item, product }) => gs + unitPriceFor(item, product) * item.quantity, 0), 0);
|
|
1399
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));
|
|
1400
1482
|
for (const { items: group, orderType } of orderGroups) {
|
|
1401
1483
|
total += await createRazorpayGroupOrder(group, orderType, {
|
|
1402
1484
|
appliedCoupons,
|
|
1403
1485
|
cartSubtotal,
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
whatsappNotifyFee,
|
|
1407
|
-
giftWrapFee,
|
|
1408
|
-
giftWrapMessage,
|
|
1409
|
-
shipmentProtectionAddon,
|
|
1486
|
+
storeAddons: cart.storeAddons,
|
|
1487
|
+
platformFeeByStore,
|
|
1410
1488
|
siteSettings,
|
|
1411
1489
|
uid,
|
|
1412
1490
|
userName,
|
|
@@ -62,7 +62,20 @@ export function orderDocumentToOrder(doc) {
|
|
|
62
62
|
total: doc.totalPrice,
|
|
63
63
|
currency: doc.currency,
|
|
64
64
|
couponCode: doc.couponCode,
|
|
65
|
+
couponDiscount: doc.couponDiscount,
|
|
65
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,
|
|
66
79
|
trackingNumber: doc.trackingNumber,
|
|
67
80
|
shippingCarrier: doc.shippingCarrier,
|
|
68
81
|
trackingUrl: doc.trackingUrl,
|
|
@@ -49,6 +49,16 @@ export interface PublicProductListInput {
|
|
|
49
49
|
freeShipping?: boolean;
|
|
50
50
|
isPartOfBundle?: boolean;
|
|
51
51
|
features?: readonly string[];
|
|
52
|
+
/** Pipe-joined multi-select over the `tags` array field. */
|
|
53
|
+
tags?: readonly string[];
|
|
54
|
+
/** Pipe-joined multi-select over `sublistingCategoryId`. */
|
|
55
|
+
sublistingCategoryIds?: readonly string[];
|
|
56
|
+
/**
|
|
57
|
+
* Per-listing-type facets (classified meetup city, digital-code delivery
|
|
58
|
+
* method, live-item species, …), keyed by TABLE_KEY. Resolved to their
|
|
59
|
+
* nested Firestore paths via `TYPE_FACET_FIELD`.
|
|
60
|
+
*/
|
|
61
|
+
typeFacets?: Record<string, string>;
|
|
52
62
|
preOrderProductionStatus?: string;
|
|
53
63
|
prizeRevealStatus?: string;
|
|
54
64
|
/** Hide sold-out rows. Applied in memory — never pushed into Firestore. */
|
|
@@ -127,4 +137,22 @@ export declare function parsePublicProductParams(params: URLSearchParams | Recor
|
|
|
127
137
|
hideEndedByDefault?: boolean;
|
|
128
138
|
}): PublicProductListInput;
|
|
129
139
|
export declare function listPublicProducts(input: PublicProductListInput, opts?: PublicProductListOptions): Promise<PublicProductListResult | null>;
|
|
140
|
+
/**
|
|
141
|
+
* One store tab's worth of listings.
|
|
142
|
+
*
|
|
143
|
+
* Every `Store*PageView` used to hand-roll this: `productRepository.list()`
|
|
144
|
+
* with an inline `sieveAnd(...)` and a `.catch(() => null)`. That is the exact
|
|
145
|
+
* shape that made `/art` render empty — an unsafe inequality (or any missing
|
|
146
|
+
* index) becomes FAILED_PRECONDITION, the catch turns it into `null`, and the
|
|
147
|
+
* page shows "no results" with nothing logged. Eight views carried it, and
|
|
148
|
+
* `audit-listing-filter-parity` had none of them registered.
|
|
149
|
+
*
|
|
150
|
+
* Routing them through `listPublicProducts` also gives every store tab
|
|
151
|
+
* something it never had: the URL's own sort/filter/page params reach the SSR
|
|
152
|
+
* fetch, so the first paint matches what the toolbar says is selected.
|
|
153
|
+
*/
|
|
154
|
+
export declare function listStoreProducts(storeId: string, listingTypes: readonly string[], searchParams?: URLSearchParams | Record<string, string | string[] | undefined>, defaults?: {
|
|
155
|
+
pageSize?: number;
|
|
156
|
+
sorts?: string;
|
|
157
|
+
}): Promise<PublicProductListResult | null>;
|
|
130
158
|
export {};
|
|
@@ -80,6 +80,40 @@ function num(raw) {
|
|
|
80
80
|
const n = Number(raw);
|
|
81
81
|
return Number.isFinite(n) ? n : undefined;
|
|
82
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* Per-type facet URL key → the Firestore field path it filters.
|
|
85
|
+
*
|
|
86
|
+
* Every target is already allowlisted in the products repository's
|
|
87
|
+
* SIEVE_FIELDS — the nested `classified.*` / `digitalCode.*` / `liveItem.*`
|
|
88
|
+
* entries have been there since those types shipped. What was missing was any
|
|
89
|
+
* code path that actually emitted a clause for them: the store listings
|
|
90
|
+
* declared these keys in their FILTER_KEYS (so they counted toward the
|
|
91
|
+
* filter badge) and rendered the controls, but never put them on the wire.
|
|
92
|
+
*/
|
|
93
|
+
const TYPE_FACET_FIELD = {
|
|
94
|
+
[TABLE_KEYS.CITY]: "classified.meetupArea.city",
|
|
95
|
+
[TABLE_KEYS.NEGOTIABLE]: "classified.negotiable",
|
|
96
|
+
[TABLE_KEYS.ACCEPTS_SHIPPING]: "classified.acceptsShipping",
|
|
97
|
+
[TABLE_KEYS.DELIVERY_METHOD]: "digitalCode.codeDeliveryMethod",
|
|
98
|
+
[TABLE_KEYS.SPECIES]: "liveItem.species",
|
|
99
|
+
[TABLE_KEYS.LIVE_SEX]: "liveItem.sex",
|
|
100
|
+
[TABLE_KEYS.JURISDICTION]: "liveItem.jurisdictionAllowed",
|
|
101
|
+
};
|
|
102
|
+
/** Read every known per-type facet present in the params. */
|
|
103
|
+
function readTypeFacets(get) {
|
|
104
|
+
const out = {};
|
|
105
|
+
for (const key of Object.keys(TYPE_FACET_FIELD)) {
|
|
106
|
+
const value = get(key);
|
|
107
|
+
if (value)
|
|
108
|
+
out[key] = value;
|
|
109
|
+
}
|
|
110
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
111
|
+
}
|
|
112
|
+
/** Split a pipe-joined multi-select value; `undefined` when empty. */
|
|
113
|
+
function pipeList(raw) {
|
|
114
|
+
const parts = raw.split("|").map((s) => s.trim()).filter(Boolean);
|
|
115
|
+
return parts.length > 0 ? parts : undefined;
|
|
116
|
+
}
|
|
83
117
|
/**
|
|
84
118
|
* Read the shared public-listing query params. SSR views hand in Next's
|
|
85
119
|
* `searchParams` record; the API route hands in `url.searchParams`. Both produce
|
|
@@ -118,6 +152,12 @@ export function parsePublicProductParams(params, defaults) {
|
|
|
118
152
|
freeShipping: get(TABLE_KEYS.FREE_SHIPPING) === "true" || undefined,
|
|
119
153
|
isPartOfBundle: get(TABLE_KEYS.IS_PART_OF_BUNDLE) === "true" || undefined,
|
|
120
154
|
features: featuresRaw ? featuresRaw.split("|").filter(Boolean) : undefined,
|
|
155
|
+
// Tags and Sublisting Type were rendered by <ProductFilters> for all three
|
|
156
|
+
// variants and counted toward the active-filter badge, but were never
|
|
157
|
+
// parsed here — so ticking one inflated the badge and changed nothing.
|
|
158
|
+
tags: pipeList(get(TABLE_KEYS.TAGS)),
|
|
159
|
+
sublistingCategoryIds: pipeList(get(TABLE_KEYS.SUBLISTING_CATEGORY)),
|
|
160
|
+
typeFacets: readTypeFacets(get),
|
|
121
161
|
preOrderProductionStatus: get(TABLE_KEYS.PREORDER_STATUS) || get("preOrderStatus") || undefined,
|
|
122
162
|
prizeRevealStatus: get(TABLE_KEYS.PRIZE_REVEAL_STATUS) || undefined,
|
|
123
163
|
// Three spellings of one intent — /products and /art call it "Show sold",
|
|
@@ -211,6 +251,31 @@ function buildFirestoreSafeFilters(input) {
|
|
|
211
251
|
if (input.features?.length === 1) {
|
|
212
252
|
parts.push(sieveFilter(PRODUCT_FIELDS.FEATURES, SIEVE_OP.CONTAINS, input.features[0]));
|
|
213
253
|
}
|
|
254
|
+
// `tags` is an array field, so the same one-value limit applies.
|
|
255
|
+
if (input.tags?.length === 1) {
|
|
256
|
+
parts.push(sieveFilter(PRODUCT_FIELDS.TAGS, SIEVE_OP.CONTAINS, input.tags[0]));
|
|
257
|
+
}
|
|
258
|
+
// A scalar field, so several values ARE expressible as an OR-group (which
|
|
259
|
+
// the Firebase adapter upgrades to `in`).
|
|
260
|
+
if (input.sublistingCategoryIds?.length) {
|
|
261
|
+
parts.push(sieveFilter(PRODUCT_FIELDS.SUBLISTING_CATEGORY_ID, SIEVE_OP.EQ, input.sublistingCategoryIds.join("|")));
|
|
262
|
+
}
|
|
263
|
+
// Per-type facets. Unknown keys can't reach here — `readTypeFacets` only
|
|
264
|
+
// reads the ones in TYPE_FACET_FIELD, so an arbitrary query param can never
|
|
265
|
+
// be turned into a Firestore field path.
|
|
266
|
+
for (const [key, raw] of Object.entries(input.typeFacets ?? {})) {
|
|
267
|
+
const field = TYPE_FACET_FIELD[key];
|
|
268
|
+
if (!field || !raw)
|
|
269
|
+
continue;
|
|
270
|
+
// `jurisdictionAllowed` is an array field — array-contains, not equality.
|
|
271
|
+
const op = field.endsWith("jurisdictionAllowed") ? SIEVE_OP.CONTAINS : SIEVE_OP.EQ;
|
|
272
|
+
const values = raw.split("|").filter(Boolean);
|
|
273
|
+
if (values.length === 0)
|
|
274
|
+
continue;
|
|
275
|
+
parts.push(op === SIEVE_OP.CONTAINS
|
|
276
|
+
? sieveFilter(field, op, values[0])
|
|
277
|
+
: sieveFilter(field, op, values.join("|")));
|
|
278
|
+
}
|
|
214
279
|
if (input.rawFilters)
|
|
215
280
|
parts.push(input.rawFilters);
|
|
216
281
|
return sieveAnd(...parts.filter(Boolean));
|
|
@@ -353,6 +418,33 @@ export async function listPublicProducts(input, opts) {
|
|
|
353
418
|
sorts,
|
|
354
419
|
};
|
|
355
420
|
}
|
|
421
|
+
/**
|
|
422
|
+
* One store tab's worth of listings.
|
|
423
|
+
*
|
|
424
|
+
* Every `Store*PageView` used to hand-roll this: `productRepository.list()`
|
|
425
|
+
* with an inline `sieveAnd(...)` and a `.catch(() => null)`. That is the exact
|
|
426
|
+
* shape that made `/art` render empty — an unsafe inequality (or any missing
|
|
427
|
+
* index) becomes FAILED_PRECONDITION, the catch turns it into `null`, and the
|
|
428
|
+
* page shows "no results" with nothing logged. Eight views carried it, and
|
|
429
|
+
* `audit-listing-filter-parity` had none of them registered.
|
|
430
|
+
*
|
|
431
|
+
* Routing them through `listPublicProducts` also gives every store tab
|
|
432
|
+
* something it never had: the URL's own sort/filter/page params reach the SSR
|
|
433
|
+
* fetch, so the first paint matches what the toolbar says is selected.
|
|
434
|
+
*/
|
|
435
|
+
export async function listStoreProducts(storeId, listingTypes, searchParams = {}, defaults) {
|
|
436
|
+
return listPublicProducts({
|
|
437
|
+
...parsePublicProductParams(searchParams, {
|
|
438
|
+
listingTypes,
|
|
439
|
+
pageSize: defaults?.pageSize ?? DEFAULT_PAGE_SIZE,
|
|
440
|
+
sorts: defaults?.sorts,
|
|
441
|
+
...defaultTogglesForListingTypes(listingTypes),
|
|
442
|
+
}),
|
|
443
|
+
// The route owns the store identity — a `?storeId=` in the URL must not be
|
|
444
|
+
// able to make one store's tab render another store's inventory.
|
|
445
|
+
storeId,
|
|
446
|
+
});
|
|
447
|
+
}
|
|
356
448
|
function sortRows(rows, field, desc) {
|
|
357
449
|
return [...rows].sort((a, b) => {
|
|
358
450
|
const av = a[field];
|
|
@@ -2,12 +2,15 @@ import { bidRepository, cartRepository, productRepository, storeRepository, } fr
|
|
|
2
2
|
import { sendNotification } from "../../../../features/admin/actions/notification-actions";
|
|
3
3
|
import { AUCTION_MESSAGES } from "../handlers/messages";
|
|
4
4
|
import { ROUTES } from "../../../../next/routing/route-map";
|
|
5
|
-
import { CART_LANE } from "../../../shared/checkout/lanes";
|
|
5
|
+
import { CART_LANE, LANE_CHECKOUT_WINDOW_MS } from "../../../shared/checkout/lanes";
|
|
6
6
|
/**
|
|
7
7
|
* How long the winner has to pay before the locked cart line lapses. Mirrors
|
|
8
8
|
* the accepted-offer window so both locked lanes behave the same.
|
|
9
9
|
*/
|
|
10
|
-
|
|
10
|
+
// Was a local `48 * 60 * 60 * 1000` here AND an identical one in bid-actions.ts
|
|
11
|
+
// (the Buy-Now path), with nothing linking them — changing one would have given
|
|
12
|
+
// the same kind of win two different deadlines depending on how it was won.
|
|
13
|
+
const AUCTION_CHECKOUT_WINDOW_MS = LANE_CHECKOUT_WINDOW_MS.auction;
|
|
11
14
|
/** Deep link the winner straight into the auction checkout lane. */
|
|
12
15
|
const AUCTION_CHECKOUT_URL = `${String(ROUTES.USER.CHECKOUT)}?lane=${CART_LANE.AUCTION}`;
|
|
13
16
|
async function settleAuction(ctx, product) {
|
|
@@ -11,6 +11,26 @@ export type CartLane = (typeof CART_LANE)[keyof typeof CART_LANE];
|
|
|
11
11
|
*/
|
|
12
12
|
export declare const CART_LANE_PRIORITY: readonly CartLane[];
|
|
13
13
|
export declare const CART_LANE_LABELS: Record<CartLane, string>;
|
|
14
|
+
/**
|
|
15
|
+
* How long a locked line survives before it lapses, per lane.
|
|
16
|
+
*
|
|
17
|
+
* Lives here, with the lane model, because the deadline is a property OF the
|
|
18
|
+
* lane — and because `AUCTION_CHECKOUT_WINDOW_MS` was previously declared twice
|
|
19
|
+
* (`auctionSettlement.ts` and `bid-actions.ts`, i.e. the settlement path and the
|
|
20
|
+
* Buy-Now path) with no link between them: changing one would silently give the
|
|
21
|
+
* same kind of win two different deadlines depending on how it was won.
|
|
22
|
+
*
|
|
23
|
+
* The two windows are equal today but mean different things, so they stay
|
|
24
|
+
* separate names rather than one shared constant:
|
|
25
|
+
* - **auction** — payment is MANDATORY. You bid, you won, you owe it. Past the
|
|
26
|
+
* deadline the bid is forfeited and the buyer is warned that repeated
|
|
27
|
+
* non-payment restricts their account.
|
|
28
|
+
* - **offer** — payment is OPTIONAL. Declining to buy at your own offer price
|
|
29
|
+
* is the buyer's right; the offer simply lapses with no penalty.
|
|
30
|
+
*/
|
|
31
|
+
export declare const LANE_CHECKOUT_WINDOW_MS: Record<"auction" | "offer", number>;
|
|
32
|
+
/** 2 days, in hours — for buyer-facing copy ("pay within 48 hours"). */
|
|
33
|
+
export declare const LANE_CHECKOUT_WINDOW_HOURS: Record<"auction" | "offer", number>;
|
|
14
34
|
/** The two lanes whose price is fixed outside the listing. */
|
|
15
35
|
export declare function isLockedLane(lane: CartLane): boolean;
|
|
16
36
|
/** Minimal shape this module needs — works on cart documents and client rows alike. */
|
|
@@ -44,6 +44,34 @@ export const CART_LANE_LABELS = {
|
|
|
44
44
|
offer: "Accepted offers",
|
|
45
45
|
standard: "Cart",
|
|
46
46
|
};
|
|
47
|
+
/**
|
|
48
|
+
* How long a locked line survives before it lapses, per lane.
|
|
49
|
+
*
|
|
50
|
+
* Lives here, with the lane model, because the deadline is a property OF the
|
|
51
|
+
* lane — and because `AUCTION_CHECKOUT_WINDOW_MS` was previously declared twice
|
|
52
|
+
* (`auctionSettlement.ts` and `bid-actions.ts`, i.e. the settlement path and the
|
|
53
|
+
* Buy-Now path) with no link between them: changing one would silently give the
|
|
54
|
+
* same kind of win two different deadlines depending on how it was won.
|
|
55
|
+
*
|
|
56
|
+
* The two windows are equal today but mean different things, so they stay
|
|
57
|
+
* separate names rather than one shared constant:
|
|
58
|
+
* - **auction** — payment is MANDATORY. You bid, you won, you owe it. Past the
|
|
59
|
+
* deadline the bid is forfeited and the buyer is warned that repeated
|
|
60
|
+
* non-payment restricts their account.
|
|
61
|
+
* - **offer** — payment is OPTIONAL. Declining to buy at your own offer price
|
|
62
|
+
* is the buyer's right; the offer simply lapses with no penalty.
|
|
63
|
+
*/
|
|
64
|
+
export const LANE_CHECKOUT_WINDOW_MS = {
|
|
65
|
+
/** 2 days. Mandatory — enforced by the expiry sweep, which forfeits the bid. */
|
|
66
|
+
auction: 48 * 60 * 60 * 1000,
|
|
67
|
+
/** 2 days. Optional — lapses quietly, no penalty. */
|
|
68
|
+
offer: 48 * 60 * 60 * 1000,
|
|
69
|
+
};
|
|
70
|
+
/** 2 days, in hours — for buyer-facing copy ("pay within 48 hours"). */
|
|
71
|
+
export const LANE_CHECKOUT_WINDOW_HOURS = {
|
|
72
|
+
auction: LANE_CHECKOUT_WINDOW_MS.auction / (60 * 60 * 1000),
|
|
73
|
+
offer: LANE_CHECKOUT_WINDOW_MS.offer / (60 * 60 * 1000),
|
|
74
|
+
};
|
|
47
75
|
/** The two lanes whose price is fixed outside the listing. */
|
|
48
76
|
export function isLockedLane(lane) {
|
|
49
77
|
return lane === CART_LANE.AUCTION || lane === CART_LANE.OFFER;
|