@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
@@ -0,0 +1,41 @@
1
+ export declare const CART_LANE: {
2
+ readonly AUCTION: "auction";
3
+ readonly OFFER: "offer";
4
+ readonly STANDARD: "standard";
5
+ };
6
+ export type CartLane = (typeof CART_LANE)[keyof typeof CART_LANE];
7
+ /**
8
+ * Highest obligation first. An auction win is a completed sale the buyer already
9
+ * committed to by bidding; an accepted offer is a price the seller has held for
10
+ * them on a clock; ordinary cart items carry no commitment at all.
11
+ */
12
+ export declare const CART_LANE_PRIORITY: readonly CartLane[];
13
+ export declare const CART_LANE_LABELS: Record<CartLane, string>;
14
+ /** The two lanes whose price is fixed outside the listing. */
15
+ export declare function isLockedLane(lane: CartLane): boolean;
16
+ /** Minimal shape this module needs — works on cart documents and client rows alike. */
17
+ export interface LaneAssignable {
18
+ isAuctionWin?: boolean;
19
+ isOffer?: boolean;
20
+ offerId?: string;
21
+ bidId?: string;
22
+ }
23
+ export declare function laneOf(item: LaneAssignable): CartLane;
24
+ export declare function laneItems<T extends LaneAssignable>(items: readonly T[], lane: CartLane): T[];
25
+ export declare function laneCounts(items: readonly LaneAssignable[]): Record<CartLane, number>;
26
+ /**
27
+ * The one lane the buyer may check out right now, or null for an empty cart.
28
+ */
29
+ export declare function activeLane(items: readonly LaneAssignable[]): CartLane | null;
30
+ export declare function isLaneCheckoutable(items: readonly LaneAssignable[], lane: CartLane): boolean;
31
+ /**
32
+ * Why `lane` can't be checked out right now — null when it can.
33
+ * Phrased for the buyer, naming the lane that's blocking and what to do.
34
+ */
35
+ export declare function laneBlockReason(items: readonly LaneAssignable[], lane: CartLane): string | null;
36
+ /**
37
+ * Whether a NEW item may be added to the cart at all. While a higher-obligation
38
+ * lane is pending, adding more shopping would let the buyer keep deferring the
39
+ * thing they already committed to.
40
+ */
41
+ export declare function canAddNewItems(items: readonly LaneAssignable[]): boolean;
@@ -0,0 +1,106 @@
1
+ /*
2
+ * WHY: A cart can hold three kinds of line with three different obligations — a
3
+ * won auction (you owe it), an accepted offer (you negotiated it, at a
4
+ * locked price, on a deadline), and ordinary shopping. Mixing them into one
5
+ * total lets a buyer settle a ₹200 sticker while a ₹30,000 auction win they
6
+ * already committed to sits unpaid, and makes every total ambiguous.
7
+ * WHAT: The lane model — a derived (never stored) partition of the cart, with a
8
+ * strict priority order. Exactly one lane is checkout-able at a time: the
9
+ * highest-priority non-empty one. Lower lanes stay visible but disabled,
10
+ * and while a higher lane is pending nothing new may be added to the cart.
11
+ *
12
+ * Derived on purpose. A stored `lane` mirror field would drift the first time a
13
+ * write path forgot it (CLAUDE.md Root Cause #42), exactly as the manual-payment
14
+ * queue state is derived rather than stored.
15
+ *
16
+ * EXPORTS:
17
+ * CartLane, CART_LANE_PRIORITY, CART_LANE_LABELS, laneOf, activeLane,
18
+ * laneItems, laneCounts, isLaneCheckoutable, laneBlockReason, isLockedLane
19
+ *
20
+ * @tag domain:checkout,cart
21
+ * @tag layer:shared
22
+ * @tag pattern:none
23
+ * @tag access:isomorphic
24
+ * @tag consumers:CartRouteClient,CheckoutRouteClient,cart-actions,order-splitter,checkout/actions
25
+ * @tag sideEffects:none
26
+ */
27
+ export const CART_LANE = {
28
+ AUCTION: "auction",
29
+ OFFER: "offer",
30
+ STANDARD: "standard",
31
+ };
32
+ /**
33
+ * Highest obligation first. An auction win is a completed sale the buyer already
34
+ * committed to by bidding; an accepted offer is a price the seller has held for
35
+ * them on a clock; ordinary cart items carry no commitment at all.
36
+ */
37
+ export const CART_LANE_PRIORITY = [
38
+ CART_LANE.AUCTION,
39
+ CART_LANE.OFFER,
40
+ CART_LANE.STANDARD,
41
+ ];
42
+ export const CART_LANE_LABELS = {
43
+ auction: "Auction wins",
44
+ offer: "Accepted offers",
45
+ standard: "Cart",
46
+ };
47
+ /** The two lanes whose price is fixed outside the listing. */
48
+ export function isLockedLane(lane) {
49
+ return lane === CART_LANE.AUCTION || lane === CART_LANE.OFFER;
50
+ }
51
+ export function laneOf(item) {
52
+ if (item.isAuctionWin || item.bidId)
53
+ return CART_LANE.AUCTION;
54
+ if (item.isOffer || item.offerId)
55
+ return CART_LANE.OFFER;
56
+ return CART_LANE.STANDARD;
57
+ }
58
+ export function laneItems(items, lane) {
59
+ return items.filter((i) => laneOf(i) === lane);
60
+ }
61
+ export function laneCounts(items) {
62
+ const counts = { auction: 0, offer: 0, standard: 0 };
63
+ for (const item of items)
64
+ counts[laneOf(item)] += 1;
65
+ return counts;
66
+ }
67
+ /**
68
+ * The one lane the buyer may check out right now, or null for an empty cart.
69
+ */
70
+ export function activeLane(items) {
71
+ const counts = laneCounts(items);
72
+ return CART_LANE_PRIORITY.find((lane) => counts[lane] > 0) ?? null;
73
+ }
74
+ export function isLaneCheckoutable(items, lane) {
75
+ return activeLane(items) === lane;
76
+ }
77
+ /**
78
+ * Why `lane` can't be checked out right now — null when it can.
79
+ * Phrased for the buyer, naming the lane that's blocking and what to do.
80
+ */
81
+ export function laneBlockReason(items, lane) {
82
+ const active = activeLane(items);
83
+ if (active === lane)
84
+ return null;
85
+ const counts = laneCounts(items);
86
+ if (counts[lane] === 0)
87
+ return "There's nothing in this tab yet.";
88
+ if (active === CART_LANE.AUCTION) {
89
+ const n = counts.auction;
90
+ return `Settle your ${n} won auction${n === 1 ? "" : "s"} first — auction wins are already committed purchases.`;
91
+ }
92
+ if (active === CART_LANE.OFFER) {
93
+ const n = counts.offer;
94
+ return `Complete your ${n} accepted offer${n === 1 ? "" : "s"} first — the agreed price is only held for a limited time.`;
95
+ }
96
+ return "This tab can't be checked out right now.";
97
+ }
98
+ /**
99
+ * Whether a NEW item may be added to the cart at all. While a higher-obligation
100
+ * lane is pending, adding more shopping would let the buyer keep deferring the
101
+ * thing they already committed to.
102
+ */
103
+ export function canAddNewItems(items) {
104
+ const active = activeLane(items);
105
+ return active === null || active === CART_LANE.STANDARD;
106
+ }
@@ -6,6 +6,25 @@
6
6
  */
7
7
  import type { CartItemDocument } from "../../../features/cart/schemas/firestore";
8
8
  import type { ProductDocument } from "../../../features/products/schemas/firestore";
9
+ /**
10
+ * The single source of truth for "what do we charge for this line".
11
+ *
12
+ * Three cases, in priority order:
13
+ *
14
+ * 1. **A locked price** — an accepted offer or a won auction. `lockedPrice` is
15
+ * the negotiated/won amount and it OVERRIDES the current listing price, which
16
+ * is the whole point of the negotiation. Until 2026-08-21 none of the five
17
+ * copies of this function looked at `lockedPrice`, so an accepted offer was
18
+ * billed at the seller's listed price while the cart UI displayed the agreed
19
+ * one (`item.lockedPrice ?? item.price`) — the buyer saw ₹X and paid ₹Y.
20
+ * 2. **A bundle line** (SB-UNI-5 2026-05-13) — `item.price` is the bundle price
21
+ * locked at add-time.
22
+ * 3. **Everything else** — the live Firestore price, so a stale cart-cached
23
+ * price is never charged on a COD/UPI order.
24
+ *
25
+ * `product` may be null for a locked line whose listing has since been archived;
26
+ * the locked price is still owed, so that case must not dereference `product`.
27
+ */
9
28
  export declare function unitPriceFor(item: CartItemDocument, product: ProductDocument | null): number;
10
29
  /**
11
30
  * P-6 — pre-order groups charge each product's own `preOrderDepositPercent`
@@ -1,11 +1,35 @@
1
1
  import { roundRupees } from "../../../utils/number.formatter";
2
- // SB-UNI-5 2026-05-13 — bundle cart-lines use item.price (locked bundle price
3
- // at add-time); regular lines use product.price (current Firestore). Prevents
4
- // stale cart-cached prices from being charged on COD/UPI orders.
2
+ /**
3
+ * The single source of truth for "what do we charge for this line".
4
+ *
5
+ * Three cases, in priority order:
6
+ *
7
+ * 1. **A locked price** — an accepted offer or a won auction. `lockedPrice` is
8
+ * the negotiated/won amount and it OVERRIDES the current listing price, which
9
+ * is the whole point of the negotiation. Until 2026-08-21 none of the five
10
+ * copies of this function looked at `lockedPrice`, so an accepted offer was
11
+ * billed at the seller's listed price while the cart UI displayed the agreed
12
+ * one (`item.lockedPrice ?? item.price`) — the buyer saw ₹X and paid ₹Y.
13
+ * 2. **A bundle line** (SB-UNI-5 2026-05-13) — `item.price` is the bundle price
14
+ * locked at add-time.
15
+ * 3. **Everything else** — the live Firestore price, so a stale cart-cached
16
+ * price is never charged on a COD/UPI order.
17
+ *
18
+ * `product` may be null for a locked line whose listing has since been archived;
19
+ * the locked price is still owed, so that case must not dereference `product`.
20
+ */
5
21
  export function unitPriceFor(item, product) {
6
- return item.bundleCategorySlug && item.bundleProductIds?.length
7
- ? item.price
8
- : product.price;
22
+ if (typeof item.lockedPrice === "number" && item.lockedPrice > 0) {
23
+ return item.lockedPrice;
24
+ }
25
+ if (item.bundleCategorySlug && item.bundleProductIds?.length) {
26
+ return item.price;
27
+ }
28
+ // Null-safe fallback: one of the four call sites this replaced (the cart
29
+ // summary/preview path) already guarded with `product?.price ?? item.price`,
30
+ // and collapsing onto a non-null assertion would have regressed it into a
31
+ // crash whenever a cart line outlives its listing.
32
+ return product?.price ?? item.price;
9
33
  }
10
34
  /**
11
35
  * P-6 — pre-order groups charge each product's own `preOrderDepositPercent`
@@ -28,6 +28,7 @@ interface FeatureFlagSnapshot {
28
28
  */
29
29
  export declare function isListingTypeEnabled(type: ListingType, settings: FeatureFlagSnapshot | null | undefined): boolean;
30
30
  export declare function isCategoryTypeEnabled(type: CategoryType, settings: FeatureFlagSnapshot | null | undefined): boolean;
31
+ export declare const ALL_LISTING_TYPES: ListingType[];
31
32
  /** Pull the full list of enabled listing types in canonical iteration order. */
32
33
  export declare function enabledListingTypes(settings: FeatureFlagSnapshot | null | undefined): ListingType[];
33
34
  export declare function enabledCategoryTypes(settings: FeatureFlagSnapshot | null | undefined): CategoryType[];
@@ -26,18 +26,32 @@ export function isCategoryTypeEnabled(type, settings) {
26
26
  const flag = settings?.featureFlags?.categoryTypes?.[type];
27
27
  return flag !== false;
28
28
  }
29
+ /**
30
+ * Every listing type, in canonical iteration order.
31
+ *
32
+ * Declared as `Record<ListingType, true>` rather than a plain array on purpose:
33
+ * adding a member to the `ListingType` union without adding it here is then a
34
+ * COMPILE error, not a silent omission. `art` / `stickers` were missing from
35
+ * the old hand-written array for exactly that reason, which meant
36
+ * `/api/products`' "strip disabled types" post-filter dropped every art and
37
+ * sticker row from any call that didn't name a listingType explicitly
38
+ * (homepage, search, related-items).
39
+ */
40
+ const ALL_LISTING_TYPES_MAP = {
41
+ standard: true,
42
+ auction: true,
43
+ "pre-order": true,
44
+ "prize-draw": true,
45
+ classified: true,
46
+ "digital-code": true,
47
+ live: true,
48
+ art: true,
49
+ stickers: true,
50
+ };
51
+ export const ALL_LISTING_TYPES = Object.keys(ALL_LISTING_TYPES_MAP);
29
52
  /** Pull the full list of enabled listing types in canonical iteration order. */
30
53
  export function enabledListingTypes(settings) {
31
- const all = [
32
- "standard",
33
- "auction",
34
- "pre-order",
35
- "prize-draw",
36
- "classified",
37
- "digital-code",
38
- "live",
39
- ];
40
- return all.filter((t) => isListingTypeEnabled(t, settings));
54
+ return ALL_LISTING_TYPES.filter((t) => isListingTypeEnabled(t, settings));
41
55
  }
42
56
  export function enabledCategoryTypes(settings) {
43
57
  const all = ["category", "sublisting", "brand", "bundle"];
package/dist/client.d.ts CHANGED
@@ -321,7 +321,7 @@ export type { GuestHistoryItem, GuestHistoryType, UserHistoryItem, HistoryProduc
321
321
  export type { TrackArgs as TrackHistoryArgs } from "./features/history/hooks/useHistory";
322
322
  export { WISHLIST_MAX, HISTORY_MAX, CART_MAX_ITEMS, } from "./constants/limits";
323
323
  export { normalizeListingType, isAuctionListing, isPreOrderListing, isStandardListing, isPrizeDrawListing, isClassifiedListing, isDigitalCodeListing, isLiveListing, isArtListing, isStickersListing, } from "./features/products/utils/listing-type";
324
- export { isListingTypeEnabled, isCategoryTypeEnabled, enabledListingTypes, enabledCategoryTypes, } from "./_internal/shared/listing-types/feature-flags";
324
+ export { isListingTypeEnabled, isCategoryTypeEnabled, enabledListingTypes, enabledCategoryTypes, ALL_LISTING_TYPES, } from "./_internal/shared/listing-types/feature-flags";
325
325
  export { actionTracker, setActionTrackerSink, resetActionTrackerSink, type ActionEvent, type ActionTrackerSink, } from "./_internal/shared/listing-types/action-tracker";
326
326
  export { cartRequiresShipping, cartIsDigitalOnly, cartIsChatOnly, } from "./_internal/shared/listing-types/cart-shipping";
327
327
  export { ACTIONS, action, act, canPerformAction, actionsForListingType, actionLabel, type ActionDef, type ActionKind, type ActionResource, type ActionTree, type ActionConfirmation, } from "./_internal/shared/actions/action-registry";
@@ -410,3 +410,4 @@ export { API_ENDPOINTS } from "./constants/index";
410
410
  export { SELLER_LISTING_TABS, type SellerListingTabId, } from "./features/products/constants/listing-tabs";
411
411
  export { ALL_TAB, EMPTY_TAB, ADMIN_PRODUCT_STATUS_TABS, ADMIN_PRODUCT_LISTING_TYPE_TABS, ADMIN_BLOG_STATUS_TABS, ADMIN_USER_STATUS_TABS, ADMIN_USER_ROLE_TABS, ADMIN_STORE_STATUS_TABS, ADMIN_PAYOUT_STATUS_TABS, ADMIN_ORDER_STATUS_TABS, ADMIN_REVIEW_STATUS_TABS, ADMIN_REVIEW_RATING_TABS, ADMIN_BID_STATUS_TABS, ADMIN_CONTACT_STATUS_TABS, ADMIN_NEWSLETTER_STATUS_TABS, ADMIN_EVENT_ENTRY_STATUS_TABS, ADMIN_EVENT_STATUS_TABS, ADMIN_CART_OWNERSHIP_TABS, ADMIN_COUPON_TYPE_TABS, SELLER_PRODUCT_STATUS_TABS, SELLER_AUCTION_STATUS_TABS, SELLER_ORDER_STATUS_TABS, SELLER_OFFER_STATUS_TABS, SELLER_BID_STATUS_TABS, } from "./features/admin/constants/filter-tabs";
412
412
  export { PRODUCT_FIELDS, PRODUCT_STATUS_TRANSITIONS, ORDER_FIELDS, REVIEW_FIELDS, BID_FIELDS, AD_FIELDS, EVENT_FIELDS, EVENT_ENTRY_FIELDS, PAYOUT_FIELDS, STORE_FIELDS, CATEGORY_FIELDS, BLOG_FIELDS, USER_FIELDS, ADDRESS_FIELDS, BRAND_FIELDS, CART_FIELDS, WISHLIST_FIELDS, HISTORY_FIELDS, NOTIFICATION_FIELDS, SESSION_FIELDS, COUPON_USAGE_FIELDS, CONVERSATION_FIELDS, SCAMMER_FIELDS, SUPPORT_TICKET_FIELDS, CAROUSEL_FIELDS, COUPON_FIELDS, FAQ_FIELDS, HOMEPAGE_SECTION_FIELDS, SITE_SETTINGS_FIELDS, COMMON_FIELDS, OAUTH_STATE_VALUES, SCHEMA_DEFAULTS, } from "./constants/field-names";
413
+ export { CART_LANE, CART_LANE_PRIORITY, CART_LANE_LABELS, laneOf, activeLane, laneItems, laneCounts, isLaneCheckoutable, laneBlockReason, isLockedLane, canAddNewItems, type CartLane, type LaneAssignable, } from "./_internal/shared/checkout/lanes";
package/dist/client.js CHANGED
@@ -319,7 +319,7 @@ export { WISHLIST_MAX, HISTORY_MAX, CART_MAX_ITEMS, } from "./constants/limits";
319
319
  // SB-UNI-F 2026-05-13 — Phase 2 predicates surfaced through client barrel.
320
320
  export { normalizeListingType, isAuctionListing, isPreOrderListing, isStandardListing, isPrizeDrawListing, isClassifiedListing, isDigitalCodeListing, isLiveListing, isArtListing, isStickersListing, } from "./features/products/utils/listing-type";
321
321
  // SB-UNI-X4 2026-05-13 — per-type feature-flag helpers (client-safe).
322
- export { isListingTypeEnabled, isCategoryTypeEnabled, enabledListingTypes, enabledCategoryTypes, } from "./_internal/shared/listing-types/feature-flags";
322
+ export { isListingTypeEnabled, isCategoryTypeEnabled, enabledListingTypes, enabledCategoryTypes, ALL_LISTING_TYPES, } from "./_internal/shared/listing-types/feature-flags";
323
323
  // SB-UNI-X5 2026-05-13 — action telemetry sink (client-safe; defaults to
324
324
  // a no-op + console.debug in dev).
325
325
  export { actionTracker, setActionTrackerSink, resetActionTrackerSink, } from "./_internal/shared/listing-types/action-tracker";
@@ -393,3 +393,6 @@ export { API_ENDPOINTS } from "./constants/index";
393
393
  export { SELLER_LISTING_TABS, } from "./features/products/constants/listing-tabs";
394
394
  export { ALL_TAB, EMPTY_TAB, ADMIN_PRODUCT_STATUS_TABS, ADMIN_PRODUCT_LISTING_TYPE_TABS, ADMIN_BLOG_STATUS_TABS, ADMIN_USER_STATUS_TABS, ADMIN_USER_ROLE_TABS, ADMIN_STORE_STATUS_TABS, ADMIN_PAYOUT_STATUS_TABS, ADMIN_ORDER_STATUS_TABS, ADMIN_REVIEW_STATUS_TABS, ADMIN_REVIEW_RATING_TABS, ADMIN_BID_STATUS_TABS, ADMIN_CONTACT_STATUS_TABS, ADMIN_NEWSLETTER_STATUS_TABS, ADMIN_EVENT_ENTRY_STATUS_TABS, ADMIN_EVENT_STATUS_TABS, ADMIN_CART_OWNERSHIP_TABS, ADMIN_COUPON_TYPE_TABS, SELLER_PRODUCT_STATUS_TABS, SELLER_AUCTION_STATUS_TABS, SELLER_ORDER_STATUS_TABS, SELLER_OFFER_STATUS_TABS, SELLER_BID_STATUS_TABS, } from "./features/admin/constants/filter-tabs";
395
395
  export { PRODUCT_FIELDS, PRODUCT_STATUS_TRANSITIONS, ORDER_FIELDS, REVIEW_FIELDS, BID_FIELDS, AD_FIELDS, EVENT_FIELDS, EVENT_ENTRY_FIELDS, PAYOUT_FIELDS, STORE_FIELDS, CATEGORY_FIELDS, BLOG_FIELDS, USER_FIELDS, ADDRESS_FIELDS, BRAND_FIELDS, CART_FIELDS, WISHLIST_FIELDS, HISTORY_FIELDS, NOTIFICATION_FIELDS, SESSION_FIELDS, COUPON_USAGE_FIELDS, CONVERSATION_FIELDS, SCAMMER_FIELDS, SUPPORT_TICKET_FIELDS, CAROUSEL_FIELDS, COUPON_FIELDS, FAQ_FIELDS, HOMEPAGE_SECTION_FIELDS, SITE_SETTINGS_FIELDS, COMMON_FIELDS, OAUTH_STATE_VALUES, SCHEMA_DEFAULTS, } from "./constants/field-names";
396
+ // Checkout lanes — the derived auction > offer > standard partition of the
397
+ // cart, and the priority rule that decides which one may be checked out.
398
+ export { CART_LANE, CART_LANE_PRIORITY, CART_LANE_LABELS, laneOf, activeLane, laneItems, laneCounts, isLaneCheckoutable, laneBlockReason, isLockedLane, canAddNewItems, } from "./_internal/shared/checkout/lanes";
@@ -22,6 +22,7 @@ const DEFAULT_ICONS = {
22
22
  bid_outbid: "⚡",
23
23
  bid_won: "🏆",
24
24
  bid_lost: "😔",
25
+ auction_ended: "🔚",
25
26
  review_approved: "⭐",
26
27
  review_replied: "💬",
27
28
  product_available: "🔔",
@@ -79,8 +79,15 @@ export function UserOffersPanel({ fetchEndpoint = ACCOUNT_ENDPOINTS.OFFERS, onAc
79
79
  }
80
80
  throw new Error(`Error ${res.status}`);
81
81
  }
82
+ // `/api/user/offers` replies through `successResponse`, i.e.
83
+ // `{ success, data: { items, total, … } }`. This used to read `json.items`
84
+ // straight off the envelope — always undefined — so "My Offers" showed
85
+ // "No offers yet" for every buyer, forever, with no error to notice.
86
+ // Both shapes are still accepted so an unwrapped caller keeps working.
82
87
  const json = (await res.json());
83
- const items = Array.isArray(json) ? json : (json.items ?? []);
88
+ const items = Array.isArray(json)
89
+ ? json
90
+ : (json.data?.items ?? json.items ?? []);
84
91
  setOffers(items);
85
92
  }
86
93
  catch (err) {
@@ -106,7 +106,7 @@ export async function sendNotification(input) {
106
106
  order_placed: "orderUpdates", order_confirmed: "orderUpdates",
107
107
  order_shipped: "orderUpdates", order_delivered: "orderUpdates",
108
108
  order_cancelled: "orderUpdates",
109
- bid_placed: "bids", bid_outbid: "bids", bid_won: "bids", bid_lost: "bids",
109
+ bid_placed: "bids", bid_outbid: "bids", bid_won: "bids", bid_lost: "bids", auction_ended: "bids",
110
110
  review_approved: "reviews", review_replied: "reviews",
111
111
  promotion: "promotions",
112
112
  system: "system", welcome: "system", account_action: "system",
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import type { AboutContentDocument } from "../../about/schemas/firestore";
6
6
  export declare const ADMIN_CHECKOUT_BYPASS_FLAG_KEY: "adminCheckoutBypass";
7
- export type NotificationType = "order_placed" | "order_confirmed" | "order_shipped" | "order_delivered" | "order_cancelled" | "bid_placed" | "bid_outbid" | "bid_won" | "bid_lost" | "review_approved" | "review_replied" | "product_available" | "promotion" | "system" | "welcome" | "account_action" | "offer_received" | "offer_responded" | "offer_expired" | "offer_counter_accepted" | "refund_initiated" | "prize_won" | "prize_reveal_expired" | "emi_installment_due_soon" | "emi_installment_overdue" | "payment_review";
7
+ export type NotificationType = "order_placed" | "order_confirmed" | "order_shipped" | "order_delivered" | "order_cancelled" | "bid_placed" | "bid_outbid" | "bid_won" | "bid_lost" | "auction_ended" | "review_approved" | "review_replied" | "product_available" | "promotion" | "system" | "welcome" | "account_action" | "offer_received" | "offer_responded" | "offer_expired" | "offer_counter_accepted" | "refund_initiated" | "prize_won" | "prize_reveal_expired" | "emi_installment_due_soon" | "emi_installment_overdue" | "payment_review";
8
8
  import type { BaseDocument } from "../../../_internal/shared/types/base-document";
9
9
  export type NotificationPriority = "low" | "normal" | "high";
10
10
  export interface NotificationDocument extends BaseDocument {
@@ -53,6 +53,7 @@ export declare const NOTIFICATION_FIELDS: {
53
53
  readonly BID_OUTBID: NotificationType;
54
54
  readonly BID_WON: NotificationType;
55
55
  readonly BID_LOST: NotificationType;
56
+ readonly AUCTION_ENDED: NotificationType;
56
57
  readonly REVIEW_APPROVED: NotificationType;
57
58
  readonly REVIEW_REPLIED: NotificationType;
58
59
  readonly PRODUCT_AVAILABLE: NotificationType;
@@ -41,6 +41,7 @@ export const NOTIFICATION_FIELDS = {
41
41
  BID_OUTBID: "bid_outbid",
42
42
  BID_WON: "bid_won",
43
43
  BID_LOST: "bid_lost",
44
+ AUCTION_ENDED: "auction_ended",
44
45
  REVIEW_APPROVED: "review_approved",
45
46
  REVIEW_REPLIED: "review_replied",
46
47
  PRODUCT_AVAILABLE: "product_available",
@@ -13,9 +13,19 @@ export interface BuyNowAuctionInput {
13
13
  productId: string;
14
14
  }
15
15
  export interface BuyNowAuctionResult {
16
- orderId: string;
16
+ /**
17
+ * Buy-Now no longer creates an order directly. It writes a LOCKED CART LINE
18
+ * (same shape auction settlement produces for a winning bidder) and the buyer
19
+ * completes the real order through the auction checkout lane. The old
20
+ * behaviour created a document that no orders UI could render and no payment
21
+ * path could reach — see auctionSettlement.ts for the full writeup.
22
+ */
23
+ cartLocked: true;
24
+ productId: string;
17
25
  amount: number;
18
26
  currency: string;
27
+ /** Where to send the buyer to actually pay. */
28
+ checkoutUrl: string;
19
29
  }
20
30
  export declare function buyNowAuction(userId: string, userName: string, userEmail: string, input: BuyNowAuctionInput): Promise<BuyNowAuctionResult>;
21
31
  export declare function listBidsByProduct(productId: string, params?: {
@@ -22,6 +22,14 @@ import { ERROR_MESSAGES, AuthorizationError, ValidationError, NotFoundError, } f
22
22
  import { BID_ERROR_CODES } from "../../../errors/error-codes";
23
23
  import { increment } from "../../../contracts/field-ops";
24
24
  import { getDefaultCurrency } from "../../../core/baseline-resolver";
25
+ import { cartRepository } from "../../cart/repository/cart.repository";
26
+ import { ROUTES } from "../../../next/routing/route-map";
27
+ import { CART_LANE } from "../../../_internal/shared/checkout/lanes";
28
+ /**
29
+ * Buy-Now and auction settlement give the buyer the same 48h window to pay for
30
+ * a locked line. Kept identical on purpose — both are "you now owe this".
31
+ */
32
+ const AUCTION_CHECKOUT_WINDOW_MS = 48 * 60 * 60 * 1000;
25
33
  import { resolveDate } from "../../../utils";
26
34
  // --- Domain Functions ----------------------------------------------------------
27
35
  export async function placeBid(userId, userEmail, input) {
@@ -194,33 +202,39 @@ export async function buyNowAuction(userId, userName, userEmail, input) {
194
202
  throw new ValidationError(ERROR_MESSAGES.BID.BUY_NOW_BIDS_STARTED, { code: BID_ERROR_CODES.BUY_NOW_BIDS_STARTED });
195
203
  }
196
204
  const currency = getDefaultCurrency();
197
- let orderId = "";
198
205
  await unitOfWork.runBatch((batch) => {
199
- const orderRef = unitOfWork.orders.createFromAuction(batch, {
200
- productId,
201
- productTitle: product.title,
202
- userId,
203
- userName,
204
- userEmail,
205
- storeId: product.storeId,
206
- amount: buyNowPrice,
207
- currency,
208
- auctionProductId: productId,
209
- });
210
- orderId = orderRef.id;
211
206
  unitOfWork.products.updateInBatch(batch, productId, {
212
207
  isSold: true,
213
208
  availableQuantity: 0,
214
209
  auctionEndDate: new Date(),
215
210
  });
216
211
  });
212
+ // Locked cart line, not an order — see BuyNowAuctionResult above. Written
213
+ // through the repository directly (auctions are canAddToCart:false for
214
+ // user-initiated adds), the same deliberate bypass checkoutOffer uses.
215
+ await cartRepository.addItem(userId, {
216
+ productId,
217
+ productTitle: product.title,
218
+ productImage: product.mainImage ?? "",
219
+ price: buyNowPrice,
220
+ currency,
221
+ quantity: 1,
222
+ storeId: product.storeId,
223
+ storeName: product.storeName ?? "",
224
+ listingType: "auction",
225
+ isAuctionWin: true,
226
+ auctionId: productId,
227
+ lockedPrice: buyNowPrice,
228
+ checkoutDeadline: new Date(Date.now() + AUCTION_CHECKOUT_WINDOW_MS),
229
+ locked: true,
230
+ });
231
+ const checkoutUrl = `${String(ROUTES.USER.CHECKOUT)}?lane=${CART_LANE.AUCTION}`;
217
232
  serverLogger.info("buyNowAuction", {
218
- orderId,
219
233
  productId,
220
234
  userId,
221
235
  amount: buyNowPrice,
222
236
  });
223
- return { orderId, amount: buyNowPrice, currency };
237
+ return { cartLocked: true, productId, amount: buyNowPrice, currency, checkoutUrl };
224
238
  }
225
239
  export async function listBidsByProduct(productId, params) {
226
240
  const result = await bidRepository.list({
@@ -2,7 +2,9 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useMemo, useState } from "react";
4
4
  import { ChevronRight } from "lucide-react";
5
- import { Badge, Button, Div, Row, Span, Stack, Text } from "../../../ui";
5
+ import { Badge, Button, Div, Row, Span, Stack, Text, TextLink } from "../../../ui";
6
+ import { ROUTES } from "../../../next/routing/route-map";
7
+ import { CART_LANE } from "../../../_internal/shared/checkout/lanes";
6
8
  import { Pagination } from "../../../ui/components/Pagination";
7
9
  const __O = {
8
10
  hidden: "overflow-hidden",
@@ -58,7 +60,9 @@ function AuctionRow({ auction, portal, }) {
58
60
  const sorted = useMemo(() => [...auction.bids].sort((a, b) => +new Date(b.bidDate) - +new Date(a.bidDate)), [auction.bids]);
59
61
  const highest = auction.bids.reduce((max, b) => Math.max(max, b.bidAmount), 0);
60
62
  const isWinning = auction.bids.some((b) => b.isWinning);
61
- return (_jsxs(Div, { className: `border border-[var(--appkit-color-border)] ${__O.hidden} bg-[var(--appkit-color-surface)]`, rounded: "xl", shadow: "sm", children: [_jsxs(Button, { type: "button", variant: "ghost", onClick: () => setExpanded((v) => !v), gap: "lg", paddingX: "md", paddingY: "md", className: "w-full text-left", "aria-expanded": expanded, children: [_jsx(ChevronRight, { className: `shrink-0 text-[var(--appkit-color-text-muted)] transition-transform ${expanded ? "rotate-90" : ""}`, size: 16 }), _jsx(Div, { className: "flex-1 min-w-0", children: _jsx(Text, { className: "text-[var(--appkit-color-text)] line-clamp-1", size: "sm", weight: "semibold", children: auction.productTitle }) }), _jsxs(Row, { gap: "sm", className: "shrink-0", children: [isWinning && (_jsx(Badge, { variant: "active", children: "Winning" })), _jsxs(Text, { variant: "secondary", size: "xs", children: [auction.bids.length, " bid", auction.bids.length !== 1 ? "s" : ""] }), _jsx(Text, { className: "text-[var(--appkit-color-text)]", size: "sm", weight: "semibold", children: formatBidAmount(highest) })] })] }), expanded && (_jsxs(Div, { className: "border-t border-[var(--appkit-color-border)]", children: [_jsxs(Div, { textSize: "xs", textWeight: "medium", paddingX: "x-md", paddingY: "y-xs", className: `grid text-[var(--appkit-color-text-muted)] uppercase tracking-wide border-b border-[var(--appkit-color-border-subtle)] ${portal === "buyer" ? "[grid-template-columns:1fr_auto_auto]" : "[grid-template-columns:1fr_1fr_auto_auto]"}`, children: [_jsx(Span, { children: portal === "buyer" ? "Amount" : "Bidder" }), portal !== "buyer" && _jsx(Span, { children: "Amount" }), _jsx(Span, { children: "Status" }), _jsx(Span, { className: "text-right", children: "Time" })] }), sorted.map((bid) => (_jsxs(Div, { layout: "grid", align: "center", paddingX: "x-md", paddingY: "y-xs-tall", className: `border-b border-[var(--appkit-color-border-subtle)] last:border-0 hover:bg-[var(--appkit-color-border-subtle)] transition-colors ${portal === "buyer" ? "[grid-template-columns:1fr_auto_auto]" : "[grid-template-columns:1fr_1fr_auto_auto]"}`, children: [portal !== "buyer" && (_jsx(Text, { className: "text-[var(--appkit-color-text)] truncate pr-[0.75rem]", size: "sm", children: bid.userName || bid.userId })), _jsx(Text, { className: "text-[var(--appkit-color-text)]", size: "sm", weight: "medium", children: formatBidAmount(bid.bidAmount) }), _jsx(Badge, { variant: STATUS_VARIANT[bid.status] ?? "pending", className: "capitalize", children: bid.status }), _jsx(Text, { variant: "secondary", size: "xs", align: "end", children: relDate(bid.bidDate) })] }, bid.id)))] }))] }));
63
+ return (_jsxs(Div, { className: `border border-[var(--appkit-color-border)] ${__O.hidden} bg-[var(--appkit-color-surface)]`, rounded: "xl", shadow: "sm", children: [_jsxs(Button, { type: "button", variant: "ghost", onClick: () => setExpanded((v) => !v), gap: "lg", paddingX: "md", paddingY: "md", className: "w-full text-left", "aria-expanded": expanded, children: [_jsx(ChevronRight, { className: `shrink-0 text-[var(--appkit-color-text-muted)] transition-transform ${expanded ? "rotate-90" : ""}`, size: 16 }), _jsx(Div, { className: "flex-1 min-w-0", children: _jsx(Text, { className: "text-[var(--appkit-color-text)] line-clamp-1", size: "sm", weight: "semibold", children: auction.productTitle }) }), _jsxs(Row, { gap: "sm", className: "shrink-0", children: [isWinning && (_jsx(Badge, { variant: "active", children: "Winning" })), _jsxs(Text, { variant: "secondary", size: "xs", children: [auction.bids.length, " bid", auction.bids.length !== 1 ? "s" : ""] }), _jsx(Text, { className: "text-[var(--appkit-color-text)]", size: "sm", weight: "semibold", children: formatBidAmount(highest) })] })] }), expanded && (_jsxs(Div, { className: "border-t border-[var(--appkit-color-border)]", children: [_jsxs(Div, { textSize: "xs", textWeight: "medium", paddingX: "x-md", paddingY: "y-xs", className: `grid text-[var(--appkit-color-text-muted)] uppercase tracking-wide border-b border-[var(--appkit-color-border-subtle)] ${portal === "buyer" ? "[grid-template-columns:1fr_auto_auto]" : "[grid-template-columns:1fr_1fr_auto_auto]"}`, children: [_jsx(Span, { children: portal === "buyer" ? "Amount" : "Bidder" }), portal !== "buyer" && _jsx(Span, { children: "Amount" }), _jsx(Span, { children: "Status" }), _jsx(Span, { className: "text-right", children: "Time" })] }), sorted.map((bid) => (_jsxs(Div, { layout: "grid", align: "center", paddingX: "x-md", paddingY: "y-xs-tall", className: `border-b border-[var(--appkit-color-border-subtle)] last:border-0 hover:bg-[var(--appkit-color-border-subtle)] transition-colors ${portal === "buyer" ? "[grid-template-columns:1fr_auto_auto]" : "[grid-template-columns:1fr_1fr_auto_auto]"}`, children: [portal !== "buyer" && (_jsx(Text, { className: "text-[var(--appkit-color-text)] truncate pr-[0.75rem]", size: "sm", children: bid.userName || bid.userId })), _jsx(Text, { className: "text-[var(--appkit-color-text)]", size: "sm", weight: "medium", children: formatBidAmount(bid.bidAmount) }), _jsx(Badge, { variant: STATUS_VARIANT[bid.status] ?? "pending", className: "capitalize", children: bid.status }), _jsxs(Stack, { gap: "xs", className: "items-end", children: [_jsx(Text, { variant: "secondary", size: "xs", align: "end", children: relDate(bid.bidDate) }), portal === "buyer" && bid.status === "won" && (_jsx(TextLink, { href: bid.orderId
64
+ ? String(ROUTES.USER.ORDER_DETAIL(bid.orderId))
65
+ : `${String(ROUTES.USER.CHECKOUT)}?lane=${CART_LANE.AUCTION}`, size: "xs", weight: "semibold", children: bid.orderId ? "View order" : "Pay now →" }))] })] }, bid.id)))] }))] }));
62
66
  }
63
67
  export function AuctionBidsTable({ bids, portal = "buyer", emptyLabel = "No bids found.", totalPages, currentPage, onPageChange, }) {
64
68
  const auctions = useMemo(() => groupByAuction(bids), [bids]);
@@ -1,62 +1,21 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { sieveFilter, SIEVE_OP } from "@mohasinac/appkit";
3
- import { productRepository } from "../../../repositories";
4
2
  import { Container, Heading, Main, Section } from "../../../ui";
5
3
  import { AdSlot } from "../../homepage/components/AdSlot";
6
- import { parseListingSearchParams } from "../../../utils/listing-params";
7
4
  import { AuctionsIndexListing } from "../../products/components/AuctionsIndexListing";
8
- const DEFAULT_PAGE = 1;
5
+ import { listPublicProducts, parsePublicProductParams, } from "../../../_internal/server/features/products/list-public";
6
+ const AUCTION_LISTING_TYPES = ["auction"];
9
7
  const DEFAULT_PAGE_SIZE = 24;
10
8
  const DEFAULT_SORT = "auctionEndDate";
11
- function sp(params, key) {
12
- const v = params[key];
13
- return Array.isArray(v) ? v[0] ?? "" : v ?? "";
14
- }
15
- function buildAuctionFilters(params) {
16
- const parts = ["status==published", "listingType==auction"];
17
- const minBid = sp(params, "minBid");
18
- const maxBid = sp(params, "maxBid");
19
- if (minBid)
20
- parts.push(sieveFilter("currentBid", SIEVE_OP.GTE, minBid));
21
- if (maxBid)
22
- parts.push(sieveFilter("currentBid", SIEVE_OP.LTE, maxBid));
23
- const store = sp(params, "store");
24
- if (store) {
25
- const values = store.split("|").filter(Boolean);
26
- if (values.length === 1)
27
- parts.push(sieveFilter("storeId", SIEVE_OP.EQ, values[0]));
28
- else if (values.length > 1)
29
- parts.push(sieveFilter("storeId", SIEVE_OP.EQ, values.join("|")));
30
- }
31
- // Mirror AuctionsIndexListing's client-side default (Root Cause pattern —
32
- // SSR initialData is seeded into React Query with staleTime:Infinity, so if
33
- // this SSR filter doesn't already exclude ended auctions, the client never
34
- // refetches and ended auctions leak into the default "Show ended" off view).
35
- const showEnded = sp(params, "showEnded") === "true";
36
- const dateFrom = showEnded
37
- ? sp(params, "dateFrom")
38
- : sp(params, "dateFrom") || new Date().toISOString();
39
- const dateTo = sp(params, "dateTo");
40
- if (dateFrom)
41
- parts.push(sieveFilter("auctionEndDate", SIEVE_OP.GTE, dateFrom));
42
- if (dateTo)
43
- parts.push(sieveFilter("auctionEndDate", SIEVE_OP.LTE, dateTo));
44
- return parts.join(",");
45
- }
46
9
  export async function AuctionsListView({ searchParams = {} }) {
47
- const std = parseListingSearchParams(searchParams);
48
- const sort = std.sorts ?? DEFAULT_SORT;
49
- const page = std.page ?? DEFAULT_PAGE;
50
- const pageSize = std.pageSize ?? DEFAULT_PAGE_SIZE;
51
- const filters = buildAuctionFilters(searchParams);
52
- const result = await productRepository
53
- .list({
54
- filters,
55
- sorts: sort,
56
- page,
57
- pageSize,
58
- })
59
- .catch(() => null);
60
- const initial = result ?? null;
10
+ // Shared with /api/products. `hideEndedByDefault` mirrors
11
+ // AuctionsIndexListing's `showEnded ? … : dateFrom=now` default; because the
12
+ // default sort IS auctionEndDate, listPublicProducts pushes that range into
13
+ // Firestore rather than filtering in memory.
14
+ const initial = await listPublicProducts(parsePublicProductParams(searchParams, {
15
+ listingTypes: AUCTION_LISTING_TYPES,
16
+ pageSize: DEFAULT_PAGE_SIZE,
17
+ sorts: DEFAULT_SORT,
18
+ hideEndedByDefault: true,
19
+ }));
61
20
  return (_jsx(Main, { children: _jsx(Section, { padding: "y-2xl", children: _jsxs(Container, { size: "xl", children: [_jsx(Heading, { level: 1, className: "mb-8", color: "primary", size: "3xl", weight: "semibold", children: "Live Auctions" }), _jsx(AdSlot, { id: "listing-sidebar-top", className: "mb-6" }), _jsx(AuctionsIndexListing, { initialData: initial }), _jsx(AdSlot, { id: "listing-sidebar-bottom", className: "mt-8" })] }) }) }));
62
21
  }
@@ -96,7 +96,16 @@ export function PlaceBidFormClient({ productId, currentBid: ssrCurrentBid, start
96
96
  return;
97
97
  }
98
98
  setSuccess(true);
99
- router.refresh();
99
+ // Buy-Now writes a locked cart line, so send the buyer straight to the
100
+ // auction checkout lane to pay for it. This used to just call
101
+ // `router.refresh()` and discard the action's result entirely, leaving
102
+ // the buyer on the product page with no indication that anything was
103
+ // owed — the action returned an orderId nobody ever used.
104
+ const checkoutUrl = result?.checkoutUrl;
105
+ if (checkoutUrl)
106
+ router.push(checkoutUrl);
107
+ else
108
+ router.refresh();
100
109
  }
101
110
  catch (err) {
102
111
  void normalizeError(err);
@@ -50,6 +50,21 @@ export declare class BidRepository extends BaseRepository<BidDocument> {
50
50
  data: BidDocument;
51
51
  } | null>;
52
52
  markWon(batch: WriteBatch, ref: DocumentReference): void;
53
+ /**
54
+ * Link a won bid to the order it eventually became.
55
+ *
56
+ * `BidDocument` had no `orderId` at all before 2026-08-21, so a won bid and
57
+ * the thing the buyer paid for were two unrelated records — the "Won" row in
58
+ * /user/bids could not link anywhere, and nothing could tell a settled win
59
+ * from an unpaid one.
60
+ */
61
+ attachOrder(bidId: string, orderId: string): Promise<void>;
62
+ /**
63
+ * The winner never paid within their checkout window. Distinct from "lost"
64
+ * (outbid) — this bidder won and then defaulted, which a seller may want to
65
+ * act on.
66
+ */
67
+ markForfeited(bidId: string): Promise<void>;
53
68
  markLost(batch: WriteBatch, ref: DocumentReference): void;
54
69
  markOutbid(batch: WriteBatch, ref: DocumentReference): void;
55
70
  markWinning(batch: WriteBatch, ref: DocumentReference): void;