@mohasinac/appkit 4.11.1 → 4.11.3
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.d.ts +4 -0
- package/dist/_internal/server/features/checkout/actions.js +138 -27
- package/dist/_internal/server/features/checkout/data.d.ts +10 -0
- 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/orders/adapters.js +7 -0
- package/dist/_internal/server/features/products/data.d.ts +12 -3
- package/dist/_internal/server/features/products/data.js +55 -4
- package/dist/_internal/server/features/products/index.d.ts +2 -1
- package/dist/_internal/server/features/products/index.js +2 -1
- package/dist/_internal/server/features/products/list-public.d.ts +130 -0
- package/dist/_internal/server/features/products/list-public.js +375 -0
- package/dist/_internal/server/features/reviews/data.d.ts +1 -1
- package/dist/_internal/server/features/reviews/data.js +3 -2
- package/dist/_internal/server/features/tester/visibility.js +3 -3
- 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/core/testerSandboxCleanup.d.ts +1 -1
- package/dist/_internal/server/jobs/core/testerSandboxCleanup.js +4 -1
- package/dist/_internal/server/jobs/core/testerSandboxRefresh.js +2 -0
- package/dist/_internal/server/jobs/handlers/_helpers.js +4 -1
- package/dist/_internal/server/jobs/handlers/messages.d.ts +5 -0
- package/dist/_internal/server/jobs/handlers/messages.js +5 -0
- package/dist/_internal/shared/checkout/lanes.d.ts +41 -0
- package/dist/_internal/shared/checkout/lanes.js +106 -0
- package/dist/_internal/shared/checkout/order-math.d.ts +19 -0
- package/dist/_internal/shared/checkout/order-math.js +30 -6
- package/dist/_internal/shared/features/promotions/schema.d.ts +0 -16
- package/dist/_internal/shared/features/promotions/schema.js +0 -2
- package/dist/_internal/shared/features/reviews/config.d.ts +8 -1
- package/dist/_internal/shared/features/reviews/config.js +8 -1
- package/dist/_internal/shared/listing-types/_registry.d.ts +88 -1
- package/dist/_internal/shared/listing-types/_registry.js +63 -1
- package/dist/_internal/shared/listing-types/art/config.d.ts +59 -0
- package/dist/_internal/shared/listing-types/art/config.js +12 -0
- package/dist/_internal/shared/listing-types/auction/config.d.ts +53 -0
- package/dist/_internal/shared/listing-types/auction/config.js +17 -0
- package/dist/_internal/shared/listing-types/classified/config.d.ts +59 -0
- package/dist/_internal/shared/listing-types/classified/config.js +14 -0
- package/dist/_internal/shared/listing-types/digital-code/config.d.ts +59 -0
- package/dist/_internal/shared/listing-types/digital-code/config.js +12 -0
- package/dist/_internal/shared/listing-types/feature-flags.d.ts +1 -0
- package/dist/_internal/shared/listing-types/feature-flags.js +24 -10
- package/dist/_internal/shared/listing-types/live/config.d.ts +59 -0
- package/dist/_internal/shared/listing-types/live/config.js +14 -0
- package/dist/_internal/shared/listing-types/pre-order/config.d.ts +110 -0
- package/dist/_internal/shared/listing-types/pre-order/config.js +14 -0
- package/dist/_internal/shared/listing-types/prize-draw/config.d.ts +44 -0
- package/dist/_internal/shared/listing-types/prize-draw/config.js +10 -0
- package/dist/_internal/shared/listing-types/standard/config.d.ts +59 -0
- package/dist/_internal/shared/listing-types/standard/config.js +9 -0
- package/dist/_internal/shared/listing-types/stickers/config.d.ts +59 -0
- package/dist/_internal/shared/listing-types/stickers/config.js +12 -0
- package/dist/client.d.ts +7 -2
- package/dist/client.js +8 -2
- package/dist/constants/field-names.d.ts +12 -0
- package/dist/constants/field-names.js +12 -0
- package/dist/constants/table-keys.d.ts +2 -0
- package/dist/constants/table-keys.js +2 -0
- package/dist/features/account/components/NotificationBell.js +1 -0
- package/dist/features/account/components/UserOffersPanel.js +8 -1
- package/dist/features/account/components/UserOrdersView.js +27 -5
- package/dist/features/admin/actions/notification-actions.js +1 -1
- package/dist/features/admin/components/AdminAddressesView.js +3 -1
- package/dist/features/admin/components/AdminCouponEditorView.js +1 -4
- package/dist/features/admin/components/AdminPaymentMethodsView.js +3 -1
- package/dist/features/admin/components/AdminProductsView.js +34 -18
- package/dist/features/admin/constants/filter-tabs.d.ts +26 -9
- package/dist/features/admin/constants/filter-tabs.js +22 -9
- 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/AuctionBottomActions.js +1 -0
- package/dist/features/auctions/components/AuctionDetailPageView.js +13 -11
- 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 -4
- package/dist/features/categories/components/BrandDetailTabs.js +31 -11
- package/dist/features/categories/components/CategoryDetailTabs.js +27 -7
- package/dist/features/categories/components/CategoryProductsListing.d.ts +9 -1
- package/dist/features/categories/components/CategoryProductsListing.js +5 -2
- package/dist/features/classified/components/ClassifiedDetailPageView.js +2 -1
- package/dist/features/contact/email.js +56 -1
- package/dist/features/digital-codes/components/DigitalCodeDetailPageView.js +17 -16
- package/dist/features/homepage/components/FeaturedResultsSection.js +1 -1
- package/dist/features/homepage/components/SectionCarousel.js +1 -1
- package/dist/features/homepage/components/ShopByCategorySection.js +1 -1
- package/dist/features/layout/AppLayoutShell.js +9 -2
- package/dist/features/layout/BottomActions.js +26 -3
- package/dist/features/layout/BottomActionsContext.d.ts +14 -0
- package/dist/features/layout/BottomActionsContext.js +13 -1
- package/dist/features/layout/hooks/useBottomActions.d.ts +6 -1
- package/dist/features/layout/hooks/useBottomActions.js +4 -1
- package/dist/features/layout/index.d.ts +1 -1
- package/dist/features/live/components/LiveItemDetailPageView.js +7 -6
- package/dist/features/orders/repository/orders.repository.d.ts +4 -15
- package/dist/features/orders/repository/orders.repository.js +4 -27
- package/dist/features/orders/types/index.d.ts +16 -0
- package/dist/features/orders/utils/order-splitter.js +14 -2
- package/dist/features/pre-orders/components/PreOrderBottomActions.d.ts +1 -1
- package/dist/features/pre-orders/components/PreOrderBottomActions.js +3 -19
- package/dist/features/pre-orders/components/PreOrdersListView.js +11 -39
- package/dist/features/products/components/ArtStickersListView.js +10 -49
- package/dist/features/products/components/AuctionsIndexListing.js +1 -2
- package/dist/features/products/components/ListingBottomActions.d.ts +28 -0
- package/dist/features/products/components/ListingBottomActions.js +25 -0
- package/dist/features/products/components/PrizeDrawBottomActions.d.ts +1 -1
- package/dist/features/products/components/PrizeDrawBottomActions.js +3 -21
- package/dist/features/products/components/ProductDetailActions.js +4 -0
- package/dist/features/products/components/ProductDetailPageView.js +7 -7
- package/dist/features/products/components/ProductsIndexPageView.js +9 -62
- package/dist/features/products/constants/listing-tabs.d.ts +43 -198
- package/dist/features/products/constants/listing-tabs.js +71 -58
- package/dist/features/products/constants/sieve.d.ts +104 -1
- package/dist/features/products/constants/sieve.js +27 -7
- package/dist/features/products/repository/products.repository.d.ts +45 -1
- package/dist/features/products/repository/products.repository.js +67 -8
- package/dist/features/products/utils/listing-badge-variant.d.ts +20 -1
- package/dist/features/products/utils/listing-badge-variant.js +26 -1
- package/dist/features/products/utils/listing-type.d.ts +24 -0
- package/dist/features/products/utils/listing-type.js +48 -0
- package/dist/features/promotions/actions/coupon-actions.d.ts +7 -0
- package/dist/features/promotions/actions/coupon-actions.js +12 -1
- package/dist/features/promotions/actions/coupon-stacking.d.ts +24 -0
- package/dist/features/promotions/actions/coupon-stacking.js +21 -0
- package/dist/features/promotions/actions/index.d.ts +1 -0
- package/dist/features/promotions/actions/index.js +1 -0
- package/dist/features/promotions/actions/seller-coupon-actions.d.ts +0 -1
- package/dist/features/promotions/actions/seller-coupon-actions.js +0 -1
- package/dist/features/promotions/components/CouponHelpDetails.d.ts +20 -0
- package/dist/features/promotions/components/CouponHelpDetails.js +17 -0
- package/dist/features/promotions/components/CouponsIndexListing.js +2 -1
- package/dist/features/promotions/components/index.d.ts +2 -0
- package/dist/features/promotions/components/index.js +1 -0
- package/dist/features/promotions/constants/coupon-help.d.ts +27 -0
- package/dist/features/promotions/constants/coupon-help.js +42 -0
- package/dist/features/promotions/repository/coupons.repository.d.ts +10 -1
- package/dist/features/promotions/repository/coupons.repository.js +22 -8
- package/dist/features/promotions/schemas/firestore.d.ts +0 -1
- package/dist/features/promotions/schemas/firestore.js +1 -1
- package/dist/features/promotions/schemas/index.d.ts +0 -20
- package/dist/features/promotions/schemas/index.js +0 -1
- package/dist/features/reviews/actions/review-actions.js +5 -1
- package/dist/features/reviews/components/ReviewDetailPageView.js +2 -1
- package/dist/features/reviews/components/ReviewFilters.d.ts +7 -1
- package/dist/features/reviews/components/ReviewFilters.js +4 -3
- package/dist/features/reviews/components/ReviewsIndexListing.d.ts +6 -0
- package/dist/features/reviews/components/ReviewsIndexListing.js +9 -109
- package/dist/features/reviews/components/ReviewsListingPanel.d.ts +41 -0
- package/dist/features/reviews/components/ReviewsListingPanel.js +226 -0
- package/dist/features/reviews/components/index.d.ts +2 -0
- package/dist/features/reviews/components/index.js +1 -0
- package/dist/features/reviews/hooks/useReviews.d.ts +8 -0
- package/dist/features/reviews/hooks/useReviews.js +14 -6
- package/dist/features/reviews/repository/reviews.repository.d.ts +29 -1
- package/dist/features/reviews/repository/reviews.repository.js +77 -41
- package/dist/features/reviews/types/index.d.ts +8 -1
- 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/components/SellerProductsCards.js +2 -2
- package/dist/features/seller/components/SellerProductsView.js +21 -18
- 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/features/stores/actions/store-query-actions.js +1 -1
- package/dist/features/stores/components/StoreAuctionsListing.js +5 -11
- package/dist/features/stores/components/StorePreOrdersListing.js +6 -11
- package/dist/features/stores/components/StoreReviewsListing.d.ts +4 -0
- package/dist/features/stores/components/StoreReviewsListing.js +7 -108
- package/dist/features/stores/schemas/index.d.ts +2 -2
- package/dist/features/tester/seed-data/coupons-tester-seed-data.js +3 -3
- package/dist/features/tester/seed-data/index.d.ts +1 -0
- package/dist/features/tester/seed-data/index.js +1 -0
- package/dist/features/tester/seed-data/offers-tester-seed-data.d.ts +2 -0
- package/dist/features/tester/seed-data/offers-tester-seed-data.js +61 -0
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +337 -1
- package/dist/index.d.ts +11 -4
- package/dist/index.js +15 -3
- package/dist/react/hooks/useListingTypeFlags.d.ts +12 -19
- package/dist/react/hooks/useListingTypeFlags.js +4 -9
- package/dist/schemas/registry.d.ts +0 -12
- package/dist/seed/claimed-coupons-seed-data.js +3 -3
- package/dist/seed/coupons-seed-data.js +29 -10
- package/dist/seed/faq-seed-data.js +2 -2
- package/dist/seed/manifest.js +2 -2
- 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/dist/styles.css +44 -39
- package/dist/tailwind-utilities.css +1 -1
- package/dist/ui/components/FilterChipGroup.d.ts +21 -3
- package/dist/ui/components/FilterChipGroup.js +33 -4
- package/dist/ui/components/HorizontalScroller.d.ts +1 -2
- package/dist/ui/components/HorizontalScroller.js +9 -5
- package/dist/ui/components/HorizontalScroller.style.css +43 -38
- package/package.json +1 -1
|
@@ -47,6 +47,8 @@ export declare const TABLE_KEYS: {
|
|
|
47
47
|
readonly DELIVERY_METHOD: "deliveryMethod";
|
|
48
48
|
readonly SPECIES: "species";
|
|
49
49
|
readonly JURISDICTION: "jurisdiction";
|
|
50
|
+
readonly LIVE_SEX: "liveSex";
|
|
51
|
+
readonly LIVE_TRANSPORT_METHOD: "liveTransportMethod";
|
|
50
52
|
};
|
|
51
53
|
export type TableKey = (typeof TABLE_KEYS)[keyof typeof TABLE_KEYS];
|
|
52
54
|
export declare const VIEW_MODE: {
|
|
@@ -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)
|
|
88
|
+
const items = Array.isArray(json)
|
|
89
|
+
? json
|
|
90
|
+
: (json.data?.items ?? json.items ?? []);
|
|
84
91
|
setOffers(items);
|
|
85
92
|
}
|
|
86
93
|
catch (err) {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx as _jsx,
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import { sortBy, sieveFilter, SIEVE_OP } from "../../../utils/sieve-builder";
|
|
4
4
|
import { ACCOUNT_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
5
5
|
import { ROUTES } from "../../../constants/index";
|
|
6
6
|
import { ACTIONS } from "../../../_internal/shared/actions/action-registry";
|
|
7
|
-
import { Button, Div, Grid, Stack, Text, TextLink } from "../../../ui";
|
|
7
|
+
import { Button, Div, FilterChipGroup, Grid, Stack, Text, TextLink } from "../../../ui";
|
|
8
8
|
import { PaginatedSelect } from "../../../ui/components/PaginatedSelect";
|
|
9
9
|
import { DataListingView } from "../../admin/components/DataListingView";
|
|
10
10
|
import { OrderCard } from "../../orders/components/OrdersList";
|
|
@@ -16,6 +16,23 @@ const SORT_OPTIONS = [
|
|
|
16
16
|
{ value: sortBy("totalPrice", "DESC"), label: "Highest total" },
|
|
17
17
|
{ value: sortBy("totalPrice", "ASC"), label: "Lowest total" },
|
|
18
18
|
];
|
|
19
|
+
/**
|
|
20
|
+
* Lane tabs — the same auction > offer > standard split the cart and checkout
|
|
21
|
+
* use, viewed after the fact. Every `id` is EXACTLY a stored
|
|
22
|
+
* `OrderDocument.orderType` value (or "" for All): a tab id that doesn't
|
|
23
|
+
* match a real stored value returns zero rows forever with no error anywhere
|
|
24
|
+
* (CLAUDE.md Root Cause #33).
|
|
25
|
+
*
|
|
26
|
+
* "standard" is filtered in memory server-side, because orders written before
|
|
27
|
+
* `orderType` existed have no such field and a Firestore `==` would exclude
|
|
28
|
+
* every one of them.
|
|
29
|
+
*/
|
|
30
|
+
const ORDER_LANE_TABS = [
|
|
31
|
+
{ id: "", label: "All" },
|
|
32
|
+
{ id: "standard", label: "Normal" },
|
|
33
|
+
{ id: "auction", label: "Auction wins" },
|
|
34
|
+
{ id: "offer", label: "Offer wins" },
|
|
35
|
+
];
|
|
19
36
|
const STATUS_OPTIONS = [
|
|
20
37
|
{ value: "pending", label: "Pending" },
|
|
21
38
|
{ value: "processing", label: "Processing" },
|
|
@@ -31,7 +48,7 @@ export function UserOrdersView({ onOrderClick }) {
|
|
|
31
48
|
title: "My Orders",
|
|
32
49
|
searchPlaceholder: "Search by order id…",
|
|
33
50
|
emptyLabel: "You haven't placed any orders yet.",
|
|
34
|
-
filterKeys: ["status"],
|
|
51
|
+
filterKeys: ["status", "orderType"],
|
|
35
52
|
defaultSort: sortBy("createdAt", "DESC"),
|
|
36
53
|
queryKey: ["user", "orders", "listing"],
|
|
37
54
|
endpoint: ACCOUNT_ENDPOINTS.ORDERS,
|
|
@@ -39,8 +56,13 @@ export function UserOrdersView({ onOrderClick }) {
|
|
|
39
56
|
hideTableView: true,
|
|
40
57
|
mapRows: (response) => response.items ?? [],
|
|
41
58
|
getTotal: (response, rows) => response.total ?? rows.length,
|
|
42
|
-
buildFilters: (state) =>
|
|
43
|
-
|
|
59
|
+
buildFilters: (state) => [
|
|
60
|
+
state.status ? sieveFilter("status", SIEVE_OP.EQ, state.status) : null,
|
|
61
|
+
state.orderType ? sieveFilter("orderType", SIEVE_OP.EQ, state.orderType) : null,
|
|
62
|
+
]
|
|
63
|
+
.filter(Boolean)
|
|
64
|
+
.join(",") || undefined,
|
|
65
|
+
renderFilterPanel: ({ pendingFilters, setPendingFilters }) => (_jsxs(Stack, { gap: "md", children: [_jsx(FilterChipGroup, { label: "Type", tabs: ORDER_LANE_TABS, value: pendingFilters.orderType ?? "", onChange: (id) => setPendingFilters((prev) => ({ ...prev, orderType: id })) }), _jsx(Div, { children: _jsx(PaginatedSelect, { value: pendingFilters.status || null, onChange: (v) => setPendingFilters((p) => ({ ...p, status: v ?? "" })), options: STATUS_OPTIONS, placeholder: "All statuses", ariaLabel: "Filter by order status" }) })] })),
|
|
44
66
|
renderCards: (rows, _view, _selection, isLoading) => {
|
|
45
67
|
if (isLoading) {
|
|
46
68
|
return (_jsx(Stack, { gap: "md", children: Array.from({ length: 3 }).map((_, i) => (_jsx(Div, { className: "h-24 animate-pulse border border-[var(--appkit-color-border)]", rounded: "xl" }, i))) }));
|
|
@@ -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",
|
|
@@ -60,7 +60,9 @@ function buildAddressConfig(banStatus, onAction) {
|
|
|
60
60
|
? `${ADMIN_ENDPOINTS.ADDRESSES}?banStatus=${encodeURIComponent(banStatus)}`
|
|
61
61
|
: ADMIN_ENDPOINTS.ADDRESSES,
|
|
62
62
|
sortOptions: [
|
|
63
|
-
|
|
63
|
+
// -bannedAt (newest flagged first) to match defaultSort — the option was
|
|
64
|
+
// "bannedAt" (oldest first), so the dropdown opened with nothing selected.
|
|
65
|
+
{ value: sortBy("bannedAt", "DESC"), label: "Flagged Date" },
|
|
64
66
|
{ value: "city", label: "City" },
|
|
65
67
|
],
|
|
66
68
|
columns: ADDRESS_COLUMNS,
|
|
@@ -81,7 +81,6 @@ export function AdminCouponEditorView({ couponId, onSaved, onDeleted, embedded,
|
|
|
81
81
|
const [isActive, setIsActive] = React.useState(true);
|
|
82
82
|
// restrictions
|
|
83
83
|
const [firstTimeOnly, setFirstTimeOnly] = React.useState(false);
|
|
84
|
-
const [combinable, setCombinable] = React.useState(false);
|
|
85
84
|
const [appliesToAuctions, setAppliesToAuctions] = React.useState(false);
|
|
86
85
|
const [applicableProducts, setApplicableProducts] = React.useState([]);
|
|
87
86
|
const [applicableCategories, setApplicableCategories] = React.useState([]);
|
|
@@ -122,7 +121,6 @@ export function AdminCouponEditorView({ couponId, onSaved, onDeleted, embedded,
|
|
|
122
121
|
setEndDate(toDateInputValue(c.validity?.endDate));
|
|
123
122
|
setIsActive(c.validity?.isActive ?? false);
|
|
124
123
|
setFirstTimeOnly(c.restrictions?.firstTimeUserOnly ?? false);
|
|
125
|
-
setCombinable(c.restrictions?.combineWithSellerCoupons ?? false);
|
|
126
124
|
setApplicableProducts(Array.isArray(c.restrictions?.applicableProducts) ? c.restrictions.applicableProducts : []);
|
|
127
125
|
setApplicableCategories(Array.isArray(c.restrictions?.applicableCategories) ? c.restrictions.applicableCategories : []);
|
|
128
126
|
setAppliesToAuctions(c.applicableToAuctions ?? false);
|
|
@@ -162,7 +160,6 @@ export function AdminCouponEditorView({ couponId, onSaved, onDeleted, embedded,
|
|
|
162
160
|
},
|
|
163
161
|
restrictions: {
|
|
164
162
|
firstTimeUserOnly: firstTimeOnly,
|
|
165
|
-
combineWithSellerCoupons: combinable,
|
|
166
163
|
...(applicableProducts.length > 0 && { applicableProducts }),
|
|
167
164
|
...(applicableCategories.length > 0 && { applicableCategories }),
|
|
168
165
|
},
|
|
@@ -208,7 +205,7 @@ export function AdminCouponEditorView({ couponId, onSaved, onDeleted, embedded,
|
|
|
208
205
|
const formSection = (_jsxs(_Fragment, { children: [_jsx(Form, { schema: couponFormSchema, onSubmit: (e) => e.preventDefault(), spacing: "md", children: ({ setFieldError, clearErrors }) => (_jsxs(_Fragment, { children: [_jsx(Select, { label: "Coupon type", options: TYPE_OPTIONS, value: type, onValueChange: (v) => setType(v), required: true }), _jsx(FieldInput, { name: "name", label: "Campaign name", value: name, onChange: (v) => handleNameChange(v), required: true, placeholder: "e.g. Summer Sale 20%" }), !isEdit && (_jsx(Input, { label: "Coupon code", value: code, onChange: (e) => {
|
|
209
206
|
setCode(toCouponCode(e.target.value));
|
|
210
207
|
setCodeManual(true);
|
|
211
|
-
}, required: true, placeholder: "e.g. SUMMER20", helperText: "Auto-generated from name. Uppercase alphanumeric + hyphens only." })), isEdit && (_jsx(Input, { label: "Coupon code", value: code, disabled: true, helperText: "Code cannot be changed after creation." })), _jsx(Input, { label: "Description (optional)", value: description, onChange: (e) => setDescription(e.target.value), placeholder: "Internal notes about this coupon" }), _jsx(CouponDiscountFields, { type: type, discountValue: discountValue, setDiscountValue: setDiscountValue, maxDiscount: maxDiscount, setMaxDiscount: setMaxDiscount, minPurchase: minPurchase, setMinPurchase: setMinPurchase, buyQty: buyQty, setBuyQty: setBuyQty, getQty: getQty, setGetQty: setGetQty, discountLabel: discountLabel }), _jsx(CouponValidityFields, { startDate: startDate, setStartDate: setStartDate, endDate: endDate, setEndDate: setEndDate, isActive: isActive, setIsActive: setIsActive, totalLimit: totalLimit, setTotalLimit: setTotalLimit, perUserLimit: perUserLimit, setPerUserLimit: setPerUserLimit, isEdit: isEdit, currentUsage: currentUsage }), _jsx(Toggle, { label: "First-time users only", checked: firstTimeOnly, onChange: setFirstTimeOnly }), _jsx(Toggle, { label: "
|
|
208
|
+
}, required: true, placeholder: "e.g. SUMMER20", helperText: "Auto-generated from name. Uppercase alphanumeric + hyphens only." })), isEdit && (_jsx(Input, { label: "Coupon code", value: code, disabled: true, helperText: "Code cannot be changed after creation." })), _jsx(Input, { label: "Description (optional)", value: description, onChange: (e) => setDescription(e.target.value), placeholder: "Internal notes about this coupon" }), _jsx(CouponDiscountFields, { type: type, discountValue: discountValue, setDiscountValue: setDiscountValue, maxDiscount: maxDiscount, setMaxDiscount: setMaxDiscount, minPurchase: minPurchase, setMinPurchase: setMinPurchase, buyQty: buyQty, setBuyQty: setBuyQty, getQty: getQty, setGetQty: setGetQty, discountLabel: discountLabel }), _jsx(CouponValidityFields, { startDate: startDate, setStartDate: setStartDate, endDate: endDate, setEndDate: setEndDate, isActive: isActive, setIsActive: setIsActive, totalLimit: totalLimit, setTotalLimit: setTotalLimit, perUserLimit: perUserLimit, setPerUserLimit: setPerUserLimit, isEdit: isEdit, currentUsage: currentUsage }), _jsx(Toggle, { label: "First-time users only", checked: firstTimeOnly, onChange: setFirstTimeOnly }), _jsx(Toggle, { label: "Applies to auctions", checked: appliesToAuctions, onChange: setAppliesToAuctions }), _jsxs(Stack, { gap: "xs", children: [_jsx(Label, { size: "sm", weight: "medium", color: "primary", children: "Applicable products (optional)" }), _jsx(ProductInlineSelect, { scope: "admin", multiple: true, value: applicableProducts, onChange: setApplicableProducts, placeholder: "Restrict to specific products\u2026" }), _jsx(Text, { size: "xs", color: "muted", children: "Leave empty to apply the coupon to every eligible product." })] }), _jsxs(Stack, { gap: "xs", children: [_jsx(Label, { size: "sm", weight: "medium", color: "primary", children: "Applicable categories (optional)" }), _jsx(CategoryInlineSelect, { value: "", onChange: (id) => {
|
|
212
209
|
if (!id || applicableCategories.includes(id))
|
|
213
210
|
return;
|
|
214
211
|
setApplicableCategories([...applicableCategories, id]);
|
|
@@ -57,7 +57,9 @@ function buildConfig(banStatus, onAction) {
|
|
|
57
57
|
? `${ADMIN_ENDPOINTS.PAYMENT_METHODS}?banStatus=${encodeURIComponent(banStatus)}`
|
|
58
58
|
: ADMIN_ENDPOINTS.PAYMENT_METHODS,
|
|
59
59
|
sortOptions: [
|
|
60
|
-
|
|
60
|
+
// -bannedAt (newest flagged first) to match defaultSort — the option was
|
|
61
|
+
// "bannedAt" (oldest first), so the dropdown opened with nothing selected.
|
|
62
|
+
{ value: sortBy("bannedAt", "DESC"), label: "Flagged Date" },
|
|
61
63
|
{ value: "type", label: "Type" },
|
|
62
64
|
],
|
|
63
65
|
columns: PM_COLUMNS,
|
|
@@ -15,21 +15,17 @@ import { ACTIONS } from "../../../_internal/shared/actions/action-registry";
|
|
|
15
15
|
import { AdminProductEditorView } from "./AdminProductEditorView";
|
|
16
16
|
import { QuickEditMenu } from "./QuickEditMenu";
|
|
17
17
|
import { ADMIN_BULK_ACTIONS, ROW_ACTION_META, ROW_ACTION_ID } from "../../products/constants/action-defs";
|
|
18
|
+
import { parseSelectedListingTypes } from "../../products/utils/listing-type";
|
|
18
19
|
const FLAG_DEFS = [
|
|
19
20
|
{ key: "featured", label: "Featured" },
|
|
20
21
|
{ key: "isPromoted", label: "Promoted" },
|
|
21
22
|
{ key: "isOnSale", label: "On Sale" },
|
|
22
23
|
{ key: "isSold", label: "Sold" },
|
|
23
24
|
];
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
Products: "listingType==standard",
|
|
29
|
-
Classifieds: "listingType==classified",
|
|
30
|
-
"Digital Codes": "listingType==digital-code",
|
|
31
|
-
"Live Items": "listingType==live",
|
|
32
|
-
};
|
|
25
|
+
// TYPE_FILTER_MAP (label -> Sieve clause) was deleted 2026-08-21. Chip ids are
|
|
26
|
+
// now canonical ListingType values, so the clause is built directly and the
|
|
27
|
+
// array is covered by audit-filter-tab-enums. The map had no `art`/`stickers`
|
|
28
|
+
// entries, which is why admin could never filter to either type.
|
|
33
29
|
function buildBaseColumns() {
|
|
34
30
|
return [
|
|
35
31
|
{
|
|
@@ -112,11 +108,23 @@ export function AdminProductsView({ children, ...props }) {
|
|
|
112
108
|
defaultSort: sortBy("createdAt", "DESC"),
|
|
113
109
|
queryKey: ["admin", "products", "listing"],
|
|
114
110
|
endpoint: ADMIN_ENDPOINTS.PRODUCTS,
|
|
111
|
+
// "Featured first" / "Promoted first" existed in STANDARD_SORT_OPTIONS but
|
|
112
|
+
// were offered nowhere, and would not have worked if they had been:
|
|
113
|
+
// `featured`/`isPromoted` were `canSort: false`, so sievejs dropped the
|
|
114
|
+
// sort silently. Both are now sortable (products.repository.ts) with
|
|
115
|
+
// matching composite indexes, and admin is where curating them matters.
|
|
116
|
+
//
|
|
117
|
+
// Deliberately a curated set, not the full 10-option STANDARD_SORT_OPTIONS:
|
|
118
|
+
// every additional sort field multiplies out against this view's
|
|
119
|
+
// isSold/status/listingType filter combinations into more composite
|
|
120
|
+
// indexes, and Rule #6 says spend those only where they earn their keep.
|
|
115
121
|
sortOptions: [
|
|
116
122
|
{ value: sortBy("createdAt", "DESC"), label: "Newest" },
|
|
117
123
|
{ value: sortBy("createdAt", "ASC"), label: "Oldest" },
|
|
118
124
|
{ value: "title", label: "Title A–Z" },
|
|
119
125
|
{ value: sortBy("price", "DESC"), label: "Highest price" },
|
|
126
|
+
{ value: sortBy("featured", "DESC"), label: "Featured first" },
|
|
127
|
+
{ value: sortBy("isPromoted", "DESC"), label: "Promoted first" },
|
|
120
128
|
],
|
|
121
129
|
columns: [...buildBaseColumns(), flagColumn],
|
|
122
130
|
mapRows: (response) => toRecordArray(response.items).map((item, index) => {
|
|
@@ -165,16 +173,24 @@ export function AdminProductsView({ children, ...props }) {
|
|
|
165
173
|
parts.push("isSold==false");
|
|
166
174
|
if (state.status && state.status !== "All")
|
|
167
175
|
parts.push(sieveFilter("status", SIEVE_OP.EQ, state.status));
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
//
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
176
|
+
// `state.type` is a pipe-joined set of canonical ListingType values
|
|
177
|
+
// (multi-select chips). It used to be a single display LABEL resolved
|
|
178
|
+
// through a TYPE_FILTER_MAP lookup — an indirection that silently
|
|
179
|
+
// dropped `art` and `stickers`, which had no map entry.
|
|
180
|
+
const selectedTypes = parseSelectedListingTypes(state.type);
|
|
181
|
+
if (selectedTypes.length > 0) {
|
|
182
|
+
parts.push(sieveFilter("listingType", SIEVE_OP.EQ, selectedTypes.join("|")));
|
|
183
|
+
}
|
|
184
|
+
// Ended/closed are per-type predicates: `auctionEndDate` and
|
|
185
|
+
// `prizeRevealWindowEnd` only exist on auctions and prize-draws. Apply
|
|
186
|
+
// each only when its type is the ONLY one selected — with a mixed
|
|
187
|
+
// selection the inequality would exclude every row of the other types
|
|
188
|
+
// (they have no such field at all), which reads as "no results".
|
|
189
|
+
const onlyType = selectedTypes.length === 1 ? selectedTypes[0] : null;
|
|
190
|
+
if (onlyType === "auction" && state.showEnded !== "true") {
|
|
175
191
|
parts.push(sieveFilter("auctionEndDate", SIEVE_OP.GTE, new Date().toISOString()));
|
|
176
192
|
}
|
|
177
|
-
if (
|
|
193
|
+
if (onlyType === "prize-draw" && state.showClosed !== "true") {
|
|
178
194
|
parts.push(sieveFilter("prizeRevealWindowEnd", SIEVE_OP.GTE, new Date().toISOString()));
|
|
179
195
|
}
|
|
180
196
|
return parts.join(",") || undefined;
|
|
@@ -247,7 +263,7 @@ export function AdminProductsView({ children, ...props }) {
|
|
|
247
263
|
submitLabel: "Save",
|
|
248
264
|
},
|
|
249
265
|
] })),
|
|
250
|
-
renderFilterPanel: ({ pendingFilters, setPendingFilters }) => (_jsxs(_Fragment, { children: [_jsx(FilterChipGroup, { label: "Status", tabs: ADMIN_PRODUCT_STATUS_TABS, value: pendingFilters.status ?? "", onChange: (id) => setPendingFilters((p) => ({ ...p, status: id })) }), _jsx(FilterChipGroup, { label: "Type", tabs: ADMIN_PRODUCT_LISTING_TYPE_TABS, value: pendingFilters.type ?? "", onChange: (
|
|
266
|
+
renderFilterPanel: ({ pendingFilters, setPendingFilters }) => (_jsxs(_Fragment, { children: [_jsx(FilterChipGroup, { label: "Status", tabs: ADMIN_PRODUCT_STATUS_TABS, value: pendingFilters.status ?? "", onChange: (id) => setPendingFilters((p) => ({ ...p, status: id })) }), _jsx(FilterChipGroup, { multiple: true, label: "Type", tabs: ADMIN_PRODUCT_LISTING_TYPE_TABS, value: pendingFilters.type ?? "", onChange: (ids) => setPendingFilters((p) => ({ ...p, type: ids })) }), _jsx(FilterChipGroup, { label: "Sold", tabs: [
|
|
251
267
|
{ id: "", label: "Hide sold" },
|
|
252
268
|
{ id: "true", label: "Show sold" },
|
|
253
269
|
], value: pendingFilters.showSold ?? "", onChange: (id) => setPendingFilters((p) => ({ ...p, showSold: id })), allId: "" }), _jsx(FilterChipGroup, { label: "Ended", tabs: [
|
|
@@ -38,32 +38,49 @@ export declare const ADMIN_PRODUCT_STATUS_TABS: readonly [{
|
|
|
38
38
|
readonly id: "archived";
|
|
39
39
|
readonly label: "Archived";
|
|
40
40
|
}];
|
|
41
|
-
/**
|
|
42
|
-
*
|
|
41
|
+
/**
|
|
42
|
+
* Admin > Products — listing-type filter chip set.
|
|
43
|
+
*
|
|
44
|
+
* IDs are canonical `ListingType` values (2026-08-21). They used to be display
|
|
45
|
+
* LABELS ("Products", "Pre-orders", …) translated to a Sieve clause through a
|
|
46
|
+
* `TYPE_FILTER_MAP` lookup in AdminProductsView — an indirection that hid two
|
|
47
|
+
* whole missing types (`art`, `stickers`) and put the array outside the reach
|
|
48
|
+
* of `audit-filter-tab-enums.mjs`, which compares chip ids against the real
|
|
49
|
+
* backing enum. Now registered there, so a missing or misspelled type blocks.
|
|
50
|
+
*
|
|
51
|
+
* Multi-select: this feeds a `<FilterChipGroup multiple>`, so several types can
|
|
52
|
+
* be active at once and the selection is emitted as a pipe-joined OR-group.
|
|
53
|
+
*/
|
|
43
54
|
export declare const ADMIN_PRODUCT_LISTING_TYPE_TABS: readonly [{
|
|
44
55
|
readonly id: "All";
|
|
45
56
|
readonly label: "All";
|
|
46
57
|
}, {
|
|
47
|
-
readonly id: "
|
|
58
|
+
readonly id: "standard";
|
|
48
59
|
readonly label: "Products";
|
|
49
60
|
}, {
|
|
50
|
-
readonly id: "
|
|
61
|
+
readonly id: "auction";
|
|
51
62
|
readonly label: "Auctions";
|
|
52
63
|
}, {
|
|
53
|
-
readonly id: "
|
|
64
|
+
readonly id: "pre-order";
|
|
54
65
|
readonly label: "Pre-orders";
|
|
55
66
|
}, {
|
|
56
|
-
readonly id: "
|
|
67
|
+
readonly id: "prize-draw";
|
|
57
68
|
readonly label: "Prize Draws";
|
|
58
69
|
}, {
|
|
59
|
-
readonly id: "
|
|
70
|
+
readonly id: "classified";
|
|
60
71
|
readonly label: "Classifieds";
|
|
61
72
|
}, {
|
|
62
|
-
readonly id: "
|
|
73
|
+
readonly id: "digital-code";
|
|
63
74
|
readonly label: "Digital Codes";
|
|
64
75
|
}, {
|
|
65
|
-
readonly id: "
|
|
76
|
+
readonly id: "live";
|
|
66
77
|
readonly label: "Live Items";
|
|
78
|
+
}, {
|
|
79
|
+
readonly id: "art";
|
|
80
|
+
readonly label: "Art";
|
|
81
|
+
}, {
|
|
82
|
+
readonly id: "stickers";
|
|
83
|
+
readonly label: "Stickers";
|
|
67
84
|
}];
|
|
68
85
|
/** Admin > Blog — post status filter chip set. */
|
|
69
86
|
export declare const ADMIN_BLOG_STATUS_TABS: readonly [{
|
|
@@ -20,17 +20,30 @@ export const ADMIN_PRODUCT_STATUS_TABS = [
|
|
|
20
20
|
{ id: "draft", label: "Draft" },
|
|
21
21
|
{ id: "archived", label: "Archived" },
|
|
22
22
|
];
|
|
23
|
-
/**
|
|
24
|
-
*
|
|
23
|
+
/**
|
|
24
|
+
* Admin > Products — listing-type filter chip set.
|
|
25
|
+
*
|
|
26
|
+
* IDs are canonical `ListingType` values (2026-08-21). They used to be display
|
|
27
|
+
* LABELS ("Products", "Pre-orders", …) translated to a Sieve clause through a
|
|
28
|
+
* `TYPE_FILTER_MAP` lookup in AdminProductsView — an indirection that hid two
|
|
29
|
+
* whole missing types (`art`, `stickers`) and put the array outside the reach
|
|
30
|
+
* of `audit-filter-tab-enums.mjs`, which compares chip ids against the real
|
|
31
|
+
* backing enum. Now registered there, so a missing or misspelled type blocks.
|
|
32
|
+
*
|
|
33
|
+
* Multi-select: this feeds a `<FilterChipGroup multiple>`, so several types can
|
|
34
|
+
* be active at once and the selection is emitted as a pipe-joined OR-group.
|
|
35
|
+
*/
|
|
25
36
|
export const ADMIN_PRODUCT_LISTING_TYPE_TABS = [
|
|
26
37
|
ALL_TAB,
|
|
27
|
-
{ id: "
|
|
28
|
-
{ id: "
|
|
29
|
-
{ id: "
|
|
30
|
-
{ id: "
|
|
31
|
-
{ id: "
|
|
32
|
-
{ id: "
|
|
33
|
-
{ id: "
|
|
38
|
+
{ id: "standard", label: "Products" },
|
|
39
|
+
{ id: "auction", label: "Auctions" },
|
|
40
|
+
{ id: "pre-order", label: "Pre-orders" },
|
|
41
|
+
{ id: "prize-draw", label: "Prize Draws" },
|
|
42
|
+
{ id: "classified", label: "Classifieds" },
|
|
43
|
+
{ id: "digital-code", label: "Digital Codes" },
|
|
44
|
+
{ id: "live", label: "Live Items" },
|
|
45
|
+
{ id: "art", label: "Art" },
|
|
46
|
+
{ id: "stickers", label: "Stickers" },
|
|
34
47
|
];
|
|
35
48
|
/** Admin > Blog — post status filter chip set. */
|
|
36
49
|
export const ADMIN_BLOG_STATUS_TABS = [
|
|
@@ -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
|
-
|
|
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 {
|
|
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) })
|
|
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", align: "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]);
|
|
@@ -31,6 +31,7 @@ export function AuctionBottomActions({ currentBid, currency, bidCount, isEnded,
|
|
|
31
31
|
],
|
|
32
32
|
secondaryLabel: formatCountdownLabel(remaining),
|
|
33
33
|
infoLabel: `${formatCurrency(currentBid, currency)} · ${bidCount} bid${bidCount !== 1 ? "s" : ""}`,
|
|
34
|
+
desktop: "after-scroll",
|
|
34
35
|
});
|
|
35
36
|
return null;
|
|
36
37
|
}
|