@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.
Files changed (68) 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/data.d.ts +2 -2
  5. package/dist/_internal/server/features/products/data.js +5 -23
  6. package/dist/_internal/server/features/products/index.d.ts +1 -0
  7. package/dist/_internal/server/features/products/index.js +1 -0
  8. package/dist/_internal/server/features/products/list-public.d.ts +111 -0
  9. package/dist/_internal/server/features/products/list-public.js +342 -0
  10. package/dist/_internal/server/jobs/core/auctionSettlement.js +89 -16
  11. package/dist/_internal/server/jobs/core/offerExpiry.js +116 -4
  12. package/dist/_internal/server/jobs/handlers/messages.d.ts +5 -0
  13. package/dist/_internal/server/jobs/handlers/messages.js +5 -0
  14. package/dist/_internal/shared/checkout/lanes.d.ts +41 -0
  15. package/dist/_internal/shared/checkout/lanes.js +106 -0
  16. package/dist/_internal/shared/checkout/order-math.d.ts +19 -0
  17. package/dist/_internal/shared/checkout/order-math.js +30 -6
  18. package/dist/_internal/shared/features/products/to-product-item.d.ts +25 -0
  19. package/dist/_internal/shared/features/products/to-product-item.js +45 -0
  20. package/dist/_internal/shared/listing-types/feature-flags.d.ts +1 -0
  21. package/dist/_internal/shared/listing-types/feature-flags.js +24 -10
  22. package/dist/client.d.ts +2 -1
  23. package/dist/client.js +4 -1
  24. package/dist/features/account/components/NotificationBell.js +1 -0
  25. package/dist/features/account/components/UserOffersPanel.js +8 -1
  26. package/dist/features/admin/actions/notification-actions.js +1 -1
  27. package/dist/features/admin/schemas/firestore.d.ts +2 -1
  28. package/dist/features/admin/schemas/firestore.js +1 -0
  29. package/dist/features/auctions/actions/bid-actions.d.ts +11 -1
  30. package/dist/features/auctions/actions/bid-actions.js +29 -15
  31. package/dist/features/auctions/components/AuctionBidsTable.js +6 -2
  32. package/dist/features/auctions/components/AuctionsListView.js +12 -53
  33. package/dist/features/auctions/components/PlaceBidFormClient.js +10 -1
  34. package/dist/features/auctions/repository/bid.repository.d.ts +15 -0
  35. package/dist/features/auctions/repository/bid.repository.js +19 -0
  36. package/dist/features/auctions/schemas/firestore.d.ts +11 -1
  37. package/dist/features/auctions/schemas/firestore.js +1 -0
  38. package/dist/features/cart/actions/cart-actions.d.ts +11 -0
  39. package/dist/features/cart/actions/cart-actions.js +24 -0
  40. package/dist/features/cart/repository/cart.repository.d.ts +24 -1
  41. package/dist/features/cart/repository/cart.repository.js +71 -6
  42. package/dist/features/cart/schemas/firestore.d.ts +23 -2
  43. package/dist/features/contact/email.js +52 -1
  44. package/dist/features/grouped/components/GroupedListingsCarousel.js +4 -1
  45. package/dist/features/orders/repository/orders.repository.d.ts +0 -15
  46. package/dist/features/orders/repository/orders.repository.js +0 -27
  47. package/dist/features/orders/utils/order-splitter.js +14 -2
  48. package/dist/features/pre-orders/components/PreOrdersListView.js +11 -39
  49. package/dist/features/products/components/ArtStickersListView.js +10 -49
  50. package/dist/features/products/components/ProductsIndexPageView.js +9 -62
  51. package/dist/features/products/repository/products.repository.js +41 -5
  52. package/dist/features/seller/actions/offer-actions.d.ts +6 -0
  53. package/dist/features/seller/actions/offer-actions.js +8 -9
  54. package/dist/features/seller/components/SellerOffersView.d.ts +6 -1
  55. package/dist/features/seller/components/SellerOffersView.js +59 -8
  56. package/dist/features/seller/repository/offer.repository.d.ts +18 -2
  57. package/dist/features/seller/repository/offer.repository.js +38 -2
  58. package/dist/features/seller/schemas/firestore.d.ts +7 -2
  59. package/dist/features/seller/schemas/firestore.js +3 -0
  60. package/dist/features/seller/schemas/offer-forms.d.ts +12 -0
  61. package/dist/features/seller/schemas/offer-forms.js +28 -0
  62. package/dist/index.d.ts +2 -1
  63. package/dist/index.js +4 -1
  64. package/dist/server-entry.d.ts +1 -0
  65. package/dist/server-entry.js +1 -0
  66. package/dist/server.d.ts +1 -0
  67. package/dist/server.js +3 -0
  68. 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`
@@ -0,0 +1,25 @@
1
+ /**
2
+ * toProductItem — Firestore doc → card-grid `ProductItem` mapper.
3
+ *
4
+ * Lives in `_internal/shared/` (client+server safe), NOT in
5
+ * `_internal/server/features/products/data.ts` where it was originally
6
+ * written. It is a pure field mapping with no server dependencies of its
7
+ * own, but `data.ts` transitively reaches `providers/db-firebase/admin.js`
8
+ * (firebase-admin), and `GroupedListingsCarousel.tsx` is a `"use client"`
9
+ * component that imports this function as a VALUE. Turbopack resolves a
10
+ * module's full static import graph before tree-shaking, so that single
11
+ * import dragged the whole firebase-admin chain into the client bundle and
12
+ * failed the production build outright:
13
+ *
14
+ * Module not found: .../providers/db-firebase/admin.js
15
+ * [Client Component Browser] → GroupedListingsCarousel
16
+ * → _internal/server/features/products/data.js
17
+ *
18
+ * This is the Turbopack client-bundle trap from CLAUDE.md's appkit Export
19
+ * Rules / Recurrent Root Cause #6, reached through an ordinary deep import
20
+ * rather than a barrel. Keep this file free of any server-only import so a
21
+ * client component can always call it directly.
22
+ */
23
+ import type { FirestoreDocument } from "../../../../schemas/types";
24
+ import type { ProductItem } from "../../../../features/products/types";
25
+ export declare function toProductItem(doc: FirestoreDocument): ProductItem;
@@ -0,0 +1,45 @@
1
+ /**
2
+ * toProductItem — Firestore doc → card-grid `ProductItem` mapper.
3
+ *
4
+ * Lives in `_internal/shared/` (client+server safe), NOT in
5
+ * `_internal/server/features/products/data.ts` where it was originally
6
+ * written. It is a pure field mapping with no server dependencies of its
7
+ * own, but `data.ts` transitively reaches `providers/db-firebase/admin.js`
8
+ * (firebase-admin), and `GroupedListingsCarousel.tsx` is a `"use client"`
9
+ * component that imports this function as a VALUE. Turbopack resolves a
10
+ * module's full static import graph before tree-shaking, so that single
11
+ * import dragged the whole firebase-admin chain into the client bundle and
12
+ * failed the production build outright:
13
+ *
14
+ * Module not found: .../providers/db-firebase/admin.js
15
+ * [Client Component Browser] → GroupedListingsCarousel
16
+ * → _internal/server/features/products/data.js
17
+ *
18
+ * This is the Turbopack client-bundle trap from CLAUDE.md's appkit Export
19
+ * Rules / Recurrent Root Cause #6, reached through an ordinary deep import
20
+ * rather than a barrel. Keep this file free of any server-only import so a
21
+ * client component can always call it directly.
22
+ */
23
+ export function toProductItem(doc) {
24
+ return {
25
+ id: String(doc.id ?? ""),
26
+ title: String(doc.title ?? doc.name ?? ""),
27
+ price: typeof doc.price === "number" ? doc.price : 0,
28
+ originalPrice: typeof doc.originalPrice === "number" ? doc.originalPrice : undefined,
29
+ mainImage: Array.isArray(doc.images)
30
+ ? doc.images[0]
31
+ : typeof doc.mainImage === "string"
32
+ ? doc.mainImage
33
+ : undefined,
34
+ status: doc.status ?? "published",
35
+ slug: typeof doc.slug === "string" ? doc.slug : undefined,
36
+ storeName: typeof doc.storeName === "string" ? doc.storeName : undefined,
37
+ rating: typeof doc.rating === "number" ? doc.rating : undefined,
38
+ reviewCount: typeof doc.reviewCount === "number" ? doc.reviewCount : undefined,
39
+ // Without this, every related-item card silently linked to the standard
40
+ // PDP regardless of its real type (pluginFor() defaults to "standard"
41
+ // when listingType is missing) — same bug class as the Phase 1 routing
42
+ // fix, one hop further downstream in the related-items card link path.
43
+ listingType: doc.listingType ?? undefined,
44
+ };
45
+ }
@@ -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]);