- Implemented FormStep component for user input (name, email, address). - Created OtpStep component for OTP verification. - Developed SuccessStep component to display success messages based on account creation. - Introduced eligibility-check.store for managing state throughout the eligibility check process. - Added commitlint configuration for standardized commit messages. - Configured knip for workspace management and project structure.
346 lines
11 KiB
TypeScript
346 lines
11 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(),
|
|
});
|
|
|
|
// ============================================================================
|
|
// 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(),
|
|
});
|
|
|
|
// ============================================================================
|
|
// 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(),
|
|
});
|