@mohasinac/appkit 4.60.1 → 4.61.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/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 +84 -57
- 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/features/promotions/schema.d.ts +66 -0
- package/dist/_internal/shared/features/promotions/schema.js +7 -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/core/unit-of-work.js +15 -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/admin/components/AdminCouponEditorView.js +8 -0
- package/dist/features/admin/components/AdminGatewayEventsView.js +2 -2
- package/dist/features/admin/components/DataListingView.js +2 -0
- package/dist/features/admin/components/analytics/AdminStatCard.js +4 -1
- 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/auctions/components/MarketplaceAuctionCard.js +1 -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/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 +72 -45
- 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/obligations/repository/obligations.repository.js +10 -1
- package/dist/features/orders/schemas/firestore.d.ts +19 -0
- package/dist/features/orders/schemas/firestore.js +19 -0
- 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/MarketplaceBundleCard.js +1 -1
- package/dist/features/products/components/MarketplacePrizeDrawCard.js +1 -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/schemas/coupon-form.d.ts +5 -0
- package/dist/features/seller/schemas/coupon-form.js +18 -0
- package/dist/features/shell/SectionForm.js +31 -9
- package/dist/features/sticker-trace/server.d.ts +2 -0
- package/dist/http/api-handler.js +4 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -3
- package/dist/next/routing/route-map.d.ts +0 -2
- package/dist/next/routing/route-map.js +10 -1
- package/dist/schemas/registry.d.ts +11 -6
- package/dist/server.d.ts +0 -1
- package/dist/server.js +5 -3
- package/dist/styles.css +51 -0
- package/dist/ui/components/PaginatedSelect.js +0 -0
- 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
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** Just the part of an event this ordering reads. */
|
|
2
|
+
export interface PinnableEvent {
|
|
3
|
+
isPinned?: boolean;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Lift pinned events to the front, preserving the relative order of both
|
|
7
|
+
* groups.
|
|
8
|
+
*
|
|
9
|
+
* 🛑 WHY THIS IS IN MEMORY AND NOT AN `orderBy`. Firestore's `orderBy`
|
|
10
|
+
* excludes every document that lacks the ordering field (Root Cause #100), and
|
|
11
|
+
* `isPinned` is absent from every event that is not pinned — which is all of
|
|
12
|
+
* them bar one. Sorting on it in the query would return an events page
|
|
13
|
+
* containing only the pinned row, with no error and a 200, and the failure
|
|
14
|
+
* reads as "the other events were deleted" rather than as a bad sort.
|
|
15
|
+
*
|
|
16
|
+
* A boolean is also a one-bit sort key, so it would need a composite index
|
|
17
|
+
* paired with `startsAt` for every filter combination the page offers, to buy
|
|
18
|
+
* an ordering a single pass over at most `pageSize` rows already gives.
|
|
19
|
+
*
|
|
20
|
+
* 🛑 THE BOUND, STATED RATHER THAN IMPLIED: this reorders WITHIN the page it
|
|
21
|
+
* is handed. A pinned event that falls on page 3 of a paginated result is
|
|
22
|
+
* lifted to the top of page 3, not onto page 1. That is sufficient while the
|
|
23
|
+
* whole catalogue fits one page — live holds 1 event against a pageSize of
|
|
24
|
+
* 24, and the homepage rail asks for 6 — and it is the reason to revisit this
|
|
25
|
+
* if events ever outgrow a single page. The fix then is a second bounded
|
|
26
|
+
* query on `isPinned == true` (an EQUALITY, which is index-friendly and does
|
|
27
|
+
* not have the missing-field problem an `orderBy` has) merged ahead of the
|
|
28
|
+
* main result, not an `orderBy` on this field.
|
|
29
|
+
*
|
|
30
|
+
* `Array.prototype.sort` is specified as stable, so the comparator only needs
|
|
31
|
+
* to express the pinned/unpinned difference to leave everything else alone.
|
|
32
|
+
*/
|
|
33
|
+
export declare function sortPinnedFirst<T extends PinnableEvent>(items: readonly T[]): T[];
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* WHY: One implementation of "pinned events come first", shared by the two
|
|
3
|
+
* surfaces that render an event list.
|
|
4
|
+
* WHAT: `sortPinnedFirst` — a stable partition on `EventDocument.isPinned`.
|
|
5
|
+
*
|
|
6
|
+
* EXPORTS:
|
|
7
|
+
* sortPinnedFirst — readonly T[] -> T[], pinned first, order otherwise kept
|
|
8
|
+
*
|
|
9
|
+
* @tag domain:events
|
|
10
|
+
* @tag layer:util
|
|
11
|
+
* @tag pattern:none
|
|
12
|
+
* @tag access:isomorphic
|
|
13
|
+
* @tag consumers:features/events/components/EventsListPageView.tsx,features/events/actions/event-actions.ts
|
|
14
|
+
* @tag sideEffects:none
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Lift pinned events to the front, preserving the relative order of both
|
|
18
|
+
* groups.
|
|
19
|
+
*
|
|
20
|
+
* 🛑 WHY THIS IS IN MEMORY AND NOT AN `orderBy`. Firestore's `orderBy`
|
|
21
|
+
* excludes every document that lacks the ordering field (Root Cause #100), and
|
|
22
|
+
* `isPinned` is absent from every event that is not pinned — which is all of
|
|
23
|
+
* them bar one. Sorting on it in the query would return an events page
|
|
24
|
+
* containing only the pinned row, with no error and a 200, and the failure
|
|
25
|
+
* reads as "the other events were deleted" rather than as a bad sort.
|
|
26
|
+
*
|
|
27
|
+
* A boolean is also a one-bit sort key, so it would need a composite index
|
|
28
|
+
* paired with `startsAt` for every filter combination the page offers, to buy
|
|
29
|
+
* an ordering a single pass over at most `pageSize` rows already gives.
|
|
30
|
+
*
|
|
31
|
+
* 🛑 THE BOUND, STATED RATHER THAN IMPLIED: this reorders WITHIN the page it
|
|
32
|
+
* is handed. A pinned event that falls on page 3 of a paginated result is
|
|
33
|
+
* lifted to the top of page 3, not onto page 1. That is sufficient while the
|
|
34
|
+
* whole catalogue fits one page — live holds 1 event against a pageSize of
|
|
35
|
+
* 24, and the homepage rail asks for 6 — and it is the reason to revisit this
|
|
36
|
+
* if events ever outgrow a single page. The fix then is a second bounded
|
|
37
|
+
* query on `isPinned == true` (an EQUALITY, which is index-friendly and does
|
|
38
|
+
* not have the missing-field problem an `orderBy` has) merged ahead of the
|
|
39
|
+
* main result, not an `orderBy` on this field.
|
|
40
|
+
*
|
|
41
|
+
* `Array.prototype.sort` is specified as stable, so the comparator only needs
|
|
42
|
+
* to express the pinned/unpinned difference to leave everything else alone.
|
|
43
|
+
*/
|
|
44
|
+
export function sortPinnedFirst(items) {
|
|
45
|
+
return [...items].sort((a, b) => Number(b.isPinned === true) - Number(a.isPinned === true));
|
|
46
|
+
}
|
|
@@ -1,46 +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 { GATEWAY_EVENTS_COLLECTION, GATEWAY_EVENT_ERROR_MAX_BYTES, GATEWAY_EVENT_FIELDS, GATEWAY_EVENT_LEASE_SECONDS, GATEWAY_EVENT_RETENTION_DAYS, createGatewayEventId, } from "../schemas/firestore.js";
|
|
4
6
|
/** gRPC `ALREADY_EXISTS`. The one failure of `.create()` that means "duplicate". */
|
|
5
|
-
const GRPC_ALREADY_EXISTS = 6;
|
|
6
|
-
/**
|
|
7
|
-
* Is this the specific "a document with that id already exists" failure, as
|
|
8
|
-
* opposed to any other write failure?
|
|
9
|
-
*
|
|
10
|
-
* 🛑 THIS DISTINCTION IS THE WHOLE POINT, and it is where the inbox differs
|
|
11
|
-
* from every other `.create()` claim in the codebase.
|
|
12
|
-
*
|
|
13
|
-
* `refunds/actions.ts` catches *every* error from its claim and treats it as a
|
|
14
|
-
* duplicate — and that is correct THERE, because the cost of being wrong is
|
|
15
|
-
* issuing a second real refund, so it must fail CLOSED.
|
|
16
|
-
*
|
|
17
|
-
* Here the cost of being wrong is inverted: treating a permissions error or a
|
|
18
|
-
* Firestore outage as "already have it" would make the route ack 200 and the
|
|
19
|
-
* event would be **silently lost** — which is the exact failure this collection
|
|
20
|
-
* exists to prevent. So the inbox fails OPEN: anything that is not
|
|
21
|
-
* ALREADY_EXISTS propagates, the route answers 503, and the gateway sends it
|
|
22
|
-
* again.
|
|
23
|
-
*
|
|
24
|
-
* Same primitive, opposite failure direction, because the asymmetry of harm is
|
|
25
|
-
* opposite. Do not "unify" these two call sites.
|
|
26
|
-
*/
|
|
27
|
-
function isAlreadyExists(err) {
|
|
28
|
-
if (typeof err !== "object" || err === null)
|
|
29
|
-
return false;
|
|
30
|
-
const code = err.code;
|
|
31
|
-
if (code === GRPC_ALREADY_EXISTS)
|
|
32
|
-
return true;
|
|
33
|
-
/*
|
|
34
|
-
* The Admin SDK has also surfaced this as a string code and as a plain
|
|
35
|
-
* message depending on version and transport. Checked in addition to, never
|
|
36
|
-
* instead of, the numeric code — a message test alone is the kind of
|
|
37
|
-
* stringly-typed guard that breaks silently on an SDK bump.
|
|
38
|
-
*/
|
|
39
|
-
if (code === "already-exists" || code === "ALREADY_EXISTS")
|
|
40
|
-
return true;
|
|
41
|
-
const message = err.message;
|
|
42
|
-
return typeof message === "string" && message.includes("ALREADY_EXISTS");
|
|
43
|
-
}
|
|
44
7
|
function truncate(value, maxBytes) {
|
|
45
8
|
if (!value)
|
|
46
9
|
return undefined;
|
|
@@ -86,7 +49,7 @@ export class GatewayEventsRepository extends BaseRepository {
|
|
|
86
49
|
return { claimed: true, id, doc };
|
|
87
50
|
}
|
|
88
51
|
catch (err) {
|
|
89
|
-
if (
|
|
52
|
+
if (isAlreadyExistsError(err)) {
|
|
90
53
|
void normalizeError(err);
|
|
91
54
|
return { claimed: false, id };
|
|
92
55
|
}
|
|
@@ -34,12 +34,12 @@ export declare const gatewayEventFirestoreSchema: z.ZodObject<{
|
|
|
34
34
|
createdAt: Date;
|
|
35
35
|
updatedAt: Date;
|
|
36
36
|
expiresAt: Date;
|
|
37
|
+
claimedAt: Date;
|
|
37
38
|
gateway: string;
|
|
38
39
|
receivedAt: Date;
|
|
39
40
|
attempts: number;
|
|
40
41
|
bodySha256: string;
|
|
41
42
|
leaseExpiresAt: Date;
|
|
42
|
-
claimedAt: Date;
|
|
43
43
|
rawEvent: string;
|
|
44
44
|
terminal?: boolean | undefined;
|
|
45
45
|
amount?: number | undefined;
|
|
@@ -59,12 +59,12 @@ export declare const gatewayEventFirestoreSchema: z.ZodObject<{
|
|
|
59
59
|
createdAt: Date;
|
|
60
60
|
updatedAt: Date;
|
|
61
61
|
expiresAt: Date;
|
|
62
|
+
claimedAt: Date;
|
|
62
63
|
gateway: string;
|
|
63
64
|
receivedAt: Date;
|
|
64
65
|
attempts: number;
|
|
65
66
|
bodySha256: string;
|
|
66
67
|
leaseExpiresAt: Date;
|
|
67
|
-
claimedAt: Date;
|
|
68
68
|
rawEvent: string;
|
|
69
69
|
terminal?: boolean | undefined;
|
|
70
70
|
amount?: number | undefined;
|
|
@@ -16,9 +16,10 @@ import { Button, Div, Form, FieldInput, FieldSelect, Stack, Text } from "../../.
|
|
|
16
16
|
import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints.js";
|
|
17
17
|
import { apiClient } from "../../../http/index.js";
|
|
18
18
|
import { sortBy } from "../../../constants/sort.js";
|
|
19
|
+
import { normalizeError } from "../../../errors/normalize.js";
|
|
19
20
|
import { sieveFilter, SIEVE_OP } from "../../../utils/sieve-builder.js";
|
|
20
21
|
import { operatorMessage } from "./operator-message.js";
|
|
21
|
-
import { BoxTypeValues } from "../schemas/firestore.js";
|
|
22
|
+
import { BoxTypeValues, STORAGE_BOX_FIELDS, } from "../schemas/firestore.js";
|
|
22
23
|
const TYPE_OPTIONS = BoxTypeValues.map((v) => ({ value: v, label: v }));
|
|
23
24
|
/**
|
|
24
25
|
* The register-a-box form.
|
|
@@ -26,47 +27,58 @@ const TYPE_OPTIONS = BoxTypeValues.map((v) => ({ value: v, label: v }));
|
|
|
26
27
|
* Exported because it is a PAGE now, not a drawer — see the note on
|
|
27
28
|
* `primaryAction` below for why create could not share the row affordance.
|
|
28
29
|
*/
|
|
30
|
+
// audit-form-sectionised-ok: a three-field quick create (code, type, location),
|
|
31
|
+
// not a multi-section editor. <SectionForm> derives its sections from a schema
|
|
32
|
+
// and pins a mobile action bar for a long scroll; neither has anything to do
|
|
33
|
+
// here, and sections of one field each would be chrome around three inputs.
|
|
29
34
|
export function RegisterBoxForm({ onCreated }) {
|
|
30
35
|
const [code, setCode] = React.useState("");
|
|
31
36
|
const [boxType, setBoxType] = React.useState(BoxTypeValues[0]);
|
|
32
37
|
const [location, setLocation] = React.useState("");
|
|
33
38
|
const [busy, setBusy] = React.useState(false);
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
39
|
+
/*
|
|
40
|
+
* Lifted out of the submit button's `onClick`, where it sat five braces deep
|
|
41
|
+
* inside the render-prop's JSX. The two `<Form>` helpers arrive as arguments
|
|
42
|
+
* rather than being captured, which is what lets this live in the component
|
|
43
|
+
* body beside the state it writes.
|
|
44
|
+
*/
|
|
45
|
+
const submitBox = async (setFieldError, clearErrors) => {
|
|
46
|
+
clearErrors();
|
|
47
|
+
if (!code.trim()) {
|
|
48
|
+
setFieldError("code", "A box needs a code");
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
setBusy(true);
|
|
52
|
+
try {
|
|
53
|
+
await apiClient.post(ADMIN_ENDPOINTS.INVENTORY_BOXES, {
|
|
54
|
+
code: code.trim(),
|
|
55
|
+
boxType,
|
|
56
|
+
...(location.trim() ? { location: location.trim() } : {}),
|
|
57
|
+
});
|
|
58
|
+
onCreated();
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
/*
|
|
62
|
+
* 🛑 `operatorMessage`, not a raw fallback. This read
|
|
63
|
+
* `toUserMessage(undefined, undefined, { fallback: e.message })`
|
|
64
|
+
* under a comment claiming it never showed server text — which is
|
|
65
|
+
* false twice over: `undefined` for the code means the map never
|
|
66
|
+
* runs, so the fallback is ALWAYS what shows. A comment asserting
|
|
67
|
+
* the fix while the code reintroduces the bug is how Root Cause #86
|
|
68
|
+
* would come back, and how the seed `listingType` bug survived its
|
|
69
|
+
* own fix one field over.
|
|
70
|
+
*
|
|
71
|
+
* The split is on status: the 409 here ("Box AC60-03 already
|
|
72
|
+
* exists") is authored for this operator and must survive; a 5xx
|
|
73
|
+
* terminates in a constant.
|
|
74
|
+
*/
|
|
75
|
+
setFieldError("code", operatorMessage(normalizeError(err)));
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
setBusy(false);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
return (_jsx(Form, { onSubmit: (e) => e.preventDefault(), className: "space-y-4", children: ({ setFieldError, clearErrors }) => (_jsxs(Stack, { gap: "md", children: [_jsx(FieldInput, { name: "code", label: "Box code", required: true, value: code, onChange: setCode, placeholder: "AC60-03", hint: "Exactly what is written on the physical label." }), _jsx(FieldSelect, { name: "boxType", label: "Box type", value: boxType, onChange: setBoxType, options: TYPE_OPTIONS }), _jsx(FieldInput, { name: "location", label: "Where it is", value: location, onChange: setLocation, placeholder: "Shelf B, second from top" }), _jsx(Text, { size: "xs", color: "muted", children: "A second box with the same code is the same box \u2014 the registry refuses a duplicate rather than merging two." }), _jsx(Div, { children: _jsx(Button, { type: "submit", variant: "primary", isLoading: busy, onClick: () => submitBox(setFieldError, clearErrors), children: "Register box" }) })] })) }));
|
|
70
82
|
}
|
|
71
83
|
export function AdminStorageBoxesView() {
|
|
72
84
|
const config = React.useMemo(() => ({
|
|
@@ -109,9 +121,13 @@ export function AdminStorageBoxesView() {
|
|
|
109
121
|
* AC60-2 — the kind of ordering that makes an operator distrust the
|
|
110
122
|
* screen — which is why `ordinal` is denormalised on write.
|
|
111
123
|
*/
|
|
112
|
-
defaultSort: "
|
|
124
|
+
defaultSort: sortBy(STORAGE_BOX_FIELDS.ORDINAL, "ASC"),
|
|
113
125
|
sortOptions: [
|
|
114
|
-
|
|
126
|
+
// Same expression as `defaultSort`, not a matching literal beside it —
|
|
127
|
+
// `sortBy(f, "ASC")` is the bare field, so the two are identical by
|
|
128
|
+
// construction. A default that is not one of the options opens the
|
|
129
|
+
// dropdown with nothing selected (Root Cause #63).
|
|
130
|
+
{ value: sortBy(STORAGE_BOX_FIELDS.ORDINAL, "ASC"), label: "Box number" },
|
|
115
131
|
{ value: sortBy("createdAt", "DESC"), label: "Newest" },
|
|
116
132
|
{ value: sortBy("code", "ASC"), label: "Code A–Z" },
|
|
117
133
|
],
|
|
@@ -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. */
|
|
@@ -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. */
|
|
@@ -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",
|
|
@@ -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
|
}
|
|
@@ -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
|
}
|