@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.
Files changed (157) hide show
  1. package/dist/_internal/server/features/auctions/actions.js +2 -2
  2. package/dist/_internal/server/features/cart/service.d.ts +3 -3
  3. package/dist/_internal/server/features/checkout/actions.js +19 -0
  4. package/dist/_internal/server/features/checkout/source-context.d.ts +5 -0
  5. package/dist/_internal/server/features/checkout/source-context.js +157 -0
  6. package/dist/_internal/server/features/offers/adapters.d.ts +103 -0
  7. package/dist/_internal/server/features/offers/adapters.js +88 -0
  8. package/dist/_internal/server/features/offers/index.d.ts +2 -0
  9. package/dist/_internal/server/features/offers/index.js +1 -0
  10. package/dist/_internal/server/features/orders/adapters.js +24 -0
  11. package/dist/_internal/server/jobs/core/offerExpiry.js +10 -10
  12. package/dist/_internal/shared/actions/action-registry.js +3 -1
  13. package/dist/_internal/shared/features/auctions/schema.d.ts +14 -2
  14. package/dist/_internal/shared/features/auctions/schema.js +13 -1
  15. package/dist/_internal/shared/features/cart/schema.d.ts +14 -9
  16. package/dist/_internal/shared/features/cart/schema.js +15 -0
  17. package/dist/_internal/shared/features/products/schema.d.ts +2 -2
  18. package/dist/_internal/shared/history/history.d.ts +22 -0
  19. package/dist/_internal/shared/history/history.js +41 -1
  20. package/dist/client.d.ts +13 -2
  21. package/dist/client.js +20 -2
  22. package/dist/constants/field-names.d.ts +8 -6
  23. package/dist/constants/field-names.js +48 -6
  24. package/dist/features/account/components/UserBidsView.js +12 -4
  25. package/dist/features/account/components/UserReturnsView.d.ts +3 -0
  26. package/dist/features/account/components/UserReturnsView.js +17 -1
  27. package/dist/features/admin/components/AdminAuditLogView.js +1 -0
  28. package/dist/features/admin/components/AdminBidsView.js +15 -2
  29. package/dist/features/admin/components/AdminBlogEditorView.js +65 -33
  30. package/dist/features/admin/components/AdminFeatureEditorView.js +35 -6
  31. package/dist/features/admin/components/AdminNavEditorView.js +15 -5
  32. package/dist/features/admin/components/AdminNewsletterView.js +18 -2
  33. package/dist/features/admin/components/AdminOffersView.js +62 -18
  34. package/dist/features/admin/components/AdminPaymentMethodsView.js +24 -4
  35. package/dist/features/admin/components/AdminScammerEditorView.d.ts +2 -1
  36. package/dist/features/admin/components/AdminScammerEditorView.js +3 -13
  37. package/dist/features/admin/components/AdminScammersView.js +3 -8
  38. package/dist/features/admin/components/AdminSectionsView.js +32 -23
  39. package/dist/features/admin/components/AdminSessionsView.js +17 -2
  40. package/dist/features/admin/components/ViewAuditLogEntryModal.js +1 -0
  41. package/dist/features/admin/components/sections/adminSectionsTypes.d.ts +4 -2
  42. package/dist/features/admin/components/sections/adminSectionsTypes.js +45 -27
  43. package/dist/features/admin/constants/filter-tabs.d.ts +3 -0
  44. package/dist/features/admin/constants/filter-tabs.js +1 -0
  45. package/dist/features/admin/schemas/homepage-section-form.d.ts +22 -0
  46. package/dist/features/admin/schemas/homepage-section-form.js +68 -0
  47. package/dist/features/admin/schemas/nav-item-form.d.ts +74 -0
  48. package/dist/features/admin/schemas/nav-item-form.js +72 -0
  49. package/dist/features/auctions/components/AuctionBidsTable.d.ts +10 -1
  50. package/dist/features/auctions/components/AuctionBidsTable.js +22 -4
  51. package/dist/features/auctions/components/bid-detail-fields.d.ts +29 -0
  52. package/dist/features/auctions/components/bid-detail-fields.js +103 -0
  53. package/dist/features/audit-log/schemas/firestore.d.ts +14 -0
  54. package/dist/features/audit-log/schemas/firestore.js +7 -0
  55. package/dist/features/blog/schemas/blog-form.d.ts +80 -0
  56. package/dist/features/blog/schemas/blog-form.js +100 -0
  57. package/dist/features/events/components/AdminEventEditorView.js +32 -54
  58. package/dist/features/events/components/AdminEventEntriesView.js +22 -3
  59. package/dist/features/events/schemas/event-form.d.ts +113 -0
  60. package/dist/features/events/schemas/event-form.js +82 -0
  61. package/dist/features/grouped/schemas/grouped-listing-form.d.ts +101 -0
  62. package/dist/features/grouped/schemas/grouped-listing-form.js +89 -0
  63. package/dist/features/homepage/actions/homepage-section-actions.d.ts +11 -3
  64. package/dist/features/homepage/actions/homepage-section-actions.js +33 -1
  65. package/dist/features/homepage/schemas/index.d.ts +31 -0
  66. package/dist/features/homepage/schemas/index.js +24 -0
  67. package/dist/features/orders/components/OrderStatusTimeline.d.ts +5 -1
  68. package/dist/features/orders/components/OrderStatusTimeline.js +39 -20
  69. package/dist/features/orders/repository/orders.repository.js +5 -0
  70. package/dist/features/orders/schemas/firestore.d.ts +36 -1
  71. package/dist/features/orders/types/index.d.ts +35 -7
  72. package/dist/features/products/components/GroupSettingsPanel.js +5 -1
  73. package/dist/features/products/schemas/index.d.ts +2 -2
  74. package/dist/features/products/schemas/product-features.validators.d.ts +73 -0
  75. package/dist/features/products/schemas/product-features.validators.js +63 -0
  76. package/dist/features/scams/constants/scammer-status-display.d.ts +28 -0
  77. package/dist/features/scams/constants/scammer-status-display.js +79 -0
  78. package/dist/features/seller/actions/offer-actions.js +32 -2
  79. package/dist/features/seller/components/OfferPhaseTimeline.d.ts +59 -0
  80. package/dist/features/seller/components/OfferPhaseTimeline.js +107 -0
  81. package/dist/features/seller/components/SellerBidsView.js +11 -4
  82. package/dist/features/seller/components/SellerCouponEditorView.js +17 -17
  83. package/dist/features/seller/components/SellerOffersView.js +2 -1
  84. package/dist/features/seller/components/SellerPayoutSettingsView.js +38 -14
  85. package/dist/features/seller/repository/offer.repository.d.ts +69 -8
  86. package/dist/features/seller/repository/offer.repository.js +127 -23
  87. package/dist/features/seller/schemas/coupon-form.d.ts +72 -0
  88. package/dist/features/seller/schemas/coupon-form.js +116 -0
  89. package/dist/features/seller/schemas/firestore.d.ts +56 -2
  90. package/dist/features/seller/schemas/firestore.js +27 -0
  91. package/dist/features/seller/schemas/offer-forms.d.ts +21 -0
  92. package/dist/features/seller/schemas/offer-forms.js +21 -0
  93. package/dist/features/shell/build-sections.d.ts +63 -0
  94. package/dist/features/shell/build-sections.js +207 -0
  95. package/dist/features/shell/field-ui-meta.d.ts +115 -0
  96. package/dist/features/shell/field-ui-meta.js +324 -0
  97. package/dist/features/shell/index.d.ts +4 -0
  98. package/dist/features/shell/index.js +2 -0
  99. package/dist/features/status-history/components/StatusTimeline.d.ts +74 -0
  100. package/dist/features/status-history/components/StatusTimeline.js +46 -0
  101. package/dist/features/status-history/index.d.ts +11 -0
  102. package/dist/features/status-history/index.js +10 -0
  103. package/dist/features/store-extensions/components/ReviewDecisionModal.d.ts +27 -0
  104. package/dist/features/store-extensions/components/ReviewDecisionModal.js +82 -0
  105. package/dist/features/store-extensions/index.d.ts +12 -0
  106. package/dist/features/store-extensions/index.js +12 -0
  107. package/dist/features/store-extensions/schemas/admin-notification-form.d.ts +36 -0
  108. package/dist/features/store-extensions/schemas/admin-notification-form.js +114 -0
  109. package/dist/features/store-extensions/schemas/analytics-forms.d.ts +57 -0
  110. package/dist/features/store-extensions/schemas/analytics-forms.js +158 -0
  111. package/dist/features/store-extensions/schemas/custom-role-form.d.ts +85 -0
  112. package/dist/features/store-extensions/schemas/custom-role-form.js +118 -0
  113. package/dist/features/store-extensions/schemas/firestore.d.ts +21 -1
  114. package/dist/features/store-extensions/schemas/firestore.js +60 -0
  115. package/dist/features/store-extensions/schemas/google-config-form.d.ts +53 -0
  116. package/dist/features/store-extensions/schemas/google-config-form.js +105 -0
  117. package/dist/features/store-extensions/schemas/item-request-create-form.d.ts +28 -0
  118. package/dist/features/store-extensions/schemas/item-request-create-form.js +80 -0
  119. package/dist/features/store-extensions/schemas/listing-template-form.d.ts +78 -0
  120. package/dist/features/store-extensions/schemas/listing-template-form.js +87 -0
  121. package/dist/features/store-extensions/schemas/moderation-review-form.d.ts +49 -0
  122. package/dist/features/store-extensions/schemas/moderation-review-form.js +101 -0
  123. package/dist/features/store-extensions/schemas/payout-method-form.d.ts +153 -0
  124. package/dist/features/store-extensions/schemas/payout-method-form.js +155 -0
  125. package/dist/features/store-extensions/schemas/report-create-form.d.ts +25 -0
  126. package/dist/features/store-extensions/schemas/report-create-form.js +79 -0
  127. package/dist/features/store-extensions/schemas/report-review-form.d.ts +47 -0
  128. package/dist/features/store-extensions/schemas/report-review-form.js +111 -0
  129. package/dist/features/store-extensions/schemas/shipping-config-form.d.ts +148 -0
  130. package/dist/features/store-extensions/schemas/shipping-config-form.js +104 -0
  131. package/dist/features/store-extensions/schemas/store-category-form.d.ts +54 -0
  132. package/dist/features/store-extensions/schemas/store-category-form.js +73 -0
  133. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +326 -0
  134. package/dist/index.d.ts +12 -2
  135. package/dist/index.js +12 -2
  136. package/dist/next/routing/route-map.d.ts +16 -2
  137. package/dist/next/routing/route-map.js +9 -2
  138. package/dist/schemas/firestore-value.d.ts +8 -0
  139. package/dist/schemas/firestore-value.js +44 -0
  140. package/dist/schemas/index.d.ts +1 -1
  141. package/dist/schemas/index.js +1 -1
  142. package/dist/schemas/registry.d.ts +634 -1
  143. package/dist/schemas/registry.js +64 -1
  144. package/dist/seed/bids-seed-data.js +30 -0
  145. package/dist/seed/offers-seed-data.js +212 -7
  146. package/dist/seed/orders-seed-data.js +336 -1
  147. package/dist/seed/products-auctions-seed-data.js +41 -0
  148. package/dist/server.d.ts +2 -0
  149. package/dist/server.js +5 -0
  150. package/dist/styles.css +1 -1
  151. package/dist/tailwind-utilities.css +1 -1
  152. package/dist/ui/components/RecordDetailModal.d.ts +10 -1
  153. package/dist/ui/components/RecordDetailModal.js +2 -2
  154. package/package.json +2 -1
  155. package/scripts/node-esm-loader.mjs +65 -0
  156. package/scripts/seed-cli-loader.mjs +6 -49
  157. 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 }) }))] }, group.id));
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
- return _jsx(DataListingView, { config: config });
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 (!draft.code.trim()) {
45
- setError("Coupon code is required");
46
- return;
47
- }
48
- if (draft.type !== "free_shipping" && !draft.value) {
49
- setError("Discount value is required");
50
- return;
51
- }
52
- if (!draft.startDate || !draft.endDate) {
53
- setError("Start and end dates are required");
54
- return;
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, { onSubmit: handleSubmit, noValidate: true, children: _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: [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"
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
- upiId: z.string(),
22
- accountHolderName: z.string(),
23
- accountNumber: z.string(),
24
- ifscCode: z.string(),
25
- bankName: z.string(),
26
- accountType: z.enum(["savings", "current"]),
27
- gstin: z.string(),
28
- pan: z.string(),
29
- businessType: z.string(),
30
- autoPayout: z.boolean(),
31
- minimumThreshold: z.string(),
32
- emiEnabled: z.boolean(),
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
- updateStatus(offerId: string, patch: OfferUpdateInput): Promise<OfferDocument>;
111
- accept(offerId: string, lockedPrice: number, sellerNote?: string, checkoutDeadline?: Date): Promise<OfferDocument>;
112
- decline(offerId: string, sellerNote?: string): Promise<OfferDocument>;
113
- counter(offerId: string, counterAmount: number, sellerNote?: string): Promise<OfferDocument>;
114
- acceptCounter(offerId: string, checkoutDeadline?: Date): Promise<OfferDocument>;
115
- withdraw(offerId: string): Promise<OfferDocument>;
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
- expireMany(offerIds: string[]): Promise<void>;
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 };