@mohasinac/appkit 4.14.0 → 4.16.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 (206) 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.d.ts +9 -2
  13. package/dist/_internal/shared/actions/action-registry.js +458 -7
  14. package/dist/_internal/shared/features/auctions/schema.d.ts +14 -2
  15. package/dist/_internal/shared/features/auctions/schema.js +13 -1
  16. package/dist/_internal/shared/features/blog/schema.d.ts +4 -4
  17. package/dist/_internal/shared/features/cart/schema.d.ts +25 -20
  18. package/dist/_internal/shared/features/cart/schema.js +15 -0
  19. package/dist/_internal/shared/features/categories/bundle-schemas.d.ts +22 -22
  20. package/dist/_internal/shared/features/orders/schema.d.ts +4 -4
  21. package/dist/_internal/shared/features/products/schema.d.ts +27 -27
  22. package/dist/_internal/shared/features/promotions/schema.d.ts +4 -4
  23. package/dist/_internal/shared/features/reviews/schema.d.ts +2 -2
  24. package/dist/_internal/shared/history/history.d.ts +22 -0
  25. package/dist/_internal/shared/history/history.js +41 -1
  26. package/dist/client.d.ts +13 -2
  27. package/dist/client.js +20 -2
  28. package/dist/constants/field-names.d.ts +8 -6
  29. package/dist/constants/field-names.js +48 -6
  30. package/dist/contracts/extend.d.ts +29 -0
  31. package/dist/contracts/extend.js +20 -0
  32. package/dist/contracts/index.d.ts +2 -1
  33. package/dist/contracts/index.js +2 -0
  34. package/dist/features/account/components/UserBidsView.js +12 -4
  35. package/dist/features/account/components/UserReturnsView.d.ts +3 -0
  36. package/dist/features/account/components/UserReturnsView.js +17 -1
  37. package/dist/features/admin/components/AdminAuditLogView.js +1 -0
  38. package/dist/features/admin/components/AdminBidsView.js +15 -2
  39. package/dist/features/admin/components/AdminBlogEditorView.js +65 -33
  40. package/dist/features/admin/components/AdminFeatureEditorView.js +35 -6
  41. package/dist/features/admin/components/AdminNavEditorView.js +15 -5
  42. package/dist/features/admin/components/AdminNewsletterView.js +18 -2
  43. package/dist/features/admin/components/AdminOffersView.js +62 -18
  44. package/dist/features/admin/components/AdminPaymentMethodsView.js +24 -4
  45. package/dist/features/admin/components/AdminScammerEditorView.d.ts +2 -1
  46. package/dist/features/admin/components/AdminScammerEditorView.js +3 -13
  47. package/dist/features/admin/components/AdminScammersView.js +3 -8
  48. package/dist/features/admin/components/AdminSectionsView.js +32 -23
  49. package/dist/features/admin/components/AdminSessionsView.js +17 -2
  50. package/dist/features/admin/components/AdminViewCards.js +24 -17
  51. package/dist/features/admin/components/DataListingView.js +15 -1
  52. package/dist/features/admin/components/DataTable.js +7 -17
  53. package/dist/features/admin/components/ViewAuditLogEntryModal.js +1 -0
  54. package/dist/features/admin/components/sections/adminSectionsTypes.d.ts +4 -2
  55. package/dist/features/admin/components/sections/adminSectionsTypes.js +45 -27
  56. package/dist/features/admin/constants/filter-tabs.d.ts +3 -0
  57. package/dist/features/admin/constants/filter-tabs.js +1 -0
  58. package/dist/features/admin/schemas/homepage-section-form.d.ts +22 -0
  59. package/dist/features/admin/schemas/homepage-section-form.js +68 -0
  60. package/dist/features/admin/schemas/nav-item-form.d.ts +74 -0
  61. package/dist/features/admin/schemas/nav-item-form.js +72 -0
  62. package/dist/features/auctions/components/AuctionBidsTable.d.ts +10 -1
  63. package/dist/features/auctions/components/AuctionBidsTable.js +22 -4
  64. package/dist/features/auctions/components/bid-detail-fields.d.ts +29 -0
  65. package/dist/features/auctions/components/bid-detail-fields.js +103 -0
  66. package/dist/features/auctions/schemas/index.d.ts +14 -14
  67. package/dist/features/audit-log/schemas/firestore.d.ts +14 -0
  68. package/dist/features/audit-log/schemas/firestore.js +7 -0
  69. package/dist/features/auth/schemas/index.d.ts +2 -2
  70. package/dist/features/blog/actions/blog-actions.d.ts +7 -7
  71. package/dist/features/blog/schemas/blog-form.d.ts +80 -0
  72. package/dist/features/blog/schemas/blog-form.js +100 -0
  73. package/dist/features/blog/schemas/index.d.ts +10 -10
  74. package/dist/features/cart/schemas/index.d.ts +3 -3
  75. package/dist/features/catalogue/schemas/validation.d.ts +12 -12
  76. package/dist/features/categories/schemas/index.d.ts +16 -16
  77. package/dist/features/collections/schemas/index.d.ts +8 -8
  78. package/dist/features/events/components/AdminEventEditorView.js +32 -54
  79. package/dist/features/events/components/AdminEventEntriesView.js +22 -3
  80. package/dist/features/events/schemas/event-form.d.ts +113 -0
  81. package/dist/features/events/schemas/event-form.js +82 -0
  82. package/dist/features/events/schemas/index.d.ts +2 -2
  83. package/dist/features/faq/actions/faq-actions.d.ts +6 -6
  84. package/dist/features/faq/schemas/index.d.ts +6 -6
  85. package/dist/features/grouped/schemas/grouped-listing-form.d.ts +101 -0
  86. package/dist/features/grouped/schemas/grouped-listing-form.js +89 -0
  87. package/dist/features/grouped/schemas/index.d.ts +4 -4
  88. package/dist/features/homepage/actions/homepage-section-actions.d.ts +11 -3
  89. package/dist/features/homepage/actions/homepage-section-actions.js +33 -1
  90. package/dist/features/homepage/schemas/index.d.ts +71 -40
  91. package/dist/features/homepage/schemas/index.js +24 -0
  92. package/dist/features/messages/schemas/index.d.ts +2 -2
  93. package/dist/features/orders/components/OrderStatusTimeline.d.ts +5 -1
  94. package/dist/features/orders/components/OrderStatusTimeline.js +39 -20
  95. package/dist/features/orders/repository/orders.repository.js +5 -0
  96. package/dist/features/orders/schemas/firestore.d.ts +36 -1
  97. package/dist/features/orders/schemas/index.d.ts +24 -24
  98. package/dist/features/orders/types/index.d.ts +35 -7
  99. package/dist/features/pre-orders/schemas/index.d.ts +4 -4
  100. package/dist/features/products/components/GroupSettingsPanel.js +5 -1
  101. package/dist/features/products/schemas/index.d.ts +38 -38
  102. package/dist/features/products/schemas/product-features.validators.d.ts +75 -2
  103. package/dist/features/products/schemas/product-features.validators.js +63 -0
  104. package/dist/features/promotions/schemas/index.d.ts +14 -14
  105. package/dist/features/reviews/schemas/index.d.ts +14 -14
  106. package/dist/features/scams/constants/scammer-status-display.d.ts +28 -0
  107. package/dist/features/scams/constants/scammer-status-display.js +79 -0
  108. package/dist/features/scams/schemas/index.d.ts +2 -2
  109. package/dist/features/search/schemas/index.d.ts +2 -2
  110. package/dist/features/seller/actions/offer-actions.js +32 -2
  111. package/dist/features/seller/components/OfferPhaseTimeline.d.ts +59 -0
  112. package/dist/features/seller/components/OfferPhaseTimeline.js +107 -0
  113. package/dist/features/seller/components/SellerBidsView.js +11 -4
  114. package/dist/features/seller/components/SellerCouponEditorView.js +17 -17
  115. package/dist/features/seller/components/SellerOffersView.js +2 -1
  116. package/dist/features/seller/components/SellerPayoutSettingsView.js +38 -14
  117. package/dist/features/seller/repository/offer.repository.d.ts +69 -8
  118. package/dist/features/seller/repository/offer.repository.js +127 -23
  119. package/dist/features/seller/schemas/coupon-form.d.ts +72 -0
  120. package/dist/features/seller/schemas/coupon-form.js +116 -0
  121. package/dist/features/seller/schemas/firestore.d.ts +56 -2
  122. package/dist/features/seller/schemas/firestore.js +27 -0
  123. package/dist/features/seller/schemas/index.d.ts +2 -2
  124. package/dist/features/seller/schemas/offer-forms.d.ts +21 -0
  125. package/dist/features/seller/schemas/offer-forms.js +21 -0
  126. package/dist/features/shell/build-sections.d.ts +63 -0
  127. package/dist/features/shell/build-sections.js +207 -0
  128. package/dist/features/shell/field-ui-meta.d.ts +115 -0
  129. package/dist/features/shell/field-ui-meta.js +324 -0
  130. package/dist/features/shell/index.d.ts +4 -0
  131. package/dist/features/shell/index.js +2 -0
  132. package/dist/features/shipments/schemas/validation.d.ts +22 -22
  133. package/dist/features/status-history/components/StatusTimeline.d.ts +74 -0
  134. package/dist/features/status-history/components/StatusTimeline.js +46 -0
  135. package/dist/features/status-history/index.d.ts +11 -0
  136. package/dist/features/status-history/index.js +10 -0
  137. package/dist/features/store-extensions/components/ReviewDecisionModal.d.ts +27 -0
  138. package/dist/features/store-extensions/components/ReviewDecisionModal.js +82 -0
  139. package/dist/features/store-extensions/index.d.ts +12 -0
  140. package/dist/features/store-extensions/index.js +12 -0
  141. package/dist/features/store-extensions/schemas/admin-notification-form.d.ts +36 -0
  142. package/dist/features/store-extensions/schemas/admin-notification-form.js +114 -0
  143. package/dist/features/store-extensions/schemas/analytics-forms.d.ts +57 -0
  144. package/dist/features/store-extensions/schemas/analytics-forms.js +158 -0
  145. package/dist/features/store-extensions/schemas/custom-role-form.d.ts +85 -0
  146. package/dist/features/store-extensions/schemas/custom-role-form.js +118 -0
  147. package/dist/features/store-extensions/schemas/firestore.d.ts +21 -1
  148. package/dist/features/store-extensions/schemas/firestore.js +60 -0
  149. package/dist/features/store-extensions/schemas/google-config-form.d.ts +53 -0
  150. package/dist/features/store-extensions/schemas/google-config-form.js +105 -0
  151. package/dist/features/store-extensions/schemas/item-request-create-form.d.ts +28 -0
  152. package/dist/features/store-extensions/schemas/item-request-create-form.js +80 -0
  153. package/dist/features/store-extensions/schemas/listing-template-form.d.ts +78 -0
  154. package/dist/features/store-extensions/schemas/listing-template-form.js +87 -0
  155. package/dist/features/store-extensions/schemas/moderation-review-form.d.ts +49 -0
  156. package/dist/features/store-extensions/schemas/moderation-review-form.js +101 -0
  157. package/dist/features/store-extensions/schemas/payout-method-form.d.ts +153 -0
  158. package/dist/features/store-extensions/schemas/payout-method-form.js +155 -0
  159. package/dist/features/store-extensions/schemas/report-create-form.d.ts +25 -0
  160. package/dist/features/store-extensions/schemas/report-create-form.js +79 -0
  161. package/dist/features/store-extensions/schemas/report-review-form.d.ts +47 -0
  162. package/dist/features/store-extensions/schemas/report-review-form.js +111 -0
  163. package/dist/features/store-extensions/schemas/shipping-config-form.d.ts +148 -0
  164. package/dist/features/store-extensions/schemas/shipping-config-form.js +104 -0
  165. package/dist/features/store-extensions/schemas/store-category-form.d.ts +54 -0
  166. package/dist/features/store-extensions/schemas/store-category-form.js +73 -0
  167. package/dist/features/stores/schemas/index.d.ts +4 -4
  168. package/dist/features/support/schemas/index.d.ts +4 -4
  169. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +350 -0
  170. package/dist/features/wishlist/hooks/useWishlistWithGuest.d.ts +1 -1
  171. package/dist/index.d.ts +12 -2
  172. package/dist/index.js +12 -2
  173. package/dist/next/routing/route-map.d.ts +16 -2
  174. package/dist/next/routing/route-map.js +9 -2
  175. package/dist/schemas/firestore-value.d.ts +8 -0
  176. package/dist/schemas/firestore-value.js +44 -0
  177. package/dist/schemas/index.d.ts +1 -1
  178. package/dist/schemas/index.js +1 -1
  179. package/dist/schemas/registry.d.ts +734 -101
  180. package/dist/schemas/registry.js +64 -1
  181. package/dist/seed/bids-seed-data.js +30 -0
  182. package/dist/seed/offers-seed-data.js +212 -7
  183. package/dist/seed/orders-seed-data.js +336 -1
  184. package/dist/seed/products-auctions-seed-data.js +41 -0
  185. package/dist/server.d.ts +2 -0
  186. package/dist/server.js +5 -0
  187. package/dist/styles.css +1 -1
  188. package/dist/tailwind-utilities.css +1 -1
  189. package/dist/ui/DataTable.js +4 -2
  190. package/dist/ui/columns/build-columns.d.ts +6 -1
  191. package/dist/ui/columns/build-columns.js +53 -0
  192. package/dist/ui/columns/cell-renderers.d.ts +68 -0
  193. package/dist/ui/columns/cell-renderers.js +111 -0
  194. package/dist/ui/columns/column-renderers.d.ts +6 -1
  195. package/dist/ui/columns/column-renderers.js +6 -1
  196. package/dist/ui/columns/index.d.ts +2 -1
  197. package/dist/ui/columns/index.js +5 -1
  198. package/dist/ui/components/Button.js +27 -2
  199. package/dist/ui/components/RecordDetailModal.d.ts +10 -1
  200. package/dist/ui/components/RecordDetailModal.js +2 -2
  201. package/dist/ui/icons/icon-registry.d.ts +65 -0
  202. package/dist/ui/icons/icon-registry.js +115 -0
  203. package/package.json +2 -1
  204. package/scripts/node-esm-loader.mjs +65 -0
  205. package/scripts/seed-cli-loader.mjs +6 -49
  206. package/scripts/seed-cli.mjs +1 -1
@@ -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 };
@@ -5,8 +5,11 @@
5
5
  * Handles buyer make-an-offer, seller counter/accept/decline lifecycle.
6
6
  */
7
7
  import { BaseRepository, prepareForFirestore, parseSieveDateValue, } from "../../../providers/db-firebase";
8
- import { OFFER_COLLECTION, OFFER_FIELDS, createOfferId } from "../schemas";
8
+ import { OFFER_COLLECTION, OFFER_FIELDS, OFFER_TRACKED_FIELDS, createOfferId } from "../schemas";
9
+ import { withHistory } from "../../../_internal/shared/history/index";
9
10
  import { encryptPiiFields, decryptPiiFields, OFFER_PII_FIELDS, } from "../../../security";
11
+ /** Max rounds in one negotiation chain — matches the per-buyer offer limit. */
12
+ const OFFER_CHAIN_MAX = 3;
10
13
  class OfferRepository extends BaseRepository {
11
14
  constructor() {
12
15
  super(OFFER_COLLECTION);
@@ -47,6 +50,13 @@ class OfferRepository extends BaseRepository {
47
50
  expiresAt,
48
51
  createdAt: now,
49
52
  updatedAt: now,
53
+ // The root of a chain stores its OWN id. Without this, round 1 is the
54
+ // only document with no `chainRootOfferId`, so `findChain(root.id)`
55
+ // would return rounds 2..N and silently omit the round the buyer
56
+ // actually started with. Set here rather than at the call site because
57
+ // the id is generated in this method.
58
+ chainRootOfferId: input.chainRootOfferId ?? id,
59
+ counterRound: input.counterRound ?? 1,
50
60
  };
51
61
  // Encrypt PII fields before persisting
52
62
  const encrypted = encryptPiiFields({ ...data }, [...OFFER_PII_FIELDS]);
@@ -142,13 +152,35 @@ class OfferRepository extends BaseRepository {
142
152
  return snap.size;
143
153
  }
144
154
  // --- Mutations -----------------------------------------------------------
145
- async updateStatus(offerId, patch) {
146
- return this.update(offerId, {
147
- ...patch,
148
- updatedAt: new Date(),
155
+ /**
156
+ * The single write choke point for offers, so `statusHistory` is appended
157
+ * in ONE place rather than at every call site — the same shape
158
+ * `OrderRepository.updateWithHistory` uses.
159
+ *
160
+ * `prior` is the already-fetched document when the caller has one. Every
161
+ * real caller does (`respondToOffer`, `acceptCounterOffer`, `withdrawOffer`,
162
+ * `counterOfferByBuyer`, `finalizeLockedLines`, the admin PATCH, both expiry
163
+ * sweeps), so threading it through means history costs **zero extra reads**.
164
+ */
165
+ async updateStatus(offerId, patch, ctx, prior) {
166
+ const full = { ...patch, updatedAt: new Date() };
167
+ const current = prior !== undefined ? prior : await this.findById(offerId);
168
+ const withEntry = withHistory(current, full, {
169
+ tracked: OFFER_TRACKED_FIELDS,
170
+ actor: ctx?.actor ?? { role: "system" },
171
+ trigger: ctx?.trigger ?? "offerRepository.updateStatus",
172
+ reason: ctx?.reason,
173
+ note: ctx?.note,
174
+ // buyerName/buyerEmail live on this document. `encryptPiiFields` is a
175
+ // flat top-level loop and never descends into arrays, so without this
176
+ // they would persist in PLAINTEXT inside statusHistory.
177
+ piiFields: OFFER_PII_FIELDS,
178
+ historyField: OFFER_FIELDS.STATUS_HISTORY,
179
+ truncatedField: OFFER_FIELDS.STATUS_HISTORY_TRUNCATED,
149
180
  });
181
+ return this.update(offerId, (withEntry ?? full));
150
182
  }
151
- async accept(offerId, lockedPrice, sellerNote, checkoutDeadline) {
183
+ async accept(offerId, lockedPrice, sellerNote, checkoutDeadline, ctx, prior) {
152
184
  // One write, not two. `checkoutDeadline` used to be missing from
153
185
  // OfferUpdateInput, which forced callers to follow every accept() with a
154
186
  // separate generic update() — leaving a window where an offer was
@@ -160,25 +192,28 @@ class OfferRepository extends BaseRepository {
160
192
  acceptedAt: new Date(),
161
193
  respondedAt: new Date(),
162
194
  ...(checkoutDeadline ? { checkoutDeadline } : {}),
163
- });
195
+ }, { actor: { role: "seller" }, trigger: "respondToOffer:accept", note: sellerNote, ...ctx }, prior);
164
196
  }
165
- async decline(offerId, sellerNote) {
197
+ async decline(offerId, sellerNote, ctx, prior) {
166
198
  return this.updateStatus(offerId, {
167
199
  status: "declined",
168
200
  sellerNote,
169
201
  respondedAt: new Date(),
170
- });
202
+ }, { actor: { role: "seller" }, trigger: "respondToOffer:decline", note: sellerNote, ...ctx }, prior);
171
203
  }
172
- async counter(offerId, counterAmount, sellerNote) {
204
+ async counter(offerId, counterAmount, sellerNote, ctx, prior) {
173
205
  return this.updateStatus(offerId, {
174
206
  status: "countered",
175
207
  counterAmount,
176
208
  sellerNote,
177
209
  respondedAt: new Date(),
178
- });
210
+ }, { actor: { role: "seller" }, trigger: "respondToOffer:counter", note: sellerNote, ...ctx }, prior);
179
211
  }
180
- async acceptCounter(offerId, checkoutDeadline) {
181
- const offer = await this.findById(offerId);
212
+ async acceptCounter(offerId, checkoutDeadline, ctx, prior) {
213
+ // Reuse the caller's document when supplied — this method used to always
214
+ // re-read, so passing `prior` makes the feature land one read CHEAPER
215
+ // than before rather than one more expensive.
216
+ const offer = prior !== undefined && prior !== null ? prior : await this.findById(offerId);
182
217
  if (!offer || !offer.counterAmount)
183
218
  throw new Error("Offer or counter not found");
184
219
  return this.updateStatus(offerId, {
@@ -187,13 +222,29 @@ class OfferRepository extends BaseRepository {
187
222
  acceptedAt: new Date(),
188
223
  respondedAt: new Date(),
189
224
  ...(checkoutDeadline ? { checkoutDeadline } : {}),
190
- });
225
+ }, { actor: { role: "buyer" }, trigger: "acceptCounterOffer", ...ctx }, offer);
191
226
  }
192
- async withdraw(offerId) {
227
+ /**
228
+ * Two distinct events share this method:
229
+ * - the buyer walks away → status alone
230
+ * - the buyer counters → status + `supersededByOfferId`
231
+ *
232
+ * The second is what makes the chain walkable forward, and it is ONE write
233
+ * with ONE history entry. A `withdrawn` document carrying
234
+ * `supersededByOfferId` renders as *Superseded* (neutral) rather than
235
+ * *Withdrawn* (negative) — the negotiation continued, it did not end.
236
+ */
237
+ async withdraw(offerId, supersededByOfferId, ctx, prior) {
193
238
  return this.updateStatus(offerId, {
194
239
  status: "withdrawn",
195
240
  respondedAt: new Date(),
196
- });
241
+ ...(supersededByOfferId ? { supersededByOfferId } : {}),
242
+ }, {
243
+ actor: { role: "buyer" },
244
+ trigger: supersededByOfferId ? "counterOfferByBuyer:supersede" : "withdrawOffer",
245
+ ...(supersededByOfferId ? { reason: "Superseded by the buyer's counter" } : {}),
246
+ ...ctx,
247
+ }, prior);
197
248
  }
198
249
  /**
199
250
  * Terminal transition, called once the buyer's order for this offer exists.
@@ -203,12 +254,35 @@ class OfferRepository extends BaseRepository {
203
254
  * client-side patch in UserOffersPanel, which meant a reload showed the offer
204
255
  * back at "accepted" and it could be added to the cart and ordered again.
205
256
  */
206
- async markPaid(offerId, orderId) {
257
+ async markPaid(offerId, orderId, prior) {
207
258
  return this.updateStatus(offerId, {
208
259
  status: "paid",
209
260
  paidOrderId: orderId,
210
261
  paidAt: new Date(),
211
- });
262
+ }, { actor: { role: "system" }, trigger: "finalizeLockedLines" }, prior);
263
+ }
264
+ /**
265
+ * Every round of one negotiation, oldest first.
266
+ *
267
+ * A single-field equality — served by Firestore's AUTOMATIC index, so this
268
+ * adds no composite index and none of the existing `offers` indexes is
269
+ * affected. The sort is done in memory because the result is at most 3 rows
270
+ * (the per-buyer offer limit), which is cheaper than the index a compound
271
+ * orderBy would demand.
272
+ *
273
+ * Offers written before `chainRootOfferId` existed return ZERO rows, not
274
+ * one — callers must fall back to `[offer]` and never read empty as
275
+ * "this offer is missing".
276
+ */
277
+ async findChain(chainRootOfferId) {
278
+ const snapshot = await this.db
279
+ .collection(this.collection)
280
+ .where(OFFER_FIELDS.CHAIN_ROOT_OFFER_ID, "==", chainRootOfferId)
281
+ .limit(OFFER_CHAIN_MAX)
282
+ .get();
283
+ return snapshot.docs
284
+ .map((doc) => this.mapDoc(doc))
285
+ .sort((a, b) => (a.counterRound ?? 1) - (b.counterRound ?? 1));
212
286
  }
213
287
  /**
214
288
  * Cloud Functions compatibility: pending/countered offers already expired.
@@ -237,14 +311,44 @@ class OfferRepository extends BaseRepository {
237
311
  .get();
238
312
  return snapshot.docs.map((doc) => this.mapDoc(doc));
239
313
  }
240
- async expireMany(offerIds) {
314
+ /**
315
+ * Bulk-expire, taking the DOCUMENTS rather than their ids.
316
+ *
317
+ * The signature changed deliberately. `arrayUnion` would let a batch append
318
+ * without a read, but it **cannot enforce the FIFO cap** — so history would
319
+ * grow unbounded on exactly the path that touches the most documents at
320
+ * once. Folding entries in memory and writing whole arrays keeps the cap,
321
+ * and costs **zero extra reads**: `findExpiredActive`, `findExpiredAccepted`
322
+ * and the admin cancel route all already hold full `OfferDocument`s.
323
+ *
324
+ * Also carries the admin path: a reasoned cancel is an expiry with an actor
325
+ * and a reason, recorded in the offer's own history as well as in
326
+ * `adminAuditLog`.
327
+ */
328
+ async expireMany(offers, ctx) {
329
+ if (offers.length === 0)
330
+ return;
241
331
  const batch = this.db.batch();
242
332
  const now = new Date();
243
- for (const id of offerIds) {
244
- batch.update(this.db.collection(this.collection).doc(id), {
245
- [OFFER_FIELDS.STATUS]: "expired",
246
- [OFFER_FIELDS.UPDATED_AT]: now,
333
+ for (const offer of offers) {
334
+ const patch = {
335
+ status: "expired",
336
+ updatedAt: now,
337
+ ...(ctx?.cancelledByAdminUid
338
+ ? { cancelledByAdminUid: ctx.cancelledByAdminUid, cancelReason: ctx.reason }
339
+ : {}),
340
+ };
341
+ const withEntry = withHistory(offer, patch, {
342
+ tracked: OFFER_TRACKED_FIELDS,
343
+ actor: ctx?.actor ?? { role: "system" },
344
+ trigger: ctx?.trigger ?? "runOfferExpiry",
345
+ reason: ctx?.reason,
346
+ piiFields: OFFER_PII_FIELDS,
347
+ historyField: OFFER_FIELDS.STATUS_HISTORY,
348
+ truncatedField: OFFER_FIELDS.STATUS_HISTORY_TRUNCATED,
349
+ now,
247
350
  });
351
+ batch.update(this.db.collection(this.collection).doc(offer.id), (withEntry ?? patch));
248
352
  }
249
353
  await batch.commit();
250
354
  }
@@ -0,0 +1,72 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * 🛑 `annotate()` must be the OUTERMOST call on each field — it keys a WeakMap
4
+ * by schema instance and every zod wrapper returns a new one.
5
+ */
6
+ export declare const sellerCouponFormSchema: z.ZodEffects<z.ZodObject<{
7
+ code: z.ZodString;
8
+ type: z.ZodEnum<["percentage", "fixed", "free_shipping"]>;
9
+ value: z.ZodEffects<z.ZodString, string, string>;
10
+ minPurchase: z.ZodEffects<z.ZodString, string, string>;
11
+ maxDiscount: z.ZodEffects<z.ZodString, string, string>;
12
+ totalLimit: z.ZodEffects<z.ZodString, string, string>;
13
+ perUserLimit: z.ZodEffects<z.ZodString, string, string>;
14
+ startDate: z.ZodString;
15
+ endDate: z.ZodString;
16
+ isActive: z.ZodBoolean;
17
+ applicableProducts: z.ZodArray<z.ZodString, "many">;
18
+ applicableCategories: z.ZodArray<z.ZodString, "many">;
19
+ }, "strip", z.ZodTypeAny, {
20
+ value: string;
21
+ code: string;
22
+ type: "fixed" | "percentage" | "free_shipping";
23
+ isActive: boolean;
24
+ endDate: string;
25
+ maxDiscount: string;
26
+ minPurchase: string;
27
+ applicableProducts: string[];
28
+ applicableCategories: string[];
29
+ totalLimit: string;
30
+ perUserLimit: string;
31
+ startDate: string;
32
+ }, {
33
+ value: string;
34
+ code: string;
35
+ type: "fixed" | "percentage" | "free_shipping";
36
+ isActive: boolean;
37
+ endDate: string;
38
+ maxDiscount: string;
39
+ minPurchase: string;
40
+ applicableProducts: string[];
41
+ applicableCategories: string[];
42
+ totalLimit: string;
43
+ perUserLimit: string;
44
+ startDate: string;
45
+ }>, {
46
+ value: string;
47
+ code: string;
48
+ type: "fixed" | "percentage" | "free_shipping";
49
+ isActive: boolean;
50
+ endDate: string;
51
+ maxDiscount: string;
52
+ minPurchase: string;
53
+ applicableProducts: string[];
54
+ applicableCategories: string[];
55
+ totalLimit: string;
56
+ perUserLimit: string;
57
+ startDate: string;
58
+ }, {
59
+ value: string;
60
+ code: string;
61
+ type: "fixed" | "percentage" | "free_shipping";
62
+ isActive: boolean;
63
+ endDate: string;
64
+ maxDiscount: string;
65
+ minPurchase: string;
66
+ applicableProducts: string[];
67
+ applicableCategories: string[];
68
+ totalLimit: string;
69
+ perUserLimit: string;
70
+ startDate: string;
71
+ }>;
72
+ export type SellerCouponFormValues = z.infer<typeof sellerCouponFormSchema>;
@@ -0,0 +1,116 @@
1
+ /*
2
+ * WHY: `SellerCouponEditorView` rendered `<Form onSubmit noValidate>` with no
3
+ * schema at all, while `AdminCouponEditorView` — the same entity, one
4
+ * portal over — has a Zod schema wired through `<Form schema>`. The
5
+ * seller side relied on four hand-rolled `if` checks that covered
6
+ * presence and date order and nothing else, so a coupon could be saved
7
+ * with a **500% discount**, a non-numeric value, a negative minimum spend
8
+ * or a per-user limit above the total limit. Every failure surfaced in a
9
+ * single banner rather than on the field that caused it.
10
+ * WHAT: The seller coupon form's schema.
11
+ *
12
+ * ## Everything is a string, on purpose
13
+ *
14
+ * `CouponEditorDraft` holds every numeric field as a string because they come
15
+ * from `<input>`s. Coercing inside the schema keeps that boundary in one place
16
+ * — the alternative is a `Number()` at each call site, which is how a `NaN`
17
+ * reaches Firestore the first time one is forgotten.
18
+ *
19
+ * ## Why the rules are conditional
20
+ *
21
+ * `free_shipping` has no discount value, `percentage` is bounded 1–100, and
22
+ * `fixed` is any positive amount. A flat per-field rule cannot say that, so
23
+ * the cross-field rules live in one `superRefine` keyed by type.
24
+ *
25
+ * EXPORTS:
26
+ * sellerCouponFormSchema, type SellerCouponFormValues
27
+ *
28
+ * @tag domain:seller,promotions
29
+ * @tag layer:schema
30
+ * @tag pattern:none
31
+ * @tag access:isomorphic
32
+ * @tag consumers:SellerCouponEditorView
33
+ * @tag sideEffects:none
34
+ */
35
+ import { z } from "zod";
36
+ import { annotate } from "../../shell/field-ui-meta";
37
+ /** A blank optional numeric string — the form's "not set". */
38
+ const numericString = (label) => z
39
+ .string()
40
+ .refine((v) => v.trim() === "" || (!Number.isNaN(Number(v)) && Number(v) >= 0), {
41
+ message: `${label} must be a number of zero or more.`,
42
+ });
43
+ /**
44
+ * 🛑 `annotate()` must be the OUTERMOST call on each field — it keys a WeakMap
45
+ * by schema instance and every zod wrapper returns a new one.
46
+ */
47
+ export const sellerCouponFormSchema = z
48
+ .object({
49
+ code: annotate(z
50
+ .string()
51
+ .min(2, "A coupon code needs at least 2 characters.")
52
+ .max(40, "Keep the code under 40 characters.")
53
+ .regex(/^[A-Z0-9_-]+$/i, "Use letters, numbers, hyphens and underscores only."), {
54
+ section: "basics", sectionLabel: "Coupon", sectionRequired: true,
55
+ quick: true, order: 1, row: "pair",
56
+ }),
57
+ type: annotate(z.enum(["percentage", "fixed", "free_shipping"]), {
58
+ section: "basics", quick: true, order: 2, row: "pair",
59
+ }),
60
+ value: annotate(numericString("The discount value"), {
61
+ section: "basics", quick: true, order: 3, row: "pair",
62
+ }),
63
+ minPurchase: annotate(numericString("Minimum purchase"), {
64
+ section: "limits", sectionLabel: "Limits", order: 1, row: "pair",
65
+ }),
66
+ maxDiscount: annotate(numericString("Maximum discount"), {
67
+ section: "limits", order: 2, row: "pair",
68
+ }),
69
+ totalLimit: annotate(numericString("Total uses"), {
70
+ section: "limits", order: 3, row: "pair",
71
+ }),
72
+ perUserLimit: annotate(numericString("Uses per customer"), {
73
+ section: "limits", order: 4, row: "pair",
74
+ }),
75
+ startDate: annotate(z.string().min(1, "A start date is required."), {
76
+ section: "validity", sectionLabel: "Validity", quick: true, order: 1, row: "pair", kind: "date",
77
+ }),
78
+ endDate: annotate(z.string().min(1, "An end date is required."), {
79
+ section: "validity", quick: true, order: 2, row: "pair", kind: "date",
80
+ }),
81
+ isActive: annotate(z.boolean(), {
82
+ section: "validity", order: 3, row: "quarter",
83
+ }),
84
+ applicableProducts: annotate(z.array(z.string()), {
85
+ section: "scope", sectionLabel: "Applies to", order: 1, row: "full", kind: "list",
86
+ }),
87
+ applicableCategories: annotate(z.array(z.string()), {
88
+ section: "scope", order: 2, row: "full", kind: "list",
89
+ }),
90
+ })
91
+ .superRefine((v, ctx) => {
92
+ const issue = (path, message) => ctx.addIssue({ code: z.ZodIssueCode.custom, path: [path], message });
93
+ // A discount value is required for everything except free shipping, and
94
+ // a percentage above 100 is not a discount — it is the store paying the
95
+ // customer. Nothing bounded this before.
96
+ if (v.type !== "free_shipping") {
97
+ const amount = Number(v.value);
98
+ if (v.value.trim() === "")
99
+ issue("value", "A discount value is required.");
100
+ else if (amount <= 0)
101
+ issue("value", "The discount must be greater than zero.");
102
+ else if (v.type === "percentage" && amount > 100) {
103
+ issue("value", "A percentage discount cannot exceed 100%.");
104
+ }
105
+ }
106
+ if (v.startDate && v.endDate && v.startDate > v.endDate) {
107
+ issue("endDate", "The end date must be after the start date.");
108
+ }
109
+ // A per-customer limit above the total makes the total unreachable — the
110
+ // coupon reads as more generous than it can ever be.
111
+ const total = Number(v.totalLimit);
112
+ const perUser = Number(v.perUserLimit);
113
+ if (v.totalLimit.trim() && v.perUserLimit.trim() && perUser > total) {
114
+ issue("perUserLimit", "Uses per customer cannot exceed the total uses.");
115
+ }
116
+ });
@@ -3,6 +3,7 @@
3
3
  */
4
4
  import { type GenerateOfferIdInput } from "../../../utils/id-generators";
5
5
  import type { BaseDocument } from "../../../_internal/shared/types/base-document";
6
+ import type { StatusChangeEntry } from "../../../_internal/shared/history/index";
6
7
  export type OfferStatus = "pending" | "accepted" | "declined" | "countered" | "expired" | "withdrawn" | "paid";
7
8
  /** Runtime-accessible offer status values — use instead of bare string literals. */
8
9
  export declare const OfferStatusValues: {
@@ -39,7 +40,52 @@ export interface OfferDocument extends BaseDocument {
39
40
  /** Set by `markPaid` once the buyer's order exists — makes "paid" auditable. */
40
41
  paidOrderId?: string;
41
42
  paidAt?: Date;
43
+ /**
44
+ * Append-only delta log — the canonical record of when things happened to
45
+ * this offer. Shares `StatusChangeEntry` with `OrderDocument`; an offer is
46
+ * an order with more phases in front of it.
47
+ *
48
+ * `respondedAt` is deliberately kept but NOT extended: it means four
49
+ * different events (seller-accept, seller-decline, seller-counter,
50
+ * buyer-withdraw) and survives only as the legacy fallback for offers
51
+ * written before this field existed.
52
+ */
53
+ statusHistory?: StatusChangeEntry[];
54
+ /** Entries dropped off the front — so the UI never implies "this is all of it". */
55
+ statusHistoryTruncated?: number;
56
+ /**
57
+ * The multi-round chain. A counter-offer is a NEW document, so without
58
+ * these three a three-round negotiation is three unrelated records —
59
+ * which is what it is today: `previousOfferId` currently exists only
60
+ * inside a `serverLogger.info` call and never reaches Firestore.
61
+ */
62
+ previousOfferId?: string;
63
+ supersededByOfferId?: string;
64
+ /**
65
+ * Denormalised root of the chain (the root stores its own id) so a list row
66
+ * renders "Round 2" with ZERO extra reads, and `findChain` is a single-field
67
+ * equality served by the automatic index — no composite index needed.
68
+ */
69
+ chainRootOfferId?: string;
70
+ /** 1-based. Absent means round 1. */
71
+ counterRound?: number;
72
+ /**
73
+ * Admin escalation. The store keeps sole authority to accept/counter/
74
+ * decline; an admin may only cancel, with a reason, and it is recorded
75
+ * here as well as in `adminAuditLog`.
76
+ */
77
+ cancelledByAdminUid?: string;
78
+ cancelReason?: string;
42
79
  }
80
+ /**
81
+ * Fields whose changes are recorded in an offer's `statusHistory`.
82
+ *
83
+ * Excludes `buyerName`/`buyerEmail` (PII — `encryptPiiFields` never descends
84
+ * into arrays, so they would persist in plaintext inside the history),
85
+ * `buyerNote`/`sellerNote` (they ride in the entry's `note`), and
86
+ * `updatedAt`/`respondedAt` (noise that would burn the FIFO cap).
87
+ */
88
+ export declare const OFFER_TRACKED_FIELDS: readonly ["status", "offerAmount", "counterAmount", "lockedPrice", "checkoutDeadline", "paidOrderId", "supersededByOfferId", "previousOfferId"];
43
89
  export declare const OFFER_COLLECTION: "offers";
44
90
  export declare const OFFER_INDEXED_FIELDS: readonly ["buyerUid", "storeId", "productId", "status", "createdAt", "expiresAt", "checkoutDeadline"];
45
91
  export declare const OFFER_FIELDS: {
@@ -67,9 +113,17 @@ export declare const OFFER_FIELDS: {
67
113
  readonly PAID_AT: "paidAt";
68
114
  readonly CREATED_AT: "createdAt";
69
115
  readonly UPDATED_AT: "updatedAt";
116
+ readonly STATUS_HISTORY: "statusHistory";
117
+ readonly STATUS_HISTORY_TRUNCATED: "statusHistoryTruncated";
118
+ readonly PREVIOUS_OFFER_ID: "previousOfferId";
119
+ readonly SUPERSEDED_BY_OFFER_ID: "supersededByOfferId";
120
+ readonly CHAIN_ROOT_OFFER_ID: "chainRootOfferId";
121
+ readonly COUNTER_ROUND: "counterRound";
122
+ readonly CANCELLED_BY_ADMIN_UID: "cancelledByAdminUid";
123
+ readonly CANCEL_REASON: "cancelReason";
70
124
  };
71
- export type OfferCreateInput = Pick<OfferDocument, "productId" | "productTitle" | "productSlug" | "productImageUrl" | "buyerUid" | "buyerName" | "buyerEmail" | "storeId" | "storeName" | "offerAmount" | "listedPrice" | "currency" | "buyerNote">;
72
- export type OfferUpdateInput = Partial<Pick<OfferDocument, "status" | "counterAmount" | "lockedPrice" | "sellerNote" | "acceptedAt" | "checkoutDeadline" | "respondedAt" | "paidOrderId" | "paidAt" | "updatedAt">>;
125
+ export type OfferCreateInput = Pick<OfferDocument, "productId" | "productTitle" | "productSlug" | "productImageUrl" | "buyerUid" | "buyerName" | "buyerEmail" | "storeId" | "storeName" | "offerAmount" | "listedPrice" | "currency" | "buyerNote" | "previousOfferId" | "chainRootOfferId" | "counterRound">;
126
+ export type OfferUpdateInput = Partial<Pick<OfferDocument, "status" | "counterAmount" | "lockedPrice" | "sellerNote" | "acceptedAt" | "checkoutDeadline" | "respondedAt" | "paidOrderId" | "paidAt" | "updatedAt" | "statusHistory" | "statusHistoryTruncated" | "supersededByOfferId" | "cancelledByAdminUid" | "cancelReason">>;
73
127
  export declare const offerQueryHelpers: {
74
128
  readonly byBuyer: (buyerUid: string) => readonly ["buyerUid", "==", string];
75
129
  readonly byStore: (storeId: string) => readonly ["storeId", "==", string];
@@ -12,6 +12,24 @@ export const OfferStatusValues = {
12
12
  WITHDRAWN: "withdrawn",
13
13
  PAID: "paid",
14
14
  };
15
+ /**
16
+ * Fields whose changes are recorded in an offer's `statusHistory`.
17
+ *
18
+ * Excludes `buyerName`/`buyerEmail` (PII — `encryptPiiFields` never descends
19
+ * into arrays, so they would persist in plaintext inside the history),
20
+ * `buyerNote`/`sellerNote` (they ride in the entry's `note`), and
21
+ * `updatedAt`/`respondedAt` (noise that would burn the FIFO cap).
22
+ */
23
+ export const OFFER_TRACKED_FIELDS = [
24
+ "status",
25
+ "offerAmount",
26
+ "counterAmount",
27
+ "lockedPrice",
28
+ "checkoutDeadline",
29
+ "paidOrderId",
30
+ "supersededByOfferId",
31
+ "previousOfferId",
32
+ ];
15
33
  export const OFFER_COLLECTION = "offers";
16
34
  export const OFFER_INDEXED_FIELDS = [
17
35
  "buyerUid",
@@ -47,6 +65,15 @@ export const OFFER_FIELDS = {
47
65
  PAID_AT: "paidAt",
48
66
  CREATED_AT: "createdAt",
49
67
  UPDATED_AT: "updatedAt",
68
+ // Negotiation history + chain (W2)
69
+ STATUS_HISTORY: "statusHistory",
70
+ STATUS_HISTORY_TRUNCATED: "statusHistoryTruncated",
71
+ PREVIOUS_OFFER_ID: "previousOfferId",
72
+ SUPERSEDED_BY_OFFER_ID: "supersededByOfferId",
73
+ CHAIN_ROOT_OFFER_ID: "chainRootOfferId",
74
+ COUNTER_ROUND: "counterRound",
75
+ CANCELLED_BY_ADMIN_UID: "cancelledByAdminUid",
76
+ CANCEL_REASON: "cancelReason",
50
77
  };
51
78
  export const offerQueryHelpers = {
52
79
  byBuyer: (buyerUid) => ["buyerUid", "==", buyerUid],
@@ -89,8 +89,8 @@ export declare const sellerStoreSchema: z.ZodObject<{
89
89
  id: string;
90
90
  createdAt: string;
91
91
  updatedAt: string;
92
- ownerId: string;
93
92
  storeName: string;
93
+ ownerId: string;
94
94
  storeSlug: string;
95
95
  isPublic: boolean;
96
96
  location?: string | undefined;
@@ -121,8 +121,8 @@ export declare const sellerStoreSchema: z.ZodObject<{
121
121
  id: string;
122
122
  createdAt: string;
123
123
  updatedAt: string;
124
- ownerId: string;
125
124
  storeName: string;
125
+ ownerId: string;
126
126
  storeSlug: string;
127
127
  isPublic: boolean;
128
128
  location?: string | undefined;