@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.
- package/dist/.build-stamp.json +3 -3
- package/dist/_internal/client/features/lottery/LotteryAdminEditView.js +19 -3
- package/dist/_internal/client/features/maintenance/views/AnalysisRunnerView.js +1 -1
- package/dist/_internal/client/features/maintenance/views/CloudLogsListView.js +1 -1
- package/dist/_internal/server/features/auctions/actions.d.ts +1 -1
- package/dist/_internal/server/features/auctions/actions.js +12 -1
- package/dist/_internal/server/features/blog/actions.d.ts +1 -1
- package/dist/_internal/server/features/blog/actions.js +12 -1
- package/dist/_internal/server/features/brands/actions.d.ts +1 -1
- package/dist/_internal/server/features/brands/actions.js +12 -1
- package/dist/_internal/server/features/cart/actions.d.ts +1 -1
- package/dist/_internal/server/features/cart/actions.js +12 -1
- package/dist/_internal/server/features/checkout/actions.js +0 -1
- package/dist/_internal/server/features/events/actions.d.ts +1 -1
- package/dist/_internal/server/features/events/actions.js +12 -1
- package/dist/_internal/server/features/gateway-events/redact.js +14 -2
- package/dist/_internal/server/features/history/actions.d.ts +1 -1
- package/dist/_internal/server/features/history/actions.js +12 -1
- package/dist/_internal/server/features/lottery/actions.d.ts +1 -1
- package/dist/_internal/server/features/lottery/actions.js +12 -1
- package/dist/_internal/server/features/orders/actions.d.ts +1 -1
- package/dist/_internal/server/features/orders/actions.js +12 -1
- package/dist/_internal/server/features/orders/cancellation-refund.js +0 -1
- package/dist/_internal/server/features/orders/service.js +31 -1
- package/dist/_internal/server/features/payouts/actions.d.ts +1 -1
- package/dist/_internal/server/features/payouts/actions.js +12 -1
- package/dist/_internal/server/features/points/award.js +9 -1
- package/dist/_internal/server/features/pre-orders/actions.d.ts +1 -1
- package/dist/_internal/server/features/pre-orders/actions.js +12 -1
- package/dist/_internal/server/features/prize-draws/actions.d.ts +1 -1
- package/dist/_internal/server/features/prize-draws/actions.js +12 -1
- package/dist/_internal/server/features/products/actions.d.ts +1 -1
- package/dist/_internal/server/features/products/actions.js +12 -1
- package/dist/_internal/server/features/promotions/actions.d.ts +1 -1
- package/dist/_internal/server/features/promotions/actions.js +12 -1
- package/dist/_internal/server/features/protection-claims/actions.d.ts +2 -2
- package/dist/_internal/server/features/protection-claims/actions.js +21 -12
- package/dist/_internal/server/features/raffle/actions.d.ts +1 -1
- package/dist/_internal/server/features/raffle/actions.js +12 -1
- package/dist/_internal/server/features/refunds/actions.d.ts +1 -3
- package/dist/_internal/server/features/refunds/actions.js +14 -2
- package/dist/_internal/server/features/reviews/actions.d.ts +1 -1
- package/dist/_internal/server/features/reviews/actions.js +12 -1
- package/dist/_internal/server/features/wishlist/actions.d.ts +1 -1
- package/dist/_internal/server/features/wishlist/actions.js +12 -1
- package/dist/_internal/server/jobs/core/obligationSweep.js +0 -1
- package/dist/_internal/server/jobs/core/paymentReconcile.js +93 -76
- package/dist/_internal/server/jobs/core/testDataCleanup.d.ts +1 -1
- package/dist/_internal/server/jobs/core/testDataCleanup.js +22 -1
- package/dist/_internal/server/jobs/handlers/onShipmentAllocationSync.js +16 -4
- package/dist/_internal/shared/actions/action-registry.js +149 -9
- package/dist/_internal/shared/features/promotions/schema.d.ts +66 -0
- package/dist/_internal/shared/features/promotions/schema.js +7 -0
- package/dist/_internal/shared/media/limits.d.ts +10 -0
- package/dist/_internal/shared/media/limits.js +10 -0
- package/dist/_internal/shared/obligations/deadlines.js +11 -1
- package/dist/_internal/shared/tester/window.d.ts +25 -0
- package/dist/_internal/shared/tester/window.js +66 -0
- package/dist/_internal/shared/types/action-result.js +15 -1
- package/dist/client/api/surface-error.js +16 -2
- package/dist/client/api/useApiMutation.js +17 -1
- package/dist/client.d.ts +1 -1
- package/dist/client.js +1 -1
- package/dist/constants/api-endpoints.d.ts +3 -0
- package/dist/constants/api-endpoints.js +1 -0
- package/dist/constants/field-names.d.ts +17 -0
- package/dist/constants/field-names.js +23 -0
- package/dist/core/server-action.js +19 -3
- package/dist/core/unit-of-work.js +15 -1
- package/dist/errors/error-mapping.js +26 -1
- package/dist/errors/firestore-codes.d.ts +37 -0
- package/dist/errors/firestore-codes.js +52 -0
- package/dist/errors/index.d.ts +0 -1
- package/dist/errors/index.js +12 -2
- package/dist/features/account/components/UserOffersPanel.js +5 -3
- package/dist/features/admin/components/AdminActionIndexView.js +100 -5
- package/dist/features/admin/components/AdminAddressClustersView.js +21 -2
- package/dist/features/admin/components/AdminBlogEditorView.js +8 -8
- package/dist/features/admin/components/AdminCarouselEditorView.js +1 -1
- package/dist/features/admin/components/AdminCarouselView.js +96 -4
- package/dist/features/admin/components/AdminCouponEditorView.js +8 -0
- package/dist/features/admin/components/AdminFulfillmentView.js +3 -2
- package/dist/features/admin/components/AdminGatewayEventsView.js +2 -2
- package/dist/features/admin/components/AdminMediaView.js +36 -3
- package/dist/features/admin/components/AdminNavigationView.js +4 -1
- package/dist/features/admin/components/AdminOffersView.js +2 -1
- package/dist/features/admin/components/AdminPaymentClustersView.js +19 -2
- package/dist/features/admin/components/AdminProductEditorView.js +2 -2
- package/dist/features/admin/components/AdminSectionsView.d.ts +10 -0
- package/dist/features/admin/components/AdminSectionsView.js +486 -224
- package/dist/features/admin/components/DataListingView.js +2 -0
- package/dist/features/admin/components/ReferenceRecordEditor.js +1 -1
- package/dist/features/admin/components/analytics/AdminStatCard.js +4 -1
- package/dist/features/admin/hooks/useOptimisticRowFlag.d.ts +33 -0
- package/dist/features/admin/hooks/useOptimisticRowFlag.js +87 -0
- package/dist/features/admin/repository/notification.repository.d.ts +15 -0
- package/dist/features/admin/repository/notification.repository.js +32 -0
- package/dist/features/admin/repository/site-settings.repository.d.ts +2 -1
- package/dist/features/admin/repository/site-settings.repository.js +7 -0
- package/dist/features/admin/schemas/action-index-form.d.ts +34 -0
- package/dist/features/admin/schemas/action-index-form.js +115 -0
- package/dist/features/auctions/components/MarketplaceAuctionCard.js +1 -1
- package/dist/features/auth/components/RegisterForm.js +2 -1
- package/dist/features/catalogue/actions/approve-catalogue-listing.d.ts +1 -1
- package/dist/features/catalogue/actions/approve-catalogue-listing.js +12 -1
- package/dist/features/catalogue/actions/list-from-catalogue.d.ts +1 -1
- package/dist/features/catalogue/actions/list-from-catalogue.js +12 -1
- package/dist/features/catalogue/actions/submit-for-approval.d.ts +1 -1
- package/dist/features/catalogue/actions/submit-for-approval.js +12 -1
- package/dist/features/catalogue/components/CatalogueItemEditorView.js +3 -3
- package/dist/features/categories/repository/price-observation.repository.d.ts +19 -0
- package/dist/features/categories/repository/price-observation.repository.js +21 -2
- package/dist/features/events/actions/event-actions.js +4 -1
- package/dist/features/events/components/AdminEventEditorView.js +79 -50
- package/dist/features/events/components/EventsListPageView.js +2 -1
- package/dist/features/events/schemas/firestore.d.ts +17 -0
- package/dist/features/events/schemas/firestore.js +1 -0
- package/dist/features/events/utils/sort-pinned.d.ts +33 -0
- package/dist/features/events/utils/sort-pinned.js +46 -0
- package/dist/features/gateway-events/repository/gateway-events.repository.js +3 -40
- package/dist/features/gateway-events/schemas/index.d.ts +2 -2
- package/dist/features/inventory/components/AdminStorageBoxesView.js +55 -39
- package/dist/features/inventory/components/ReceiveStationView.js +61 -17
- package/dist/features/inventory/repository/storage-boxes.repository.d.ts +3 -1
- package/dist/features/inventory/repository/storage-boxes.repository.js +36 -3
- package/dist/features/layout/hooks/useFormBottomActions.js +20 -1
- package/dist/features/obligations/repository/obligations.repository.js +10 -1
- package/dist/features/orders/components/EmiInstallmentsPanel.js +25 -7
- package/dist/features/orders/components/RefundRequestView.js +4 -4
- package/dist/features/orders/schemas/firestore.d.ts +19 -0
- package/dist/features/orders/schemas/firestore.js +19 -0
- package/dist/features/orders/schemas/index.d.ts +6 -6
- package/dist/features/payment-cases/repository/payment-cases.repository.d.ts +1 -0
- package/dist/features/payment-cases/repository/payment-cases.repository.js +3 -14
- package/dist/features/pre-orders/components/MarketplacePreorderCard.js +1 -1
- package/dist/features/products/components/GroupMemberPicker.js +4 -1
- package/dist/features/products/components/MarketplaceBundleCard.js +1 -1
- package/dist/features/products/components/MarketplacePrizeDrawCard.js +1 -1
- package/dist/features/products/components/PrizeDrawEntryActions.js +2 -1
- package/dist/features/products/components/ProductDetailActions.js +2 -1
- package/dist/features/products/components/ProductForm.js +2 -2
- package/dist/features/products/components/ProductGrid.js +1 -1
- package/dist/features/promotions/repository/claimed-coupons.repository.js +31 -1
- package/dist/features/promotions/schemas/firestore.d.ts +22 -0
- package/dist/features/promotions/schemas/firestore.js +22 -0
- package/dist/features/promotions/schemas/index.d.ts +479 -6
- package/dist/features/promotions/schemas/index.js +153 -0
- package/dist/features/protection-claims/repository/protection-claims.repository.js +22 -15
- package/dist/features/seller/components/FulfillmentView.js +3 -2
- package/dist/features/seller/components/SellerAddressesView.js +32 -4
- package/dist/features/seller/components/SellerAnalyticsAlertsView.js +62 -3
- package/dist/features/seller/components/SellerBundlesView.js +53 -14
- package/dist/features/seller/components/SellerGroupedListingsView.js +23 -3
- package/dist/features/seller/components/SellerListingTypeView.js +56 -10
- package/dist/features/seller/components/SellerPayoutMethodsView.js +33 -1
- package/dist/features/seller/components/SellerPayoutSettingsView.js +48 -9
- package/dist/features/seller/components/SellerProductsView.js +5 -1
- package/dist/features/seller/components/SellerShippingConfigsView.js +33 -1
- package/dist/features/seller/components/SellerShippingView.js +2 -1
- package/dist/features/seller/components/SellerStoreCategoriesView.js +33 -1
- package/dist/features/seller/components/SellerSupportView.js +69 -2
- package/dist/features/seller/schemas/coupon-form.d.ts +5 -0
- package/dist/features/seller/schemas/coupon-form.js +18 -0
- package/dist/features/shell/SectionForm.js +35 -9
- package/dist/features/shell/build-sections.js +14 -1
- package/dist/features/shipments/components/AdminShipmentLotItemsView.js +54 -1
- package/dist/features/shipments/components/AdminShipmentProjectionsView.js +3 -2
- package/dist/features/sticker-trace/components/StickerCommitPanel.js +2 -1
- package/dist/features/sticker-trace/components/StickerPrintPanel.js +2 -1
- package/dist/features/sticker-trace/components/StickerRetryPanel.js +24 -4
- package/dist/features/sticker-trace/components/StickerTraceStudio.js +2 -1
- package/dist/features/sticker-trace/server.d.ts +2 -0
- package/dist/features/tester/components/AdminTesterFeedbackIssuesView.js +3 -2
- package/dist/http/api-handler.js +4 -1
- package/dist/index.d.ts +8 -4
- package/dist/index.js +6 -4
- package/dist/next/routing/route-map.d.ts +4 -2
- package/dist/next/routing/route-map.js +19 -1
- package/dist/schemas/registry.d.ts +15 -10
- package/dist/schemas/sieve.d.ts +3 -3
- package/dist/server.d.ts +1 -2
- package/dist/server.js +6 -4
- package/dist/styles.css +51 -0
- package/dist/ui/components/Checkbox.js +10 -2
- package/dist/ui/components/DateInput.js +8 -1
- package/dist/ui/components/Input.js +22 -2
- package/dist/ui/components/PaginatedSelect.d.ts +8 -19
- package/dist/ui/components/PaginatedSelect.js +0 -0
- package/dist/ui/components/Radio.js +8 -1
- package/dist/ui/components/Select.js +10 -2
- package/dist/ui/components/TagInput.d.ts +8 -1
- package/dist/ui/components/TagInput.js +4 -3
- package/dist/ui/components/Textarea.js +10 -2
- package/dist/ui/forms/FieldCheckbox.js +1 -1
- package/dist/ui/forms/FormErrorList.d.ts +0 -16
- package/dist/ui/forms/FormErrorList.js +34 -1
- package/dist/ui/forms/apply-zod-issues.d.ts +19 -1
- package/dist/ui/forms/apply-zod-issues.js +23 -1
- package/package.json +1 -1
- package/scripts/seed-cli.mjs +1822 -1812
- package/dist/features/tester/utils/phases.d.ts +0 -48
- 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
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
-
|
|
240
|
-
|
|
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
|
-
|
|
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) =>
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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:
|
|
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 }) => (
|
|
85
|
-
|
|
86
|
-
|
|
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 (!
|
|
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, {
|
|
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.
|
|
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, {
|
|
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, {
|
|
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.
|
|
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?.
|
|
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("
|
|
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
|
}
|