/** * Opportunity Domain - Schemas * * Zod schemas for runtime validation of Opportunity data. * Includes cancellation form validation with deadline logic. */ import { z } from "zod"; // ============================================================================ // Enum Value Arrays (for Zod schemas) // ============================================================================ /** * Opportunity stage values - match existing Salesforce picklist */ const OPPORTUNITY_STAGE_VALUES = [ "Introduction", "WIKI", "Ready", "Post Processing", "Active", "△Cancelling", "〇Cancelled", "Completed", "Void", "Pending", ] as const; /** * Application stage values */ const APPLICATION_STAGE_VALUES = ["INTRO-1", "N/A"] as const; /** * Cancellation notice values */ const CANCELLATION_NOTICE_VALUES = ["有", "未", "不要", "移転"] as const; /** * Line return status values */ const LINE_RETURN_STATUS_VALUES = [ "NotYet", "SentKit", "AS/Pickup予定", "Returned1", "Returned2", "NTT派遣", "Compensated", "N/A", ] as const; const OPPORTUNITY_PRODUCT_TYPE_VALUES = ["Internet", "SIM", "VPN"] as const; /** * Commodity type values - existing Salesforce picklist */ const COMMODITY_TYPE_VALUES = [ "Personal SonixNet Home Internet", "Corporate SonixNet Home Internet", "SIM", "VPN", "Onsite Support", ] as const; const OPPORTUNITY_SOURCE_VALUES = [ "Portal - Internet Eligibility Request", "Portal - SIM Checkout Registration", "Portal - Order Placement", "Agent Created", ] as const; // ============================================================================ // Opportunity Record Schema // ============================================================================ /** * Schema for Opportunity record returned from Salesforce */ export const opportunityRecordSchema = z.object({ id: z.string(), name: z.string(), accountId: z.string(), stage: z.enum(OPPORTUNITY_STAGE_VALUES), closeDate: z.string(), commodityType: z.enum(COMMODITY_TYPE_VALUES).optional(), productType: z.enum(OPPORTUNITY_PRODUCT_TYPE_VALUES).optional(), // Derived from commodityType source: z.enum(OPPORTUNITY_SOURCE_VALUES).optional(), applicationStage: z.enum(APPLICATION_STAGE_VALUES).optional(), isClosed: z.boolean(), // Linked entities // Note: Cases and Orders link TO Opportunity via their OpportunityId field whmcsServiceId: z.number().int().optional(), // Cancellation fields (updated by CS when processing cancellation Case) cancellationNotice: z.enum(CANCELLATION_NOTICE_VALUES).optional(), scheduledCancellationDate: z.string().optional(), lineReturnStatus: z.enum(LINE_RETURN_STATUS_VALUES).optional(), // NOTE: alternativeContactEmail and cancellationComments are on the Cancellation Case // Metadata createdDate: z.string(), lastModifiedDate: z.string(), }); export type OpportunityRecord = z.infer; // ============================================================================ // Create Opportunity Request Schema // ============================================================================ /** * Schema for creating a new Opportunity * * Note: Opportunity Name is auto-generated by Salesforce workflow, * so we don't need to provide account name. */ export const createOpportunityRequestSchema = z.object({ accountId: z.string().min(15, "Salesforce Account ID must be at least 15 characters"), productType: z.enum(OPPORTUNITY_PRODUCT_TYPE_VALUES), stage: z.enum(OPPORTUNITY_STAGE_VALUES), source: z.enum(OPPORTUNITY_SOURCE_VALUES), applicationStage: z.enum(APPLICATION_STAGE_VALUES).optional(), closeDate: z.string().optional(), // Note: Create Case separately with Case.OpportunityId = returned Opportunity ID }); export type CreateOpportunityRequest = z.infer; // ============================================================================ // Update Stage Request Schema // ============================================================================ /** * Schema for updating Opportunity stage */ export const updateOpportunityStageRequestSchema = z.object({ opportunityId: z.string().min(15, "Salesforce Opportunity ID must be at least 15 characters"), stage: z.enum(OPPORTUNITY_STAGE_VALUES), reason: z.string().optional(), }); export type UpdateOpportunityStageRequest = z.infer; // ============================================================================ // Cancellation Form Schema // ============================================================================ /** * Regex for YYYY-MM format */ const CANCELLATION_MONTH_REGEX = /^\d{4}-(0[1-9]|1[0-2])$/; /** * Schema for cancellation form data from customer * * Validates: * - cancellationMonth is in YYYY-MM format * - Both confirmations are checked * - Email is valid if provided */ export const cancellationFormDataSchema = z .object({ cancellationMonth: z .string() .regex(CANCELLATION_MONTH_REGEX, "Cancellation month must be in YYYY-MM format"), confirmTermsRead: z.boolean(), confirmMonthEndCancellation: z.boolean(), alternativeEmail: z.string().email().optional().or(z.literal("")), comments: z.string().max(2000, "Comments must be 2000 characters or less").optional(), }) .refine(data => data.confirmTermsRead === true, { message: "You must confirm you have read the cancellation terms", path: ["confirmTermsRead"], }) .refine(data => data.confirmMonthEndCancellation === true, { message: "You must confirm you understand cancellation is at month end", path: ["confirmMonthEndCancellation"], }); export type CancellationFormData = z.infer; /** * Schema for cancellation data to populate on Opportunity * NOTE: alternativeEmail and comments are captured in the Cancellation Case, not Opportunity */ export const cancellationOpportunityDataSchema = z.object({ scheduledCancellationDate: z.string(), cancellationNotice: z.enum(CANCELLATION_NOTICE_VALUES), lineReturnStatus: z.enum(LINE_RETURN_STATUS_VALUES), }); export type CancellationOpportunityData = z.infer; // ============================================================================ // Cancellation Eligibility Schema // ============================================================================ /** * Schema for a cancellation month option */ export const cancellationMonthOptionSchema = z.object({ value: z.string(), label: z.string(), serviceEndDate: z.string(), rentalReturnDeadline: z.string(), isCurrentMonth: z.boolean(), }); export type CancellationMonthOption = z.infer; /** * Schema for cancellation eligibility check result */ export const cancellationEligibilitySchema = z.object({ canCancel: z.boolean(), earliestCancellationMonth: z.string(), availableMonths: z.array(cancellationMonthOptionSchema), currentMonthDeadline: z.string().nullable(), reason: z.string().optional(), }); export type CancellationEligibility = z.infer; // ============================================================================ // Cancellation Status Schema // ============================================================================ /** * Schema for cancellation status display */ export const cancellationStatusSchema = z.object({ stage: z.enum(OPPORTUNITY_STAGE_VALUES), isPending: z.boolean(), isComplete: z.boolean(), scheduledEndDate: z.string().optional(), rentalReturnStatus: z.enum(LINE_RETURN_STATUS_VALUES).optional(), rentalReturnDeadline: z.string().optional(), hasRentalEquipment: z.boolean(), }); export type CancellationStatus = z.infer; // ============================================================================ // Opportunity Lookup Schema // ============================================================================ /** * Schema for Opportunity lookup criteria */ export const opportunityLookupCriteriaSchema = z.object({ accountId: z.string().min(15), productType: z.enum(OPPORTUNITY_PRODUCT_TYPE_VALUES), allowedStages: z.array(z.enum(OPPORTUNITY_STAGE_VALUES)).optional(), }); export type OpportunityLookupCriteria = z.infer; // ============================================================================ // Opportunity Match Result Schema // ============================================================================ /** * Schema for Opportunity matching result */ export const opportunityMatchResultSchema = z.object({ opportunityId: z.string(), wasCreated: z.boolean(), previousStage: z.enum(OPPORTUNITY_STAGE_VALUES).optional(), }); export type OpportunityMatchResult = z.infer; // ============================================================================ // API Response Schemas // ============================================================================ /** * Schema for Opportunity API response */ export const opportunityResponseSchema = z.object({ opportunity: opportunityRecordSchema, }); /** * Schema for create Opportunity response */ export const createOpportunityResponseSchema = z.object({ opportunityId: z.string(), stage: z.enum(OPPORTUNITY_STAGE_VALUES), }); /** * Schema for cancellation preview response */ export const cancellationPreviewResponseSchema = z.object({ eligibility: cancellationEligibilitySchema, terms: z.object({ deadlineDay: z.number(), rentalReturnDeadlineDay: z.number(), fullMonthCharge: z.boolean(), }), });