306 lines
9.5 KiB
TypeScript
Raw Normal View History

/**
* 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<typeof opportunityRecordSchema>;
// ============================================================================
// 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<typeof createOpportunityRequestSchema>;
// ============================================================================
// 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<typeof updateOpportunityStageRequestSchema>;
// ============================================================================
// 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<typeof cancellationFormDataSchema>;
/**
* 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<typeof cancellationOpportunityDataSchema>;
// ============================================================================
// 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<typeof cancellationMonthOptionSchema>;
/**
* 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<typeof cancellationEligibilitySchema>;
// ============================================================================
// 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<typeof cancellationStatusSchema>;
// ============================================================================
// 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<typeof opportunityLookupCriteriaSchema>;
// ============================================================================
// 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<typeof opportunityMatchResultSchema>;
// ============================================================================
// 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(),
}),
});