/** * SIM Domain - Schemas */ import { z } from "zod"; import { simCatalogProductSchema } from "../services/schema.js"; export const simStatusSchema = z.enum(["active", "suspended", "cancelled", "pending"]); export const simTypeSchema = z.enum(["standard", "nano", "micro", "esim"]); export const simDetailsSchema = z.object({ account: z.string(), status: simStatusSchema, planCode: z.string(), planName: z.string(), simType: simTypeSchema, iccid: z.string(), eid: z.string(), msisdn: z.string(), imsi: z.string(), remainingQuotaMb: z.number(), remainingQuotaKb: z.number(), voiceMailEnabled: z.boolean(), callWaitingEnabled: z.boolean(), internationalRoamingEnabled: z.boolean(), networkType: z.string(), activatedAt: z.string().optional(), expiresAt: z.string().optional(), startDate: z.string().optional(), // Optional network/connectivity fields ipv4: z.string().optional(), ipv6: z.string().optional(), // Optional capability flags hasVoice: z.boolean().optional(), hasSms: z.boolean().optional(), }); export const recentDayUsageSchema = z.object({ date: z.string(), usageKb: z.number(), usageMb: z.number(), }); export const simUsageSchema = z.object({ account: z.string(), todayUsageMb: z.number(), todayUsageKb: z.number(), monthlyUsageMb: z.number().optional(), monthlyUsageKb: z.number().optional(), recentDaysUsage: z.array(recentDayUsageSchema), isBlacklisted: z.boolean(), lastUpdated: z.string().optional(), }); export const simTopUpHistoryEntrySchema = z.object({ quotaKb: z.number(), quotaMb: z.number(), addedDate: z.string(), expiryDate: z.string(), campaignCode: z.string(), }); export const simTopUpHistorySchema = z.object({ account: z.string(), totalAdditions: z.number(), additionCount: z.number(), history: z.array(simTopUpHistoryEntrySchema), }); /** * Combined SIM info payload (details + usage) */ export const simInfoSchema = z.object({ details: simDetailsSchema, usage: simUsageSchema, }); // ============================================================================ // SIM Management Request Schemas // ============================================================================ export const simTopUpRequestSchema = z.object({ quotaMb: z .number() .int() .min(100, "Quota must be at least 100MB") .max(51200, "Quota must be 50GB or less"), }); /** * Schema for SIM top-up pricing information */ export const simTopUpPricingSchema = z.object({ pricePerGbJpy: z.number().positive("Price per GB must be positive"), minQuotaMb: z.number().int().positive("Minimum quota must be positive"), maxQuotaMb: z.number().int().positive("Maximum quota must be positive"), currency: z.string().default("JPY"), }); export type SimTopUpPricing = z.infer; /** * Schema for top-up pricing preview request */ export const simTopUpPricingPreviewRequestSchema = z.object({ quotaMb: z.number().int().positive("Quota must be positive"), }); export type SimTopUpPricingPreviewRequest = z.infer; /** * Schema for top-up pricing preview response */ export const simTopUpPricingPreviewResponseSchema = z.object({ quotaMb: z.number(), quotaGb: z.number(), totalPriceJpy: z.number(), pricePerGbJpy: z.number(), currency: z.string(), }); export type SimTopUpPricingPreviewResponse = z.infer; export const simPlanChangeRequestSchema = z.object({ newPlanCode: z.string().min(1, "New plan code is required"), assignGlobalIp: z.boolean().optional(), scheduledAt: z .string() .regex(/^\d{8}$/, "Scheduled date must be in YYYYMMDD format") .optional(), }); export const simCancelRequestSchema = z.object({ scheduledAt: z .string() .regex(/^\d{8}$/, "Scheduled date must be in YYYYMMDD format") .optional(), }); export const simTopUpHistoryRequestSchema = z.object({ fromDate: z.string().regex(/^\d{8}$/, "From date must be in YYYYMMDD format"), toDate: z.string().regex(/^\d{8}$/, "To date must be in YYYYMMDD format"), }); export const simFeaturesUpdateRequestSchema = z.object({ voiceMailEnabled: z.boolean().optional(), callWaitingEnabled: z.boolean().optional(), internationalRoamingEnabled: z.boolean().optional(), networkType: z.enum(["4G", "5G"]).optional(), }); export const simReissueRequestSchema = z.object({ newEid: z .string() .regex(/^\d{32}$/, "EID must be exactly 32 digits") .optional(), simType: z.enum(["physical", "esim"]).optional(), }); // ============================================================================ // Portal-facing SIM DTOs (responses + request contracts) // ============================================================================ /** * Available plan for SIM plan change (portal-facing) * * This extends the catalog's SIM product shape with: * - freebitPlanCode: the actual plan code used by Freebit * - isCurrentPlan: whether this is the customer's current plan */ export const simAvailablePlanSchema = simCatalogProductSchema.extend({ freebitPlanCode: z.string(), isCurrentPlan: z.boolean(), }); export type SimAvailablePlan = z.infer; /** * Cancellation month option for SIM cancellation preview */ export const simCancellationMonthSchema = z.object({ value: z.string().regex(/^\d{4}-\d{2}$/, "Month must be in YYYY-MM format"), label: z.string(), runDate: z.string().regex(/^\d{8}$/, "runDate must be in YYYYMMDD format"), }); export type SimCancellationMonth = z.infer; /** * SIM cancellation preview payload (portal-facing) */ export const simCancellationPreviewSchema = z.object({ simNumber: z.string(), serialNumber: z.string().optional(), planCode: z.string(), startDate: z.string().optional(), minimumContractEndDate: z.string().optional(), isWithinMinimumTerm: z.boolean(), availableMonths: z.array(simCancellationMonthSchema), customerEmail: z.string(), customerName: z.string(), }); export type SimCancellationPreview = z.infer; /** * Reissue SIM request (full flow) for `/api/subscriptions/:id/sim/reissue` * * - Physical SIM: simType="physical" (newEid optional/ignored) * - eSIM: simType="esim" and newEid is required */ export const simReissueFullRequestSchema = z .object({ simType: z.enum(["physical", "esim"]), newEid: z .string() .regex(/^\d{32}$/, "EID must be exactly 32 digits") .optional(), }) .superRefine((data, ctx) => { if (data.simType === "esim" && !data.newEid) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: "newEid is required for eSIM reissue", path: ["newEid"], }); } }); export type SimReissueFullRequest = z.infer; // ============================================================================ // SIM Call/SMS History (portal-facing) // ============================================================================ const simHistoryMonthSchema = z.string().regex(/^\d{4}-\d{2}$/, "Month must be in YYYY-MM format"); const isoDateSchema = z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Date must be in YYYY-MM-DD format"); const timeHmsSchema = z.string().regex(/^\d{2}:\d{2}:\d{2}$/, "Time must be in HH:MM:SS format"); export const simCallHistoryPaginationSchema = z.object({ page: z.number().int().min(1), limit: z.number().int().min(1), total: z.number().int().min(0), totalPages: z.number().int().min(0), }); export type SimCallHistoryPagination = z.infer; export const simDomesticCallRecordSchema = z.object({ id: z.string(), date: isoDateSchema, time: timeHmsSchema, calledTo: z.string(), callLength: z.string(), callCharge: z.number(), }); export type SimDomesticCallRecord = z.infer; export const simDomesticCallHistoryResponseSchema = z.object({ calls: z.array(simDomesticCallRecordSchema), pagination: simCallHistoryPaginationSchema, month: simHistoryMonthSchema, }); export type SimDomesticCallHistoryResponse = z.infer; export const simInternationalCallRecordSchema = z.object({ id: z.string(), date: isoDateSchema, startTime: timeHmsSchema, stopTime: z.string().nullable(), country: z.string().nullable(), calledTo: z.string(), callCharge: z.number(), }); export type SimInternationalCallRecord = z.infer; export const simInternationalCallHistoryResponseSchema = z.object({ calls: z.array(simInternationalCallRecordSchema), pagination: simCallHistoryPaginationSchema, month: simHistoryMonthSchema, }); export type SimInternationalCallHistoryResponse = z.infer< typeof simInternationalCallHistoryResponseSchema >; export const simSmsRecordSchema = z.object({ id: z.string(), date: isoDateSchema, time: timeHmsSchema, sentTo: z.string(), type: z.string(), }); export type SimSmsRecord = z.infer; export const simSmsHistoryResponseSchema = z.object({ messages: z.array(simSmsRecordSchema), pagination: simCallHistoryPaginationSchema, month: simHistoryMonthSchema, }); export type SimSmsHistoryResponse = z.infer; // Enhanced cancellation request with more details export const simCancelFullRequestSchema = z .object({ cancellationMonth: z .string() .regex(/^\d{4}-\d{2}$/, "Cancellation month must be in YYYY-MM format"), confirmRead: z.boolean(), confirmCancel: z.boolean(), alternativeEmail: z.string().email().optional().or(z.literal("")), comments: z.string().max(1000).optional(), }) .refine(data => data.confirmRead === true && data.confirmCancel === true, { message: "You must confirm both checkboxes to proceed", }); // Top-up request with enhanced details for email export const simTopUpFullRequestSchema = z.object({ quotaMb: z .number() .int() .min(100, "Quota must be at least 100MB") .max(51200, "Quota must be 50GB or less"), }); // Change plan request with Salesforce product info export const simChangePlanFullRequestSchema = z.object({ newPlanCode: z.string().min(1, "New plan code is required"), newPlanSku: z.string().min(1, "New plan SKU is required"), newPlanName: z.string().optional(), assignGlobalIp: z.boolean().optional(), }); const simMnpFormSchema = z.object({ reservationNumber: z.string().min(1, "Reservation number is required"), expiryDate: z.string().regex(/^\d{8}$/, "Expiry date must be in YYYYMMDD format"), phoneNumber: z.string().min(1, "Phone number is required"), mvnoAccountNumber: z.string().optional(), portingLastName: z.string().optional(), portingFirstName: z.string().optional(), portingLastNameKatakana: z.string().optional(), portingFirstNameKatakana: z.string().optional(), portingGender: z.string().optional(), portingDateOfBirth: z .string() .regex(/^\d{8}$/, "Birthday must be in YYYYMMDD format") .optional(), }); export const simCardTypeSchema = z.enum(["eSIM", "Physical SIM"]); export const simActivationTypeSchema = z.enum(["Immediate", "Scheduled"]); export const simConfigureFormSchema = z .object({ simType: simCardTypeSchema, eid: z .string() .min(15, "EID must be at least 15 characters") .max(32, "EID must be at most 32 characters") .optional(), selectedAddons: z.array(z.string()).default([]), activationType: simActivationTypeSchema, scheduledActivationDate: z .string() .regex(/^\d{8}$/, "Scheduled date must be in YYYYMMDD format") .optional(), wantsMnp: z.boolean().default(false), mnpData: simMnpFormSchema.optional(), }) .superRefine((data, ctx) => { if (data.simType === "eSIM" && (!data.eid || data.eid.trim().length < 15)) { ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["eid"], message: "EID is required for eSIM configuration", }); } if (data.activationType === "Scheduled" && !data.scheduledActivationDate) { ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["scheduledActivationDate"], message: "Scheduled activation date is required when activation type is Scheduled", }); } if (data.wantsMnp) { if (!data.mnpData) { ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["mnpData"], message: "MNP data is required when porting is selected", }); return; } const { reservationNumber, expiryDate, phoneNumber } = data.mnpData; if (!reservationNumber) { ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["mnpData", "reservationNumber"], message: "Reservation number is required", }); } if (!expiryDate) { ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["mnpData", "expiryDate"], message: "Reservation expiry date is required", }); } if (!phoneNumber) { ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["mnpData", "phoneNumber"], message: "Phone number is required for porting", }); } } }); // ============================================================================ // SIM Order Activation Schemas // ============================================================================ export const simOrderActivationMnpSchema = z.object({ reserveNumber: z.string().min(1, "Reserve number is required"), reserveExpireDate: z.string().regex(/^\d{8}$/, "Reserve expire date must be in YYYYMMDD format"), account: z.string().optional(), firstnameKanji: z.string().optional(), lastnameKanji: z.string().optional(), firstnameZenKana: z.string().optional(), lastnameZenKana: z.string().optional(), gender: z.string().optional(), birthday: z .string() .regex(/^\d{8}$/, "Birthday must be in YYYYMMDD format") .optional(), }); export const simOrderActivationAddonsSchema = z.object({ voiceMail: z.boolean().optional(), callWaiting: z.boolean().optional(), }); export const simOrderActivationRequestSchema = z .object({ planSku: z.string().min(1, "Plan SKU is required"), simType: simCardTypeSchema, eid: z.string().min(15, "EID must be at least 15 characters").optional(), activationType: simActivationTypeSchema, scheduledAt: z .string() .regex(/^\d{8}$/, "Scheduled date must be in YYYYMMDD format") .optional(), addons: simOrderActivationAddonsSchema.optional(), mnp: simOrderActivationMnpSchema.optional(), msisdn: z.string().min(1, "Phone number (msisdn) is required"), oneTimeAmountJpy: z.number().nonnegative("One-time amount must be non-negative"), monthlyAmountJpy: z.number().nonnegative("Monthly amount must be non-negative"), }) .refine( data => { // If simType is eSIM, eid is required if (data.simType === "eSIM" && (!data.eid || data.eid.length < 15)) { return false; } return true; }, { message: "EID is required for eSIM and must be at least 15 characters", path: ["eid"], } ) .refine( data => { // If activationType is Scheduled, scheduledAt is required if (data.activationType === "Scheduled" && !data.scheduledAt) { return false; } return true; }, { message: "Scheduled date is required for Scheduled activation", path: ["scheduledAt"], } ); export type SimOrderActivationRequest = z.infer; export type SimOrderActivationMnp = z.infer; export type SimOrderActivationAddons = z.infer; // Legacy aliases for backward compatibility export const simTopupRequestSchema = simTopUpRequestSchema; export type SimTopupRequest = SimTopUpRequest; export const simChangePlanRequestSchema = simPlanChangeRequestSchema; export type SimChangePlanRequest = SimPlanChangeRequest; export const simFeaturesRequestSchema = simFeaturesUpdateRequestSchema; export type SimFeaturesRequest = SimFeaturesUpdateRequest; // ============================================================================ // Inferred Types from Schemas (Schema-First Approach) // ============================================================================ export type SimStatus = z.infer; export type SimType = z.infer; export type SimDetails = z.infer; export type RecentDayUsage = z.infer; export type SimUsage = z.infer; export type SimTopUpHistoryEntry = z.infer; export type SimTopUpHistory = z.infer; export type SimInfo = z.infer; // Request types (derived from request schemas) export type SimTopUpRequest = z.infer; export type SimPlanChangeRequest = z.infer; export type SimCancelRequest = z.infer; export type SimTopUpHistoryRequest = z.infer; export type SimFeaturesUpdateRequest = z.infer; export type SimReissueRequest = z.infer; export type SimCancelFullRequest = z.infer; export type SimTopUpFullRequest = z.infer; export type SimChangePlanFullRequest = z.infer; export type SimConfigureFormData = z.infer; export type SimCardType = z.infer; export type ActivationType = z.infer; export type MnpData = z.infer; export interface SimConfigureFormToRequestOptions { planSku: string; msisdn: string; monthlyAmountJpy: number; oneTimeAmountJpy: number; addons?: SimOrderActivationAddons; } export function simConfigureFormToRequest( formData: SimConfigureFormData, options: SimConfigureFormToRequestOptions ): SimOrderActivationRequest { const scheduledAt = formData.activationType === "Scheduled" ? formData.scheduledActivationDate : undefined; const mnp = formData.wantsMnp && formData.mnpData ? { reserveNumber: formData.mnpData.reservationNumber, reserveExpireDate: formData.mnpData.expiryDate, account: formData.mnpData.mvnoAccountNumber ?? undefined, firstnameKanji: formData.mnpData.portingFirstName ?? undefined, lastnameKanji: formData.mnpData.portingLastName ?? undefined, firstnameZenKana: formData.mnpData.portingFirstNameKatakana ?? undefined, lastnameZenKana: formData.mnpData.portingLastNameKatakana ?? undefined, gender: formData.mnpData.portingGender ?? undefined, birthday: formData.mnpData.portingDateOfBirth ?? undefined, } : undefined; return simOrderActivationRequestSchema.parse({ planSku: options.planSku, simType: formData.simType, eid: formData.simType === "eSIM" ? formData.eid?.trim() || undefined : undefined, activationType: formData.activationType, scheduledAt, addons: options.addons, mnp, msisdn: options.msisdn, oneTimeAmountJpy: options.oneTimeAmountJpy, monthlyAmountJpy: options.monthlyAmountJpy, }); }