/** * Get Started Domain - Schemas * * Zod validation schemas for the unified "Get Started" flow. */ import { z } from "zod"; import { emailSchema, nameSchema, passwordSchema, phoneSchema, genderEnum, } from "../common/schema.js"; import { addressFormSchema } from "../customer/schema.js"; // ============================================================================ // OTP Verification Schemas // ============================================================================ /** * Request to send a verification code to an email address */ export const sendVerificationCodeRequestSchema = z.object({ email: emailSchema, }); /** * Response after sending verification code */ export const sendVerificationCodeResponseSchema = z.object({ /** Whether the code was sent successfully */ sent: z.boolean(), /** Message to display to user */ message: z.string(), /** When a new code can be requested (ISO timestamp) */ retryAfter: z.string().datetime().optional(), }); /** * 6-digit OTP code schema */ export const otpCodeSchema = z .string() .length(6, "Code must be 6 digits") .regex(/^\d{6}$/, "Code must be 6 digits"); /** * Request to verify an OTP code */ export const verifyCodeRequestSchema = z.object({ email: emailSchema, code: otpCodeSchema, /** Optional handoff token from guest eligibility check - used to pre-fill data */ handoffToken: z.string().optional(), }); /** * Account status enum for verification response */ export const accountStatusSchema = z.enum([ "portal_exists", "whmcs_unmapped", "sf_unmapped", "new_customer", ]); /** * Response after verifying OTP code * Includes account status to determine next flow */ export const verifyCodeResponseSchema = z.object({ /** Whether the code was valid */ verified: z.boolean(), /** Error message if verification failed */ error: z.string().optional(), /** Remaining attempts if verification failed */ attemptsRemaining: z.number().optional(), /** Session token for continuing the flow (only if verified) */ sessionToken: z.string().optional(), /** Account status determining next flow (only if verified) */ accountStatus: accountStatusSchema.optional(), /** Pre-filled data from existing account (only if SF or WHMCS exists) */ prefill: z .object({ firstName: z.string().optional(), lastName: z.string().optional(), email: z.string().optional(), phone: z.string().optional(), address: addressFormSchema.partial().optional(), eligibilityStatus: z.string().optional(), }) .optional(), }); // ============================================================================ // Helpers // ============================================================================ /** * ISO date string (YYYY-MM-DD) */ const isoDateOnlySchema = z .string() .regex(/^\d{4}-\d{2}-\d{2}$/, "Enter a valid date (YYYY-MM-DD)") .refine(value => !Number.isNaN(Date.parse(value)), "Enter a valid date (YYYY-MM-DD)"); // ============================================================================ // Guest Eligibility Check Schemas (No OTP Required) // ============================================================================ /** * Bilingual address schema for eligibility requests * Contains both English (for WHMCS) and Japanese (for Salesforce) address fields */ export const bilingualEligibilityAddressSchema = z.object({ // English/Romanized fields (for WHMCS) address1: z.string().min(1, "Address is required").max(200).trim(), address2: z.string().max(200).trim().optional(), city: z.string().min(1, "City is required").max(100).trim(), state: z.string().min(1, "Prefecture is required").max(100).trim(), postcode: z.string().min(1, "Postcode is required").max(20).trim(), country: z.string().max(100).trim().optional(), // Japanese fields (for Salesforce) prefectureJa: z.string().max(100).trim().optional(), cityJa: z.string().max(100).trim().optional(), townJa: z.string().max(200).trim().optional(), streetAddress: z.string().max(50).trim().optional(), buildingName: z.string().max(200).trim().nullable().optional(), roomNumber: z.string().max(50).trim().nullable().optional(), }); /** * Request for guest eligibility check - NO email verification required * Allows users to check availability without verifying email first */ export const guestEligibilityRequestSchema = z.object({ /** Customer email (for notifications, not verified) */ email: emailSchema, /** Customer first name */ firstName: nameSchema, /** Customer last name */ lastName: nameSchema, /** Full address with both English and Japanese fields for eligibility check */ address: bilingualEligibilityAddressSchema, /** Optional phone number */ phone: phoneSchema.optional(), /** Whether user wants to continue to account creation */ continueToAccount: z.boolean().default(false), }); /** * Response from guest eligibility check */ export const guestEligibilityResponseSchema = z.object({ /** Whether the request was submitted successfully */ submitted: z.boolean(), /** Case ID for the eligibility request */ requestId: z.string().optional(), /** SF Account ID created */ sfAccountId: z.string().optional(), /** Message to display */ message: z.string(), /** Handoff token for account creation flow (if continueToAccount was true) */ handoffToken: z.string().optional(), }); /** * Guest handoff token data stored in Redis * Used to transfer data from eligibility check to account creation */ export const guestHandoffTokenSchema = z.object({ /** Token ID */ id: z.string(), /** Token type identifier */ type: z.literal("guest_handoff"), /** Email address (NOT verified) */ email: z.string(), /** Whether email has been verified (always false for guest handoff) */ emailVerified: z.literal(false), /** First name */ firstName: z.string(), /** Last name */ lastName: z.string(), /** Address from eligibility check */ address: addressFormSchema.partial().optional(), /** Phone number if provided */ phone: z.string().optional(), /** SF Account ID created during eligibility check */ sfAccountId: z.string(), /** Token creation timestamp */ createdAt: z.string().datetime(), }); // ============================================================================ // Account Completion Schemas // ============================================================================ /** * Request to complete account for SF-only users or new customers * Creates WHMCS client and Portal user, links to existing SF Account (if any) * * For SF-only users: name/address comes from session (prefilled from handoff token) * For new customers: name/address must be provided in the request */ export const completeAccountRequestSchema = z.object({ /** Session token from verified email */ sessionToken: z.string().min(1, "Session token is required"), /** Customer first name (required for new customers, optional for SF-only) */ firstName: nameSchema.optional(), /** Customer last name (required for new customers, optional for SF-only) */ lastName: nameSchema.optional(), /** Address (required for new customers, optional for SF-only who have it in session) */ address: addressFormSchema.optional(), /** Password for the new portal account */ password: passwordSchema, /** Phone number (may be pre-filled from SF) */ phone: phoneSchema, /** Date of birth */ dateOfBirth: isoDateOnlySchema, /** Gender */ gender: genderEnum, /** Accept terms of service */ acceptTerms: z.boolean().refine(val => val === true, { message: "You must accept the terms of service", }), /** Marketing consent */ marketingConsent: z.boolean().optional(), }); // ============================================================================ // Signup With Eligibility Schema (Full Inline Signup) // ============================================================================ /** * Request for full signup with eligibility check * Creates SF Account + Case + WHMCS + Portal in one operation * Used when user chooses "Create Account" on eligibility check page */ export const signupWithEligibilityRequestSchema = z.object({ /** Session token from verified email */ sessionToken: z.string().min(1, "Session token is required"), /** Customer first name */ firstName: nameSchema, /** Customer last name */ lastName: nameSchema, /** Full address for eligibility check and WHMCS */ address: addressFormSchema, /** Phone number */ phone: phoneSchema, /** Password for the new portal account */ password: passwordSchema, /** Date of birth */ dateOfBirth: isoDateOnlySchema, /** Gender */ gender: genderEnum, /** Accept terms of service */ acceptTerms: z.boolean().refine(val => val === true, { message: "You must accept the terms of service", }), /** Marketing consent */ marketingConsent: z.boolean().optional(), }); /** * Response from signup with eligibility */ export const signupWithEligibilityResponseSchema = z.object({ /** Whether signup was successful */ success: z.boolean(), /** Error message if failed */ message: z.string().optional(), /** Case ID for the eligibility request (if successful) */ eligibilityRequestId: z.string().optional(), }); // ============================================================================ // WHMCS Migration Schema (Passwordless Migration) // ============================================================================ /** * Request to migrate WHMCS account to portal without legacy password * For whmcs_unmapped users after email verification * Creates portal user and syncs password to WHMCS */ export const migrateWhmcsAccountRequestSchema = z.object({ /** Session token from verified email */ sessionToken: z.string().min(1, "Session token is required"), /** Password for the new portal account (will also sync to WHMCS) */ password: passwordSchema, /** Date of birth */ dateOfBirth: isoDateOnlySchema, /** Gender */ gender: genderEnum, /** Accept terms of service */ acceptTerms: z.boolean().refine(val => val === true, { message: "You must accept the terms of service", }), /** Marketing consent */ marketingConsent: z.boolean().optional(), }); // ============================================================================ // Session Schema // ============================================================================ /** * Get Started session stored in Redis * Tracks progress through the unified flow */ export const getStartedSessionSchema = z.object({ /** Email address (normalized) */ email: z.string(), /** Whether email has been verified via OTP */ emailVerified: z.boolean(), /** First name (if provided during quick check) */ firstName: z.string().optional(), /** Last name (if provided during quick check) */ lastName: z.string().optional(), /** Address (if provided during quick check) */ address: addressFormSchema.partial().optional(), /** Phone number (if provided) */ phone: z.string().optional(), /** Account status after verification */ accountStatus: accountStatusSchema.optional(), /** SF Account ID (if exists or created) */ sfAccountId: z.string().optional(), /** WHMCS Client ID (if exists) */ whmcsClientId: z.number().optional(), /** Eligibility status from SF */ eligibilityStatus: z.string().optional(), /** Session creation timestamp */ createdAt: z.string().datetime(), /** Session expiry timestamp */ expiresAt: z.string().datetime(), });