@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,87 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* WHY: `/store/listing-templates/[id]/edit` was a wholly broken page. The route
|
|
3
|
+
* it calls — `/api/store/listing-templates/[id]` — did not exist, while
|
|
4
|
+
* `src/constants/api.ts` defined the endpoint, the edit page called it
|
|
5
|
+
* three times (GET on load, PATCH on save, DELETE), and the list page
|
|
6
|
+
* rendered an Edit button pointing straight at it. A seller clicking Edit
|
|
7
|
+
* got a page that 404'd on load, on save and on delete.
|
|
8
|
+
* Root Cause #37's dead-route shape, with a real page and a real button
|
|
9
|
+
* in front of it.
|
|
10
|
+
* WHAT: One schema for the listing-template form AND the route that receives
|
|
11
|
+
* it, so the two cannot drift.
|
|
12
|
+
*
|
|
13
|
+
* ## `defaults` stays open on purpose
|
|
14
|
+
*
|
|
15
|
+
* It is a JSON blob of per-listing-type field defaults — the whole point is
|
|
16
|
+
* that it holds whatever fields the target listing type has. `z.record()`
|
|
17
|
+
* rather than a named shape: modelling it would mean restating every listing
|
|
18
|
+
* type's field set here, and getting that wrong strips a seller's saved
|
|
19
|
+
* defaults on the next save (`z.object()` drops unknown keys). The page
|
|
20
|
+
* already `JSON.parse`s it and reports malformed JSON.
|
|
21
|
+
*
|
|
22
|
+
* EXPORTS:
|
|
23
|
+
* listingTemplateFormSchema, listingTemplateCreateSchema,
|
|
24
|
+
* listingTemplateUpdateSchema, type ListingTemplateFormValues
|
|
25
|
+
*
|
|
26
|
+
* @tag domain:store-extensions
|
|
27
|
+
* @tag layer:schema
|
|
28
|
+
* @tag pattern:none
|
|
29
|
+
* @tag access:isomorphic
|
|
30
|
+
* @tag consumers:store/listing-templates pages,/api/store/listing-templates
|
|
31
|
+
* @tag sideEffects:none
|
|
32
|
+
*/
|
|
33
|
+
import { z } from "zod";
|
|
34
|
+
import { annotate } from "../../shell/field-ui-meta";
|
|
35
|
+
import { LISTING_TEMPLATE_TYPES } from "./firestore";
|
|
36
|
+
import { firestoreValueSchema } from "../../../schemas/firestore-value";
|
|
37
|
+
/**
|
|
38
|
+
* Derived from the runtime array, never restated. A hand-written copy here
|
|
39
|
+
* would be the eleventh independent enumeration of this union — and the tenth
|
|
40
|
+
* had already drifted, which is how `art`/`stickers` came to be unsaveable as
|
|
41
|
+
* templates while `bundle` lingered.
|
|
42
|
+
*/
|
|
43
|
+
const listingTemplateTypeSchema = z.enum(LISTING_TEMPLATE_TYPES);
|
|
44
|
+
/**
|
|
45
|
+
* 🛑 `annotate()` must be the OUTERMOST call on each field — it keys a WeakMap
|
|
46
|
+
* by schema instance and every zod wrapper returns a new one.
|
|
47
|
+
*/
|
|
48
|
+
export const listingTemplateFormSchema = z.object({
|
|
49
|
+
name: annotate(z.string().min(1, "Name is required").max(120, "Keep the name under 120 characters."), {
|
|
50
|
+
section: "basics", sectionLabel: "Template", sectionRequired: true,
|
|
51
|
+
quick: true, order: 1, row: "full",
|
|
52
|
+
}),
|
|
53
|
+
description: annotate(z.string().max(500, "Keep the description under 500 characters.").optional(), {
|
|
54
|
+
section: "basics", order: 2, row: "full",
|
|
55
|
+
}),
|
|
56
|
+
listingType: annotate(listingTemplateTypeSchema, {
|
|
57
|
+
section: "basics", quick: true, order: 3, row: "pair",
|
|
58
|
+
}),
|
|
59
|
+
// Typed as a Firestore value tree, not `unknown`: `unknown` does not satisfy
|
|
60
|
+
// `ListingTemplateDocument.defaults` (a `FirestoreDocument`), and casting at
|
|
61
|
+
// the repository call to paper over that would defeat the only type-level
|
|
62
|
+
// signal that the shapes agree.
|
|
63
|
+
defaults: annotate(z.record(firestoreValueSchema), {
|
|
64
|
+
section: "defaults", sectionLabel: "Default field values",
|
|
65
|
+
order: 1, row: "full", kind: "list",
|
|
66
|
+
help: "JSON applied to a new listing when this template is chosen.",
|
|
67
|
+
}),
|
|
68
|
+
isShared: annotate(z.boolean(), {
|
|
69
|
+
section: "visibility", sectionLabel: "Visibility", order: 1, row: "quarter",
|
|
70
|
+
}),
|
|
71
|
+
isActive: annotate(z.boolean(), {
|
|
72
|
+
section: "visibility", order: 2, row: "quarter",
|
|
73
|
+
}),
|
|
74
|
+
});
|
|
75
|
+
/**
|
|
76
|
+
* Create contract. `storeId`/`ownerId` are deliberately absent: the route sets
|
|
77
|
+
* both from the session, so a caller cannot create a template inside someone
|
|
78
|
+
* else's store by adding a field to the body.
|
|
79
|
+
*/
|
|
80
|
+
export const listingTemplateCreateSchema = listingTemplateFormSchema.partial({
|
|
81
|
+
description: true,
|
|
82
|
+
isShared: true,
|
|
83
|
+
isActive: true,
|
|
84
|
+
defaults: true,
|
|
85
|
+
});
|
|
86
|
+
/** Update contract — every field optional, and the same ownership fields absent. */
|
|
87
|
+
export const listingTemplateUpdateSchema = listingTemplateFormSchema.partial();
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* The statuses a human review can produce.
|
|
4
|
+
*
|
|
5
|
+
* `satisfies readonly ModerationStatus[]` is load-bearing: it is what stops
|
|
6
|
+
* this becoming an independent, drifting copy of the union.
|
|
7
|
+
*/
|
|
8
|
+
export declare const MODERATION_REVIEWER_STATUSES: readonly ["approved", "rejected"];
|
|
9
|
+
/**
|
|
10
|
+
* 🛑 `annotate()` must be the OUTERMOST call on each field — it keys a WeakMap
|
|
11
|
+
* by schema instance and every zod wrapper returns a new one.
|
|
12
|
+
*/
|
|
13
|
+
export declare const moderationReviewFormSchema: z.ZodEffects<z.ZodObject<{
|
|
14
|
+
status: z.ZodEnum<["approved", "rejected"]>;
|
|
15
|
+
reason: z.ZodOptional<z.ZodString>;
|
|
16
|
+
}, "strict", z.ZodTypeAny, {
|
|
17
|
+
status: "approved" | "rejected";
|
|
18
|
+
reason?: string | undefined;
|
|
19
|
+
}, {
|
|
20
|
+
status: "approved" | "rejected";
|
|
21
|
+
reason?: string | undefined;
|
|
22
|
+
}>, {
|
|
23
|
+
status: "approved" | "rejected";
|
|
24
|
+
reason?: string | undefined;
|
|
25
|
+
}, {
|
|
26
|
+
status: "approved" | "rejected";
|
|
27
|
+
reason?: string | undefined;
|
|
28
|
+
}>;
|
|
29
|
+
export type ModerationReviewFormValues = z.infer<typeof moderationReviewFormSchema>;
|
|
30
|
+
/**
|
|
31
|
+
* Route contract. Identical to the form contract — a review is a single
|
|
32
|
+
* atomic decision, so there is no partial variant.
|
|
33
|
+
*/
|
|
34
|
+
export declare const moderationReviewUpdateSchema: z.ZodEffects<z.ZodObject<{
|
|
35
|
+
status: z.ZodEnum<["approved", "rejected"]>;
|
|
36
|
+
reason: z.ZodOptional<z.ZodString>;
|
|
37
|
+
}, "strict", z.ZodTypeAny, {
|
|
38
|
+
status: "approved" | "rejected";
|
|
39
|
+
reason?: string | undefined;
|
|
40
|
+
}, {
|
|
41
|
+
status: "approved" | "rejected";
|
|
42
|
+
reason?: string | undefined;
|
|
43
|
+
}>, {
|
|
44
|
+
status: "approved" | "rejected";
|
|
45
|
+
reason?: string | undefined;
|
|
46
|
+
}, {
|
|
47
|
+
status: "approved" | "rejected";
|
|
48
|
+
reason?: string | undefined;
|
|
49
|
+
}>;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* WHY: `PATCH /api/admin/moderation/[id]` spread the raw request body straight
|
|
3
|
+
* into Firestore:
|
|
4
|
+
*
|
|
5
|
+
* const body = await parseJsonBody<Record<string, JsonValue>>(request);
|
|
6
|
+
* await moderationQueueRepository.update(id, { ...body, reviewerId, reviewedAt });
|
|
7
|
+
*
|
|
8
|
+
* There was no Zod anywhere on this path — not on the route, not on
|
|
9
|
+
* either of the two client surfaces that call it. Under `ROLES_ADMIN_MOD`
|
|
10
|
+
* a caller could write ANY key onto a moderation record, including
|
|
11
|
+
* `entityId`, `ownerId`, `submittedAt` or an invented one, and could set
|
|
12
|
+
* `status` to any string at all — the real `ModerationStatus` union was
|
|
13
|
+
* never consulted.
|
|
14
|
+
*
|
|
15
|
+
* Two client surfaces call it and neither validated: the queue page's
|
|
16
|
+
* inline Approve/Reject controls and `ModerationDetailActions`. Both let
|
|
17
|
+
* a rejection be submitted with an EMPTY reason, so content could be
|
|
18
|
+
* blocked with no record of why.
|
|
19
|
+
* WHAT: One schema shared by the route and both client surfaces.
|
|
20
|
+
*
|
|
21
|
+
* ## A reviewer may only set the two statuses a REVIEW produces
|
|
22
|
+
*
|
|
23
|
+
* `ModerationStatus` has four values, but `pending` is the initial state and
|
|
24
|
+
* `auto-approved` is what the system writes when nothing needed a human. A
|
|
25
|
+
* reviewer setting either by hand would be falsifying the record — so the
|
|
26
|
+
* settable set is the subset below, declared `satisfies readonly
|
|
27
|
+
* ModerationStatus[]` rather than restated, which makes a value outside the
|
|
28
|
+
* real union a COMPILE error instead of a filter that matches nothing
|
|
29
|
+
* (Recurrent Root Cause #33).
|
|
30
|
+
*
|
|
31
|
+
* ## `reviewerId` / `reviewedAt` are absent on purpose
|
|
32
|
+
*
|
|
33
|
+
* The route sets both from the session and the server clock. Accepting them
|
|
34
|
+
* here would let a caller attribute a review to somebody else, or backdate it.
|
|
35
|
+
*
|
|
36
|
+
* EXPORTS:
|
|
37
|
+
* moderationReviewFormSchema, moderationReviewUpdateSchema,
|
|
38
|
+
* MODERATION_REVIEWER_STATUSES, type ModerationReviewFormValues
|
|
39
|
+
*
|
|
40
|
+
* @tag domain:store-extensions,moderation
|
|
41
|
+
* @tag layer:schema
|
|
42
|
+
* @tag pattern:none
|
|
43
|
+
* @tag access:isomorphic
|
|
44
|
+
* @tag consumers:admin/moderation pages,/api/admin/moderation/[id]
|
|
45
|
+
* @tag sideEffects:none
|
|
46
|
+
*/
|
|
47
|
+
import { z } from "zod";
|
|
48
|
+
import { annotate } from "../../shell/field-ui-meta";
|
|
49
|
+
/**
|
|
50
|
+
* The statuses a human review can produce.
|
|
51
|
+
*
|
|
52
|
+
* `satisfies readonly ModerationStatus[]` is load-bearing: it is what stops
|
|
53
|
+
* this becoming an independent, drifting copy of the union.
|
|
54
|
+
*/
|
|
55
|
+
export const MODERATION_REVIEWER_STATUSES = [
|
|
56
|
+
"approved",
|
|
57
|
+
"rejected",
|
|
58
|
+
];
|
|
59
|
+
/**
|
|
60
|
+
* 🛑 `annotate()` must be the OUTERMOST call on each field — it keys a WeakMap
|
|
61
|
+
* by schema instance and every zod wrapper returns a new one.
|
|
62
|
+
*/
|
|
63
|
+
export const moderationReviewFormSchema = z
|
|
64
|
+
.object({
|
|
65
|
+
status: annotate(z.enum(MODERATION_REVIEWER_STATUSES), {
|
|
66
|
+
section: "review",
|
|
67
|
+
sectionLabel: "Review decision",
|
|
68
|
+
sectionRequired: true,
|
|
69
|
+
quick: true,
|
|
70
|
+
order: 1,
|
|
71
|
+
row: "pair",
|
|
72
|
+
}),
|
|
73
|
+
reason: annotate(z.string().max(500, "Keep the reason under 500 characters.").optional(), {
|
|
74
|
+
section: "review",
|
|
75
|
+
order: 2,
|
|
76
|
+
row: "full",
|
|
77
|
+
kind: "textarea",
|
|
78
|
+
help: "Required when rejecting — this is the record of why the media was blocked.",
|
|
79
|
+
}),
|
|
80
|
+
})
|
|
81
|
+
// Unknown keys are a 400, not a silent drop. A plain `z.object()` would
|
|
82
|
+
// STRIP them, which is the quieter half of the same bug: the caller is told
|
|
83
|
+
// the write succeeded and the key it sent simply vanished.
|
|
84
|
+
.strict()
|
|
85
|
+
.superRefine((v, ctx) => {
|
|
86
|
+
// An approval needs no justification; a rejection does. Blocking someone's
|
|
87
|
+
// media with an empty `reason` leaves the owner, and the next reviewer,
|
|
88
|
+
// with no way to know what was wrong with it.
|
|
89
|
+
if (v.status === "rejected" && !v.reason?.trim()) {
|
|
90
|
+
ctx.addIssue({
|
|
91
|
+
code: z.ZodIssueCode.custom,
|
|
92
|
+
path: ["reason"],
|
|
93
|
+
message: "Give a reason for the rejection.",
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
/**
|
|
98
|
+
* Route contract. Identical to the form contract — a review is a single
|
|
99
|
+
* atomic decision, so there is no partial variant.
|
|
100
|
+
*/
|
|
101
|
+
export const moderationReviewUpdateSchema = moderationReviewFormSchema;
|
|
@@ -0,0 +1,153 @@
|
|
|
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 payoutMethodFormSchema: z.ZodEffects<z.ZodObject<{
|
|
7
|
+
type: z.ZodEnum<["upi", "bank", "card", "other"]>;
|
|
8
|
+
label: z.ZodString;
|
|
9
|
+
upiVpa: z.ZodOptional<z.ZodString>;
|
|
10
|
+
accountHolderName: z.ZodOptional<z.ZodString>;
|
|
11
|
+
accountNumber: z.ZodOptional<z.ZodString>;
|
|
12
|
+
ifscCode: z.ZodOptional<z.ZodString>;
|
|
13
|
+
bankName: z.ZodOptional<z.ZodString>;
|
|
14
|
+
isDefault: z.ZodBoolean;
|
|
15
|
+
isActive: z.ZodBoolean;
|
|
16
|
+
}, "strip", z.ZodTypeAny, {
|
|
17
|
+
type: "card" | "other" | "upi" | "bank";
|
|
18
|
+
label: string;
|
|
19
|
+
isDefault: boolean;
|
|
20
|
+
isActive: boolean;
|
|
21
|
+
accountHolderName?: string | undefined;
|
|
22
|
+
ifscCode?: string | undefined;
|
|
23
|
+
bankName?: string | undefined;
|
|
24
|
+
accountNumber?: string | undefined;
|
|
25
|
+
upiVpa?: string | undefined;
|
|
26
|
+
}, {
|
|
27
|
+
type: "card" | "other" | "upi" | "bank";
|
|
28
|
+
label: string;
|
|
29
|
+
isDefault: boolean;
|
|
30
|
+
isActive: boolean;
|
|
31
|
+
accountHolderName?: string | undefined;
|
|
32
|
+
ifscCode?: string | undefined;
|
|
33
|
+
bankName?: string | undefined;
|
|
34
|
+
accountNumber?: string | undefined;
|
|
35
|
+
upiVpa?: string | undefined;
|
|
36
|
+
}>, {
|
|
37
|
+
type: "card" | "other" | "upi" | "bank";
|
|
38
|
+
label: string;
|
|
39
|
+
isDefault: boolean;
|
|
40
|
+
isActive: boolean;
|
|
41
|
+
accountHolderName?: string | undefined;
|
|
42
|
+
ifscCode?: string | undefined;
|
|
43
|
+
bankName?: string | undefined;
|
|
44
|
+
accountNumber?: string | undefined;
|
|
45
|
+
upiVpa?: string | undefined;
|
|
46
|
+
}, {
|
|
47
|
+
type: "card" | "other" | "upi" | "bank";
|
|
48
|
+
label: string;
|
|
49
|
+
isDefault: boolean;
|
|
50
|
+
isActive: boolean;
|
|
51
|
+
accountHolderName?: string | undefined;
|
|
52
|
+
ifscCode?: string | undefined;
|
|
53
|
+
bankName?: string | undefined;
|
|
54
|
+
accountNumber?: string | undefined;
|
|
55
|
+
upiVpa?: string | undefined;
|
|
56
|
+
}>;
|
|
57
|
+
export type PayoutMethodFormValues = z.infer<typeof payoutMethodFormSchema>;
|
|
58
|
+
/**
|
|
59
|
+
* Create contract. `sellerId`/`storeId` are absent on purpose — the route sets
|
|
60
|
+
* both from the session, so a caller cannot file a payout method against
|
|
61
|
+
* another seller by adding fields to the body.
|
|
62
|
+
*/
|
|
63
|
+
export declare const payoutMethodCreateSchema: z.ZodEffects<z.ZodObject<{
|
|
64
|
+
type: z.ZodEnum<["upi", "bank", "card", "other"]>;
|
|
65
|
+
label: z.ZodString;
|
|
66
|
+
upiVpa: z.ZodOptional<z.ZodString>;
|
|
67
|
+
accountHolderName: z.ZodOptional<z.ZodString>;
|
|
68
|
+
accountNumber: z.ZodOptional<z.ZodString>;
|
|
69
|
+
ifscCode: z.ZodOptional<z.ZodString>;
|
|
70
|
+
bankName: z.ZodOptional<z.ZodString>;
|
|
71
|
+
isDefault: z.ZodBoolean;
|
|
72
|
+
isActive: z.ZodBoolean;
|
|
73
|
+
}, "strip", z.ZodTypeAny, {
|
|
74
|
+
type: "card" | "other" | "upi" | "bank";
|
|
75
|
+
label: string;
|
|
76
|
+
isDefault: boolean;
|
|
77
|
+
isActive: boolean;
|
|
78
|
+
accountHolderName?: string | undefined;
|
|
79
|
+
ifscCode?: string | undefined;
|
|
80
|
+
bankName?: string | undefined;
|
|
81
|
+
accountNumber?: string | undefined;
|
|
82
|
+
upiVpa?: string | undefined;
|
|
83
|
+
}, {
|
|
84
|
+
type: "card" | "other" | "upi" | "bank";
|
|
85
|
+
label: string;
|
|
86
|
+
isDefault: boolean;
|
|
87
|
+
isActive: boolean;
|
|
88
|
+
accountHolderName?: string | undefined;
|
|
89
|
+
ifscCode?: string | undefined;
|
|
90
|
+
bankName?: string | undefined;
|
|
91
|
+
accountNumber?: string | undefined;
|
|
92
|
+
upiVpa?: string | undefined;
|
|
93
|
+
}>, {
|
|
94
|
+
type: "card" | "other" | "upi" | "bank";
|
|
95
|
+
label: string;
|
|
96
|
+
isDefault: boolean;
|
|
97
|
+
isActive: boolean;
|
|
98
|
+
accountHolderName?: string | undefined;
|
|
99
|
+
ifscCode?: string | undefined;
|
|
100
|
+
bankName?: string | undefined;
|
|
101
|
+
accountNumber?: string | undefined;
|
|
102
|
+
upiVpa?: string | undefined;
|
|
103
|
+
}, {
|
|
104
|
+
type: "card" | "other" | "upi" | "bank";
|
|
105
|
+
label: string;
|
|
106
|
+
isDefault: boolean;
|
|
107
|
+
isActive: boolean;
|
|
108
|
+
accountHolderName?: string | undefined;
|
|
109
|
+
ifscCode?: string | undefined;
|
|
110
|
+
bankName?: string | undefined;
|
|
111
|
+
accountNumber?: string | undefined;
|
|
112
|
+
upiVpa?: string | undefined;
|
|
113
|
+
}>;
|
|
114
|
+
/**
|
|
115
|
+
* Update contract.
|
|
116
|
+
*
|
|
117
|
+
* The per-type rules deliberately do NOT apply here: a PATCH may carry a
|
|
118
|
+
* single field (`isDefault`, say) and requiring the whole bank block on every
|
|
119
|
+
* partial update would make toggling the default impossible. The bounds still
|
|
120
|
+
* apply to whatever IS sent, and the create path is where completeness is
|
|
121
|
+
* enforced.
|
|
122
|
+
*/
|
|
123
|
+
export declare const payoutMethodUpdateSchema: z.ZodObject<{
|
|
124
|
+
type: z.ZodOptional<z.ZodEnum<["upi", "bank", "card", "other"]>>;
|
|
125
|
+
label: z.ZodOptional<z.ZodString>;
|
|
126
|
+
upiVpa: z.ZodOptional<z.ZodString>;
|
|
127
|
+
accountHolderName: z.ZodOptional<z.ZodString>;
|
|
128
|
+
accountNumber: z.ZodOptional<z.ZodString>;
|
|
129
|
+
ifscCode: z.ZodOptional<z.ZodString>;
|
|
130
|
+
bankName: z.ZodOptional<z.ZodString>;
|
|
131
|
+
isDefault: z.ZodOptional<z.ZodBoolean>;
|
|
132
|
+
isActive: z.ZodOptional<z.ZodBoolean>;
|
|
133
|
+
}, "strict", z.ZodTypeAny, {
|
|
134
|
+
type?: "card" | "other" | "upi" | "bank" | undefined;
|
|
135
|
+
label?: string | undefined;
|
|
136
|
+
isDefault?: boolean | undefined;
|
|
137
|
+
accountHolderName?: string | undefined;
|
|
138
|
+
ifscCode?: string | undefined;
|
|
139
|
+
bankName?: string | undefined;
|
|
140
|
+
isActive?: boolean | undefined;
|
|
141
|
+
accountNumber?: string | undefined;
|
|
142
|
+
upiVpa?: string | undefined;
|
|
143
|
+
}, {
|
|
144
|
+
type?: "card" | "other" | "upi" | "bank" | undefined;
|
|
145
|
+
label?: string | undefined;
|
|
146
|
+
isDefault?: boolean | undefined;
|
|
147
|
+
accountHolderName?: string | undefined;
|
|
148
|
+
ifscCode?: string | undefined;
|
|
149
|
+
bankName?: string | undefined;
|
|
150
|
+
isActive?: boolean | undefined;
|
|
151
|
+
accountNumber?: string | undefined;
|
|
152
|
+
upiVpa?: string | undefined;
|
|
153
|
+
}>;
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* WHY: A seller's payout method is where their money goes, and it was
|
|
3
|
+
* validated at ZERO layers. Measured 2026-08-24:
|
|
4
|
+
* · `store/payout-methods/new` — `accountNumber` and `ifscCode` are
|
|
5
|
+
* bare <Input>s with no `required` and no pattern; `onSave` has no
|
|
6
|
+
* validation at all and posts `form as Record<string, unknown>`.
|
|
7
|
+
* Both default to `""`, so an empty bank payout method saved cleanly.
|
|
8
|
+
* · `.../[id]/edit` — `onSave` has no validation either, and does not
|
|
9
|
+
* even render the account-number/IFSC inputs.
|
|
10
|
+
* · `POST /api/store/payout-methods` — `parseJsonBody` then `...body`
|
|
11
|
+
* spread into `.create()`. No Zod.
|
|
12
|
+
* · `PATCH .../[id]` — the raw body passed straight to `.update()`.
|
|
13
|
+
* No Zod, no field filtering.
|
|
14
|
+
* A payout method with a blank account number is not a validation nicety;
|
|
15
|
+
* it is a payout that cannot be made, discovered at payout time.
|
|
16
|
+
* WHAT: One schema for the pages AND the routes.
|
|
17
|
+
*
|
|
18
|
+
* ## The rules are per TYPE, which is why this needs a superRefine
|
|
19
|
+
*
|
|
20
|
+
* A UPI method needs a VPA and no bank fields; a bank method needs an account
|
|
21
|
+
* number, an IFSC and a holder name and no VPA. Per-field `.min(1)` cannot
|
|
22
|
+
* express that — it would make every field mandatory for every type.
|
|
23
|
+
*
|
|
24
|
+
* The IFSC and UPI patterns are the ones already used by
|
|
25
|
+
* `SellerPayoutSettingsView` (`^[A-Z]{4}0[A-Z0-9]{6}$` and the VPA shape), not
|
|
26
|
+
* a second pair invented here — two regexes for one format is how they come to
|
|
27
|
+
* disagree about what a valid IFSC is.
|
|
28
|
+
*
|
|
29
|
+
* EXPORTS:
|
|
30
|
+
* payoutMethodFormSchema, payoutMethodCreateSchema,
|
|
31
|
+
* payoutMethodUpdateSchema, type PayoutMethodFormValues
|
|
32
|
+
*
|
|
33
|
+
* @tag domain:store-extensions
|
|
34
|
+
* @tag layer:schema
|
|
35
|
+
* @tag pattern:none
|
|
36
|
+
* @tag access:isomorphic
|
|
37
|
+
* @tag consumers:store/payout-methods pages,/api/store/payout-methods
|
|
38
|
+
* @tag sideEffects:none
|
|
39
|
+
*/
|
|
40
|
+
import { z } from "zod";
|
|
41
|
+
import { annotate } from "../../shell/field-ui-meta";
|
|
42
|
+
/** Same pattern `SellerPayoutSettingsView` enforces. */
|
|
43
|
+
const IFSC_RE = /^[A-Z]{4}0[A-Z0-9]{6}$/;
|
|
44
|
+
/** Same pattern `SellerPayoutSettingsView` enforces. */
|
|
45
|
+
const UPI_VPA_RE = /^[\w.\-_]{2,256}@[a-zA-Z]{2,64}$/;
|
|
46
|
+
/**
|
|
47
|
+
* 🛑 `annotate()` must be the OUTERMOST call on each field — it keys a WeakMap
|
|
48
|
+
* by schema instance and every zod wrapper returns a new one.
|
|
49
|
+
*/
|
|
50
|
+
export const payoutMethodFormSchema = z
|
|
51
|
+
.object({
|
|
52
|
+
type: annotate(z.enum(["upi", "bank", "card", "other"]), {
|
|
53
|
+
section: "method", sectionLabel: "Payout Method", sectionRequired: true,
|
|
54
|
+
quick: true, order: 1, row: "pair",
|
|
55
|
+
}),
|
|
56
|
+
label: annotate(z.string().min(1, "Give this method a name you'll recognise.").max(80), {
|
|
57
|
+
section: "method", quick: true, order: 2, row: "pair",
|
|
58
|
+
}),
|
|
59
|
+
upiVpa: annotate(z.string().max(256).optional(), {
|
|
60
|
+
section: "method", order: 3, row: "full",
|
|
61
|
+
help: "For UPI methods, e.g. name@bank.",
|
|
62
|
+
}),
|
|
63
|
+
accountHolderName: annotate(z.string().max(120).optional(), {
|
|
64
|
+
section: "bank", sectionLabel: "Bank Account", order: 1, row: "full",
|
|
65
|
+
}),
|
|
66
|
+
accountNumber: annotate(z.string().max(34).optional(), {
|
|
67
|
+
section: "bank", order: 2, row: "pair",
|
|
68
|
+
}),
|
|
69
|
+
ifscCode: annotate(z.string().max(11).optional(), {
|
|
70
|
+
section: "bank", order: 3, row: "pair",
|
|
71
|
+
}),
|
|
72
|
+
bankName: annotate(z.string().max(120).optional(), {
|
|
73
|
+
section: "bank", order: 4, row: "pair",
|
|
74
|
+
}),
|
|
75
|
+
isDefault: annotate(z.boolean(), {
|
|
76
|
+
section: "visibility", sectionLabel: "Visibility", order: 1, row: "quarter",
|
|
77
|
+
}),
|
|
78
|
+
isActive: annotate(z.boolean(), {
|
|
79
|
+
section: "visibility", order: 2, row: "quarter",
|
|
80
|
+
}),
|
|
81
|
+
})
|
|
82
|
+
.superRefine((v, ctx) => {
|
|
83
|
+
const issue = (path, message) => ctx.addIssue({ code: z.ZodIssueCode.custom, path: [path], message });
|
|
84
|
+
/**
|
|
85
|
+
* "Required, and if present the right shape" — the same two-part check
|
|
86
|
+
* every one of these fields needs. One helper rather than a required
|
|
87
|
+
* branch plus a format branch per field, which is what turned this into
|
|
88
|
+
* an unreadable if/else chain.
|
|
89
|
+
*/
|
|
90
|
+
const require = (path, raw, missing, format) => {
|
|
91
|
+
const value = raw?.trim() ?? "";
|
|
92
|
+
if (!value)
|
|
93
|
+
return issue(path, missing);
|
|
94
|
+
if (!format)
|
|
95
|
+
return;
|
|
96
|
+
const candidate = format.normalise ? format.normalise(value) : value;
|
|
97
|
+
if (!format.re.test(candidate))
|
|
98
|
+
issue(path, format.message);
|
|
99
|
+
};
|
|
100
|
+
// Per-type rules by lookup, not by branching. A new payout type adds an
|
|
101
|
+
// entry here rather than another arm of a chain.
|
|
102
|
+
const RULES = {
|
|
103
|
+
upi: () => require("upiVpa", v.upiVpa, "A UPI ID is required for a UPI payout method.", {
|
|
104
|
+
re: UPI_VPA_RE,
|
|
105
|
+
message: "Please enter a valid UPI ID, e.g. name@bank.",
|
|
106
|
+
}),
|
|
107
|
+
// These four are what make a payout actually possible. Every one of them
|
|
108
|
+
// could previously be left blank and saved.
|
|
109
|
+
bank: () => {
|
|
110
|
+
require("accountHolderName", v.accountHolderName, "The account holder's name is required.");
|
|
111
|
+
require("accountNumber", v.accountNumber, "An account number is required.", {
|
|
112
|
+
re: /^\d{6,18}$/,
|
|
113
|
+
message: "An account number is 6–18 digits.",
|
|
114
|
+
});
|
|
115
|
+
require("ifscCode", v.ifscCode, "An IFSC code is required.", {
|
|
116
|
+
re: IFSC_RE,
|
|
117
|
+
message: "Please enter a valid IFSC code, e.g. HDFC0001234.",
|
|
118
|
+
normalise: (x) => x.toUpperCase(),
|
|
119
|
+
});
|
|
120
|
+
require("bankName", v.bankName, "The bank name is required.");
|
|
121
|
+
},
|
|
122
|
+
// `card` and `other` carry no extra requirements today — the label is
|
|
123
|
+
// enough to identify them, and inventing rules for them would reject
|
|
124
|
+
// methods sellers can legitimately record.
|
|
125
|
+
};
|
|
126
|
+
RULES[v.type]?.();
|
|
127
|
+
});
|
|
128
|
+
/**
|
|
129
|
+
* Create contract. `sellerId`/`storeId` are absent on purpose — the route sets
|
|
130
|
+
* both from the session, so a caller cannot file a payout method against
|
|
131
|
+
* another seller by adding fields to the body.
|
|
132
|
+
*/
|
|
133
|
+
export const payoutMethodCreateSchema = payoutMethodFormSchema;
|
|
134
|
+
/**
|
|
135
|
+
* Update contract.
|
|
136
|
+
*
|
|
137
|
+
* The per-type rules deliberately do NOT apply here: a PATCH may carry a
|
|
138
|
+
* single field (`isDefault`, say) and requiring the whole bank block on every
|
|
139
|
+
* partial update would make toggling the default impossible. The bounds still
|
|
140
|
+
* apply to whatever IS sent, and the create path is where completeness is
|
|
141
|
+
* enforced.
|
|
142
|
+
*/
|
|
143
|
+
export const payoutMethodUpdateSchema = z
|
|
144
|
+
.object({
|
|
145
|
+
type: z.enum(["upi", "bank", "card", "other"]).optional(),
|
|
146
|
+
label: z.string().min(1).max(80).optional(),
|
|
147
|
+
upiVpa: z.string().max(256).regex(UPI_VPA_RE, "Please enter a valid UPI ID.").optional(),
|
|
148
|
+
accountHolderName: z.string().max(120).optional(),
|
|
149
|
+
accountNumber: z.string().regex(/^\d{6,18}$/, "An account number is 6–18 digits.").optional(),
|
|
150
|
+
ifscCode: z.string().regex(IFSC_RE, "Please enter a valid IFSC code.").optional(),
|
|
151
|
+
bankName: z.string().max(120).optional(),
|
|
152
|
+
isDefault: z.boolean().optional(),
|
|
153
|
+
isActive: z.boolean().optional(),
|
|
154
|
+
})
|
|
155
|
+
.strict();
|
|
@@ -0,0 +1,25 @@
|
|
|
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 reportCreateSchema: z.ZodObject<{
|
|
7
|
+
entityType: z.ZodEnum<[import("./firestore").ReportEntityType, ...import("./firestore").ReportEntityType[]]>;
|
|
8
|
+
entityId: z.ZodString;
|
|
9
|
+
reason: z.ZodEnum<[import("./firestore").ReportReason, ...import("./firestore").ReportReason[]]>;
|
|
10
|
+
detail: z.ZodString;
|
|
11
|
+
evidenceUrls: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
12
|
+
}, "strict", z.ZodTypeAny, {
|
|
13
|
+
reason: import("./firestore").ReportReason;
|
|
14
|
+
entityId: string;
|
|
15
|
+
entityType: import("./firestore").ReportEntityType;
|
|
16
|
+
detail: string;
|
|
17
|
+
evidenceUrls?: string[] | undefined;
|
|
18
|
+
}, {
|
|
19
|
+
reason: import("./firestore").ReportReason;
|
|
20
|
+
entityId: string;
|
|
21
|
+
entityType: import("./firestore").ReportEntityType;
|
|
22
|
+
detail: string;
|
|
23
|
+
evidenceUrls?: string[] | undefined;
|
|
24
|
+
}>;
|
|
25
|
+
export type ReportCreateValues = z.infer<typeof reportCreateSchema>;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* WHY: `POST /api/reports` — the surface any signed-in user files an abuse
|
|
3
|
+
* report through — spread the raw request body into Firestore:
|
|
4
|
+
*
|
|
5
|
+
* const body = await parseJsonBody<Record<string, JsonValue>>(request);
|
|
6
|
+
* await reportsRepository.create({ ...body, reporterId, reporterEmail, status: "pending" });
|
|
7
|
+
*
|
|
8
|
+
* Nothing was checked. Two distinct consequences:
|
|
9
|
+
*
|
|
10
|
+
* 1. An EMPTY report could be filed. `entityType`, `entityId`, `reason`
|
|
11
|
+
* and `detail` are what make a report actionable and none was
|
|
12
|
+
* required, so a row could reach the moderation queue naming nothing
|
|
13
|
+
* and describing nothing.
|
|
14
|
+
* 2. A reporter could set fields that belong to the REVIEWER — a body
|
|
15
|
+
* carrying `assignedTo`, `resolution` or `resolvedAt` was written
|
|
16
|
+
* verbatim, so a user could file a report that already claimed to be
|
|
17
|
+
* resolved by a named admin.
|
|
18
|
+
*
|
|
19
|
+
* `status` was pinned server-side, which is the only reason (2) was not
|
|
20
|
+
* worse than it is.
|
|
21
|
+
*
|
|
22
|
+
* WHAT: The create contract. Four required fields, evidence optional, and
|
|
23
|
+
* nothing a reviewer owns.
|
|
24
|
+
*
|
|
25
|
+
* ## `reporterId` / `reporterEmail` / `status` are absent on purpose
|
|
26
|
+
*
|
|
27
|
+
* The route sets all three. Accepting `reporterId` would let one user file a
|
|
28
|
+
* report in another's name, which is a way to weaponise the abuse queue rather
|
|
29
|
+
* than a validation nicety.
|
|
30
|
+
*
|
|
31
|
+
* EXPORTS: reportCreateSchema, type ReportCreateValues
|
|
32
|
+
*
|
|
33
|
+
* @tag domain:store-extensions,reports
|
|
34
|
+
* @tag layer:schema
|
|
35
|
+
* @tag pattern:none
|
|
36
|
+
* @tag access:isomorphic
|
|
37
|
+
* @tag consumers:/api/reports
|
|
38
|
+
* @tag sideEffects:none
|
|
39
|
+
*/
|
|
40
|
+
import { z } from "zod";
|
|
41
|
+
import { annotate } from "../../shell/field-ui-meta";
|
|
42
|
+
import { REPORT_ENTITY_TYPES, REPORT_REASONS } from "./firestore";
|
|
43
|
+
/**
|
|
44
|
+
* 🛑 `annotate()` must be the OUTERMOST call on each field — it keys a WeakMap
|
|
45
|
+
* by schema instance and every zod wrapper returns a new one.
|
|
46
|
+
*/
|
|
47
|
+
export const reportCreateSchema = z
|
|
48
|
+
.object({
|
|
49
|
+
entityType: annotate(z.enum(REPORT_ENTITY_TYPES), {
|
|
50
|
+
section: "target",
|
|
51
|
+
sectionLabel: "What are you reporting?",
|
|
52
|
+
sectionRequired: true,
|
|
53
|
+
quick: true,
|
|
54
|
+
order: 1,
|
|
55
|
+
row: "pair",
|
|
56
|
+
}),
|
|
57
|
+
entityId: annotate(z.string().min(1, "Tell us which item this is about."), {
|
|
58
|
+
section: "target",
|
|
59
|
+
quick: true,
|
|
60
|
+
order: 2,
|
|
61
|
+
row: "pair",
|
|
62
|
+
}),
|
|
63
|
+
reason: annotate(z.enum(REPORT_REASONS), {
|
|
64
|
+
section: "detail",
|
|
65
|
+
sectionLabel: "Why",
|
|
66
|
+
sectionRequired: true,
|
|
67
|
+
quick: true,
|
|
68
|
+
order: 1,
|
|
69
|
+
row: "pair",
|
|
70
|
+
}),
|
|
71
|
+
detail: annotate(z
|
|
72
|
+
.string()
|
|
73
|
+
.min(10, "Please describe the problem in at least 10 characters.")
|
|
74
|
+
.max(5000, "Keep the description under 5000 characters."), { section: "detail", quick: true, order: 2, row: "full", kind: "textarea" }),
|
|
75
|
+
// Capped: an unbounded array on a user-writable document is a way to grow
|
|
76
|
+
// one row past what a list read can afford (Rule #6).
|
|
77
|
+
evidenceUrls: annotate(z.array(z.string().url("Each piece of evidence must be a URL.")).max(10).optional(), { section: "detail", order: 3, row: "full", kind: "list" }),
|
|
78
|
+
})
|
|
79
|
+
.strict();
|