@mohasinac/appkit 4.14.0 → 4.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_internal/server/features/auctions/actions.js +2 -2
- package/dist/_internal/server/features/cart/service.d.ts +3 -3
- package/dist/_internal/server/features/checkout/actions.js +19 -0
- package/dist/_internal/server/features/checkout/source-context.d.ts +5 -0
- package/dist/_internal/server/features/checkout/source-context.js +157 -0
- package/dist/_internal/server/features/offers/adapters.d.ts +103 -0
- package/dist/_internal/server/features/offers/adapters.js +88 -0
- package/dist/_internal/server/features/offers/index.d.ts +2 -0
- package/dist/_internal/server/features/offers/index.js +1 -0
- package/dist/_internal/server/features/orders/adapters.js +24 -0
- package/dist/_internal/server/jobs/core/offerExpiry.js +10 -10
- package/dist/_internal/shared/actions/action-registry.js +3 -1
- package/dist/_internal/shared/features/auctions/schema.d.ts +14 -2
- package/dist/_internal/shared/features/auctions/schema.js +13 -1
- package/dist/_internal/shared/features/cart/schema.d.ts +14 -9
- package/dist/_internal/shared/features/cart/schema.js +15 -0
- package/dist/_internal/shared/features/products/schema.d.ts +2 -2
- package/dist/_internal/shared/history/history.d.ts +22 -0
- package/dist/_internal/shared/history/history.js +41 -1
- package/dist/client.d.ts +13 -2
- package/dist/client.js +20 -2
- package/dist/constants/field-names.d.ts +8 -6
- package/dist/constants/field-names.js +48 -6
- package/dist/features/account/components/UserBidsView.js +12 -4
- package/dist/features/account/components/UserReturnsView.d.ts +3 -0
- package/dist/features/account/components/UserReturnsView.js +17 -1
- package/dist/features/admin/components/AdminAuditLogView.js +1 -0
- package/dist/features/admin/components/AdminBidsView.js +15 -2
- package/dist/features/admin/components/AdminBlogEditorView.js +65 -33
- package/dist/features/admin/components/AdminFeatureEditorView.js +35 -6
- package/dist/features/admin/components/AdminNavEditorView.js +15 -5
- package/dist/features/admin/components/AdminNewsletterView.js +18 -2
- package/dist/features/admin/components/AdminOffersView.js +62 -18
- package/dist/features/admin/components/AdminPaymentMethodsView.js +24 -4
- package/dist/features/admin/components/AdminScammerEditorView.d.ts +2 -1
- package/dist/features/admin/components/AdminScammerEditorView.js +3 -13
- package/dist/features/admin/components/AdminScammersView.js +3 -8
- package/dist/features/admin/components/AdminSectionsView.js +32 -23
- package/dist/features/admin/components/AdminSessionsView.js +17 -2
- package/dist/features/admin/components/ViewAuditLogEntryModal.js +1 -0
- package/dist/features/admin/components/sections/adminSectionsTypes.d.ts +4 -2
- package/dist/features/admin/components/sections/adminSectionsTypes.js +45 -27
- package/dist/features/admin/constants/filter-tabs.d.ts +3 -0
- package/dist/features/admin/constants/filter-tabs.js +1 -0
- package/dist/features/admin/schemas/homepage-section-form.d.ts +22 -0
- package/dist/features/admin/schemas/homepage-section-form.js +68 -0
- package/dist/features/admin/schemas/nav-item-form.d.ts +74 -0
- package/dist/features/admin/schemas/nav-item-form.js +72 -0
- package/dist/features/auctions/components/AuctionBidsTable.d.ts +10 -1
- package/dist/features/auctions/components/AuctionBidsTable.js +22 -4
- package/dist/features/auctions/components/bid-detail-fields.d.ts +29 -0
- package/dist/features/auctions/components/bid-detail-fields.js +103 -0
- package/dist/features/audit-log/schemas/firestore.d.ts +14 -0
- package/dist/features/audit-log/schemas/firestore.js +7 -0
- package/dist/features/blog/schemas/blog-form.d.ts +80 -0
- package/dist/features/blog/schemas/blog-form.js +100 -0
- package/dist/features/events/components/AdminEventEditorView.js +32 -54
- package/dist/features/events/components/AdminEventEntriesView.js +22 -3
- package/dist/features/events/schemas/event-form.d.ts +113 -0
- package/dist/features/events/schemas/event-form.js +82 -0
- package/dist/features/grouped/schemas/grouped-listing-form.d.ts +101 -0
- package/dist/features/grouped/schemas/grouped-listing-form.js +89 -0
- package/dist/features/homepage/actions/homepage-section-actions.d.ts +11 -3
- package/dist/features/homepage/actions/homepage-section-actions.js +33 -1
- package/dist/features/homepage/schemas/index.d.ts +31 -0
- package/dist/features/homepage/schemas/index.js +24 -0
- package/dist/features/orders/components/OrderStatusTimeline.d.ts +5 -1
- package/dist/features/orders/components/OrderStatusTimeline.js +39 -20
- package/dist/features/orders/repository/orders.repository.js +5 -0
- package/dist/features/orders/schemas/firestore.d.ts +36 -1
- package/dist/features/orders/types/index.d.ts +35 -7
- package/dist/features/products/components/GroupSettingsPanel.js +5 -1
- package/dist/features/products/schemas/index.d.ts +2 -2
- package/dist/features/products/schemas/product-features.validators.d.ts +73 -0
- package/dist/features/products/schemas/product-features.validators.js +63 -0
- package/dist/features/scams/constants/scammer-status-display.d.ts +28 -0
- package/dist/features/scams/constants/scammer-status-display.js +79 -0
- package/dist/features/seller/actions/offer-actions.js +32 -2
- package/dist/features/seller/components/OfferPhaseTimeline.d.ts +59 -0
- package/dist/features/seller/components/OfferPhaseTimeline.js +107 -0
- package/dist/features/seller/components/SellerBidsView.js +11 -4
- package/dist/features/seller/components/SellerCouponEditorView.js +17 -17
- package/dist/features/seller/components/SellerOffersView.js +2 -1
- package/dist/features/seller/components/SellerPayoutSettingsView.js +38 -14
- package/dist/features/seller/repository/offer.repository.d.ts +69 -8
- package/dist/features/seller/repository/offer.repository.js +127 -23
- package/dist/features/seller/schemas/coupon-form.d.ts +72 -0
- package/dist/features/seller/schemas/coupon-form.js +116 -0
- package/dist/features/seller/schemas/firestore.d.ts +56 -2
- package/dist/features/seller/schemas/firestore.js +27 -0
- package/dist/features/seller/schemas/offer-forms.d.ts +21 -0
- package/dist/features/seller/schemas/offer-forms.js +21 -0
- package/dist/features/shell/build-sections.d.ts +63 -0
- package/dist/features/shell/build-sections.js +207 -0
- package/dist/features/shell/field-ui-meta.d.ts +115 -0
- package/dist/features/shell/field-ui-meta.js +324 -0
- package/dist/features/shell/index.d.ts +4 -0
- package/dist/features/shell/index.js +2 -0
- package/dist/features/status-history/components/StatusTimeline.d.ts +74 -0
- package/dist/features/status-history/components/StatusTimeline.js +46 -0
- package/dist/features/status-history/index.d.ts +11 -0
- package/dist/features/status-history/index.js +10 -0
- package/dist/features/store-extensions/components/ReviewDecisionModal.d.ts +27 -0
- package/dist/features/store-extensions/components/ReviewDecisionModal.js +82 -0
- package/dist/features/store-extensions/index.d.ts +12 -0
- package/dist/features/store-extensions/index.js +12 -0
- package/dist/features/store-extensions/schemas/admin-notification-form.d.ts +36 -0
- package/dist/features/store-extensions/schemas/admin-notification-form.js +114 -0
- package/dist/features/store-extensions/schemas/analytics-forms.d.ts +57 -0
- package/dist/features/store-extensions/schemas/analytics-forms.js +158 -0
- package/dist/features/store-extensions/schemas/custom-role-form.d.ts +85 -0
- package/dist/features/store-extensions/schemas/custom-role-form.js +118 -0
- package/dist/features/store-extensions/schemas/firestore.d.ts +21 -1
- package/dist/features/store-extensions/schemas/firestore.js +60 -0
- package/dist/features/store-extensions/schemas/google-config-form.d.ts +53 -0
- package/dist/features/store-extensions/schemas/google-config-form.js +105 -0
- package/dist/features/store-extensions/schemas/item-request-create-form.d.ts +28 -0
- package/dist/features/store-extensions/schemas/item-request-create-form.js +80 -0
- package/dist/features/store-extensions/schemas/listing-template-form.d.ts +78 -0
- package/dist/features/store-extensions/schemas/listing-template-form.js +87 -0
- package/dist/features/store-extensions/schemas/moderation-review-form.d.ts +49 -0
- package/dist/features/store-extensions/schemas/moderation-review-form.js +101 -0
- package/dist/features/store-extensions/schemas/payout-method-form.d.ts +153 -0
- package/dist/features/store-extensions/schemas/payout-method-form.js +155 -0
- package/dist/features/store-extensions/schemas/report-create-form.d.ts +25 -0
- package/dist/features/store-extensions/schemas/report-create-form.js +79 -0
- package/dist/features/store-extensions/schemas/report-review-form.d.ts +47 -0
- package/dist/features/store-extensions/schemas/report-review-form.js +111 -0
- package/dist/features/store-extensions/schemas/shipping-config-form.d.ts +148 -0
- package/dist/features/store-extensions/schemas/shipping-config-form.js +104 -0
- package/dist/features/store-extensions/schemas/store-category-form.d.ts +54 -0
- package/dist/features/store-extensions/schemas/store-category-form.js +73 -0
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +326 -0
- package/dist/index.d.ts +12 -2
- package/dist/index.js +12 -2
- package/dist/next/routing/route-map.d.ts +16 -2
- package/dist/next/routing/route-map.js +9 -2
- package/dist/schemas/firestore-value.d.ts +8 -0
- package/dist/schemas/firestore-value.js +44 -0
- package/dist/schemas/index.d.ts +1 -1
- package/dist/schemas/index.js +1 -1
- package/dist/schemas/registry.d.ts +634 -1
- package/dist/schemas/registry.js +64 -1
- package/dist/seed/bids-seed-data.js +30 -0
- package/dist/seed/offers-seed-data.js +212 -7
- package/dist/seed/orders-seed-data.js +336 -1
- package/dist/seed/products-auctions-seed-data.js +41 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +5 -0
- package/dist/styles.css +1 -1
- package/dist/tailwind-utilities.css +1 -1
- package/dist/ui/components/RecordDetailModal.d.ts +10 -1
- package/dist/ui/components/RecordDetailModal.js +2 -2
- package/package.json +2 -1
- package/scripts/node-esm-loader.mjs +65 -0
- package/scripts/seed-cli-loader.mjs +6 -49
- package/scripts/seed-cli.mjs +1 -1
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/** The statuses a human review can produce. `pending` is the initial state. */
|
|
3
|
+
export declare const REPORT_REVIEWER_STATUSES: readonly ["under-review", "actioned", "dismissed"];
|
|
4
|
+
/**
|
|
5
|
+
* The two that CLOSE a report — the server stamps `resolvedAt` for these and
|
|
6
|
+
* for no others. Exported so the route and any future consumer share one
|
|
7
|
+
* answer to "is this report finished", rather than each spelling out the pair.
|
|
8
|
+
*/
|
|
9
|
+
export declare const REPORT_TERMINAL_STATUSES: readonly ["actioned", "dismissed"];
|
|
10
|
+
/**
|
|
11
|
+
* 🛑 `annotate()` must be the OUTERMOST call on each field — it keys a WeakMap
|
|
12
|
+
* by schema instance and every zod wrapper returns a new one.
|
|
13
|
+
*/
|
|
14
|
+
export declare const reportReviewFormSchema: z.ZodEffects<z.ZodObject<{
|
|
15
|
+
status: z.ZodEnum<["under-review", "actioned", "dismissed"]>;
|
|
16
|
+
resolution: z.ZodOptional<z.ZodString>;
|
|
17
|
+
}, "strict", z.ZodTypeAny, {
|
|
18
|
+
status: "dismissed" | "under-review" | "actioned";
|
|
19
|
+
resolution?: string | undefined;
|
|
20
|
+
}, {
|
|
21
|
+
status: "dismissed" | "under-review" | "actioned";
|
|
22
|
+
resolution?: string | undefined;
|
|
23
|
+
}>, {
|
|
24
|
+
status: "dismissed" | "under-review" | "actioned";
|
|
25
|
+
resolution?: string | undefined;
|
|
26
|
+
}, {
|
|
27
|
+
status: "dismissed" | "under-review" | "actioned";
|
|
28
|
+
resolution?: string | undefined;
|
|
29
|
+
}>;
|
|
30
|
+
export type ReportReviewFormValues = z.infer<typeof reportReviewFormSchema>;
|
|
31
|
+
/** Route contract — identical; a review is one atomic decision. */
|
|
32
|
+
export declare const reportReviewUpdateSchema: z.ZodEffects<z.ZodObject<{
|
|
33
|
+
status: z.ZodEnum<["under-review", "actioned", "dismissed"]>;
|
|
34
|
+
resolution: z.ZodOptional<z.ZodString>;
|
|
35
|
+
}, "strict", z.ZodTypeAny, {
|
|
36
|
+
status: "dismissed" | "under-review" | "actioned";
|
|
37
|
+
resolution?: string | undefined;
|
|
38
|
+
}, {
|
|
39
|
+
status: "dismissed" | "under-review" | "actioned";
|
|
40
|
+
resolution?: string | undefined;
|
|
41
|
+
}>, {
|
|
42
|
+
status: "dismissed" | "under-review" | "actioned";
|
|
43
|
+
resolution?: string | undefined;
|
|
44
|
+
}, {
|
|
45
|
+
status: "dismissed" | "under-review" | "actioned";
|
|
46
|
+
resolution?: string | undefined;
|
|
47
|
+
}>;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* WHY: `PATCH /api/admin/reports/[id]` spread the raw request body straight
|
|
3
|
+
* into Firestore, exactly as the moderation route did:
|
|
4
|
+
*
|
|
5
|
+
* const body = await parseJsonBody<Record<string, JsonValue>>(request);
|
|
6
|
+
* await reportsRepository.update(id, { ...body, assignedTo: user.uid });
|
|
7
|
+
*
|
|
8
|
+
* No Zod on the route, none on either client surface. `status` was never
|
|
9
|
+
* checked against the real `ReportStatus` union, and any key at all could
|
|
10
|
+
* be written onto a report — `reporterId`, `entityId`, `evidenceUrls`,
|
|
11
|
+
* or something invented.
|
|
12
|
+
*
|
|
13
|
+
* ## 🛑 And it was actively corrupting `resolvedAt`
|
|
14
|
+
*
|
|
15
|
+
* BOTH client surfaces — the list page's inline controls and
|
|
16
|
+
* `ReportDetailActions` — sent:
|
|
17
|
+
*
|
|
18
|
+
* resolvedAt: status === "actioned" || status === "dismissed"
|
|
19
|
+
* ? new Date() : undefined
|
|
20
|
+
*
|
|
21
|
+
* `new Date()` does not survive `JSON.stringify`. It arrives at the route
|
|
22
|
+
* as an ISO **string**, and the raw spread wrote that string into a field
|
|
23
|
+
* `ReportDocument` declares as `Date`. Seeded reports hold real
|
|
24
|
+
* Timestamps, so the collection carries BOTH shapes for one field —
|
|
25
|
+
* the sort of split that later reads as "the date filter is broken" long
|
|
26
|
+
* after the cause is out of sight (Recurrent Root Cause #47).
|
|
27
|
+
*
|
|
28
|
+
* Nothing renders `resolvedAt` today, which is the only reason it was
|
|
29
|
+
* never noticed. It is a latent corruption, not a cosmetic one.
|
|
30
|
+
*
|
|
31
|
+
* WHAT: One schema shared by the route and both client surfaces — and
|
|
32
|
+
* `resolvedAt` is REMOVED from the client contract entirely. The server
|
|
33
|
+
* stamps it, the same way the moderation route already stamps
|
|
34
|
+
* `reviewedAt`. A timestamp the client cannot send is a timestamp the
|
|
35
|
+
* client cannot send in the wrong type.
|
|
36
|
+
*
|
|
37
|
+
* ## A reviewer may only set the three statuses a REVIEW produces
|
|
38
|
+
*
|
|
39
|
+
* `ReportStatus` has four values; `pending` is the state a report is born in.
|
|
40
|
+
* The settable subset is declared `satisfies readonly ReportStatus[]` rather
|
|
41
|
+
* than restated, so a value outside the real union is a COMPILE error instead
|
|
42
|
+
* of a filter that silently matches nothing (Recurrent Root Cause #33).
|
|
43
|
+
*
|
|
44
|
+
* EXPORTS:
|
|
45
|
+
* reportReviewFormSchema, reportReviewUpdateSchema,
|
|
46
|
+
* REPORT_REVIEWER_STATUSES, REPORT_TERMINAL_STATUSES,
|
|
47
|
+
* type ReportReviewFormValues
|
|
48
|
+
*
|
|
49
|
+
* @tag domain:store-extensions,reports
|
|
50
|
+
* @tag layer:schema
|
|
51
|
+
* @tag pattern:none
|
|
52
|
+
* @tag access:isomorphic
|
|
53
|
+
* @tag consumers:admin/reports pages,/api/admin/reports/[id]
|
|
54
|
+
* @tag sideEffects:none
|
|
55
|
+
*/
|
|
56
|
+
import { z } from "zod";
|
|
57
|
+
import { annotate } from "../../shell/field-ui-meta";
|
|
58
|
+
/** The statuses a human review can produce. `pending` is the initial state. */
|
|
59
|
+
export const REPORT_REVIEWER_STATUSES = [
|
|
60
|
+
"under-review",
|
|
61
|
+
"actioned",
|
|
62
|
+
"dismissed",
|
|
63
|
+
];
|
|
64
|
+
/**
|
|
65
|
+
* The two that CLOSE a report — the server stamps `resolvedAt` for these and
|
|
66
|
+
* for no others. Exported so the route and any future consumer share one
|
|
67
|
+
* answer to "is this report finished", rather than each spelling out the pair.
|
|
68
|
+
*/
|
|
69
|
+
export const REPORT_TERMINAL_STATUSES = [
|
|
70
|
+
"actioned",
|
|
71
|
+
"dismissed",
|
|
72
|
+
];
|
|
73
|
+
/**
|
|
74
|
+
* 🛑 `annotate()` must be the OUTERMOST call on each field — it keys a WeakMap
|
|
75
|
+
* by schema instance and every zod wrapper returns a new one.
|
|
76
|
+
*/
|
|
77
|
+
export const reportReviewFormSchema = z
|
|
78
|
+
.object({
|
|
79
|
+
status: annotate(z.enum(REPORT_REVIEWER_STATUSES), {
|
|
80
|
+
section: "review",
|
|
81
|
+
sectionLabel: "Report decision",
|
|
82
|
+
sectionRequired: true,
|
|
83
|
+
quick: true,
|
|
84
|
+
order: 1,
|
|
85
|
+
row: "pair",
|
|
86
|
+
}),
|
|
87
|
+
resolution: annotate(z.string().max(1000, "Keep the resolution under 1000 characters.").optional(), {
|
|
88
|
+
section: "review",
|
|
89
|
+
order: 2,
|
|
90
|
+
row: "full",
|
|
91
|
+
kind: "textarea",
|
|
92
|
+
help: "Required when actioning or dismissing — the reporter may see a summary.",
|
|
93
|
+
}),
|
|
94
|
+
// `resolvedAt` is deliberately NOT a field. See the header.
|
|
95
|
+
})
|
|
96
|
+
.strict()
|
|
97
|
+
.superRefine((v, ctx) => {
|
|
98
|
+
// Taking a report for review needs no note; closing one does. "Actioned"
|
|
99
|
+
// with an empty resolution tells the reporter, and the next admin, nothing
|
|
100
|
+
// about what was actually done.
|
|
101
|
+
if (REPORT_TERMINAL_STATUSES.includes(v.status) &&
|
|
102
|
+
!v.resolution?.trim()) {
|
|
103
|
+
ctx.addIssue({
|
|
104
|
+
code: z.ZodIssueCode.custom,
|
|
105
|
+
path: ["resolution"],
|
|
106
|
+
message: "Say what was done about this report before closing it.",
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
/** Route contract — identical; a review is one atomic decision. */
|
|
111
|
+
export const reportReviewUpdateSchema = reportReviewFormSchema;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* 🛑 `annotate()` must be the OUTERMOST call on each field — it keys a WeakMap
|
|
4
|
+
* by schema instance and every zod wrapper returns a new one.
|
|
5
|
+
*/
|
|
6
|
+
export declare const shippingConfigFormSchema: z.ZodEffects<z.ZodObject<{
|
|
7
|
+
label: z.ZodString;
|
|
8
|
+
method: z.ZodEnum<["free", "flat", "weight", "express", "pickup", "custom"]>;
|
|
9
|
+
flatRate: z.ZodOptional<z.ZodNumber>;
|
|
10
|
+
pricePerKg: z.ZodOptional<z.ZodNumber>;
|
|
11
|
+
freeAbove: z.ZodOptional<z.ZodNumber>;
|
|
12
|
+
expressSurcharge: z.ZodOptional<z.ZodNumber>;
|
|
13
|
+
estimatedDays: z.ZodOptional<z.ZodNumber>;
|
|
14
|
+
isDefault: z.ZodBoolean;
|
|
15
|
+
isActive: z.ZodBoolean;
|
|
16
|
+
}, "strip", z.ZodTypeAny, {
|
|
17
|
+
label: string;
|
|
18
|
+
isDefault: boolean;
|
|
19
|
+
isActive: boolean;
|
|
20
|
+
method: "flat" | "weight" | "custom" | "free" | "express" | "pickup";
|
|
21
|
+
estimatedDays?: number | undefined;
|
|
22
|
+
freeAbove?: number | undefined;
|
|
23
|
+
flatRate?: number | undefined;
|
|
24
|
+
pricePerKg?: number | undefined;
|
|
25
|
+
expressSurcharge?: number | undefined;
|
|
26
|
+
}, {
|
|
27
|
+
label: string;
|
|
28
|
+
isDefault: boolean;
|
|
29
|
+
isActive: boolean;
|
|
30
|
+
method: "flat" | "weight" | "custom" | "free" | "express" | "pickup";
|
|
31
|
+
estimatedDays?: number | undefined;
|
|
32
|
+
freeAbove?: number | undefined;
|
|
33
|
+
flatRate?: number | undefined;
|
|
34
|
+
pricePerKg?: number | undefined;
|
|
35
|
+
expressSurcharge?: number | undefined;
|
|
36
|
+
}>, {
|
|
37
|
+
label: string;
|
|
38
|
+
isDefault: boolean;
|
|
39
|
+
isActive: boolean;
|
|
40
|
+
method: "flat" | "weight" | "custom" | "free" | "express" | "pickup";
|
|
41
|
+
estimatedDays?: number | undefined;
|
|
42
|
+
freeAbove?: number | undefined;
|
|
43
|
+
flatRate?: number | undefined;
|
|
44
|
+
pricePerKg?: number | undefined;
|
|
45
|
+
expressSurcharge?: number | undefined;
|
|
46
|
+
}, {
|
|
47
|
+
label: string;
|
|
48
|
+
isDefault: boolean;
|
|
49
|
+
isActive: boolean;
|
|
50
|
+
method: "flat" | "weight" | "custom" | "free" | "express" | "pickup";
|
|
51
|
+
estimatedDays?: number | undefined;
|
|
52
|
+
freeAbove?: number | undefined;
|
|
53
|
+
flatRate?: number | undefined;
|
|
54
|
+
pricePerKg?: number | undefined;
|
|
55
|
+
expressSurcharge?: number | undefined;
|
|
56
|
+
}>;
|
|
57
|
+
export type ShippingConfigFormValues = z.infer<typeof shippingConfigFormSchema>;
|
|
58
|
+
/** `storeId` is absent — the route sets it from the session, never the body. */
|
|
59
|
+
export declare const shippingConfigCreateSchema: z.ZodEffects<z.ZodObject<{
|
|
60
|
+
label: z.ZodString;
|
|
61
|
+
method: z.ZodEnum<["free", "flat", "weight", "express", "pickup", "custom"]>;
|
|
62
|
+
flatRate: z.ZodOptional<z.ZodNumber>;
|
|
63
|
+
pricePerKg: z.ZodOptional<z.ZodNumber>;
|
|
64
|
+
freeAbove: z.ZodOptional<z.ZodNumber>;
|
|
65
|
+
expressSurcharge: z.ZodOptional<z.ZodNumber>;
|
|
66
|
+
estimatedDays: z.ZodOptional<z.ZodNumber>;
|
|
67
|
+
isDefault: z.ZodBoolean;
|
|
68
|
+
isActive: z.ZodBoolean;
|
|
69
|
+
}, "strip", z.ZodTypeAny, {
|
|
70
|
+
label: string;
|
|
71
|
+
isDefault: boolean;
|
|
72
|
+
isActive: boolean;
|
|
73
|
+
method: "flat" | "weight" | "custom" | "free" | "express" | "pickup";
|
|
74
|
+
estimatedDays?: number | undefined;
|
|
75
|
+
freeAbove?: number | undefined;
|
|
76
|
+
flatRate?: number | undefined;
|
|
77
|
+
pricePerKg?: number | undefined;
|
|
78
|
+
expressSurcharge?: number | undefined;
|
|
79
|
+
}, {
|
|
80
|
+
label: string;
|
|
81
|
+
isDefault: boolean;
|
|
82
|
+
isActive: boolean;
|
|
83
|
+
method: "flat" | "weight" | "custom" | "free" | "express" | "pickup";
|
|
84
|
+
estimatedDays?: number | undefined;
|
|
85
|
+
freeAbove?: number | undefined;
|
|
86
|
+
flatRate?: number | undefined;
|
|
87
|
+
pricePerKg?: number | undefined;
|
|
88
|
+
expressSurcharge?: number | undefined;
|
|
89
|
+
}>, {
|
|
90
|
+
label: string;
|
|
91
|
+
isDefault: boolean;
|
|
92
|
+
isActive: boolean;
|
|
93
|
+
method: "flat" | "weight" | "custom" | "free" | "express" | "pickup";
|
|
94
|
+
estimatedDays?: number | undefined;
|
|
95
|
+
freeAbove?: number | undefined;
|
|
96
|
+
flatRate?: number | undefined;
|
|
97
|
+
pricePerKg?: number | undefined;
|
|
98
|
+
expressSurcharge?: number | undefined;
|
|
99
|
+
}, {
|
|
100
|
+
label: string;
|
|
101
|
+
isDefault: boolean;
|
|
102
|
+
isActive: boolean;
|
|
103
|
+
method: "flat" | "weight" | "custom" | "free" | "express" | "pickup";
|
|
104
|
+
estimatedDays?: number | undefined;
|
|
105
|
+
freeAbove?: number | undefined;
|
|
106
|
+
flatRate?: number | undefined;
|
|
107
|
+
pricePerKg?: number | undefined;
|
|
108
|
+
expressSurcharge?: number | undefined;
|
|
109
|
+
}>;
|
|
110
|
+
/**
|
|
111
|
+
* Update contract — partial, and `.strict()` so an unknown key is a 400 rather
|
|
112
|
+
* than a silent write. The per-method completeness rules do not apply: a PATCH
|
|
113
|
+
* may legitimately carry only `isActive`.
|
|
114
|
+
*/
|
|
115
|
+
export declare const shippingConfigUpdateSchema: z.ZodObject<{
|
|
116
|
+
label: z.ZodOptional<z.ZodString>;
|
|
117
|
+
method: z.ZodOptional<z.ZodEnum<["free", "flat", "weight", "express", "pickup", "custom"]>>;
|
|
118
|
+
flatRate: z.ZodOptional<z.ZodNumber>;
|
|
119
|
+
pricePerKg: z.ZodOptional<z.ZodNumber>;
|
|
120
|
+
freeAbove: z.ZodOptional<z.ZodNumber>;
|
|
121
|
+
expressSurcharge: z.ZodOptional<z.ZodNumber>;
|
|
122
|
+
estimatedDays: z.ZodOptional<z.ZodNumber>;
|
|
123
|
+
zones: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
124
|
+
isDefault: z.ZodOptional<z.ZodBoolean>;
|
|
125
|
+
isActive: z.ZodOptional<z.ZodBoolean>;
|
|
126
|
+
}, "strict", z.ZodTypeAny, {
|
|
127
|
+
label?: string | undefined;
|
|
128
|
+
isDefault?: boolean | undefined;
|
|
129
|
+
isActive?: boolean | undefined;
|
|
130
|
+
method?: "flat" | "weight" | "custom" | "free" | "express" | "pickup" | undefined;
|
|
131
|
+
estimatedDays?: number | undefined;
|
|
132
|
+
freeAbove?: number | undefined;
|
|
133
|
+
flatRate?: number | undefined;
|
|
134
|
+
pricePerKg?: number | undefined;
|
|
135
|
+
expressSurcharge?: number | undefined;
|
|
136
|
+
zones?: string[] | undefined;
|
|
137
|
+
}, {
|
|
138
|
+
label?: string | undefined;
|
|
139
|
+
isDefault?: boolean | undefined;
|
|
140
|
+
isActive?: boolean | undefined;
|
|
141
|
+
method?: "flat" | "weight" | "custom" | "free" | "express" | "pickup" | undefined;
|
|
142
|
+
estimatedDays?: number | undefined;
|
|
143
|
+
freeAbove?: number | undefined;
|
|
144
|
+
flatRate?: number | undefined;
|
|
145
|
+
pricePerKg?: number | undefined;
|
|
146
|
+
expressSurcharge?: number | undefined;
|
|
147
|
+
zones?: string[] | undefined;
|
|
148
|
+
}>;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* WHY: `POST /api/store/shipping-configs` spread the raw request body into
|
|
3
|
+
* `.create()` and the PATCH handed it straight to `.update()` — no Zod on
|
|
4
|
+
* either, and neither page validated anything. A shipping rule is money:
|
|
5
|
+
* a `flatRate` of `"abc"`, a negative `freeAbove`, or an invented key are
|
|
6
|
+
* all things that were persisted verbatim.
|
|
7
|
+
* WHAT: One schema for the pages AND the routes.
|
|
8
|
+
*
|
|
9
|
+
* ## The numeric fields are `.coerce` on purpose
|
|
10
|
+
*
|
|
11
|
+
* The pages hold them as strings from `<input type="number">`. Coercing here
|
|
12
|
+
* rather than at each call site means every consumer gets a real number and
|
|
13
|
+
* no page has to remember a `Number()` cast — the kind of per-callsite
|
|
14
|
+
* conversion that produces a `NaN` in Firestore the first time someone forgets.
|
|
15
|
+
*
|
|
16
|
+
* EXPORTS:
|
|
17
|
+
* shippingConfigFormSchema, shippingConfigCreateSchema,
|
|
18
|
+
* shippingConfigUpdateSchema, type ShippingConfigFormValues
|
|
19
|
+
*
|
|
20
|
+
* @tag domain:store-extensions
|
|
21
|
+
* @tag layer:schema
|
|
22
|
+
* @tag pattern:none
|
|
23
|
+
* @tag access:isomorphic
|
|
24
|
+
* @tag consumers:store/shipping-configs pages,/api/store/shipping-configs
|
|
25
|
+
* @tag sideEffects:none
|
|
26
|
+
*/
|
|
27
|
+
import { z } from "zod";
|
|
28
|
+
import { annotate } from "../../shell/field-ui-meta";
|
|
29
|
+
/** Mirrors `ShippingMethod` in ./firestore. */
|
|
30
|
+
const shippingMethodSchema = z.enum(["free", "flat", "weight", "express", "pickup", "custom"]);
|
|
31
|
+
/** A non-negative money/threshold amount, coerced from the form's string. */
|
|
32
|
+
const amount = (label) => z.coerce.number({ invalid_type_error: `${label} must be a number.` }).min(0, `${label} cannot be negative.`);
|
|
33
|
+
/**
|
|
34
|
+
* 🛑 `annotate()` must be the OUTERMOST call on each field — it keys a WeakMap
|
|
35
|
+
* by schema instance and every zod wrapper returns a new one.
|
|
36
|
+
*/
|
|
37
|
+
export const shippingConfigFormSchema = z
|
|
38
|
+
.object({
|
|
39
|
+
label: annotate(z.string().min(1, "Give this rule a name.").max(80), {
|
|
40
|
+
section: "basics", sectionLabel: "Shipping Rule", sectionRequired: true,
|
|
41
|
+
quick: true, order: 1, row: "pair",
|
|
42
|
+
}),
|
|
43
|
+
method: annotate(shippingMethodSchema, {
|
|
44
|
+
section: "basics", quick: true, order: 2, row: "pair",
|
|
45
|
+
}),
|
|
46
|
+
flatRate: annotate(amount("Flat rate").optional(), {
|
|
47
|
+
section: "rates", sectionLabel: "Rates", order: 1, row: "pair",
|
|
48
|
+
}),
|
|
49
|
+
pricePerKg: annotate(amount("Price per kg").optional(), {
|
|
50
|
+
section: "rates", order: 2, row: "pair",
|
|
51
|
+
}),
|
|
52
|
+
freeAbove: annotate(amount("Free above").optional(), {
|
|
53
|
+
section: "rates", order: 3, row: "pair",
|
|
54
|
+
}),
|
|
55
|
+
expressSurcharge: annotate(amount("Express surcharge").optional(), {
|
|
56
|
+
section: "rates", order: 4, row: "pair",
|
|
57
|
+
}),
|
|
58
|
+
estimatedDays: annotate(z.coerce.number().int("Estimated days must be a whole number.").min(0).max(365).optional(), { section: "rates", order: 5, row: "pair" }),
|
|
59
|
+
isDefault: annotate(z.boolean(), {
|
|
60
|
+
section: "visibility", sectionLabel: "Visibility", order: 1, row: "quarter",
|
|
61
|
+
}),
|
|
62
|
+
isActive: annotate(z.boolean(), {
|
|
63
|
+
section: "visibility", order: 2, row: "quarter",
|
|
64
|
+
}),
|
|
65
|
+
})
|
|
66
|
+
.superRefine((v, ctx) => {
|
|
67
|
+
// A rate rule with no rate is not a rule. Checked per method, because the
|
|
68
|
+
// required field differs and a blanket `.min(1)` would make every rate
|
|
69
|
+
// mandatory for a free/pickup rule that has none.
|
|
70
|
+
const need = (path, label) => {
|
|
71
|
+
if (v[path] == null) {
|
|
72
|
+
ctx.addIssue({
|
|
73
|
+
code: z.ZodIssueCode.custom,
|
|
74
|
+
path: [path],
|
|
75
|
+
message: `A "${label}" rule needs a ${path === "flatRate" ? "flat rate" : "price per kg"}.`,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
if (v.method === "flat")
|
|
80
|
+
need("flatRate", "flat");
|
|
81
|
+
if (v.method === "weight")
|
|
82
|
+
need("pricePerKg", "weight");
|
|
83
|
+
});
|
|
84
|
+
/** `storeId` is absent — the route sets it from the session, never the body. */
|
|
85
|
+
export const shippingConfigCreateSchema = shippingConfigFormSchema;
|
|
86
|
+
/**
|
|
87
|
+
* Update contract — partial, and `.strict()` so an unknown key is a 400 rather
|
|
88
|
+
* than a silent write. The per-method completeness rules do not apply: a PATCH
|
|
89
|
+
* may legitimately carry only `isActive`.
|
|
90
|
+
*/
|
|
91
|
+
export const shippingConfigUpdateSchema = z
|
|
92
|
+
.object({
|
|
93
|
+
label: z.string().min(1).max(80).optional(),
|
|
94
|
+
method: shippingMethodSchema.optional(),
|
|
95
|
+
flatRate: amount("Flat rate").optional(),
|
|
96
|
+
pricePerKg: amount("Price per kg").optional(),
|
|
97
|
+
freeAbove: amount("Free above").optional(),
|
|
98
|
+
expressSurcharge: amount("Express surcharge").optional(),
|
|
99
|
+
estimatedDays: z.coerce.number().int().min(0).max(365).optional(),
|
|
100
|
+
zones: z.array(z.string()).optional(),
|
|
101
|
+
isDefault: z.boolean().optional(),
|
|
102
|
+
isActive: z.boolean().optional(),
|
|
103
|
+
})
|
|
104
|
+
.strict();
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* 🛑 `annotate()` must be the OUTERMOST call on each field — it keys a WeakMap
|
|
4
|
+
* by schema instance and every zod wrapper returns a new one, so annotating
|
|
5
|
+
* before `.optional()` silently loses the metadata.
|
|
6
|
+
*/
|
|
7
|
+
export declare const storeCategoryFormSchema: z.ZodObject<{
|
|
8
|
+
label: z.ZodString;
|
|
9
|
+
slug: z.ZodOptional<z.ZodString>;
|
|
10
|
+
description: z.ZodOptional<z.ZodString>;
|
|
11
|
+
coverImageUrl: z.ZodOptional<z.ZodString>;
|
|
12
|
+
displayOrder: z.ZodNumber;
|
|
13
|
+
}, "strip", z.ZodTypeAny, {
|
|
14
|
+
label: string;
|
|
15
|
+
displayOrder: number;
|
|
16
|
+
description?: string | undefined;
|
|
17
|
+
slug?: string | undefined;
|
|
18
|
+
coverImageUrl?: string | undefined;
|
|
19
|
+
}, {
|
|
20
|
+
label: string;
|
|
21
|
+
displayOrder: number;
|
|
22
|
+
description?: string | undefined;
|
|
23
|
+
slug?: string | undefined;
|
|
24
|
+
coverImageUrl?: string | undefined;
|
|
25
|
+
}>;
|
|
26
|
+
export type StoreCategoryFormValues = z.infer<typeof storeCategoryFormSchema>;
|
|
27
|
+
/**
|
|
28
|
+
* The server's create contract.
|
|
29
|
+
*
|
|
30
|
+
* Identical fields, but `slug` is REQUIRED here: the client derives it from
|
|
31
|
+
* the label when blank, and a category with no slug has no URL. Keeping the
|
|
32
|
+
* derivation on the client and the requirement on the server means the route
|
|
33
|
+
* never has to guess what the user meant.
|
|
34
|
+
*/
|
|
35
|
+
export declare const storeCategoryCreateSchema: z.ZodObject<{
|
|
36
|
+
label: z.ZodString;
|
|
37
|
+
description: z.ZodOptional<z.ZodString>;
|
|
38
|
+
coverImageUrl: z.ZodOptional<z.ZodString>;
|
|
39
|
+
displayOrder: z.ZodNumber;
|
|
40
|
+
} & {
|
|
41
|
+
slug: z.ZodString;
|
|
42
|
+
}, "strip", z.ZodTypeAny, {
|
|
43
|
+
label: string;
|
|
44
|
+
slug: string;
|
|
45
|
+
displayOrder: number;
|
|
46
|
+
description?: string | undefined;
|
|
47
|
+
coverImageUrl?: string | undefined;
|
|
48
|
+
}, {
|
|
49
|
+
label: string;
|
|
50
|
+
slug: string;
|
|
51
|
+
displayOrder: number;
|
|
52
|
+
description?: string | undefined;
|
|
53
|
+
coverImageUrl?: string | undefined;
|
|
54
|
+
}>;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* WHY: `/store/categories/new` collected five fields through raw <Input>s with
|
|
3
|
+
* no <Form>, no schema and no check of any kind, and `POST
|
|
4
|
+
* /api/store/categories` spread the raw request body straight into
|
|
5
|
+
* `storeCategoriesRepository.create()`. Neither side validated anything,
|
|
6
|
+
* so an entirely empty storefront category could be created — and any
|
|
7
|
+
* extra key a caller sent was written verbatim into Firestore.
|
|
8
|
+
* WHAT: One schema for both sides. The client parses it to show inline errors
|
|
9
|
+
* before submitting; the route parses it to decide what is allowed to
|
|
10
|
+
* reach the database.
|
|
11
|
+
*
|
|
12
|
+
* ## Why one schema and not two
|
|
13
|
+
*
|
|
14
|
+
* A client copy and a server copy of the same rules is how they come to
|
|
15
|
+
* disagree — and when they do, the client's is the one users see while the
|
|
16
|
+
* server's is the one that decides. The route strips unknown keys BECAUSE it
|
|
17
|
+
* parses this: `storeId` is set from the session, never from the body, so a
|
|
18
|
+
* caller cannot write a category into someone else's store by adding a field.
|
|
19
|
+
*
|
|
20
|
+
* `productIds` and `isActive` are deliberately absent: they are managed after
|
|
21
|
+
* creation (assigning products, toggling visibility), not typed into the
|
|
22
|
+
* create form, and the repository supplies their defaults.
|
|
23
|
+
*
|
|
24
|
+
* EXPORTS:
|
|
25
|
+
* storeCategoryFormSchema, storeCategoryCreateSchema,
|
|
26
|
+
* type StoreCategoryFormValues
|
|
27
|
+
*
|
|
28
|
+
* @tag domain:store-extensions
|
|
29
|
+
* @tag layer:schema
|
|
30
|
+
* @tag pattern:none
|
|
31
|
+
* @tag access:isomorphic
|
|
32
|
+
* @tag consumers:store/categories/new,POST /api/store/categories
|
|
33
|
+
* @tag sideEffects:none
|
|
34
|
+
*/
|
|
35
|
+
import { z } from "zod";
|
|
36
|
+
import { annotate } from "../../shell/field-ui-meta";
|
|
37
|
+
/**
|
|
38
|
+
* 🛑 `annotate()` must be the OUTERMOST call on each field — it keys a WeakMap
|
|
39
|
+
* by schema instance and every zod wrapper returns a new one, so annotating
|
|
40
|
+
* before `.optional()` silently loses the metadata.
|
|
41
|
+
*/
|
|
42
|
+
export const storeCategoryFormSchema = z.object({
|
|
43
|
+
label: annotate(z.string().min(1, "Label is required").max(80, "Keep the label under 80 characters."), {
|
|
44
|
+
section: "basics", sectionLabel: "Category", sectionRequired: true,
|
|
45
|
+
quick: true, order: 1, row: "full",
|
|
46
|
+
}),
|
|
47
|
+
slug: annotate(z.string().max(80, "Keep the slug under 80 characters.").optional(), {
|
|
48
|
+
section: "basics", order: 2, row: "full",
|
|
49
|
+
// T1 calculated: derived from the label when left blank, then frozen —
|
|
50
|
+
// the same create-only rule every other slug in this codebase follows,
|
|
51
|
+
// because renaming one breaks inbound links.
|
|
52
|
+
tier: "t1-derive",
|
|
53
|
+
help: "Left blank, this is generated from the label.",
|
|
54
|
+
}),
|
|
55
|
+
description: annotate(z.string().max(500, "Keep the description under 500 characters.").optional(), {
|
|
56
|
+
section: "basics", order: 3, row: "full",
|
|
57
|
+
}),
|
|
58
|
+
coverImageUrl: annotate(z.string().optional(), {
|
|
59
|
+
section: "media", sectionLabel: "Media", order: 1, row: "full", kind: "media",
|
|
60
|
+
}),
|
|
61
|
+
displayOrder: annotate(z.coerce.number().int("Display order must be a whole number.").min(0, "Display order cannot be negative."), { section: "visibility", sectionLabel: "Visibility", order: 1, row: "pair" }),
|
|
62
|
+
});
|
|
63
|
+
/**
|
|
64
|
+
* The server's create contract.
|
|
65
|
+
*
|
|
66
|
+
* Identical fields, but `slug` is REQUIRED here: the client derives it from
|
|
67
|
+
* the label when blank, and a category with no slug has no URL. Keeping the
|
|
68
|
+
* derivation on the client and the requirement on the server means the route
|
|
69
|
+
* never has to guess what the user meant.
|
|
70
|
+
*/
|
|
71
|
+
export const storeCategoryCreateSchema = storeCategoryFormSchema.extend({
|
|
72
|
+
slug: z.string().min(1, "Slug is required").max(80),
|
|
73
|
+
});
|