@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
@@ -96,8 +96,56 @@ const api = {
96
96
  // `audit-form-schema` already enforces presence at FormShell sites; this
97
97
  // registry centralizes the schemas for cross-route reuse.
98
98
  // ---------------------------------------------------------------------------
99
+ /**
100
+ * Entity id -> the form schema that validates it.
101
+ *
102
+ * Populated one entity per slice from W4 on. Deliberately NOT pre-declared
103
+ * with placeholder keys: an empty bucket is honest, and a bucket full of
104
+ * `undefined` reads as "migrated" to everyone who comes after — this codebase
105
+ * already documents eight abandoned migrations that started exactly that way.
106
+ *
107
+ * `roundtrip-diff` reads this. An entity registered here is checked, on every
108
+ * run, against real documents for silent field loss; an entity absent from it
109
+ * has no such protection, which is the whole reason to register early.
110
+ */
111
+ import { blogDraftSchema } from "../features/blog/schemas/blog-form";
112
+ import { eventDraftSchema } from "../features/events/schemas/event-form";
113
+ import { productFeatureFormSchema } from "../features/products/schemas/product-features.validators";
114
+ import { storeCategoryFormSchema } from "../features/store-extensions/schemas/store-category-form";
115
+ import { listingTemplateFormSchema } from "../features/store-extensions/schemas/listing-template-form";
116
+ import { payoutMethodFormSchema } from "../features/store-extensions/schemas/payout-method-form";
117
+ import { shippingConfigFormSchema } from "../features/store-extensions/schemas/shipping-config-form";
118
+ import { groupedListingFormSchema } from "../features/grouped/schemas/grouped-listing-form";
119
+ import { sellerCouponFormSchema } from "../features/seller/schemas/coupon-form";
120
+ import { customRoleFormSchema } from "../features/store-extensions/schemas/custom-role-form";
121
+ import { moderationReviewFormSchema } from "../features/store-extensions/schemas/moderation-review-form";
122
+ import { reportReviewFormSchema } from "../features/store-extensions/schemas/report-review-form";
123
+ import { reportCreateSchema } from "../features/store-extensions/schemas/report-create-form";
124
+ import { itemRequestCreateSchema } from "../features/store-extensions/schemas/item-request-create-form";
125
+ import { analyticsAlertCreateSchema, analyticsCardCreateSchema, } from "../features/store-extensions/schemas/analytics-forms";
126
+ import { storeGoogleConfigUpdateSchema } from "../features/store-extensions/schemas/google-config-form";
127
+ import { adminNotificationCreateSchema } from "../features/store-extensions/schemas/admin-notification-form";
128
+ import { homepageSectionCreateSchema } from "../features/homepage/schemas/index";
99
129
  const forms = {
100
- // Populated in W5 as section builders + admin forms are tightened.
130
+ blog: blogDraftSchema,
131
+ event: eventDraftSchema,
132
+ productFeature: productFeatureFormSchema,
133
+ storeCategory: storeCategoryFormSchema,
134
+ listingTemplate: listingTemplateFormSchema,
135
+ payoutMethod: payoutMethodFormSchema,
136
+ shippingConfig: shippingConfigFormSchema,
137
+ groupedListing: groupedListingFormSchema,
138
+ sellerCoupon: sellerCouponFormSchema,
139
+ customRole: customRoleFormSchema,
140
+ moderationReview: moderationReviewFormSchema,
141
+ reportReview: reportReviewFormSchema,
142
+ reportCreate: reportCreateSchema,
143
+ itemRequest: itemRequestCreateSchema,
144
+ analyticsAlert: analyticsAlertCreateSchema,
145
+ analyticsCard: analyticsCardCreateSchema,
146
+ storeGoogleConfig: storeGoogleConfigUpdateSchema,
147
+ adminNotification: adminNotificationCreateSchema,
148
+ homepageSection: homepageSectionCreateSchema,
101
149
  };
102
150
  // ---------------------------------------------------------------------------
103
151
  // Sieve filter-grammar schemas — populated by W6. Each collection registers
@@ -161,6 +209,21 @@ export function lookupApiSchema(key) {
161
209
  }
162
210
  return entry;
163
211
  }
212
+ /**
213
+ * The form schema for an entity.
214
+ *
215
+ * Throws rather than returning undefined: a caller asking for a schema it
216
+ * expects to exist should fail loudly at the call site, not silently skip
217
+ * validation and strip every field on the next save.
218
+ */
219
+ export function lookupFormSchema(entity) {
220
+ const entry = SCHEMAS.forms[entity];
221
+ if (!entry) {
222
+ throw new Error(`[schemas] No form schema registered for "${String(entity)}". ` +
223
+ `Register it in SCHEMAS.forms as part of that entity's slice.`);
224
+ }
225
+ return entry;
226
+ }
164
227
  export function lookupFirestoreSchema(collection) {
165
228
  const entry = SCHEMAS.firestore[collection];
166
229
  if (!entry) {
@@ -143,5 +143,35 @@ const _rawBidsSeedData = [
143
143
  currentBid: 1450,
144
144
  bidderIds: ["user-ananya-collector", "user-rohit-collector", "user-meera-bey"],
145
145
  }),
146
+ // auction-beyblade-burst-spriggan-requiem-bought-out — a BUYOUT ended it.
147
+ //
148
+ // Three competitive bids that lost, then the buyout that won. A buyout is a
149
+ // real BidDocument (`isBuyout: true`) — it is not a bid-free purchase, which
150
+ // is why the order's `sourceContext.bidCount` is 3 rather than 0: the buyout
151
+ // itself is excluded from the "against N bidders" figure, the three it beat
152
+ // are not. `standingBidAtBuyout` is read from these bids and NOT from
153
+ // `product.currentBid`, because a pending buyout never writes that mirror.
154
+ ...buildLadder({
155
+ productId: "auction-beyblade-burst-spriggan-requiem-bought-out",
156
+ productTitle: "Beyblade Burst B-128 Spriggan Requiem (Ended — Bought Out)",
157
+ startingBid: 2499,
158
+ currentBid: 3100,
159
+ bidderIds: ["user-meera-bey", "user-ananya-collector", "user-rohit-collector"],
160
+ closed: true,
161
+ }).map((b) => ({ ...b, status: "lost", isWinning: false })),
162
+ {
163
+ id: "bid-beyblade-burst-spriggan-requiem-seto-kaiba-20260820-buyout",
164
+ productId: "auction-beyblade-burst-spriggan-requiem-bought-out",
165
+ productTitle: "Beyblade Burst B-128 Spriggan Requiem (Ended — Bought Out)",
166
+ userId: "user-seto-kaiba",
167
+ userName: "Mock User 2",
168
+ bidAmount: 4999,
169
+ status: "won",
170
+ isWinning: true,
171
+ isBuyout: true,
172
+ bidDate: daysAgo(4),
173
+ createdAt: daysAgo(4),
174
+ orderId: "order-1-20260820-buyout",
175
+ },
146
176
  ];
147
177
  export const bidsSeedData = _rawBidsSeedData.map(withBidDefaults);
@@ -1,6 +1,29 @@
1
1
  /*
2
- * WHY: Seeds offers for the Beyblade marketplace — 8 offers across all statuses.
3
- * WHAT: pending (2), accepted (1), declined (1), countered (1), expired (1), withdrawn (1), paid (1).
2
+ * WHY: Seeds offers for the Beyblade marketplace — every status, plus a real
3
+ * multi-round negotiation. Before W2 a counter-offer created an unlinked
4
+ * document, so no fixture could demonstrate a chain and the timeline had
5
+ * nothing to render.
6
+ * WHAT: 10 offers. pending (2), accepted (1), declined (1), countered (1),
7
+ * expired (1), withdrawn (3 — one a genuine walk-away, two superseded
8
+ * rounds of the Metal Storm Pegasus chain), paid (1).
9
+ *
10
+ * ## The Metal Storm Pegasus chain
11
+ *
12
+ * Three linked documents ending at the pre-existing `…-countered` id, so the
13
+ * final round keeps the id every other fixture and checklist case already
14
+ * refers to and `appkit-seed delete` still cleans the whole chain up:
15
+ *
16
+ * …-r1 (withdrawn, superseded by r2) → …-r2 (withdrawn, superseded by the
17
+ * countered offer) → …-countered (round 3, live)
18
+ *
19
+ * Two withdrawn plus one live keeps `hasActiveOffer` consistent, and three
20
+ * rounds makes the 3-offer chain cap demonstrable.
21
+ *
22
+ * ## One fixture deliberately has NO statusHistory
23
+ *
24
+ * `offer-kaiba-x-wizard-arrow-expired` is left history-less on purpose — see
25
+ * the comment at that fixture. It is the only way the timeline's legacy branch
26
+ * is testable, and it is the exact shape of every offer written before W2.
4
27
  *
5
28
  * EXPORTS:
6
29
  * offersSeedData — Array of Partial<OfferDocument> for seed runner
@@ -16,6 +39,23 @@ import { OfferStatusValues } from "../features/seller/schemas/firestore";
16
39
  const NOW = new Date();
17
40
  const daysAgo = (n) => new Date(NOW.getTime() - n * 86400000);
18
41
  const daysFromNow = (n) => new Date(NOW.getTime() + n * 86400000);
42
+ /**
43
+ * One history entry, in the same delta shape `withHistory()` produces at
44
+ * runtime — so a seeded timeline and a real one render identically.
45
+ *
46
+ * `actorUid` is the only identity a history entry may ever carry. Never a name
47
+ * or an email: `encryptPiiFields` is a flat top-level loop that does not
48
+ * descend into arrays, so a PII value placed inside `statusHistory` would be
49
+ * written to Firestore in plaintext and never decrypted on the way back out.
50
+ */
51
+ function entry(at, actorRole, trigger, changes, extra = {}) {
52
+ return { at, actorRole, trigger, changes, ...extra };
53
+ }
54
+ const BUYER_YUGI = "user-yugi-muto";
55
+ const BUYER_KAIBA = "user-seto-kaiba";
56
+ const BUYER_ADMIN = "user-admin-letitrip";
57
+ const STORE = "store-beyblade-arena";
58
+ const PEGASUS_CHAIN_ROOT = "offer-yugi-metal-storm-pegasus-r1";
19
59
  export const offersSeedData = [
20
60
  // ── 1. PENDING — Rehan → Beyblade Arena — Burst Valkyrie ────────────────
21
61
  {
@@ -35,6 +75,12 @@ export const offersSeedData = [
35
75
  expiresAt: daysFromNow(3),
36
76
  createdAt: daysAgo(0),
37
77
  updatedAt: daysAgo(0),
78
+ statusHistory: [
79
+ entry(daysAgo(0), "buyer", "makeOffer", {
80
+ status: { from: null, to: "pending" },
81
+ offerAmount: { from: null, to: 780 },
82
+ }, { actorUid: BUYER_YUGI, note: "Would you accept ₹780 for the Burst Valkyrie?" }),
83
+ ],
38
84
  },
39
85
  // ── 2. COUNTERED — Rehan → Beyblade Arena — Metal Storm Pegasus ─────────
40
86
  {
@@ -57,6 +103,107 @@ export const offersSeedData = [
57
103
  respondedAt: daysAgo(1),
58
104
  createdAt: daysAgo(3),
59
105
  updatedAt: daysAgo(1),
106
+ // Round 3 — the live end of the chain. Keeping the original id here (rather
107
+ // than appending an `…-r3`) means every existing reference and checklist
108
+ // href still resolves, and `appkit-seed delete` still removes the lot.
109
+ previousOfferId: "offer-yugi-metal-storm-pegasus-r2",
110
+ chainRootOfferId: PEGASUS_CHAIN_ROOT,
111
+ counterRound: 3,
112
+ statusHistory: [
113
+ entry(daysAgo(3), "buyer", "counterOfferByBuyer", {
114
+ status: { from: null, to: "pending" },
115
+ offerAmount: { from: 900, to: 1000 },
116
+ previousOfferId: { from: null, to: "offer-yugi-metal-storm-pegasus-r2" },
117
+ }, { actorUid: BUYER_YUGI, note: "₹1,000 — will you take it?" }),
118
+ entry(daysAgo(1), "seller", "respondToOffer:counter", {
119
+ status: { from: "pending", to: "countered" },
120
+ counterAmount: { from: null, to: 1150 },
121
+ }, { actorUid: STORE, note: "Best I can do is ₹1,150." }),
122
+ ],
123
+ },
124
+ // ── 2a. Metal Storm Pegasus — ROUND 1 (superseded, not a walk-away) ─────
125
+ {
126
+ id: PEGASUS_CHAIN_ROOT,
127
+ productId: "product-beyblade-metal-storm-pegasus",
128
+ productTitle: "Beyblade Metal Storm Pegasus",
129
+ storeId: STORE,
130
+ storeName: "Beyblade Arena",
131
+ buyerUid: BUYER_YUGI,
132
+ buyerName: "Mock User 3",
133
+ buyerEmail: "rehan.sheikh@gmail.com",
134
+ listedPrice: 1299,
135
+ offerAmount: 800,
136
+ counterAmount: 1200,
137
+ currency: "INR",
138
+ // `withdrawn` PLUS `supersededByOfferId` is what makes the timeline render
139
+ // this round neutral ("Superseded") instead of negative ("Withdrawn").
140
+ // `superseded` is a render key, deliberately never an eighth OfferStatus —
141
+ // an eighth value would add a filter chip matching zero stored rows.
142
+ status: OfferStatusValues.WITHDRAWN,
143
+ buyerNote: "Starting at ₹800 for the Storm Pegasus.",
144
+ sellerNote: "₹1,200 and it's yours.",
145
+ expiresAt: daysAgo(6),
146
+ respondedAt: daysAgo(7),
147
+ createdAt: daysAgo(9),
148
+ updatedAt: daysAgo(7),
149
+ supersededByOfferId: "offer-yugi-metal-storm-pegasus-r2",
150
+ chainRootOfferId: PEGASUS_CHAIN_ROOT,
151
+ counterRound: 1,
152
+ statusHistory: [
153
+ entry(daysAgo(9), "buyer", "makeOffer", {
154
+ status: { from: null, to: "pending" },
155
+ offerAmount: { from: null, to: 800 },
156
+ }, { actorUid: BUYER_YUGI, note: "Starting at ₹800 for the Storm Pegasus." }),
157
+ entry(daysAgo(8), "seller", "respondToOffer:counter", {
158
+ status: { from: "pending", to: "countered" },
159
+ counterAmount: { from: null, to: 1200 },
160
+ }, { actorUid: STORE, note: "₹1,200 and it's yours." }),
161
+ entry(daysAgo(7), "buyer", "counterOfferByBuyer:supersede", {
162
+ status: { from: "countered", to: "withdrawn" },
163
+ supersededByOfferId: { from: null, to: "offer-yugi-metal-storm-pegasus-r2" },
164
+ }, { actorUid: BUYER_YUGI, reason: "Superseded by the buyer's counter" }),
165
+ ],
166
+ },
167
+ // ── 2b. Metal Storm Pegasus — ROUND 2 (superseded) ──────────────────
168
+ {
169
+ id: "offer-yugi-metal-storm-pegasus-r2",
170
+ productId: "product-beyblade-metal-storm-pegasus",
171
+ productTitle: "Beyblade Metal Storm Pegasus",
172
+ storeId: STORE,
173
+ storeName: "Beyblade Arena",
174
+ buyerUid: BUYER_YUGI,
175
+ buyerName: "Mock User 3",
176
+ buyerEmail: "rehan.sheikh@gmail.com",
177
+ listedPrice: 1299,
178
+ offerAmount: 900,
179
+ counterAmount: 1175,
180
+ currency: "INR",
181
+ status: OfferStatusValues.WITHDRAWN,
182
+ buyerNote: "Can you meet me at ₹900?",
183
+ sellerNote: "₹1,175 is as low as I can go.",
184
+ expiresAt: daysAgo(3),
185
+ respondedAt: daysAgo(3),
186
+ createdAt: daysAgo(7),
187
+ updatedAt: daysAgo(3),
188
+ previousOfferId: PEGASUS_CHAIN_ROOT,
189
+ supersededByOfferId: "offer-yugi-metal-storm-pegasus-countered",
190
+ chainRootOfferId: PEGASUS_CHAIN_ROOT,
191
+ counterRound: 2,
192
+ statusHistory: [
193
+ entry(daysAgo(7), "buyer", "counterOfferByBuyer", {
194
+ status: { from: null, to: "pending" },
195
+ offerAmount: { from: 800, to: 900 },
196
+ previousOfferId: { from: null, to: PEGASUS_CHAIN_ROOT },
197
+ }, { actorUid: BUYER_YUGI, note: "Can you meet me at ₹900?" }),
198
+ entry(daysAgo(4), "seller", "respondToOffer:counter", {
199
+ status: { from: "pending", to: "countered" },
200
+ counterAmount: { from: null, to: 1175 },
201
+ }, { actorUid: STORE, note: "₹1,175 is as low as I can go." }),
202
+ entry(daysAgo(3), "buyer", "counterOfferByBuyer:supersede", {
203
+ status: { from: "countered", to: "withdrawn" },
204
+ supersededByOfferId: { from: null, to: "offer-yugi-metal-storm-pegasus-countered" },
205
+ }, { actorUid: BUYER_YUGI, reason: "Superseded by the buyer's counter" }),
206
+ ],
60
207
  },
61
208
  // ── 3. PAID — Admin → Beyblade Arena — X Knife Shinobi (order placed) ───
62
209
  {
@@ -81,6 +228,19 @@ export const offersSeedData = [
81
228
  respondedAt: daysAgo(12),
82
229
  createdAt: daysAgo(14),
83
230
  updatedAt: daysAgo(11),
231
+ statusHistory: [
232
+ entry(daysAgo(14), "buyer", "makeOffer", {
233
+ status: { from: null, to: "pending" },
234
+ offerAmount: { from: null, to: 800 },
235
+ }, { actorUid: BUYER_ADMIN, note: "Would you accept ₹800 for the X Knife Shinobi?" }),
236
+ entry(daysAgo(12), "seller", "respondToOffer:accept", {
237
+ status: { from: "pending", to: "accepted" },
238
+ lockedPrice: { from: null, to: 800 },
239
+ }, { actorUid: STORE, note: "Accepted. Thank you!" }),
240
+ entry(daysAgo(11), "system", "finalizeLockedLines", {
241
+ status: { from: "accepted", to: "paid" },
242
+ }),
243
+ ],
84
244
  },
85
245
  // ── 4. DECLINED — Rehan → Beyblade Arena — Burst Regalia Genesis ────────
86
246
  {
@@ -96,12 +256,23 @@ export const offersSeedData = [
96
256
  offerAmount: 1050,
97
257
  currency: "INR",
98
258
  status: OfferStatusValues.DECLINED,
99
- buyerNote: "Offering ₹2,500 — slightly above my budget.",
100
- sellerNote: "Sorry, we cannot go below ₹3,200 for this piece.",
259
+ // Both notes quoted prices from the pre-Beyblade catalogue this seed was
260
+ // narrowed away from, so a ₹1,050 offer read as "offering ₹2,500".
261
+ buyerNote: "Offering ₹1,050 — slightly above my budget.",
262
+ sellerNote: "Sorry, we cannot go below ₹1,300 for this piece.",
101
263
  expiresAt: daysAgo(5),
102
264
  respondedAt: daysAgo(8),
103
265
  createdAt: daysAgo(10),
104
266
  updatedAt: daysAgo(8),
267
+ statusHistory: [
268
+ entry(daysAgo(10), "buyer", "makeOffer", {
269
+ status: { from: null, to: "pending" },
270
+ offerAmount: { from: null, to: 1050 },
271
+ }, { actorUid: BUYER_YUGI, note: "Offering ₹1,050 — slightly above my budget." }),
272
+ entry(daysAgo(8), "seller", "respondToOffer:decline", {
273
+ status: { from: "pending", to: "declined" },
274
+ }, { actorUid: STORE, note: "Sorry, we cannot go below ₹1,300 for this piece." }),
275
+ ],
105
276
  },
106
277
  // ── 5. EXPIRED — Vivaan → Beyblade Arena — X Wizard Arrow ───────────────
107
278
  {
@@ -117,10 +288,16 @@ export const offersSeedData = [
117
288
  offerAmount: 700,
118
289
  currency: "INR",
119
290
  status: OfferStatusValues.EXPIRED,
120
- buyerNote: "₹1,300 — let me know.",
291
+ buyerNote: "₹700 — let me know.",
121
292
  expiresAt: daysAgo(2),
122
293
  createdAt: daysAgo(9),
123
294
  updatedAt: daysAgo(2),
295
+ // NO `statusHistory`, deliberately. This is the ONLY fixture exercising the
296
+ // timeline's legacy branch, and it is exactly the shape of every offer
297
+ // written before W2. Its "Expired" step renders with an em-dash and no
298
+ // date — `expireMany` never wrote a timestamp, `updatedAt` means "last
299
+ // write of any kind", and `expiresAt` is a deadline rather than an event,
300
+ // so any date shown here would be fabricated. Do not "complete" this one.
124
301
  },
125
302
  // ── 6. WITHDRAWN — Admin → Beyblade Arena — Original Driger V ───────────
126
303
  {
@@ -136,10 +313,21 @@ export const offersSeedData = [
136
313
  offerAmount: 1450,
137
314
  currency: "INR",
138
315
  status: OfferStatusValues.WITHDRAWN,
139
- buyerNote: "Offering ₹1,600 for the Driger V.",
316
+ buyerNote: "Offering ₹1,450 for the Driger V.",
140
317
  expiresAt: daysAgo(3),
141
318
  createdAt: daysAgo(7),
142
319
  updatedAt: daysAgo(5),
320
+ // No `supersededByOfferId` — a genuine walk-away, so this renders as
321
+ // "Withdrawn" (negative). Contrast with the two Pegasus rounds above.
322
+ statusHistory: [
323
+ entry(daysAgo(7), "buyer", "makeOffer", {
324
+ status: { from: null, to: "pending" },
325
+ offerAmount: { from: null, to: 1450 },
326
+ }, { actorUid: BUYER_ADMIN, note: "Offering ₹1,450 for the Driger V." }),
327
+ entry(daysAgo(5), "buyer", "withdrawOffer", {
328
+ status: { from: "pending", to: "withdrawn" },
329
+ }, { actorUid: BUYER_ADMIN }),
330
+ ],
143
331
  },
144
332
  // ── 7. ACCEPTED — Vivaan → Beyblade Arena — Original Dranzer S (pending payment) ──
145
333
  {
@@ -167,6 +355,17 @@ export const offersSeedData = [
167
355
  respondedAt: daysAgo(0),
168
356
  createdAt: daysAgo(1),
169
357
  updatedAt: daysAgo(0),
358
+ statusHistory: [
359
+ entry(daysAgo(1), "buyer", "makeOffer", {
360
+ status: { from: null, to: "pending" },
361
+ offerAmount: { from: null, to: 1250 },
362
+ }, { actorUid: BUYER_KAIBA, note: "₹1,250 final offer on the sealed Dranzer S." }),
363
+ entry(daysAgo(0), "seller", "respondToOffer:accept", {
364
+ status: { from: "pending", to: "accepted" },
365
+ lockedPrice: { from: null, to: 1250 },
366
+ checkoutDeadline: { from: null, to: daysFromNow(2) },
367
+ }, { actorUid: STORE, note: "Accepted! Please complete payment within 24 hours." }),
368
+ ],
170
369
  },
171
370
  // ── 8. PENDING — Admin → Beyblade Arena — Metal Lightning L-Drago ───────
172
371
  {
@@ -182,9 +381,15 @@ export const offersSeedData = [
182
381
  offerAmount: 950,
183
382
  currency: "INR",
184
383
  status: OfferStatusValues.PENDING,
185
- buyerNote: "Interested in this piece. Would you accept ₹4,700?",
384
+ buyerNote: "Interested in this piece. Would you accept ₹950?",
186
385
  expiresAt: daysFromNow(3),
187
386
  createdAt: daysAgo(1),
188
387
  updatedAt: daysAgo(1),
388
+ statusHistory: [
389
+ entry(daysAgo(1), "buyer", "makeOffer", {
390
+ status: { from: null, to: "pending" },
391
+ offerAmount: { from: null, to: 950 },
392
+ }, { actorUid: BUYER_ADMIN, note: "Interested in this piece. Would you accept ₹950?" }),
393
+ ],
189
394
  },
190
395
  ];