649 lines
21 KiB
TypeScript
Raw Normal View History

/**
* 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<typeof simTopUpPricingSchema>;
/**
* 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<typeof simTopUpPricingPreviewRequestSchema>;
/**
* 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<typeof simTopUpPricingPreviewResponseSchema>;
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<typeof simAvailablePlanSchema>;
export const simAvailablePlanArraySchema = z.array(simAvailablePlanSchema);
export type SimAvailablePlanArray = z.infer<typeof simAvailablePlanArraySchema>;
/**
* 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<typeof simCancellationMonthSchema>;
/**
* 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<typeof simCancellationPreviewSchema>;
/**
* 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<typeof simReissueFullRequestSchema>;
// ============================================================================
// SIM Call/SMS History (portal-facing)
// ============================================================================
export const simHistoryMonthSchema = z
.string()
.regex(/^\d{4}-\d{2}$/, "Month must be in YYYY-MM format");
export type SimHistoryMonth = z.infer<typeof simHistoryMonthSchema>;
export const simHistoryAvailableMonthsSchema = z.array(simHistoryMonthSchema);
export type SimHistoryAvailableMonths = z.infer<typeof simHistoryAvailableMonthsSchema>;
export const simCallHistoryImportResultSchema = z.object({
domestic: z.number().int().min(0),
international: z.number().int().min(0),
sms: z.number().int().min(0),
});
export type SimCallHistoryImportResult = z.infer<typeof simCallHistoryImportResultSchema>;
export const simSftpFilesSchema = z.array(z.string());
export type SimSftpFiles = z.infer<typeof simSftpFilesSchema>;
export const simSftpListResultSchema = z.object({
path: z.string(),
files: simSftpFilesSchema,
});
export type SimSftpListResult = z.infer<typeof simSftpListResultSchema>;
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<typeof simCallHistoryPaginationSchema>;
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<typeof simDomesticCallRecordSchema>;
export const simDomesticCallHistoryResponseSchema = z.object({
calls: z.array(simDomesticCallRecordSchema),
pagination: simCallHistoryPaginationSchema,
month: simHistoryMonthSchema,
});
export type SimDomesticCallHistoryResponse = z.infer<typeof simDomesticCallHistoryResponseSchema>;
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<typeof simInternationalCallRecordSchema>;
export const simInternationalCallHistoryResponseSchema = z.object({
calls: z.array(simInternationalCallRecordSchema),
pagination: simCallHistoryPaginationSchema,
month: simHistoryMonthSchema,
});
export type SimInternationalCallHistoryResponse = z.infer<
typeof simInternationalCallHistoryResponseSchema
>;
/**
* Schema for SIM history query parameters (pagination + month)
*/
export const simHistoryQuerySchema = z.object({
month: simHistoryMonthSchema.optional(),
page: z.coerce.number().int().min(1).optional().default(1),
limit: z.coerce.number().int().min(1).max(100).optional().default(50),
});
export type SimHistoryQuery = z.infer<typeof simHistoryQuerySchema>;
/**
* Schema for SFTP file listing query
*/
export const simSftpListQuerySchema = z.object({
path: z.string().startsWith("/home/PASI", "Invalid path").default("/home/PASI"),
});
export type SimSftpListQuery = z.infer<typeof simSftpListQuerySchema>;
/**
* Schema for call history import query
*/
export const simCallHistoryImportQuerySchema = z.object({
month: z.string().regex(/^\d{6}$/, "Invalid month format (expected YYYYMM)"),
});
export type SimCallHistoryImportQuery = z.infer<typeof simCallHistoryImportQuerySchema>;
/**
* Schema for SIM eSIM reissue request
*/
export const simReissueEsimRequestSchema = z.object({
newEid: z
.string()
.regex(/^\d{32}$/, "EID must be exactly 32 digits")
.optional(),
});
export type SimReissueEsimRequest = z.infer<typeof simReissueEsimRequestSchema>;
export const simSmsRecordSchema = z.object({
id: z.string(),
date: isoDateSchema,
time: timeHmsSchema,
sentTo: z.string(),
type: z.string(),
});
export type SimSmsRecord = z.infer<typeof simSmsRecordSchema>;
export const simSmsHistoryResponseSchema = z.object({
messages: z.array(simSmsRecordSchema),
pagination: simCallHistoryPaginationSchema,
month: simHistoryMonthSchema,
});
export type SimSmsHistoryResponse = z.infer<typeof simSmsHistoryResponseSchema>;
// 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<typeof simOrderActivationRequestSchema>;
export type SimOrderActivationMnp = z.infer<typeof simOrderActivationMnpSchema>;
export type SimOrderActivationAddons = z.infer<typeof simOrderActivationAddonsSchema>;
// 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<typeof simStatusSchema>;
export type SimType = z.infer<typeof simTypeSchema>;
export type SimDetails = z.infer<typeof simDetailsSchema>;
export type RecentDayUsage = z.infer<typeof recentDayUsageSchema>;
export type SimUsage = z.infer<typeof simUsageSchema>;
export type SimTopUpHistoryEntry = z.infer<typeof simTopUpHistoryEntrySchema>;
export type SimTopUpHistory = z.infer<typeof simTopUpHistorySchema>;
export type SimInfo = z.infer<typeof simInfoSchema>;
// Request types (derived from request schemas)
export type SimTopUpRequest = z.infer<typeof simTopUpRequestSchema>;
export type SimPlanChangeRequest = z.infer<typeof simPlanChangeRequestSchema>;
export type SimCancelRequest = z.infer<typeof simCancelRequestSchema>;
export type SimTopUpHistoryRequest = z.infer<typeof simTopUpHistoryRequestSchema>;
export type SimFeaturesUpdateRequest = z.infer<typeof simFeaturesUpdateRequestSchema>;
export type SimReissueRequest = z.infer<typeof simReissueRequestSchema>;
export type SimCancelFullRequest = z.infer<typeof simCancelFullRequestSchema>;
export type SimTopUpFullRequest = z.infer<typeof simTopUpFullRequestSchema>;
export type SimChangePlanFullRequest = z.infer<typeof simChangePlanFullRequestSchema>;
export type SimConfigureFormData = z.infer<typeof simConfigureFormSchema>;
export type SimCardType = z.infer<typeof simCardTypeSchema>;
export type ActivationType = z.infer<typeof simActivationTypeSchema>;
export type MnpData = z.infer<typeof simMnpFormSchema>;
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,
});
}