barsa 1d1602f5e7 feat: Implement unified eligibility check flow with inline OTP verification
- Refactor PublicLandingView to enhance service section animations.
- Update SimPlansContent and PublicEligibilityCheck to streamline service highlights.
- Revise PublicEligibilityCheck to support new flow: "Send Request Only" and "Continue to Create Account".
- Introduce guest eligibility check API with handoff token for account creation.
- Modify success step to provide clear options for account creation and navigation.
- Enhance form handling and error management in PublicEligibilityCheckView.
- Update domain schemas to accommodate guest eligibility requests and responses.
- Document new eligibility check flows and testing procedures.
2026-01-14 17:14:07 +09:00

300 lines
9.2 KiB
TypeScript

/**
* 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(),
});
// ============================================================================
// Quick Eligibility Check Schemas
// ============================================================================
/**
* 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)");
/**
* Request for quick eligibility check (guest flow)
* Minimal data required to create SF Account and check eligibility
*/
export const quickEligibilityRequestSchema = z.object({
/** Session token from email verification */
sessionToken: z.string().min(1, "Session token is required"),
/** Customer first name */
firstName: nameSchema,
/** Customer last name */
lastName: nameSchema,
/** Full address for eligibility check */
address: addressFormSchema,
/** Optional phone number */
phone: phoneSchema.optional(),
});
/**
* Response from quick eligibility check
*/
export const quickEligibilityResponseSchema = 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(),
});
// ============================================================================
// Guest Eligibility Check Schemas (No OTP Required)
// ============================================================================
/**
* 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 for eligibility check */
address: addressFormSchema,
/** 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
* Creates WHMCS client and Portal user, links to existing SF Account
*/
export const completeAccountRequestSchema = z.object({
/** Session token from verified email */
sessionToken: z.string().min(1, "Session token is required"),
/** 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(),
});
// ============================================================================
// "Maybe Later" Flow Schemas
// ============================================================================
/**
* Request for "Maybe Later" flow
* Creates SF Account and eligibility case, customer can return later
*/
export const maybeLaterRequestSchema = z.object({
/** Session token from email verification */
sessionToken: z.string().min(1, "Session token is required"),
/** Customer first name */
firstName: nameSchema,
/** Customer last name */
lastName: nameSchema,
/** Full address for eligibility check */
address: addressFormSchema,
/** Optional phone number */
phone: phoneSchema.optional(),
});
/**
* Response from "Maybe Later" flow
*/
export const maybeLaterResponseSchema = z.object({
/** Whether the SF account and case were created */
success: z.boolean(),
/** Case ID for the eligibility request */
requestId: z.string().optional(),
/** Message to display */
message: z.string(),
});
// ============================================================================
// 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(),
});