@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
@@ -95,6 +95,11 @@ class OrderRepository extends BaseRepository {
95
95
  reason: ctx?.reason,
96
96
  note: ctx?.note,
97
97
  extraChanges: ctx?.extraChanges,
98
+ // `encryptPiiFields` is a flat top-level loop and never descends into
99
+ // arrays, so anything PII-named reaching `changes` would persist in
100
+ // PLAINTEXT inside statusHistory. None of ORDER_TRACKED_FIELDS is PII
101
+ // today — this is defence for the next field somebody adds.
102
+ piiFields: ORDER_PII_FIELDS,
98
103
  historyField: "statusHistory",
99
104
  truncatedField: "statusHistoryTruncated",
100
105
  });
@@ -24,12 +24,39 @@ export interface OrderSourceAuctionWon {
24
24
  /** Second-highest bid, when there was one. */
25
25
  runnerUpAmount?: number;
26
26
  }
27
+ /**
28
+ * A buyout is a REAL BID, not a bid-free purchase.
29
+ *
30
+ * An earlier draft of this type asserted "zero bids BY DEFINITION, because
31
+ * buy-now is only valid while `!bidsHaveStarted`". That gate no longer
32
+ * exists: `buyNowAuction` places an actual `BidDocument` (`isBuyout: true`)
33
+ * plus a 1h locked cart line and leaves the auction **live**, and Buy Now is
34
+ * offered whenever `buyNowPrice` still beats the standing bid. Recording
35
+ * `bidCount: 0` would bake a falsehood into an immutable audit record.
36
+ *
37
+ * `standingBidAtBuyout` is the highest NON-buyout bid the buyout beat — read
38
+ * from the bids rather than `product.currentBid`, because a pending buyout
39
+ * deliberately never writes to that mirror.
40
+ *
41
+ * An unpaid buyout lapses to **`cancelled`**, not `forfeited`, and the seller
42
+ * is deliberately not notified: nothing was ever sold. That makes its
43
+ * timeline materially different from a lapsed auction win.
44
+ */
27
45
  export interface OrderSourceAuctionBuyNow {
28
46
  path: "auction-buy-now";
29
47
  auctionId: string;
48
+ /** The buyout bid itself — a buyout IS a bid. */
49
+ bidId: string;
30
50
  buyNowPrice: number;
31
51
  listPrice: number;
52
+ /** Highest non-buyout bid at the moment of buyout. 0 when nobody had bid. */
53
+ standingBidAtBuyout: number;
54
+ /** Active non-buyout bids at buyout time. NOT always zero. */
55
+ bidCount: number;
32
56
  boughtAt: Date;
57
+ /** `min(now + AUCTION_BUYOUT_WINDOW_MS, auctionEndDate)`. */
58
+ checkoutDeadline: Date;
59
+ auctionEndDate?: Date;
33
60
  }
34
61
  export interface OrderSourceOfferAccepted {
35
62
  path: "offer-accepted";
@@ -38,7 +65,15 @@ export interface OrderSourceOfferAccepted {
38
65
  /** The asking price at the moment the offer was made. */
39
66
  listPriceAtOffer: number;
40
67
  counterAmount?: number;
41
- /** How many counter rounds before agreement. 0 = accepted outright. */
68
+ /**
69
+ * How many counter rounds before agreement. 1 = accepted outright.
70
+ *
71
+ * Read straight off the accepted offer's `counterRound`, which W2 made a
72
+ * persisted field. Before that this number was **unknowable**: each counter
73
+ * created an unlinked document and `previousOfferId` lived only inside a
74
+ * `serverLogger.info` call, so a three-round negotiation left no trace of
75
+ * having been one.
76
+ */
42
77
  counterRounds: number;
43
78
  acceptedBy: string;
44
79
  acceptedAt: Date;
@@ -2,7 +2,8 @@ import { type PaymentGateway, PaymentGatewayValues } from "../../payments/schema
2
2
  export { type PaymentGateway, PaymentGatewayValues };
3
3
  import type { ListingType } from "../../products/types/index";
4
4
  import type { OrderType } from "../utils/order-splitter";
5
- import type { AppliedOrderDiscount } from "../schemas/firestore";
5
+ import type { AppliedOrderDiscount, OrderSourceContext } from "../schemas/firestore";
6
+ import type { FieldChange } from "../../../_internal/shared/history/index";
6
7
  export type PaymentStatus = "pending" | "processing" | "paid" | "failed" | "refunded" | "partial_refund";
7
8
  export interface UserAddress {
8
9
  id: string;
@@ -46,12 +47,34 @@ export interface OrderItem {
46
47
  /** Snapshot of bundle.bundleProductIds at order-creation time. */
47
48
  bundleProductIds?: string[];
48
49
  }
49
- export interface OrderTimeline {
50
- status: OrderStatus;
51
- message?: string;
52
- timestamp: string;
53
- actor?: string;
50
+ /**
51
+ * One entry on an order's timeline — the client-facing shape of
52
+ * `StatusChangeEntry`, with dates as ISO strings.
53
+ *
54
+ * The previous declaration here was `{status, message?, timestamp, actor?}`
55
+ * and had **zero writers** in the entire codebase since the day it was
56
+ * added, so nothing depended on it. `OrderStatusTimeline` synthesised its
57
+ * steps from four scalar date fields instead, which meant any transition
58
+ * without a dedicated date field — payment reviewed, proof re-upload asked
59
+ * for, a partial refund posted — could not appear at all.
60
+ */
61
+ export interface OrderTimelineEntry {
62
+ at: string;
63
+ actorUid?: string;
64
+ actorRole: "buyer" | "seller" | "admin" | "system";
65
+ /**
66
+ * Delta only: `{ status: { from: "confirmed", to: "shipped" } }`.
67
+ * Reuses the primitive's `FieldChange` rather than restating the shape, so
68
+ * the client type cannot drift from what the repository writes.
69
+ */
70
+ changes: Record<string, FieldChange>;
71
+ reason?: string;
72
+ note?: string;
73
+ /** The function or job that caused it — what the timeline renders by. */
74
+ trigger: string;
54
75
  }
76
+ /** @deprecated Use {@link OrderTimelineEntry}. Kept only to avoid breaking a stale import. */
77
+ export type OrderTimeline = OrderTimelineEntry;
55
78
  export interface Order {
56
79
  id: string;
57
80
  userId: string;
@@ -108,7 +131,12 @@ export interface Order {
108
131
  /** Carrier tracking page URL — rendered as an external link that opens in a new tab. No live-tracking API integration; this is just the passthrough URL a seller/admin entered. */
109
132
  trackingUrl?: string;
110
133
  notes?: string;
111
- timeline?: OrderTimeline[];
134
+ /** Real recorded transitions — see {@link OrderTimelineEntry}. */
135
+ timeline?: OrderTimelineEntry[];
136
+ /** Entries trimmed off the front of the capped history, if any. */
137
+ timelineTruncated?: number;
138
+ /** How the buyer acquired the right to buy. Written once, at creation. */
139
+ sourceContext?: OrderSourceContext;
112
140
  physicalLocation?: {
113
141
  zone: string;
114
142
  shelf: string;
@@ -235,7 +235,11 @@ export function GroupSettingsPanel({ productId, productSlug, groupId, isGroupPar
235
235
  setOpen(next);
236
236
  if (next && isGroupParent && children === null)
237
237
  loadChildren();
238
- }, paddingX: "none", paddingY: "xs", justify: "between", className: "w-full text-left group", "aria-expanded": open, children: [_jsx(Heading, { level: 3, size: "sm", weight: "semibold", color: "muted", children: "Group Settings" }), _jsx(Span, { size: "xs", className: "group-hover:text-[var(--appkit-color-text-muted)] dark:group-hover:text-[var(--appkit-color-text-faint)] transition-colors", color: "faint", children: open ? "▲" : "▼" })] }), open && (_jsxs(Stack, { gap: "sm", className: `mt-3 ${__P.p4}`, surface: "muted", rounded: "lg", border: "default", children: [!groupId && !groupParentSlug && (_jsxs(Stack, { gap: "xs", children: [_jsx(Text, { size: "xs", color: "muted", children: "Group related listings together \u2014 e.g. a set, bundle, or multi-part item. Parts can be sold individually but shown together." }), _jsx(Button, { type: "button", variant: "secondary", size: "sm", onClick: startGroup, isLoading: loading, children: "Start a group" })] })), isGroupParent && groupId && (_jsxs(Stack, { gap: "md", children: [_jsxs(FormShellContext.Provider, { value: titleShellCtx, children: [_jsxs(Row, { align: "start", gap: "sm", wrap: true, children: [_jsx(Div, { className: "flex-1 min-w-[200px]", children: _jsx(FormField, { name: "groupTitle", label: "Group title", type: "text", value: editTitle, onChange: setEditTitle, placeholder: "e.g. Human Toy Complete Set" }) }), _jsx(Div, { padding: "t-lg", children: _jsx(Button, { type: "button", variant: "secondary", size: "sm", onClick: saveTitle, isLoading: loading, children: "Save title" }) })] }), _jsx(FormErrorSummary, {})] }), _jsxs(Div, { children: [_jsxs(Text, { className: "mb-2", color: "muted", size: "xs", weight: "semibold", children: ["Members (", childSlugsCount + 1, " \u2014 including this listing)"] }), loading && !children ? (_jsx(Text, { size: "xs", color: "faint", children: "Loading\u2026" })) : (_jsxs(Div, { className: "divide-y divide-zinc-100 divide-[var(--appkit-color-border-subtle)]", children: [_jsxs(Row, { align: "center", gap: "sm", padding: "y-xs", children: [_jsx(Span, { padding: "pill-2xs", weight: "semibold", className: "bg-[var(--appkit-color-primary)]/10 text-[var(--appkit-color-primary)] text-[10px]", rounded: "default", children: "Parent" }), _jsx(Text, { className: "flex-1", color: "primary", size: "sm", children: productSlug })] }), (children ?? []).filter((c) => c.id !== productId).map((child) => (_jsxs(Row, { align: "center", gap: "sm", padding: "y-xs", children: [child.images?.[0] ? (_jsx(Div, { className: "relative w-8 h-8 flex-shrink-0 border border-[var(--appkit-color-border)]", rounded: "full", overflow: "hidden", children: _jsx(MediaImage, { src: child.images[0], alt: child.title, size: "thumbnail" }) })) : (_jsx(Div, { className: "w-8 h-8", surface: "subtle", rounded: "full" })), _jsx(Text, { className: "flex-1 truncate", color: "primary", size: "sm", children: child.title }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => requestUnlinkChild(child.id), isLoading: loading, className: `${CLS_DELETE_LINK} text-[length:var(--appkit-text-xs)]`, children: "Unlink" })] }, child.id)))] }))] }), _jsxs(Row, { gap: "sm", wrap: true, children: [_jsx(Button, { type: "button", variant: "secondary", size: "sm", onClick: () => setShowAddModal(true), children: "Add child listing" }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: requestDissolveGroup, isLoading: loading, className: CLS_DELETE_LINK, children: "Dissolve group" })] })] })), !isGroupParent && groupParentSlug && (_jsxs(Stack, { gap: "xs", children: [_jsxs(Text, { size: "sm", color: "muted", children: ["Part of:", " ", _jsx(Span, { weight: "medium", color: "primary", children: groupTitle ?? groupParentSlug })] }), _jsxs(Text, { size: "xs", color: "muted", children: ["Parent listing: ", _jsx(Code, { className: "font-mono", children: groupParentSlug })] }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: requestLeaveGroup, isLoading: loading, className: `${CLS_DELETE_LINK} w-fit`, children: "Leave group" })] }))] })), (groupChildSlugs?.length ?? 0) >= 4 ? (_jsx(SideDrawer, { isOpen: showAddModal, onClose: () => setShowAddModal(false), title: "Add child listing", children: _jsx(AddChildContent, { addTab: addTab, setAddTab: setAddTab, createForm: createForm, setCreateForm: setCreateForm, productSlug: productSlug, storeProductsEndpoint: storeProductsEndpoint, linkTargets: linkTargets, setLinkTargets: setLinkTargets, onAddCreate: addCreateChild, onAddLink: addLinkChild, loading: loading }) })) : (_jsx(Modal, { open: showAddModal, onClose: () => setShowAddModal(false), title: "Add child listing", size: "lg", children: _jsx(AddChildContent, { addTab: addTab, setAddTab: setAddTab, createForm: createForm, setCreateForm: setCreateForm, productSlug: productSlug, storeProductsEndpoint: storeProductsEndpoint, linkTargets: linkTargets, setLinkTargets: setLinkTargets, onAddCreate: addCreateChild, onAddLink: addLinkChild, loading: loading }) })), confirmAction && (_jsx(ConfirmDeleteModal, { isOpen: true, title: confirmAction.title, message: confirmAction.message, onConfirm: confirmAction.onConfirm, onClose: () => setConfirmAction(null), isDeleting: loading }))] }));
238
+ }, paddingX: "none", paddingY: "xs", justify: "between", className: "w-full text-left group", "aria-expanded": open, children: [_jsx(Heading, { level: 3, size: "sm", weight: "semibold", color: "muted", children: "Group Settings" }), _jsx(Span, { size: "xs", className: "group-hover:text-[var(--appkit-color-text-muted)] dark:group-hover:text-[var(--appkit-color-text-faint)] transition-colors", color: "faint", children: open ? "▲" : "▼" })] }), open && (_jsxs(Stack, { gap: "sm", className: `mt-3 ${__P.p4}`, surface: "muted", rounded: "lg", border: "default", children: [!groupId && !groupParentSlug && (_jsxs(Stack, { gap: "xs", children: [_jsx(Text, { size: "xs", color: "muted", children: "Group related listings together \u2014 e.g. a set, bundle, or multi-part item. Parts can be sold individually but shown together." }), _jsx(Button, { type: "button", variant: "secondary", size: "sm", onClick: startGroup, isLoading: loading, children: "Start a group" })] })), isGroupParent && groupId && (_jsxs(Stack, { gap: "md", children: [_jsxs(FormShellContext.Provider, { value: titleShellCtx, children: [_jsxs(Row, { align: "start", gap: "sm", wrap: true, children: [_jsx(Div, { className: "flex-1 min-w-[200px]", children: _jsx(FormField, { name: "groupTitle", label: "Group title", type: "text", value: editTitle, onChange: setEditTitle, placeholder: "e.g. Human Toy Complete Set" }) }), _jsx(Div, { padding: "t-lg", children: _jsx(Button, { type: "button", variant: "secondary", size: "sm", onClick: saveTitle, isLoading: loading, children: "Save title" }) })] }), _jsx(FormErrorSummary, {})] }), _jsxs(Div, { children: [_jsxs(Text, { className: "mb-2", color: "muted", size: "xs", weight: "semibold", children: ["Members (", childSlugsCount + 1, " \u2014 including this listing)"] }), loading && !children ? (_jsx(Text, { size: "xs", color: "faint", children: "Loading\u2026" })) : (_jsxs(Div, { className: "divide-y divide-zinc-100 divide-[var(--appkit-color-border-subtle)]", children: [_jsxs(Row, { align: "center", gap: "sm", padding: "y-xs", children: [_jsx(Span, { padding: "pill-2xs", weight: "semibold", className: "bg-[var(--appkit-color-primary)]/10 text-[var(--appkit-color-primary)] text-[10px]", rounded: "default", children: "Parent" }), _jsx(Text, { className: "flex-1", color: "primary", size: "sm", children: productSlug })] }), (children ?? []).filter((c) => c.id !== productId).map((child) => (_jsxs(Row, { align: "center", gap: "sm", padding: "y-xs", children: [child.images?.[0] ? (_jsx(Div, { className: "relative w-8 h-8 flex-shrink-0 border border-[var(--appkit-color-border)]", rounded: "full", overflow: "hidden", children: _jsx(MediaImage, { src: child.images[0], alt: child.title, size: "thumbnail" }) })) : (_jsx(Div, { className: "w-8 h-8", surface: "subtle", rounded: "full" })), _jsx(Text, { className: "flex-1 truncate", color: "primary", size: "sm", children: child.title }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => requestUnlinkChild(child.id), isLoading: loading, className: `${CLS_DELETE_LINK} text-[length:var(--appkit-text-xs)]`, children: "Unlink" })] }, child.id)))] }))] }), _jsxs(Row, { gap: "sm", wrap: true, children: [_jsx(Button, { type: "button", variant: "secondary", size: "sm", onClick: () => setShowAddModal(true), children: "Add child listing" }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: requestDissolveGroup, isLoading: loading, className: CLS_DELETE_LINK, children: "Dissolve group" })] })] })), !isGroupParent && groupParentSlug && (_jsxs(Stack, { gap: "xs", children: [_jsxs(Text, { size: "sm", color: "muted", children: ["Part of:", " ", _jsx(Span, { weight: "medium", color: "primary", children: groupTitle ?? groupParentSlug })] }), _jsxs(Text, { size: "xs", color: "muted", children: ["Parent listing: ", _jsx(Code, { className: "font-mono", children: groupParentSlug })] }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: requestLeaveGroup, isLoading: loading, className: `${CLS_DELETE_LINK} w-fit`, children: "Leave group" })] }))] })), (() => {
239
+ const content = (_jsx(AddChildContent, { addTab: addTab, setAddTab: setAddTab, createForm: createForm, setCreateForm: setCreateForm, productSlug: productSlug, storeProductsEndpoint: storeProductsEndpoint, linkTargets: linkTargets, setLinkTargets: setLinkTargets, onAddCreate: addCreateChild, onAddLink: addLinkChild, loading: loading }));
240
+ const close = () => setShowAddModal(false);
241
+ return (groupChildSlugs?.length ?? 0) >= 4 ? (_jsx(SideDrawer, { isOpen: showAddModal, onClose: close, title: "Add child listing", children: content })) : (_jsx(Modal, { open: showAddModal, onClose: close, title: "Add child listing", size: "lg", children: content }));
242
+ })(), confirmAction && (_jsx(ConfirmDeleteModal, { isOpen: true, title: confirmAction.title, message: confirmAction.message, onConfirm: confirmAction.onConfirm, onClose: () => setConfirmAction(null), isDeleting: loading }))] }));
239
243
  }
240
244
  function AddChildContent({ addTab, setAddTab, createForm, setCreateForm, productSlug, storeProductsEndpoint, linkTargets, setLinkTargets, onAddCreate, onAddLink, loading, }) {
241
245
  const { shellCtx, validate } = useFormShellState(createChildSchema);
@@ -377,7 +377,7 @@ export declare const productItemSchema: z.ZodObject<{
377
377
  preOrderDepositAmount?: number | undefined;
378
378
  preOrderMaxQuantity?: number | undefined;
379
379
  preOrderCurrentCount?: number | undefined;
380
- preOrderProductionStatus?: "in_production" | "upcoming" | "ready_to_ship" | undefined;
380
+ preOrderProductionStatus?: "upcoming" | "in_production" | "ready_to_ship" | undefined;
381
381
  preOrderCancellable?: boolean | undefined;
382
382
  isPromoted?: boolean | undefined;
383
383
  pickupAddressId?: string | undefined;
@@ -516,7 +516,7 @@ export declare const productItemSchema: z.ZodObject<{
516
516
  preOrderDepositAmount?: number | undefined;
517
517
  preOrderMaxQuantity?: number | undefined;
518
518
  preOrderCurrentCount?: number | undefined;
519
- preOrderProductionStatus?: "in_production" | "upcoming" | "ready_to_ship" | undefined;
519
+ preOrderProductionStatus?: "upcoming" | "in_production" | "ready_to_ship" | undefined;
520
520
  preOrderCancellable?: boolean | undefined;
521
521
  isPromoted?: boolean | undefined;
522
522
  pickupAddressId?: string | undefined;
@@ -102,6 +102,79 @@ export declare const productFeatureUpdateSchema: z.ZodObject<{
102
102
  iconColor?: string | undefined;
103
103
  productTypes?: ("all" | "classified" | "auction" | "prize-draw" | "digital-code" | "live" | "product" | "preorder")[] | undefined;
104
104
  }>;
105
+ /**
106
+ * The FORM schema for the product-feature editor, with UI metadata.
107
+ *
108
+ * `AdminFeatureEditorView` had **no validation whatsoever** before W5 — ten
109
+ * `useState` fields, `required` on the inputs (an HTML attribute any
110
+ * programmatic submit bypasses), and nothing else. Meanwhile these validators
111
+ * had existed all along and simply were not wired: the "schema exists, the
112
+ * consumer ignores it" case the plan catalogues.
113
+ *
114
+ * It reuses `productFeatureAdminCreateSchema`'s field shapes rather than
115
+ * restating them — a second copy of the same bounds is how the two would come
116
+ * to disagree. Only the annotations are new.
117
+ *
118
+ * 🛑 `annotate()` must be the OUTERMOST call — it keys a WeakMap by schema
119
+ * instance and every zod wrapper returns a new one.
120
+ */
121
+ export declare const productFeatureFormSchema: z.ZodEffects<z.ZodObject<{
122
+ label: z.ZodString;
123
+ description: z.ZodOptional<z.ZodString>;
124
+ icon: z.ZodString;
125
+ iconColor: z.ZodOptional<z.ZodString>;
126
+ category: z.ZodEnum<["shipping", "seller", "condition", "platform", "auction", "preorder", "custom"]>;
127
+ scope: z.ZodEnum<["platform", "store"]>;
128
+ productTypes: z.ZodArray<z.ZodEnum<["product", "auction", "preorder", "prize-draw", "classified", "digital-code", "live", "all"]>, "many">;
129
+ storeId: z.ZodOptional<z.ZodString>;
130
+ isActive: z.ZodBoolean;
131
+ displayOrder: z.ZodNumber;
132
+ }, "strip", z.ZodTypeAny, {
133
+ label: string;
134
+ scope: "store" | "platform";
135
+ icon: string;
136
+ category: "shipping" | "custom" | "seller" | "condition" | "auction" | "platform" | "preorder";
137
+ isActive: boolean;
138
+ displayOrder: number;
139
+ productTypes: ("all" | "classified" | "auction" | "prize-draw" | "digital-code" | "live" | "product" | "preorder")[];
140
+ storeId?: string | undefined;
141
+ description?: string | undefined;
142
+ iconColor?: string | undefined;
143
+ }, {
144
+ label: string;
145
+ scope: "store" | "platform";
146
+ icon: string;
147
+ category: "shipping" | "custom" | "seller" | "condition" | "auction" | "platform" | "preorder";
148
+ isActive: boolean;
149
+ displayOrder: number;
150
+ productTypes: ("all" | "classified" | "auction" | "prize-draw" | "digital-code" | "live" | "product" | "preorder")[];
151
+ storeId?: string | undefined;
152
+ description?: string | undefined;
153
+ iconColor?: string | undefined;
154
+ }>, {
155
+ label: string;
156
+ scope: "store" | "platform";
157
+ icon: string;
158
+ category: "shipping" | "custom" | "seller" | "condition" | "auction" | "platform" | "preorder";
159
+ isActive: boolean;
160
+ displayOrder: number;
161
+ productTypes: ("all" | "classified" | "auction" | "prize-draw" | "digital-code" | "live" | "product" | "preorder")[];
162
+ storeId?: string | undefined;
163
+ description?: string | undefined;
164
+ iconColor?: string | undefined;
165
+ }, {
166
+ label: string;
167
+ scope: "store" | "platform";
168
+ icon: string;
169
+ category: "shipping" | "custom" | "seller" | "condition" | "auction" | "platform" | "preorder";
170
+ isActive: boolean;
171
+ displayOrder: number;
172
+ productTypes: ("all" | "classified" | "auction" | "prize-draw" | "digital-code" | "live" | "product" | "preorder")[];
173
+ storeId?: string | undefined;
174
+ description?: string | undefined;
175
+ iconColor?: string | undefined;
176
+ }>;
177
+ export type ProductFeatureFormValues = z.infer<typeof productFeatureFormSchema>;
105
178
  export type ProductFeatureAdminCreatePayload = z.infer<typeof productFeatureAdminCreateSchema>;
106
179
  export type ProductFeatureStoreCreatePayload = z.infer<typeof productFeatureStoreCreateSchema>;
107
180
  export type ProductFeatureUpdatePayload = z.infer<typeof productFeatureUpdateSchema>;
@@ -6,6 +6,7 @@
6
6
  * of hand-rolling parallel schemas.
7
7
  */
8
8
  import { z } from "zod";
9
+ import { annotate } from "../../shell/field-ui-meta";
9
10
  export const PRODUCT_FEATURE_PRODUCT_TYPE_ENUM = z.enum([
10
11
  "product",
11
12
  "auction",
@@ -66,3 +67,65 @@ export const productFeatureUpdateSchema = z.object({
66
67
  isActive: z.boolean().optional(),
67
68
  displayOrder: z.number().int().min(0).max(FEATURE_DISPLAY_ORDER_MAX).optional(),
68
69
  });
70
+ /**
71
+ * The FORM schema for the product-feature editor, with UI metadata.
72
+ *
73
+ * `AdminFeatureEditorView` had **no validation whatsoever** before W5 — ten
74
+ * `useState` fields, `required` on the inputs (an HTML attribute any
75
+ * programmatic submit bypasses), and nothing else. Meanwhile these validators
76
+ * had existed all along and simply were not wired: the "schema exists, the
77
+ * consumer ignores it" case the plan catalogues.
78
+ *
79
+ * It reuses `productFeatureAdminCreateSchema`'s field shapes rather than
80
+ * restating them — a second copy of the same bounds is how the two would come
81
+ * to disagree. Only the annotations are new.
82
+ *
83
+ * 🛑 `annotate()` must be the OUTERMOST call — it keys a WeakMap by schema
84
+ * instance and every zod wrapper returns a new one.
85
+ */
86
+ export const productFeatureFormSchema = z.object({
87
+ label: annotate(productFeatureAdminCreateSchema.shape.label, {
88
+ section: "basics", sectionLabel: "Feature", sectionRequired: true,
89
+ quick: true, order: 1, row: "full",
90
+ }),
91
+ description: annotate(productFeatureAdminCreateSchema.shape.description, {
92
+ section: "basics", order: 2, row: "full",
93
+ }),
94
+ icon: annotate(productFeatureAdminCreateSchema.shape.icon, {
95
+ section: "basics", quick: true, order: 3, row: "pair",
96
+ }),
97
+ iconColor: annotate(productFeatureAdminCreateSchema.shape.iconColor, {
98
+ section: "basics", order: 4, row: "pair",
99
+ }),
100
+ category: annotate(productFeatureAdminCreateSchema.shape.category, {
101
+ section: "scope", sectionLabel: "Scope & Applicability",
102
+ quick: true, order: 1, row: "pair",
103
+ }),
104
+ scope: annotate(productFeatureAdminCreateSchema.shape.scope, {
105
+ section: "scope", quick: true, order: 2, row: "pair",
106
+ }),
107
+ productTypes: annotate(productFeatureAdminCreateSchema.shape.productTypes, {
108
+ section: "scope", order: 3, row: "full", kind: "list",
109
+ }),
110
+ storeId: annotate(productFeatureAdminCreateSchema.shape.storeId, {
111
+ section: "scope", order: 4, row: "pair",
112
+ help: "Only used when scope is \"store\".",
113
+ }),
114
+ isActive: annotate(productFeatureAdminCreateSchema.shape.isActive, {
115
+ section: "visibility", sectionLabel: "Visibility", order: 1, row: "quarter",
116
+ }),
117
+ displayOrder: annotate(productFeatureAdminCreateSchema.shape.displayOrder, {
118
+ section: "visibility", order: 2, row: "pair",
119
+ }),
120
+ }).superRefine((v, ctx) => {
121
+ // The one rule no per-field bound can state: a store-scoped feature must
122
+ // name its store. The server enforces it too — this exists so the admin is
123
+ // told before submitting rather than by a 400.
124
+ if (v.scope === "store" && !v.storeId?.trim()) {
125
+ ctx.addIssue({
126
+ code: z.ZodIssueCode.custom,
127
+ path: ["storeId"],
128
+ message: "A store-scoped feature must name a store.",
129
+ });
130
+ }
131
+ });
@@ -0,0 +1,28 @@
1
+ import { type ScammerStatus } from "../schemas/firestore";
2
+ /** Human-readable label. Replaces two `.replace(/_/g, " ")` call sites, which
3
+ * rendered "pending review" — mechanically correct, and not a written label. */
4
+ export declare const SCAMMER_STATUS_LABEL: Record<ScammerStatus, string>;
5
+ /**
6
+ * Inline-chip styling. Every entry is a `bg-{status}-surface` + `text-{status}`
7
+ * pair per Recurrent Root Cause #67 — the two tokens invert together with the
8
+ * theme, which a literal `text-white` would not.
9
+ *
10
+ * `removed` is the neutral case: it has no status colour because it is not a
11
+ * status outcome, it is a withdrawal. ONE background, ONE ink.
12
+ */
13
+ export declare const SCAMMER_STATUS_BADGE: Record<ScammerStatus, string>;
14
+ /** Dropdown options, derived from the union so one cannot go missing. */
15
+ export declare const SCAMMER_STATUS_OPTIONS: {
16
+ value: "verified" | "rejected" | "removed" | "pending_review";
17
+ label: string;
18
+ }[];
19
+ /**
20
+ * Narrow an unknown API value to a real status.
21
+ *
22
+ * The list view used `toStringValue(item.status, "pending_review")`, which
23
+ * produces a `string` — so an unrecognised value flowed on as if it were a
24
+ * status and every consumer fell back to the pending styling. This maps
25
+ * anything unrecognised to `pending_review` explicitly, and the return type
26
+ * says so.
27
+ */
28
+ export declare function toScammerStatus(value: unknown): ScammerStatus;
@@ -0,0 +1,79 @@
1
+ /*
2
+ * WHY: "How does a scammer status look and read" had FOUR hand-written answers
3
+ * and they had already diverged in a way that shipped a real defect:
4
+ *
5
+ * · `AdminScammersView.STATUS_BADGE` — Record<string, string>
6
+ * · `AdminScammerEditorView.STATUS_COLOR` — Record<string, string>
7
+ * · `AdminScammerEditorView.STATUS_OPTIONS` — a literal label list
8
+ * · `ADMIN_SCAMMER_STATUS_TABS` — the filter chips
9
+ *
10
+ * Both colour maps carried the SAME broken `removed` entry:
11
+ *
12
+ * "bg-[…surface] text-[…muted] bg-[…surface-elevated] text-[…muted]"
13
+ *
14
+ * — two backgrounds and two inks on one element. Tailwind emits all four
15
+ * and the winner is decided by stylesheet order, not by position in the
16
+ * class string, so which background actually painted was not what either
17
+ * map said. One bug, two copies: the Duplication Framework's bug-fix
18
+ * multiplier, which is what makes this a consolidate rather than a
19
+ * third copy.
20
+ *
21
+ * WHAT: One display module beside the union it describes.
22
+ *
23
+ * ## Everything here is keyed `Record<ScammerStatus, …>`
24
+ *
25
+ * Not `Record<string, …>`. A status added to the union without a label or a
26
+ * badge is then a COMPILE error rather than a row that silently renders with
27
+ * the fallback styling of a status it is not — which is precisely how the
28
+ * loose maps hid the broken `removed` entry.
29
+ *
30
+ * EXPORTS:
31
+ * SCAMMER_STATUS_LABEL, SCAMMER_STATUS_BADGE, SCAMMER_STATUS_OPTIONS,
32
+ * toScammerStatus
33
+ *
34
+ * @tag domain:scams
35
+ * @tag layer:constants
36
+ * @tag pattern:none
37
+ * @tag access:isomorphic
38
+ * @tag consumers:AdminScammersView,AdminScammerEditorView
39
+ * @tag sideEffects:none
40
+ */
41
+ import { ScammerStatusValues } from "../schemas/firestore";
42
+ /** Human-readable label. Replaces two `.replace(/_/g, " ")` call sites, which
43
+ * rendered "pending review" — mechanically correct, and not a written label. */
44
+ export const SCAMMER_STATUS_LABEL = {
45
+ pending_review: "Pending review",
46
+ verified: "Verified",
47
+ rejected: "Rejected",
48
+ removed: "Removed",
49
+ };
50
+ /**
51
+ * Inline-chip styling. Every entry is a `bg-{status}-surface` + `text-{status}`
52
+ * pair per Recurrent Root Cause #67 — the two tokens invert together with the
53
+ * theme, which a literal `text-white` would not.
54
+ *
55
+ * `removed` is the neutral case: it has no status colour because it is not a
56
+ * status outcome, it is a withdrawal. ONE background, ONE ink.
57
+ */
58
+ export const SCAMMER_STATUS_BADGE = {
59
+ pending_review: "bg-warning-surface text-warning",
60
+ verified: "bg-success-surface text-success",
61
+ rejected: "bg-error-surface text-error",
62
+ removed: "bg-[var(--appkit-color-surface-elevated)] text-[var(--appkit-color-text-muted)]",
63
+ };
64
+ /** Dropdown options, derived from the union so one cannot go missing. */
65
+ export const SCAMMER_STATUS_OPTIONS = Object.values(ScammerStatusValues).map((value) => ({ value, label: SCAMMER_STATUS_LABEL[value] }));
66
+ /**
67
+ * Narrow an unknown API value to a real status.
68
+ *
69
+ * The list view used `toStringValue(item.status, "pending_review")`, which
70
+ * produces a `string` — so an unrecognised value flowed on as if it were a
71
+ * status and every consumer fell back to the pending styling. This maps
72
+ * anything unrecognised to `pending_review` explicitly, and the return type
73
+ * says so.
74
+ */
75
+ export function toScammerStatus(value) {
76
+ return Object.values(ScammerStatusValues).includes(value)
77
+ ? value
78
+ : ScammerStatusValues.PENDING_REVIEW;
79
+ }
@@ -210,7 +210,24 @@ export async function counterOfferByBuyer(userId, userEmail, input) {
210
210
  const offerCount = await offerRepository.countByBuyerAndProduct(userId, offer.productId, product.updatedAt);
211
211
  if (offerCount >= 3)
212
212
  throw new ValidationError(ERROR_MESSAGES.OFFER.LIMIT_REACHED);
213
- await offerRepository.withdraw(offerId);
213
+ // ── Create BEFORE superseding. Order matters. ───────────────────────────
214
+ //
215
+ // This used to `withdraw()` first and `create()` second. If `create` threw —
216
+ // a validation slip, a Firestore blip — the buyer's live counter was ALREADY
217
+ // withdrawn, no replacement existed, the negotiation was dead, and one of
218
+ // their three offer slots had been burnt for nothing.
219
+ //
220
+ // Reversed, the worst failure is a transient extra pending offer, which
221
+ // `hasActiveOffer` and the expiry sweep both already handle.
222
+ //
223
+ // The chain fields are what make a multi-round negotiation readable at all:
224
+ // `previousOfferId` used to exist ONLY inside the serverLogger call below
225
+ // and never reached Firestore, so three rounds were three unrelated
226
+ // documents. `chainRootOfferId` is denormalised onto every round so a list
227
+ // row can render "Round 2" with zero extra reads, and `findChain` stays a
228
+ // single-field equality served by the automatic index.
229
+ const chainRoot = offer.chainRootOfferId ?? offer.id;
230
+ const nextRound = (offer.counterRound ?? 1) + 1;
214
231
  const newOffer = await offerRepository.create({
215
232
  productId: offer.productId,
216
233
  productTitle: offer.productTitle,
@@ -225,7 +242,18 @@ export async function counterOfferByBuyer(userId, userEmail, input) {
225
242
  listedPrice: offer.listedPrice,
226
243
  currency: offer.currency,
227
244
  buyerNote,
245
+ // NOT carrying `sellerNote` forward: it would render as "Seller note" on
246
+ // the new round, making the seller look like they had already answered a
247
+ // counter they have never seen. Round N-1 keeps its own note and is now
248
+ // reachable via `previousOfferId`.
249
+ previousOfferId: offer.id,
250
+ chainRootOfferId: chainRoot,
251
+ counterRound: nextRound,
228
252
  });
253
+ // Now supersede the old round — one write, one history entry. A `withdrawn`
254
+ // document carrying `supersededByOfferId` renders as *Superseded* (neutral),
255
+ // not *Withdrawn* (negative): the buyer did not walk away, they countered.
256
+ await offerRepository.withdraw(offerId, newOffer.id, { actor: { uid: userId, role: "buyer" } }, offer);
229
257
  const buyerCounterStore = offer.storeId ? await storeRepository.findById(offer.storeId) : null;
230
258
  if (buyerCounterStore?.ownerId)
231
259
  await sendNotification({
@@ -237,12 +265,14 @@ export async function counterOfferByBuyer(userId, userEmail, input) {
237
265
  relatedId: newOffer.id,
238
266
  relatedType: "offer",
239
267
  });
268
+ // `previousOfferId` is no longer logged as its only record — it is a
269
+ // persisted field on `newOffer` now. A log line is not a data model.
240
270
  serverLogger.info("counterOfferByBuyer", {
241
- previousOfferId: offerId,
242
271
  newOfferId: newOffer.id,
243
272
  buyerUid: userId,
244
273
  productId: offer.productId,
245
274
  counterAmount,
275
+ counterRound: nextRound,
246
276
  });
247
277
  return newOffer;
248
278
  }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * OfferPhaseTimeline — the offer-shaped wrapper around `<StatusTimeline>`.
3
+ *
4
+ * An offer is an order with more phases in front of it, so it shares the rail
5
+ * and supplies only its own vocabulary:
6
+ *
7
+ * made → countered → accepted → paid
8
+ * with declined / withdrawn / expired as the terminal-negative branch
9
+ *
10
+ * ## `superseded` is a RENDER KEY, never an OfferStatus
11
+ *
12
+ * A document that is `withdrawn` **and** carries `supersededByOfferId` is not
13
+ * a walk-away — the buyer countered, and the negotiation continued into the
14
+ * next round. It renders neutral ("Superseded"), not negative ("Withdrawn").
15
+ *
16
+ * Deliberately NOT an eighth `OfferStatus` value: `ADMIN_OFFER_STATUS_TABS`
17
+ * would then gain a chip that matches zero stored rows, which is exactly the
18
+ * defect `audit-filter-tab-enums` exists to catch.
19
+ *
20
+ * ## Back-compat
21
+ *
22
+ * Offers written before `statusHistory` existed have none. Branch B derives
23
+ * what it can from the scalars — and `expired` gets **no date at all**,
24
+ * because `expireMany` never wrote one. `updatedAt` is "last write of any
25
+ * kind" and `expiresAt` is a deadline, not an event; using either would be a
26
+ * fabricated timestamp.
27
+ */
28
+ import { type TimelineEntry } from "../../status-history/components/StatusTimeline";
29
+ /** One round of a negotiation, as returned by the API. */
30
+ export interface OfferChainRound {
31
+ id: string;
32
+ counterRound: number;
33
+ status: string;
34
+ offerAmount: number;
35
+ counterAmount?: number;
36
+ superseded: boolean;
37
+ createdAt: string;
38
+ respondedAt?: string;
39
+ sellerNote?: string;
40
+ buyerNote?: string;
41
+ }
42
+ export interface OfferPhaseTimelineProps {
43
+ status: string;
44
+ timeline?: TimelineEntry[];
45
+ timelineTruncated?: number;
46
+ superseded?: boolean;
47
+ createdAt?: string;
48
+ respondedAt?: string;
49
+ acceptedAt?: string;
50
+ paidAt?: string;
51
+ /**
52
+ * Earlier rounds, oldest first. Concatenated ahead of this offer's own
53
+ * steps into ONE rail, each introduced by a "Round N" divider — a
54
+ * negotiation reads as one story, not N disconnected cards.
55
+ */
56
+ chain?: OfferChainRound[];
57
+ className?: string;
58
+ }
59
+ export declare function OfferPhaseTimeline({ status, timeline, timelineTruncated, superseded, createdAt, respondedAt, acceptedAt, paidAt, chain, className, }: OfferPhaseTimelineProps): import("react").JSX.Element;