@mohasinac/appkit 4.11.0 → 4.11.2
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.js +27 -23
- package/dist/_internal/server/features/checkout/locked-lines.d.ts +30 -0
- package/dist/_internal/server/features/checkout/locked-lines.js +116 -0
- package/dist/_internal/server/features/products/data.d.ts +2 -2
- package/dist/_internal/server/features/products/data.js +5 -23
- package/dist/_internal/server/features/products/index.d.ts +1 -0
- package/dist/_internal/server/features/products/index.js +1 -0
- package/dist/_internal/server/features/products/list-public.d.ts +111 -0
- package/dist/_internal/server/features/products/list-public.js +342 -0
- package/dist/_internal/server/jobs/core/auctionSettlement.js +89 -16
- package/dist/_internal/server/jobs/core/offerExpiry.js +116 -4
- package/dist/_internal/server/jobs/handlers/messages.d.ts +5 -0
- package/dist/_internal/server/jobs/handlers/messages.js +5 -0
- package/dist/_internal/shared/checkout/lanes.d.ts +41 -0
- package/dist/_internal/shared/checkout/lanes.js +106 -0
- package/dist/_internal/shared/checkout/order-math.d.ts +19 -0
- package/dist/_internal/shared/checkout/order-math.js +30 -6
- package/dist/_internal/shared/features/products/to-product-item.d.ts +25 -0
- package/dist/_internal/shared/features/products/to-product-item.js +45 -0
- package/dist/_internal/shared/listing-types/feature-flags.d.ts +1 -0
- package/dist/_internal/shared/listing-types/feature-flags.js +24 -10
- package/dist/client.d.ts +2 -1
- package/dist/client.js +4 -1
- package/dist/features/account/components/NotificationBell.js +1 -0
- package/dist/features/account/components/UserOffersPanel.js +8 -1
- package/dist/features/admin/actions/notification-actions.js +1 -1
- package/dist/features/admin/schemas/firestore.d.ts +2 -1
- package/dist/features/admin/schemas/firestore.js +1 -0
- package/dist/features/auctions/actions/bid-actions.d.ts +11 -1
- package/dist/features/auctions/actions/bid-actions.js +29 -15
- package/dist/features/auctions/components/AuctionBidsTable.js +6 -2
- package/dist/features/auctions/components/AuctionsListView.js +12 -53
- package/dist/features/auctions/components/PlaceBidFormClient.js +10 -1
- package/dist/features/auctions/repository/bid.repository.d.ts +15 -0
- package/dist/features/auctions/repository/bid.repository.js +19 -0
- package/dist/features/auctions/schemas/firestore.d.ts +11 -1
- package/dist/features/auctions/schemas/firestore.js +1 -0
- package/dist/features/cart/actions/cart-actions.d.ts +11 -0
- package/dist/features/cart/actions/cart-actions.js +24 -0
- package/dist/features/cart/repository/cart.repository.d.ts +24 -1
- package/dist/features/cart/repository/cart.repository.js +71 -6
- package/dist/features/cart/schemas/firestore.d.ts +23 -2
- package/dist/features/contact/email.js +52 -1
- package/dist/features/grouped/components/GroupedListingsCarousel.js +4 -1
- package/dist/features/orders/repository/orders.repository.d.ts +0 -15
- package/dist/features/orders/repository/orders.repository.js +0 -27
- package/dist/features/orders/utils/order-splitter.js +14 -2
- package/dist/features/pre-orders/components/PreOrdersListView.js +11 -39
- package/dist/features/products/components/ArtStickersListView.js +10 -49
- package/dist/features/products/components/ProductsIndexPageView.js +9 -62
- package/dist/features/products/repository/products.repository.js +41 -5
- package/dist/features/seller/actions/offer-actions.d.ts +6 -0
- package/dist/features/seller/actions/offer-actions.js +8 -9
- package/dist/features/seller/components/SellerOffersView.d.ts +6 -1
- package/dist/features/seller/components/SellerOffersView.js +59 -8
- package/dist/features/seller/repository/offer.repository.d.ts +18 -2
- package/dist/features/seller/repository/offer.repository.js +38 -2
- package/dist/features/seller/schemas/firestore.d.ts +7 -2
- package/dist/features/seller/schemas/firestore.js +3 -0
- package/dist/features/seller/schemas/offer-forms.d.ts +12 -0
- package/dist/features/seller/schemas/offer-forms.js +28 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +4 -1
- package/dist/server-entry.d.ts +1 -0
- package/dist/server-entry.js +1 -0
- package/dist/server.d.ts +1 -0
- package/dist/server.js +3 -0
- package/package.json +1 -1
|
@@ -13,10 +13,12 @@ import { ORDER_FIELDS } from "../../../../constants/field-names";
|
|
|
13
13
|
import { serverLogger } from "../../../../monitoring";
|
|
14
14
|
import { unitOfWork, siteSettingsRepository, userRepository, storeRepository, couponsRepository, claimedCouponsRepository, addressesRepository, productRepository, } from "../../../../repositories";
|
|
15
15
|
import { calculateGst } from "../../../shared/fees/calculator";
|
|
16
|
-
import { computePreOrderDepositAmount } from "../../../shared/checkout/order-math";
|
|
16
|
+
import { computePreOrderDepositAmount, unitPriceFor } from "../../../shared/checkout/order-math";
|
|
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 { assertCheckoutLane, assertLockedLinesStillValid, finalizeLockedLines, } from "./locked-lines";
|
|
21
|
+
import { activeLane, laneOf } from "../../../shared/checkout/lanes";
|
|
20
22
|
import { computeCodHandlingFee, computeWhatsAppNotifyFee, computeGiftWrapFee, computeShipmentProtectionFee, } from "../../../shared/fees/calculator";
|
|
21
23
|
import { getAdminDb, getAdminRealtimeDb, RTDB_PATHS, } from "../../../../providers/db-firebase";
|
|
22
24
|
import { PRODUCT_COLLECTION, PRODUCT_CODES_SUBCOLLECTION } from "../../../../features/products/schemas/firestore";
|
|
@@ -272,14 +274,6 @@ function dispatchOrderConfirmationEmails(emailsToSend) {
|
|
|
272
274
|
return;
|
|
273
275
|
Promise.all(emailsToSend.map((e) => sendOrderConfirmationEmail(e))).catch((err) => serverLogger.error("Order confirmation email error:", err));
|
|
274
276
|
}
|
|
275
|
-
// SB-UNI-5 2026-05-13 — bundle cart-lines use item.price (locked bundle price
|
|
276
|
-
// at add-time); regular lines use product.price (current Firestore). Prevents
|
|
277
|
-
// stale cart-cached prices from being charged on COD/UPI orders.
|
|
278
|
-
function unitPriceFor(item, product) {
|
|
279
|
-
return item.bundleCategorySlug && item.bundleProductIds?.length
|
|
280
|
-
? item.price
|
|
281
|
-
: product.price;
|
|
282
|
-
}
|
|
283
277
|
/**
|
|
284
278
|
* Places one order for one seller-group of the checkout cart — fee math
|
|
285
279
|
* (COD deposit/handling, EMI schedule), coupon apportionment, order-doc
|
|
@@ -461,6 +455,11 @@ async function createOrderForGroup(group, orderType, ctx) {
|
|
|
461
455
|
...extraOrderFields,
|
|
462
456
|
});
|
|
463
457
|
orderIds.push(order.id);
|
|
458
|
+
// Close the loop on any locked line this order settled: flip the offer to
|
|
459
|
+
// "paid" (its terminal status had NO server-side writer before 2026-08-21,
|
|
460
|
+
// so an accepted offer could be re-added and re-ordered) and back-link the
|
|
461
|
+
// winning bid to the order it became.
|
|
462
|
+
await finalizeLockedLines(group.map(({ item }) => item), order.id);
|
|
464
463
|
// SB-UNI-N — claim a digital code for digital-code orders (fire-and-forget,
|
|
465
464
|
// order is already persisted so a claim failure is logged, not thrown).
|
|
466
465
|
if ((group[0]?.product?.listingType ?? "standard") === "digital-code") {
|
|
@@ -541,6 +540,9 @@ export async function createCheckoutOrderAction(input) {
|
|
|
541
540
|
if (cartItems.length === 0) {
|
|
542
541
|
throw new ValidationError(ERROR_MESSAGES.CHECKOUT.CART_EMPTY);
|
|
543
542
|
}
|
|
543
|
+
// Lane priority + locked-line revalidation, before any money math runs.
|
|
544
|
+
assertCheckoutLane(cart.items, cartItems);
|
|
545
|
+
await assertLockedLinesStillValid(cartItems, uid);
|
|
544
546
|
// Tier PP — OTP gate for high-value checkouts. Evaluated against the
|
|
545
547
|
// WHOLE checkout batch's total (not each resulting per-seller order),
|
|
546
548
|
// otherwise a buyer could dodge it by splitting a big cart across
|
|
@@ -780,7 +782,12 @@ export async function previewCheckoutPricing(input) {
|
|
|
780
782
|
const cart = await unitOfWork.carts.getOrCreate(uid);
|
|
781
783
|
const excludedSet = new Set(excludedProductIds);
|
|
782
784
|
const selectedSet = cart.selectedItemIds?.length ? new Set(cart.selectedItemIds) : null;
|
|
783
|
-
const
|
|
785
|
+
const previewLane = activeLane(cart.items ?? []);
|
|
786
|
+
const cartItems = (cart.items ?? []).filter((item) => !excludedSet.has(item.productId) &&
|
|
787
|
+
(!selectedSet || selectedSet.has(item.itemId)) &&
|
|
788
|
+
// A preview never throws — it just shows the lane the buyer can actually
|
|
789
|
+
// check out, matching what the checkout page's active tab displays.
|
|
790
|
+
(!previewLane || laneOf(item) === previewLane));
|
|
784
791
|
if (cartItems.length === 0)
|
|
785
792
|
return EMPTY_PRICING_PREVIEW;
|
|
786
793
|
// Best-effort product lookup — a preview never throws on a since-unpublished
|
|
@@ -795,7 +802,6 @@ export async function previewCheckoutPricing(input) {
|
|
|
795
802
|
resolvedAddress =
|
|
796
803
|
addressDoc && addressDoc.ownerType === "user" && addressDoc.ownerId === uid ? addressDoc : null;
|
|
797
804
|
}
|
|
798
|
-
const unitPriceFor = (item, product) => item.bundleCategorySlug && item.bundleProductIds?.length ? item.price : (product?.price ?? item.price);
|
|
799
805
|
const checks = cartItems.map((item) => ({ item, product: productById.get(item.productId) ?? null }));
|
|
800
806
|
const orderGroups = splitCartIntoOrderGroups(checks);
|
|
801
807
|
const cartSubtotal = orderGroups.reduce((s, { items: g }) => s + g.reduce((gs, { item, product }) => gs + unitPriceFor(item, product) * item.quantity, 0), 0);
|
|
@@ -971,12 +977,6 @@ async function refundDroppedItemsForRazorpayCheckout(input) {
|
|
|
971
977
|
*/
|
|
972
978
|
async function createRazorpayGroupOrder(group, orderType, ctx) {
|
|
973
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;
|
|
974
|
-
// SB-UNI-5 2026-05-13 — bundle cart-lines use item.price (locked bundle
|
|
975
|
-
// price); regular lines use product.price. The helper keeps the math
|
|
976
|
-
// local to this block.
|
|
977
|
-
const unitPriceFor = (item, product) => item.bundleCategorySlug && item.bundleProductIds?.length
|
|
978
|
-
? item.price
|
|
979
|
-
: product.price;
|
|
980
980
|
const firstItem = group[0].item;
|
|
981
981
|
const groupTotal = group.reduce((sum, { item, product }) => sum + unitPriceFor(item, product) * item.quantity, 0);
|
|
982
982
|
// Reuses the same store/seller lookup as the COD/UPI path above instead
|
|
@@ -1089,6 +1089,11 @@ async function createRazorpayGroupOrder(group, orderType, ctx) {
|
|
|
1089
1089
|
...extraOrderFields,
|
|
1090
1090
|
});
|
|
1091
1091
|
orderIds.push(order.id);
|
|
1092
|
+
// Close the loop on any locked line this order settled: flip the offer to
|
|
1093
|
+
// "paid" (its terminal status had NO server-side writer before 2026-08-21,
|
|
1094
|
+
// so an accepted offer could be re-added and re-ordered) and back-link the
|
|
1095
|
+
// winning bid to the order it became.
|
|
1096
|
+
await finalizeLockedLines(group.map(({ item }) => item), order.id);
|
|
1092
1097
|
// SB-UNI-N — claim a digital code for digital-code orders (fire-and-forget,
|
|
1093
1098
|
// order is already persisted so a claim failure is logged, not thrown).
|
|
1094
1099
|
if ((group[0]?.product?.listingType ?? "standard") === "digital-code") {
|
|
@@ -1150,6 +1155,11 @@ export async function verifyAndPlaceRazorpayOrderAction(input) {
|
|
|
1150
1155
|
if (!cart.items || cart.items.length === 0) {
|
|
1151
1156
|
throw new ValidationError(ERROR_MESSAGES.CHECKOUT.CART_EMPTY);
|
|
1152
1157
|
}
|
|
1158
|
+
// Same lane priority + locked-line revalidation the manual/COD path runs.
|
|
1159
|
+
// The Razorpay path settles the whole cart (no selectedItemIds), so the
|
|
1160
|
+
// "selected" set is the cart itself.
|
|
1161
|
+
assertCheckoutLane(cart.items, cart.items);
|
|
1162
|
+
await assertLockedLinesStillValid(cart.items, uid);
|
|
1153
1163
|
const isDigitalCartRazorpay = cartIsDigitalOnly(cart.items);
|
|
1154
1164
|
let shippingAddress;
|
|
1155
1165
|
let resolvedAddressRzp = null;
|
|
@@ -1281,12 +1291,6 @@ export async function verifyAndPlaceRazorpayOrderAction(input) {
|
|
|
1281
1291
|
const orderIds = [];
|
|
1282
1292
|
let total = 0;
|
|
1283
1293
|
const emailsToSend = [];
|
|
1284
|
-
// SB-UNI-5 2026-05-13 — bundle cart-lines use item.price (locked bundle
|
|
1285
|
-
// price); regular lines use product.price. The helper keeps the math
|
|
1286
|
-
// local to this block.
|
|
1287
|
-
const unitPriceFor = (item, product) => item.bundleCategorySlug && item.bundleProductIds?.length
|
|
1288
|
-
? item.price
|
|
1289
|
-
: product.price;
|
|
1290
1294
|
const cartSubtotal = orderGroups.reduce((s, { items: g }) => s +
|
|
1291
1295
|
g.reduce((gs, { item, product }) => gs + unitPriceFor(item, product) * item.quantity, 0), 0);
|
|
1292
1296
|
for (const { items: group, orderType } of orderGroups) {
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { CartItemDocument } from "../../../../features/cart/schemas/firestore";
|
|
2
|
+
/**
|
|
3
|
+
* Refuse a checkout that isn't the cart's active lane.
|
|
4
|
+
*
|
|
5
|
+
* The client scopes its selection to one lane's tab, but a selection is just a
|
|
6
|
+
* list of item ids in a request body — nothing stopped a caller from checking
|
|
7
|
+
* out their ₹200 standard items while a ₹30,000 auction win they already
|
|
8
|
+
* committed to sat unpaid. Priority is enforced here, on the server, over the
|
|
9
|
+
* WHOLE cart, not over the caller-supplied subset.
|
|
10
|
+
*/
|
|
11
|
+
export declare function assertCheckoutLane(allCartItems: readonly CartItemDocument[], selectedItems: readonly CartItemDocument[]): void;
|
|
12
|
+
/** Error code the client maps to a lane-aware message. */
|
|
13
|
+
export declare const CHECKOUT_LANE_BLOCKED = "CHECKOUT_LANE_BLOCKED";
|
|
14
|
+
/**
|
|
15
|
+
* Re-validate every locked line immediately before the order is written.
|
|
16
|
+
*
|
|
17
|
+
* `checkoutOffer()` already checks status/deadline/stock when the line is ADDED
|
|
18
|
+
* to the cart, but nothing re-checked at order time — so a buyer could add an
|
|
19
|
+
* accepted offer, wait out the 48h window, and still be charged the locked
|
|
20
|
+
* price. Throws on the first invalid line; the caller's transaction aborts.
|
|
21
|
+
*/
|
|
22
|
+
export declare function assertLockedLinesStillValid(items: readonly CartItemDocument[], buyerUid: string): Promise<void>;
|
|
23
|
+
/**
|
|
24
|
+
* Close the loop after the order exists.
|
|
25
|
+
*
|
|
26
|
+
* Best-effort by design: the order is already persisted and paid-for, so a
|
|
27
|
+
* failure here must be logged rather than thrown — losing the back-link is a
|
|
28
|
+
* bookkeeping problem, refusing a completed order is a customer-facing one.
|
|
29
|
+
*/
|
|
30
|
+
export declare function finalizeLockedLines(items: readonly CartItemDocument[], orderId: string): Promise<void>;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* WHY: A "locked line" is a cart line whose price was fixed outside the listing —
|
|
3
|
+
* an accepted offer or a won auction. Two things were missing before
|
|
4
|
+
* 2026-08-21: the offer/bid was never re-checked at ORDER time (only at
|
|
5
|
+
* add-to-cart time, so a line could sit in the cart past its deadline and
|
|
6
|
+
* still check out), and nothing ever closed the loop afterwards — the
|
|
7
|
+
* offer stayed "accepted" forever and could be re-added and re-ordered,
|
|
8
|
+
* and a won bid had no link to the order it became.
|
|
9
|
+
* WHAT: `assertLockedLinesStillValid` (pre-order-creation guard) and
|
|
10
|
+
* `finalizeLockedLines` (post-order-creation close-out). Both are called
|
|
11
|
+
* from every order-creation path so the manual/COD and Razorpay flows
|
|
12
|
+
* cannot diverge.
|
|
13
|
+
*
|
|
14
|
+
* EXPORTS:
|
|
15
|
+
* assertLockedLinesStillValid, finalizeLockedLines
|
|
16
|
+
*
|
|
17
|
+
* @tag domain:checkout,offers,auctions
|
|
18
|
+
* @tag layer:server-action
|
|
19
|
+
* @tag pattern:none
|
|
20
|
+
* @tag access:server-only
|
|
21
|
+
* @tag consumers:checkout/actions.ts
|
|
22
|
+
* @tag sideEffects:firestore-write
|
|
23
|
+
*/
|
|
24
|
+
import { offerRepository } from "../../../../features/seller/repository/offer.repository";
|
|
25
|
+
import { bidRepository } from "../../../../repositories";
|
|
26
|
+
import { OfferStatusValues } from "../../../../features/seller/schemas";
|
|
27
|
+
import { ValidationError } from "../../../../errors";
|
|
28
|
+
import { serverLogger } from "../../../../monitoring/server-logger";
|
|
29
|
+
import { normalizeError } from "../../../../errors/normalize";
|
|
30
|
+
import { activeLane, laneOf, laneBlockReason } from "../../../shared/checkout/lanes";
|
|
31
|
+
/**
|
|
32
|
+
* Refuse a checkout that isn't the cart's active lane.
|
|
33
|
+
*
|
|
34
|
+
* The client scopes its selection to one lane's tab, but a selection is just a
|
|
35
|
+
* list of item ids in a request body — nothing stopped a caller from checking
|
|
36
|
+
* out their ₹200 standard items while a ₹30,000 auction win they already
|
|
37
|
+
* committed to sat unpaid. Priority is enforced here, on the server, over the
|
|
38
|
+
* WHOLE cart, not over the caller-supplied subset.
|
|
39
|
+
*/
|
|
40
|
+
export function assertCheckoutLane(allCartItems, selectedItems) {
|
|
41
|
+
const active = activeLane(allCartItems);
|
|
42
|
+
if (!active)
|
|
43
|
+
return;
|
|
44
|
+
const offLane = selectedItems.filter((item) => laneOf(item) !== active);
|
|
45
|
+
if (offLane.length === 0)
|
|
46
|
+
return;
|
|
47
|
+
throw new ValidationError(laneBlockReason(allCartItems, laneOf(offLane[0])) ??
|
|
48
|
+
"Please complete your outstanding items first.", { code: CHECKOUT_LANE_BLOCKED });
|
|
49
|
+
}
|
|
50
|
+
/** Error code the client maps to a lane-aware message. */
|
|
51
|
+
export const CHECKOUT_LANE_BLOCKED = "CHECKOUT_LANE_BLOCKED";
|
|
52
|
+
/**
|
|
53
|
+
* Re-validate every locked line immediately before the order is written.
|
|
54
|
+
*
|
|
55
|
+
* `checkoutOffer()` already checks status/deadline/stock when the line is ADDED
|
|
56
|
+
* to the cart, but nothing re-checked at order time — so a buyer could add an
|
|
57
|
+
* accepted offer, wait out the 48h window, and still be charged the locked
|
|
58
|
+
* price. Throws on the first invalid line; the caller's transaction aborts.
|
|
59
|
+
*/
|
|
60
|
+
export async function assertLockedLinesStillValid(items, buyerUid) {
|
|
61
|
+
const now = new Date();
|
|
62
|
+
for (const item of items) {
|
|
63
|
+
if (!item.offerId)
|
|
64
|
+
continue;
|
|
65
|
+
const offer = await offerRepository.findById(item.offerId);
|
|
66
|
+
if (!offer) {
|
|
67
|
+
throw new ValidationError("The offer for one of your items no longer exists. Remove it and try again.");
|
|
68
|
+
}
|
|
69
|
+
if (offer.buyerUid !== buyerUid) {
|
|
70
|
+
throw new ValidationError("This offer belongs to a different account.");
|
|
71
|
+
}
|
|
72
|
+
if (offer.status !== OfferStatusValues.ACCEPTED) {
|
|
73
|
+
throw new ValidationError(offer.status === OfferStatusValues.PAID
|
|
74
|
+
? "This offer has already been used for an order."
|
|
75
|
+
: "This offer is no longer accepted. Remove it from your cart and make a new offer.");
|
|
76
|
+
}
|
|
77
|
+
if (offer.checkoutDeadline && now > offer.checkoutDeadline) {
|
|
78
|
+
throw new ValidationError("The checkout window for this accepted offer has expired. Please make a new offer.");
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Close the loop after the order exists.
|
|
84
|
+
*
|
|
85
|
+
* Best-effort by design: the order is already persisted and paid-for, so a
|
|
86
|
+
* failure here must be logged rather than thrown — losing the back-link is a
|
|
87
|
+
* bookkeeping problem, refusing a completed order is a customer-facing one.
|
|
88
|
+
*/
|
|
89
|
+
export async function finalizeLockedLines(items, orderId) {
|
|
90
|
+
for (const item of items) {
|
|
91
|
+
if (item.offerId) {
|
|
92
|
+
try {
|
|
93
|
+
await offerRepository.markPaid(item.offerId, orderId);
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
void normalizeError(err);
|
|
97
|
+
serverLogger.error("finalizeLockedLines: markPaid failed", {
|
|
98
|
+
offerId: item.offerId,
|
|
99
|
+
orderId,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (item.bidId) {
|
|
104
|
+
try {
|
|
105
|
+
await bidRepository.attachOrder(item.bidId, orderId);
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
void normalizeError(err);
|
|
109
|
+
serverLogger.error("finalizeLockedLines: attachOrder failed", {
|
|
110
|
+
bidId: item.bidId,
|
|
111
|
+
orderId,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { toProductItem } from "../../../shared/features/products/to-product-item";
|
|
2
|
+
export { toProductItem };
|
|
1
3
|
import type { ProductDocument } from "../../../../features/products/schemas/firestore";
|
|
2
4
|
import type { ListingType } from "../../../../features/products/types";
|
|
3
5
|
import type { ProductItem } from "../../../../features/products/types";
|
|
@@ -26,8 +28,6 @@ export interface RelatedItemsResult {
|
|
|
26
28
|
relatedByTags: ProductItem[];
|
|
27
29
|
relatedByStore: ProductItem[];
|
|
28
30
|
}
|
|
29
|
-
/** Firestore doc → card-grid shape. Shared by every related/similar-items carousel. */
|
|
30
|
-
export declare function toProductItem(doc: FirestoreDocument): ProductItem;
|
|
31
31
|
/**
|
|
32
32
|
* Same 4-signal related-items computation used across every listing-type
|
|
33
33
|
* detail page (category / brand / tag-overlap / same-store), each excluding
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import { cache } from "react";
|
|
2
|
+
// Imported (not just re-exported) because this module calls it internally.
|
|
3
|
+
// It lives in _internal/shared so `"use client"` components can import it
|
|
4
|
+
// without dragging this file's firebase-admin chain into the client bundle.
|
|
5
|
+
import { toProductItem } from "../../../shared/features/products/to-product-item";
|
|
6
|
+
export { toProductItem };
|
|
2
7
|
import { productRepository } from "../../../../repositories";
|
|
3
8
|
import { reviewRepository } from "../../../../repositories";
|
|
4
9
|
import { getAdminDb } from "../../../../providers/db-firebase";
|
|
@@ -94,29 +99,6 @@ export async function listSitemapProducts() {
|
|
|
94
99
|
const RELATED_QUERY_PAGE_SIZE = 9;
|
|
95
100
|
const RELATED_DISPLAY_LIMIT = 8;
|
|
96
101
|
/** Firestore doc → card-grid shape. Shared by every related/similar-items carousel. */
|
|
97
|
-
export function toProductItem(doc) {
|
|
98
|
-
return {
|
|
99
|
-
id: String(doc.id ?? ""),
|
|
100
|
-
title: String(doc.title ?? doc.name ?? ""),
|
|
101
|
-
price: typeof doc.price === "number" ? doc.price : 0,
|
|
102
|
-
originalPrice: typeof doc.originalPrice === "number" ? doc.originalPrice : undefined,
|
|
103
|
-
mainImage: Array.isArray(doc.images)
|
|
104
|
-
? doc.images[0]
|
|
105
|
-
: typeof doc.mainImage === "string"
|
|
106
|
-
? doc.mainImage
|
|
107
|
-
: undefined,
|
|
108
|
-
status: doc.status ?? "published",
|
|
109
|
-
slug: typeof doc.slug === "string" ? doc.slug : undefined,
|
|
110
|
-
storeName: typeof doc.storeName === "string" ? doc.storeName : undefined,
|
|
111
|
-
rating: typeof doc.rating === "number" ? doc.rating : undefined,
|
|
112
|
-
reviewCount: typeof doc.reviewCount === "number" ? doc.reviewCount : undefined,
|
|
113
|
-
// Without this, every related-item card silently linked to the standard
|
|
114
|
-
// PDP regardless of its real type (pluginFor() defaults to "standard"
|
|
115
|
-
// when listingType is missing) — same bug class as the Phase 1 routing
|
|
116
|
-
// fix, one hop further downstream in the related-items card link path.
|
|
117
|
-
listingType: doc.listingType ?? undefined,
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
102
|
/**
|
|
121
103
|
* Excludes the current product plus any listing-type-specific "invalid to
|
|
122
104
|
* surface as related" state: sold/archived/out-of-stock/draft, ended
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { getProductForDetail, getReviewsForProduct, listSitemapProducts, type SitemapProduct, computeRelatedItems, toProductItem, toReview, getReviewItemsForProduct, type RelatedItemsResult, } from "./data";
|
|
2
|
+
export { listPublicProducts, parsePublicProductParams, PUBLIC_PRODUCT_MAX_PAGE_SIZE, type PublicProductListInput, type PublicProductListResult, type PublicProductListOptions, type PublicProductExecutor, type PublicProductQuery, type ExecutorResult, } from "./list-public";
|
|
2
3
|
export { assertProductOwnership, assertStatusTransition, assertInStock, effectivePrice, isAvailableForPurchase, } from "./service";
|
|
3
4
|
export { createProductAction, createAuctionAction, createPreOrderAction, updateProductAction, deleteProductAction, setProductStatusAction, setProductFeaturedAction, } from "./actions";
|
|
4
5
|
export { PRODUCTS_PAGE_SIZE, PRODUCTS_FEATURED_LIMIT, PRODUCTS_RELATED_LIMIT, PRODUCTS_SITEMAP_LIMIT } from "../../../shared/features/products/config";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { getProductForDetail, getReviewsForProduct, listSitemapProducts, computeRelatedItems, toProductItem, toReview, getReviewItemsForProduct, } from "./data";
|
|
2
|
+
export { listPublicProducts, parsePublicProductParams, PUBLIC_PRODUCT_MAX_PAGE_SIZE, } from "./list-public";
|
|
2
3
|
export { assertProductOwnership, assertStatusTransition, assertInStock, effectivePrice, isAvailableForPurchase, } from "./service";
|
|
3
4
|
export { createProductAction, createAuctionAction, createPreOrderAction, updateProductAction, deleteProductAction, setProductStatusAction, setProductFeaturedAction, } from "./actions";
|
|
4
5
|
export { PRODUCTS_PAGE_SIZE, PRODUCTS_FEATURED_LIMIT, PRODUCTS_RELATED_LIMIT, PRODUCTS_SITEMAP_LIMIT } from "../../../shared/features/products/config";
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import type { JsonValue } from "../../../../schemas/types";
|
|
2
|
+
/** Vercel Hobby Fluid Compute ceiling (CLAUDE.md Rule #6) — never fetch more at once. */
|
|
3
|
+
export declare const PUBLIC_PRODUCT_MAX_PAGE_SIZE = 50;
|
|
4
|
+
/**
|
|
5
|
+
* A product document as it comes back from either executor. Deliberately
|
|
6
|
+
* `JsonValue`-shaped rather than `ProductDocument`: the upstream listingProcessor
|
|
7
|
+
* Function returns already-serialised rows, and this module only ever reads a
|
|
8
|
+
* handful of scalar fields off them (stockQuantity, listingType, the sort key).
|
|
9
|
+
*/
|
|
10
|
+
type Row = Record<string, JsonValue>;
|
|
11
|
+
export interface PublicProductListInput {
|
|
12
|
+
/**
|
|
13
|
+
* Listing types this page spans. Emitted as one pipe-joined OR-group, which
|
|
14
|
+
* the Firebase Sieve adapter upgrades to a `.where(…, "in", …)` query.
|
|
15
|
+
*/
|
|
16
|
+
listingTypes?: readonly string[];
|
|
17
|
+
q?: string;
|
|
18
|
+
category?: string;
|
|
19
|
+
brand?: string;
|
|
20
|
+
/** Pipe-joined multi-select (`new|used`) — emitted as an OR-group, never as AND. */
|
|
21
|
+
condition?: string;
|
|
22
|
+
storeId?: string;
|
|
23
|
+
status?: string;
|
|
24
|
+
minPrice?: string;
|
|
25
|
+
maxPrice?: string;
|
|
26
|
+
minBid?: string;
|
|
27
|
+
maxBid?: string;
|
|
28
|
+
featured?: boolean;
|
|
29
|
+
isPromoted?: boolean;
|
|
30
|
+
freeShipping?: boolean;
|
|
31
|
+
isPartOfBundle?: boolean;
|
|
32
|
+
features?: readonly string[];
|
|
33
|
+
preOrderProductionStatus?: string;
|
|
34
|
+
prizeRevealStatus?: string;
|
|
35
|
+
/** Hide sold-out rows. Applied in memory — never pushed into Firestore. */
|
|
36
|
+
inStock?: boolean;
|
|
37
|
+
dateFrom?: string;
|
|
38
|
+
dateTo?: string;
|
|
39
|
+
/** Pre-validated raw Sieve string (callers must safelist fields themselves). */
|
|
40
|
+
rawFilters?: string | null;
|
|
41
|
+
page?: number;
|
|
42
|
+
pageSize?: number;
|
|
43
|
+
sorts?: string;
|
|
44
|
+
cursor?: string | null;
|
|
45
|
+
}
|
|
46
|
+
export interface PublicProductListResult {
|
|
47
|
+
items: Row[];
|
|
48
|
+
total: number;
|
|
49
|
+
page: number;
|
|
50
|
+
pageSize: number;
|
|
51
|
+
totalPages: number;
|
|
52
|
+
hasMore: boolean;
|
|
53
|
+
cursor: string | null;
|
|
54
|
+
/** The Sieve string actually sent to Firestore — surfaced for debugging. */
|
|
55
|
+
filters: string;
|
|
56
|
+
sorts: string;
|
|
57
|
+
}
|
|
58
|
+
export interface PublicProductQuery {
|
|
59
|
+
filters: string;
|
|
60
|
+
sorts: string;
|
|
61
|
+
page: number;
|
|
62
|
+
pageSize: number;
|
|
63
|
+
cursor?: string | null;
|
|
64
|
+
}
|
|
65
|
+
export interface ExecutorResult {
|
|
66
|
+
items: unknown[];
|
|
67
|
+
total: number;
|
|
68
|
+
page: number;
|
|
69
|
+
totalPages: number;
|
|
70
|
+
hasMore: boolean;
|
|
71
|
+
cursor?: string | null;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* How the query actually runs. Defaults to `productRepository.list`. The consumer
|
|
75
|
+
* route overrides this to prefer the colocated `listingProcessor` Function and fall
|
|
76
|
+
* back to the repository — the seam exists so that preference never has to be
|
|
77
|
+
* duplicated into appkit, which has no access to the consumer's env.
|
|
78
|
+
*/
|
|
79
|
+
export type PublicProductExecutor = (query: PublicProductQuery) => Promise<ExecutorResult>;
|
|
80
|
+
export interface PublicProductListOptions {
|
|
81
|
+
executor?: PublicProductExecutor;
|
|
82
|
+
/**
|
|
83
|
+
* When supplied, rows whose `listingType` is not in this set are stripped after
|
|
84
|
+
* the fetch. Callers that can read siteSettings pass `enabledListingTypes(...)`.
|
|
85
|
+
*/
|
|
86
|
+
enabledListingTypes?: ReadonlySet<string>;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Read the shared public-listing query params. SSR views hand in Next's
|
|
90
|
+
* `searchParams` record; the API route hands in `url.searchParams`. Both produce
|
|
91
|
+
* the same typed input, which is the whole point — the two paths cannot drift.
|
|
92
|
+
*/
|
|
93
|
+
export declare function parsePublicProductParams(params: URLSearchParams | Record<string, string | string[] | undefined>, defaults?: {
|
|
94
|
+
listingTypes?: readonly string[];
|
|
95
|
+
pageSize?: number;
|
|
96
|
+
sorts?: string;
|
|
97
|
+
/**
|
|
98
|
+
* Public browse pages hide sold-out rows until the user flips "Show sold".
|
|
99
|
+
* The API route leaves this off so non-browse callers (related items,
|
|
100
|
+
* homepage sections) keep their current behaviour.
|
|
101
|
+
*/
|
|
102
|
+
hideSoldByDefault?: boolean;
|
|
103
|
+
/**
|
|
104
|
+
* Auctions / pre-orders hide already-ended rows until "Show ended" is on,
|
|
105
|
+
* by defaulting `dateFrom` to now. Must mirror the paired client listing
|
|
106
|
+
* component exactly or the SSR paint and the refetch disagree.
|
|
107
|
+
*/
|
|
108
|
+
hideEndedByDefault?: boolean;
|
|
109
|
+
}): PublicProductListInput;
|
|
110
|
+
export declare function listPublicProducts(input: PublicProductListInput, opts?: PublicProductListOptions): Promise<PublicProductListResult | null>;
|
|
111
|
+
export {};
|