@mohasinac/appkit 4.60.1 → 4.62.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 (202) hide show
  1. package/dist/.build-stamp.json +3 -3
  2. package/dist/_internal/client/features/lottery/LotteryAdminEditView.js +19 -3
  3. package/dist/_internal/client/features/maintenance/views/AnalysisRunnerView.js +1 -1
  4. package/dist/_internal/client/features/maintenance/views/CloudLogsListView.js +1 -1
  5. package/dist/_internal/server/features/auctions/actions.d.ts +1 -1
  6. package/dist/_internal/server/features/auctions/actions.js +12 -1
  7. package/dist/_internal/server/features/blog/actions.d.ts +1 -1
  8. package/dist/_internal/server/features/blog/actions.js +12 -1
  9. package/dist/_internal/server/features/brands/actions.d.ts +1 -1
  10. package/dist/_internal/server/features/brands/actions.js +12 -1
  11. package/dist/_internal/server/features/cart/actions.d.ts +1 -1
  12. package/dist/_internal/server/features/cart/actions.js +12 -1
  13. package/dist/_internal/server/features/checkout/actions.js +0 -1
  14. package/dist/_internal/server/features/events/actions.d.ts +1 -1
  15. package/dist/_internal/server/features/events/actions.js +12 -1
  16. package/dist/_internal/server/features/gateway-events/redact.js +14 -2
  17. package/dist/_internal/server/features/history/actions.d.ts +1 -1
  18. package/dist/_internal/server/features/history/actions.js +12 -1
  19. package/dist/_internal/server/features/lottery/actions.d.ts +1 -1
  20. package/dist/_internal/server/features/lottery/actions.js +12 -1
  21. package/dist/_internal/server/features/orders/actions.d.ts +1 -1
  22. package/dist/_internal/server/features/orders/actions.js +12 -1
  23. package/dist/_internal/server/features/orders/cancellation-refund.js +0 -1
  24. package/dist/_internal/server/features/orders/service.js +31 -1
  25. package/dist/_internal/server/features/payouts/actions.d.ts +1 -1
  26. package/dist/_internal/server/features/payouts/actions.js +12 -1
  27. package/dist/_internal/server/features/points/award.js +9 -1
  28. package/dist/_internal/server/features/pre-orders/actions.d.ts +1 -1
  29. package/dist/_internal/server/features/pre-orders/actions.js +12 -1
  30. package/dist/_internal/server/features/prize-draws/actions.d.ts +1 -1
  31. package/dist/_internal/server/features/prize-draws/actions.js +12 -1
  32. package/dist/_internal/server/features/products/actions.d.ts +1 -1
  33. package/dist/_internal/server/features/products/actions.js +12 -1
  34. package/dist/_internal/server/features/promotions/actions.d.ts +1 -1
  35. package/dist/_internal/server/features/promotions/actions.js +12 -1
  36. package/dist/_internal/server/features/protection-claims/actions.d.ts +2 -2
  37. package/dist/_internal/server/features/protection-claims/actions.js +21 -12
  38. package/dist/_internal/server/features/raffle/actions.d.ts +1 -1
  39. package/dist/_internal/server/features/raffle/actions.js +12 -1
  40. package/dist/_internal/server/features/refunds/actions.d.ts +1 -3
  41. package/dist/_internal/server/features/refunds/actions.js +14 -2
  42. package/dist/_internal/server/features/reviews/actions.d.ts +1 -1
  43. package/dist/_internal/server/features/reviews/actions.js +12 -1
  44. package/dist/_internal/server/features/wishlist/actions.d.ts +1 -1
  45. package/dist/_internal/server/features/wishlist/actions.js +12 -1
  46. package/dist/_internal/server/jobs/core/obligationSweep.js +0 -1
  47. package/dist/_internal/server/jobs/core/paymentReconcile.js +93 -76
  48. package/dist/_internal/server/jobs/core/testDataCleanup.d.ts +1 -1
  49. package/dist/_internal/server/jobs/core/testDataCleanup.js +22 -1
  50. package/dist/_internal/server/jobs/handlers/onShipmentAllocationSync.js +16 -4
  51. package/dist/_internal/shared/actions/action-registry.js +149 -9
  52. package/dist/_internal/shared/features/promotions/schema.d.ts +66 -0
  53. package/dist/_internal/shared/features/promotions/schema.js +7 -0
  54. package/dist/_internal/shared/media/limits.d.ts +10 -0
  55. package/dist/_internal/shared/media/limits.js +10 -0
  56. package/dist/_internal/shared/obligations/deadlines.js +11 -1
  57. package/dist/_internal/shared/tester/window.d.ts +25 -0
  58. package/dist/_internal/shared/tester/window.js +66 -0
  59. package/dist/_internal/shared/types/action-result.js +15 -1
  60. package/dist/client/api/surface-error.js +16 -2
  61. package/dist/client/api/useApiMutation.js +17 -1
  62. package/dist/client.d.ts +1 -1
  63. package/dist/client.js +1 -1
  64. package/dist/constants/api-endpoints.d.ts +3 -0
  65. package/dist/constants/api-endpoints.js +1 -0
  66. package/dist/constants/field-names.d.ts +17 -0
  67. package/dist/constants/field-names.js +23 -0
  68. package/dist/core/server-action.js +19 -3
  69. package/dist/core/unit-of-work.js +15 -1
  70. package/dist/errors/error-mapping.js +26 -1
  71. package/dist/errors/firestore-codes.d.ts +37 -0
  72. package/dist/errors/firestore-codes.js +52 -0
  73. package/dist/errors/index.d.ts +0 -1
  74. package/dist/errors/index.js +12 -2
  75. package/dist/features/account/components/UserOffersPanel.js +5 -3
  76. package/dist/features/admin/components/AdminActionIndexView.js +100 -5
  77. package/dist/features/admin/components/AdminAddressClustersView.js +21 -2
  78. package/dist/features/admin/components/AdminBlogEditorView.js +8 -8
  79. package/dist/features/admin/components/AdminCarouselEditorView.js +1 -1
  80. package/dist/features/admin/components/AdminCarouselView.js +96 -4
  81. package/dist/features/admin/components/AdminCouponEditorView.js +8 -0
  82. package/dist/features/admin/components/AdminFulfillmentView.js +3 -2
  83. package/dist/features/admin/components/AdminGatewayEventsView.js +2 -2
  84. package/dist/features/admin/components/AdminMediaView.js +36 -3
  85. package/dist/features/admin/components/AdminNavigationView.js +4 -1
  86. package/dist/features/admin/components/AdminOffersView.js +2 -1
  87. package/dist/features/admin/components/AdminPaymentClustersView.js +19 -2
  88. package/dist/features/admin/components/AdminProductEditorView.js +2 -2
  89. package/dist/features/admin/components/AdminSectionsView.d.ts +10 -0
  90. package/dist/features/admin/components/AdminSectionsView.js +486 -224
  91. package/dist/features/admin/components/DataListingView.js +2 -0
  92. package/dist/features/admin/components/ReferenceRecordEditor.js +1 -1
  93. package/dist/features/admin/components/analytics/AdminStatCard.js +4 -1
  94. package/dist/features/admin/hooks/useOptimisticRowFlag.d.ts +33 -0
  95. package/dist/features/admin/hooks/useOptimisticRowFlag.js +87 -0
  96. package/dist/features/admin/repository/notification.repository.d.ts +15 -0
  97. package/dist/features/admin/repository/notification.repository.js +32 -0
  98. package/dist/features/admin/repository/site-settings.repository.d.ts +2 -1
  99. package/dist/features/admin/repository/site-settings.repository.js +7 -0
  100. package/dist/features/admin/schemas/action-index-form.d.ts +34 -0
  101. package/dist/features/admin/schemas/action-index-form.js +115 -0
  102. package/dist/features/auctions/components/MarketplaceAuctionCard.js +1 -1
  103. package/dist/features/auth/components/RegisterForm.js +2 -1
  104. package/dist/features/catalogue/actions/approve-catalogue-listing.d.ts +1 -1
  105. package/dist/features/catalogue/actions/approve-catalogue-listing.js +12 -1
  106. package/dist/features/catalogue/actions/list-from-catalogue.d.ts +1 -1
  107. package/dist/features/catalogue/actions/list-from-catalogue.js +12 -1
  108. package/dist/features/catalogue/actions/submit-for-approval.d.ts +1 -1
  109. package/dist/features/catalogue/actions/submit-for-approval.js +12 -1
  110. package/dist/features/catalogue/components/CatalogueItemEditorView.js +3 -3
  111. package/dist/features/categories/repository/price-observation.repository.d.ts +19 -0
  112. package/dist/features/categories/repository/price-observation.repository.js +21 -2
  113. package/dist/features/events/actions/event-actions.js +4 -1
  114. package/dist/features/events/components/AdminEventEditorView.js +79 -50
  115. package/dist/features/events/components/EventsListPageView.js +2 -1
  116. package/dist/features/events/schemas/firestore.d.ts +17 -0
  117. package/dist/features/events/schemas/firestore.js +1 -0
  118. package/dist/features/events/utils/sort-pinned.d.ts +33 -0
  119. package/dist/features/events/utils/sort-pinned.js +46 -0
  120. package/dist/features/gateway-events/repository/gateway-events.repository.js +3 -40
  121. package/dist/features/gateway-events/schemas/index.d.ts +2 -2
  122. package/dist/features/inventory/components/AdminStorageBoxesView.js +55 -39
  123. package/dist/features/inventory/components/ReceiveStationView.js +61 -17
  124. package/dist/features/inventory/repository/storage-boxes.repository.d.ts +3 -1
  125. package/dist/features/inventory/repository/storage-boxes.repository.js +36 -3
  126. package/dist/features/layout/hooks/useFormBottomActions.js +20 -1
  127. package/dist/features/obligations/repository/obligations.repository.js +10 -1
  128. package/dist/features/orders/components/EmiInstallmentsPanel.js +25 -7
  129. package/dist/features/orders/components/RefundRequestView.js +4 -4
  130. package/dist/features/orders/schemas/firestore.d.ts +19 -0
  131. package/dist/features/orders/schemas/firestore.js +19 -0
  132. package/dist/features/orders/schemas/index.d.ts +6 -6
  133. package/dist/features/payment-cases/repository/payment-cases.repository.d.ts +1 -0
  134. package/dist/features/payment-cases/repository/payment-cases.repository.js +3 -14
  135. package/dist/features/pre-orders/components/MarketplacePreorderCard.js +1 -1
  136. package/dist/features/products/components/GroupMemberPicker.js +4 -1
  137. package/dist/features/products/components/MarketplaceBundleCard.js +1 -1
  138. package/dist/features/products/components/MarketplacePrizeDrawCard.js +1 -1
  139. package/dist/features/products/components/PrizeDrawEntryActions.js +2 -1
  140. package/dist/features/products/components/ProductDetailActions.js +2 -1
  141. package/dist/features/products/components/ProductForm.js +2 -2
  142. package/dist/features/products/components/ProductGrid.js +1 -1
  143. package/dist/features/promotions/repository/claimed-coupons.repository.js +31 -1
  144. package/dist/features/promotions/schemas/firestore.d.ts +22 -0
  145. package/dist/features/promotions/schemas/firestore.js +22 -0
  146. package/dist/features/promotions/schemas/index.d.ts +479 -6
  147. package/dist/features/promotions/schemas/index.js +153 -0
  148. package/dist/features/protection-claims/repository/protection-claims.repository.js +22 -15
  149. package/dist/features/seller/components/FulfillmentView.js +3 -2
  150. package/dist/features/seller/components/SellerAddressesView.js +32 -4
  151. package/dist/features/seller/components/SellerAnalyticsAlertsView.js +62 -3
  152. package/dist/features/seller/components/SellerBundlesView.js +53 -14
  153. package/dist/features/seller/components/SellerGroupedListingsView.js +23 -3
  154. package/dist/features/seller/components/SellerListingTypeView.js +56 -10
  155. package/dist/features/seller/components/SellerPayoutMethodsView.js +33 -1
  156. package/dist/features/seller/components/SellerPayoutSettingsView.js +48 -9
  157. package/dist/features/seller/components/SellerProductsView.js +5 -1
  158. package/dist/features/seller/components/SellerShippingConfigsView.js +33 -1
  159. package/dist/features/seller/components/SellerShippingView.js +2 -1
  160. package/dist/features/seller/components/SellerStoreCategoriesView.js +33 -1
  161. package/dist/features/seller/components/SellerSupportView.js +69 -2
  162. package/dist/features/seller/schemas/coupon-form.d.ts +5 -0
  163. package/dist/features/seller/schemas/coupon-form.js +18 -0
  164. package/dist/features/shell/SectionForm.js +35 -9
  165. package/dist/features/shell/build-sections.js +14 -1
  166. package/dist/features/shipments/components/AdminShipmentLotItemsView.js +54 -1
  167. package/dist/features/shipments/components/AdminShipmentProjectionsView.js +3 -2
  168. package/dist/features/sticker-trace/components/StickerCommitPanel.js +2 -1
  169. package/dist/features/sticker-trace/components/StickerPrintPanel.js +2 -1
  170. package/dist/features/sticker-trace/components/StickerRetryPanel.js +24 -4
  171. package/dist/features/sticker-trace/components/StickerTraceStudio.js +2 -1
  172. package/dist/features/sticker-trace/server.d.ts +2 -0
  173. package/dist/features/tester/components/AdminTesterFeedbackIssuesView.js +3 -2
  174. package/dist/http/api-handler.js +4 -1
  175. package/dist/index.d.ts +8 -4
  176. package/dist/index.js +6 -4
  177. package/dist/next/routing/route-map.d.ts +4 -2
  178. package/dist/next/routing/route-map.js +19 -1
  179. package/dist/schemas/registry.d.ts +15 -10
  180. package/dist/schemas/sieve.d.ts +3 -3
  181. package/dist/server.d.ts +1 -2
  182. package/dist/server.js +6 -4
  183. package/dist/styles.css +51 -0
  184. package/dist/ui/components/Checkbox.js +10 -2
  185. package/dist/ui/components/DateInput.js +8 -1
  186. package/dist/ui/components/Input.js +22 -2
  187. package/dist/ui/components/PaginatedSelect.d.ts +8 -19
  188. package/dist/ui/components/PaginatedSelect.js +0 -0
  189. package/dist/ui/components/Radio.js +8 -1
  190. package/dist/ui/components/Select.js +10 -2
  191. package/dist/ui/components/TagInput.d.ts +8 -1
  192. package/dist/ui/components/TagInput.js +4 -3
  193. package/dist/ui/components/Textarea.js +10 -2
  194. package/dist/ui/forms/FieldCheckbox.js +1 -1
  195. package/dist/ui/forms/FormErrorList.d.ts +0 -16
  196. package/dist/ui/forms/FormErrorList.js +34 -1
  197. package/dist/ui/forms/apply-zod-issues.d.ts +19 -1
  198. package/dist/ui/forms/apply-zod-issues.js +23 -1
  199. package/package.json +1 -1
  200. package/scripts/seed-cli.mjs +1822 -1812
  201. package/dist/features/tester/utils/phases.d.ts +0 -48
  202. package/dist/features/tester/utils/phases.js +0 -139
@@ -31,6 +31,8 @@ import { apiClient } from "../../../http/index.js";
31
31
  import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints.js";
32
32
  import { ROUTES } from "../../../next/routing/route-map.js";
33
33
  import { operatorMessage } from "./operator-message.js";
34
+ import { normalizeError } from "../../../errors/normalize.js";
35
+ import { trackError, ErrorCategory, ErrorSeverity, } from "../../../monitoring/error-tracking.js";
34
36
  import { MAX_RECEIVE_ROWS } from "../schemas/firestore.js";
35
37
  /** `boxId::productId` — the staging table's natural key, mirroring `placementId`. */
36
38
  function rowKey(boxId, productId) {
@@ -67,6 +69,21 @@ export function ReceiveStationView({ shipmentId, shipmentNumber, items, unlinked
67
69
  rowsRef.current = computed;
68
70
  setRows(computed);
69
71
  }
72
+ /**
73
+ * Drop one staged row. Named rather than inline in the table's Remove
74
+ * button, where the updater's block body sat a brace too deep.
75
+ */
76
+ function removeRow(key) {
77
+ applyRows((c) => {
78
+ const next = { ...c };
79
+ delete next[key];
80
+ return next;
81
+ });
82
+ }
83
+ /** Set one staged row's quantity. Same reason as `removeRow`. */
84
+ function setRowQuantity(key, quantity) {
85
+ applyRows((c) => ({ ...c, [key]: { ...c[key], quantity } }));
86
+ }
70
87
  const [scan, setScan] = useState("");
71
88
  const [committing, setCommitting] = useState(false);
72
89
  /** The in-flight guard. State renders the spinner; this prevents the second send. */
@@ -197,6 +214,12 @@ export function ReceiveStationView({ shipmentId, shipmentNumber, items, unlinked
197
214
  * It runs only on a miss, so the common case stays instant and a box-arm is
198
215
  * roughly one scan in twenty anyway.
199
216
  */
217
+ /*
218
+ * Three outcomes, and the third is the point: `BoxOption` (found), `null`
219
+ * (asked, no such box), `undefined` (could not ask). The caller used to see
220
+ * only the first two and told the operator "No box registered as …" for a
221
+ * network failure.
222
+ */
200
223
  async function resolveBoxFromServer(code) {
201
224
  const bare = code.startsWith("BOX-") ? code.slice(4) : code;
202
225
  try {
@@ -206,10 +229,26 @@ export function ReceiveStationView({ shipmentId, shipmentNumber, items, unlinked
206
229
  ? { id: hit.id, code: hit.code, location: hit.location ?? null }
207
230
  : null;
208
231
  }
209
- catch {
210
- // A failed lookup must not masquerade as "no such box" — the caller
211
- // distinguishes the two in what it tells the operator.
212
- return null;
232
+ catch (err) {
233
+ /*
234
+ * 🛑 The predecessor comment here claimed "a failed lookup must not
235
+ * masquerade as 'no such box' — the caller distinguishes the two". It
236
+ * could not: the success path returns `null` on a miss and this returned
237
+ * the same `null`, so the two were indistinguishable by construction and
238
+ * a network failure WAS reported to the operator as an unrecognised
239
+ * code. That is the exact confusion the function's own header says it
240
+ * exists to prevent.
241
+ *
242
+ * Classified and recorded rather than swallowed, and `undefined` now
243
+ * means "could not ask" while `null` keeps meaning "asked, no such box".
244
+ */
245
+ const n = normalizeError(err);
246
+ trackError(n.message, ErrorCategory.API, ErrorSeverity.MEDIUM, {
247
+ component: "ReceiveStationView",
248
+ action: "resolveBoxFromServer",
249
+ metadata: { code: bare },
250
+ });
251
+ return undefined;
213
252
  }
214
253
  }
215
254
  async function handleScan(raw) {
@@ -235,10 +274,18 @@ export function ReceiveStationView({ shipmentId, shipmentNumber, items, unlinked
235
274
  setLastScan({ text: `Armed ${fetched.code}`, tone: "ok" });
236
275
  return;
237
276
  }
238
- setLastScan({
239
- text: `No box registered as ${code.slice(4)} — register it first`,
240
- tone: "bad",
241
- });
277
+ setLastScan(fetched === undefined
278
+ ? {
279
+ // "Could not ask" — never "does not exist". The operator is
280
+ // holding the box; telling them it is unregistered sends them
281
+ // to re-register something that is already in the system.
282
+ text: `Could not check ${code.slice(4)} — connection problem, scan again`,
283
+ tone: "bad",
284
+ }
285
+ : {
286
+ text: `No box registered as ${code.slice(4)} — register it first`,
287
+ tone: "bad",
288
+ });
242
289
  return;
243
290
  }
244
291
  const item = byCode.get(code);
@@ -362,7 +409,11 @@ export function ReceiveStationView({ shipmentId, shipmentNumber, items, unlinked
362
409
  setLastScan({ text: "Committed. Carry on.", tone: "ok" });
363
410
  }
364
411
  catch (err) {
365
- showToast(operatorMessage(err), "error");
412
+ // Normalized first: `operatorMessage` duck-types `.status`/`.code`/
413
+ // `.message`, and the api/app branches of `NormalizedError` carry exactly
414
+ // those three — so the 4xx passthrough still works and every other kind
415
+ // correctly falls through to a constant instead of server text.
416
+ showToast(operatorMessage(normalizeError(err)), "error");
366
417
  }
367
418
  finally {
368
419
  committingRef.current = false;
@@ -382,14 +433,7 @@ export function ReceiveStationView({ shipmentId, shipmentNumber, items, unlinked
382
433
  return (_jsxs(Tr, { children: [_jsx(Td, { children: _jsx(Badge, { variant: "info", children: r.boxCode }) }), _jsx(Td, { children: r.productTitle }), _jsx(Td, { children: _jsx(Text, { numeric: true, size: "sm", children: r.code ?? "—" }) }), _jsx(Td, { children: _jsx(QuantityStepper, { value: r.quantity, min: 1, max: 9999,
383
434
  // Many steppers on one page, so a bare "Quantity"
384
435
  // names none of them.
385
- ariaLabel: `Quantity of ${r.productTitle} for ${r.boxCode}`, onChange: (n) => applyRows((c) => ({
386
- ...c,
387
- [key]: { ...c[key], quantity: n },
388
- })) }) }), _jsx(Td, { children: _jsx(Button, { variant: "ghost", onClick: () => applyRows((c) => {
389
- const next = { ...c };
390
- delete next[key];
391
- return next;
392
- }), children: "Remove" }) })] }, key));
436
+ ariaLabel: `Quantity of ${r.productTitle} for ${r.boxCode}`, onChange: (n) => setRowQuantity(key, n) }) }), _jsx(Td, { children: _jsx(Button, { variant: "ghost", onClick: () => removeRow(key), children: "Remove" }) })] }, key));
393
437
  }) })] }))] }) }), _jsx(Card, { variant: "default", children: _jsxs(CardBody, { children: [_jsx(Heading, { level: 2, size: "lg", children: "Add by hand" }), items.length === 0 ? (_jsx(Text, { color: "muted", children: "No linked items on this shipment yet." })) : (_jsxs(Table, { children: [_jsx(Thead, { children: _jsxs(Tr, { children: [_jsx(Th, { children: "Item" }), _jsx(Th, { children: "Scan code" }), _jsx(Th, { children: "Expected" }), _jsx(Th, { children: "Add" })] }) }), _jsx(Tbody, { children: items.map((it) => (_jsxs(Tr, { children: [_jsx(Td, { children: it.productTitle }), _jsx(Td, { children: _jsx(Text, { numeric: true, size: "sm", children: it.barcodeId ??
394
438
  it.sku ??
395
439
  "no code yet — add by hand, or run the SKU backfill" }) }), _jsx(Td, { children: it.expectedQuantity }), _jsx(Td, { children: _jsx(Button, { variant: "outline",
@@ -37,7 +37,9 @@ export declare class StorageBoxesRepository extends BaseRepository<StorageBoxDoc
37
37
  * recurse, which is the entire reason for the choice.
38
38
  */
39
39
  setRollup(id: string, distinctProducts: number, totalUnits: number): Promise<void>;
40
- listForAdmin(model: SieveModel): Promise<FirebaseSieveResult<StorageBoxDocument>>;
40
+ listForAdmin(model: SieveModel, opts?: {
41
+ search?: string;
42
+ }): Promise<FirebaseSieveResult<StorageBoxDocument>>;
41
43
  /** Every Timestamp field carries `parseSieveDateValue` — Root Cause #47. */
42
44
  static readonly SIEVE_FIELDS: {
43
45
  id: {
@@ -15,9 +15,13 @@
15
15
  * @tag access:server
16
16
  * @tag sideEffects:firestore
17
17
  */
18
- import { BaseRepository, parseSieveDateValue, } from "../../../providers/db-firebase/index.js";
18
+ import { BaseRepository, parseSieveDateValue, applySieveToFirestore, } from "../../../providers/db-firebase/index.js";
19
19
  import { generateStorageBoxId } from "../../../utils/id-generators.js";
20
20
  import { buildStorageBoxSearchTxt } from "../../../utils/search-txt-builders.js";
21
+ // The shared three — hand-writing them is how ten repositories each ended up
22
+ // with two of the three, which reads as a working multi-word search and
23
+ // silently cannot reach page 2.
24
+ import { planSearchTxt, beginSearchWindow, emptySearchResult, } from "../../../utils/search-txt-query.js";
21
25
  import { BoxStatusValues, BoxTypeValues, STORAGE_BOXES_COLLECTION, STORAGE_BOX_FIELDS, } from "../schemas/firestore.js";
22
26
  /**
23
27
  * `AC60-03` → ordinal 3.
@@ -100,8 +104,37 @@ export class StorageBoxesRepository extends BaseRepository {
100
104
  updatedAt: new Date(),
101
105
  });
102
106
  }
103
- async listForAdmin(model) {
104
- return this.sieveQuery(model, StorageBoxesRepository.SIEVE_FIELDS, { defaultPageSize: 25, maxPageSize: 50 });
107
+ async listForAdmin(model, opts) {
108
+ /*
109
+ * 🛑 `searchTxt` HAD NO READER. `buildStorageBoxSearchTxt` wrote tokens on
110
+ * every create and update, `SIEVE_FIELDS` listed the field as filterable,
111
+ * and `AdminStorageBoxesView` rendered a search box promising "Search by
112
+ * code or location" — and this method ignored the term entirely. Typing in
113
+ * it returned the unfiltered list with a 200, which reads as "no box
114
+ * matches that" only if you already know the box exists.
115
+ *
116
+ * Same two-part shape as the other migrated collections: `plan.head` is
117
+ * one `array-contains` on the longest term, `plan.rest` is AND-refined in
118
+ * memory. Via `planSearchTxt` for the `empty` guard — a query that
119
+ * normalises to no usable token must return NOTHING, because emitting no
120
+ * clause returns the whole collection and calls it a search result.
121
+ */
122
+ const plan = planSearchTxt(opts?.search);
123
+ if (plan.empty)
124
+ return emptySearchResult();
125
+ if (!plan.head) {
126
+ return this.sieveQuery(model, StorageBoxesRepository.SIEVE_FIELDS, { defaultPageSize: 25, maxPageSize: 50 });
127
+ }
128
+ const baseQuery = this.getCollection().where(STORAGE_BOX_FIELDS.SEARCH_TXT, "array-contains", plan.head);
129
+ const win = beginSearchWindow(model, plan);
130
+ const result = await applySieveToFirestore({
131
+ baseQuery,
132
+ model: win.model,
133
+ options: { maxPageSize: win.maxPageSize(100) },
134
+ fields: StorageBoxesRepository.SIEVE_FIELDS,
135
+ mapDoc: (snap) => this.mapDoc(snap),
136
+ });
137
+ return win.finish(result);
105
138
  }
106
139
  }
107
140
  /** Every Timestamp field carries `parseSieveDateValue` — Root Cause #47. */
@@ -3,6 +3,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import { useContext, useMemo } from "react";
4
4
  import { FormShellContext } from "../../../ui/forms/FormShell.js";
5
5
  import { FormErrorList } from "../../../ui/forms/FormErrorList.js";
6
+ import { Div } from "../../../ui/components/Div.js";
6
7
  import { useIsInsideOverlay } from "../../../ui/components/overlay-context.js";
7
8
  import { useBottomActions } from "./useBottomActions.js";
8
9
  /**
@@ -98,7 +99,25 @@ export function useFormBottomActions({ onSubmit, onCancel, submitLabel = "Save",
98
99
  infoLabel: showErrors
99
100
  ? `Fix ${errorCount} ${errorCount === 1 ? "issue" : "issues"}`
100
101
  : undefined,
101
- infoPanel: showErrors && ctx ? _jsx(FormErrorList, { ctx: ctx }) : undefined,
102
+ /*
103
+ * 🛑 The alert region lives HERE, not in `FormErrorList`.
104
+ *
105
+ * Without it a failed submit on a phone announced nothing at all: the
106
+ * list has no role, `InfoPanel` has no role, and the only signal was the
107
+ * disclosure button's text changing — which a screen reader does not read
108
+ * out unprompted. This is the whole error surface on mobile, since
109
+ * `useBottomActions` defaults to `desktop: "hidden"`.
110
+ *
111
+ * It cannot go on `FormErrorList` itself: the list's other consumer is
112
+ * `<FormErrorSummary>`, whose wrapper is already `role="alert"`, and
113
+ * nesting one alert region inside another makes the announcement
114
+ * ambiguous and duplicated.
115
+ *
116
+ * `aria-live="polite"` rather than `role="alert"`'s implicit assertive —
117
+ * the user has just pressed Save and is expecting a response, so there is
118
+ * nothing to interrupt. Same choice `FormErrorSummary` makes.
119
+ */
120
+ infoPanel: showErrors && ctx ? (_jsx(Div, { role: "alert", "aria-live": "polite", children: _jsx(FormErrorList, { ctx: ctx }) })) : undefined,
102
121
  // Bumped by every submit attempt, so a second failed Save re-opens a
103
122
  // sheet the user collapsed — while an ordinary re-render does not.
104
123
  infoOpenSignal: showErrors ? ctx?.submitAttemptCount : undefined,
@@ -1,5 +1,6 @@
1
1
  import { BaseRepository, prepareForFirestore } from "../../../providers/db-firebase/index.js";
2
2
  import { normalizeError } from "../../../errors/normalize.js";
3
+ import { serverLogger } from "../../../monitoring/index.js";
3
4
  import { OBLIGATIONS_COLLECTION, OBLIGATION_ACTIVE_STATES, OBLIGATION_FIELDS, createObligationId, } from "../schemas/firestore.js";
4
5
  export class ObligationsRepository extends BaseRepository {
5
6
  /*
@@ -138,7 +139,15 @@ export class ObligationsRepository extends BaseRepository {
138
139
  });
139
140
  }
140
141
  catch (err) {
141
- void normalizeError(err);
142
+ /*
143
+ * Best effort, but RECORDED. `void normalizeError(err)` alone types the
144
+ * error and reports it nowhere — as silent as `catch {}` while looking
145
+ * handled, which is what audit-silent-degrade calls normalize theatre.
146
+ * Swallowing is still correct here (a warning marker is a derived hint; the next order write reconciles again); losing the reason is not.
147
+ */
148
+ serverLogger.warn("obligations.warn failed", {
149
+ error: normalizeError(err).message,
150
+ });
142
151
  }
143
152
  }
144
153
  /** Refs for a subject's obligations — used when an order is hard-deleted. */
@@ -4,6 +4,7 @@ import { useState } from "react";
4
4
  import { Badge, Button, Div, Form, Row, Stack, Text, useToast } from "../../../ui/index.js";
5
5
  import { FieldInput } from "../../../ui/forms/index.js";
6
6
  import { formatCurrency } from "../../../utils/number.formatter.js";
7
+ import { toUserMessage } from "../../../errors/error-display-map.js";
7
8
  import { normalizeError } from "../../../errors/normalize.js";
8
9
  function stateOf(i) {
9
10
  if (i.status === "paid")
@@ -28,19 +29,34 @@ export function EmiInstallmentsPanel({ orderId, installments, remainingBalance,
28
29
  const money = (value) => formatCurrency(value, currency);
29
30
  const ordered = [...installments].sort((a, b) => a.index - b.index);
30
31
  const outstanding = ordered.filter((i) => i.status !== "paid").length;
31
- const submit = async (index) => {
32
+ /*
33
+ * Takes `setFieldError` from the <Form> render prop rather than closing
34
+ * over state, because the failure belongs ON the reference field.
35
+ *
36
+ * This panel used to be toast-only AND gate submission by disabling the
37
+ * button on an empty reference — so an incomplete form could not be sent
38
+ * and said nothing about why, and a rejected reference produced a toast
39
+ * that was gone before the buyer looked back at the input. Both are money
40
+ * surfaces on somebody's order.
41
+ */
42
+ const submit = async (index, fieldName, setFieldError) => {
43
+ const trimmed = reference.trim();
44
+ if (trimmed.length === 0) {
45
+ setFieldError(fieldName, "Enter the UTR or reference number from your transfer.");
46
+ return;
47
+ }
32
48
  setIsSubmitting(true);
33
49
  try {
34
- await onSubmit({ installmentIndex: index, transactionId: reference.trim() });
50
+ await onSubmit({ installmentIndex: index, transactionId: trimmed });
35
51
  showToast("Payment reference submitted — the seller will confirm it.", "success");
36
52
  setOpenIndex(null);
37
53
  setReference("");
38
54
  }
39
55
  catch (err) {
40
- void normalizeError(err);
41
- // The action's own message is surfaced by the caller's error mapping;
42
- // this only has to stop the panel pretending the submission landed.
43
- showToast("Couldn't submit that payment. Please try again.", "error");
56
+ const normalized = normalizeError(err);
57
+ setFieldError(fieldName, toUserMessage(normalized.code, undefined, {
58
+ fallback: "Couldn't submit that payment reference. Please try again.",
59
+ }));
44
60
  }
45
61
  finally {
46
62
  setIsSubmitting(false);
@@ -49,9 +65,11 @@ export function EmiInstallmentsPanel({ orderId, installments, remainingBalance,
49
65
  return (_jsx(Div, { surface: "card", border: "default", rounded: "lg", padding: "md", children: _jsxs(Stack, { gap: "sm", children: [_jsxs(Row, { align: "center", justify: "between", gap: "sm", children: [_jsx(Text, { weight: "semibold", children: "Your EMI plan" }), remainingBalance !== undefined && remainingBalance > 0 && (_jsxs(Text, { size: "sm", color: "muted", children: [money(remainingBalance), " outstanding across ", outstanding, " instalment", outstanding === 1 ? "" : "s"] }))] }), _jsx(Text, { size: "xs", color: "muted", children: "You can pay any instalment whenever you like, including ahead of its due date \u2014 paying early settles the plan sooner and does not cost more." }), _jsx(Stack, { gap: "xs", children: ordered.map((inst) => {
50
66
  const state = stateOf(inst);
51
67
  const due = inst.dueDate ? new Date(inst.dueDate) : null;
68
+ // Per-installment, so each open row's error is keyed to its own field.
69
+ const fieldName = `emi-reference-${orderId}-${inst.index}`;
52
70
  return (_jsxs(Div, { border: "default", rounded: "md", padding: "sm", children: [_jsxs(Row, { align: "center", justify: "between", gap: "sm", className: "min-w-0", children: [_jsxs(Stack, { gap: "xs", className: "min-w-0", children: [_jsxs(Text, { size: "sm", weight: "medium", children: ["Instalment ", inst.index, " \u00B7 ", money(inst.amount)] }), _jsx(Text, { size: "xs", color: "muted", children: due ? `Due ${due.toLocaleDateString()}` : "No due date recorded" })] }), _jsxs(Row, { align: "center", gap: "xs", className: "flex-shrink-0", children: [_jsx(Badge, { variant: state === "paid" ? "success" : state === "awaiting-review" ? "warning" : "default", children: STATE_LABEL[state] }), state === "payable" && (_jsx(Button, { type: "button", size: "sm", variant: "secondary", onClick: () => {
53
71
  setOpenIndex(openIndex === inst.index ? null : inst.index);
54
72
  setReference("");
55
- }, children: openIndex === inst.index ? "Cancel" : "Pay now" }))] })] }), openIndex === inst.index && (_jsx(Div, { className: "mt-3", children: _jsx(Form, { onSubmit: (e) => e.preventDefault(), children: () => (_jsxs(Stack, { gap: "sm", children: [_jsxs(Text, { size: "xs", color: "muted", children: ["Transfer ", money(inst.amount), " to the seller, then enter the UTR or reference number so they can confirm it."] }), _jsx(FieldInput, { name: `emi-reference-${orderId}-${inst.index}`, label: "UTR / reference number", required: true, value: reference, onChange: setReference }), _jsx(Button, { type: "submit", size: "sm", isLoading: isSubmitting, disabled: reference.trim().length === 0 || isSubmitting, onClick: () => { void submit(inst.index); }, children: "Submit payment reference" })] })) }) }))] }, inst.index));
73
+ }, children: openIndex === inst.index ? "Cancel" : "Pay now" }))] })] }), openIndex === inst.index && (_jsx(Div, { className: "mt-3", children: _jsx(Form, { onSubmit: (e) => e.preventDefault(), children: ({ setFieldError, clearErrors }) => (_jsxs(Stack, { gap: "sm", children: [_jsxs(Text, { size: "xs", color: "muted", children: ["Transfer ", money(inst.amount), " to the seller, then enter the UTR or reference number so they can confirm it."] }), _jsx(FieldInput, { name: fieldName, label: "UTR / reference number", required: true, value: reference, onChange: (v) => { clearErrors(); setReference(v); } }), _jsx(Button, { type: "submit", size: "sm", isLoading: isSubmitting, disabled: isSubmitting, onClick: () => { void submit(inst.index, fieldName, setFieldError); }, children: "Submit payment reference" })] })) }) }))] }, inst.index));
56
74
  }) })] }) }));
57
75
  }
@@ -71,7 +71,7 @@ export function RefundRequestView({ order, onSubmitRequest, isLoading = false, c
71
71
  label: "Why are you returning this?",
72
72
  required: true,
73
73
  fields: ["reasonCode", "reasonNote"],
74
- render: ({ values, onChange }) => (_jsxs(Stack, { gap: "md", children: [_jsxs(Text, { size: "sm", color: "muted", children: [REFUND_COPY.request.orderTotalLabel, " ", _jsx(Span, { weight: "bold", children: formatCurrency(order.total, order.currency ?? "INR") })] }), hasFinalSaleLine && (_jsx(Alert, { variant: "warning", title: REFUND_COPY.request.finalSaleNoticeHeading, children: "You can\u2019t return it for a change of mind, but you can still claim if it never arrived, arrived damaged, was the wrong item, wasn\u2019t as described, or is counterfeit. Only those reasons are listed below." })), _jsxs(Stack, { gap: "xs", children: [_jsxs(Text, { size: "sm", weight: "semibold", children: [REFUND_COPY.request.reasonSelectLabel, " ", _jsx(Span, { "aria-hidden": true, children: "*" })] }), _jsx(PaginatedSelect, { value: values.reasonCode === "" ? null : values.reasonCode, onChange: (v) => onChange({ reasonCode: v ?? "" }), options: allowedReasons.map((r) => ({
74
+ render: ({ values, onChange, errors }) => (_jsxs(Stack, { gap: "md", children: [_jsxs(Text, { size: "sm", color: "muted", children: [REFUND_COPY.request.orderTotalLabel, " ", _jsx(Span, { weight: "bold", children: formatCurrency(order.total, order.currency ?? "INR") })] }), hasFinalSaleLine && (_jsx(Alert, { variant: "warning", title: REFUND_COPY.request.finalSaleNoticeHeading, children: "You can\u2019t return it for a change of mind, but you can still claim if it never arrived, arrived damaged, was the wrong item, wasn\u2019t as described, or is counterfeit. Only those reasons are listed below." })), _jsxs(Stack, { gap: "xs", children: [_jsxs(Text, { size: "sm", weight: "semibold", children: [REFUND_COPY.request.reasonSelectLabel, " ", _jsx(Span, { "aria-hidden": true, children: "*" })] }), _jsx(PaginatedSelect, { name: "reasonCode", error: errors.reasonCode, value: values.reasonCode === "" ? null : values.reasonCode, onChange: (v) => onChange({ reasonCode: v ?? "" }), options: allowedReasons.map((r) => ({
75
75
  value: r,
76
76
  label: RETURN_REASON_LABEL[r],
77
77
  })), placeholder: REFUND_COPY.request.reasonSelectPlaceholder, ariaLabel: REFUND_COPY.request.reasonSelectLabel })] }), _jsx(FieldTextarea, { name: "reasonNote", label: REFUND_COPY.request.reasonNoteLabel, value: values.reasonNote, onChange: (v) => onChange({ reasonNote: v }), rows: 3, maxLength: 500, showCharCount: true })] })),
@@ -81,9 +81,9 @@ export function RefundRequestView({ order, onSubmitRequest, isLoading = false, c
81
81
  label: REFUND_COPY.request.acknowledgeHeading,
82
82
  required: true,
83
83
  fields: ["acknowledged"],
84
- render: ({ values, onChange }) => (_jsx(Stack, { gap: "sm", children: REFUND_COPY.request.acknowledgments.map((text, i) => (_jsx(Checkbox, { checked: values.acknowledged[i] ?? false, onChange: () => onChange({
85
- acknowledged: values.acknowledged.map((v, idx) => idx === i ? !v : v),
86
- }), label: _jsx(Text, { size: "sm", className: "leading-snug", children: text }), "aria-label": `Acknowledgment ${i + 1}` }, i))) })),
84
+ render: ({ values, onChange, errors }) => (_jsxs(Stack, { gap: "sm", children: [REFUND_COPY.request.acknowledgments.map((text, i) => (_jsx(Checkbox, { checked: values.acknowledged[i] ?? false, onChange: () => onChange({
85
+ acknowledged: values.acknowledged.map((v, idx) => idx === i ? !v : v),
86
+ }), label: _jsx(Text, { size: "sm", className: "leading-snug", children: text }), "aria-label": `Acknowledgment ${i + 1}`, "aria-invalid": errors.acknowledged ? "true" : undefined }, i))), errors.acknowledged && (_jsx(Text, { size: "sm", variant: "error", role: "alert", children: errors.acknowledged }))] })),
87
87
  },
88
88
  ], [order.total, order.currency, hasFinalSaleLine, allowedReasons]);
89
89
  const nav = useSectionFormNav(sections, draft);
@@ -1080,6 +1080,25 @@ export declare const ORDER_COLLECTION: "orders";
1080
1080
  * only list it belongs in. (I added it to both first; R1 would have failed.)
1081
1081
  */
1082
1082
  export declare const REFUND_CLAIMS_COLLECTION: "refundClaims";
1083
+ /**
1084
+ * Field names on a refund-claim row.
1085
+ *
1086
+ * 🛑 `STATE`, and it is NOT `ADDRESS_FIELDS.STATE`.
1087
+ * `audit-route-nav-field-constants` R3 suggested exactly that for the raw
1088
+ * `where("state", …)` in `paymentReconcile` — a POSTAL state, on an address.
1089
+ * Borrowing it would have made a refund-claim sweep read as though it filtered
1090
+ * addresses. The field names here are the claim's own lifecycle.
1091
+ *
1092
+ * The collection has no document interface (it is written ad hoc by
1093
+ * `processRefundAction` as an idempotency record), so this is the only place
1094
+ * its field names are stated.
1095
+ */
1096
+ export declare const REFUND_CLAIM_FIELDS: {
1097
+ /** `"claimed"` while a refund is in flight — the idempotency latch. */
1098
+ readonly STATE: "state";
1099
+ readonly CLAIMED_AT: "claimedAt";
1100
+ readonly ORDER_ID: "orderId";
1101
+ };
1083
1102
  export declare const ORDER_INDEXED_FIELDS: readonly ["searchTxt", "userId", "productId", "storeId", "status", "paymentStatus", "payoutStatus", "shippingMethod", "orderDate", "createdAt", "assignedWorkerId", "pickedAt", "packedAt"];
1084
1103
  export declare const DEFAULT_ORDER_DATA: Partial<OrderDocument>;
1085
1104
  export declare const ORDER_PUBLIC_FIELDS: readonly ["id", "productId", "productTitle", "quantity", "unitPrice", "totalPrice", "currency", "status", "paymentStatus", "shippingAddress", "trackingNumber", "notes", "orderDate", "shippingDate", "deliveryDate", "createdAt"];
@@ -73,6 +73,25 @@ export const ORDER_COLLECTION = "orders";
73
73
  * only list it belongs in. (I added it to both first; R1 would have failed.)
74
74
  */
75
75
  export const REFUND_CLAIMS_COLLECTION = "refundClaims";
76
+ /**
77
+ * Field names on a refund-claim row.
78
+ *
79
+ * 🛑 `STATE`, and it is NOT `ADDRESS_FIELDS.STATE`.
80
+ * `audit-route-nav-field-constants` R3 suggested exactly that for the raw
81
+ * `where("state", …)` in `paymentReconcile` — a POSTAL state, on an address.
82
+ * Borrowing it would have made a refund-claim sweep read as though it filtered
83
+ * addresses. The field names here are the claim's own lifecycle.
84
+ *
85
+ * The collection has no document interface (it is written ad hoc by
86
+ * `processRefundAction` as an idempotency record), so this is the only place
87
+ * its field names are stated.
88
+ */
89
+ export const REFUND_CLAIM_FIELDS = {
90
+ /** `"claimed"` while a refund is in flight — the idempotency latch. */
91
+ STATE: "state",
92
+ CLAIMED_AT: "claimedAt",
93
+ ORDER_ID: "orderId",
94
+ };
76
95
  export const ORDER_INDEXED_FIELDS = [
77
96
  "searchTxt",
78
97
  "userId",
@@ -53,8 +53,8 @@ export declare const emiInstallmentSchema: z.ZodObject<{
53
53
  }, z.ZodTypeAny, "passthrough">>]>>;
54
54
  }, "strip", z.ZodTypeAny, {
55
55
  status: "paid" | "pending" | "overdue";
56
- amount: number;
57
56
  index: number;
57
+ amount: number;
58
58
  dueDate: string | Date | z.objectOutputType<{
59
59
  toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
60
60
  }, z.ZodTypeAny, "passthrough">;
@@ -68,8 +68,8 @@ export declare const emiInstallmentSchema: z.ZodObject<{
68
68
  }, z.ZodTypeAny, "passthrough"> | undefined;
69
69
  }, {
70
70
  status: "paid" | "pending" | "overdue";
71
- amount: number;
72
71
  index: number;
72
+ amount: number;
73
73
  dueDate: string | Date | z.objectInputType<{
74
74
  toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
75
75
  }, z.ZodTypeAny, "passthrough">;
@@ -405,8 +405,8 @@ export declare const orderFirestoreSchema: z.ZodObject<{
405
405
  }, z.ZodTypeAny, "passthrough">>]>>;
406
406
  }, "strip", z.ZodTypeAny, {
407
407
  status: "paid" | "pending" | "overdue";
408
- amount: number;
409
408
  index: number;
409
+ amount: number;
410
410
  dueDate: string | Date | z.objectOutputType<{
411
411
  toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
412
412
  }, z.ZodTypeAny, "passthrough">;
@@ -420,8 +420,8 @@ export declare const orderFirestoreSchema: z.ZodObject<{
420
420
  }, z.ZodTypeAny, "passthrough"> | undefined;
421
421
  }, {
422
422
  status: "paid" | "pending" | "overdue";
423
- amount: number;
424
423
  index: number;
424
+ amount: number;
425
425
  dueDate: string | Date | z.objectInputType<{
426
426
  toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
427
427
  }, z.ZodTypeAny, "passthrough">;
@@ -684,8 +684,8 @@ export declare const orderFirestoreSchema: z.ZodObject<{
684
684
  emiSurchargeAmount?: number | undefined;
685
685
  emiInstallments?: {
686
686
  status: "paid" | "pending" | "overdue";
687
- amount: number;
688
687
  index: number;
688
+ amount: number;
689
689
  dueDate: string | Date | z.objectOutputType<{
690
690
  toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
691
691
  }, z.ZodTypeAny, "passthrough">;
@@ -838,8 +838,8 @@ export declare const orderFirestoreSchema: z.ZodObject<{
838
838
  emiSurchargeAmount?: number | undefined;
839
839
  emiInstallments?: {
840
840
  status: "paid" | "pending" | "overdue";
841
- amount: number;
842
841
  index: number;
842
+ amount: number;
843
843
  dueDate: string | Date | z.objectInputType<{
844
844
  toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
845
845
  }, z.ZodTypeAny, "passthrough">;
@@ -2,6 +2,7 @@ import type { DocumentReference } from "firebase-admin/firestore";
2
2
  import type { FirebaseSieveFields, FirebaseSieveResult, SieveModel } from "../../../providers/db-firebase";
3
3
  import { BaseRepository } from "../../../providers/db-firebase";
4
4
  import { type PaymentCaseDocument, type PaymentCaseKind, type PaymentCaseSeverity, type PaymentCaseStatus } from "../schemas/firestore";
5
+ /** gRPC `ALREADY_EXISTS` — the one `.create()` failure meaning "we have it". */
5
6
  export interface OpenCaseInput {
6
7
  kind: PaymentCaseKind;
7
8
  /** Identifies THE PROBLEM, not the observation — see `createPaymentCaseId`. */
@@ -1,20 +1,9 @@
1
+ // One shared predicate — this file had its own copy; see firestore-codes.ts.
2
+ import { isAlreadyExistsError } from "../../../errors/firestore-codes.js";
1
3
  import { BaseRepository, applySieveToFirestore, parseSieveDateValue, prepareForFirestore, } from "../../../providers/db-firebase/index.js";
2
4
  import { normalizeError } from "../../../errors/normalize.js";
3
5
  import { buildHistoryEntry } from "../../../_internal/shared/history/history.js";
4
6
  import { PAYMENT_CASES_COLLECTION, PAYMENT_CASE_ACTIVE_STATUSES, PAYMENT_CASE_FIELDS, PAYMENT_CASE_RESOLUTION_MIN, PAYMENT_CASE_SEVERITY_RANK, createPaymentCaseId, } from "../schemas/firestore.js";
5
- /** gRPC `ALREADY_EXISTS` — the one `.create()` failure meaning "we have it". */
6
- const GRPC_ALREADY_EXISTS = 6;
7
- function isAlreadyExists(err) {
8
- if (typeof err !== "object" || err === null)
9
- return false;
10
- const code = err.code;
11
- if (code === GRPC_ALREADY_EXISTS)
12
- return true;
13
- if (code === "already-exists" || code === "ALREADY_EXISTS")
14
- return true;
15
- const message = err.message;
16
- return typeof message === "string" && message.includes("ALREADY_EXISTS");
17
- }
18
7
  export class PaymentCasesRepository extends BaseRepository {
19
8
  constructor() {
20
9
  super(PAYMENT_CASES_COLLECTION);
@@ -78,7 +67,7 @@ export class PaymentCasesRepository extends BaseRepository {
78
67
  return { id, created: true };
79
68
  }
80
69
  catch (err) {
81
- if (!isAlreadyExists(err)) {
70
+ if (!isAlreadyExistsError(err)) {
82
71
  void normalizeError(err);
83
72
  throw err;
84
73
  }
@@ -88,5 +88,5 @@ export function MarketplacePreorderCard({ product, className = "", variant = "gr
88
88
  * Sibling of the card, never a child — see the prop's docstring. `z-20`
89
89
  * clears this card's own overlays, which all sit at `z-10`.
90
90
  */
91
- return (_jsxs(Div, { className: "flex h-full flex-col", children: [card, _jsx(Div, { className: "flex justify-end px-[var(--appkit-space-3)] pb-[var(--appkit-space-3)]", children: ownerActions })] }));
91
+ return (_jsxs(Div, { layout: "flex-col", className: "h-full", children: [card, _jsx(Div, { layout: "flex", justify: "end", className: "px-[var(--appkit-space-3)] pb-[var(--appkit-space-3)]", children: ownerActions })] }));
92
92
  }
@@ -29,6 +29,7 @@ import { pluginFor } from "../../../_internal/shared/listing-types/_registry.js"
29
29
  import { normalizeListingType } from "../utils/listing-type.js";
30
30
  import { resolveGroupMemberImage } from "../utils/group-member-image.js";
31
31
  import { checkGroupRules, describeGroupRules, EMPTY_GROUP_RULES, } from "../../../_internal/shared/features/products/group-rules.js";
32
+ import { toUserMessage } from "../../../errors/error-display-map.js";
32
33
  const __O = { xAuto: "overflow-x-auto", hidden: "overflow-hidden" };
33
34
  const memberImage = resolveGroupMemberImage;
34
35
  function memberHref(m) {
@@ -112,9 +113,11 @@ export function GroupMemberPicker({ groupId, groupSource, members, rules = EMPTY
112
113
  onAuthRequired?.();
113
114
  return;
114
115
  }
116
+ // `code` is the stable identifier the error envelope carries; `error` is
117
+ // the server's own sentence and is deliberately no longer read here.
115
118
  const json = (await res.json());
116
119
  if (!res.ok || json.success === false) {
117
- setError(json.error ?? "Couldn't add those items to your cart.");
120
+ setError(toUserMessage(json.code, undefined, { fallback: "Couldn't add those items to your cart." }));
118
121
  return;
119
122
  }
120
123
  setQty({});
@@ -77,5 +77,5 @@ export function MarketplaceBundleCard({ bundle, className = "", variant = "grid"
77
77
  * Sibling of the card, never a child — see the prop's docstring. `z-20`
78
78
  * clears this card's own overlays, which all sit at `z-10`.
79
79
  */
80
- return (_jsxs(Div, { className: "flex h-full flex-col", children: [card, _jsx(Div, { className: "flex justify-end px-[var(--appkit-space-3)] pb-[var(--appkit-space-3)]", children: ownerActions })] }));
80
+ return (_jsxs(Div, { layout: "flex-col", className: "h-full", children: [card, _jsx(Div, { layout: "flex", justify: "end", className: "px-[var(--appkit-space-3)] pb-[var(--appkit-space-3)]", children: ownerActions })] }));
81
81
  }
@@ -119,5 +119,5 @@ export function MarketplacePrizeDrawCard({ product, className = "", variant = "g
119
119
  * Sibling of the card, never a child — see the prop's docstring. `z-20`
120
120
  * clears this card's own overlays, which all sit at `z-10`.
121
121
  */
122
- return (_jsxs(Div, { className: "flex h-full flex-col", children: [card, _jsx(Div, { className: "flex justify-end px-[var(--appkit-space-3)] pb-[var(--appkit-space-3)]", children: ownerActions })] }));
122
+ return (_jsxs(Div, { layout: "flex-col", className: "h-full", children: [card, _jsx(Div, { layout: "flex", justify: "end", className: "px-[var(--appkit-space-3)] pb-[var(--appkit-space-3)]", children: ownerActions })] }));
123
123
  }
@@ -12,6 +12,7 @@ import { NonRefundableConsentModal } from "./NonRefundableConsentModal.js";
12
12
  import { useAuthGate } from "../../../react/hooks/useAuthGate.js";
13
13
  import { ACTION_ID } from "../constants/action-defs.js";
14
14
  import { ACTIONS } from "../../../_internal/shared/actions/action-registry.js";
15
+ import { toUserMessage } from "../../../errors/error-display-map.js";
15
16
  /**
16
17
  * Client buy panel for a prize-draw detail page (SB4-G).
17
18
  *
@@ -81,7 +82,7 @@ export function PrizeDrawEntryActions({ productId, productSlug, title, pricePerE
81
82
  * a correct refusal look like the same silent nothing this flow did
82
83
  * before it was repaired.
83
84
  */
84
- showToast(n.message, "error");
85
+ showToast(toUserMessage(n.code, undefined, { fallback: "We couldn't enter you into that draw. Please try again." }), "error");
85
86
  return;
86
87
  }
87
88
  router.push(String(ROUTES.USER.CART));
@@ -15,6 +15,7 @@ import { useBottomActions } from "../../layout/hooks/useBottomActions.js";
15
15
  import { formatCurrency } from "../../../utils/number.formatter.js";
16
16
  import { useCanSeePrices } from "../../../react/hooks/useCanSeePrices.js";
17
17
  import { normalizeError } from "../../../errors/normalize.js";
18
+ import { toUserMessage } from "../../../errors/error-display-map.js";
18
19
  /**
19
20
  * Wires the three primary product-detail CTAs (Buy Now / Add to Cart / Wishlist)
20
21
  * to the server APIs with guest-fallback. Replaces the previously-unwired
@@ -35,7 +36,7 @@ export function ProductDetailActions({ productId, productTitle, productImage, pr
35
36
  // itself — centralised so every add-to-cart call site gets the same
36
37
  // richer toast, not just this one.
37
38
  const addCart = useAddToCart({
38
- onError: (err) => showToast(err?.message ?? "Could not add to cart", "error"),
39
+ onError: (err) => showToast(toUserMessage(err?.code, undefined, { fallback: "Could not add to cart" }), "error"),
39
40
  });
40
41
  async function handleAddToCart(then) {
41
42
  setBusy("cart");
@@ -3,7 +3,7 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
3
3
  import { Row } from "@mohasinac/appkit/ui";
4
4
  import { useRef } from "react";
5
5
  import { useTranslations } from "next-intl";
6
- import { Alert, Checkbox, Div, FormField, FormGroup, Heading, Label, RichTextEditor, Stack, TagInput, Text, Toggle, } from "../../../ui/index.js";
6
+ import { Alert, Checkbox, Div, FormField, FormGroup, Heading, Label, Span, RichTextEditor, Stack, TagInput, Text, Toggle, } from "../../../ui/index.js";
7
7
  import { isFinalSale } from "../constants/final-sale.js";
8
8
  import { PRODUCT_MAX_IMAGES, PRODUCT_MAX_VIDEOS, PRODUCT_IMAGE_INDEX_MAX, } from "../../../_internal/shared/media/limits.js";
9
9
  import { CustomSectionsEditor } from "./CustomSectionsEditor.js";
@@ -313,5 +313,5 @@ canEditTax = false, renderDescriptionEditor, renderCategorySelector, renderPrice
313
313
  disabled: isReadonly,
314
314
  }), !isAuctionListing(product) && renderGroupSettings?.(product), _jsx(ProductFeaturesSelector, { value: product.features ?? [], onChange: (features) => update({ features }), productType: LISTING_TYPE_TO_FEATURE_PRODUCT_TYPE[normalizeListingType(product)], storeId: product.storeId, disabled: isReadonly }), _jsxs(Div, { children: [_jsx(Heading, { level: 3, className: "mb-1", color: "muted", size: "sm", weight: "semibold", children: "Custom Sections" }), _jsx(Text, { className: "mb-3", color: "muted", size: "xs", children: "Add up to 3 custom tabs to your product page \u2014 e.g. \"Box Contents\", \"Compatibility\", \"Grading Details\"." }), _jsx(CustomSectionsEditor, { sections: product.customSections ?? [], onChange: (sections) => update({ customSections: sections }) })] }), _jsx(FormField, { name: "shippingInfo", label: t("formShipping"), type: "textarea", value: product.shippingInfo || "", onChange: (value) => update({ shippingInfo: value }), disabled: isReadonly, placeholder: "Shipping information..." }), _jsxs(Div, { children: [_jsx(Toggle, { checked: !isFinalSale(product), onChange: (checked) => update({ finalSale: !checked }), disabled: isReadonly, label: "Accept change-of-mind returns" }), _jsx(Text, { className: "mt-1", size: "xs", color: "muted", children: isFinalSale(product)
315
315
  ? "Final sale (default). Not-received, damaged, wrong-item, not-as-described and counterfeit claims are still accepted."
316
- : "Buyers can return this listing within the platform return window for any reason." })] }), _jsx(FormField, { name: "returnPolicy", label: t("formReturnPolicy"), type: "textarea", value: product.returnPolicy || "", onChange: (value) => update({ returnPolicy: value }), disabled: isReadonly, placeholder: "Return policy details..." }), _jsxs(Div, { children: [_jsx(Heading, { level: 3, className: "mb-1", color: "muted", size: "sm", weight: "semibold", children: "Physical Inventory" }), existingLocation ? (_jsxs(Text, { className: "mb-3", color: "muted", size: "xs", children: ["Legacy location on this listing: ", _jsx("strong", { children: existingLocation }), ". It is still printed on labels, but it is free text with no box behind it \u2014 place this item in a storage box to replace it."] })) : (_jsx(Text, { className: "mb-3", color: "muted", size: "xs", children: "Where this item is stored is recorded by placing it in a storage box, not typed here \u2014 a placement has a quantity, and one product can sit in two boxes. Use the receive station or the box's own page." }))] }), product.storeName && (_jsx(FormField, { name: "storeName", label: t("formSeller"), type: "text", value: product.storeName, onChange: () => { }, disabled: true }))] }));
316
+ : "Buyers can return this listing within the platform return window for any reason." })] }), _jsx(FormField, { name: "returnPolicy", label: t("formReturnPolicy"), type: "textarea", value: product.returnPolicy || "", onChange: (value) => update({ returnPolicy: value }), disabled: isReadonly, placeholder: "Return policy details..." }), _jsxs(Div, { children: [_jsx(Heading, { level: 3, className: "mb-1", color: "muted", size: "sm", weight: "semibold", children: "Physical Inventory" }), existingLocation ? (_jsxs(Text, { className: "mb-3", color: "muted", size: "xs", children: ["Legacy location on this listing: ", _jsx(Span, { weight: "semibold", children: existingLocation }), ". It is still printed on labels, but it is free text with no box behind it \u2014 place this item in a storage box to replace it."] })) : (_jsx(Text, { className: "mb-3", color: "muted", size: "xs", children: "Where this item is stored is recorded by placing it in a storage box, not typed here \u2014 a placement has a quantity, and one product can sit in two boxes. Use the receive station or the box's own page." }))] }), product.storeName && (_jsx(FormField, { name: "storeName", label: t("formSeller"), type: "text", value: product.storeName, onChange: () => { }, disabled: true }))] }));
317
317
  }