/** * API Request Schemas * Schemas for data that the backend receives and validates * These are the "source of truth" for business logic validation */ import { z } from "zod"; import { emailSchema, passwordSchema, nameSchema, phoneSchema, addressSchema, requiredAddressSchema, genderEnum, } from "../shared/primitives"; import { invoiceStatusSchema } from "../shared/entities"; const invoiceStatusEnum = z.enum(["Paid", "Unpaid", "Overdue", "Cancelled", "Collections"]); const subscriptionStatusEnum = z.enum([ "Active", "Suspended", "Terminated", "Cancelled", "Pending", ]); export const paginationQuerySchema = z.object({ page: z.coerce.number().int().min(1).default(1), limit: z.coerce.number().int().min(1).max(100).default(10), }); export type PaginationQuery = z.infer; export const auditLogQuerySchema = paginationQuerySchema.extend({ action: z.string().optional(), userId: z.string().uuid().optional(), }); export type AuditLogQuery = z.infer; export const dryRunQuerySchema = z.object({ dryRun: z.coerce.boolean().optional(), }); export type DryRunQuery = z.infer; export const invoiceListQuerySchema = paginationQuerySchema.extend({ status: invoiceStatusEnum.optional(), }); export type InvoiceListQuery = z.infer; export const subscriptionQuerySchema = z.object({ status: subscriptionStatusEnum.optional(), }); export type SubscriptionQuery = z.infer; // ===================================================== // AUTH REQUEST SCHEMAS // ===================================================== export const loginRequestSchema = z.object({ email: emailSchema, password: z.string().min(1, "Password is required"), }); export const signupRequestSchema = z.object({ email: emailSchema, password: passwordSchema, firstName: nameSchema, lastName: nameSchema, company: z.string().optional(), phone: phoneSchema, sfNumber: z.string().min(6, "Customer number must be at least 6 characters"), address: requiredAddressSchema, nationality: z.string().optional(), dateOfBirth: z.string().optional(), gender: genderEnum.optional(), }); export const passwordResetRequestSchema = z.object({ email: emailSchema, }); export const passwordResetSchema = z.object({ token: z.string().min(1, "Reset token is required"), password: passwordSchema, }); export const setPasswordRequestSchema = z.object({ email: emailSchema, password: passwordSchema, }); export const changePasswordRequestSchema = z.object({ currentPassword: z.string().min(1, "Current password is required"), newPassword: passwordSchema, }); export const linkWhmcsRequestSchema = z.object({ email: emailSchema, password: z.string().min(1, "Password is required"), }); export const validateSignupRequestSchema = z.object({ sfNumber: z.string().min(1, "Customer number is required"), }); export const accountStatusRequestSchema = z.object({ email: emailSchema, }); export const ssoLinkRequestSchema = z.object({ destination: z.string().optional(), }); export const checkPasswordNeededRequestSchema = z.object({ email: emailSchema, }); export const refreshTokenRequestSchema = z.object({ refreshToken: z.string().min(1, "Refresh token is required").optional(), deviceId: z.string().optional(), }); // ===================================================== // TYPE EXPORTS // ===================================================== export type LoginRequestInput = z.infer; export type SignupRequestInput = z.infer; export type PasswordResetRequestInput = z.infer; export type PasswordResetInput = z.infer; export type SetPasswordRequestInput = z.infer; export type ChangePasswordRequestInput = z.infer; export type LinkWhmcsRequestInput = z.infer; export type ValidateSignupRequestInput = z.infer; export type AccountStatusRequestInput = z.infer; export type SsoLinkRequestInput = z.infer; export type CheckPasswordNeededRequestInput = z.infer; export type RefreshTokenRequestInput = z.infer; // ===================================================== // USER MANAGEMENT REQUEST SCHEMAS // ===================================================== export const updateProfileRequestSchema = z.object({ firstName: nameSchema.optional(), lastName: nameSchema.optional(), phone: phoneSchema.optional(), company: z.string().max(200).optional(), }); export const updateAddressRequestSchema = addressSchema; // ===================================================== // ORDER REQUEST SCHEMAS // ===================================================== export const orderConfigurationsSchema = z.object({ // Activation (All order types) activationType: z.enum(["Immediate", "Scheduled"]).optional(), scheduledAt: z.string().datetime().optional(), // Internet specific accessMode: z.enum(["IPoE-BYOR", "IPoE-HGW", "PPPoE"]).optional(), // SIM specific simType: z.enum(["eSIM", "Physical SIM"]).optional(), eid: z.string().optional(), // Required for eSIM // MNP/Porting isMnp: z.string().optional(), // "true" | "false" mnpNumber: z.string().optional(), mnpExpiry: z.string().optional(), mnpPhone: z.string().optional(), mvnoAccountNumber: z.string().optional(), portingLastName: z.string().optional(), portingFirstName: z.string().optional(), portingLastNameKatakana: z.string().optional(), portingFirstNameKatakana: z.string().optional(), portingGender: z.enum(["Male", "Female", "Corporate/Other"]).optional(), portingDateOfBirth: z.string().date().optional(), // Optional address override captured at checkout address: addressSchema.optional(), }); export const createOrderRequestSchema = z.object({ orderType: z.enum(["Internet", "SIM", "VPN", "Other"]), skus: z.array(z.string().min(1, "SKU cannot be empty")), configurations: orderConfigurationsSchema.optional(), }); export const orderIdParamSchema = z.object({ id: z.coerce.number().int().positive("Order ID must be positive"), }); // ===================================================== // SUBSCRIPTION MANAGEMENT REQUEST SCHEMAS // ===================================================== export const simTopupRequestSchema = z.object({ amount: z.number().positive("Amount must be positive"), currency: z.string().length(3, "Currency must be 3 characters").default("JPY"), quotaMb: z.number().positive("Quota in MB must be positive"), }); export const simCancelRequestSchema = z.object({ reason: z.string().min(1, "Cancellation reason is required").optional(), scheduledAt: z .string() .regex(/^\d{8}$/, "Scheduled date must be in YYYYMMDD format") .optional(), }); export const simChangePlanRequestSchema = z.object({ newPlanSku: z.string().min(1, "New plan SKU is required"), newPlanCode: z.string().min(1, "New plan code is required"), effectiveDate: z.string().date().optional(), }); export const simFeaturesRequestSchema = z.object({ voiceMailEnabled: z.boolean().optional(), callWaitingEnabled: z.boolean().optional(), internationalRoamingEnabled: z.boolean().optional(), networkType: z.enum(["4G", "5G"]).optional(), }); // ===================================================== // CONTACT REQUEST SCHEMAS // ===================================================== export const contactRequestSchema = z.object({ subject: z.string().min(1, "Subject is required").max(200, "Subject is too long"), message: z.string().min(1, "Message is required").max(2000, "Message is too long"), category: z.enum(["technical", "billing", "account", "general"]), priority: z.enum(["low", "medium", "high", "urgent"]).default("medium"), }); // ===================================================== // TYPE EXPORTS // ===================================================== export type UpdateProfileRequest = z.infer; export type UpdateAddressRequest = z.infer; export type OrderConfigurations = z.infer; export type CreateOrderRequest = z.infer; export type SimTopupRequest = z.infer; export type SimCancelRequest = z.infer; export type SimChangePlanRequest = z.infer; export type SimFeaturesRequest = z.infer; export type ContactRequest = z.infer; // ===================================================== // INVOICE SCHEMAS // ===================================================== export const invoiceItemSchema = z.object({ id: z.number().int().positive(), description: z.string().min(1, "Description is required"), amount: z.number().nonnegative("Amount must be non-negative"), quantity: z.number().int().positive().optional().default(1), type: z.string().min(1, "Type is required"), serviceId: z.number().int().positive().optional(), }); export const invoiceSchema = z.object({ id: z.number().int().positive(), number: z.string().min(1, "Invoice number is required"), status: invoiceStatusSchema, currency: z.string().length(3, "Currency must be 3 characters"), currencySymbol: z.string().optional(), total: z.number().nonnegative("Total must be non-negative"), subtotal: z.number().nonnegative("Subtotal must be non-negative"), tax: z.number().nonnegative("Tax must be non-negative"), issuedAt: z.string().datetime().optional(), dueDate: z.string().datetime().optional(), paidDate: z.string().datetime().optional(), pdfUrl: z.string().url().optional(), paymentUrl: z.string().url().optional(), description: z.string().optional(), items: z.array(invoiceItemSchema).optional(), }); export const paginationSchema = z.object({ page: z.number().int().min(1), totalPages: z.number().int().min(0), totalItems: z.number().int().min(0), nextCursor: z.string().optional(), }); export const invoiceListSchema = z.object({ invoices: z.array(invoiceSchema), pagination: paginationSchema, }); // ===================================================== // INVOICE TYPE EXPORTS // ===================================================== export type InvoiceItem = z.infer; export type Invoice = z.infer; export type Pagination = z.infer; export type InvoiceList = z.infer; export const invoicePaymentLinkSchema = z.object({ paymentMethodId: z.coerce.number().int().positive().optional(), gatewayName: z.string().min(1).optional(), }); export type InvoicePaymentLinkInput = z.infer; export const sfOrderIdParamSchema = z.object({ sfOrderId: z.string().min(1, "Salesforce order ID is required"), }); export type SfOrderIdParam = z.infer; // ===================================================== // ID MAPPING SCHEMAS // ===================================================== export const createMappingRequestSchema = z.object({ userId: z.string().uuid("User ID must be a valid UUID"), whmcsClientId: z.number().int().positive("WHMCS client ID must be a positive integer"), sfAccountId: z .string() .regex( /^[a-zA-Z0-9]{15}$|^[a-zA-Z0-9]{18}$/, "Salesforce account ID must be a valid 15 or 18 character ID" ) .optional(), }); export const updateMappingRequestSchema = z .object({ whmcsClientId: z .number() .int() .positive("WHMCS client ID must be a positive integer") .optional(), sfAccountId: z .string() .regex( /^[a-zA-Z0-9]{15}$|^[a-zA-Z0-9]{18}$/, "Salesforce account ID must be a valid 15 or 18 character ID" ) .optional(), }) .refine(data => data.whmcsClientId !== undefined || data.sfAccountId !== undefined, { message: "At least one field must be provided for update", }); export const userIdMappingSchema = z.object({ userId: z.string().uuid("User ID must be a valid UUID"), whmcsClientId: z.number().int().positive("WHMCS client ID must be a positive integer"), sfAccountId: z .string() .regex( /^[a-zA-Z0-9]{15}$|^[a-zA-Z0-9]{18}$/, "Salesforce account ID must be a valid 15 or 18 character ID" ) .optional(), createdAt: z.string().datetime().optional(), updatedAt: z.string().datetime().optional(), }); // ===================================================== // ID MAPPING TYPE EXPORTS // ===================================================== export type CreateMappingRequest = z.infer; export type UpdateMappingRequest = z.infer; export type UserIdMapping = z.infer; export type UpdateProfileRequestInput = z.infer; export type UpdateAddressRequestInput = z.infer; export type ContactRequestInput = z.infer;