2025-10-03 14:26:55 +09:00
|
|
|
|
/**
|
|
|
|
|
|
* Subscriptions Domain - Schemas
|
2025-12-23 15:19:20 +09:00
|
|
|
|
*
|
2025-10-03 14:26:55 +09:00
|
|
|
|
* Zod validation schemas for subscription domain types.
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
import { z } from "zod";
|
|
|
|
|
|
|
2026-02-24 19:05:30 +09:00
|
|
|
|
import { billingCycleSchema } from "../common/schema.js";
|
|
|
|
|
|
|
2025-10-03 14:26:55 +09:00
|
|
|
|
// Subscription Status Schema
|
|
|
|
|
|
export const subscriptionStatusSchema = z.enum([
|
|
|
|
|
|
"Active",
|
|
|
|
|
|
"Inactive",
|
|
|
|
|
|
"Pending",
|
|
|
|
|
|
"Cancelled",
|
|
|
|
|
|
"Suspended",
|
|
|
|
|
|
"Terminated",
|
|
|
|
|
|
"Completed",
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
2026-02-24 19:05:30 +09:00
|
|
|
|
// Subscription Cycle Schema — re-exported from common
|
|
|
|
|
|
export const subscriptionCycleSchema = billingCycleSchema;
|
2025-10-03 14:26:55 +09:00
|
|
|
|
|
|
|
|
|
|
// Subscription Schema
|
|
|
|
|
|
export const subscriptionSchema = z.object({
|
|
|
|
|
|
id: z.number().int().positive("Subscription id must be positive"),
|
|
|
|
|
|
serviceId: z.number().int().positive("Service id must be positive"),
|
|
|
|
|
|
productName: z.string().min(1, "Product name is required"),
|
|
|
|
|
|
domain: z.string().optional(),
|
|
|
|
|
|
cycle: subscriptionCycleSchema,
|
|
|
|
|
|
status: subscriptionStatusSchema,
|
|
|
|
|
|
nextDue: z.string().optional(),
|
|
|
|
|
|
amount: z.number(),
|
|
|
|
|
|
currency: z.string().min(1, "Currency is required"),
|
|
|
|
|
|
currencySymbol: z.string().optional(),
|
|
|
|
|
|
registrationDate: z.string().min(1, "Registration date is required"),
|
|
|
|
|
|
notes: z.string().optional(),
|
|
|
|
|
|
customFields: z.record(z.string(), z.string()).optional(),
|
|
|
|
|
|
orderNumber: z.string().optional(),
|
|
|
|
|
|
groupName: z.string().optional(),
|
|
|
|
|
|
paymentMethod: z.string().optional(),
|
|
|
|
|
|
serverName: z.string().optional(),
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-12-26 13:40:10 +09:00
|
|
|
|
export const subscriptionArraySchema = z.array(subscriptionSchema);
|
|
|
|
|
|
|
2025-10-03 14:26:55 +09:00
|
|
|
|
// Subscription List Schema
|
|
|
|
|
|
export const subscriptionListSchema = z.object({
|
|
|
|
|
|
subscriptions: z.array(subscriptionSchema),
|
|
|
|
|
|
totalCount: z.number().int().nonnegative(),
|
|
|
|
|
|
});
|
2025-10-07 17:38:39 +09:00
|
|
|
|
|
2025-12-26 13:40:10 +09:00
|
|
|
|
// ============================================================================
|
|
|
|
|
|
// Route Param Schemas (BFF)
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
export const subscriptionIdParamSchema = z.object({
|
|
|
|
|
|
id: z.coerce.number().int().positive("Subscription id must be positive"),
|
|
|
|
|
|
});
|
|
|
|
|
|
export type SubscriptionIdParam = z.infer<typeof subscriptionIdParamSchema>;
|
|
|
|
|
|
|
2025-10-07 17:38:39 +09:00
|
|
|
|
// ============================================================================
|
|
|
|
|
|
// Query Parameter Schemas
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Schema for subscription query parameters
|
|
|
|
|
|
*/
|
|
|
|
|
|
export const subscriptionQueryParamsSchema = z.object({
|
|
|
|
|
|
page: z.coerce.number().int().positive().optional(),
|
|
|
|
|
|
limit: z.coerce.number().int().positive().max(100).optional(),
|
|
|
|
|
|
status: subscriptionStatusSchema.optional(),
|
|
|
|
|
|
type: z.string().optional(),
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
export type SubscriptionQueryParams = z.infer<typeof subscriptionQueryParamsSchema>;
|
2025-10-08 10:33:33 +09:00
|
|
|
|
|
|
|
|
|
|
export const subscriptionQuerySchema = subscriptionQueryParamsSchema;
|
|
|
|
|
|
export type SubscriptionQuery = SubscriptionQueryParams;
|
|
|
|
|
|
|
2025-10-08 16:31:42 +09:00
|
|
|
|
// ============================================================================
|
|
|
|
|
|
// Response Schemas
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Schema for subscription statistics
|
|
|
|
|
|
*/
|
|
|
|
|
|
export const subscriptionStatsSchema = z.object({
|
|
|
|
|
|
total: z.number().int().nonnegative(),
|
|
|
|
|
|
active: z.number().int().nonnegative(),
|
|
|
|
|
|
completed: z.number().int().nonnegative(),
|
|
|
|
|
|
cancelled: z.number().int().nonnegative(),
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Schema for SIM action responses (top-up, cancellation, feature updates)
|
|
|
|
|
|
*/
|
2025-12-29 11:12:20 +09:00
|
|
|
|
export const simActionResponseSchema = z.object({
|
|
|
|
|
|
message: z.string(),
|
2026-02-24 19:05:30 +09:00
|
|
|
|
/** Action-specific payload — varies by SIM/internet operation (top-up, cancellation, etc.) */
|
|
|
|
|
|
data: z.record(z.string(), z.unknown()).optional(),
|
2025-10-08 16:31:42 +09:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Schema for SIM plan change result with IP addresses
|
|
|
|
|
|
*/
|
2025-12-29 11:12:20 +09:00
|
|
|
|
export const simPlanChangeResultSchema = z.object({
|
|
|
|
|
|
message: z.string(),
|
2025-10-08 16:31:42 +09:00
|
|
|
|
ipv4: z.string().optional(),
|
|
|
|
|
|
ipv6: z.string().optional(),
|
2025-12-26 10:30:09 +09:00
|
|
|
|
scheduledAt: z
|
|
|
|
|
|
.string()
|
|
|
|
|
|
.regex(/^\d{8}$/, "Scheduled date must be in YYYYMMDD format")
|
|
|
|
|
|
.optional(),
|
2025-10-08 16:31:42 +09:00
|
|
|
|
});
|
|
|
|
|
|
|
2025-10-08 10:33:33 +09:00
|
|
|
|
// ============================================================================
|
|
|
|
|
|
// Inferred Types from Schemas (Schema-First Approach)
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
export type SubscriptionStatus = z.infer<typeof subscriptionStatusSchema>;
|
|
|
|
|
|
export type SubscriptionCycle = z.infer<typeof subscriptionCycleSchema>;
|
|
|
|
|
|
export type Subscription = z.infer<typeof subscriptionSchema>;
|
2025-12-26 13:40:10 +09:00
|
|
|
|
export type SubscriptionArray = z.infer<typeof subscriptionArraySchema>;
|
2025-10-08 10:33:33 +09:00
|
|
|
|
export type SubscriptionList = z.infer<typeof subscriptionListSchema>;
|
2025-10-08 16:31:42 +09:00
|
|
|
|
export type SubscriptionStats = z.infer<typeof subscriptionStatsSchema>;
|
|
|
|
|
|
export type SimActionResponse = z.infer<typeof simActionResponseSchema>;
|
|
|
|
|
|
export type SimPlanChangeResult = z.infer<typeof simPlanChangeResultSchema>;
|
2025-12-23 15:19:20 +09:00
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
// Internet Cancellation Schemas
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Available cancellation month for the customer
|
|
|
|
|
|
*/
|
|
|
|
|
|
export const internetCancellationMonthSchema = z.object({
|
|
|
|
|
|
value: z.string(), // YYYY-MM format
|
|
|
|
|
|
label: z.string(), // Display label like "November 2025"
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Internet cancellation preview response (service details + available months)
|
|
|
|
|
|
*/
|
|
|
|
|
|
export const internetCancellationPreviewSchema = z.object({
|
|
|
|
|
|
productName: z.string(),
|
|
|
|
|
|
billingAmount: z.number(),
|
|
|
|
|
|
nextDueDate: z.string().optional(),
|
|
|
|
|
|
registrationDate: z.string().optional(),
|
|
|
|
|
|
availableMonths: z.array(internetCancellationMonthSchema),
|
|
|
|
|
|
customerEmail: z.string(),
|
|
|
|
|
|
customerName: z.string(),
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Internet cancellation request from customer
|
|
|
|
|
|
*/
|
|
|
|
|
|
export const internetCancelRequestSchema = z.object({
|
|
|
|
|
|
cancellationMonth: z
|
|
|
|
|
|
.string()
|
|
|
|
|
|
.regex(/^\d{4}-\d{2}$/, "Cancellation month must be in YYYY-MM format"),
|
|
|
|
|
|
confirmRead: z.boolean(),
|
|
|
|
|
|
confirmCancel: z.boolean(),
|
|
|
|
|
|
comments: z.string().max(1000).optional(),
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
export type InternetCancellationMonth = z.infer<typeof internetCancellationMonthSchema>;
|
|
|
|
|
|
export type InternetCancellationPreview = z.infer<typeof internetCancellationPreviewSchema>;
|
|
|
|
|
|
export type InternetCancelRequest = z.infer<typeof internetCancelRequestSchema>;
|
2026-01-05 17:06:25 +09:00
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
// Unified Cancellation Preview (SIM + Internet)
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Service type for cancellation flows
|
|
|
|
|
|
*/
|
|
|
|
|
|
export const serviceTypeSchema = z.enum(["sim", "internet"]);
|
|
|
|
|
|
export type ServiceType = z.infer<typeof serviceTypeSchema>;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Structured notice/term content for cancellation pages
|
|
|
|
|
|
*/
|
|
|
|
|
|
export const cancellationNoticeSchema = z.object({
|
|
|
|
|
|
title: z.string().min(1),
|
|
|
|
|
|
content: z.string().min(1),
|
|
|
|
|
|
});
|
|
|
|
|
|
export type CancellationNotice = z.infer<typeof cancellationNoticeSchema>;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Cancellation status derived from Salesforce Opportunity (if available)
|
|
|
|
|
|
*
|
|
|
|
|
|
* Notes:
|
|
|
|
|
|
* - We only need a small subset for portal display.
|
|
|
|
|
|
* - This is nullable because some services may not have a linked Opportunity.
|
|
|
|
|
|
*/
|
|
|
|
|
|
export const cancellationStatusSchema = z
|
|
|
|
|
|
.object({
|
|
|
|
|
|
stage: z.enum(["Active", "△Cancelling", "〇Cancelled"]),
|
|
|
|
|
|
scheduledEndDate: z.string().optional(),
|
|
|
|
|
|
// Internet only
|
|
|
|
|
|
rentalReturnStatus: z.string().optional(),
|
|
|
|
|
|
})
|
|
|
|
|
|
.nullable();
|
|
|
|
|
|
export type CancellationStatus = z.infer<typeof cancellationStatusSchema>;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Unified cancellation preview response used by the generic cancellation page.
|
|
|
|
|
|
*
|
|
|
|
|
|
* Includes:
|
|
|
|
|
|
* - Service type + display fields
|
|
|
|
|
|
* - Terms and notices (service-type specific)
|
|
|
|
|
|
* - Cancellation status (derived from Opportunity when WHMCS isn't already cancelled)
|
|
|
|
|
|
*/
|
|
|
|
|
|
export const cancellationPreviewSchema = z.object({
|
|
|
|
|
|
serviceType: serviceTypeSchema,
|
|
|
|
|
|
serviceName: z.string().min(1),
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Salesforce Opportunity ID read from WHMCS service custom fields (already stored in WHMCS).
|
|
|
|
|
|
* Optional because not all services are guaranteed to have it.
|
|
|
|
|
|
*/
|
|
|
|
|
|
opportunityId: z.string().min(15).max(18).optional(),
|
|
|
|
|
|
|
|
|
|
|
|
serviceInfo: z.array(
|
|
|
|
|
|
z.object({
|
|
|
|
|
|
label: z.string().min(1),
|
|
|
|
|
|
value: z.string().min(1),
|
|
|
|
|
|
mono: z.boolean().optional(),
|
|
|
|
|
|
})
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
|
|
terms: z.array(cancellationNoticeSchema),
|
|
|
|
|
|
warnings: z.array(cancellationNoticeSchema).default([]),
|
|
|
|
|
|
step3Notices: z.array(cancellationNoticeSchema).default([]),
|
|
|
|
|
|
|
|
|
|
|
|
cancellationStatus: cancellationStatusSchema,
|
|
|
|
|
|
|
|
|
|
|
|
availableMonths: z.array(internetCancellationMonthSchema),
|
|
|
|
|
|
customerEmail: z.string(),
|
|
|
|
|
|
customerName: z.string().min(1),
|
|
|
|
|
|
|
|
|
|
|
|
// SIM-specific (optional)
|
|
|
|
|
|
isWithinMinimumTerm: z.boolean().optional(),
|
|
|
|
|
|
minimumContractEndDate: z.string().optional(),
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
export type CancellationPreview = z.infer<typeof cancellationPreviewSchema>;
|