@mohasinac/appkit 4.11.2 → 4.11.3
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 +4 -0
- package/dist/_internal/server/features/checkout/actions.js +111 -4
- package/dist/_internal/server/features/checkout/data.d.ts +10 -0
- package/dist/_internal/server/features/orders/adapters.js +7 -0
- package/dist/_internal/server/features/products/data.d.ts +12 -3
- package/dist/_internal/server/features/products/data.js +55 -4
- package/dist/_internal/server/features/products/index.d.ts +1 -1
- package/dist/_internal/server/features/products/index.js +1 -1
- package/dist/_internal/server/features/products/list-public.d.ts +19 -0
- package/dist/_internal/server/features/products/list-public.js +39 -6
- package/dist/_internal/server/features/reviews/data.d.ts +1 -1
- package/dist/_internal/server/features/reviews/data.js +3 -2
- package/dist/_internal/server/features/tester/visibility.js +3 -3
- package/dist/_internal/server/jobs/core/testerSandboxCleanup.d.ts +1 -1
- package/dist/_internal/server/jobs/core/testerSandboxCleanup.js +4 -1
- package/dist/_internal/server/jobs/core/testerSandboxRefresh.js +2 -0
- package/dist/_internal/server/jobs/handlers/_helpers.js +4 -1
- package/dist/_internal/shared/features/promotions/schema.d.ts +0 -16
- package/dist/_internal/shared/features/promotions/schema.js +0 -2
- package/dist/_internal/shared/features/reviews/config.d.ts +8 -1
- package/dist/_internal/shared/features/reviews/config.js +8 -1
- package/dist/_internal/shared/listing-types/_registry.d.ts +88 -1
- package/dist/_internal/shared/listing-types/_registry.js +63 -1
- package/dist/_internal/shared/listing-types/art/config.d.ts +59 -0
- package/dist/_internal/shared/listing-types/art/config.js +12 -0
- package/dist/_internal/shared/listing-types/auction/config.d.ts +53 -0
- package/dist/_internal/shared/listing-types/auction/config.js +17 -0
- package/dist/_internal/shared/listing-types/classified/config.d.ts +59 -0
- package/dist/_internal/shared/listing-types/classified/config.js +14 -0
- package/dist/_internal/shared/listing-types/digital-code/config.d.ts +59 -0
- package/dist/_internal/shared/listing-types/digital-code/config.js +12 -0
- package/dist/_internal/shared/listing-types/live/config.d.ts +59 -0
- package/dist/_internal/shared/listing-types/live/config.js +14 -0
- package/dist/_internal/shared/listing-types/pre-order/config.d.ts +110 -0
- package/dist/_internal/shared/listing-types/pre-order/config.js +14 -0
- package/dist/_internal/shared/listing-types/prize-draw/config.d.ts +44 -0
- package/dist/_internal/shared/listing-types/prize-draw/config.js +10 -0
- package/dist/_internal/shared/listing-types/standard/config.d.ts +59 -0
- package/dist/_internal/shared/listing-types/standard/config.js +9 -0
- package/dist/_internal/shared/listing-types/stickers/config.d.ts +59 -0
- package/dist/_internal/shared/listing-types/stickers/config.js +12 -0
- package/dist/client.d.ts +5 -1
- package/dist/client.js +4 -1
- package/dist/constants/field-names.d.ts +12 -0
- package/dist/constants/field-names.js +12 -0
- package/dist/constants/table-keys.d.ts +2 -0
- package/dist/constants/table-keys.js +2 -0
- package/dist/features/account/components/UserOrdersView.js +27 -5
- package/dist/features/admin/components/AdminAddressesView.js +3 -1
- package/dist/features/admin/components/AdminCouponEditorView.js +1 -4
- package/dist/features/admin/components/AdminPaymentMethodsView.js +3 -1
- package/dist/features/admin/components/AdminProductsView.js +34 -18
- package/dist/features/admin/constants/filter-tabs.d.ts +26 -9
- package/dist/features/admin/constants/filter-tabs.js +22 -9
- package/dist/features/auctions/components/AuctionBidsTable.js +1 -1
- package/dist/features/auctions/components/AuctionBottomActions.js +1 -0
- package/dist/features/auctions/components/AuctionDetailPageView.js +13 -11
- package/dist/features/cart/schemas/firestore.d.ts +0 -2
- package/dist/features/categories/components/BrandDetailTabs.js +31 -11
- package/dist/features/categories/components/CategoryDetailTabs.js +27 -7
- package/dist/features/categories/components/CategoryProductsListing.d.ts +9 -1
- package/dist/features/categories/components/CategoryProductsListing.js +5 -2
- package/dist/features/classified/components/ClassifiedDetailPageView.js +2 -1
- package/dist/features/contact/email.js +5 -1
- package/dist/features/digital-codes/components/DigitalCodeDetailPageView.js +17 -16
- package/dist/features/homepage/components/FeaturedResultsSection.js +1 -1
- package/dist/features/homepage/components/SectionCarousel.js +1 -1
- package/dist/features/homepage/components/ShopByCategorySection.js +1 -1
- package/dist/features/layout/AppLayoutShell.js +9 -2
- package/dist/features/layout/BottomActions.js +26 -3
- package/dist/features/layout/BottomActionsContext.d.ts +14 -0
- package/dist/features/layout/BottomActionsContext.js +13 -1
- package/dist/features/layout/hooks/useBottomActions.d.ts +6 -1
- package/dist/features/layout/hooks/useBottomActions.js +4 -1
- package/dist/features/layout/index.d.ts +1 -1
- package/dist/features/live/components/LiveItemDetailPageView.js +7 -6
- package/dist/features/orders/repository/orders.repository.d.ts +4 -0
- package/dist/features/orders/repository/orders.repository.js +4 -0
- package/dist/features/orders/types/index.d.ts +16 -0
- package/dist/features/pre-orders/components/PreOrderBottomActions.d.ts +1 -1
- package/dist/features/pre-orders/components/PreOrderBottomActions.js +3 -19
- package/dist/features/products/components/AuctionsIndexListing.js +1 -2
- package/dist/features/products/components/ListingBottomActions.d.ts +28 -0
- package/dist/features/products/components/ListingBottomActions.js +25 -0
- package/dist/features/products/components/PrizeDrawBottomActions.d.ts +1 -1
- package/dist/features/products/components/PrizeDrawBottomActions.js +3 -21
- package/dist/features/products/components/ProductDetailActions.js +4 -0
- package/dist/features/products/components/ProductDetailPageView.js +7 -7
- package/dist/features/products/constants/listing-tabs.d.ts +43 -198
- package/dist/features/products/constants/listing-tabs.js +71 -58
- package/dist/features/products/constants/sieve.d.ts +104 -1
- package/dist/features/products/constants/sieve.js +27 -7
- package/dist/features/products/repository/products.repository.d.ts +45 -1
- package/dist/features/products/repository/products.repository.js +26 -3
- package/dist/features/products/utils/listing-badge-variant.d.ts +20 -1
- package/dist/features/products/utils/listing-badge-variant.js +26 -1
- package/dist/features/products/utils/listing-type.d.ts +24 -0
- package/dist/features/products/utils/listing-type.js +48 -0
- package/dist/features/promotions/actions/coupon-actions.d.ts +7 -0
- package/dist/features/promotions/actions/coupon-actions.js +12 -1
- package/dist/features/promotions/actions/coupon-stacking.d.ts +24 -0
- package/dist/features/promotions/actions/coupon-stacking.js +21 -0
- package/dist/features/promotions/actions/index.d.ts +1 -0
- package/dist/features/promotions/actions/index.js +1 -0
- package/dist/features/promotions/actions/seller-coupon-actions.d.ts +0 -1
- package/dist/features/promotions/actions/seller-coupon-actions.js +0 -1
- package/dist/features/promotions/components/CouponHelpDetails.d.ts +20 -0
- package/dist/features/promotions/components/CouponHelpDetails.js +17 -0
- package/dist/features/promotions/components/CouponsIndexListing.js +2 -1
- package/dist/features/promotions/components/index.d.ts +2 -0
- package/dist/features/promotions/components/index.js +1 -0
- package/dist/features/promotions/constants/coupon-help.d.ts +27 -0
- package/dist/features/promotions/constants/coupon-help.js +42 -0
- package/dist/features/promotions/repository/coupons.repository.d.ts +10 -1
- package/dist/features/promotions/repository/coupons.repository.js +22 -8
- package/dist/features/promotions/schemas/firestore.d.ts +0 -1
- package/dist/features/promotions/schemas/firestore.js +1 -1
- package/dist/features/promotions/schemas/index.d.ts +0 -20
- package/dist/features/promotions/schemas/index.js +0 -1
- package/dist/features/reviews/actions/review-actions.js +5 -1
- package/dist/features/reviews/components/ReviewDetailPageView.js +2 -1
- package/dist/features/reviews/components/ReviewFilters.d.ts +7 -1
- package/dist/features/reviews/components/ReviewFilters.js +4 -3
- package/dist/features/reviews/components/ReviewsIndexListing.d.ts +6 -0
- package/dist/features/reviews/components/ReviewsIndexListing.js +9 -109
- package/dist/features/reviews/components/ReviewsListingPanel.d.ts +41 -0
- package/dist/features/reviews/components/ReviewsListingPanel.js +226 -0
- package/dist/features/reviews/components/index.d.ts +2 -0
- package/dist/features/reviews/components/index.js +1 -0
- package/dist/features/reviews/hooks/useReviews.d.ts +8 -0
- package/dist/features/reviews/hooks/useReviews.js +14 -6
- package/dist/features/reviews/repository/reviews.repository.d.ts +29 -1
- package/dist/features/reviews/repository/reviews.repository.js +77 -41
- package/dist/features/reviews/types/index.d.ts +8 -1
- package/dist/features/seller/components/SellerProductsCards.js +2 -2
- package/dist/features/seller/components/SellerProductsView.js +21 -18
- package/dist/features/stores/actions/store-query-actions.js +1 -1
- package/dist/features/stores/components/StoreAuctionsListing.js +5 -11
- package/dist/features/stores/components/StorePreOrdersListing.js +6 -11
- package/dist/features/stores/components/StoreReviewsListing.d.ts +4 -0
- package/dist/features/stores/components/StoreReviewsListing.js +7 -108
- package/dist/features/stores/schemas/index.d.ts +2 -2
- package/dist/features/tester/seed-data/coupons-tester-seed-data.js +3 -3
- package/dist/features/tester/seed-data/index.d.ts +1 -0
- package/dist/features/tester/seed-data/index.js +1 -0
- package/dist/features/tester/seed-data/offers-tester-seed-data.d.ts +2 -0
- package/dist/features/tester/seed-data/offers-tester-seed-data.js +61 -0
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +337 -1
- package/dist/index.d.ts +9 -3
- package/dist/index.js +11 -2
- package/dist/react/hooks/useListingTypeFlags.d.ts +12 -19
- package/dist/react/hooks/useListingTypeFlags.js +4 -9
- package/dist/schemas/registry.d.ts +0 -12
- package/dist/seed/claimed-coupons-seed-data.js +3 -3
- package/dist/seed/coupons-seed-data.js +29 -10
- package/dist/seed/faq-seed-data.js +2 -2
- package/dist/seed/manifest.js +2 -2
- package/dist/styles.css +44 -39
- package/dist/tailwind-utilities.css +1 -1
- package/dist/ui/components/FilterChipGroup.d.ts +21 -3
- package/dist/ui/components/FilterChipGroup.js +33 -4
- package/dist/ui/components/HorizontalScroller.d.ts +1 -2
- package/dist/ui/components/HorizontalScroller.js +9 -5
- package/dist/ui/components/HorizontalScroller.style.css +43 -38
- package/package.json +1 -1
|
@@ -44,6 +44,10 @@ export declare function resolveShippingCost(storeId: string | undefined): Promis
|
|
|
44
44
|
storeEmiEnabled: boolean;
|
|
45
45
|
storeState: string | undefined;
|
|
46
46
|
}>;
|
|
47
|
+
export interface DroppedCoupon {
|
|
48
|
+
code: string;
|
|
49
|
+
reason: string;
|
|
50
|
+
}
|
|
47
51
|
/**
|
|
48
52
|
* Place order(s) from the user's cart in a single Firestore transaction.
|
|
49
53
|
*
|
|
@@ -17,6 +17,7 @@ import { computePreOrderDepositAmount, unitPriceFor } from "../../../shared/chec
|
|
|
17
17
|
import { failedCheckoutRepository } from "../../../../features/checkout/repository/failed-checkout.repository";
|
|
18
18
|
import { sendOrderConfirmationEmail } from "../../../../features/contact/server";
|
|
19
19
|
import { splitCartIntoOrderGroups } from "../../../../features/orders/index";
|
|
20
|
+
import { resolveCouponCategorySlugs } from "../../../../features/promotions/actions/coupon-actions";
|
|
20
21
|
import { assertCheckoutLane, assertLockedLinesStillValid, finalizeLockedLines, } from "./locked-lines";
|
|
21
22
|
import { activeLane, laneOf } from "../../../shared/checkout/lanes";
|
|
22
23
|
import { computeCodHandlingFee, computeWhatsAppNotifyFee, computeGiftWrapFee, computeShipmentProtectionFee, } from "../../../shared/fees/calculator";
|
|
@@ -238,6 +239,90 @@ function buildStockUpdatePayload(product, qtyDelta) {
|
|
|
238
239
|
...rule.stockDecrementExtras(product, qtyDelta),
|
|
239
240
|
};
|
|
240
241
|
}
|
|
242
|
+
/**
|
|
243
|
+
* Re-checks every coupon persisted on the cart against the items actually
|
|
244
|
+
* being ordered, immediately before pricing.
|
|
245
|
+
*
|
|
246
|
+
* A cart can sit for days, so the `discountAmount` frozen onto
|
|
247
|
+
* `cart.appliedCoupons` at apply-time may since have become wrong or
|
|
248
|
+
* unearned — the coupon may have expired, hit its total/per-user limit, been
|
|
249
|
+
* deactivated, or had its eligible items removed from the cart. Trusting the
|
|
250
|
+
* stored amount let all of those redeem anyway.
|
|
251
|
+
*
|
|
252
|
+
* Invalid coupons are DROPPED (and reported back to the caller) rather than
|
|
253
|
+
* failing the whole checkout: the buyer is on the payment step and cannot fix
|
|
254
|
+
* a lapsed coupon from there, so killing an otherwise-valid order is hostile.
|
|
255
|
+
*
|
|
256
|
+
* One parallel round of reads for at most a handful of coupons (the stacking
|
|
257
|
+
* rule caps it at one per store plus one platform-wide), so this stays inside
|
|
258
|
+
* the Vercel Hobby round-trip budget.
|
|
259
|
+
*/
|
|
260
|
+
async function revalidateAppliedCoupons(appliedCoupons, available, uid) {
|
|
261
|
+
if (appliedCoupons.length === 0)
|
|
262
|
+
return { coupons: [], dropped: [] };
|
|
263
|
+
const cartItems = available.map(({ item, product }) => ({
|
|
264
|
+
productId: item.productId,
|
|
265
|
+
storeId: item.storeId,
|
|
266
|
+
price: unitPriceFor(item, product),
|
|
267
|
+
quantity: item.quantity,
|
|
268
|
+
listingType: (item.listingType ?? "standard"),
|
|
269
|
+
}));
|
|
270
|
+
const results = await Promise.all(appliedCoupons.map(async (coupon) => {
|
|
271
|
+
try {
|
|
272
|
+
const check = await couponsRepository.validateCouponForCart(coupon.code, uid, cartItems, { resolveCategorySlugs: resolveCouponCategorySlugs });
|
|
273
|
+
if (!check.valid) {
|
|
274
|
+
return {
|
|
275
|
+
dropped: {
|
|
276
|
+
code: coupon.code,
|
|
277
|
+
reason: check.message ?? "This coupon is no longer valid",
|
|
278
|
+
},
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
// Re-derive the amount and the eligible lines from the CURRENT cart —
|
|
282
|
+
// the stored values were computed against whatever was in the cart at
|
|
283
|
+
// apply-time.
|
|
284
|
+
const applicableItemIds = check.eligibleProductIds
|
|
285
|
+
? available
|
|
286
|
+
.filter(({ item }) => check.eligibleProductIds.includes(item.productId))
|
|
287
|
+
.map(({ item }) => item.itemId)
|
|
288
|
+
: undefined;
|
|
289
|
+
return {
|
|
290
|
+
coupon: {
|
|
291
|
+
...coupon,
|
|
292
|
+
discountAmount: check.discountAmount ?? 0,
|
|
293
|
+
scope: check.scope ?? coupon.scope,
|
|
294
|
+
storeId: check.storeId ?? coupon.storeId,
|
|
295
|
+
applicableItemIds,
|
|
296
|
+
},
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
catch (err) {
|
|
300
|
+
void normalizeError(err);
|
|
301
|
+
// A lookup failure must not silently grant the discount.
|
|
302
|
+
return {
|
|
303
|
+
dropped: {
|
|
304
|
+
code: coupon.code,
|
|
305
|
+
reason: "This coupon could not be verified",
|
|
306
|
+
},
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
}));
|
|
310
|
+
const coupons = [];
|
|
311
|
+
const dropped = [];
|
|
312
|
+
for (const r of results) {
|
|
313
|
+
if (r.coupon)
|
|
314
|
+
coupons.push(r.coupon);
|
|
315
|
+
else if (r.dropped)
|
|
316
|
+
dropped.push(r.dropped);
|
|
317
|
+
}
|
|
318
|
+
if (dropped.length > 0) {
|
|
319
|
+
serverLogger.info("Dropped invalid coupons at checkout", {
|
|
320
|
+
uid,
|
|
321
|
+
codes: dropped.map((d) => d.code).join(","),
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
return { coupons, dropped };
|
|
325
|
+
}
|
|
241
326
|
async function flushCouponUsageAccumulator(accumulator, uid) {
|
|
242
327
|
if (accumulator.size === 0)
|
|
243
328
|
return;
|
|
@@ -701,7 +786,7 @@ export async function createCheckoutOrderAction(input) {
|
|
|
701
786
|
.catch((logErr) => { void normalizeError(logErr); serverLogger.warn(AUDIT_LOG_FAIL_MSG, { error: logErr instanceof Error ? logErr.message : String(logErr) }); });
|
|
702
787
|
throw new ValidationError(ERROR_MESSAGES.CHECKOUT.INSUFFICIENT_STOCK);
|
|
703
788
|
}
|
|
704
|
-
const appliedCoupons = cart.appliedCoupons ?? [];
|
|
789
|
+
const { coupons: appliedCoupons, dropped: droppedCoupons } = await revalidateAppliedCoupons(cart.appliedCoupons ?? [], available, uid);
|
|
705
790
|
const orderGroups = splitCartIntoOrderGroups(available);
|
|
706
791
|
// Admin-bypass orders are immediately paid and processing; generate a shared
|
|
707
792
|
// transaction reference for all orders in this checkout batch.
|
|
@@ -753,6 +838,7 @@ export async function createCheckoutOrderAction(input) {
|
|
|
753
838
|
total,
|
|
754
839
|
itemCount: orderIds.length,
|
|
755
840
|
...(unavailable.length > 0 ? { unavailableItems: unavailable } : {}),
|
|
841
|
+
...(droppedCoupons.length > 0 ? { droppedCoupons } : {}),
|
|
756
842
|
};
|
|
757
843
|
}
|
|
758
844
|
const EMPTY_PRICING_PREVIEW = {
|
|
@@ -805,7 +891,9 @@ export async function previewCheckoutPricing(input) {
|
|
|
805
891
|
const checks = cartItems.map((item) => ({ item, product: productById.get(item.productId) ?? null }));
|
|
806
892
|
const orderGroups = splitCartIntoOrderGroups(checks);
|
|
807
893
|
const cartSubtotal = orderGroups.reduce((s, { items: g }) => s + g.reduce((gs, { item, product }) => gs + unitPriceFor(item, product) * item.quantity, 0), 0);
|
|
808
|
-
|
|
894
|
+
// Same re-validation the real order placement runs, so the preview never
|
|
895
|
+
// shows a discount the order won't actually honour.
|
|
896
|
+
const { coupons: appliedCoupons } = await revalidateAppliedCoupons(cart.appliedCoupons ?? [], checks, uid);
|
|
809
897
|
let shippingFee = 0;
|
|
810
898
|
let codHandlingFee = 0;
|
|
811
899
|
let whatsappNotifyFeeTotal = 0;
|
|
@@ -976,7 +1064,7 @@ async function refundDroppedItemsForRazorpayCheckout(input) {
|
|
|
976
1064
|
* from the inline version this replaced.
|
|
977
1065
|
*/
|
|
978
1066
|
async function createRazorpayGroupOrder(group, orderType, ctx) {
|
|
979
|
-
const { appliedCoupons, cartSubtotal, platformFeePercent, gstPercent, whatsappNotifyFee, giftWrapFee, giftWrapMessage, shipmentProtectionAddon, siteSettings, uid, userName, userEmail, razorpay_order_id, razorpay_payment_id, razorpay_signature, shippingAddress, notes, outOfStockPolicy, unavailablePaid, orderIds, emailsToSend, } = ctx;
|
|
1067
|
+
const { appliedCoupons, cartSubtotal, platformFeePercent, gstPercent, whatsappNotifyFee, giftWrapFee, giftWrapMessage, shipmentProtectionAddon, siteSettings, uid, userName, userEmail, razorpay_order_id, razorpay_payment_id, razorpay_signature, shippingAddress, notes, outOfStockPolicy, unavailablePaid, orderIds, emailsToSend, couponUsageAccumulator, } = ctx;
|
|
980
1068
|
const firstItem = group[0].item;
|
|
981
1069
|
const groupTotal = group.reduce((sum, { item, product }) => sum + unitPriceFor(item, product) * item.quantity, 0);
|
|
982
1070
|
// Reuses the same store/seller lookup as the COD/UPI path above instead
|
|
@@ -985,6 +1073,16 @@ async function createRazorpayGroupOrder(group, orderType, ctx) {
|
|
|
985
1073
|
const { shippingFee, storeOwnerId } = await resolveShippingCost(firstItem.storeId);
|
|
986
1074
|
const groupLines = group.map(({ item, product }) => ({ itemId: item.itemId, lineTotal: unitPriceFor(item, product) * item.quantity }));
|
|
987
1075
|
const { couponDiscount, appliedDiscounts } = computeGroupCouponDiscount(appliedCoupons, groupLines, groupTotal, cartSubtotal, firstItem.storeId);
|
|
1076
|
+
// Mirrors createOrderForGroup — without this the online-payment path wrote
|
|
1077
|
+
// the discount onto the order but never incremented usage.currentUsage or
|
|
1078
|
+
// the per-user counter, so limits were unenforceable for Razorpay orders.
|
|
1079
|
+
const groupCouponCodes = new Set();
|
|
1080
|
+
for (const discount of appliedDiscounts) {
|
|
1081
|
+
if (discount.couponId) {
|
|
1082
|
+
accumulateCouponUsage(couponUsageAccumulator, discount.code, discount.couponId, discount.discountAmount);
|
|
1083
|
+
groupCouponCodes.add(discount.code);
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
988
1086
|
const rawPlatformFee = roundRupees(groupTotal * (platformFeePercent / 100));
|
|
989
1087
|
const gstOnFee = roundRupees(rawPlatformFee * (gstPercent / 100));
|
|
990
1088
|
const platformFee = rawPlatformFee + gstOnFee;
|
|
@@ -1103,6 +1201,11 @@ async function createRazorpayGroupOrder(group, orderType, ctx) {
|
|
|
1103
1201
|
productTitle: firstItem.productTitle,
|
|
1104
1202
|
}).catch((e) => serverLogger.error("claimDigitalCode", e));
|
|
1105
1203
|
}
|
|
1204
|
+
for (const code of groupCouponCodes) {
|
|
1205
|
+
const entry = couponUsageAccumulator.get(code);
|
|
1206
|
+
if (entry)
|
|
1207
|
+
entry.orderIds.push(order.id);
|
|
1208
|
+
}
|
|
1106
1209
|
if (userEmail) {
|
|
1107
1210
|
emailsToSend.push({
|
|
1108
1211
|
to: userEmail,
|
|
@@ -1282,7 +1385,7 @@ export async function verifyAndPlaceRazorpayOrderAction(input) {
|
|
|
1282
1385
|
throw new ValidationError(ERROR_MESSAGES.CHECKOUT.PAYMENT_FAILED);
|
|
1283
1386
|
}
|
|
1284
1387
|
}
|
|
1285
|
-
const appliedCoupons = cart.appliedCoupons ?? [];
|
|
1388
|
+
const { coupons: appliedCoupons, dropped: droppedCoupons } = await revalidateAppliedCoupons(cart.appliedCoupons ?? [], bucketedPaid.available, uid);
|
|
1286
1389
|
// "skip_items" (or an all-available cart under any policy) — build order
|
|
1287
1390
|
// groups from the available bucket only. Dropped items are never ordered;
|
|
1288
1391
|
// their value is refunded below since Razorpay already captured payment
|
|
@@ -1293,6 +1396,7 @@ export async function verifyAndPlaceRazorpayOrderAction(input) {
|
|
|
1293
1396
|
const emailsToSend = [];
|
|
1294
1397
|
const cartSubtotal = orderGroups.reduce((s, { items: g }) => s +
|
|
1295
1398
|
g.reduce((gs, { item, product }) => gs + unitPriceFor(item, product) * item.quantity, 0), 0);
|
|
1399
|
+
const couponUsageAccumulator = new Map();
|
|
1296
1400
|
for (const { items: group, orderType } of orderGroups) {
|
|
1297
1401
|
total += await createRazorpayGroupOrder(group, orderType, {
|
|
1298
1402
|
appliedCoupons,
|
|
@@ -1316,8 +1420,10 @@ export async function verifyAndPlaceRazorpayOrderAction(input) {
|
|
|
1316
1420
|
unavailablePaid,
|
|
1317
1421
|
orderIds,
|
|
1318
1422
|
emailsToSend,
|
|
1423
|
+
couponUsageAccumulator,
|
|
1319
1424
|
});
|
|
1320
1425
|
}
|
|
1426
|
+
await flushCouponUsageAccumulator(couponUsageAccumulator, uid);
|
|
1321
1427
|
// SB-UNI-5 2026-05-13 — bundle-aware batch decrement. We iterate the
|
|
1322
1428
|
// cumulative `decrements` map (one entry per unique member product) so a
|
|
1323
1429
|
// bundle's members + any sibling cart line touching the same product
|
|
@@ -1366,5 +1472,6 @@ export async function verifyAndPlaceRazorpayOrderAction(input) {
|
|
|
1366
1472
|
total,
|
|
1367
1473
|
itemCount: orderIds.length,
|
|
1368
1474
|
...(unavailablePaid.length > 0 ? { unavailableItems: unavailablePaid } : {}),
|
|
1475
|
+
...(droppedCoupons.length > 0 ? { droppedCoupons } : {}),
|
|
1369
1476
|
};
|
|
1370
1477
|
}
|
|
@@ -10,4 +10,14 @@ export interface CheckoutOrderResult {
|
|
|
10
10
|
requestedQty: number;
|
|
11
11
|
availableQty: number;
|
|
12
12
|
}[];
|
|
13
|
+
/**
|
|
14
|
+
* Coupons that were on the cart but failed re-validation at placement time
|
|
15
|
+
* (expired, limit reached, no longer any eligible items). They were removed
|
|
16
|
+
* and the order priced without them — surfaced so the UI can tell the buyer
|
|
17
|
+
* rather than silently charging more than the cart showed.
|
|
18
|
+
*/
|
|
19
|
+
droppedCoupons?: {
|
|
20
|
+
code: string;
|
|
21
|
+
reason: string;
|
|
22
|
+
}[];
|
|
13
23
|
}
|
|
@@ -50,12 +50,19 @@ export function orderDocumentToOrder(doc) {
|
|
|
50
50
|
address,
|
|
51
51
|
orderStatus: doc.status,
|
|
52
52
|
paymentStatus: doc.paymentStatus,
|
|
53
|
+
// Which checkout lane produced this order (standard / auction / offer /
|
|
54
|
+
// preorder / prize-draw). Drives the lane tabs on /user/orders. Absent on
|
|
55
|
+
// orders written before `orderType` existed — every reader must treat a
|
|
56
|
+
// missing value as "standard" rather than filtering it out.
|
|
57
|
+
orderType: doc.orderType,
|
|
58
|
+
offerId: doc.offerId,
|
|
53
59
|
subtotal,
|
|
54
60
|
shippingCost: shippingCost || undefined,
|
|
55
61
|
discount: discount || undefined,
|
|
56
62
|
total: doc.totalPrice,
|
|
57
63
|
currency: doc.currency,
|
|
58
64
|
couponCode: doc.couponCode,
|
|
65
|
+
appliedDiscounts: doc.appliedDiscounts,
|
|
59
66
|
trackingNumber: doc.trackingNumber,
|
|
60
67
|
shippingCarrier: doc.shippingCarrier,
|
|
61
68
|
trackingUrl: doc.trackingUrl,
|
|
@@ -4,15 +4,24 @@ import type { ProductDocument } from "../../../../features/products/schemas/fire
|
|
|
4
4
|
import type { ListingType } from "../../../../features/products/types";
|
|
5
5
|
import type { ProductItem } from "../../../../features/products/types";
|
|
6
6
|
import type { FirestoreDocument } from "../../../../schemas/types";
|
|
7
|
-
import type { Review } from "../../../../features/reviews/types";
|
|
7
|
+
import type { Review, ReviewListResponse } from "../../../../features/reviews/types";
|
|
8
8
|
/** Fetch a single product by slug or id, deduped per request. */
|
|
9
9
|
export declare const getProductForDetail: (slugOrId: string) => Promise<ProductDocument | null>;
|
|
10
10
|
/** Fetch the first page of approved reviews for a product, deduped per request. */
|
|
11
|
-
export declare const getReviewsForProduct: (productId: string) => Promise<unknown[]>;
|
|
11
|
+
export declare const getReviewsForProduct: (productId: string, limit?: number) => Promise<unknown[]>;
|
|
12
12
|
/** Firestore review doc → client Review shape. Shared by every listing-type detail page's reviews tab. */
|
|
13
13
|
export declare function toReview(doc: FirestoreDocument): Review;
|
|
14
14
|
/** Fetch + map approved reviews for a product straight to the client Review shape, deduped per request. */
|
|
15
|
-
export declare const getReviewItemsForProduct: (productId: string) => Promise<Review[]>;
|
|
15
|
+
export declare const getReviewItemsForProduct: (productId: string, limit?: number) => Promise<Review[]>;
|
|
16
|
+
/**
|
|
17
|
+
* Page 1 of a product's approved reviews, shaped exactly like `GET /api/reviews?productId=`
|
|
18
|
+
* so it can seed `<ReviewsListingPanel initialData>` without a client round-trip.
|
|
19
|
+
*
|
|
20
|
+
* The defaults here MUST match the panel's bare (unfiltered, page-1) query defaults —
|
|
21
|
+
* `staleTime: Infinity` freezes whatever `initialData` is handed over, so a mismatch would
|
|
22
|
+
* pin the wrong rows into the cache (Recurrent Root Cause #30).
|
|
23
|
+
*/
|
|
24
|
+
export declare const getReviewPageForProduct: (productId: string, pageSize?: number) => Promise<ReviewListResponse>;
|
|
16
25
|
/** Minimal product fields needed for sitemap generation. */
|
|
17
26
|
export interface SitemapProduct {
|
|
18
27
|
slugOrId: string;
|
|
@@ -13,6 +13,8 @@ import { PRODUCT_COLLECTION } from "../../../../features/products/schemas/firest
|
|
|
13
13
|
import { PRODUCTS_SITEMAP_LIMIT } from "../../../shared/features/products/config";
|
|
14
14
|
import { PRODUCT_FIELDS } from "../../../../constants/field-names";
|
|
15
15
|
import { SIEVE_OP, sieveAnd, sieveFilter, sortBy } from "../../../../utils/sieve-builder";
|
|
16
|
+
import { REVIEW_FIELDS } from "../../../../constants/field-names";
|
|
17
|
+
import { REVIEWS_DETAIL_PAGE_SIZE } from "../../../shared/features/reviews/config";
|
|
16
18
|
// ---------------------------------------------------------------------------
|
|
17
19
|
// Request-scoped cache — each function is deduplicated per RSC render tree.
|
|
18
20
|
// Both generateMetadata() and the page component get the same promise.
|
|
@@ -26,8 +28,8 @@ export const getProductForDetail = cache(async (slugOrId) => {
|
|
|
26
28
|
return product;
|
|
27
29
|
});
|
|
28
30
|
/** Fetch the first page of approved reviews for a product, deduped per request. */
|
|
29
|
-
export const getReviewsForProduct = cache(async (productId) => {
|
|
30
|
-
return reviewRepository.findApprovedByProduct(productId).catch(() => []);
|
|
31
|
+
export const getReviewsForProduct = cache(async (productId, limit = REVIEWS_DETAIL_PAGE_SIZE) => {
|
|
32
|
+
return reviewRepository.findApprovedByProduct(productId, limit).catch(() => []);
|
|
31
33
|
});
|
|
32
34
|
/** Firestore review doc → client Review shape. Shared by every listing-type detail page's reviews tab. */
|
|
33
35
|
export function toReview(doc) {
|
|
@@ -57,10 +59,59 @@ export function toReview(doc) {
|
|
|
57
59
|
};
|
|
58
60
|
}
|
|
59
61
|
/** Fetch + map approved reviews for a product straight to the client Review shape, deduped per request. */
|
|
60
|
-
export const getReviewItemsForProduct = cache(async (productId) => {
|
|
61
|
-
const docs = await reviewRepository
|
|
62
|
+
export const getReviewItemsForProduct = cache(async (productId, limit = REVIEWS_DETAIL_PAGE_SIZE) => {
|
|
63
|
+
const docs = await reviewRepository
|
|
64
|
+
.findApprovedByProduct(productId, limit)
|
|
65
|
+
.catch(() => []);
|
|
62
66
|
return docs.map(toReview);
|
|
63
67
|
});
|
|
68
|
+
/**
|
|
69
|
+
* Page 1 of a product's approved reviews, shaped exactly like `GET /api/reviews?productId=`
|
|
70
|
+
* so it can seed `<ReviewsListingPanel initialData>` without a client round-trip.
|
|
71
|
+
*
|
|
72
|
+
* The defaults here MUST match the panel's bare (unfiltered, page-1) query defaults —
|
|
73
|
+
* `staleTime: Infinity` freezes whatever `initialData` is handed over, so a mismatch would
|
|
74
|
+
* pin the wrong rows into the cache (Recurrent Root Cause #30).
|
|
75
|
+
*/
|
|
76
|
+
export const getReviewPageForProduct = cache(async (productId, pageSize = REVIEWS_DETAIL_PAGE_SIZE) => {
|
|
77
|
+
const empty = {
|
|
78
|
+
items: [],
|
|
79
|
+
total: 0,
|
|
80
|
+
page: 1,
|
|
81
|
+
pageSize,
|
|
82
|
+
totalPages: 1,
|
|
83
|
+
hasMore: false,
|
|
84
|
+
averageRating: 0,
|
|
85
|
+
ratingDistribution: {},
|
|
86
|
+
totalApproved: 0,
|
|
87
|
+
};
|
|
88
|
+
try {
|
|
89
|
+
const [summary, result] = await Promise.all([
|
|
90
|
+
reviewRepository.getApprovedRatingSummary(productId),
|
|
91
|
+
reviewRepository.listForProduct(productId, {
|
|
92
|
+
filters: sieveFilter(REVIEW_FIELDS.STATUS, SIEVE_OP.EQ, "approved"),
|
|
93
|
+
sorts: sortBy(REVIEW_FIELDS.CREATED_AT),
|
|
94
|
+
page: 1,
|
|
95
|
+
pageSize,
|
|
96
|
+
}),
|
|
97
|
+
]);
|
|
98
|
+
return {
|
|
99
|
+
items: result.items.map(toReview),
|
|
100
|
+
total: result.total,
|
|
101
|
+
page: result.page,
|
|
102
|
+
pageSize: result.pageSize,
|
|
103
|
+
totalPages: result.totalPages,
|
|
104
|
+
hasMore: result.hasMore,
|
|
105
|
+
averageRating: summary.averageRating,
|
|
106
|
+
ratingDistribution: summary.ratingDistribution,
|
|
107
|
+
totalApproved: summary.total,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
void normalizeError(err);
|
|
112
|
+
return empty;
|
|
113
|
+
}
|
|
114
|
+
});
|
|
64
115
|
/** List all published products for sitemap generation. */
|
|
65
116
|
export async function listSitemapProducts() {
|
|
66
117
|
try {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { getProductForDetail, getReviewsForProduct, listSitemapProducts, type SitemapProduct, computeRelatedItems, toProductItem, toReview, getReviewItemsForProduct, type RelatedItemsResult, } from "./data";
|
|
1
|
+
export { getProductForDetail, getReviewsForProduct, listSitemapProducts, type SitemapProduct, computeRelatedItems, toProductItem, toReview, getReviewItemsForProduct, getReviewPageForProduct, type RelatedItemsResult, } from "./data";
|
|
2
2
|
export { listPublicProducts, parsePublicProductParams, PUBLIC_PRODUCT_MAX_PAGE_SIZE, type PublicProductListInput, type PublicProductListResult, type PublicProductListOptions, type PublicProductExecutor, type PublicProductQuery, type ExecutorResult, } from "./list-public";
|
|
3
3
|
export { assertProductOwnership, assertStatusTransition, assertInStock, effectivePrice, isAvailableForPurchase, } from "./service";
|
|
4
4
|
export { createProductAction, createAuctionAction, createPreOrderAction, updateProductAction, deleteProductAction, setProductStatusAction, setProductFeaturedAction, } from "./actions";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { getProductForDetail, getReviewsForProduct, listSitemapProducts, computeRelatedItems, toProductItem, toReview, getReviewItemsForProduct, } from "./data";
|
|
1
|
+
export { getProductForDetail, getReviewsForProduct, listSitemapProducts, computeRelatedItems, toProductItem, toReview, getReviewItemsForProduct, getReviewPageForProduct, } from "./data";
|
|
2
2
|
export { listPublicProducts, parsePublicProductParams, PUBLIC_PRODUCT_MAX_PAGE_SIZE, } from "./list-public";
|
|
3
3
|
export { assertProductOwnership, assertStatusTransition, assertInStock, effectivePrice, isAvailableForPurchase, } from "./service";
|
|
4
4
|
export { createProductAction, createAuctionAction, createPreOrderAction, updateProductAction, deleteProductAction, setProductStatusAction, setProductFeaturedAction, } from "./actions";
|
|
@@ -1,6 +1,25 @@
|
|
|
1
1
|
import type { JsonValue } from "../../../../schemas/types";
|
|
2
2
|
/** Vercel Hobby Fluid Compute ceiling (CLAUDE.md Rule #6) — never fetch more at once. */
|
|
3
3
|
export declare const PUBLIC_PRODUCT_MAX_PAGE_SIZE = 50;
|
|
4
|
+
/**
|
|
5
|
+
* Which "Show X" defaults apply to a set of listing types.
|
|
6
|
+
*
|
|
7
|
+
* THE POINT OF THIS FUNCTION is that SSR and the client refetch call the SAME
|
|
8
|
+
* implementation. `staleTime: Infinity` freezes SSR `initialData` forever, so
|
|
9
|
+
* if the SSR filter-builder computes a different default than the client's
|
|
10
|
+
* bare-URL state, the first paint and every later refetch disagree and one of
|
|
11
|
+
* them is wrong — permanently (Root Cause #30). Two mirrored literals is
|
|
12
|
+
* exactly how that bug is written; one shared function is how it isn't.
|
|
13
|
+
*
|
|
14
|
+
* `/products` now spans all nine listing types, which makes this load-bearing
|
|
15
|
+
* rather than theoretical: a mixed set includes both sold-out-able types
|
|
16
|
+
* (products, art) and time-boxed ones (auctions, pre-orders, prize draws), so
|
|
17
|
+
* BOTH toggles can apply at once.
|
|
18
|
+
*/
|
|
19
|
+
export declare function defaultTogglesForListingTypes(types: readonly string[] | undefined): {
|
|
20
|
+
hideSoldByDefault: boolean;
|
|
21
|
+
hideEndedByDefault: boolean;
|
|
22
|
+
};
|
|
4
23
|
/**
|
|
5
24
|
* A product document as it comes back from either executor. Deliberately
|
|
6
25
|
* `JsonValue`-shaped rather than `ProductDocument`: the upstream listingProcessor
|
|
@@ -32,8 +32,36 @@ import { PRODUCT_FIELDS } from "../../../../constants/field-names";
|
|
|
32
32
|
import { TABLE_KEYS } from "../../../../constants/table-keys";
|
|
33
33
|
import { sortBy } from "../../../../constants/sort";
|
|
34
34
|
import { SIEVE_OP, sieveAnd, sieveFilter, expandSieveParam, } from "../../../../utils/sieve-builder";
|
|
35
|
+
import { ALL_LISTING_TYPES } from "../../../shared/listing-types/feature-flags";
|
|
36
|
+
import { hideDefaultsFor } from "../../../shared/listing-types/_registry";
|
|
35
37
|
/** Vercel Hobby Fluid Compute ceiling (CLAUDE.md Rule #6) — never fetch more at once. */
|
|
36
38
|
export const PUBLIC_PRODUCT_MAX_PAGE_SIZE = 50;
|
|
39
|
+
/**
|
|
40
|
+
* Which "Show X" defaults apply to a set of listing types.
|
|
41
|
+
*
|
|
42
|
+
* THE POINT OF THIS FUNCTION is that SSR and the client refetch call the SAME
|
|
43
|
+
* implementation. `staleTime: Infinity` freezes SSR `initialData` forever, so
|
|
44
|
+
* if the SSR filter-builder computes a different default than the client's
|
|
45
|
+
* bare-URL state, the first paint and every later refetch disagree and one of
|
|
46
|
+
* them is wrong — permanently (Root Cause #30). Two mirrored literals is
|
|
47
|
+
* exactly how that bug is written; one shared function is how it isn't.
|
|
48
|
+
*
|
|
49
|
+
* `/products` now spans all nine listing types, which makes this load-bearing
|
|
50
|
+
* rather than theoretical: a mixed set includes both sold-out-able types
|
|
51
|
+
* (products, art) and time-boxed ones (auctions, pre-orders, prize draws), so
|
|
52
|
+
* BOTH toggles can apply at once.
|
|
53
|
+
*/
|
|
54
|
+
export function defaultTogglesForListingTypes(types) {
|
|
55
|
+
// No explicit set = every type is in play, so both defaults apply.
|
|
56
|
+
const effective = types && types.length > 0 ? types : ALL_LISTING_TYPES;
|
|
57
|
+
const defaults = hideDefaultsFor(effective.filter((t) => ALL_LISTING_TYPES.includes(t)));
|
|
58
|
+
return {
|
|
59
|
+
hideSoldByDefault: defaults.includes("sold"),
|
|
60
|
+
// "ended" and "closed" are the same query mechanic (a date range against
|
|
61
|
+
// the type's own end field); they differ only in the toggle's wording.
|
|
62
|
+
hideEndedByDefault: defaults.includes("ended") || defaults.includes("closed"),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
37
65
|
const DEFAULT_PAGE = 1;
|
|
38
66
|
const DEFAULT_PAGE_SIZE = 24;
|
|
39
67
|
const DEFAULT_SORTS = sortBy(PRODUCT_FIELDS.CREATED_AT);
|
|
@@ -59,14 +87,19 @@ function num(raw) {
|
|
|
59
87
|
*/
|
|
60
88
|
export function parsePublicProductParams(params, defaults) {
|
|
61
89
|
const get = (k) => first(params, k);
|
|
62
|
-
//
|
|
90
|
+
// The type filter is a multi-select checkbox group, so its URL value is a
|
|
91
|
+
// pipe-joined set (`auction|pre-order`). Selected types NARROW the page's
|
|
92
|
+
// span; anything outside the span is discarded rather than allowed to widen
|
|
93
|
+
// it (an /art URL must not be able to ask for auctions). Nothing selected —
|
|
94
|
+
// or nothing valid selected — means the page's full span.
|
|
63
95
|
const typeParam = get(TABLE_KEYS.LISTING_TYPE);
|
|
64
96
|
const span = defaults?.listingTypes;
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
97
|
+
const selected = typeParam
|
|
98
|
+
.split("|")
|
|
99
|
+
.map((t) => t.trim())
|
|
100
|
+
.filter((t) => t && t !== "All");
|
|
101
|
+
const narrowed = span ? selected.filter((t) => span.includes(t)) : selected;
|
|
102
|
+
const listingTypes = narrowed.length > 0 ? narrowed : span;
|
|
70
103
|
const featuresRaw = get(TABLE_KEYS.FEATURES);
|
|
71
104
|
return {
|
|
72
105
|
listingTypes,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const getReviewsForProduct: (productId: string) => Promise<never[] | import("../../../..").ReviewDocument[]>;
|
|
1
|
+
export declare const getReviewsForProduct: (productId: string, limit?: number) => Promise<never[] | import("../../../..").ReviewDocument[]>;
|
|
2
2
|
export declare const getReviewsForStore: (storeId: string, page?: number, pageSize?: number) => Promise<import("../../../..").FirebaseSieveResult<import("../../../..").ReviewDocument> | {
|
|
3
3
|
items: never[];
|
|
4
4
|
total: number;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { cache } from "react";
|
|
2
2
|
import { reviewRepository, orderRepository } from "../../../../repositories";
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
import { REVIEWS_DETAIL_PAGE_SIZE } from "../../../shared/features/reviews/config";
|
|
4
|
+
export const getReviewsForProduct = cache(async (productId, limit = REVIEWS_DETAIL_PAGE_SIZE) => {
|
|
5
|
+
return reviewRepository.findApprovedByProduct(productId, limit).catch(() => []);
|
|
5
6
|
});
|
|
6
7
|
export const getReviewsForStore = cache(async (storeId, page = 1, pageSize = 20) => {
|
|
7
8
|
return reviewRepository.listForStore(storeId, { page, pageSize }).catch(() => ({
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
* created as fully public/active documents so testers exercise the real browse/search/
|
|
4
4
|
* checkout code paths, but must stay invisible to non-testers. Filtering happens at the
|
|
5
5
|
* application layer (post-fetch, in-memory) rather than via a Firestore `where` clause,
|
|
6
|
-
* because `isTestData` is a new optional field — a Firestore inequality filter
|
|
7
|
-
* (
|
|
8
|
-
*
|
|
6
|
+
* because `isTestData` is a new optional field — a Firestore inequality filter on it
|
|
7
|
+
* (a `!=` clause) would silently exclude every pre-existing document that doesn't have
|
|
8
|
+
* the field set at all, which is worse than the problem it solves.
|
|
9
9
|
*/
|
|
10
10
|
import { isAdminUser } from "../../../../features/auth/role-predicates";
|
|
11
11
|
/** True when the viewer is allowed to see isTestData:true documents. */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { JobContext } from "../runtime/types";
|
|
2
|
-
export declare const SANDBOX_COLLECTIONS: readonly ["categories", "stores", "products", "blogPosts", "events"];
|
|
2
|
+
export declare const SANDBOX_COLLECTIONS: readonly ["categories", "stores", "products", "blogPosts", "events", "offers"];
|
|
3
3
|
export interface TesterSandboxCleanupOptions {
|
|
4
4
|
/** true = wipe every isTestData:true doc regardless of testDataExpiresAt (manual force-purge). */
|
|
5
5
|
force?: boolean;
|
|
@@ -3,7 +3,10 @@ import { BID_FIELDS } from "../../../../constants/field-names";
|
|
|
3
3
|
// Exported so testerSandboxRefresh.ts (the every-4h revert+prune job) reuses
|
|
4
4
|
// the exact same collection scope instead of redefining it — one source of
|
|
5
5
|
// truth for "what counts as sandbox scope."
|
|
6
|
-
|
|
6
|
+
// Keep in sync with testerSandboxRefresh.ts's SEED_BY_COLLECTION — the daily
|
|
7
|
+
// TTL cleanup and the 4-hourly revert+prune must agree on what "sandbox" means
|
|
8
|
+
// (CLAUDE.md § Tester QA standards #3).
|
|
9
|
+
export const SANDBOX_COLLECTIONS = ["categories", "stores", "products", "blogPosts", "events", "offers"];
|
|
7
10
|
const BID_CHUNK_SIZE = 30; // Firestore `in` query cap
|
|
8
11
|
export async function runTesterSandboxCleanup(ctx, opts = {}) {
|
|
9
12
|
const cutoff = opts.force ? null : new Date();
|
|
@@ -8,6 +8,7 @@ import { storesTesterSeedData } from "../../../../features/tester/seed-data/stor
|
|
|
8
8
|
import { productsTesterSeedData } from "../../../../features/tester/seed-data/products-tester-seed-data";
|
|
9
9
|
import { blogTesterSeedData } from "../../../../features/tester/seed-data/blog-tester-seed-data";
|
|
10
10
|
import { eventsTesterSeedData } from "../../../../features/tester/seed-data/events-tester-seed-data";
|
|
11
|
+
import { offersTesterSeedData } from "../../../../features/tester/seed-data/offers-tester-seed-data";
|
|
11
12
|
import { bidsTesterSeedData } from "../../../../features/tester/seed-data/bids-tester-seed-data";
|
|
12
13
|
const SEED_BY_COLLECTION = {
|
|
13
14
|
categories: categoriesTesterSeedData,
|
|
@@ -15,6 +16,7 @@ const SEED_BY_COLLECTION = {
|
|
|
15
16
|
products: productsTesterSeedData,
|
|
16
17
|
blogPosts: blogTesterSeedData,
|
|
17
18
|
events: eventsTesterSeedData,
|
|
19
|
+
offers: offersTesterSeedData,
|
|
18
20
|
};
|
|
19
21
|
const BID_CHUNK_SIZE = 30; // Firestore `in` query cap
|
|
20
22
|
/**
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Shared helpers for job handlers.
|
|
3
3
|
*/
|
|
4
4
|
import { getAdminRealtimeDb } from "../../../../providers/db-firebase";
|
|
5
|
+
import { PRODUCT_FIELDS } from "../../../../constants/field-names";
|
|
5
6
|
import { BATCH_LIMIT } from "./messages";
|
|
6
7
|
/** Batch-delete document refs, respecting Firestore's 500-write batch limit. */
|
|
7
8
|
export async function batchDelete(ctx, refs) {
|
|
@@ -23,7 +24,9 @@ export { getAdminRealtimeDb };
|
|
|
23
24
|
* only the ones past testDataExpiresAt (the scheduled 7-day sweep).
|
|
24
25
|
*/
|
|
25
26
|
export async function getTestDataRefs(db, collection, cutoff) {
|
|
26
|
-
let q = db
|
|
27
|
+
let q = db
|
|
28
|
+
.collection(collection)
|
|
29
|
+
.where(PRODUCT_FIELDS.IS_TEST_DATA, "==", true);
|
|
27
30
|
if (cutoff)
|
|
28
31
|
q = q.where("testDataExpiresAt", "<=", cutoff);
|
|
29
32
|
const snap = await q.limit(200).get();
|