@mohasinac/appkit 4.11.1 → 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.
Files changed (63) hide show
  1. package/dist/_internal/server/features/checkout/actions.js +27 -23
  2. package/dist/_internal/server/features/checkout/locked-lines.d.ts +30 -0
  3. package/dist/_internal/server/features/checkout/locked-lines.js +116 -0
  4. package/dist/_internal/server/features/products/index.d.ts +1 -0
  5. package/dist/_internal/server/features/products/index.js +1 -0
  6. package/dist/_internal/server/features/products/list-public.d.ts +111 -0
  7. package/dist/_internal/server/features/products/list-public.js +342 -0
  8. package/dist/_internal/server/jobs/core/auctionSettlement.js +89 -16
  9. package/dist/_internal/server/jobs/core/offerExpiry.js +116 -4
  10. package/dist/_internal/server/jobs/handlers/messages.d.ts +5 -0
  11. package/dist/_internal/server/jobs/handlers/messages.js +5 -0
  12. package/dist/_internal/shared/checkout/lanes.d.ts +41 -0
  13. package/dist/_internal/shared/checkout/lanes.js +106 -0
  14. package/dist/_internal/shared/checkout/order-math.d.ts +19 -0
  15. package/dist/_internal/shared/checkout/order-math.js +30 -6
  16. package/dist/_internal/shared/listing-types/feature-flags.d.ts +1 -0
  17. package/dist/_internal/shared/listing-types/feature-flags.js +24 -10
  18. package/dist/client.d.ts +2 -1
  19. package/dist/client.js +4 -1
  20. package/dist/features/account/components/NotificationBell.js +1 -0
  21. package/dist/features/account/components/UserOffersPanel.js +8 -1
  22. package/dist/features/admin/actions/notification-actions.js +1 -1
  23. package/dist/features/admin/schemas/firestore.d.ts +2 -1
  24. package/dist/features/admin/schemas/firestore.js +1 -0
  25. package/dist/features/auctions/actions/bid-actions.d.ts +11 -1
  26. package/dist/features/auctions/actions/bid-actions.js +29 -15
  27. package/dist/features/auctions/components/AuctionBidsTable.js +6 -2
  28. package/dist/features/auctions/components/AuctionsListView.js +12 -53
  29. package/dist/features/auctions/components/PlaceBidFormClient.js +10 -1
  30. package/dist/features/auctions/repository/bid.repository.d.ts +15 -0
  31. package/dist/features/auctions/repository/bid.repository.js +19 -0
  32. package/dist/features/auctions/schemas/firestore.d.ts +11 -1
  33. package/dist/features/auctions/schemas/firestore.js +1 -0
  34. package/dist/features/cart/actions/cart-actions.d.ts +11 -0
  35. package/dist/features/cart/actions/cart-actions.js +24 -0
  36. package/dist/features/cart/repository/cart.repository.d.ts +24 -1
  37. package/dist/features/cart/repository/cart.repository.js +71 -6
  38. package/dist/features/cart/schemas/firestore.d.ts +23 -2
  39. package/dist/features/contact/email.js +52 -1
  40. package/dist/features/orders/repository/orders.repository.d.ts +0 -15
  41. package/dist/features/orders/repository/orders.repository.js +0 -27
  42. package/dist/features/orders/utils/order-splitter.js +14 -2
  43. package/dist/features/pre-orders/components/PreOrdersListView.js +11 -39
  44. package/dist/features/products/components/ArtStickersListView.js +10 -49
  45. package/dist/features/products/components/ProductsIndexPageView.js +9 -62
  46. package/dist/features/products/repository/products.repository.js +41 -5
  47. package/dist/features/seller/actions/offer-actions.d.ts +6 -0
  48. package/dist/features/seller/actions/offer-actions.js +8 -9
  49. package/dist/features/seller/components/SellerOffersView.d.ts +6 -1
  50. package/dist/features/seller/components/SellerOffersView.js +59 -8
  51. package/dist/features/seller/repository/offer.repository.d.ts +18 -2
  52. package/dist/features/seller/repository/offer.repository.js +38 -2
  53. package/dist/features/seller/schemas/firestore.d.ts +7 -2
  54. package/dist/features/seller/schemas/firestore.js +3 -0
  55. package/dist/features/seller/schemas/offer-forms.d.ts +12 -0
  56. package/dist/features/seller/schemas/offer-forms.js +28 -0
  57. package/dist/index.d.ts +2 -1
  58. package/dist/index.js +4 -1
  59. package/dist/server-entry.d.ts +1 -0
  60. package/dist/server-entry.js +1 -0
  61. package/dist/server.d.ts +1 -0
  62. package/dist/server.js +3 -0
  63. 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 cartItems = (cart.items ?? []).filter((item) => !excludedSet.has(item.productId) && (!selectedSet || selectedSet.has(item.itemId)));
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,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 {};