@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.
- package/dist/_internal/server/features/checkout/actions.js +27 -23
- package/dist/_internal/server/features/checkout/locked-lines.d.ts +30 -0
- package/dist/_internal/server/features/checkout/locked-lines.js +116 -0
- package/dist/_internal/server/features/products/index.d.ts +1 -0
- package/dist/_internal/server/features/products/index.js +1 -0
- package/dist/_internal/server/features/products/list-public.d.ts +111 -0
- package/dist/_internal/server/features/products/list-public.js +342 -0
- package/dist/_internal/server/jobs/core/auctionSettlement.js +89 -16
- package/dist/_internal/server/jobs/core/offerExpiry.js +116 -4
- package/dist/_internal/server/jobs/handlers/messages.d.ts +5 -0
- package/dist/_internal/server/jobs/handlers/messages.js +5 -0
- package/dist/_internal/shared/checkout/lanes.d.ts +41 -0
- package/dist/_internal/shared/checkout/lanes.js +106 -0
- package/dist/_internal/shared/checkout/order-math.d.ts +19 -0
- package/dist/_internal/shared/checkout/order-math.js +30 -6
- package/dist/_internal/shared/listing-types/feature-flags.d.ts +1 -0
- package/dist/_internal/shared/listing-types/feature-flags.js +24 -10
- package/dist/client.d.ts +2 -1
- package/dist/client.js +4 -1
- package/dist/features/account/components/NotificationBell.js +1 -0
- package/dist/features/account/components/UserOffersPanel.js +8 -1
- package/dist/features/admin/actions/notification-actions.js +1 -1
- package/dist/features/admin/schemas/firestore.d.ts +2 -1
- package/dist/features/admin/schemas/firestore.js +1 -0
- package/dist/features/auctions/actions/bid-actions.d.ts +11 -1
- package/dist/features/auctions/actions/bid-actions.js +29 -15
- package/dist/features/auctions/components/AuctionBidsTable.js +6 -2
- package/dist/features/auctions/components/AuctionsListView.js +12 -53
- package/dist/features/auctions/components/PlaceBidFormClient.js +10 -1
- package/dist/features/auctions/repository/bid.repository.d.ts +15 -0
- package/dist/features/auctions/repository/bid.repository.js +19 -0
- package/dist/features/auctions/schemas/firestore.d.ts +11 -1
- package/dist/features/auctions/schemas/firestore.js +1 -0
- package/dist/features/cart/actions/cart-actions.d.ts +11 -0
- package/dist/features/cart/actions/cart-actions.js +24 -0
- package/dist/features/cart/repository/cart.repository.d.ts +24 -1
- package/dist/features/cart/repository/cart.repository.js +71 -6
- package/dist/features/cart/schemas/firestore.d.ts +23 -2
- package/dist/features/contact/email.js +52 -1
- package/dist/features/orders/repository/orders.repository.d.ts +0 -15
- package/dist/features/orders/repository/orders.repository.js +0 -27
- package/dist/features/orders/utils/order-splitter.js +14 -2
- package/dist/features/pre-orders/components/PreOrdersListView.js +11 -39
- package/dist/features/products/components/ArtStickersListView.js +10 -49
- package/dist/features/products/components/ProductsIndexPageView.js +9 -62
- package/dist/features/products/repository/products.repository.js +41 -5
- package/dist/features/seller/actions/offer-actions.d.ts +6 -0
- package/dist/features/seller/actions/offer-actions.js +8 -9
- package/dist/features/seller/components/SellerOffersView.d.ts +6 -1
- package/dist/features/seller/components/SellerOffersView.js +59 -8
- package/dist/features/seller/repository/offer.repository.d.ts +18 -2
- package/dist/features/seller/repository/offer.repository.js +38 -2
- package/dist/features/seller/schemas/firestore.d.ts +7 -2
- package/dist/features/seller/schemas/firestore.js +3 -0
- package/dist/features/seller/schemas/offer-forms.d.ts +12 -0
- package/dist/features/seller/schemas/offer-forms.js +28 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +4 -1
- package/dist/server-entry.d.ts +1 -0
- package/dist/server-entry.js +1 -0
- package/dist/server.d.ts +1 -0
- package/dist/server.js +3 -0
- package/package.json +1 -1
|
@@ -1,75 +1,22 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { productRepository } from "../../../repositories";
|
|
3
2
|
import { ROUTES } from "../../../constants";
|
|
4
3
|
import { Container, Div, Heading, Main, Section, Text, TextLink } from "../../../ui";
|
|
5
4
|
import { AdSlot } from "../../homepage/components/AdSlot";
|
|
6
|
-
import { parseListingSearchParams } from "../../../utils/listing-params";
|
|
7
5
|
import { ProductsIndexListing } from "./ProductsIndexListing";
|
|
8
6
|
import { PRODUCT_FIELDS } from "../../../constants/field-names";
|
|
9
|
-
import { sieveFilter, sieveMultiEq, sieveAnd, SIEVE_OP } from "../../../utils/sieve-builder";
|
|
10
7
|
import { sortBy } from "../../../constants/sort";
|
|
11
8
|
import { GENERIC_PRODUCT_LISTING_TYPES } from "../constants/listing-tabs";
|
|
12
|
-
|
|
9
|
+
import { listPublicProducts, parsePublicProductParams, } from "../../../_internal/server/features/products/list-public";
|
|
13
10
|
const DEFAULT_PAGE_SIZE = 24;
|
|
14
11
|
const DEFAULT_SORT = sortBy(PRODUCT_FIELDS.CREATED_AT);
|
|
15
|
-
function sp(params, key) {
|
|
16
|
-
const v = params[key];
|
|
17
|
-
return Array.isArray(v) ? v[0] ?? "" : v ?? "";
|
|
18
|
-
}
|
|
19
|
-
function buildProductFilters(params) {
|
|
20
|
-
const listingTypeParam = sp(params, "listingType");
|
|
21
|
-
const listingType = listingTypeParam && GENERIC_PRODUCT_LISTING_TYPES.includes(listingTypeParam)
|
|
22
|
-
? listingTypeParam
|
|
23
|
-
: GENERIC_PRODUCT_LISTING_TYPES.join("|");
|
|
24
|
-
const parts = [
|
|
25
|
-
sieveFilter(PRODUCT_FIELDS.STATUS, SIEVE_OP.EQ, PRODUCT_FIELDS.STATUS_VALUES.PUBLISHED),
|
|
26
|
-
sieveFilter(PRODUCT_FIELDS.LISTING_TYPE, SIEVE_OP.EQ, listingType),
|
|
27
|
-
];
|
|
28
|
-
const condition = sp(params, "condition");
|
|
29
|
-
if (condition) {
|
|
30
|
-
const values = condition.split("|").filter(Boolean);
|
|
31
|
-
if (values.length === 1)
|
|
32
|
-
parts.push(sieveFilter(PRODUCT_FIELDS.CONDITION, SIEVE_OP.EQ, values[0]));
|
|
33
|
-
// BUG FIX: pipe is invalid for ==; expand to multiple AND clauses
|
|
34
|
-
else if (values.length > 1)
|
|
35
|
-
parts.push(sieveMultiEq(PRODUCT_FIELDS.CONDITION, values));
|
|
36
|
-
}
|
|
37
|
-
const minPrice = sp(params, "minPrice");
|
|
38
|
-
const maxPrice = sp(params, "maxPrice");
|
|
39
|
-
if (minPrice)
|
|
40
|
-
parts.push(sieveFilter(PRODUCT_FIELDS.PRICE, SIEVE_OP.GTE, minPrice));
|
|
41
|
-
if (maxPrice)
|
|
42
|
-
parts.push(sieveFilter(PRODUCT_FIELDS.PRICE, SIEVE_OP.LTE, maxPrice));
|
|
43
|
-
const storeId = sp(params, "seller");
|
|
44
|
-
if (storeId)
|
|
45
|
-
parts.push(sieveFilter(PRODUCT_FIELDS.STORE_ID, SIEVE_OP.EQ, storeId));
|
|
46
|
-
const freeShipping = sp(params, "freeShipping");
|
|
47
|
-
if (freeShipping === "true") {
|
|
48
|
-
parts.push(sieveFilter(PRODUCT_FIELDS.SHIPPING_PAID_BY, SIEVE_OP.EQ, PRODUCT_FIELDS.SHIPPING_PAID_BY_VALUES.SELLER));
|
|
49
|
-
}
|
|
50
|
-
// Mirror ProductsIndexListing's client-side "Show sold" default (same Root Cause
|
|
51
|
-
// pattern as auctions' dateFrom default — SSR initialData is seeded into React
|
|
52
|
-
// Query with staleTime:Infinity, so if this filter doesn't already exclude sold /
|
|
53
|
-
// out-of-stock products, the client never refetches and they leak into the default view).
|
|
54
|
-
const showSold = sp(params, "showSold") === "true";
|
|
55
|
-
if (!showSold)
|
|
56
|
-
parts.push(sieveFilter(PRODUCT_FIELDS.STOCK_QUANTITY, SIEVE_OP.GT, 0));
|
|
57
|
-
return sieveAnd(...parts);
|
|
58
|
-
}
|
|
59
12
|
export async function ProductsIndexPageView({ searchParams = {} }) {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
sorts: sort,
|
|
69
|
-
page,
|
|
70
|
-
pageSize,
|
|
71
|
-
})
|
|
72
|
-
.catch(() => null);
|
|
73
|
-
const products = result ?? null;
|
|
13
|
+
// See ArtStickersListView — one shared filter implementation with
|
|
14
|
+
// /api/products, so SSR and the client refetch agree by construction.
|
|
15
|
+
const products = await listPublicProducts(parsePublicProductParams(searchParams, {
|
|
16
|
+
listingTypes: GENERIC_PRODUCT_LISTING_TYPES,
|
|
17
|
+
pageSize: DEFAULT_PAGE_SIZE,
|
|
18
|
+
sorts: DEFAULT_SORT,
|
|
19
|
+
hideSoldByDefault: true,
|
|
20
|
+
}));
|
|
74
21
|
return (_jsxs(Main, { children: [_jsx(Section, { border: "bottom-subtle", paddingY: "b-md", padding: "t-xl", children: _jsxs(Container, { size: "xl", children: [_jsx(Heading, { level: 1, size: "3xl", weight: "bold", color: "primary", children: "Products" }), _jsx(Text, { className: "mt-1", color: "muted", size: "sm", children: "Discover amazing products and deals" }), _jsx(Div, { className: "mt-3", children: _jsx(TextLink, { variant: "bare", href: String(ROUTES.PUBLIC.AUCTIONS), rounded: "full", paddingX: "sm", paddingY: "2xs", size: "xs", weight: "medium", layout: "inline-flex", align: "center", gap: "sm", className: "border border-primary/30 bg-primary/10 text-primary-700 dark:text-primary-400 hover:bg-primary/15 transition-colors", children: "\uD83C\uDFF7\uFE0F Looking for unique deals? Browse Auctions \u2192" }) })] }) }), _jsxs(Container, { size: "xl", padding: "x-md", children: [_jsx(AdSlot, { id: "listing-sidebar-top", className: "mb-4 mt-4" }), _jsx(ProductsIndexListing, { initialData: products }), _jsx(AdSlot, { id: "listing-sidebar-bottom", className: "mt-8" })] })] }));
|
|
75
22
|
}
|
|
@@ -3,6 +3,7 @@ import { increment, serverTimestamp } from "../../../contracts/field-ops";
|
|
|
3
3
|
import { DatabaseError } from "../../../errors";
|
|
4
4
|
import { BaseRepository, prepareForFirestore, parseSieveDateValue, } from "../../../providers/db-firebase";
|
|
5
5
|
import { cacheManager } from "../../../core";
|
|
6
|
+
import { serverLogger } from "../../../monitoring";
|
|
6
7
|
import { generateUniqueId, slugify, buildSearchTokens, tokenizeQuery, generateBarcodeId } from "../../../utils";
|
|
7
8
|
import { PRODUCT_COLLECTION, ProductStatusValues } from "../schemas";
|
|
8
9
|
import { PRODUCT_FIELDS } from "../../../constants/field-names";
|
|
@@ -31,6 +32,8 @@ const LISTING_KIND_ALIAS_MAP = {
|
|
|
31
32
|
classified: LISTING_TYPE_VALUES.CLASSIFIED,
|
|
32
33
|
"digital-code": LISTING_TYPE_VALUES.DIGITAL_CODE,
|
|
33
34
|
live: LISTING_TYPE_VALUES.LIVE,
|
|
35
|
+
art: LISTING_TYPE_VALUES.ART,
|
|
36
|
+
stickers: LISTING_TYPE_VALUES.STICKERS,
|
|
34
37
|
// Legacy → canonical.
|
|
35
38
|
product: LISTING_TYPE_VALUES.STANDARD,
|
|
36
39
|
preorder: LISTING_TYPE_VALUES.PRE_ORDER,
|
|
@@ -559,12 +562,45 @@ ProductRepository.FILTER_ALIASES = {
|
|
|
559
562
|
if (operator !== "==" && operator !== "!=")
|
|
560
563
|
return "";
|
|
561
564
|
// Accept both canonical tokens (`standard`, `auction`, `pre-order`,
|
|
562
|
-
// `prize-draw`) and legacy aliases (`product`,
|
|
563
|
-
// `LISTING_KIND_ACCEPTED` is the single source
|
|
564
|
-
// sync with `LISTING_KIND_ALIAS_MAP`.
|
|
565
|
-
|
|
565
|
+
// `prize-draw`, `art`, `stickers`, …) and legacy aliases (`product`,
|
|
566
|
+
// `preorder`, `prizedraw`). `LISTING_KIND_ACCEPTED` is the single source
|
|
567
|
+
// of truth — keep it in sync with `LISTING_KIND_ALIAS_MAP`.
|
|
568
|
+
//
|
|
569
|
+
// A pipe-joined value (`art|stickers`, `standard|classified|live`) is a
|
|
570
|
+
// sievejs OR-group that the Firebase adapter upgrades to a `.where(…,
|
|
571
|
+
// "in", …)` query — the combined browse pages (/products, /art) send
|
|
572
|
+
// exactly this. Before 2026-08-21 the whole string was tested against
|
|
573
|
+
// `LISTING_KIND_ACCEPTED` as one token, never matched, and the clause was
|
|
574
|
+
// dropped silently, so those pages queried with NO listing-type filter at
|
|
575
|
+
// all. Split first, map each part, then re-emit the OR-group.
|
|
576
|
+
const parts = value.split("|").filter(Boolean);
|
|
577
|
+
if (parts.length === 0)
|
|
566
578
|
return "";
|
|
567
|
-
|
|
579
|
+
const unknown = parts.filter((p) => !LISTING_KIND_ACCEPTED.has(p));
|
|
580
|
+
if (unknown.length > 0) {
|
|
581
|
+
// Loud, not silent — a dropped filter reads as "no results" downstream
|
|
582
|
+
// with nothing to trace it back to (Root Cause: the art/stickers bug).
|
|
583
|
+
serverLogger.warn("Unknown listingType filter value(s) — clause dropped", {
|
|
584
|
+
value,
|
|
585
|
+
unknown,
|
|
586
|
+
accepted: [...LISTING_KIND_ACCEPTED],
|
|
587
|
+
});
|
|
588
|
+
return "";
|
|
589
|
+
}
|
|
590
|
+
if (operator === "!=") {
|
|
591
|
+
// Firestore allows at most one `!=` per query, so a multi-value
|
|
592
|
+
// exclusion can't be expressed here — reject rather than half-apply it.
|
|
593
|
+
if (parts.length > 1) {
|
|
594
|
+
serverLogger.warn("Multi-value listingType != is not supported — clause dropped", { value });
|
|
595
|
+
return "";
|
|
596
|
+
}
|
|
597
|
+
return buildListingKindClause(parts[0], true);
|
|
598
|
+
}
|
|
599
|
+
const canonical = parts.map((p) => LISTING_KIND_ALIAS_MAP[p]);
|
|
600
|
+
// De-dupe: legacy aliases can collapse onto the same canonical token
|
|
601
|
+
// (e.g. `product|standard`), and Firestore rejects a duplicate `in` value.
|
|
602
|
+
const unique = [...new Set(canonical)];
|
|
603
|
+
return `${PRODUCT_FIELDS.LISTING_TYPE}==${unique.join("|")}`;
|
|
568
604
|
},
|
|
569
605
|
/**
|
|
570
606
|
* Public catalog scope shorthand. Maps a single token to the canonical
|
|
@@ -5,6 +5,12 @@
|
|
|
5
5
|
* Auth, rate-limiting, and input validation are handled by the calling server action.
|
|
6
6
|
*/
|
|
7
7
|
import type { OfferDocument } from "../schemas";
|
|
8
|
+
/**
|
|
9
|
+
* How long an accepted offer stays claimable at its locked price. Shared by the
|
|
10
|
+
* seller-accept path, the buyer-accepts-counter path, and the expiry sweep so
|
|
11
|
+
* all three can't drift.
|
|
12
|
+
*/
|
|
13
|
+
export declare const OFFER_CHECKOUT_WINDOW_MS: number;
|
|
8
14
|
import type { CartDocument } from "../../cart/schemas/firestore";
|
|
9
15
|
export interface MakeOfferInput {
|
|
10
16
|
productId: string;
|
|
@@ -15,6 +15,12 @@ import { cartRepository } from "../../cart/repository/cart.repository";
|
|
|
15
15
|
import { maskOfferForSeller } from "../../../security";
|
|
16
16
|
import { ERROR_MESSAGES, AuthorizationError, ValidationError, NotFoundError, OFFER_ERROR_CODES, } from "../../../errors";
|
|
17
17
|
import { OfferStatusValues } from "../schemas";
|
|
18
|
+
/**
|
|
19
|
+
* How long an accepted offer stays claimable at its locked price. Shared by the
|
|
20
|
+
* seller-accept path, the buyer-accepts-counter path, and the expiry sweep so
|
|
21
|
+
* all three can't drift.
|
|
22
|
+
*/
|
|
23
|
+
export const OFFER_CHECKOUT_WINDOW_MS = 48 * 60 * 60 * 1000;
|
|
18
24
|
const ERR_OFFER_NOT_FOUND = "Offer not found";
|
|
19
25
|
const ERR_NOT_AUTHORISED = "Not authorized";
|
|
20
26
|
// --- Buyer: Make an Offer --------------------------------------------------
|
|
@@ -89,12 +95,8 @@ export async function respondToOffer(userId, input) {
|
|
|
89
95
|
if (new Date() > offer.expiresAt)
|
|
90
96
|
throw new ValidationError(ERROR_MESSAGES.OFFER.EXPIRED);
|
|
91
97
|
let updated;
|
|
92
|
-
const CHECKOUT_WINDOW_MS = 48 * 60 * 60 * 1000;
|
|
93
98
|
if (action === "accept") {
|
|
94
|
-
updated = await offerRepository.accept(offerId, offer.offerAmount, sellerNote);
|
|
95
|
-
await offerRepository.update(offerId, {
|
|
96
|
-
checkoutDeadline: new Date(Date.now() + CHECKOUT_WINDOW_MS),
|
|
97
|
-
});
|
|
99
|
+
updated = await offerRepository.accept(offerId, offer.offerAmount, sellerNote, new Date(Date.now() + OFFER_CHECKOUT_WINDOW_MS));
|
|
98
100
|
}
|
|
99
101
|
else if (action === "decline") {
|
|
100
102
|
updated = await offerRepository.decline(offerId, sellerNote);
|
|
@@ -144,10 +146,7 @@ export async function acceptCounterOffer(userId, offerId) {
|
|
|
144
146
|
throw new ValidationError("No counter to accept.");
|
|
145
147
|
if (new Date() > offer.expiresAt)
|
|
146
148
|
throw new ValidationError(ERROR_MESSAGES.OFFER.EXPIRED);
|
|
147
|
-
const updated = await offerRepository.acceptCounter(offerId);
|
|
148
|
-
await offerRepository.update(offerId, {
|
|
149
|
-
checkoutDeadline: new Date(Date.now() + 48 * 60 * 60 * 1000),
|
|
150
|
-
});
|
|
149
|
+
const updated = await offerRepository.acceptCounter(offerId, new Date(Date.now() + OFFER_CHECKOUT_WINDOW_MS));
|
|
151
150
|
const counterStore = offer.storeId ? await storeRepository.findById(offer.storeId) : null;
|
|
152
151
|
if (counterStore?.ownerId)
|
|
153
152
|
await sendNotification({
|
|
@@ -3,6 +3,11 @@ import type { ListingLayoutProps } from "../../../ui";
|
|
|
3
3
|
export interface SellerOffersViewProps extends ListingLayoutProps {
|
|
4
4
|
onAcceptOffer?: (id: string) => Promise<void>;
|
|
5
5
|
onRejectOffer?: (id: string) => Promise<void>;
|
|
6
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Countering needs an amount, so this view hosts the form (a QuickFormDrawer,
|
|
8
|
+
* per Rule #9) and hands the consumer the collected values — rather than the
|
|
9
|
+
* old `(id) => void` shape, which left every consumer to invent its own input.
|
|
10
|
+
*/
|
|
11
|
+
onCounterOffer?: (id: string, counterAmount: number, sellerNote?: string) => Promise<void>;
|
|
7
12
|
}
|
|
8
13
|
export declare function SellerOffersView({ children, onAcceptOffer, onRejectOffer, onCounterOffer, ...props }: SellerOffersViewProps): React.JSX.Element;
|
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { sieveFilter, SIEVE_OP } from "@mohasinac/appkit/client";
|
|
4
4
|
import { sortBy } from "@mohasinac/appkit/client";
|
|
5
5
|
import React from "react";
|
|
6
|
-
import { FilterChipGroup, ListingLayout, RowActionMenu } from "../../../ui";
|
|
6
|
+
import { FilterChipGroup, ListingLayout, RowActionMenu, RecordDetailModal } from "../../../ui";
|
|
7
|
+
import { QuickFormDrawer } from "../../shell/QuickFormDrawer";
|
|
8
|
+
import { counterOfferFormSchema } from "../schemas/offer-forms";
|
|
7
9
|
import { SELLER_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
8
10
|
import { SELLER_OFFER_STATUS_TABS } from "../../admin/constants/filter-tabs";
|
|
9
11
|
import { ACTIONS } from "../../../_internal/shared/actions/action-registry";
|
|
10
12
|
import { toRecordArray, toRelativeDate, toCurrency, toStringValue, } from "../../admin/hooks/useAdminListingData";
|
|
11
13
|
import { DataListingView } from "../../admin/components/DataListingView";
|
|
12
14
|
export function SellerOffersView({ children, onAcceptOffer, onRejectOffer, onCounterOffer, ...props }) {
|
|
15
|
+
const [detailRow, setDetailRow] = React.useState(null);
|
|
16
|
+
const [counterRow, setCounterRow] = React.useState(null);
|
|
17
|
+
const [isCountering, setIsCountering] = React.useState(false);
|
|
13
18
|
if (React.Children.count(children) > 0) {
|
|
14
19
|
return (_jsx(ListingLayout, { portal: "seller", ...props, children: children }));
|
|
15
20
|
}
|
|
@@ -26,26 +31,31 @@ export function SellerOffersView({ children, onAcceptOffer, onRejectOffer, onCou
|
|
|
26
31
|
{ value: sortBy("createdAt", "DESC"), label: "Newest" },
|
|
27
32
|
{ value: sortBy("createdAt", "ASC"), label: "Oldest" },
|
|
28
33
|
],
|
|
29
|
-
mapRows: (response) => toRecordArray(response.
|
|
34
|
+
mapRows: (response) => toRecordArray(response.items).map((item, index) => ({
|
|
30
35
|
id: toStringValue(item.id, `offer-${index}`),
|
|
31
36
|
primary: toStringValue(item.productTitle ?? item.title, "Untitled product"),
|
|
32
37
|
secondary: [
|
|
33
38
|
`Offer: ${toCurrency(item.offerAmount ?? item.amount)}`,
|
|
39
|
+
`Listed: ${toCurrency(item.listedPrice)}`,
|
|
34
40
|
toStringValue(item.buyerName ?? "Unknown buyer", "Unknown buyer"),
|
|
35
41
|
].join(" · "),
|
|
36
42
|
status: toStringValue(item.status, "Pending"),
|
|
37
43
|
updatedAt: toRelativeDate(item.updatedAt ?? item.createdAt),
|
|
44
|
+
detail: item,
|
|
38
45
|
})),
|
|
39
|
-
getTotal: (response, mappedRows) => typeof response.
|
|
40
|
-
|
|
41
|
-
|
|
46
|
+
getTotal: (response, mappedRows) => typeof response.total === "number" ? response.total : mappedRows.length,
|
|
47
|
+
// A menu of pure mutations is not a detail affordance — the seller must be
|
|
48
|
+
// able to READ the offer (buyer note, listed vs offered price, expiry)
|
|
49
|
+
// before accepting or declining it (Root Cause #56).
|
|
50
|
+
onRowClick: (row) => setDetailRow(row),
|
|
42
51
|
buildFilters: (state) => state.status && state.status !== "All" ? sieveFilter("status", SIEVE_OP.EQ, state.status) : undefined,
|
|
43
52
|
renderRowActions: (row) => (_jsx(RowActionMenu, { actions: [
|
|
53
|
+
{ label: "View details", onClick: () => setDetailRow(row) },
|
|
44
54
|
...(onAcceptOffer
|
|
45
55
|
? [{ label: ACTIONS.STORE["accept-offer"].label, onClick: () => void onAcceptOffer(row.id) }]
|
|
46
56
|
: []),
|
|
47
57
|
...(onCounterOffer
|
|
48
|
-
? [{ label: ACTIONS.STORE["counter-offer"].label, onClick: () =>
|
|
58
|
+
? [{ label: ACTIONS.STORE["counter-offer"].label, onClick: () => setCounterRow(row) }]
|
|
49
59
|
: []),
|
|
50
60
|
...(onRejectOffer
|
|
51
61
|
? [{ label: ACTIONS.STORE["reject-offer"].label, destructive: true, onClick: () => void onRejectOffer(row.id) }]
|
|
@@ -53,5 +63,46 @@ export function SellerOffersView({ children, onAcceptOffer, onRejectOffer, onCou
|
|
|
53
63
|
] })),
|
|
54
64
|
renderFilterPanel: ({ pendingFilters, setPendingFilters }) => (_jsx(FilterChipGroup, { label: "Status", tabs: SELLER_OFFER_STATUS_TABS, value: pendingFilters.status ?? "", onChange: (id) => setPendingFilters((p) => ({ ...p, status: id })) })),
|
|
55
65
|
};
|
|
56
|
-
|
|
66
|
+
const d = detailRow?.detail ?? {};
|
|
67
|
+
return (_jsxs(_Fragment, { children: [_jsx(DataListingView, { config: config }), _jsx(RecordDetailModal, { isOpen: detailRow !== null, onClose: () => setDetailRow(null), title: detailRow?.primary ?? "Offer", badges: detailRow ? [{ label: detailRow.status }] : undefined, description: toStringValue(d.buyerNote, ""), fields: [
|
|
68
|
+
{ label: "Buyer", value: toStringValue(d.buyerName, "Unknown buyer") },
|
|
69
|
+
{ label: "Listed price", value: toCurrency(d.listedPrice) },
|
|
70
|
+
{ label: "Offered", value: toCurrency(d.offerAmount) },
|
|
71
|
+
...(d.counterAmount
|
|
72
|
+
? [{ label: "Your counter", value: toCurrency(d.counterAmount) }]
|
|
73
|
+
: []),
|
|
74
|
+
...(d.lockedPrice
|
|
75
|
+
? [{ label: "Agreed price", value: toCurrency(d.lockedPrice) }]
|
|
76
|
+
: []),
|
|
77
|
+
{ label: "Offer expires", value: toRelativeDate(d.expiresAt) },
|
|
78
|
+
...(d.checkoutDeadline
|
|
79
|
+
? [{ label: "Buyer must pay by", value: toRelativeDate(d.checkoutDeadline) }]
|
|
80
|
+
: []),
|
|
81
|
+
...(d.sellerNote ? [{ label: "Your note", value: toStringValue(d.sellerNote, "") }] : []),
|
|
82
|
+
] }), _jsx(QuickFormDrawer, { isOpen: counterRow !== null, onClose: () => setCounterRow(null), title: `Counter offer — ${counterRow?.primary ?? ""}`, submitLabel: "Send counter", isLoading: isCountering, schema: counterOfferFormSchema, fields: [
|
|
83
|
+
{
|
|
84
|
+
name: "counterAmount",
|
|
85
|
+
label: "Your counter price (₹)",
|
|
86
|
+
type: "number",
|
|
87
|
+
required: true,
|
|
88
|
+
helperText: "Must be below your listed price and different from the buyer's offer.",
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
name: "sellerNote",
|
|
92
|
+
label: "Note to buyer (optional)",
|
|
93
|
+
type: "textarea",
|
|
94
|
+
placeholder: "Best I can do is …",
|
|
95
|
+
},
|
|
96
|
+
], onSubmit: async (values) => {
|
|
97
|
+
if (!counterRow || !onCounterOffer)
|
|
98
|
+
return;
|
|
99
|
+
setIsCountering(true);
|
|
100
|
+
try {
|
|
101
|
+
await onCounterOffer(counterRow.id, Number(values.counterAmount), values.sellerNote ? String(values.sellerNote) : undefined);
|
|
102
|
+
setCounterRow(null);
|
|
103
|
+
}
|
|
104
|
+
finally {
|
|
105
|
+
setIsCountering(false);
|
|
106
|
+
}
|
|
107
|
+
} })] }));
|
|
57
108
|
}
|
|
@@ -51,15 +51,31 @@ declare class OfferRepository extends BaseRepository<OfferDocument> {
|
|
|
51
51
|
*/
|
|
52
52
|
countByBuyerAndProduct(buyerUid: string, productId: string, since: Date): Promise<number>;
|
|
53
53
|
updateStatus(offerId: string, patch: OfferUpdateInput): Promise<OfferDocument>;
|
|
54
|
-
accept(offerId: string, lockedPrice: number, sellerNote?: string): Promise<OfferDocument>;
|
|
54
|
+
accept(offerId: string, lockedPrice: number, sellerNote?: string, checkoutDeadline?: Date): Promise<OfferDocument>;
|
|
55
55
|
decline(offerId: string, sellerNote?: string): Promise<OfferDocument>;
|
|
56
56
|
counter(offerId: string, counterAmount: number, sellerNote?: string): Promise<OfferDocument>;
|
|
57
|
-
acceptCounter(offerId: string): Promise<OfferDocument>;
|
|
57
|
+
acceptCounter(offerId: string, checkoutDeadline?: Date): Promise<OfferDocument>;
|
|
58
58
|
withdraw(offerId: string): Promise<OfferDocument>;
|
|
59
|
+
/**
|
|
60
|
+
* Terminal transition, called once the buyer's order for this offer exists.
|
|
61
|
+
*
|
|
62
|
+
* `"paid"` has been in `OfferStatusValues` since the feature shipped but had
|
|
63
|
+
* NO server-side writer — the only thing that ever set it was an optimistic
|
|
64
|
+
* client-side patch in UserOffersPanel, which meant a reload showed the offer
|
|
65
|
+
* back at "accepted" and it could be added to the cart and ordered again.
|
|
66
|
+
*/
|
|
67
|
+
markPaid(offerId: string, orderId: string): Promise<OfferDocument>;
|
|
59
68
|
/**
|
|
60
69
|
* Cloud Functions compatibility: pending/countered offers already expired.
|
|
61
70
|
*/
|
|
62
71
|
findExpiredActive(now: Date): Promise<OfferDocument[]>;
|
|
72
|
+
/**
|
|
73
|
+
* Accepted offers whose 48h checkout window has lapsed. These were never
|
|
74
|
+
* swept before — `findExpiredActive` only looks at pending/countered — so an
|
|
75
|
+
* accepted-but-never-paid offer stayed "accepted" forever and its locked
|
|
76
|
+
* price remained claimable long after the seller expected it to lapse.
|
|
77
|
+
*/
|
|
78
|
+
findExpiredAccepted(now: Date): Promise<OfferDocument[]>;
|
|
63
79
|
expireMany(offerIds: string[]): Promise<void>;
|
|
64
80
|
}
|
|
65
81
|
export declare const offerRepository: OfferRepository;
|
|
@@ -134,13 +134,18 @@ class OfferRepository extends BaseRepository {
|
|
|
134
134
|
updatedAt: new Date(),
|
|
135
135
|
});
|
|
136
136
|
}
|
|
137
|
-
async accept(offerId, lockedPrice, sellerNote) {
|
|
137
|
+
async accept(offerId, lockedPrice, sellerNote, checkoutDeadline) {
|
|
138
|
+
// One write, not two. `checkoutDeadline` used to be missing from
|
|
139
|
+
// OfferUpdateInput, which forced callers to follow every accept() with a
|
|
140
|
+
// separate generic update() — leaving a window where an offer was
|
|
141
|
+
// "accepted" with no deadline at all.
|
|
138
142
|
return this.updateStatus(offerId, {
|
|
139
143
|
status: "accepted",
|
|
140
144
|
lockedPrice,
|
|
141
145
|
sellerNote,
|
|
142
146
|
acceptedAt: new Date(),
|
|
143
147
|
respondedAt: new Date(),
|
|
148
|
+
...(checkoutDeadline ? { checkoutDeadline } : {}),
|
|
144
149
|
});
|
|
145
150
|
}
|
|
146
151
|
async decline(offerId, sellerNote) {
|
|
@@ -158,7 +163,7 @@ class OfferRepository extends BaseRepository {
|
|
|
158
163
|
respondedAt: new Date(),
|
|
159
164
|
});
|
|
160
165
|
}
|
|
161
|
-
async acceptCounter(offerId) {
|
|
166
|
+
async acceptCounter(offerId, checkoutDeadline) {
|
|
162
167
|
const offer = await this.findById(offerId);
|
|
163
168
|
if (!offer || !offer.counterAmount)
|
|
164
169
|
throw new Error("Offer or counter not found");
|
|
@@ -167,6 +172,7 @@ class OfferRepository extends BaseRepository {
|
|
|
167
172
|
lockedPrice: offer.counterAmount,
|
|
168
173
|
acceptedAt: new Date(),
|
|
169
174
|
respondedAt: new Date(),
|
|
175
|
+
...(checkoutDeadline ? { checkoutDeadline } : {}),
|
|
170
176
|
});
|
|
171
177
|
}
|
|
172
178
|
async withdraw(offerId) {
|
|
@@ -175,6 +181,21 @@ class OfferRepository extends BaseRepository {
|
|
|
175
181
|
respondedAt: new Date(),
|
|
176
182
|
});
|
|
177
183
|
}
|
|
184
|
+
/**
|
|
185
|
+
* Terminal transition, called once the buyer's order for this offer exists.
|
|
186
|
+
*
|
|
187
|
+
* `"paid"` has been in `OfferStatusValues` since the feature shipped but had
|
|
188
|
+
* NO server-side writer — the only thing that ever set it was an optimistic
|
|
189
|
+
* client-side patch in UserOffersPanel, which meant a reload showed the offer
|
|
190
|
+
* back at "accepted" and it could be added to the cart and ordered again.
|
|
191
|
+
*/
|
|
192
|
+
async markPaid(offerId, orderId) {
|
|
193
|
+
return this.updateStatus(offerId, {
|
|
194
|
+
status: "paid",
|
|
195
|
+
paidOrderId: orderId,
|
|
196
|
+
paidAt: new Date(),
|
|
197
|
+
});
|
|
198
|
+
}
|
|
178
199
|
/**
|
|
179
200
|
* Cloud Functions compatibility: pending/countered offers already expired.
|
|
180
201
|
*/
|
|
@@ -187,6 +208,21 @@ class OfferRepository extends BaseRepository {
|
|
|
187
208
|
.get();
|
|
188
209
|
return snapshot.docs.map((doc) => this.mapDoc(doc));
|
|
189
210
|
}
|
|
211
|
+
/**
|
|
212
|
+
* Accepted offers whose 48h checkout window has lapsed. These were never
|
|
213
|
+
* swept before — `findExpiredActive` only looks at pending/countered — so an
|
|
214
|
+
* accepted-but-never-paid offer stayed "accepted" forever and its locked
|
|
215
|
+
* price remained claimable long after the seller expected it to lapse.
|
|
216
|
+
*/
|
|
217
|
+
async findExpiredAccepted(now) {
|
|
218
|
+
const snapshot = await this.db
|
|
219
|
+
.collection(this.collection)
|
|
220
|
+
.where(OFFER_FIELDS.STATUS, "==", "accepted")
|
|
221
|
+
.where(OFFER_FIELDS.CHECKOUT_DEADLINE, "<=", now)
|
|
222
|
+
.limit(500)
|
|
223
|
+
.get();
|
|
224
|
+
return snapshot.docs.map((doc) => this.mapDoc(doc));
|
|
225
|
+
}
|
|
190
226
|
async expireMany(offerIds) {
|
|
191
227
|
const batch = this.db.batch();
|
|
192
228
|
const now = new Date();
|
|
@@ -36,9 +36,12 @@ export interface OfferDocument extends BaseDocument {
|
|
|
36
36
|
acceptedAt?: Date;
|
|
37
37
|
checkoutDeadline?: Date;
|
|
38
38
|
respondedAt?: Date;
|
|
39
|
+
/** Set by `markPaid` once the buyer's order exists — makes "paid" auditable. */
|
|
40
|
+
paidOrderId?: string;
|
|
41
|
+
paidAt?: Date;
|
|
39
42
|
}
|
|
40
43
|
export declare const OFFER_COLLECTION: "offers";
|
|
41
|
-
export declare const OFFER_INDEXED_FIELDS: readonly ["buyerUid", "storeId", "productId", "status", "createdAt", "expiresAt"];
|
|
44
|
+
export declare const OFFER_INDEXED_FIELDS: readonly ["buyerUid", "storeId", "productId", "status", "createdAt", "expiresAt", "checkoutDeadline"];
|
|
42
45
|
export declare const OFFER_FIELDS: {
|
|
43
46
|
readonly ID: "id";
|
|
44
47
|
readonly PRODUCT_ID: "productId";
|
|
@@ -60,11 +63,13 @@ export declare const OFFER_FIELDS: {
|
|
|
60
63
|
readonly ACCEPTED_AT: "acceptedAt";
|
|
61
64
|
readonly CHECKOUT_DEADLINE: "checkoutDeadline";
|
|
62
65
|
readonly RESPONDED_AT: "respondedAt";
|
|
66
|
+
readonly PAID_ORDER_ID: "paidOrderId";
|
|
67
|
+
readonly PAID_AT: "paidAt";
|
|
63
68
|
readonly CREATED_AT: "createdAt";
|
|
64
69
|
readonly UPDATED_AT: "updatedAt";
|
|
65
70
|
};
|
|
66
71
|
export type OfferCreateInput = Pick<OfferDocument, "productId" | "productTitle" | "productSlug" | "productImageUrl" | "buyerUid" | "buyerName" | "buyerEmail" | "storeId" | "storeName" | "offerAmount" | "listedPrice" | "currency" | "buyerNote">;
|
|
67
|
-
export type OfferUpdateInput = Partial<Pick<OfferDocument, "status" | "counterAmount" | "lockedPrice" | "sellerNote" | "acceptedAt" | "respondedAt" | "updatedAt">>;
|
|
72
|
+
export type OfferUpdateInput = Partial<Pick<OfferDocument, "status" | "counterAmount" | "lockedPrice" | "sellerNote" | "acceptedAt" | "checkoutDeadline" | "respondedAt" | "paidOrderId" | "paidAt" | "updatedAt">>;
|
|
68
73
|
export declare const offerQueryHelpers: {
|
|
69
74
|
readonly byBuyer: (buyerUid: string) => readonly ["buyerUid", "==", string];
|
|
70
75
|
readonly byStore: (storeId: string) => readonly ["storeId", "==", string];
|
|
@@ -20,6 +20,7 @@ export const OFFER_INDEXED_FIELDS = [
|
|
|
20
20
|
"status",
|
|
21
21
|
"createdAt",
|
|
22
22
|
"expiresAt",
|
|
23
|
+
"checkoutDeadline",
|
|
23
24
|
];
|
|
24
25
|
export const OFFER_FIELDS = {
|
|
25
26
|
ID: "id",
|
|
@@ -42,6 +43,8 @@ export const OFFER_FIELDS = {
|
|
|
42
43
|
ACCEPTED_AT: "acceptedAt",
|
|
43
44
|
CHECKOUT_DEADLINE: "checkoutDeadline",
|
|
44
45
|
RESPONDED_AT: "respondedAt",
|
|
46
|
+
PAID_ORDER_ID: "paidOrderId",
|
|
47
|
+
PAID_AT: "paidAt",
|
|
45
48
|
CREATED_AT: "createdAt",
|
|
46
49
|
UPDATED_AT: "updatedAt",
|
|
47
50
|
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const counterOfferFormSchema: z.ZodObject<{
|
|
3
|
+
counterAmount: z.ZodNumber;
|
|
4
|
+
sellerNote: z.ZodOptional<z.ZodString>;
|
|
5
|
+
}, "strip", z.ZodTypeAny, {
|
|
6
|
+
counterAmount: number;
|
|
7
|
+
sellerNote?: string | undefined;
|
|
8
|
+
}, {
|
|
9
|
+
counterAmount: number;
|
|
10
|
+
sellerNote?: string | undefined;
|
|
11
|
+
}>;
|
|
12
|
+
export type CounterOfferFormValues = z.infer<typeof counterOfferFormSchema>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* WHY: Rule #9 — every appkit form is schema-driven. The seller's counter-offer
|
|
3
|
+
* drawer had no form at all before 2026-08-21 (the view took an
|
|
4
|
+
* `(id) => void` callback and left each consumer to invent an input), so
|
|
5
|
+
* `respondToOffer`'s counter branch had zero real callers.
|
|
6
|
+
* WHAT: Zod schema for the counter-offer QuickFormDrawer. Deliberately only the
|
|
7
|
+
* field-shape rules that can be checked without loading the offer — the
|
|
8
|
+
* relational rules (below listed price, different from the buyer's offer)
|
|
9
|
+
* stay server-side in `respondToOffer`, which is the only place that can
|
|
10
|
+
* be trusted with them.
|
|
11
|
+
*
|
|
12
|
+
* EXPORTS:
|
|
13
|
+
* counterOfferFormSchema, type CounterOfferFormValues
|
|
14
|
+
*
|
|
15
|
+
* @tag domain:seller,offers
|
|
16
|
+
* @tag layer:schema
|
|
17
|
+
* @tag pattern:none
|
|
18
|
+
* @tag access:isomorphic
|
|
19
|
+
* @tag consumers:SellerOffersView
|
|
20
|
+
* @tag sideEffects:none
|
|
21
|
+
*/
|
|
22
|
+
import { z } from "zod";
|
|
23
|
+
export const counterOfferFormSchema = z.object({
|
|
24
|
+
counterAmount: z.coerce
|
|
25
|
+
.number({ invalid_type_error: "Enter a counter amount." })
|
|
26
|
+
.positive("Counter amount must be greater than zero."),
|
|
27
|
+
sellerNote: z.string().max(500, "Keep your note under 500 characters.").optional(),
|
|
28
|
+
});
|
package/dist/index.d.ts
CHANGED
|
@@ -2457,7 +2457,7 @@ export { getProductFilterKeys } from "./features/products/index";
|
|
|
2457
2457
|
export { getProductSortOptions } from "./features/products/index";
|
|
2458
2458
|
export { getProductTableColumns } from "./features/products/index";
|
|
2459
2459
|
export { normalizeListingType, isAuctionListing, isPreOrderListing, isStandardListing, isPrizeDrawListing, isClassifiedListing, isDigitalCodeListing, isLiveListing, isArtListing, isStickersListing, } from "./features/products/index";
|
|
2460
|
-
export { isListingTypeEnabled, isCategoryTypeEnabled, enabledListingTypes, enabledCategoryTypes, } from "./_internal/shared/listing-types/feature-flags";
|
|
2460
|
+
export { isListingTypeEnabled, isCategoryTypeEnabled, enabledListingTypes, enabledCategoryTypes, ALL_LISTING_TYPES, } from "./_internal/shared/listing-types/feature-flags";
|
|
2461
2461
|
export { actionTracker, setActionTrackerSink, resetActionTrackerSink, type ActionEvent, type ActionTrackerSink, } from "./_internal/shared/listing-types/action-tracker";
|
|
2462
2462
|
export { cartRequiresShipping, cartIsDigitalOnly, cartIsChatOnly, } from "./_internal/shared/listing-types/cart-shipping";
|
|
2463
2463
|
export { ACTIONS, action, act, canPerformAction, actionsForListingType, actionLabel, type ActionDef, type ActionKind, type ActionResource, type ActionTree, type ActionConfirmation, } from "./_internal/shared/actions/action-registry";
|
|
@@ -3264,3 +3264,4 @@ export { safeFireAndForget } from "./utils/safe-fire-forget";
|
|
|
3264
3264
|
export { withRetry } from "./http/retry";
|
|
3265
3265
|
export { normalizeError, getErrorMessage, isApiNormalized, isAppNormalized, isFirebaseAuthNormalized, isFirebaseFirestoreNormalized, isFirebaseStorageNormalized, isNativeNormalized, isNetworkNormalized, isUnknownNormalized, isZodNormalized, } from "./errors/normalize";
|
|
3266
3266
|
export type { NormalizedError, NormalizedApiError, NormalizedAppError, NormalizedFirebaseAuthError, NormalizedFirebaseFirestoreError, NormalizedFirebaseStorageError, NormalizedNativeError, NormalizedNetworkError, NormalizedUnknownThrownValue, NormalizedZodError, } from "./errors/normalize";
|
|
3267
|
+
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/index.js
CHANGED
|
@@ -4463,7 +4463,7 @@ export { getProductTableColumns } from "./features/products/index";
|
|
|
4463
4463
|
// SB-UNI-F 2026-05-13 â€" Phase 2 predicates added (classified/digital-code/live).
|
|
4464
4464
|
export { normalizeListingType, isAuctionListing, isPreOrderListing, isStandardListing, isPrizeDrawListing, isClassifiedListing, isDigitalCodeListing, isLiveListing, isArtListing, isStickersListing, } from "./features/products/index";
|
|
4465
4465
|
// SB-UNI-X4 2026-05-13 â€" per-type feature-flag helpers.
|
|
4466
|
-
export { isListingTypeEnabled, isCategoryTypeEnabled, enabledListingTypes, enabledCategoryTypes, } from "./_internal/shared/listing-types/feature-flags";
|
|
4466
|
+
export { isListingTypeEnabled, isCategoryTypeEnabled, enabledListingTypes, enabledCategoryTypes, ALL_LISTING_TYPES, } from "./_internal/shared/listing-types/feature-flags";
|
|
4467
4467
|
// SB-UNI-X5 2026-05-13 â€" action telemetry sink.
|
|
4468
4468
|
export { actionTracker, setActionTrackerSink, resetActionTrackerSink, } from "./_internal/shared/listing-types/action-tracker";
|
|
4469
4469
|
// SB-UNI-S 2026-05-13 â€" cart-level shipping-requirement helpers.
|
|
@@ -5666,3 +5666,6 @@ export { withRetry } from "./http/retry";
|
|
|
5666
5666
|
// Every catch (e: unknown) funnels through normalizeError(e) which returns
|
|
5667
5667
|
// a NormalizedError discriminated union. Audit-catch-normalize enforces this.
|
|
5668
5668
|
export { normalizeError, getErrorMessage, isApiNormalized, isAppNormalized, isFirebaseAuthNormalized, isFirebaseFirestoreNormalized, isFirebaseStorageNormalized, isNativeNormalized, isNetworkNormalized, isUnknownNormalized, isZodNormalized, } from "./errors/normalize";
|
|
5669
|
+
// Checkout lanes — the derived auction > offer > standard partition of the
|
|
5670
|
+
// cart, and the priority rule that decides which one may be checked out.
|
|
5671
|
+
export { CART_LANE, CART_LANE_PRIORITY, CART_LANE_LABELS, laneOf, activeLane, laneItems, laneCounts, isLaneCheckoutable, laneBlockReason, isLockedLane, canAddNewItems, } from "./_internal/shared/checkout/lanes";
|
package/dist/server-entry.d.ts
CHANGED
|
@@ -45,6 +45,7 @@ export { StoreClassifiedsPageView } from "./features/stores/components/StoreClas
|
|
|
45
45
|
export { StoreDigitalCodesPageView } from "./features/stores/components/StoreDigitalCodesPageView";
|
|
46
46
|
export { StoreLiveItemsPageView } from "./features/stores/components/StoreLiveItemsPageView";
|
|
47
47
|
export { getProductForDetail, listSitemapProducts, type SitemapProduct, } from "./_internal/server/features/products/index";
|
|
48
|
+
export { listPublicProducts, parsePublicProductParams, PUBLIC_PRODUCT_MAX_PAGE_SIZE, type PublicProductListInput, type PublicProductListResult, type PublicProductListOptions, type PublicProductExecutor, type PublicProductQuery, } from "./_internal/server/features/products/index";
|
|
48
49
|
export { getAuctionForDetail, getProductFeaturesForAuction, } from "./_internal/server/features/auctions/index";
|
|
49
50
|
export { getPreOrderForDetail, getProductFeaturesForPreOrder, } from "./_internal/server/features/pre-orders/index";
|
|
50
51
|
export { getBrandForDetail, getBrandCategoryForDetail, createBrandAction, updateBrandAction, deleteBrandAction, toggleBrandActiveAction, } from "./_internal/server/features/brands/index";
|
package/dist/server-entry.js
CHANGED
|
@@ -51,6 +51,7 @@ export { StoreDigitalCodesPageView } from "./features/stores/components/StoreDig
|
|
|
51
51
|
export { StoreLiveItemsPageView } from "./features/stores/components/StoreLiveItemsPageView";
|
|
52
52
|
// S2: products data layer — deduped via React.cache()
|
|
53
53
|
export { getProductForDetail, listSitemapProducts, } from "./_internal/server/features/products/index";
|
|
54
|
+
export { listPublicProducts, parsePublicProductParams, PUBLIC_PRODUCT_MAX_PAGE_SIZE, } from "./_internal/server/features/products/index";
|
|
54
55
|
// S3: auctions data layer — deduped via React.cache()
|
|
55
56
|
export { getAuctionForDetail, getProductFeaturesForAuction, } from "./_internal/server/features/auctions/index";
|
|
56
57
|
// S3: pre-orders data layer — deduped via React.cache()
|
package/dist/server.d.ts
CHANGED
|
@@ -501,6 +501,7 @@ export type { GoogleReview, GoogleReviewsResult } from "./features/homepage/lib/
|
|
|
501
501
|
export { GoogleReviewsSection } from "./features/homepage/components/GoogleReviewsSection";
|
|
502
502
|
export type { GoogleReviewsSectionProps } from "./features/homepage/components/GoogleReviewsSection";
|
|
503
503
|
export { getProductForDetail, listSitemapProducts, computeRelatedItems, toProductItem, type SitemapProduct, type RelatedItemsResult, } from "./_internal/server/features/products/index";
|
|
504
|
+
export { listPublicProducts, parsePublicProductParams, PUBLIC_PRODUCT_MAX_PAGE_SIZE, type PublicProductListInput, type PublicProductListResult, type PublicProductListOptions, type PublicProductExecutor, type PublicProductQuery, } from "./_internal/server/features/products/index";
|
|
504
505
|
export { getBundleForDetail, listBundleMembers, listFeaturedBundles, getRelatedBundles, resolveBundleMemberIds, resolveBundleOriginalTotal, buildBundleMetadata, renderBundleOg, renderBundleOgImage, addBundleToCartAction, type BundleDataOptions, type BundleMetadataOptions, type BundleOgData, } from "./_internal/server/features/bundles/index";
|
|
505
506
|
export { getGroupsForProduct, getGroupsWithItemsForProduct, getGroupsForCategory, getGroupsForBrand } from "./_internal/server/features/grouped/index";
|
|
506
507
|
export { getAuctionForDetail, getProductFeaturesForAuction } from "./_internal/server/features/auctions/index";
|
package/dist/server.js
CHANGED
|
@@ -1353,6 +1353,9 @@ export { GoogleReviewsSection } from "./features/homepage/components/GoogleRevie
|
|
|
1353
1353
|
// ---------------------------------------------------------------------------
|
|
1354
1354
|
// Data layers — each wrapped in React.cache for request-scoped dedup
|
|
1355
1355
|
export { getProductForDetail, listSitemapProducts, computeRelatedItems, toProductItem, } from "./_internal/server/features/products/index";
|
|
1356
|
+
// The single public-listing query — every SSR listing view and /api/products
|
|
1357
|
+
// share it so their filter semantics cannot drift (Root Cause #30).
|
|
1358
|
+
export { listPublicProducts, parsePublicProductParams, PUBLIC_PRODUCT_MAX_PAGE_SIZE, } from "./_internal/server/features/products/index";
|
|
1356
1359
|
// S-SBUNI-3/4 2026-05-13 — bundle data/metadata/og layer.
|
|
1357
1360
|
export { getBundleForDetail, listBundleMembers, listFeaturedBundles, getRelatedBundles, resolveBundleMemberIds, resolveBundleOriginalTotal, buildBundleMetadata, renderBundleOg, renderBundleOgImage, addBundleToCartAction, } from "./_internal/server/features/bundles/index";
|
|
1358
1361
|
export { getGroupsForProduct, getGroupsWithItemsForProduct, getGroupsForCategory, getGroupsForBrand } from "./_internal/server/features/grouped/index";
|