@mohasinac/appkit 4.17.0 → 4.19.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/client/features/lottery/LotteryAdminEditView.js +3 -1
- package/dist/_internal/client/features/lottery/LotteryPullForm.js +3 -1
- package/dist/_internal/shared/features/lottery/config-form.d.ts +58 -0
- package/dist/_internal/shared/features/lottery/config-form.js +71 -0
- package/dist/client.d.ts +8 -0
- package/dist/client.js +8 -0
- package/dist/features/account/schemas/index.d.ts +23 -0
- package/dist/features/account/schemas/index.js +21 -0
- package/dist/features/admin/components/AdminAdEditorView.js +4 -2
- package/dist/features/admin/components/AdminAddressEditorView.js +4 -2
- package/dist/features/admin/components/AdminEmployeeEditorView.js +3 -1
- package/dist/features/admin/components/AdminFeatureFlagsView.js +4 -2
- package/dist/features/admin/components/AdminOrderEditorView.js +4 -2
- package/dist/features/admin/components/AdminSiteSettingsView.js +8 -6
- package/dist/features/admin/components/AdminSiteView.js +4 -2
- package/dist/features/admin/components/AdminStoreEditorView.js +11 -9
- package/dist/features/admin/components/AdminUserEditorView.js +14 -12
- package/dist/features/admin/components/AdminUsersView.js +4 -2
- package/dist/features/admin/schemas/admin-editor-forms.d.ts +119 -0
- package/dist/features/admin/schemas/admin-editor-forms.js +141 -0
- package/dist/features/admin/schemas/admin-ops-forms.d.ts +123 -0
- package/dist/features/admin/schemas/admin-ops-forms.js +147 -0
- package/dist/features/admin/schemas/admin-user-form.d.ts +82 -0
- package/dist/features/admin/schemas/admin-user-form.js +102 -0
- package/dist/features/admin/schemas/site-settings-form.d.ts +309 -0
- package/dist/features/admin/schemas/site-settings-form.js +159 -0
- package/dist/features/auctions/components/PlaceBidFormClient.js +3 -2
- package/dist/features/orders/schemas/buyer-forms.d.ts +46 -0
- package/dist/features/orders/schemas/buyer-forms.js +81 -0
- package/dist/features/scams/schemas/report-form.d.ts +63 -0
- package/dist/features/scams/schemas/report-form.js +88 -0
- package/dist/features/store-extensions/schemas/sublisting-category-form.d.ts +15 -0
- package/dist/features/store-extensions/schemas/sublisting-category-form.js +41 -0
- package/dist/features/whatsapp-bot/components/SellerWhatsAppSettingsView.js +4 -2
- package/dist/index.d.ts +8 -0
- package/dist/index.js +8 -0
- package/dist/schemas/registry.d.ts +682 -0
- package/dist/schemas/registry.js +22 -0
- package/package.json +1 -1
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* WHY: A cluster of admin editors rendered `<Form>` with no schema. Two of
|
|
3
|
+
* them write PII, and one changes a store's standing on the platform.
|
|
4
|
+
*
|
|
5
|
+
* `AdminAddressEditorView` is the PII one: `fullName`, `phone` and the
|
|
6
|
+
* address lines are all encrypted at rest, and none was checked — a
|
|
7
|
+
* postal code could be any string, and `ownerId` (which decides WHOSE
|
|
8
|
+
* address this is) was free text.
|
|
9
|
+
*
|
|
10
|
+
* `AdminStoreEditorView` sets `storeStatus`, `isVerified`, `isFeatured`
|
|
11
|
+
* and `capabilities`. Recurrent Root Cause #38 lived here: saving any
|
|
12
|
+
* field re-sent every other one, so an unrelated edit could silently
|
|
13
|
+
* un-verify a store.
|
|
14
|
+
*
|
|
15
|
+
* WHAT: One schema per editor.
|
|
16
|
+
*
|
|
17
|
+
* EXPORTS:
|
|
18
|
+
* adminAddressFormSchema, adminStoreUpdateSchema, announcementBarSchema,
|
|
19
|
+
* and their inferred value types
|
|
20
|
+
*
|
|
21
|
+
* @tag domain:admin
|
|
22
|
+
* @tag layer:schema
|
|
23
|
+
* @tag pattern:none
|
|
24
|
+
* @tag access:isomorphic
|
|
25
|
+
* @tag consumers:AdminAddressEditorView,AdminStoreEditorView,AdminSiteView
|
|
26
|
+
* @tag sideEffects:none
|
|
27
|
+
*/
|
|
28
|
+
import { z } from "zod";
|
|
29
|
+
import { annotate } from "../../shell/field-ui-meta";
|
|
30
|
+
const PHONE_RE = /^(\+?91[-\s]?)?[6-9]\d{9}$/;
|
|
31
|
+
/** Indian PIN: six digits, never starting at zero. */
|
|
32
|
+
const PIN_RE = /^[1-9]\d{5}$/;
|
|
33
|
+
/**
|
|
34
|
+
* An address written by an admin on someone's behalf.
|
|
35
|
+
*
|
|
36
|
+
* Shapes match `AddressDocument` (`addressLine1`, not `line1`) — deliberately
|
|
37
|
+
* NOT `userAddressSchema`, which is the account-side view of the same data
|
|
38
|
+
* under different field names. Reusing that one here would have meant
|
|
39
|
+
* translating on every write.
|
|
40
|
+
*/
|
|
41
|
+
export const adminAddressFormSchema = z.object({
|
|
42
|
+
ownerType: annotate(z.enum(["user", "store"]), {
|
|
43
|
+
section: "owner", sectionLabel: "Belongs to", sectionRequired: true, quick: true, order: 1, row: "pair",
|
|
44
|
+
}),
|
|
45
|
+
ownerId: annotate(z.string().trim().min(1, "Choose who this address belongs to."), {
|
|
46
|
+
section: "owner", quick: true, order: 2, row: "pair",
|
|
47
|
+
}),
|
|
48
|
+
label: annotate(z.string().trim().max(60, "Keep the label short — e.g. Home, Warehouse.").optional(), {
|
|
49
|
+
section: "address", sectionLabel: "Address", order: 1, row: "pair",
|
|
50
|
+
}),
|
|
51
|
+
fullName: annotate(z.string().trim().min(2, "A recipient name is required.").max(120), {
|
|
52
|
+
section: "address", sectionRequired: true, quick: true, order: 2, row: "pair",
|
|
53
|
+
}),
|
|
54
|
+
phone: annotate(z.string().trim().regex(PHONE_RE, "Enter a 10-digit Indian mobile number."), {
|
|
55
|
+
section: "address", quick: true, order: 3, row: "pair",
|
|
56
|
+
}),
|
|
57
|
+
addressLine1: annotate(z.string().trim().min(3, "Street address is required.").max(200), {
|
|
58
|
+
section: "address", quick: true, order: 4, row: "full",
|
|
59
|
+
}),
|
|
60
|
+
addressLine2: annotate(z.string().trim().max(200).optional(), {
|
|
61
|
+
section: "address", order: 5, row: "full",
|
|
62
|
+
}),
|
|
63
|
+
landmark: annotate(z.string().trim().max(120).optional(), {
|
|
64
|
+
section: "address", order: 6, row: "pair",
|
|
65
|
+
}),
|
|
66
|
+
city: annotate(z.string().trim().min(2, "City is required.").max(80), {
|
|
67
|
+
section: "address", order: 7, row: "pair",
|
|
68
|
+
}),
|
|
69
|
+
state: annotate(z.string().trim().min(2, "State is required.").max(80), {
|
|
70
|
+
section: "address", order: 8, row: "pair",
|
|
71
|
+
}),
|
|
72
|
+
// A wrong PIN is the single most common reason a parcel is undeliverable,
|
|
73
|
+
// and it was previously any string at all.
|
|
74
|
+
postalCode: annotate(z.string().trim().regex(PIN_RE, "Enter a 6-digit PIN code."), {
|
|
75
|
+
section: "address", order: 9, row: "pair",
|
|
76
|
+
}),
|
|
77
|
+
country: annotate(z.string().trim().min(2).max(60), {
|
|
78
|
+
section: "address", order: 10, row: "pair",
|
|
79
|
+
}),
|
|
80
|
+
isDefault: annotate(z.boolean().optional(), {
|
|
81
|
+
section: "address", order: 11, row: "quarter",
|
|
82
|
+
}),
|
|
83
|
+
});
|
|
84
|
+
/**
|
|
85
|
+
* An admin changing a store's standing.
|
|
86
|
+
*
|
|
87
|
+
* `suspensionReason` is required when — and only when — the status is
|
|
88
|
+
* `suspended`: suspending a seller without a recorded reason leaves them, and
|
|
89
|
+
* the next admin, with nothing to appeal or review.
|
|
90
|
+
*/
|
|
91
|
+
export const adminStoreUpdateSchema = z
|
|
92
|
+
.object({
|
|
93
|
+
storeStatus: annotate(z.enum(["pending", "active", "suspended", "closed"]), {
|
|
94
|
+
section: "standing", sectionLabel: "Store standing", sectionRequired: true, quick: true, order: 1, row: "pair",
|
|
95
|
+
}),
|
|
96
|
+
isVerified: annotate(z.boolean().optional(), {
|
|
97
|
+
section: "standing", order: 2, row: "quarter",
|
|
98
|
+
}),
|
|
99
|
+
isFeatured: annotate(z.boolean().optional(), {
|
|
100
|
+
section: "standing", order: 3, row: "quarter",
|
|
101
|
+
}),
|
|
102
|
+
suspensionReason: annotate(z.string().trim().max(500).optional(), {
|
|
103
|
+
section: "standing", order: 4, row: "full", kind: "textarea",
|
|
104
|
+
}),
|
|
105
|
+
capabilities: annotate(z.array(z.string().min(1)).max(50).optional(), {
|
|
106
|
+
section: "capabilities", sectionLabel: "Capabilities", order: 1, row: "full", kind: "list",
|
|
107
|
+
}),
|
|
108
|
+
adminNotes: annotate(z.string().trim().max(2000).optional(), {
|
|
109
|
+
section: "capabilities", order: 2, row: "full", kind: "textarea",
|
|
110
|
+
help: "Internal only — never shown to the store owner or the public.",
|
|
111
|
+
}),
|
|
112
|
+
})
|
|
113
|
+
.superRefine((v, ctx) => {
|
|
114
|
+
if (v.storeStatus === "suspended" && !v.suspensionReason?.trim()) {
|
|
115
|
+
ctx.addIssue({
|
|
116
|
+
code: z.ZodIssueCode.custom,
|
|
117
|
+
path: ["suspensionReason"],
|
|
118
|
+
message: "Record why this store is being suspended.",
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
/**
|
|
123
|
+
* The homepage announcement bar.
|
|
124
|
+
*
|
|
125
|
+
* 🛑 `message` is the canonical key. The main admin editor saved this copy
|
|
126
|
+
* under `text` until 2026-08-24 — a key no renderer has ever read — so
|
|
127
|
+
* admin-authored announcements saved successfully and never appeared. Naming
|
|
128
|
+
* it here is what stops that recurring.
|
|
129
|
+
*/
|
|
130
|
+
export const announcementBarSchema = z.object({
|
|
131
|
+
message: annotate(z.string().trim().max(300, "Keep the announcement under 300 characters.").optional(), {
|
|
132
|
+
section: "announcement", sectionLabel: "Announcement bar", sectionRequired: true,
|
|
133
|
+
quick: true, order: 1, row: "full",
|
|
134
|
+
}),
|
|
135
|
+
enabled: annotate(z.boolean().optional(), {
|
|
136
|
+
section: "announcement", order: 2, row: "quarter",
|
|
137
|
+
}),
|
|
138
|
+
link: annotate(z.string().trim().max(300).optional(), {
|
|
139
|
+
section: "announcement", order: 3, row: "full",
|
|
140
|
+
}),
|
|
141
|
+
});
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const adminOrderUpdateSchema: z.ZodObject<{
|
|
3
|
+
status: z.ZodEnum<[string, ...string[]]>;
|
|
4
|
+
trackingNumber: z.ZodOptional<z.ZodString>;
|
|
5
|
+
carrier: z.ZodOptional<z.ZodString>;
|
|
6
|
+
refundAmount: z.ZodOptional<z.ZodNumber>;
|
|
7
|
+
notes: z.ZodOptional<z.ZodString>;
|
|
8
|
+
}, "strip", z.ZodTypeAny, {
|
|
9
|
+
status: string;
|
|
10
|
+
notes?: string | undefined;
|
|
11
|
+
trackingNumber?: string | undefined;
|
|
12
|
+
refundAmount?: number | undefined;
|
|
13
|
+
carrier?: string | undefined;
|
|
14
|
+
}, {
|
|
15
|
+
status: string;
|
|
16
|
+
notes?: string | undefined;
|
|
17
|
+
trackingNumber?: string | undefined;
|
|
18
|
+
refundAmount?: number | undefined;
|
|
19
|
+
carrier?: string | undefined;
|
|
20
|
+
}>;
|
|
21
|
+
export type AdminOrderUpdateValues = z.infer<typeof adminOrderUpdateSchema>;
|
|
22
|
+
/** Inviting a team member — an email plus the permissions it grants. */
|
|
23
|
+
export declare const employeeInviteSchema: z.ZodObject<{
|
|
24
|
+
email: z.ZodString;
|
|
25
|
+
permissionGroup: z.ZodString;
|
|
26
|
+
permissions: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
27
|
+
}, "strip", z.ZodTypeAny, {
|
|
28
|
+
email: string;
|
|
29
|
+
permissionGroup: string;
|
|
30
|
+
permissions?: string[] | undefined;
|
|
31
|
+
}, {
|
|
32
|
+
email: string;
|
|
33
|
+
permissionGroup: string;
|
|
34
|
+
permissions?: string[] | undefined;
|
|
35
|
+
}>;
|
|
36
|
+
export type EmployeeInviteValues = z.infer<typeof employeeInviteSchema>;
|
|
37
|
+
/**
|
|
38
|
+
* The reason recorded against a hard ban.
|
|
39
|
+
*
|
|
40
|
+
* A hard ban runs an eight-stage destructive cascade; this string is the only
|
|
41
|
+
* durable record of why. A 10-character floor, matching the admin offer-cancel
|
|
42
|
+
* reason, because "spam" is not an audit trail.
|
|
43
|
+
*/
|
|
44
|
+
export declare const hardBanReasonSchema: z.ZodObject<{
|
|
45
|
+
reason: z.ZodString;
|
|
46
|
+
}, "strip", z.ZodTypeAny, {
|
|
47
|
+
reason: string;
|
|
48
|
+
}, {
|
|
49
|
+
reason: string;
|
|
50
|
+
}>;
|
|
51
|
+
export type HardBanReasonValues = z.infer<typeof hardBanReasonSchema>;
|
|
52
|
+
/**
|
|
53
|
+
* Feature flags.
|
|
54
|
+
*
|
|
55
|
+
* Deliberately open records: flags are added and removed constantly, and a
|
|
56
|
+
* closed schema here would silently STRIP a newly-added flag on the next save
|
|
57
|
+
* — the `productBaseSchema` failure applied to configuration. The values are
|
|
58
|
+
* still constrained, which is what stops a flag being set to an object.
|
|
59
|
+
*/
|
|
60
|
+
export declare const featureFlagsUpdateSchema: z.ZodObject<{
|
|
61
|
+
flags: z.ZodRecord<z.ZodString, z.ZodBoolean>;
|
|
62
|
+
rollouts: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodNumber>>;
|
|
63
|
+
}, "strip", z.ZodTypeAny, {
|
|
64
|
+
flags: Record<string, boolean>;
|
|
65
|
+
rollouts?: Record<string, number> | undefined;
|
|
66
|
+
}, {
|
|
67
|
+
flags: Record<string, boolean>;
|
|
68
|
+
rollouts?: Record<string, number> | undefined;
|
|
69
|
+
}>;
|
|
70
|
+
export type FeatureFlagsUpdateValues = z.infer<typeof featureFlagsUpdateSchema>;
|
|
71
|
+
/** An ad placement. `priority` orders competing ads in the same slot. */
|
|
72
|
+
export declare const adminAdFormSchema: z.ZodEffects<z.ZodObject<{
|
|
73
|
+
name: z.ZodString;
|
|
74
|
+
provider: z.ZodEnum<["manual", "adsense", "thirdParty"]>;
|
|
75
|
+
status: z.ZodEnum<["draft", "active", "scheduled", "paused"]>;
|
|
76
|
+
priority: z.ZodNumber;
|
|
77
|
+
placementIds: z.ZodArray<z.ZodString, "many">;
|
|
78
|
+
requiresConsent: z.ZodBoolean;
|
|
79
|
+
startAt: z.ZodOptional<z.ZodString>;
|
|
80
|
+
endAt: z.ZodOptional<z.ZodString>;
|
|
81
|
+
creative: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodType<import("../../..").FirestoreValue, z.ZodTypeDef, import("../../..").FirestoreValue>>>;
|
|
82
|
+
}, "strip", z.ZodTypeAny, {
|
|
83
|
+
status: "draft" | "active" | "scheduled" | "paused";
|
|
84
|
+
name: string;
|
|
85
|
+
priority: number;
|
|
86
|
+
provider: "manual" | "adsense" | "thirdParty";
|
|
87
|
+
placementIds: string[];
|
|
88
|
+
requiresConsent: boolean;
|
|
89
|
+
startAt?: string | undefined;
|
|
90
|
+
endAt?: string | undefined;
|
|
91
|
+
creative?: Record<string, import("../../..").FirestoreValue> | undefined;
|
|
92
|
+
}, {
|
|
93
|
+
status: "draft" | "active" | "scheduled" | "paused";
|
|
94
|
+
name: string;
|
|
95
|
+
priority: number;
|
|
96
|
+
provider: "manual" | "adsense" | "thirdParty";
|
|
97
|
+
placementIds: string[];
|
|
98
|
+
requiresConsent: boolean;
|
|
99
|
+
startAt?: string | undefined;
|
|
100
|
+
endAt?: string | undefined;
|
|
101
|
+
creative?: Record<string, import("../../..").FirestoreValue> | undefined;
|
|
102
|
+
}>, {
|
|
103
|
+
status: "draft" | "active" | "scheduled" | "paused";
|
|
104
|
+
name: string;
|
|
105
|
+
priority: number;
|
|
106
|
+
provider: "manual" | "adsense" | "thirdParty";
|
|
107
|
+
placementIds: string[];
|
|
108
|
+
requiresConsent: boolean;
|
|
109
|
+
startAt?: string | undefined;
|
|
110
|
+
endAt?: string | undefined;
|
|
111
|
+
creative?: Record<string, import("../../..").FirestoreValue> | undefined;
|
|
112
|
+
}, {
|
|
113
|
+
status: "draft" | "active" | "scheduled" | "paused";
|
|
114
|
+
name: string;
|
|
115
|
+
priority: number;
|
|
116
|
+
provider: "manual" | "adsense" | "thirdParty";
|
|
117
|
+
placementIds: string[];
|
|
118
|
+
requiresConsent: boolean;
|
|
119
|
+
startAt?: string | undefined;
|
|
120
|
+
endAt?: string | undefined;
|
|
121
|
+
creative?: Record<string, import("../../..").FirestoreValue> | undefined;
|
|
122
|
+
}>;
|
|
123
|
+
export type AdminAdFormValues = z.infer<typeof adminAdFormSchema>;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* WHY: Four more admin editors rendered `<Form>` with no schema. Each writes
|
|
3
|
+
* something an admin cannot easily undo.
|
|
4
|
+
*
|
|
5
|
+
* `AdminOrderEditorView` is the one that touches money: `refundAmount`
|
|
6
|
+
* was parsed with a bare `parseFloat` and guarded only by
|
|
7
|
+
* `!isNaN(amount) && amount > 0`, with no upper bound and no check
|
|
8
|
+
* against the order — so a mistyped figure went straight through as a
|
|
9
|
+
* refund instruction.
|
|
10
|
+
*
|
|
11
|
+
* `AdminEmployeeEditorView` grants permissions, and `AdminUsersView`'s
|
|
12
|
+
* hard-ban takes a reason that is the entire record of why an account
|
|
13
|
+
* was destroyed.
|
|
14
|
+
*
|
|
15
|
+
* WHAT: One schema per editor.
|
|
16
|
+
*
|
|
17
|
+
* EXPORTS:
|
|
18
|
+
* adminOrderUpdateSchema, employeeInviteSchema, hardBanReasonSchema,
|
|
19
|
+
* featureFlagsUpdateSchema, adminAdFormSchema, and their value types
|
|
20
|
+
*
|
|
21
|
+
* @tag domain:admin
|
|
22
|
+
* @tag layer:schema
|
|
23
|
+
* @tag pattern:none
|
|
24
|
+
* @tag access:isomorphic
|
|
25
|
+
* @tag consumers:AdminOrderEditorView,AdminEmployeeEditorView,AdminUsersView,AdminFeatureFlagsView,AdminAdEditorView
|
|
26
|
+
* @tag sideEffects:none
|
|
27
|
+
*/
|
|
28
|
+
import { z } from "zod";
|
|
29
|
+
import { annotate } from "../../shell/field-ui-meta";
|
|
30
|
+
import { firestoreValueSchema } from "../../../schemas/firestore-value";
|
|
31
|
+
import { OrderStatusValues } from "../../orders/schemas/firestore";
|
|
32
|
+
/** Derived from the real 9-value union, never restated (Root Cause #36). */
|
|
33
|
+
const orderStatusSchema = z.enum(Object.values(OrderStatusValues));
|
|
34
|
+
export const adminOrderUpdateSchema = z.object({
|
|
35
|
+
status: annotate(orderStatusSchema, {
|
|
36
|
+
section: "status", sectionLabel: "Order", sectionRequired: true, quick: true, order: 1, row: "pair",
|
|
37
|
+
}),
|
|
38
|
+
trackingNumber: annotate(z.string().trim().max(120).optional(), {
|
|
39
|
+
section: "shipping", sectionLabel: "Shipping", order: 1, row: "pair",
|
|
40
|
+
}),
|
|
41
|
+
carrier: annotate(z.string().trim().max(80).optional(), {
|
|
42
|
+
section: "shipping", order: 2, row: "pair",
|
|
43
|
+
}),
|
|
44
|
+
/*
|
|
45
|
+
* 🛑 Bounded. This was `parseFloat` + `> 0` with no ceiling, so a slipped
|
|
46
|
+
* decimal point issued a refund an order of magnitude too large. The cap is
|
|
47
|
+
* a sanity bound, not a business rule — the route still checks the amount
|
|
48
|
+
* against the order itself.
|
|
49
|
+
*/
|
|
50
|
+
refundAmount: annotate(z.coerce
|
|
51
|
+
.number({ invalid_type_error: "Enter the refund amount as a number." })
|
|
52
|
+
.positive("A refund must be greater than zero.")
|
|
53
|
+
.max(10000000, "That looks like a typo — check the decimal point.")
|
|
54
|
+
.optional(), { section: "shipping", order: 3, row: "pair", kind: "number" }),
|
|
55
|
+
notes: annotate(z.string().trim().max(2000).optional(), {
|
|
56
|
+
section: "shipping", order: 4, row: "full", kind: "textarea",
|
|
57
|
+
}),
|
|
58
|
+
});
|
|
59
|
+
/** Inviting a team member — an email plus the permissions it grants. */
|
|
60
|
+
export const employeeInviteSchema = z.object({
|
|
61
|
+
email: annotate(z.string().trim().toLowerCase().min(1, "An email address is required.").email("That doesn't look like an email address."), { section: "member", sectionLabel: "Team member", sectionRequired: true, quick: true, order: 1, row: "pair" }),
|
|
62
|
+
permissionGroup: annotate(z.string().trim().min(1, "Choose a permission group."), {
|
|
63
|
+
section: "member", quick: true, order: 2, row: "pair",
|
|
64
|
+
}),
|
|
65
|
+
permissions: annotate(z.array(z.string().min(1)).max(200).optional(), {
|
|
66
|
+
section: "member", order: 3, row: "full", kind: "list",
|
|
67
|
+
}),
|
|
68
|
+
});
|
|
69
|
+
/**
|
|
70
|
+
* The reason recorded against a hard ban.
|
|
71
|
+
*
|
|
72
|
+
* A hard ban runs an eight-stage destructive cascade; this string is the only
|
|
73
|
+
* durable record of why. A 10-character floor, matching the admin offer-cancel
|
|
74
|
+
* reason, because "spam" is not an audit trail.
|
|
75
|
+
*/
|
|
76
|
+
export const hardBanReasonSchema = z.object({
|
|
77
|
+
reason: annotate(z.string().trim().min(10, "Record why this account is being banned — at least 10 characters.").max(500), { section: "ban", sectionLabel: "Reason", sectionRequired: true, quick: true, order: 1, row: "full", kind: "textarea" }),
|
|
78
|
+
});
|
|
79
|
+
/**
|
|
80
|
+
* Feature flags.
|
|
81
|
+
*
|
|
82
|
+
* Deliberately open records: flags are added and removed constantly, and a
|
|
83
|
+
* closed schema here would silently STRIP a newly-added flag on the next save
|
|
84
|
+
* — the `productBaseSchema` failure applied to configuration. The values are
|
|
85
|
+
* still constrained, which is what stops a flag being set to an object.
|
|
86
|
+
*/
|
|
87
|
+
export const featureFlagsUpdateSchema = z.object({
|
|
88
|
+
flags: annotate(z.record(z.string(), z.boolean()), {
|
|
89
|
+
section: "flags", sectionLabel: "Feature flags", sectionRequired: true, order: 1, row: "full", kind: "list",
|
|
90
|
+
}),
|
|
91
|
+
rollouts: annotate(z.record(z.string(), z.coerce.number().min(0).max(100)).optional(), {
|
|
92
|
+
section: "flags", order: 2, row: "full", kind: "list",
|
|
93
|
+
help: "Percentage rollout per flag, 0–100.",
|
|
94
|
+
}),
|
|
95
|
+
});
|
|
96
|
+
/** An ad placement. `priority` orders competing ads in the same slot. */
|
|
97
|
+
export const adminAdFormSchema = z
|
|
98
|
+
.object({
|
|
99
|
+
name: annotate(z.string().trim().min(2, "Give the ad a name.").max(120), {
|
|
100
|
+
section: "basics", sectionLabel: "Ad", sectionRequired: true, quick: true, order: 1, row: "pair",
|
|
101
|
+
}),
|
|
102
|
+
provider: annotate(z.enum(["manual", "adsense", "thirdParty"]), {
|
|
103
|
+
section: "basics", quick: true, order: 2, row: "pair",
|
|
104
|
+
}),
|
|
105
|
+
status: annotate(z.enum(["draft", "active", "scheduled", "paused"]), {
|
|
106
|
+
section: "basics", quick: true, order: 3, row: "pair",
|
|
107
|
+
}),
|
|
108
|
+
priority: annotate(z.coerce.number().int().min(0).max(1000), {
|
|
109
|
+
section: "basics", order: 4, row: "pair", kind: "number",
|
|
110
|
+
}),
|
|
111
|
+
placementIds: annotate(z.array(z.string().min(1)).min(1, "Choose at least one placement."), {
|
|
112
|
+
section: "placement", sectionLabel: "Placement", sectionRequired: true, order: 1, row: "full", kind: "list",
|
|
113
|
+
}),
|
|
114
|
+
requiresConsent: annotate(z.boolean(), {
|
|
115
|
+
section: "placement", order: 2, row: "quarter",
|
|
116
|
+
}),
|
|
117
|
+
startAt: annotate(z.string().optional(), {
|
|
118
|
+
section: "schedule", sectionLabel: "Schedule", order: 1, row: "pair", kind: "date",
|
|
119
|
+
}),
|
|
120
|
+
endAt: annotate(z.string().optional(), {
|
|
121
|
+
section: "schedule", order: 2, row: "pair", kind: "date",
|
|
122
|
+
}),
|
|
123
|
+
// Creative shape varies by provider, so it stays open — an AdSense slot and
|
|
124
|
+
// a manual banner share almost no fields. Open to a Firestore VALUE tree,
|
|
125
|
+
// not to anything: `z.unknown()` would pass a function straight to the
|
|
126
|
+
// driver, which then throws at write time rather than at parse time.
|
|
127
|
+
creative: annotate(z.record(firestoreValueSchema).optional(), {
|
|
128
|
+
section: "creative", sectionLabel: "Creative", order: 1, row: "full", kind: "list",
|
|
129
|
+
}),
|
|
130
|
+
})
|
|
131
|
+
.superRefine((v, ctx) => {
|
|
132
|
+
// A scheduled ad with no start date never runs, and nothing would say so.
|
|
133
|
+
if (v.status === "scheduled" && !v.startAt) {
|
|
134
|
+
ctx.addIssue({
|
|
135
|
+
code: z.ZodIssueCode.custom,
|
|
136
|
+
path: ["startAt"],
|
|
137
|
+
message: "A scheduled ad needs a start date.",
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
if (v.startAt && v.endAt && v.endAt <= v.startAt) {
|
|
141
|
+
ctx.addIssue({
|
|
142
|
+
code: z.ZodIssueCode.custom,
|
|
143
|
+
path: ["endAt"],
|
|
144
|
+
message: "The end date must be after the start date.",
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const adminUserUpdateSchema: z.ZodObject<{
|
|
3
|
+
displayName: z.ZodOptional<z.ZodString>;
|
|
4
|
+
phoneNumber: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
|
|
5
|
+
isTester: z.ZodOptional<z.ZodBoolean>;
|
|
6
|
+
canTestAdmin: z.ZodOptional<z.ZodBoolean>;
|
|
7
|
+
adminNotes: z.ZodOptional<z.ZodString>;
|
|
8
|
+
publicProfile: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodType<import("../../..").FirestoreValue, z.ZodTypeDef, import("../../..").FirestoreValue>>>;
|
|
9
|
+
}, "strip", z.ZodTypeAny, {
|
|
10
|
+
displayName?: string | undefined;
|
|
11
|
+
adminNotes?: string | undefined;
|
|
12
|
+
phoneNumber?: string | undefined;
|
|
13
|
+
isTester?: boolean | undefined;
|
|
14
|
+
canTestAdmin?: boolean | undefined;
|
|
15
|
+
publicProfile?: Record<string, import("../../..").FirestoreValue> | undefined;
|
|
16
|
+
}, {
|
|
17
|
+
displayName?: string | undefined;
|
|
18
|
+
adminNotes?: string | undefined;
|
|
19
|
+
phoneNumber?: string | undefined;
|
|
20
|
+
isTester?: boolean | undefined;
|
|
21
|
+
canTestAdmin?: boolean | undefined;
|
|
22
|
+
publicProfile?: Record<string, import("../../..").FirestoreValue> | undefined;
|
|
23
|
+
}>;
|
|
24
|
+
export type AdminUserUpdateValues = z.infer<typeof adminUserUpdateSchema>;
|
|
25
|
+
/**
|
|
26
|
+
* A store's WhatsApp Business connection.
|
|
27
|
+
*
|
|
28
|
+
* `accessToken` is optional because the form shows a masked placeholder and
|
|
29
|
+
* only sends the field when the seller types a NEW value — so "absent" means
|
|
30
|
+
* "unchanged", not "clear it". The `.min(20)` therefore applies only to a real
|
|
31
|
+
* replacement, and is there because a truncated token fails silently at sync
|
|
32
|
+
* time rather than on save.
|
|
33
|
+
*/
|
|
34
|
+
export declare const whatsappSettingsSchema: z.ZodObject<{
|
|
35
|
+
catalogSyncEnabled: z.ZodBoolean;
|
|
36
|
+
phoneNumber: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
|
|
37
|
+
wabaId: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
|
|
38
|
+
catalogId: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
|
|
39
|
+
accessToken: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
|
|
40
|
+
}, "strip", z.ZodTypeAny, {
|
|
41
|
+
catalogSyncEnabled: boolean;
|
|
42
|
+
phoneNumber?: string | undefined;
|
|
43
|
+
accessToken?: string | undefined;
|
|
44
|
+
wabaId?: string | undefined;
|
|
45
|
+
catalogId?: string | undefined;
|
|
46
|
+
}, {
|
|
47
|
+
catalogSyncEnabled: boolean;
|
|
48
|
+
phoneNumber?: string | undefined;
|
|
49
|
+
accessToken?: string | undefined;
|
|
50
|
+
wabaId?: string | undefined;
|
|
51
|
+
catalogId?: string | undefined;
|
|
52
|
+
}>;
|
|
53
|
+
export type WhatsappSettingsValues = z.infer<typeof whatsappSettingsSchema>;
|
|
54
|
+
/**
|
|
55
|
+
* A buyer's lottery entry.
|
|
56
|
+
*
|
|
57
|
+
* `purchasedItemNumber` was `parseInt(itemNumber, 10)` — `NaN` for anything
|
|
58
|
+
* non-numeric — and `paymentTime` was `new Date(...)`, which yields
|
|
59
|
+
* `Invalid Date` and then throws on `.toISOString()`. Both are coerced and
|
|
60
|
+
* checked here so the failure is an inline message rather than a crash or a
|
|
61
|
+
* `NaN` written to the entry.
|
|
62
|
+
*/
|
|
63
|
+
export declare const lotteryPullSchema: z.ZodObject<{
|
|
64
|
+
sourceType: z.ZodEnum<["event", "product"]>;
|
|
65
|
+
transactionId: z.ZodString;
|
|
66
|
+
paymentTime: z.ZodEffects<z.ZodString, string, string>;
|
|
67
|
+
purchasedItemNumber: z.ZodNumber;
|
|
68
|
+
userPhone: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
|
|
69
|
+
}, "strip", z.ZodTypeAny, {
|
|
70
|
+
transactionId: string;
|
|
71
|
+
sourceType: "event" | "product";
|
|
72
|
+
paymentTime: string;
|
|
73
|
+
purchasedItemNumber: number;
|
|
74
|
+
userPhone?: string | undefined;
|
|
75
|
+
}, {
|
|
76
|
+
transactionId: string;
|
|
77
|
+
sourceType: "event" | "product";
|
|
78
|
+
paymentTime: string;
|
|
79
|
+
purchasedItemNumber: number;
|
|
80
|
+
userPhone?: string | undefined;
|
|
81
|
+
}>;
|
|
82
|
+
export type LotteryPullValues = z.infer<typeof lotteryPullSchema>;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* WHY: Three more forms, each writing something with a blast radius.
|
|
3
|
+
*
|
|
4
|
+
* `AdminUserEditorView` edits another person's account — `displayName`
|
|
5
|
+
* and `phoneNumber` are PII, encrypted at rest, and neither was checked.
|
|
6
|
+
* `isTester`/`canTestAdmin` are the flags Recurrent Root Cause #38 was
|
|
7
|
+
* about: the list endpoint dropped them, the editor defaulted them to
|
|
8
|
+
* false, and saving any unrelated field silently stripped a real
|
|
9
|
+
* tester's access.
|
|
10
|
+
*
|
|
11
|
+
* `SellerWhatsAppSettingsView` writes `accessToken` — a live Meta
|
|
12
|
+
* credential — plus the WABA and catalog ids the sync runs against. A
|
|
13
|
+
* malformed id there fails at sync time, hours later, with no obvious
|
|
14
|
+
* cause.
|
|
15
|
+
*
|
|
16
|
+
* `LotteryPullForm` is a buyer-facing entry: a transaction id, a payment
|
|
17
|
+
* time and an item number, all parsed with a bare `parseInt` and a `new
|
|
18
|
+
* Date(...)` that yields `Invalid Date` on anything unparseable.
|
|
19
|
+
*
|
|
20
|
+
* EXPORTS:
|
|
21
|
+
* adminUserUpdateSchema, whatsappSettingsSchema, lotteryPullSchema,
|
|
22
|
+
* and their inferred value types
|
|
23
|
+
*
|
|
24
|
+
* @tag domain:admin,whatsapp,lottery
|
|
25
|
+
* @tag layer:schema
|
|
26
|
+
* @tag pattern:none
|
|
27
|
+
* @tag access:isomorphic
|
|
28
|
+
* @tag consumers:AdminUserEditorView,SellerWhatsAppSettingsView,LotteryPullForm
|
|
29
|
+
* @tag sideEffects:none
|
|
30
|
+
*/
|
|
31
|
+
import { z } from "zod";
|
|
32
|
+
import { annotate } from "../../shell/field-ui-meta";
|
|
33
|
+
import { firestoreValueSchema } from "../../../schemas/firestore-value";
|
|
34
|
+
const PHONE_RE = /^(\+?91[-\s]?)?[6-9]\d{9}$/;
|
|
35
|
+
export const adminUserUpdateSchema = z.object({
|
|
36
|
+
displayName: annotate(z.string().trim().min(1).max(120).optional(), {
|
|
37
|
+
section: "profile", sectionLabel: "Profile", sectionRequired: true, quick: true, order: 1, row: "pair",
|
|
38
|
+
}),
|
|
39
|
+
phoneNumber: annotate(z.string().trim().regex(PHONE_RE, "Enter a 10-digit Indian mobile number.").optional().or(z.literal("")), { section: "profile", order: 2, row: "pair" }),
|
|
40
|
+
/*
|
|
41
|
+
* The tester flags. Kept OPTIONAL rather than defaulted, deliberately: a
|
|
42
|
+
* PATCH that omits them must leave them alone. Defaulting either to `false`
|
|
43
|
+
* here would reproduce Root Cause #38 in the schema layer — the editor
|
|
44
|
+
* already did exactly that from the list row.
|
|
45
|
+
*/
|
|
46
|
+
isTester: annotate(z.boolean().optional(), {
|
|
47
|
+
section: "access", sectionLabel: "Access", order: 1, row: "quarter",
|
|
48
|
+
}),
|
|
49
|
+
canTestAdmin: annotate(z.boolean().optional(), {
|
|
50
|
+
section: "access", order: 2, row: "quarter",
|
|
51
|
+
}),
|
|
52
|
+
adminNotes: annotate(z.string().trim().max(2000).optional(), {
|
|
53
|
+
section: "access", order: 3, row: "full", kind: "textarea",
|
|
54
|
+
help: "Internal only — never shown to the user.",
|
|
55
|
+
}),
|
|
56
|
+
// Open: the public profile carries per-user social links and bio fields that
|
|
57
|
+
// are added over time, and a closed shape would strip a newly-added one.
|
|
58
|
+
publicProfile: annotate(z.record(firestoreValueSchema).optional(), {
|
|
59
|
+
section: "access", order: 4, row: "full", kind: "list",
|
|
60
|
+
}),
|
|
61
|
+
});
|
|
62
|
+
/**
|
|
63
|
+
* A store's WhatsApp Business connection.
|
|
64
|
+
*
|
|
65
|
+
* `accessToken` is optional because the form shows a masked placeholder and
|
|
66
|
+
* only sends the field when the seller types a NEW value — so "absent" means
|
|
67
|
+
* "unchanged", not "clear it". The `.min(20)` therefore applies only to a real
|
|
68
|
+
* replacement, and is there because a truncated token fails silently at sync
|
|
69
|
+
* time rather than on save.
|
|
70
|
+
*/
|
|
71
|
+
export const whatsappSettingsSchema = z.object({
|
|
72
|
+
catalogSyncEnabled: annotate(z.boolean(), {
|
|
73
|
+
section: "sync", sectionLabel: "Catalog sync", sectionRequired: true, quick: true, order: 1, row: "quarter",
|
|
74
|
+
}),
|
|
75
|
+
phoneNumber: annotate(z.string().trim().regex(PHONE_RE, "Enter the 10-digit WhatsApp Business number.").optional().or(z.literal("")), { section: "connection", sectionLabel: "Connection", order: 1, row: "pair" }),
|
|
76
|
+
// Meta ids are numeric strings; a pasted URL or a stray space is the usual
|
|
77
|
+
// mistake and produces a sync that never runs.
|
|
78
|
+
wabaId: annotate(z.string().trim().regex(/^\d{5,32}$/, "A WABA ID is a numeric id, not a URL.").optional().or(z.literal("")), { section: "connection", order: 2, row: "pair" }),
|
|
79
|
+
catalogId: annotate(z.string().trim().regex(/^\d{5,32}$/, "A catalog ID is a numeric id, not a URL.").optional().or(z.literal("")), { section: "connection", order: 3, row: "pair" }),
|
|
80
|
+
accessToken: annotate(z.string().trim().min(20, "That access token looks truncated.").optional().or(z.literal("")), { section: "connection", order: 4, row: "full", help: "Leave blank to keep the current token." }),
|
|
81
|
+
});
|
|
82
|
+
/**
|
|
83
|
+
* A buyer's lottery entry.
|
|
84
|
+
*
|
|
85
|
+
* `purchasedItemNumber` was `parseInt(itemNumber, 10)` — `NaN` for anything
|
|
86
|
+
* non-numeric — and `paymentTime` was `new Date(...)`, which yields
|
|
87
|
+
* `Invalid Date` and then throws on `.toISOString()`. Both are coerced and
|
|
88
|
+
* checked here so the failure is an inline message rather than a crash or a
|
|
89
|
+
* `NaN` written to the entry.
|
|
90
|
+
*/
|
|
91
|
+
export const lotteryPullSchema = z.object({
|
|
92
|
+
sourceType: annotate(z.enum(["event", "product"]), {
|
|
93
|
+
section: "entry", sectionLabel: "Entry", sectionRequired: true, quick: true, order: 1, row: "pair",
|
|
94
|
+
}),
|
|
95
|
+
transactionId: annotate(z.string().trim().min(4, "Enter the payment reference from your app.").max(120), { section: "entry", quick: true, order: 2, row: "pair" }),
|
|
96
|
+
paymentTime: annotate(z.string().trim().min(1, "When did you pay?").refine((v) => !Number.isNaN(Date.parse(v)), "That isn't a valid date and time."), { section: "entry", quick: true, order: 3, row: "pair", kind: "date" }),
|
|
97
|
+
purchasedItemNumber: annotate(z.coerce
|
|
98
|
+
.number({ invalid_type_error: "Enter the slot number you bought." })
|
|
99
|
+
.int("A slot number is a whole number.")
|
|
100
|
+
.min(1, "Slot numbers start at 1."), { section: "entry", quick: true, order: 4, row: "pair", kind: "number" }),
|
|
101
|
+
userPhone: annotate(z.string().trim().regex(PHONE_RE, "Enter a 10-digit Indian mobile number.").optional().or(z.literal("")), { section: "entry", order: 5, row: "pair" }),
|
|
102
|
+
});
|