barsa ff9ee10860 Merge main into alt-design
Resolved merge conflicts between main and alt-design branches.

Key decisions:
- BFF: Adopted SIM-first workflow from main (PA05-18 → PA02-01 → PA05-05 → WHMCS)
- BFF: Kept FreebitFacade pattern, added new services (AccountRegistration, VoiceOptions, SemiBlack)
- BFF: Fixed freebit-usage.service.ts bug (quotaKb → quotaMb)
- BFF: Merged rate limiting + HTTP status parsing in WHMCS error handler
- Portal: Took main's UI implementations
- Deleted: TV page, SignupForm, ServicesGrid (as per main)
- Added whmcsRegistrationUrl to field-maps.ts (was missing after file consolidation)

TODO post-merge:
- Refactor order-fulfillment-orchestrator.service.ts to use buildTransactionSteps abstraction
- Fix ESLint errors from main's code (skipped pre-commit for merge)
2026-02-03 16:12:05 +09:00

348 lines
13 KiB
TypeScript

/**
* SIM Domain - Freebit Provider Request Schemas
*
* Zod schemas for all Freebit API request payloads.
*/
import { z } from "zod";
// ============================================================================
// Validation Constants
// ============================================================================
const MSG_ACCOUNT_REQUIRED = "Account is required";
const YYYYMMDD_REGEX = /^\d{8}$/;
const MSG_SHIP_DATE_FORMAT = "Ship date must be in YYYYMMDD format";
// ============================================================================
// Account Details & Traffic Info
// ============================================================================
export const freebitAccountDetailsRequestSchema = z.object({
version: z.string().optional(),
requestDatas: z
.array(
z.object({
kind: z.enum(["MASTER", "MVNO"]),
account: z.union([z.string(), z.number()]).optional(),
})
)
.min(1, "At least one request data entry is required"),
});
export const freebitTrafficInfoRequestSchema = z.object({
account: z.string().min(1, MSG_ACCOUNT_REQUIRED),
});
// ============================================================================
// Top-Up
// ============================================================================
export const freebitTopUpOptionsSchema = z.object({
campaignCode: z.string().optional(),
expiryDate: z.string().optional(),
scheduledAt: z.string().optional(),
});
export const freebitTopUpRequestPayloadSchema = z.object({
account: z.string().min(1, MSG_ACCOUNT_REQUIRED),
quotaMb: z.number().positive("Quota must be positive"),
options: freebitTopUpOptionsSchema.optional(),
});
export const freebitTopUpApiRequestSchema = z.object({
account: z.string().min(1, MSG_ACCOUNT_REQUIRED),
quota: z.number().positive("Quota must be positive"),
quotaCode: z.string().optional(),
expire: z.string().optional(),
runTime: z.string().optional(),
});
// ============================================================================
// Plan Change & Cancellation
// ============================================================================
export const freebitPlanChangeRequestSchema = z.object({
account: z.string().min(1, MSG_ACCOUNT_REQUIRED),
newPlanCode: z.string().min(1, "New plan code is required"),
assignGlobalIp: z.boolean().optional(),
scheduledAt: z.string().optional(),
});
export const freebitPlanChangeApiRequestSchema = z.object({
account: z.string().min(1, MSG_ACCOUNT_REQUIRED),
plancode: z.string().min(1, "Plan code is required"),
globalip: z.enum(["0", "1"]).optional(),
runTime: z.string().optional(),
});
export const freebitAddSpecRequestSchema = z.object({
account: z.string().min(1, MSG_ACCOUNT_REQUIRED),
specCode: z.string().min(1, "Spec code is required"),
enabled: z.boolean().optional(),
networkType: z.enum(["4G", "5G"]).optional(),
});
export const freebitRemoveSpecRequestSchema = z.object({
account: z.string().min(1, MSG_ACCOUNT_REQUIRED),
specCode: z.string().min(1, "Spec code is required"),
});
export const freebitCancelPlanRequestSchema = z.object({
account: z.string().min(1, MSG_ACCOUNT_REQUIRED),
runDate: z.string().optional(),
});
export const freebitCancelPlanApiRequestSchema = z.object({
account: z.string().min(1, MSG_ACCOUNT_REQUIRED),
runTime: z.string().optional(),
});
export const freebitQuotaHistoryRequestSchema = z.object({
account: z.string().min(1, MSG_ACCOUNT_REQUIRED),
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 freebitQuotaHistoryResponseSchema = z.object({
resultCode: z.string(),
status: z
.object({
message: z.string(),
statusCode: z.union([z.string(), z.number()]),
})
.optional(),
total: z.union([z.string(), z.number()]),
count: z.union([z.string(), z.number()]),
quotaHistory: z.array(
z.object({
addQuotaKb: z.union([z.string(), z.number()]),
addDate: z.string(),
expireDate: z.string(),
campaignCode: z.string().optional(),
})
),
});
export const freebitEsimMnpSchema = 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"),
});
export const freebitEsimReissueRequestSchema = z.object({
account: z.string().min(1, MSG_ACCOUNT_REQUIRED),
newEid: z.string().min(1, "New EID is required"),
oldEid: z.string().optional(),
planCode: z.string().optional(),
oldProductNumber: z.string().optional(),
});
export const freebitEsimAddAccountRequestSchema = z.object({
authKey: z.string().min(1).optional(),
aladinOperated: z.enum(["10", "20"]).default("10"),
account: z.string().min(1, MSG_ACCOUNT_REQUIRED),
eid: z.string().min(1, "EID is required"),
addKind: z.enum(["N", "R"]).default("N"),
shipDate: z.string().regex(YYYYMMDD_REGEX, MSG_SHIP_DATE_FORMAT).optional(),
planCode: z.string().optional(),
contractLine: z.enum(["4G", "5G"]).optional(),
mnp: freebitEsimMnpSchema.optional(),
});
// =========================================================================
// SIM Features
// =========================================================================
export const freebitSimFeaturesRequestSchema = z.object({
account: z.string().min(1, MSG_ACCOUNT_REQUIRED),
voiceMailEnabled: z.boolean().optional(),
callWaitingEnabled: z.boolean().optional(),
callForwardingEnabled: z.boolean().optional(),
callerIdEnabled: z.boolean().optional(),
});
export const freebitGlobalIpRequestSchema = z.object({
account: z.string().min(1, MSG_ACCOUNT_REQUIRED),
assign: z.boolean(), // true to assign, false to remove
});
// =========================================================================
// eSIM Activation
// =========================================================================
export const freebitAuthRequestSchema = z.object({
oemId: z.string().min(1),
oemKey: z.string().min(1),
});
export const freebitCancelAccountRequestSchema = z.object({
account: z.string().min(1),
runDate: z.string().optional(),
});
export const freebitEsimIdentitySchema = z.object({
firstnameKanji: z.string().optional(),
lastnameKanji: z.string().optional(),
firstnameZenKana: z.string().optional(),
lastnameZenKana: z.string().optional(),
gender: z.enum(["M", "F"]).optional(),
birthday: z
.string()
.regex(/^\d{8}$/, "Birthday must be in YYYYMMDD format")
.optional(),
});
/**
* Freebit eSIM Account Activation Request Schema
* PA05-41 (addAcct) API endpoint
*/
export const freebitEsimActivationRequestSchema = z.object({
authKey: z.string().min(1, "Auth key is required"),
aladinOperated: z.enum(["10", "20"]).default("10"), // 10: issue profile, 20: no-issue
createType: z.enum(["new", "reissue", "exchange"]).default("new"),
account: z.string().min(1, "Account (MSISDN) is required"),
eid: z.string().min(1, "EID is required for eSIM"),
simkind: z.enum(["esim", "psim"]).default("esim"),
planCode: z.string().optional(),
contractLine: z.enum(["4G", "5G"]).optional(),
shipDate: z.string().regex(YYYYMMDD_REGEX, MSG_SHIP_DATE_FORMAT).optional(),
mnp: freebitEsimMnpSchema.optional(),
// Identity fields (flattened for API)
firstnameKanji: z.string().optional(),
lastnameKanji: z.string().optional(),
firstnameZenKana: z.string().optional(),
lastnameZenKana: z.string().optional(),
gender: z.enum(["M", "F"]).optional(),
birthday: z
.string()
.regex(/^\d{8}$/, "Birthday must be in YYYYMMDD format")
.optional(),
// Additional fields for reissue/exchange
masterAccount: z.string().optional(),
masterPassword: z.string().optional(),
repAccount: z.string().optional(),
size: z.string().optional(),
addKind: z.string().optional(), // 'R' for reissue
oldEid: z.string().optional(),
oldProductNumber: z.string().optional(),
deliveryCode: z.string().optional(),
globalIp: z.enum(["10", "20"]).optional(), // 10: none, 20: with global IP
});
/**
* Freebit eSIM Activation Response Schema
* Note: The 'data' field type varies by API version and is not used in production.
* Using z.unknown() for type safety while allowing any shape if present.
*/
export const freebitEsimActivationResponseSchema = z.object({
resultCode: z.string(),
resultMessage: z.string().optional(),
data: z.unknown().optional(),
status: z
.object({
statusCode: z.union([z.string(), z.number()]),
message: z.string(),
})
.optional(),
message: z.string().optional(),
});
/**
* Higher-level eSIM activation parameters schema
* Used for business logic layer before mapping to API request
*/
export const freebitEsimActivationParamsSchema = z.object({
account: z.string().min(1, MSG_ACCOUNT_REQUIRED),
eid: z.string().min(1, "EID is required"),
planCode: z.string().optional(),
contractLine: z.enum(["4G", "5G"]).optional(),
aladinOperated: z.enum(["10", "20"]).default("10"),
shipDate: z.string().regex(YYYYMMDD_REGEX, MSG_SHIP_DATE_FORMAT).optional(),
mnp: freebitEsimMnpSchema.optional(),
identity: freebitEsimIdentitySchema.optional(),
});
// ============================================================================
// Type Exports
// ============================================================================
export type FreebitAccountDetailsRequest = z.infer<typeof freebitAccountDetailsRequestSchema>;
export type FreebitTrafficInfoRequest = z.infer<typeof freebitTrafficInfoRequestSchema>;
export type FreebitTopUpRequest = z.infer<typeof freebitTopUpRequestPayloadSchema>;
export type FreebitTopUpApiRequest = z.infer<typeof freebitTopUpApiRequestSchema>;
export type FreebitPlanChangeRequest = z.infer<typeof freebitPlanChangeRequestSchema>;
export type FreebitPlanChangeApiRequest = z.infer<typeof freebitPlanChangeApiRequestSchema>;
export type FreebitAddSpecRequest = z.infer<typeof freebitAddSpecRequestSchema>;
export type FreebitRemoveSpecRequest = z.infer<typeof freebitRemoveSpecRequestSchema>;
export type FreebitCancelPlanRequest = z.infer<typeof freebitCancelPlanRequestSchema>;
export type FreebitCancelPlanApiRequest = z.infer<typeof freebitCancelPlanApiRequestSchema>;
export type FreebitSimFeaturesRequest = z.infer<typeof freebitSimFeaturesRequestSchema>;
export type FreebitGlobalIpRequest = z.infer<typeof freebitGlobalIpRequestSchema>;
export type FreebitEsimActivationRequest = z.infer<typeof freebitEsimActivationRequestSchema>;
export type FreebitEsimActivationResponse = z.infer<typeof freebitEsimActivationResponseSchema>;
export type FreebitEsimActivationParams = z.infer<typeof freebitEsimActivationParamsSchema>;
export type FreebitEsimReissueRequest = z.infer<typeof freebitEsimReissueRequestSchema>;
export type FreebitQuotaHistoryRequest = z.infer<typeof freebitQuotaHistoryRequestSchema>;
export type FreebitQuotaHistoryResponse = z.infer<typeof freebitQuotaHistoryResponseSchema>;
export type FreebitEsimAddAccountRequest = z.infer<typeof freebitEsimAddAccountRequestSchema>;
export type FreebitAuthRequest = z.infer<typeof freebitAuthRequestSchema>;
export type FreebitCancelAccountRequest = z.infer<typeof freebitCancelAccountRequestSchema>;
// ============================================================================
// Physical SIM OTA Activation (PA05-33)
// ============================================================================
/**
* Freebit OTA Account Activation Request Schema
* PA05-33 API endpoint: /mvno/ota/addAcnt/
* Used for Physical SIM activation via OTA (Over-The-Air)
*/
export const freebitOtaActivationRequestSchema = z.object({
authKey: z.string().min(1, "Auth key is required"),
aladinOperated: z.enum(["10", "20"]).default("10"), // 10: issue profile, 20: no-issue
createType: z.enum(["new", "reissue"]).default("new"),
account: z.string().min(1, "Account (MSISDN) is required"),
productNumber: z.string().min(1, "Product number (PT) is required"),
simkind: z.string().optional(), // Physical SIM kind (e.g., '3MS', '3MR')
planCode: z.string().optional(),
contractLine: z.enum(["4G", "5G"]).optional(),
size: z.enum(["standard", "micro", "nano"]).default("nano"),
shipDate: z
.string()
.regex(/^\d{8}$/, "Ship date must be in YYYYMMDD format")
.optional(),
deliveryCode: z.string().optional(), // OEM ID code
addKind: z.enum(["N", "M"]).default("N"), // N: New, M: MNP
mnp: z
.object({
reserveNumber: z.string().min(1, "MNP reserve number is required"),
reserveExpireDate: z.string().regex(/^\d{8}$/, "Reserve expire date must be YYYYMMDD"),
account: z.string().optional(),
firstnameKanji: z.string().optional(),
lastnameKanji: z.string().optional(),
firstnameZenKana: z.string().optional(),
lastnameZenKana: z.string().optional(),
gender: z.enum(["M", "F"]).optional(),
birthday: z.string().optional(),
})
.optional(),
});
/**
* Freebit OTA Account Activation Response Schema
*/
export const freebitOtaActivationResponseSchema = z.object({
resultCode: z.string(),
resultMessage: z.string().optional(),
status: z
.object({
statusCode: z.union([z.string(), z.number()]),
message: z.string(),
})
.optional(),
message: z.string().optional(),
});
export type FreebitOtaActivationRequest = z.infer<typeof freebitOtaActivationRequestSchema>;
export type FreebitOtaActivationResponse = z.infer<typeof freebitOtaActivationResponseSchema>;