barsa b206de8dba refactor: enterprise-grade cleanup of BFF and domain packages
Comprehensive refactoring across 70 files (net -298 lines) improving
type safety, error handling, and code organization:

- Replace .passthrough()/.catchall(z.unknown()) with .strip() in all Zod schemas
- Tighten Record<string, unknown> to bounded union types where possible
- Replace throw new Error with domain-specific exceptions (OrderException,
  FulfillmentException, WhmcsOperationException, SalesforceOperationException, etc.)
- Split AuthTokenService (625 lines) into TokenGeneratorService and
  TokenRefreshService with thin orchestrator
- Deduplicate FreebitClientService with shared makeRequest() method
- Add typed interfaces to WHMCS facade, order service, and fulfillment mapper
- Externalize hardcoded config values to ConfigService with env fallbacks
- Consolidate duplicate billing cycle enums into shared billingCycleSchema
- Standardize logger usage (nestjs-pino @Inject(Logger) everywhere)
- Move shared WHMCS number coercion helpers to whmcs-utils/schema.ts
2026-02-24 19:05:30 +09:00

250 lines
8.3 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Subscriptions Domain - Schemas
*
* Zod validation schemas for subscription domain types.
*/
import { z } from "zod";
import { billingCycleSchema } from "../common/schema.js";
// Subscription Status Schema
export const subscriptionStatusSchema = z.enum([
"Active",
"Inactive",
"Pending",
"Cancelled",
"Suspended",
"Terminated",
"Completed",
]);
// Subscription Cycle Schema — re-exported from common
export const subscriptionCycleSchema = billingCycleSchema;
// 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(),
});
export const subscriptionArraySchema = z.array(subscriptionSchema);
// Subscription List Schema
export const subscriptionListSchema = z.object({
subscriptions: z.array(subscriptionSchema),
totalCount: z.number().int().nonnegative(),
});
// ============================================================================
// 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>;
// ============================================================================
// 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>;
export const subscriptionQuerySchema = subscriptionQueryParamsSchema;
export type SubscriptionQuery = SubscriptionQueryParams;
// ============================================================================
// 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)
*/
export const simActionResponseSchema = z.object({
message: z.string(),
/** Action-specific payload — varies by SIM/internet operation (top-up, cancellation, etc.) */
data: z.record(z.string(), z.unknown()).optional(),
});
/**
* Schema for SIM plan change result with IP addresses
*/
export const simPlanChangeResultSchema = z.object({
message: z.string(),
ipv4: z.string().optional(),
ipv6: z.string().optional(),
scheduledAt: z
.string()
.regex(/^\d{8}$/, "Scheduled date must be in YYYYMMDD format")
.optional(),
});
// ============================================================================
// 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>;
export type SubscriptionArray = z.infer<typeof subscriptionArraySchema>;
export type SubscriptionList = z.infer<typeof subscriptionListSchema>;
export type SubscriptionStats = z.infer<typeof subscriptionStatsSchema>;
export type SimActionResponse = z.infer<typeof simActionResponseSchema>;
export type SimPlanChangeResult = z.infer<typeof simPlanChangeResultSchema>;
// ============================================================================
// 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>;
// ============================================================================
// 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>;