2025-09-19 16:34:10 +09:00
|
|
|
/**
|
|
|
|
|
* API Request Schemas
|
|
|
|
|
* Schemas for data that the backend receives and validates
|
|
|
|
|
* These are the "source of truth" for business logic validation
|
|
|
|
|
*/
|
|
|
|
|
|
2025-09-24 18:00:49 +09:00
|
|
|
import { z } from "zod";
|
2025-09-19 16:34:10 +09:00
|
|
|
import {
|
|
|
|
|
emailSchema,
|
|
|
|
|
passwordSchema,
|
|
|
|
|
nameSchema,
|
|
|
|
|
phoneSchema,
|
|
|
|
|
addressSchema,
|
|
|
|
|
requiredAddressSchema,
|
|
|
|
|
genderEnum,
|
2025-09-24 18:00:49 +09:00
|
|
|
} from "../shared/primitives";
|
2025-09-19 16:34:10 +09:00
|
|
|
|
2025-10-02 16:33:25 +09:00
|
|
|
const invoiceStatusEnum = z.enum(["Paid", "Unpaid", "Overdue", "Cancelled", "Collections"]);
|
|
|
|
|
const subscriptionStatusEnum = z.enum([
|
|
|
|
|
"Active",
|
|
|
|
|
"Suspended",
|
|
|
|
|
"Terminated",
|
|
|
|
|
"Cancelled",
|
|
|
|
|
"Pending",
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
export const paginationQuerySchema = z.object({
|
|
|
|
|
page: z.coerce.number().int().min(1).default(1),
|
|
|
|
|
limit: z.coerce.number().int().min(1).max(100).default(10),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export type PaginationQuery = z.infer<typeof paginationQuerySchema>;
|
|
|
|
|
|
|
|
|
|
export const auditLogQuerySchema = paginationQuerySchema.extend({
|
|
|
|
|
action: z.string().optional(),
|
|
|
|
|
userId: z.string().uuid().optional(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export type AuditLogQuery = z.infer<typeof auditLogQuerySchema>;
|
|
|
|
|
|
2025-10-02 17:19:39 +09:00
|
|
|
export const dryRunQuerySchema = z.object({
|
|
|
|
|
dryRun: z.coerce.boolean().optional(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export type DryRunQuery = z.infer<typeof dryRunQuerySchema>;
|
|
|
|
|
|
2025-10-02 16:33:25 +09:00
|
|
|
export const invoiceListQuerySchema = paginationQuerySchema.extend({
|
|
|
|
|
status: invoiceStatusEnum.optional(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export type InvoiceListQuery = z.infer<typeof invoiceListQuerySchema>;
|
|
|
|
|
|
|
|
|
|
export const subscriptionQuerySchema = z.object({
|
|
|
|
|
status: subscriptionStatusEnum.optional(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export type SubscriptionQuery = z.infer<typeof subscriptionQuerySchema>;
|
|
|
|
|
|
2025-09-19 16:34:10 +09:00
|
|
|
// =====================================================
|
|
|
|
|
// AUTH REQUEST SCHEMAS
|
|
|
|
|
// =====================================================
|
|
|
|
|
|
|
|
|
|
export const loginRequestSchema = z.object({
|
|
|
|
|
email: emailSchema,
|
2025-09-24 18:00:49 +09:00
|
|
|
password: z.string().min(1, "Password is required"),
|
2025-09-19 16:34:10 +09:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export const signupRequestSchema = z.object({
|
|
|
|
|
email: emailSchema,
|
|
|
|
|
password: passwordSchema,
|
|
|
|
|
firstName: nameSchema,
|
|
|
|
|
lastName: nameSchema,
|
|
|
|
|
company: z.string().optional(),
|
|
|
|
|
phone: phoneSchema,
|
2025-09-24 18:00:49 +09:00
|
|
|
sfNumber: z.string().min(6, "Customer number must be at least 6 characters"),
|
2025-09-19 16:34:10 +09:00
|
|
|
address: requiredAddressSchema,
|
|
|
|
|
nationality: z.string().optional(),
|
2025-09-24 18:00:49 +09:00
|
|
|
dateOfBirth: z.string().optional(),
|
2025-09-19 16:34:10 +09:00
|
|
|
gender: genderEnum.optional(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export const passwordResetRequestSchema = z.object({
|
|
|
|
|
email: emailSchema,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export const passwordResetSchema = z.object({
|
2025-09-24 18:00:49 +09:00
|
|
|
token: z.string().min(1, "Reset token is required"),
|
2025-09-19 16:34:10 +09:00
|
|
|
password: passwordSchema,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export const setPasswordRequestSchema = z.object({
|
|
|
|
|
email: emailSchema,
|
|
|
|
|
password: passwordSchema,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export const changePasswordRequestSchema = z.object({
|
2025-09-24 18:00:49 +09:00
|
|
|
currentPassword: z.string().min(1, "Current password is required"),
|
2025-09-19 16:34:10 +09:00
|
|
|
newPassword: passwordSchema,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export const linkWhmcsRequestSchema = z.object({
|
|
|
|
|
email: emailSchema,
|
2025-09-24 18:00:49 +09:00
|
|
|
password: z.string().min(1, "Password is required"),
|
2025-09-19 16:34:10 +09:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export const validateSignupRequestSchema = z.object({
|
2025-09-24 18:00:49 +09:00
|
|
|
sfNumber: z.string().min(1, "Customer number is required"),
|
2025-09-19 16:34:10 +09:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export const accountStatusRequestSchema = z.object({
|
|
|
|
|
email: emailSchema,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export const ssoLinkRequestSchema = z.object({
|
|
|
|
|
destination: z.string().optional(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export const checkPasswordNeededRequestSchema = z.object({
|
|
|
|
|
email: emailSchema,
|
|
|
|
|
});
|
|
|
|
|
|
2025-09-20 11:35:40 +09:00
|
|
|
export const refreshTokenRequestSchema = z.object({
|
2025-09-26 16:30:00 +09:00
|
|
|
refreshToken: z.string().min(1, "Refresh token is required").optional(),
|
2025-09-20 11:35:40 +09:00
|
|
|
deviceId: z.string().optional(),
|
|
|
|
|
});
|
|
|
|
|
|
2025-09-19 16:34:10 +09:00
|
|
|
// =====================================================
|
|
|
|
|
// TYPE EXPORTS
|
|
|
|
|
// =====================================================
|
|
|
|
|
|
|
|
|
|
export type LoginRequestInput = z.infer<typeof loginRequestSchema>;
|
|
|
|
|
export type SignupRequestInput = z.infer<typeof signupRequestSchema>;
|
|
|
|
|
export type PasswordResetRequestInput = z.infer<typeof passwordResetRequestSchema>;
|
|
|
|
|
export type PasswordResetInput = z.infer<typeof passwordResetSchema>;
|
|
|
|
|
export type SetPasswordRequestInput = z.infer<typeof setPasswordRequestSchema>;
|
|
|
|
|
export type ChangePasswordRequestInput = z.infer<typeof changePasswordRequestSchema>;
|
|
|
|
|
export type LinkWhmcsRequestInput = z.infer<typeof linkWhmcsRequestSchema>;
|
|
|
|
|
export type ValidateSignupRequestInput = z.infer<typeof validateSignupRequestSchema>;
|
|
|
|
|
export type AccountStatusRequestInput = z.infer<typeof accountStatusRequestSchema>;
|
|
|
|
|
export type SsoLinkRequestInput = z.infer<typeof ssoLinkRequestSchema>;
|
|
|
|
|
export type CheckPasswordNeededRequestInput = z.infer<typeof checkPasswordNeededRequestSchema>;
|
2025-09-20 11:35:40 +09:00
|
|
|
export type RefreshTokenRequestInput = z.infer<typeof refreshTokenRequestSchema>;
|
2025-09-19 16:34:10 +09:00
|
|
|
|
|
|
|
|
// =====================================================
|
|
|
|
|
// USER MANAGEMENT REQUEST SCHEMAS
|
|
|
|
|
// =====================================================
|
|
|
|
|
|
|
|
|
|
export const updateProfileRequestSchema = z.object({
|
|
|
|
|
firstName: nameSchema.optional(),
|
|
|
|
|
lastName: nameSchema.optional(),
|
|
|
|
|
phone: phoneSchema.optional(),
|
|
|
|
|
company: z.string().max(200).optional(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export const updateAddressRequestSchema = addressSchema;
|
|
|
|
|
|
|
|
|
|
// =====================================================
|
|
|
|
|
// ORDER REQUEST SCHEMAS
|
|
|
|
|
// =====================================================
|
|
|
|
|
|
|
|
|
|
export const orderConfigurationsSchema = z.object({
|
|
|
|
|
// Activation (All order types)
|
2025-09-24 18:00:49 +09:00
|
|
|
activationType: z.enum(["Immediate", "Scheduled"]).optional(),
|
2025-09-19 16:34:10 +09:00
|
|
|
scheduledAt: z.string().datetime().optional(),
|
|
|
|
|
|
|
|
|
|
// Internet specific
|
2025-09-24 18:00:49 +09:00
|
|
|
accessMode: z.enum(["IPoE-BYOR", "IPoE-HGW", "PPPoE"]).optional(),
|
2025-09-19 16:34:10 +09:00
|
|
|
|
|
|
|
|
// SIM specific
|
2025-09-24 18:00:49 +09:00
|
|
|
simType: z.enum(["eSIM", "Physical SIM"]).optional(),
|
2025-09-19 16:34:10 +09:00
|
|
|
eid: z.string().optional(), // Required for eSIM
|
|
|
|
|
|
|
|
|
|
// MNP/Porting
|
|
|
|
|
isMnp: z.string().optional(), // "true" | "false"
|
|
|
|
|
mnpNumber: z.string().optional(),
|
|
|
|
|
mnpExpiry: z.string().optional(),
|
|
|
|
|
mnpPhone: z.string().optional(),
|
|
|
|
|
mvnoAccountNumber: z.string().optional(),
|
|
|
|
|
portingLastName: z.string().optional(),
|
|
|
|
|
portingFirstName: z.string().optional(),
|
|
|
|
|
portingLastNameKatakana: z.string().optional(),
|
|
|
|
|
portingFirstNameKatakana: z.string().optional(),
|
2025-09-24 18:00:49 +09:00
|
|
|
portingGender: z.enum(["Male", "Female", "Corporate/Other"]).optional(),
|
2025-09-19 16:34:10 +09:00
|
|
|
portingDateOfBirth: z.string().date().optional(),
|
|
|
|
|
|
|
|
|
|
// Optional address override captured at checkout
|
|
|
|
|
address: addressSchema.optional(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export const createOrderRequestSchema = z.object({
|
2025-09-24 18:00:49 +09:00
|
|
|
orderType: z.enum(["Internet", "SIM", "VPN", "Other"]),
|
|
|
|
|
skus: z.array(z.string().min(1, "SKU cannot be empty")),
|
2025-09-19 16:34:10 +09:00
|
|
|
configurations: orderConfigurationsSchema.optional(),
|
|
|
|
|
});
|
|
|
|
|
|
2025-10-02 16:33:25 +09:00
|
|
|
export const orderIdParamSchema = z.object({
|
|
|
|
|
id: z.coerce.number().int().positive("Order ID must be positive"),
|
|
|
|
|
});
|
|
|
|
|
|
2025-09-19 16:34:10 +09:00
|
|
|
// =====================================================
|
|
|
|
|
// SUBSCRIPTION MANAGEMENT REQUEST SCHEMAS
|
|
|
|
|
// =====================================================
|
|
|
|
|
|
|
|
|
|
export const simTopupRequestSchema = z.object({
|
2025-09-24 18:00:49 +09:00
|
|
|
amount: z.number().positive("Amount must be positive"),
|
|
|
|
|
currency: z.string().length(3, "Currency must be 3 characters").default("JPY"),
|
|
|
|
|
quotaMb: z.number().positive("Quota in MB must be positive"),
|
2025-09-19 16:34:10 +09:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export const simCancelRequestSchema = z.object({
|
2025-09-24 18:00:49 +09:00
|
|
|
reason: z.string().min(1, "Cancellation reason is required").optional(),
|
|
|
|
|
scheduledAt: z
|
|
|
|
|
.string()
|
|
|
|
|
.regex(/^\d{8}$/, "Scheduled date must be in YYYYMMDD format")
|
|
|
|
|
.optional(),
|
2025-09-19 16:34:10 +09:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export const simChangePlanRequestSchema = z.object({
|
2025-09-24 18:00:49 +09:00
|
|
|
newPlanSku: z.string().min(1, "New plan SKU is required"),
|
|
|
|
|
newPlanCode: z.string().min(1, "New plan code is required"),
|
2025-09-19 16:34:10 +09:00
|
|
|
effectiveDate: z.string().date().optional(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export const simFeaturesRequestSchema = z.object({
|
2025-09-20 11:35:40 +09:00
|
|
|
voiceMailEnabled: z.boolean().optional(),
|
|
|
|
|
callWaitingEnabled: z.boolean().optional(),
|
|
|
|
|
internationalRoamingEnabled: z.boolean().optional(),
|
2025-09-24 18:00:49 +09:00
|
|
|
networkType: z.enum(["4G", "5G"]).optional(),
|
2025-09-19 16:34:10 +09:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// =====================================================
|
|
|
|
|
// CONTACT REQUEST SCHEMAS
|
|
|
|
|
// =====================================================
|
|
|
|
|
|
|
|
|
|
export const contactRequestSchema = z.object({
|
2025-09-24 18:00:49 +09:00
|
|
|
subject: z.string().min(1, "Subject is required").max(200, "Subject is too long"),
|
|
|
|
|
message: z.string().min(1, "Message is required").max(2000, "Message is too long"),
|
|
|
|
|
category: z.enum(["technical", "billing", "account", "general"]),
|
|
|
|
|
priority: z.enum(["low", "medium", "high", "urgent"]).default("medium"),
|
2025-09-19 16:34:10 +09:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// =====================================================
|
|
|
|
|
// TYPE EXPORTS
|
|
|
|
|
// =====================================================
|
|
|
|
|
|
|
|
|
|
export type UpdateProfileRequest = z.infer<typeof updateProfileRequestSchema>;
|
|
|
|
|
export type UpdateAddressRequest = z.infer<typeof updateAddressRequestSchema>;
|
|
|
|
|
export type OrderConfigurations = z.infer<typeof orderConfigurationsSchema>;
|
|
|
|
|
export type CreateOrderRequest = z.infer<typeof createOrderRequestSchema>;
|
|
|
|
|
export type SimTopupRequest = z.infer<typeof simTopupRequestSchema>;
|
|
|
|
|
export type SimCancelRequest = z.infer<typeof simCancelRequestSchema>;
|
|
|
|
|
export type SimChangePlanRequest = z.infer<typeof simChangePlanRequestSchema>;
|
|
|
|
|
export type SimFeaturesRequest = z.infer<typeof simFeaturesRequestSchema>;
|
|
|
|
|
export type ContactRequest = z.infer<typeof contactRequestSchema>;
|
|
|
|
|
|
|
|
|
|
// =====================================================
|
|
|
|
|
// INVOICE SCHEMAS
|
|
|
|
|
// =====================================================
|
|
|
|
|
|
|
|
|
|
export const invoiceItemSchema = z.object({
|
|
|
|
|
id: z.number().int().positive(),
|
2025-09-24 18:00:49 +09:00
|
|
|
description: z.string().min(1, "Description is required"),
|
|
|
|
|
amount: z.number().nonnegative("Amount must be non-negative"),
|
2025-09-19 16:34:10 +09:00
|
|
|
quantity: z.number().int().positive().optional().default(1),
|
2025-09-24 18:00:49 +09:00
|
|
|
type: z.string().min(1, "Type is required"),
|
2025-09-19 16:34:10 +09:00
|
|
|
serviceId: z.number().int().positive().optional(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export const invoiceSchema = z.object({
|
|
|
|
|
id: z.number().int().positive(),
|
2025-09-24 18:00:49 +09:00
|
|
|
number: z.string().min(1, "Invoice number is required"),
|
|
|
|
|
status: z.string().min(1, "Status is required"),
|
|
|
|
|
currency: z.string().length(3, "Currency must be 3 characters"),
|
2025-09-19 16:34:10 +09:00
|
|
|
currencySymbol: z.string().optional(),
|
2025-09-24 18:00:49 +09:00
|
|
|
total: z.number().nonnegative("Total must be non-negative"),
|
|
|
|
|
subtotal: z.number().nonnegative("Subtotal must be non-negative"),
|
|
|
|
|
tax: z.number().nonnegative("Tax must be non-negative"),
|
2025-09-19 16:34:10 +09:00
|
|
|
issuedAt: z.string().datetime().optional(),
|
|
|
|
|
dueDate: z.string().datetime().optional(),
|
|
|
|
|
paidDate: z.string().datetime().optional(),
|
|
|
|
|
pdfUrl: z.string().url().optional(),
|
|
|
|
|
paymentUrl: z.string().url().optional(),
|
|
|
|
|
description: z.string().optional(),
|
|
|
|
|
items: z.array(invoiceItemSchema).optional(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export const paginationSchema = z.object({
|
|
|
|
|
page: z.number().int().min(1),
|
|
|
|
|
totalPages: z.number().int().min(0),
|
|
|
|
|
totalItems: z.number().int().min(0),
|
|
|
|
|
nextCursor: z.string().optional(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export const invoiceListSchema = z.object({
|
|
|
|
|
invoices: z.array(invoiceSchema),
|
|
|
|
|
pagination: paginationSchema,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// =====================================================
|
|
|
|
|
// INVOICE TYPE EXPORTS
|
|
|
|
|
// =====================================================
|
|
|
|
|
|
|
|
|
|
export type InvoiceItem = z.infer<typeof invoiceItemSchema>;
|
|
|
|
|
export type Invoice = z.infer<typeof invoiceSchema>;
|
|
|
|
|
export type Pagination = z.infer<typeof paginationSchema>;
|
|
|
|
|
export type InvoiceList = z.infer<typeof invoiceListSchema>;
|
|
|
|
|
|
2025-10-02 17:19:39 +09:00
|
|
|
export const invoicePaymentLinkSchema = z.object({
|
|
|
|
|
paymentMethodId: z.coerce.number().int().positive().optional(),
|
|
|
|
|
gatewayName: z.string().min(1).optional(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export type InvoicePaymentLinkInput = z.infer<typeof invoicePaymentLinkSchema>;
|
|
|
|
|
|
|
|
|
|
export const sfOrderIdParamSchema = z.object({
|
|
|
|
|
sfOrderId: z.string().min(1, "Salesforce order ID is required"),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export type SfOrderIdParam = z.infer<typeof sfOrderIdParamSchema>;
|
2025-09-19 16:34:10 +09:00
|
|
|
// =====================================================
|
|
|
|
|
// ID MAPPING SCHEMAS
|
|
|
|
|
// =====================================================
|
|
|
|
|
|
|
|
|
|
export const createMappingRequestSchema = z.object({
|
2025-09-24 18:00:49 +09:00
|
|
|
userId: z.string().uuid("User ID must be a valid UUID"),
|
|
|
|
|
whmcsClientId: z.number().int().positive("WHMCS client ID must be a positive integer"),
|
|
|
|
|
sfAccountId: z
|
|
|
|
|
.string()
|
|
|
|
|
.regex(
|
|
|
|
|
/^[a-zA-Z0-9]{15}$|^[a-zA-Z0-9]{18}$/,
|
|
|
|
|
"Salesforce account ID must be a valid 15 or 18 character ID"
|
|
|
|
|
)
|
|
|
|
|
.optional(),
|
2025-09-19 16:34:10 +09:00
|
|
|
});
|
|
|
|
|
|
2025-09-24 18:00:49 +09:00
|
|
|
export const updateMappingRequestSchema = z
|
|
|
|
|
.object({
|
|
|
|
|
whmcsClientId: z
|
|
|
|
|
.number()
|
|
|
|
|
.int()
|
|
|
|
|
.positive("WHMCS client ID must be a positive integer")
|
|
|
|
|
.optional(),
|
|
|
|
|
sfAccountId: z
|
|
|
|
|
.string()
|
|
|
|
|
.regex(
|
|
|
|
|
/^[a-zA-Z0-9]{15}$|^[a-zA-Z0-9]{18}$/,
|
|
|
|
|
"Salesforce account ID must be a valid 15 or 18 character ID"
|
|
|
|
|
)
|
|
|
|
|
.optional(),
|
|
|
|
|
})
|
|
|
|
|
.refine(data => data.whmcsClientId !== undefined || data.sfAccountId !== undefined, {
|
|
|
|
|
message: "At least one field must be provided for update",
|
|
|
|
|
});
|
2025-09-19 16:34:10 +09:00
|
|
|
|
|
|
|
|
export const userIdMappingSchema = z.object({
|
2025-09-24 18:00:49 +09:00
|
|
|
userId: z.string().uuid("User ID must be a valid UUID"),
|
|
|
|
|
whmcsClientId: z.number().int().positive("WHMCS client ID must be a positive integer"),
|
|
|
|
|
sfAccountId: z
|
|
|
|
|
.string()
|
|
|
|
|
.regex(
|
|
|
|
|
/^[a-zA-Z0-9]{15}$|^[a-zA-Z0-9]{18}$/,
|
|
|
|
|
"Salesforce account ID must be a valid 15 or 18 character ID"
|
|
|
|
|
)
|
|
|
|
|
.optional(),
|
2025-09-19 16:34:10 +09:00
|
|
|
createdAt: z.string().datetime().optional(),
|
|
|
|
|
updatedAt: z.string().datetime().optional(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// =====================================================
|
|
|
|
|
// ID MAPPING TYPE EXPORTS
|
|
|
|
|
// =====================================================
|
|
|
|
|
|
|
|
|
|
export type CreateMappingRequest = z.infer<typeof createMappingRequestSchema>;
|
|
|
|
|
export type UpdateMappingRequest = z.infer<typeof updateMappingRequestSchema>;
|
|
|
|
|
export type UserIdMapping = z.infer<typeof userIdMappingSchema>;
|
|
|
|
|
export type UpdateProfileRequestInput = z.infer<typeof updateProfileRequestSchema>;
|
|
|
|
|
export type UpdateAddressRequestInput = z.infer<typeof updateAddressRequestSchema>;
|
|
|
|
|
export type ContactRequestInput = z.infer<typeof contactRequestSchema>;
|