@mohasinac/appkit 4.42.16 → 4.42.18

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.
@@ -22,6 +22,7 @@
22
22
  * @tag sideEffects:firestore-write
23
23
  */
24
24
  import { offerRepository } from "../../../../features/seller/repository/offer.repository";
25
+ import { cartRepository } from "../../../../features/cart/repository/cart.repository";
25
26
  import { bidRepository } from "../../../../repositories";
26
27
  import { OfferStatusValues } from "../../../../features/seller/schemas";
27
28
  import { BID_COLLECTION } from "../../../../features/auctions/schemas/firestore";
@@ -199,7 +200,31 @@ export async function assertLockedLinesStillValid(items, buyerUid) {
199
200
  continue;
200
201
  const offer = await offerRepository.findById(item.offerId);
201
202
  if (!offer) {
202
- throw new ValidationError("The offer for one of your items no longer exists. Remove it and try again.");
203
+ /*
204
+ * 🛑 PRUNE IT, do not just refuse. This used to throw "Remove it and
205
+ * try again" — an instruction the buyer cannot follow, because
206
+ * `cartRepository.removeItem` rejects any `locked` line and the cart
207
+ * page renders no remove control for one. Three guards then disagreed
208
+ * about one fact and trapped the buyer between them: checkout said
209
+ * remove it, remove said the line requires payment, and
210
+ * `assertCanAddNewItems` said settle the offer lane first — so the
211
+ * whole cart, including unrelated standard items, became unbuyable
212
+ * with no way out. Measured on production: three real items stranded
213
+ * behind one tombstone line.
214
+ *
215
+ * At THIS point the lock protects nothing. The lock exists so a buyer
216
+ * cannot walk away from a committed purchase; an offer record that no
217
+ * longer exists is not a commitment to anything, and the line can never
218
+ * be fulfilled. `removeItemsByOfferId` is the same path admin
219
+ * cancellation already uses, so it is allowed to clear a locked line.
220
+ *
221
+ * Still a ValidationError rather than a silent continue: the buyer's
222
+ * total just changed, and a checkout that quietly drops a line and
223
+ * charges a different amount than the screen showed is its own defect.
224
+ * They retry once and it goes through.
225
+ */
226
+ await cartRepository.removeItemsByOfferId(buyerUid, item.offerId);
227
+ throw new ValidationError("One item was removed because its offer no longer exists. Please review your cart and try again.");
203
228
  }
204
229
  if (offer.buyerUid !== buyerUid) {
205
230
  throw new ValidationError("This offer belongs to a different account.");
@@ -6,6 +6,7 @@ import { sortBy } from "@mohasinac/appkit/client";
6
6
  import { useState, useCallback } from "react";
7
7
  import { Badge, Button, Div, FilterChipGroup, RecordDetailModal, Span, Text, useToast } from "../../../ui";
8
8
  import { buildBidDetailFields, bidStatusBadge } from "../../auctions/components/bid-detail-fields";
9
+ import { RecordStatusTimeline } from "../../status-history/components/RecordStatusTimeline";
9
10
  import { SELLER_ENDPOINTS } from "../../../constants/api-endpoints";
10
11
  import { ACTIONS } from "../../../_internal/shared/actions/action-registry";
11
12
  import { buildBulkAction } from "../../../_internal/shared/actions/bulk-helpers";
@@ -113,6 +114,9 @@ export function SellerBidsView({ endpoint = SELLER_ENDPOINTS.BIDS }) {
113
114
  bidAmount: Number(item.bidAmount ?? 0),
114
115
  status: toStringValue(item.status, "active"),
115
116
  bidDate: toRelativeDate(item.bidDate ?? item.createdAt),
117
+ // The one cast, at the JSON -> document boundary where it belongs, so
118
+ // that every CONSUMER of `raw` typechecks for real.
119
+ raw: item,
116
120
  })),
117
121
  getTotal: (response, mappedRows) => (typeof response.total === "number" ? response.total : mappedRows.length),
118
122
  buildFilters: (state) => (state.status ? sieveFilter("status", SIEVE_OP.EQ, state.status) : undefined),
@@ -145,8 +149,14 @@ export function SellerBidsView({ endpoint = SELLER_ENDPOINTS.BIDS }) {
145
149
  },
146
150
  };
147
151
  const detailBadge = detail ? bidStatusBadge(detail.status) : null;
148
- return (_jsxs(_Fragment, { children: [_jsx(DataListingView, { config: config }), _jsx(RecordDetailModal, { isOpen: detail !== null, onClose: () => setDetail(null), title: detail?.productTitle || "Bid", badges: detailBadge ? [{ label: detailBadge.label }] : undefined, fields: detail ? buildBidDetailFields(detail, "seller") : undefined, footer: detail ? (
152
+ return (_jsxs(_Fragment, { children: [_jsx(DataListingView, { config: config }), _jsx(RecordDetailModal, { isOpen: detail !== null, onClose: () => setDetail(null), title: detail?.productTitle || "Bid", badges: detailBadge ? [{ label: detailBadge.label }] : undefined, fields: detail ? buildBidDetailFields(detail.raw, "seller") : undefined, footer: detail ? (
149
153
  // The full page — a modal cannot be linked, bookmarked or reloaded
150
154
  // into, which is the whole reason the page exists.
151
- _jsx(TextLink, { href: String(ROUTES.STORE.BID_DETAIL(detail.id)), children: "Open full page \u2192" })) : undefined })] }));
155
+ _jsx(TextLink, { href: String(ROUTES.STORE.BID_DETAIL(detail.id)), children: "Open full page \u2192" })) : undefined, extra:
156
+ // Present on the seller's full page and absent from this modal until
157
+ // 2026-10-04, which is a label the two surfaces disagreed about —
158
+ // exactly what the modal-vs-page case exists to catch. Mirrors
159
+ // AdminBidsView: absent history renders the empty label, never a
160
+ // step invented from `updatedAt`.
161
+ _jsx(RecordStatusTimeline, { entries: detail?.raw?.statusHistory, truncatedCount: detail?.raw?.statusHistoryTruncated }) })] }));
152
162
  }
@@ -40,8 +40,20 @@ export const authored = {
40
40
  "Read the current bid, the minimum increment and the bid panel.",
41
41
  ],
42
42
  expectedBehaviour: "The twin of the guest case above — the bid figures and the working bid form are exactly what the gate must not cost a signed-in buyer.",
43
- expectedUiState: "'Current bid ₹15,000.00' is shown, a minimum increment of ₹1,000.00 is stated, and the bid panel carries a real amount input and a submit button. Neither 'Sign in to see the current bid' nor 'Bidding is for members' appears anywhere on the page.",
44
- expectedData: { currentBid: 15000, minIncrement: 1000 },
43
+ expectedUiState:
44
+ /*
45
+ * Literals corrected 2026-10-04 to the auction this case actually opens.
46
+ * They read '₹15,000.00' and a '₹1,000.00' increment, which are the
47
+ * numbers of a DIFFERENT fixture — `auction-money-flows-closing`, seeded
48
+ * by fixtures/money-flows__auction-win-to-payment.mjs at
49
+ * startingBid/currentBid 15000 with a 1000 increment, because its own
50
+ * case enters 16000 against it. This case's startPage is
51
+ * auction-beyblade-original-dragoon-storm, seeded startingBid 2999 /
52
+ * currentBid 3499 / buyNowPrice 5999, and the page renders exactly that.
53
+ * The product was never wrong; the expectation named another auction.
54
+ */
55
+ "'Current bid ₹3,499.00' is shown with its bid count, a 'min increment ₹200.00' is stated, a Buy Now price of ₹5,999.00 is offered, and the bid panel carries a real amount input and a 'Place bid' submit button. Neither 'Sign in to see price' nor any other gate prompt appears anywhere on the page.",
56
+ expectedData: { currentBid: 3499, minIncrement: 200 },
45
57
  endResult: "Nothing persists unless a bid is actually submitted, which this case does not do.",
46
58
  },
47
59
  "checklist-buying-product-detail-bundle-purchase": {
@@ -55,6 +55,27 @@ const _rawproductsLiveItemsSeedData = [
55
55
  seedPhoto("live-image-golden-retriever-1-20260816", 900, 900),
56
56
  seedPhoto("live-image-golden-retriever-2-20260816", 900, 900),
57
57
  ],
58
+ /*
59
+ * 🛑 A LIVE LISTING WITHOUT A VIDEO IS NOT A LISTING THE PRODUCT WOULD ACCEPT.
60
+ *
61
+ * `/store/live/new` refuses to save one — "a buyer purchasing an animal or
62
+ * plant needs to see it move" — so a seeded live fixture carrying only
63
+ * stills is data no seller could have created through the form, and every
64
+ * case written against the real creation flow then asserts against a
65
+ * fixture that contradicts it. Found 2026-10-04 driving
66
+ * `live-item-detail`, whose expectation that the gallery strip offers a
67
+ * "View video" entry was correct about the product and wrong about the
68
+ * data: the page shipped `video: undefined` and rendered 2 image slides.
69
+ *
70
+ * `url` stays RAW and unwrapped (Root Cause #27) — the media proxy is
71
+ * image-only and 400s on video/mp4 — while `thumbnailUrl` is wrapped,
72
+ * because a poster frame IS an image and should carry the watermark.
73
+ */
74
+ video: {
75
+ url: "/demo-media/sample-video.mp4",
76
+ thumbnailUrl: seedPhoto("live-video-thumb-golden-retriever-20261004", 800, 450),
77
+ duration: 15,
78
+ },
58
79
  status: PRODUCT_FIELDS.STATUS_VALUES.PUBLISHED,
59
80
  condition: PRODUCT_FIELDS.CONDITION_VALUES.NEW,
60
81
  listingType: "live",
@@ -102,6 +123,12 @@ const _rawproductsLiveItemsSeedData = [
102
123
  isSold: false,
103
124
  mainImage: seedPhoto("live-image-bearded-dragon-1-20260817", 900, 900),
104
125
  images: [seedPhoto("live-image-bearded-dragon-1-20260817", 900, 900)],
126
+ // Required for a live listing — see the note on the retriever above.
127
+ video: {
128
+ url: "/demo-media/sample-video.mp4",
129
+ thumbnailUrl: seedPhoto("live-video-thumb-bearded-dragon-20261004", 800, 450),
130
+ duration: 15,
131
+ },
105
132
  status: PRODUCT_FIELDS.STATUS_VALUES.PUBLISHED,
106
133
  condition: PRODUCT_FIELDS.CONDITION_VALUES.NEW,
107
134
  listingType: "live",
@@ -153,6 +180,12 @@ const _rawproductsLiveItemsSeedData = [
153
180
  seedPhoto("live-image-bonsai-juniper-1-20260818", 900, 900),
154
181
  seedPhoto("live-image-bonsai-juniper-2-20260818", 900, 900),
155
182
  ],
183
+ // Required for a live listing — see the note on the retriever above.
184
+ video: {
185
+ url: "/demo-media/sample-video.mp4",
186
+ thumbnailUrl: seedPhoto("live-video-thumb-bonsai-juniper-20261004", 800, 450),
187
+ duration: 15,
188
+ },
156
189
  status: PRODUCT_FIELDS.STATUS_VALUES.PUBLISHED,
157
190
  condition: PRODUCT_FIELDS.CONDITION_VALUES.NEW,
158
191
  listingType: "live",
@@ -203,6 +236,15 @@ const _rawproductsLiveItemsSeedData = [
203
236
  images: [
204
237
  seedPhoto("live-image-juniper-bonsai-sold-1-20260824", 900, 900),
205
238
  ],
239
+ // Required for a live listing — see the note on the retriever above. A SOLD
240
+ // fixture needs it just as much: it is the row the "Sold & Ended" scope
241
+ // renders, and a sold listing that could never have been created is no
242
+ // more representative than an available one.
243
+ video: {
244
+ url: "/demo-media/sample-video.mp4",
245
+ thumbnailUrl: seedPhoto("live-video-thumb-juniper-bonsai-sold-20261004", 800, 450),
246
+ duration: 15,
247
+ },
206
248
  status: PRODUCT_FIELDS.STATUS_VALUES.PUBLISHED,
207
249
  condition: PRODUCT_FIELDS.CONDITION_VALUES.NEW,
208
250
  listingType: "live",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mohasinac/appkit",
3
- "version": "4.42.16",
3
+ "version": "4.42.18",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public"