barsa d5e22f14f5 feat: add address reconciliation queue service for Salesforce integration
- Implement AddressReconcileQueueService to handle address reconciliation jobs between WHMCS and Salesforce.
- Define job data structure and queue configuration for retries and error handling.
- Add methods for enqueueing reconciliation jobs and retrieving queue health metrics.

feat: create loading components for various services in the portal

- Add loading skeletons for Internet, SIM, VPN, and public services configuration.
- Implement loading states for account-related views including account details, services, and verification settings.
- Introduce loading states for support case details and subscription actions.

feat: implement OTP input component for user verification

- Create OtpInput component to handle 6-digit OTP input with auto-focus and navigation.
- Add LoginOtpStep component for OTP verification during login, including countdown timer and error handling.

feat: define address domain constants for validation

- Establish constants for address field length limits to ensure compliance with WHMCS API constraints.
- Include maximum lengths for address fields and user input fields to maintain data integrity.
2026-02-03 11:48:49 +09:00

346 lines
12 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";
// ============================================================================
// Validation Message Constants
// ============================================================================
const SESSION_TOKEN_REQUIRED = "Session token is required";
const ACCEPT_TERMS_REQUIRED = "You must accept the terms of service";
// ============================================================================
// 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_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: ACCEPT_TERMS_REQUIRED,
}),
/** 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_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: ACCEPT_TERMS_REQUIRED,
}),
/** 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_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: ACCEPT_TERMS_REQUIRED,
}),
/** 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(),
});