181 lines
5.8 KiB
TypeScript
181 lines
5.8 KiB
TypeScript
import { z } from "zod"
|
|
import { emailSchema } from "@/lib/validation"
|
|
|
|
const totpCodeSchema = z
|
|
.string()
|
|
.trim()
|
|
.length(6, "Verification code must be 6 digits")
|
|
.regex(/^\d+$/, "Verification code must be numeric")
|
|
|
|
/** POST /api/auth/verify-mfa */
|
|
export const authVerifyMfaSchema = z.object({
|
|
email: emailSchema,
|
|
code: totpCodeSchema,
|
|
})
|
|
|
|
/** POST /api/auth/verify-email/verify */
|
|
export const authVerifyEmailBodySchema = z.object({
|
|
token: z.string().trim().min(1).max(512),
|
|
email: emailSchema,
|
|
})
|
|
|
|
/** POST /api/settings/mfa/toggle */
|
|
export const mfaToggleSchema = z.object({
|
|
enabled: z.boolean(),
|
|
})
|
|
|
|
/** POST /api/settings/mfa/verify */
|
|
export const mfaSetupVerifySchema = z.object({
|
|
code: totpCodeSchema,
|
|
})
|
|
|
|
/** POST /api/stores/switch */
|
|
export const storeSwitchSchema = z.object({
|
|
storeId: z.string().trim().min(1).max(128),
|
|
})
|
|
|
|
const shopifyDomainField = z.string().trim().min(1).max(512)
|
|
const accessTokenField = z.string().trim().min(1).max(4000)
|
|
|
|
/** POST /api/onboarding/complete */
|
|
export const onboardingCompleteSchema = z.object({
|
|
businessName: z.string().trim().min(1).max(200),
|
|
businessEmail: z.union([emailSchema, z.literal("")]).optional(),
|
|
country: z.string().trim().max(100).optional(),
|
|
timezone: z.string().trim().max(100).optional(),
|
|
storeName: z.string().trim().max(200).optional(),
|
|
domain: shopifyDomainField,
|
|
apiKey: z.string().trim().max(500).optional(),
|
|
apiSecret: z.string().trim().max(500).optional(),
|
|
accessToken: accessTokenField,
|
|
})
|
|
|
|
/** POST /api/onboarding/shopify */
|
|
export const onboardingShopifySchema = z.object({
|
|
businessName: z.string().trim().min(1).max(200),
|
|
storeName: z.string().trim().min(1).max(200),
|
|
domain: shopifyDomainField,
|
|
apiKey: z.string().trim().min(1).max(500),
|
|
apiSecret: z.string().trim().min(1).max(500),
|
|
accessToken: accessTokenField,
|
|
})
|
|
|
|
/** POST /api/stores/[storeId]/kb/query */
|
|
export const kbQuerySchema = z
|
|
.object({
|
|
query: z.string().max(16_000),
|
|
topK: z.number().int().min(1).max(20).optional(),
|
|
includeFaq: z.boolean().optional(),
|
|
includeDocuments: z.boolean().optional(),
|
|
includeStructured: z.boolean().optional(),
|
|
})
|
|
.transform((d) => ({
|
|
...d,
|
|
query: d.query.trim(),
|
|
}))
|
|
.refine((d) => d.query.length >= 1, { message: "Query is required", path: ["query"] })
|
|
|
|
/** POST /api/stores/[storeId]/kb/faqs */
|
|
export const kbFaqCreateSchema = z.object({
|
|
question: z.string().trim().min(1).max(4000),
|
|
answer: z.string().trim().min(1).max(50_000),
|
|
tags: z.unknown().optional(),
|
|
})
|
|
|
|
const FAQ_IMPORT_MAX_ITEMS = 500
|
|
|
|
/** JSON array for POST /api/stores/[storeId]/kb/faqs/import */
|
|
export const kbFaqImportArraySchema = z
|
|
.array(
|
|
z.object({
|
|
question: z.string().trim().min(1).max(4000),
|
|
answer: z.string().trim().min(1).max(50_000),
|
|
tags: z.unknown().optional(),
|
|
}),
|
|
)
|
|
.min(1, "At least one FAQ is required")
|
|
.max(FAQ_IMPORT_MAX_ITEMS, `At most ${FAQ_IMPORT_MAX_ITEMS} FAQs per import`)
|
|
|
|
/** PUT /api/supplier/vendors/[connectionId] */
|
|
export const supplierConnectionStatusSchema = z.object({
|
|
status: z.enum(["CONNECTED", "REJECTED"]),
|
|
})
|
|
|
|
const finitePrice = z.number().finite().min(0).max(1e12)
|
|
const positiveQty = z.number().int().min(1).max(1e9)
|
|
|
|
/** PUT /api/supplier/vendors/offers/[offerId] */
|
|
export const supplierOfferUpdateSchema = z
|
|
.object({
|
|
price: finitePrice.optional(),
|
|
quantity: positiveQty.optional(),
|
|
})
|
|
.refine((d) => d.price !== undefined || d.quantity !== undefined, {
|
|
message: "At least one of price or quantity must be provided",
|
|
})
|
|
|
|
/** POST /api/supplier/vendors/[connectionId]/offers */
|
|
export const supplierOfferCreateSchema = z.object({
|
|
productId: z.string().trim().min(1).max(128),
|
|
price: finitePrice,
|
|
quantity: positiveQty,
|
|
})
|
|
|
|
/** POST /api/vendors/suppliers/[supplierId]/invite */
|
|
export const vendorInvitePostSchema = z
|
|
.object({
|
|
connectToAllStores: z.boolean().optional(),
|
|
})
|
|
.strict()
|
|
|
|
/** DELETE /api/vendors/suppliers/[supplierId]/invite */
|
|
export const vendorInviteDeleteSchema = z
|
|
.object({
|
|
storeId: z.string().trim().min(1).max(128).optional().nullable(),
|
|
})
|
|
.strict()
|
|
|
|
/** POST /api/supplier/invitations/accept */
|
|
export const supplierInvitationAcceptSchema = z.object({
|
|
token: z.string().trim().min(1).max(128),
|
|
})
|
|
|
|
/** POST /api/juno-engine/flagged-emails */
|
|
export const flaggedEmailCreateSchema = z.object({
|
|
subject: z.string().trim().min(1).max(500),
|
|
from: z.string().trim().min(1).max(500),
|
|
reason: z.string().trim().min(1).max(8000),
|
|
priority: z.enum(["LOW", "MEDIUM", "HIGH"]).optional(),
|
|
messageId: z.string().trim().max(500).optional(),
|
|
})
|
|
|
|
/** POST /api/shopify/validate */
|
|
export const shopifyValidateSchema = z.object({
|
|
domain: shopifyDomainField,
|
|
accessToken: accessTokenField,
|
|
})
|
|
|
|
/** PATCH /api/stores/[storeId]/shopify-access-token — vendor updates token after app reinstall / scope change */
|
|
export const storeShopifyAccessTokenUpdateSchema = z.object({
|
|
accessToken: accessTokenField,
|
|
})
|
|
|
|
function optionalTrackingField(maxLen: number) {
|
|
return z
|
|
.union([z.string(), z.number(), z.null(), z.undefined()])
|
|
.transform((v) => {
|
|
if (v === undefined || v === null) return null
|
|
const t = String(v).trim()
|
|
return t === "" ? null : t
|
|
})
|
|
.refine((v) => v === null || v.length <= maxLen, "Tracking value too long")
|
|
}
|
|
|
|
/** PATCH /api/supplier/orders/[orderId] */
|
|
export const supplierOrderPatchSchema = z.object({
|
|
fulfillmentStatus: z.union([z.string(), z.number(), z.null(), z.undefined()]).optional(),
|
|
trackingNumber: optionalTrackingField(500),
|
|
trackingCompany: optionalTrackingField(500),
|
|
trackingUrl: optionalTrackingField(2000),
|
|
})
|