@mohasinac/appkit 4.14.0 → 4.15.0
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/auctions/actions.js +2 -2
- package/dist/_internal/server/features/cart/service.d.ts +3 -3
- package/dist/_internal/server/features/checkout/actions.js +19 -0
- package/dist/_internal/server/features/checkout/source-context.d.ts +5 -0
- package/dist/_internal/server/features/checkout/source-context.js +157 -0
- package/dist/_internal/server/features/offers/adapters.d.ts +103 -0
- package/dist/_internal/server/features/offers/adapters.js +88 -0
- package/dist/_internal/server/features/offers/index.d.ts +2 -0
- package/dist/_internal/server/features/offers/index.js +1 -0
- package/dist/_internal/server/features/orders/adapters.js +24 -0
- package/dist/_internal/server/jobs/core/offerExpiry.js +10 -10
- package/dist/_internal/shared/actions/action-registry.js +3 -1
- package/dist/_internal/shared/features/auctions/schema.d.ts +14 -2
- package/dist/_internal/shared/features/auctions/schema.js +13 -1
- package/dist/_internal/shared/features/cart/schema.d.ts +14 -9
- package/dist/_internal/shared/features/cart/schema.js +15 -0
- package/dist/_internal/shared/features/products/schema.d.ts +2 -2
- package/dist/_internal/shared/history/history.d.ts +22 -0
- package/dist/_internal/shared/history/history.js +41 -1
- package/dist/client.d.ts +13 -2
- package/dist/client.js +20 -2
- package/dist/constants/field-names.d.ts +8 -6
- package/dist/constants/field-names.js +48 -6
- package/dist/features/account/components/UserBidsView.js +12 -4
- package/dist/features/account/components/UserReturnsView.d.ts +3 -0
- package/dist/features/account/components/UserReturnsView.js +17 -1
- package/dist/features/admin/components/AdminAuditLogView.js +1 -0
- package/dist/features/admin/components/AdminBidsView.js +15 -2
- package/dist/features/admin/components/AdminBlogEditorView.js +65 -33
- package/dist/features/admin/components/AdminFeatureEditorView.js +35 -6
- package/dist/features/admin/components/AdminNavEditorView.js +15 -5
- package/dist/features/admin/components/AdminNewsletterView.js +18 -2
- package/dist/features/admin/components/AdminOffersView.js +62 -18
- package/dist/features/admin/components/AdminPaymentMethodsView.js +24 -4
- package/dist/features/admin/components/AdminScammerEditorView.d.ts +2 -1
- package/dist/features/admin/components/AdminScammerEditorView.js +3 -13
- package/dist/features/admin/components/AdminScammersView.js +3 -8
- package/dist/features/admin/components/AdminSectionsView.js +32 -23
- package/dist/features/admin/components/AdminSessionsView.js +17 -2
- package/dist/features/admin/components/ViewAuditLogEntryModal.js +1 -0
- package/dist/features/admin/components/sections/adminSectionsTypes.d.ts +4 -2
- package/dist/features/admin/components/sections/adminSectionsTypes.js +45 -27
- package/dist/features/admin/constants/filter-tabs.d.ts +3 -0
- package/dist/features/admin/constants/filter-tabs.js +1 -0
- package/dist/features/admin/schemas/homepage-section-form.d.ts +22 -0
- package/dist/features/admin/schemas/homepage-section-form.js +68 -0
- package/dist/features/admin/schemas/nav-item-form.d.ts +74 -0
- package/dist/features/admin/schemas/nav-item-form.js +72 -0
- package/dist/features/auctions/components/AuctionBidsTable.d.ts +10 -1
- package/dist/features/auctions/components/AuctionBidsTable.js +22 -4
- package/dist/features/auctions/components/bid-detail-fields.d.ts +29 -0
- package/dist/features/auctions/components/bid-detail-fields.js +103 -0
- package/dist/features/audit-log/schemas/firestore.d.ts +14 -0
- package/dist/features/audit-log/schemas/firestore.js +7 -0
- package/dist/features/blog/schemas/blog-form.d.ts +80 -0
- package/dist/features/blog/schemas/blog-form.js +100 -0
- package/dist/features/events/components/AdminEventEditorView.js +32 -54
- package/dist/features/events/components/AdminEventEntriesView.js +22 -3
- package/dist/features/events/schemas/event-form.d.ts +113 -0
- package/dist/features/events/schemas/event-form.js +82 -0
- package/dist/features/grouped/schemas/grouped-listing-form.d.ts +101 -0
- package/dist/features/grouped/schemas/grouped-listing-form.js +89 -0
- package/dist/features/homepage/actions/homepage-section-actions.d.ts +11 -3
- package/dist/features/homepage/actions/homepage-section-actions.js +33 -1
- package/dist/features/homepage/schemas/index.d.ts +31 -0
- package/dist/features/homepage/schemas/index.js +24 -0
- package/dist/features/orders/components/OrderStatusTimeline.d.ts +5 -1
- package/dist/features/orders/components/OrderStatusTimeline.js +39 -20
- package/dist/features/orders/repository/orders.repository.js +5 -0
- package/dist/features/orders/schemas/firestore.d.ts +36 -1
- package/dist/features/orders/types/index.d.ts +35 -7
- package/dist/features/products/components/GroupSettingsPanel.js +5 -1
- package/dist/features/products/schemas/index.d.ts +2 -2
- package/dist/features/products/schemas/product-features.validators.d.ts +73 -0
- package/dist/features/products/schemas/product-features.validators.js +63 -0
- package/dist/features/scams/constants/scammer-status-display.d.ts +28 -0
- package/dist/features/scams/constants/scammer-status-display.js +79 -0
- package/dist/features/seller/actions/offer-actions.js +32 -2
- package/dist/features/seller/components/OfferPhaseTimeline.d.ts +59 -0
- package/dist/features/seller/components/OfferPhaseTimeline.js +107 -0
- package/dist/features/seller/components/SellerBidsView.js +11 -4
- package/dist/features/seller/components/SellerCouponEditorView.js +17 -17
- package/dist/features/seller/components/SellerOffersView.js +2 -1
- package/dist/features/seller/components/SellerPayoutSettingsView.js +38 -14
- package/dist/features/seller/repository/offer.repository.d.ts +69 -8
- package/dist/features/seller/repository/offer.repository.js +127 -23
- package/dist/features/seller/schemas/coupon-form.d.ts +72 -0
- package/dist/features/seller/schemas/coupon-form.js +116 -0
- package/dist/features/seller/schemas/firestore.d.ts +56 -2
- package/dist/features/seller/schemas/firestore.js +27 -0
- package/dist/features/seller/schemas/offer-forms.d.ts +21 -0
- package/dist/features/seller/schemas/offer-forms.js +21 -0
- package/dist/features/shell/build-sections.d.ts +63 -0
- package/dist/features/shell/build-sections.js +207 -0
- package/dist/features/shell/field-ui-meta.d.ts +115 -0
- package/dist/features/shell/field-ui-meta.js +324 -0
- package/dist/features/shell/index.d.ts +4 -0
- package/dist/features/shell/index.js +2 -0
- package/dist/features/status-history/components/StatusTimeline.d.ts +74 -0
- package/dist/features/status-history/components/StatusTimeline.js +46 -0
- package/dist/features/status-history/index.d.ts +11 -0
- package/dist/features/status-history/index.js +10 -0
- package/dist/features/store-extensions/components/ReviewDecisionModal.d.ts +27 -0
- package/dist/features/store-extensions/components/ReviewDecisionModal.js +82 -0
- package/dist/features/store-extensions/index.d.ts +12 -0
- package/dist/features/store-extensions/index.js +12 -0
- package/dist/features/store-extensions/schemas/admin-notification-form.d.ts +36 -0
- package/dist/features/store-extensions/schemas/admin-notification-form.js +114 -0
- package/dist/features/store-extensions/schemas/analytics-forms.d.ts +57 -0
- package/dist/features/store-extensions/schemas/analytics-forms.js +158 -0
- package/dist/features/store-extensions/schemas/custom-role-form.d.ts +85 -0
- package/dist/features/store-extensions/schemas/custom-role-form.js +118 -0
- package/dist/features/store-extensions/schemas/firestore.d.ts +21 -1
- package/dist/features/store-extensions/schemas/firestore.js +60 -0
- package/dist/features/store-extensions/schemas/google-config-form.d.ts +53 -0
- package/dist/features/store-extensions/schemas/google-config-form.js +105 -0
- package/dist/features/store-extensions/schemas/item-request-create-form.d.ts +28 -0
- package/dist/features/store-extensions/schemas/item-request-create-form.js +80 -0
- package/dist/features/store-extensions/schemas/listing-template-form.d.ts +78 -0
- package/dist/features/store-extensions/schemas/listing-template-form.js +87 -0
- package/dist/features/store-extensions/schemas/moderation-review-form.d.ts +49 -0
- package/dist/features/store-extensions/schemas/moderation-review-form.js +101 -0
- package/dist/features/store-extensions/schemas/payout-method-form.d.ts +153 -0
- package/dist/features/store-extensions/schemas/payout-method-form.js +155 -0
- package/dist/features/store-extensions/schemas/report-create-form.d.ts +25 -0
- package/dist/features/store-extensions/schemas/report-create-form.js +79 -0
- package/dist/features/store-extensions/schemas/report-review-form.d.ts +47 -0
- package/dist/features/store-extensions/schemas/report-review-form.js +111 -0
- package/dist/features/store-extensions/schemas/shipping-config-form.d.ts +148 -0
- package/dist/features/store-extensions/schemas/shipping-config-form.js +104 -0
- package/dist/features/store-extensions/schemas/store-category-form.d.ts +54 -0
- package/dist/features/store-extensions/schemas/store-category-form.js +73 -0
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +326 -0
- package/dist/index.d.ts +12 -2
- package/dist/index.js +12 -2
- package/dist/next/routing/route-map.d.ts +16 -2
- package/dist/next/routing/route-map.js +9 -2
- package/dist/schemas/firestore-value.d.ts +8 -0
- package/dist/schemas/firestore-value.js +44 -0
- package/dist/schemas/index.d.ts +1 -1
- package/dist/schemas/index.js +1 -1
- package/dist/schemas/registry.d.ts +634 -1
- package/dist/schemas/registry.js +64 -1
- package/dist/seed/bids-seed-data.js +30 -0
- package/dist/seed/offers-seed-data.js +212 -7
- package/dist/seed/orders-seed-data.js +336 -1
- package/dist/seed/products-auctions-seed-data.js +41 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +5 -0
- package/dist/styles.css +1 -1
- package/dist/tailwind-utilities.css +1 -1
- package/dist/ui/components/RecordDetailModal.d.ts +10 -1
- package/dist/ui/components/RecordDetailModal.js +2 -2
- package/package.json +2 -1
- package/scripts/node-esm-loader.mjs +65 -0
- package/scripts/seed-cli-loader.mjs +6 -49
- package/scripts/seed-cli.mjs +1 -1
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
/**
|
|
4
|
+
* OfferPhaseTimeline — the offer-shaped wrapper around `<StatusTimeline>`.
|
|
5
|
+
*
|
|
6
|
+
* An offer is an order with more phases in front of it, so it shares the rail
|
|
7
|
+
* and supplies only its own vocabulary:
|
|
8
|
+
*
|
|
9
|
+
* made → countered → accepted → paid
|
|
10
|
+
* with declined / withdrawn / expired as the terminal-negative branch
|
|
11
|
+
*
|
|
12
|
+
* ## `superseded` is a RENDER KEY, never an OfferStatus
|
|
13
|
+
*
|
|
14
|
+
* A document that is `withdrawn` **and** carries `supersededByOfferId` is not
|
|
15
|
+
* a walk-away — the buyer countered, and the negotiation continued into the
|
|
16
|
+
* next round. It renders neutral ("Superseded"), not negative ("Withdrawn").
|
|
17
|
+
*
|
|
18
|
+
* Deliberately NOT an eighth `OfferStatus` value: `ADMIN_OFFER_STATUS_TABS`
|
|
19
|
+
* would then gain a chip that matches zero stored rows, which is exactly the
|
|
20
|
+
* defect `audit-filter-tab-enums` exists to catch.
|
|
21
|
+
*
|
|
22
|
+
* ## Back-compat
|
|
23
|
+
*
|
|
24
|
+
* Offers written before `statusHistory` existed have none. Branch B derives
|
|
25
|
+
* what it can from the scalars — and `expired` gets **no date at all**,
|
|
26
|
+
* because `expireMany` never wrote one. `updatedAt` is "last write of any
|
|
27
|
+
* kind" and `expiresAt` is a deadline, not an event; using either would be a
|
|
28
|
+
* fabricated timestamp.
|
|
29
|
+
*/
|
|
30
|
+
import { StatusTimeline, stepsFromEntries, } from "../../status-history/components/StatusTimeline";
|
|
31
|
+
const PHASE_LABELS = {
|
|
32
|
+
pending: "Offer made",
|
|
33
|
+
countered: "Countered",
|
|
34
|
+
accepted: "Accepted",
|
|
35
|
+
paid: "Paid",
|
|
36
|
+
declined: "Declined",
|
|
37
|
+
withdrawn: "Withdrawn",
|
|
38
|
+
expired: "Expired",
|
|
39
|
+
superseded: "Superseded",
|
|
40
|
+
};
|
|
41
|
+
const NEGATIVE = new Set(["declined", "withdrawn", "expired"]);
|
|
42
|
+
const labelFor = (status) => PHASE_LABELS[status] ?? status;
|
|
43
|
+
const isNegative = (status) => NEGATIVE.has(status);
|
|
44
|
+
/** Branch B — scalars only, for offers with no recorded history. */
|
|
45
|
+
function buildLegacySteps(o) {
|
|
46
|
+
const steps = [
|
|
47
|
+
{ key: "pending", label: PHASE_LABELS.pending, date: o.createdAt },
|
|
48
|
+
];
|
|
49
|
+
const s = (o.status ?? "").toLowerCase();
|
|
50
|
+
if (s === "countered") {
|
|
51
|
+
steps.push({ key: "countered", label: PHASE_LABELS.countered, date: o.respondedAt });
|
|
52
|
+
}
|
|
53
|
+
if (s === "accepted" || s === "paid") {
|
|
54
|
+
steps.push({
|
|
55
|
+
key: "accepted",
|
|
56
|
+
label: PHASE_LABELS.accepted,
|
|
57
|
+
date: o.acceptedAt ?? o.respondedAt,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
if (s === "paid") {
|
|
61
|
+
steps.push({ key: "paid", label: PHASE_LABELS.paid, date: o.paidAt });
|
|
62
|
+
}
|
|
63
|
+
if (s === "declined") {
|
|
64
|
+
steps.push({ key: "declined", label: PHASE_LABELS.declined, date: o.respondedAt, negative: true });
|
|
65
|
+
}
|
|
66
|
+
if (s === "withdrawn") {
|
|
67
|
+
steps.push(o.superseded
|
|
68
|
+
? { key: "superseded", label: PHASE_LABELS.superseded, date: o.respondedAt, muted: true }
|
|
69
|
+
: { key: "withdrawn", label: PHASE_LABELS.withdrawn, date: o.respondedAt, negative: true });
|
|
70
|
+
}
|
|
71
|
+
if (s === "expired") {
|
|
72
|
+
// No date, on purpose. `expireMany` never wrote one before W2.
|
|
73
|
+
steps.push({ key: "expired", label: PHASE_LABELS.expired, negative: true });
|
|
74
|
+
}
|
|
75
|
+
return steps;
|
|
76
|
+
}
|
|
77
|
+
/** Re-key a withdrawn+superseded step so it reads neutral, not negative. */
|
|
78
|
+
function applySupersede(steps, superseded) {
|
|
79
|
+
if (!superseded)
|
|
80
|
+
return steps;
|
|
81
|
+
return steps.map((s) => s.key === "withdrawn"
|
|
82
|
+
? { ...s, key: "superseded", label: PHASE_LABELS.superseded, negative: false, muted: true }
|
|
83
|
+
: s);
|
|
84
|
+
}
|
|
85
|
+
export function OfferPhaseTimeline({ status, timeline, timelineTruncated, superseded = false, createdAt, respondedAt, acceptedAt, paidAt, chain, className = "", }) {
|
|
86
|
+
const own = applySupersede(stepsFromEntries(timeline, labelFor, isNegative).length
|
|
87
|
+
? stepsFromEntries(timeline, labelFor, isNegative)
|
|
88
|
+
: buildLegacySteps({ status, createdAt, respondedAt, acceptedAt, paidAt, superseded }), superseded);
|
|
89
|
+
const priorRounds = (chain ?? []).filter((r) => r.id !== undefined);
|
|
90
|
+
const hasChain = priorRounds.length > 1;
|
|
91
|
+
const steps = hasChain
|
|
92
|
+
? priorRounds.flatMap((round, idx) => {
|
|
93
|
+
const roundSteps = applySupersede(buildLegacySteps({
|
|
94
|
+
status: round.status,
|
|
95
|
+
createdAt: round.createdAt,
|
|
96
|
+
respondedAt: round.respondedAt,
|
|
97
|
+
superseded: round.superseded,
|
|
98
|
+
}), round.superseded);
|
|
99
|
+
return roundSteps.map((s, i) => ({
|
|
100
|
+
...s,
|
|
101
|
+
roundLabel: i === 0 ? `Round ${round.counterRound || idx + 1}` : undefined,
|
|
102
|
+
note: i === roundSteps.length - 1 ? (round.sellerNote ?? round.buyerNote) : undefined,
|
|
103
|
+
}));
|
|
104
|
+
})
|
|
105
|
+
: own;
|
|
106
|
+
return (_jsx(StatusTimeline, { title: hasChain ? "Negotiation" : "Offer history", steps: steps, truncatedCount: timelineTruncated, emptyLabel: "No offer history yet", className: className }));
|
|
107
|
+
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import { normalizeError } from "../../../errors/normalize";
|
|
4
4
|
import { SIEVE_OP, Stack, sieveFilter } from "@mohasinac/appkit/client";
|
|
5
5
|
import { sortBy } from "@mohasinac/appkit/client";
|
|
6
6
|
import { useState, useCallback } from "react";
|
|
7
|
-
import { Badge, Button, Div, FilterChipGroup, Span, Text, useToast } from "../../../ui";
|
|
7
|
+
import { Badge, Button, Div, FilterChipGroup, RecordDetailModal, Span, Text, useToast } from "../../../ui";
|
|
8
|
+
import { buildBidDetailFields, bidStatusBadge } from "../../auctions/components/bid-detail-fields";
|
|
8
9
|
import { SELLER_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
9
10
|
import { ACTIONS } from "../../../_internal/shared/actions/action-registry";
|
|
10
11
|
import { buildBulkAction } from "../../../_internal/shared/actions/bulk-helpers";
|
|
@@ -29,6 +30,7 @@ const STATUS_BADGE = {
|
|
|
29
30
|
cancelled: "danger",
|
|
30
31
|
};
|
|
31
32
|
export function SellerBidsView({ endpoint = SELLER_ENDPOINTS.BIDS }) {
|
|
33
|
+
const [detail, setDetail] = useState(null);
|
|
32
34
|
const { showToast } = useToast();
|
|
33
35
|
const [collapsedGroups, setCollapsedGroups] = useState(new Set());
|
|
34
36
|
const toggleGroup = (id) => setCollapsedGroups((s) => {
|
|
@@ -130,9 +132,14 @@ export function SellerBidsView({ endpoint = SELLER_ENDPOINTS.BIDS }) {
|
|
|
130
132
|
}
|
|
131
133
|
return (_jsx(Stack, { gap: "3", children: groupedRows.map((group) => {
|
|
132
134
|
const collapsed = collapsedGroups.has(group.id);
|
|
133
|
-
return (_jsxs(Div, { className: "border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)]", rounded: "lg", children: [_jsxs(Button, { variant: "ghost", type: "button", onClick: () => toggleGroup(group.id), justify: "between", paddingX: "md", paddingY: "y-xs-tall", textSize: "sm", weight: "semibold", textColor: "default", className: "w-full hover:bg-[var(--appkit-color-surface-raised)]", children: [_jsxs(Span, { className: "truncate", children: [group.title, " ", _jsxs(Span, { size: "xs", weight: "normal", className: "text-[var(--appkit-color-text-muted)]", children: ["\u00B7 ", group.bids.length, " bid", group.bids.length === 1 ? "" : "s"] })] }), _jsx(Span, { "aria-hidden": true, className: "text-[var(--appkit-color-text-muted)]", children: collapsed ? "▸" : "▾" })] }), !collapsed && (_jsx(Div, { className: "border-t border-[var(--appkit-color-border)]", children: _jsx(DataTable, { rows: group.bids, columns: columns, isLoading: false, emptyLabel: "", selectedIds: new Set(selection.selectedIds), onToggleSelect: selection.toggleSelect
|
|
135
|
+
return (_jsxs(Div, { className: "border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)]", rounded: "lg", children: [_jsxs(Button, { variant: "ghost", type: "button", onClick: () => toggleGroup(group.id), justify: "between", paddingX: "md", paddingY: "y-xs-tall", textSize: "sm", weight: "semibold", textColor: "default", className: "w-full hover:bg-[var(--appkit-color-surface-raised)]", children: [_jsxs(Span, { className: "truncate", children: [group.title, " ", _jsxs(Span, { size: "xs", weight: "normal", className: "text-[var(--appkit-color-text-muted)]", children: ["\u00B7 ", group.bids.length, " bid", group.bids.length === 1 ? "" : "s"] })] }), _jsx(Span, { "aria-hidden": true, className: "text-[var(--appkit-color-text-muted)]", children: collapsed ? "▸" : "▾" })] }), !collapsed && (_jsx(Div, { className: "border-t border-[var(--appkit-color-border)]", children: _jsx(DataTable, { rows: group.bids, columns: columns, isLoading: false, emptyLabel: "", selectedIds: new Set(selection.selectedIds), onToggleSelect: selection.toggleSelect,
|
|
136
|
+
// Without this the row could be selected and bulk-acted
|
|
137
|
+
// on but never opened — a seller could cancel a bid they
|
|
138
|
+
// had no way to read (Root Cause #56).
|
|
139
|
+
onRowClick: (row) => setDetail(row) }) }))] }, group.id));
|
|
134
140
|
}) }));
|
|
135
141
|
},
|
|
136
142
|
};
|
|
137
|
-
|
|
143
|
+
const detailBadge = detail ? bidStatusBadge(detail.status) : null;
|
|
144
|
+
return (_jsxs(_Fragment, { children: [_jsx(DataListingView, { config: config }), _jsx(RecordDetailModal, { isOpen: detail !== null, onClose: () => setDetail(null), title: detail?.productTitle || "Bid", badges: detailBadge ? [{ label: detailBadge.label }] : undefined, fields: detail ? buildBidDetailFields(detail, "seller") : undefined })] }));
|
|
138
145
|
}
|
|
@@ -5,6 +5,8 @@ import { Badge, Button, Checkbox, Div, Form, Heading, IconButton, Input, Label,
|
|
|
5
5
|
import { ProductInlineSelect } from "./ProductInlineSelect";
|
|
6
6
|
import { CategoryInlineSelect } from "./CategoryInlineSelect";
|
|
7
7
|
import { normalizeError } from "../../../errors/normalize";
|
|
8
|
+
import { FormErrorSummary, applyZodIssues } from "../../../ui/forms";
|
|
9
|
+
import { sellerCouponFormSchema } from "../schemas/coupon-form";
|
|
8
10
|
const __O = {
|
|
9
11
|
hidden: "overflow-hidden",
|
|
10
12
|
};
|
|
@@ -39,22 +41,20 @@ export function SellerCouponEditorView({ couponId, initial, onSave, onCancel, })
|
|
|
39
41
|
const [saving, setSaving] = useState(false);
|
|
40
42
|
const [error, setError] = useState(null);
|
|
41
43
|
const set = (key, value) => setDraft((d) => ({ ...d, [key]: value }));
|
|
42
|
-
const handleSubmit = async (e) => {
|
|
44
|
+
const handleSubmit = async (e, setFieldError) => {
|
|
43
45
|
e.preventDefault();
|
|
44
|
-
if
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
if (draft.startDate > draft.endDate) {
|
|
57
|
-
setError("Start date must be before end date");
|
|
46
|
+
// Replaces four hand-rolled `if` checks that covered presence and date
|
|
47
|
+
// order and nothing else — a 500% percentage discount, a non-numeric
|
|
48
|
+
// value, a negative minimum spend and a per-customer limit above the
|
|
49
|
+
// total all passed. Errors now land on the field, not in one banner.
|
|
50
|
+
const parsed = sellerCouponFormSchema.safeParse(draft);
|
|
51
|
+
if (!parsed.success) {
|
|
52
|
+
if (setFieldError) {
|
|
53
|
+
applyZodIssues(parsed.error.issues, setFieldError);
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
setError(parsed.error.issues[0]?.message ?? "Please check the form.");
|
|
57
|
+
}
|
|
58
58
|
return;
|
|
59
59
|
}
|
|
60
60
|
setError(null);
|
|
@@ -71,11 +71,11 @@ export function SellerCouponEditorView({ couponId, initial, onSave, onCancel, })
|
|
|
71
71
|
}
|
|
72
72
|
};
|
|
73
73
|
const isEdit = Boolean(couponId);
|
|
74
|
-
return (_jsx(Form, {
|
|
74
|
+
return (_jsx(Form, { schema: sellerCouponFormSchema, onSubmit: (e) => e.preventDefault(), children: ({ setFieldError }) => (_jsxs(Stack, { gap: "none", className: `max-w-lg mx-auto border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] ${__O.hidden}`, rounded: "xl", shadow: "sm", children: [_jsx(Div, { className: "h-[3px] w-full [background:linear-gradient(to_right,var(--appkit-color-primary-700)_0%,var(--appkit-color-cobalt)_55%,var(--appkit-color-secondary-400)_100%)]", "aria-hidden": "true" }), _jsx(Div, { border: "bottom", paddingY: "y-md-lg", padding: "x-lg", children: _jsxs(Row, { justify: "between", gap: "3", children: [_jsx(Heading, { level: 2, className: "text-[var(--appkit-color-text)]", size: "lg", weight: "semibold", children: isEdit ? "Edit Coupon" : "Create Coupon" }), isEdit && (_jsx(Badge, { variant: draft.isActive ? "success" : "default", children: draft.isActive ? "Active" : "Inactive" }))] }) }), _jsxs(Stack, { gap: "5", padding: "lg", children: [_jsx(FormErrorSummary, {}), error && (_jsx(Div, { textSize: "sm", className: "border border-error/20", color: "error", surface: "danger-surface", padding: "inline", rounded: "lg", children: error })), _jsx(Input, { label: "Coupon Code", value: draft.code, onChange: (e) => set("code", e.target.value.toUpperCase().replace(/\s+/g, "")), placeholder: "e.g. WELCOME10", required: true, disabled: isEdit, helperText: isEdit ? "Code cannot be changed after creation" : "Customers enter this at checkout" }), _jsx(Select, { label: "Discount Type", value: draft.type, options: TYPE_OPTIONS, onChange: (e) => set("type", e.target.value) }), draft.type !== "free_shipping" && (_jsx(Input, { label: draft.type === "percentage" ? "Discount Percentage (%)" : "Discount Amount (₹)", type: "number", min: 1, max: draft.type === "percentage" ? 100 : undefined, value: draft.value, onChange: (e) => set("value", e.target.value), placeholder: draft.type === "percentage" ? "e.g. 10" : "e.g. 50", required: true, helperText: draft.type === "percentage"
|
|
75
75
|
? "Enter a value between 1 and 100"
|
|
76
76
|
: "Fixed rupee discount applied to the order" })), draft.type === "percentage" && (_jsx(Input, { label: "Max Discount Cap (\u20B9, optional)", type: "number", min: 0, value: draft.maxDiscount, onChange: (e) => set("maxDiscount", e.target.value), placeholder: "Leave blank for no cap", helperText: "Maximum rupee discount regardless of percentage" })), _jsx(Input, { label: "Minimum Order Amount (\u20B9, optional)", type: "number", min: 0, value: draft.minPurchase, onChange: (e) => set("minPurchase", e.target.value), placeholder: "Leave blank for no minimum" }), _jsxs(Div, { children: [_jsx(Text, { size: "sm", className: CLS_SECTION_LABEL, children: "Usage Limits" }), _jsxs(Div, { layout: "grid", gap: "3", className: "grid-cols-2", children: [_jsx(Input, { label: "Total Uses", type: "number", min: 0, value: draft.totalLimit, onChange: (e) => set("totalLimit", e.target.value), placeholder: "0 = unlimited" }), _jsx(Input, { label: "Per Customer", type: "number", min: 0, value: draft.perUserLimit, onChange: (e) => set("perUserLimit", e.target.value), placeholder: "0 = unlimited" })] })] }), _jsxs(Div, { children: [_jsx(Text, { size: "sm", className: CLS_SECTION_LABEL, children: "Validity Period" }), _jsxs(Div, { layout: "grid", gap: "3", className: "grid-cols-2", children: [_jsx(Input, { label: "Start Date", type: "date", value: draft.startDate, onChange: (e) => set("startDate", e.target.value), required: true }), _jsx(Input, { label: "End Date", type: "date", value: draft.endDate, onChange: (e) => set("endDate", e.target.value), required: true })] })] }), _jsxs(Div, { children: [_jsx(Text, { size: "sm", className: CLS_SECTION_LABEL, children: "Applicability (optional)" }), _jsxs(Stack, { gap: "sm", children: [_jsxs(Div, { children: [_jsx(Text, { size: "xs", className: "mb-1 text-[var(--appkit-color-text-secondary)]", children: "Applicable products" }), _jsx(ProductInlineSelect, { scope: "store", multiple: true, value: draft.applicableProducts, onChange: (v) => set("applicableProducts", v), placeholder: "Restrict to specific products\u2026" })] }), _jsxs(Div, { children: [_jsx(Text, { size: "xs", className: "mb-1 text-[var(--appkit-color-text-secondary)]", children: "Applicable categories" }), _jsx(CategoryInlineSelect, { value: "", onChange: (id) => {
|
|
77
77
|
if (!id || draft.applicableCategories.includes(id))
|
|
78
78
|
return;
|
|
79
79
|
set("applicableCategories", [...draft.applicableCategories, id]);
|
|
80
|
-
}, placeholder: "Add a category\u2026" }), draft.applicableCategories.length > 0 && (_jsx(Row, { wrap: true, gap: "sm", padding: "t-xs", children: draft.applicableCategories.map((cid) => (_jsxs(Span, { layout: "inline-flex", gap: "xs", border: "strong", padding: "pill-sm", rounded: "full", surface: "muted", color: "primary", size: "xs", children: [cid, _jsx(IconButton, { type: "button", "aria-label": `Remove ${cid}`, variant: "ghost", className: "text-[var(--appkit-color-text-muted)] hover:text-[var(--appkit-color-text)]", onClick: () => set("applicableCategories", draft.applicableCategories.filter((c) => c !== cid)), children: "\u00D7" })] }, cid))) }))] }), _jsx(Text, { size: "xs", className: "text-[var(--appkit-color-text-secondary)]", children: "Leave both empty to apply the coupon to every product in your store." })] })] }), _jsx(Row, { className: "border border-[var(--appkit-color-border)] dark:border-[var(--appkit-color-border-dark)]", padding: "inline", align: "center", gap: "3", rounded: "lg", children: _jsxs(Label, { layout: "flex", gap: "lg", className: "cursor-pointer w-full", children: [_jsx(Checkbox, { bare: true, checked: draft.isActive, onChange: (e) => set("isActive", e.target.checked), className: "h-4 w-4 rounded border-[var(--appkit-color-border)] text-[var(--appkit-color-primary)] focus:ring-[var(--appkit-color-primary)]" }), _jsxs(Div, { className: "flex-1 min-w-0", children: [_jsx(Text, { size: "sm", className: "text-[var(--appkit-color-text-primary)]", weight: "medium", children: "Active" }), _jsx(Text, { size: "xs", className: "text-[var(--appkit-color-text-secondary)]", children: "Customers can apply this coupon at checkout" })] })] }) })] }), _jsx(Div, { className: "border-t border-[var(--appkit-color-border)]", padding: "inlineLg", children: _jsxs(Row, { justify: "end", gap: "3", children: [onCancel && (_jsx(Button, { type: "button", variant: "outline", onClick: onCancel, disabled: saving, children: "Cancel" })), _jsx(Button, { type: "submit", isLoading: saving, disabled: saving, children: isEdit ? "Save Changes" : "Create Coupon" })] }) })] }) }));
|
|
80
|
+
}, placeholder: "Add a category\u2026" }), draft.applicableCategories.length > 0 && (_jsx(Row, { wrap: true, gap: "sm", padding: "t-xs", children: draft.applicableCategories.map((cid) => (_jsxs(Span, { layout: "inline-flex", gap: "xs", border: "strong", padding: "pill-sm", rounded: "full", surface: "muted", color: "primary", size: "xs", children: [cid, _jsx(IconButton, { type: "button", "aria-label": `Remove ${cid}`, variant: "ghost", className: "text-[var(--appkit-color-text-muted)] hover:text-[var(--appkit-color-text)]", onClick: () => set("applicableCategories", draft.applicableCategories.filter((c) => c !== cid)), children: "\u00D7" })] }, cid))) }))] }), _jsx(Text, { size: "xs", className: "text-[var(--appkit-color-text-secondary)]", children: "Leave both empty to apply the coupon to every product in your store." })] })] }), _jsx(Row, { className: "border border-[var(--appkit-color-border)] dark:border-[var(--appkit-color-border-dark)]", padding: "inline", align: "center", gap: "3", rounded: "lg", children: _jsxs(Label, { layout: "flex", gap: "lg", className: "cursor-pointer w-full", children: [_jsx(Checkbox, { bare: true, checked: draft.isActive, onChange: (e) => set("isActive", e.target.checked), className: "h-4 w-4 rounded border-[var(--appkit-color-border)] text-[var(--appkit-color-primary)] focus:ring-[var(--appkit-color-primary)]" }), _jsxs(Div, { className: "flex-1 min-w-0", children: [_jsx(Text, { size: "sm", className: "text-[var(--appkit-color-text-primary)]", weight: "medium", children: "Active" }), _jsx(Text, { size: "xs", className: "text-[var(--appkit-color-text-secondary)]", children: "Customers can apply this coupon at checkout" })] })] }) })] }), _jsx(Div, { className: "border-t border-[var(--appkit-color-border)]", padding: "inlineLg", children: _jsxs(Row, { justify: "end", gap: "3", children: [onCancel && (_jsx(Button, { type: "button", variant: "outline", onClick: onCancel, disabled: saving, children: "Cancel" })), _jsx(Button, { type: "submit", isLoading: saving, disabled: saving, onClick: (e) => void handleSubmit(e, setFieldError), children: isEdit ? "Save Changes" : "Create Coupon" })] }) })] })) }));
|
|
81
81
|
}
|
|
@@ -5,6 +5,7 @@ import { sortBy } from "@mohasinac/appkit/client";
|
|
|
5
5
|
import React from "react";
|
|
6
6
|
import { FilterChipGroup, ListingLayout, RowActionMenu, RecordDetailModal } from "../../../ui";
|
|
7
7
|
import { QuickFormDrawer } from "../../shell/QuickFormDrawer";
|
|
8
|
+
import { OfferPhaseTimeline } from "./OfferPhaseTimeline";
|
|
8
9
|
import { counterOfferFormSchema } from "../schemas/offer-forms";
|
|
9
10
|
import { SELLER_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
10
11
|
import { SELLER_OFFER_STATUS_TABS } from "../../admin/constants/filter-tabs";
|
|
@@ -79,7 +80,7 @@ export function SellerOffersView({ children, onAcceptOffer, onRejectOffer, onCou
|
|
|
79
80
|
? [{ label: "Buyer must pay by", value: toRelativeDate(d.checkoutDeadline) }]
|
|
80
81
|
: []),
|
|
81
82
|
...(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
|
+
], extra: _jsx(OfferPhaseTimeline, { status: toStringValue(d.status, "pending"), timeline: d.timeline, timelineTruncated: d.timelineTruncated, superseded: Boolean(d.superseded), createdAt: toStringValue(d.createdAt, ""), respondedAt: toStringValue(d.respondedAt, ""), acceptedAt: toStringValue(d.acceptedAt, ""), paidAt: toStringValue(d.paidAt, ""), chain: d.chain }) }), _jsx(QuickFormDrawer, { isOpen: counterRow !== null, onClose: () => setCounterRow(null), title: `Counter offer — ${counterRow?.primary ?? ""}`, submitLabel: "Send counter", isLoading: isCountering, schema: counterOfferFormSchema, fields: [
|
|
83
84
|
{
|
|
84
85
|
name: "counterAmount",
|
|
85
86
|
label: "Your counter price (₹)",
|
|
@@ -5,7 +5,7 @@ import { z } from "zod";
|
|
|
5
5
|
import { Alert, Badge, Checkbox, Div, FormField, FormGroup, Heading, Label, Row, Stack, Text, Toggle } from "../../../ui";
|
|
6
6
|
import { StackedViewShell } from "../../../ui";
|
|
7
7
|
import { FormShellContext, useFormShellState, applyZodIssues, FormErrorSummary } from "../../../ui/forms";
|
|
8
|
-
import { SectionForm, useSectionFormNav } from "../../shell";
|
|
8
|
+
import { SectionForm, useSectionFormNav, annotate } from "../../shell";
|
|
9
9
|
import { SELLER_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
10
10
|
import { normalizeError } from "../../../errors/normalize";
|
|
11
11
|
const __P = {
|
|
@@ -16,20 +16,44 @@ const __P = {
|
|
|
16
16
|
// accountNumber field's helpText below), and the exact keep-existing
|
|
17
17
|
// semantics live server-side; a client-side format requirement risks
|
|
18
18
|
// blocking that legitimate blank-resave case.
|
|
19
|
+
/**
|
|
20
|
+
* Section membership and row span live on the FIELD (W3).
|
|
21
|
+
*
|
|
22
|
+
* The `sections` array below still hand-writes this form's JSX, because its
|
|
23
|
+
* Payout Method panel is genuinely bespoke — a configured/not-configured
|
|
24
|
+
* badge, a "current UPI/bank" alert, and a UPI-vs-bank branch. What is NOT
|
|
25
|
+
* hand-written any more is which field belongs where and how wide it is:
|
|
26
|
+
* that is read off the schema, and `audit-form-field-roundtrip` (W4) can
|
|
27
|
+
* cross-check the two so they cannot drift apart.
|
|
28
|
+
*
|
|
29
|
+
* 🛑 `annotate()` must be the OUTERMOST call on each field. It keys a WeakMap
|
|
30
|
+
* by schema instance, and every zod wrapper returns a NEW instance — so
|
|
31
|
+
* `annotate(z.string(), …).optional()` silently loses the metadata and the
|
|
32
|
+
* field falls into the "advanced" fallback section. `audit-field-ui-meta`
|
|
33
|
+
* blocks that ordering.
|
|
34
|
+
*/
|
|
19
35
|
const payoutSettingsDraftSchema = z.object({
|
|
20
|
-
method: z.enum(["upi", "bank_transfer"]),
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
36
|
+
method: annotate(z.enum(["upi", "bank_transfer"]), {
|
|
37
|
+
section: "method", sectionLabel: "Payout Method", sectionRequired: true,
|
|
38
|
+
quick: true, order: 1, row: "pair",
|
|
39
|
+
}),
|
|
40
|
+
upiId: annotate(z.string(), { section: "method", quick: true, order: 2, row: "pair" }),
|
|
41
|
+
accountHolderName: annotate(z.string(), { section: "method", quick: true, order: 3, row: "full" }),
|
|
42
|
+
// Account number and IFSC read as one unit and each needs about half the
|
|
43
|
+
// width — the pairing this whole mechanism exists to make declarative
|
|
44
|
+
// instead of a bare `grid-cols-2` that is two columns wide at 320px.
|
|
45
|
+
accountNumber: annotate(z.string(), { section: "method", quick: true, order: 4, row: "pair" }),
|
|
46
|
+
ifscCode: annotate(z.string(), { section: "method", quick: true, order: 5, row: "pair" }),
|
|
47
|
+
bankName: annotate(z.string(), { section: "method", quick: true, order: 6, row: "pair" }),
|
|
48
|
+
accountType: annotate(z.enum(["savings", "current"]), { section: "method", quick: true, order: 7, row: "pair" }),
|
|
49
|
+
gstin: annotate(z.string(), { section: "tax", sectionLabel: "Tax Info", order: 1, row: "pair" }),
|
|
50
|
+
pan: annotate(z.string(), { section: "tax", order: 2, row: "pair" }),
|
|
51
|
+
businessType: annotate(z.string(), { section: "tax", order: 3, row: "pair" }),
|
|
52
|
+
autoPayout: annotate(z.boolean(), {
|
|
53
|
+
section: "preferences", sectionLabel: "Preferences", order: 1, row: "quarter",
|
|
54
|
+
}),
|
|
55
|
+
minimumThreshold: annotate(z.string(), { section: "preferences", order: 2, row: "pair" }),
|
|
56
|
+
emiEnabled: annotate(z.boolean(), { section: "preferences", order: 3, row: "quarter" }),
|
|
33
57
|
}).superRefine((v, ctx) => {
|
|
34
58
|
if (v.method === "upi") {
|
|
35
59
|
if (!v.upiId.trim()) {
|
|
@@ -7,6 +7,17 @@
|
|
|
7
7
|
import { BaseRepository, parseSieveDateValue } from "../../../providers/db-firebase";
|
|
8
8
|
import type { SieveModel, FirebaseSieveResult } from "../../../providers/db-firebase";
|
|
9
9
|
import type { OfferDocument, OfferCreateInput, OfferUpdateInput } from "../schemas";
|
|
10
|
+
import { type HistoryActor } from "../../../_internal/shared/history/index";
|
|
11
|
+
/**
|
|
12
|
+
* Who/why for a write that lands in an offer's `statusHistory`.
|
|
13
|
+
* Optional throughout so existing callers keep compiling and record `system`.
|
|
14
|
+
*/
|
|
15
|
+
export interface OfferWriteContext {
|
|
16
|
+
actor?: HistoryActor;
|
|
17
|
+
trigger?: string;
|
|
18
|
+
reason?: string;
|
|
19
|
+
note?: string;
|
|
20
|
+
}
|
|
10
21
|
declare class OfferRepository extends BaseRepository<OfferDocument> {
|
|
11
22
|
constructor();
|
|
12
23
|
/** Override mapDoc to auto-decrypt PII on every offer read */
|
|
@@ -107,12 +118,32 @@ declare class OfferRepository extends BaseRepository<OfferDocument> {
|
|
|
107
118
|
* `product.updatedAt` advances past the previously-created offer dates.
|
|
108
119
|
*/
|
|
109
120
|
countByBuyerAndProduct(buyerUid: string, productId: string, since: Date): Promise<number>;
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
121
|
+
/**
|
|
122
|
+
* The single write choke point for offers, so `statusHistory` is appended
|
|
123
|
+
* in ONE place rather than at every call site — the same shape
|
|
124
|
+
* `OrderRepository.updateWithHistory` uses.
|
|
125
|
+
*
|
|
126
|
+
* `prior` is the already-fetched document when the caller has one. Every
|
|
127
|
+
* real caller does (`respondToOffer`, `acceptCounterOffer`, `withdrawOffer`,
|
|
128
|
+
* `counterOfferByBuyer`, `finalizeLockedLines`, the admin PATCH, both expiry
|
|
129
|
+
* sweeps), so threading it through means history costs **zero extra reads**.
|
|
130
|
+
*/
|
|
131
|
+
updateStatus(offerId: string, patch: OfferUpdateInput, ctx?: OfferWriteContext, prior?: OfferDocument | null): Promise<OfferDocument>;
|
|
132
|
+
accept(offerId: string, lockedPrice: number, sellerNote?: string, checkoutDeadline?: Date, ctx?: OfferWriteContext, prior?: OfferDocument | null): Promise<OfferDocument>;
|
|
133
|
+
decline(offerId: string, sellerNote?: string, ctx?: OfferWriteContext, prior?: OfferDocument | null): Promise<OfferDocument>;
|
|
134
|
+
counter(offerId: string, counterAmount: number, sellerNote?: string, ctx?: OfferWriteContext, prior?: OfferDocument | null): Promise<OfferDocument>;
|
|
135
|
+
acceptCounter(offerId: string, checkoutDeadline?: Date, ctx?: OfferWriteContext, prior?: OfferDocument | null): Promise<OfferDocument>;
|
|
136
|
+
/**
|
|
137
|
+
* Two distinct events share this method:
|
|
138
|
+
* - the buyer walks away → status alone
|
|
139
|
+
* - the buyer counters → status + `supersededByOfferId`
|
|
140
|
+
*
|
|
141
|
+
* The second is what makes the chain walkable forward, and it is ONE write
|
|
142
|
+
* with ONE history entry. A `withdrawn` document carrying
|
|
143
|
+
* `supersededByOfferId` renders as *Superseded* (neutral) rather than
|
|
144
|
+
* *Withdrawn* (negative) — the negotiation continued, it did not end.
|
|
145
|
+
*/
|
|
146
|
+
withdraw(offerId: string, supersededByOfferId?: string, ctx?: OfferWriteContext, prior?: OfferDocument | null): Promise<OfferDocument>;
|
|
116
147
|
/**
|
|
117
148
|
* Terminal transition, called once the buyer's order for this offer exists.
|
|
118
149
|
*
|
|
@@ -121,7 +152,21 @@ declare class OfferRepository extends BaseRepository<OfferDocument> {
|
|
|
121
152
|
* client-side patch in UserOffersPanel, which meant a reload showed the offer
|
|
122
153
|
* back at "accepted" and it could be added to the cart and ordered again.
|
|
123
154
|
*/
|
|
124
|
-
markPaid(offerId: string, orderId: string): Promise<OfferDocument>;
|
|
155
|
+
markPaid(offerId: string, orderId: string, prior?: OfferDocument | null): Promise<OfferDocument>;
|
|
156
|
+
/**
|
|
157
|
+
* Every round of one negotiation, oldest first.
|
|
158
|
+
*
|
|
159
|
+
* A single-field equality — served by Firestore's AUTOMATIC index, so this
|
|
160
|
+
* adds no composite index and none of the existing `offers` indexes is
|
|
161
|
+
* affected. The sort is done in memory because the result is at most 3 rows
|
|
162
|
+
* (the per-buyer offer limit), which is cheaper than the index a compound
|
|
163
|
+
* orderBy would demand.
|
|
164
|
+
*
|
|
165
|
+
* Offers written before `chainRootOfferId` existed return ZERO rows, not
|
|
166
|
+
* one — callers must fall back to `[offer]` and never read empty as
|
|
167
|
+
* "this offer is missing".
|
|
168
|
+
*/
|
|
169
|
+
findChain(chainRootOfferId: string): Promise<OfferDocument[]>;
|
|
125
170
|
/**
|
|
126
171
|
* Cloud Functions compatibility: pending/countered offers already expired.
|
|
127
172
|
*/
|
|
@@ -133,7 +178,23 @@ declare class OfferRepository extends BaseRepository<OfferDocument> {
|
|
|
133
178
|
* price remained claimable long after the seller expected it to lapse.
|
|
134
179
|
*/
|
|
135
180
|
findExpiredAccepted(now: Date): Promise<OfferDocument[]>;
|
|
136
|
-
|
|
181
|
+
/**
|
|
182
|
+
* Bulk-expire, taking the DOCUMENTS rather than their ids.
|
|
183
|
+
*
|
|
184
|
+
* The signature changed deliberately. `arrayUnion` would let a batch append
|
|
185
|
+
* without a read, but it **cannot enforce the FIFO cap** — so history would
|
|
186
|
+
* grow unbounded on exactly the path that touches the most documents at
|
|
187
|
+
* once. Folding entries in memory and writing whole arrays keeps the cap,
|
|
188
|
+
* and costs **zero extra reads**: `findExpiredActive`, `findExpiredAccepted`
|
|
189
|
+
* and the admin cancel route all already hold full `OfferDocument`s.
|
|
190
|
+
*
|
|
191
|
+
* Also carries the admin path: a reasoned cancel is an expiry with an actor
|
|
192
|
+
* and a reason, recorded in the offer's own history as well as in
|
|
193
|
+
* `adminAuditLog`.
|
|
194
|
+
*/
|
|
195
|
+
expireMany(offers: OfferDocument[], ctx?: OfferWriteContext & {
|
|
196
|
+
cancelledByAdminUid?: string;
|
|
197
|
+
}): Promise<void>;
|
|
137
198
|
}
|
|
138
199
|
export declare const offerRepository: OfferRepository;
|
|
139
200
|
export { OfferRepository };
|