barsa f447ba1800 Refactor codebase: eliminate duplication, standardize patterns, resolve circular deps
Phase 1: Portal Duplication Cleanup
- Delete apps/portal/src/lib/ directory (12 duplicate files)
- Update imports to use canonical locations (core/, shared/)

Phase 2: Domain Package Standardization
- Add contract.ts to notifications and checkout modules
- Update billing schema to derive enums from contract

Phase 3: BFF Error Handling
- Remove hardcoded test SIM number from SimValidationService
- Use ConfigService for TEST_SIM_ACCOUNT env variable

Phase 4: Circular Dependency Resolution
- Create VoiceOptionsModule to break FreebitModule <-> SimManagementModule cycle
- Remove forwardRef usage between these modules
- Move SimVoiceOptionsService to new voice-options module

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-13 14:25:14 +09:00

197 lines
6.4 KiB
TypeScript

/**
* Billing Domain - Schemas
*
* Zod validation schemas for billing domain types.
* Used for runtime validation of data from any source.
*/
import { z } from "zod";
import { INVOICE_PAGINATION } from "./constants.js";
import { INVOICE_STATUS } from "./contract.js";
// ============================================================================
// Currency (Domain Model)
// ============================================================================
/**
* Normalized currency model used across the Portal and BFF.
* This is intentionally provider-agnostic (even if sourced from WHMCS).
*/
export const currencySchema = z.object({
id: z.number().int().positive(),
code: z.string().min(1),
prefix: z.string(),
suffix: z.string().optional(),
format: z.string().optional(),
rate: z.string().optional(),
});
export type Currency = z.infer<typeof currencySchema>;
// Invoice Status Schema - derived from contract constants
const INVOICE_STATUS_VALUES = Object.values(INVOICE_STATUS) as [string, ...string[]];
export const invoiceStatusSchema = z.enum(INVOICE_STATUS_VALUES);
// Invoice Item Schema
export const invoiceItemSchema = z.object({
id: z.number().int().positive("Invoice item id must be positive"),
description: z.string().min(1, "Description is required"),
amount: z.number(),
quantity: z.number().int().positive("Quantity must be positive").optional(),
type: z.string().min(1, "Item type is required"),
serviceId: z.number().int().positive().optional(),
});
// Invoice Schema
export const invoiceSchema = z.object({
id: z.number().int().positive("Invoice id must be positive"),
number: z.string().min(1, "Invoice number is required"),
status: invoiceStatusSchema,
currency: z.string().min(1, "Currency is required"),
currencySymbol: z.string().min(1, "Currency symbol is required").optional(),
total: z.number(),
subtotal: z.number(),
tax: z.number(),
issuedAt: z.string().optional(),
dueDate: z.string().optional(),
paidDate: z.string().optional(),
pdfUrl: z.string().optional(),
paymentUrl: z.string().optional(),
description: z.string().optional(),
items: z.array(invoiceItemSchema).optional(),
daysOverdue: z.number().int().nonnegative().optional(),
});
// ============================================================================
// Route Param Schemas (BFF)
// ============================================================================
export const invoiceIdParamSchema = z.object({
id: z.coerce.number().int().positive("Invoice id must be positive"),
});
export type InvoiceIdParam = z.infer<typeof invoiceIdParamSchema>;
// Invoice Pagination Schema
export const invoicePaginationSchema = z.object({
page: z.number().int().nonnegative(),
totalPages: z.number().int().nonnegative(),
totalItems: z.number().int().nonnegative(),
nextCursor: z.string().optional(),
});
// Invoice List Schema
export const invoiceListSchema = z.object({
invoices: z.array(invoiceSchema),
pagination: invoicePaginationSchema,
});
// Invoice SSO Link Schema
export const invoiceSsoLinkSchema = z.object({
url: z.string().url(),
expiresAt: z.string(),
});
// Payment Invoice Request Schema
export const paymentInvoiceRequestSchema = z.object({
invoiceId: z.number().int().positive(),
paymentMethodId: z.number().int().positive().optional(),
gatewayName: z.string().optional(),
amount: z.number().positive().optional(),
});
// Billing Summary Schema
export const billingSummarySchema = z.object({
totalOutstanding: z.number(),
totalOverdue: z.number(),
totalPaid: z.number(),
currency: z.string(),
currencySymbol: z.string().optional(),
invoiceCount: z.object({
total: z.number().int().min(0),
unpaid: z.number().int().min(0),
overdue: z.number().int().min(0),
paid: z.number().int().min(0),
}),
});
// ============================================================================
// Query Parameter Schemas
// ============================================================================
/**
* Schema for invoice list query parameters
*/
export const invoiceQueryParamsSchema = z.object({
page: z.coerce
.number()
.int()
.min(INVOICE_PAGINATION.DEFAULT_PAGE)
.optional()
.default(INVOICE_PAGINATION.DEFAULT_PAGE),
limit: z.coerce
.number()
.int()
.min(INVOICE_PAGINATION.MIN_LIMIT)
.max(INVOICE_PAGINATION.MAX_LIMIT)
.optional()
.default(INVOICE_PAGINATION.DEFAULT_LIMIT),
status: invoiceStatusSchema.optional(),
dateFrom: z.string().datetime().optional(),
dateTo: z.string().datetime().optional(),
});
export type InvoiceQueryParams = z.infer<typeof invoiceQueryParamsSchema>;
const invoiceListStatusSchema = z.enum(["Paid", "Unpaid", "Cancelled", "Overdue", "Collections"]);
export const invoiceListQuerySchema = z.object({
page: z.coerce
.number()
.int()
.min(INVOICE_PAGINATION.DEFAULT_PAGE)
.optional()
.default(INVOICE_PAGINATION.DEFAULT_PAGE),
limit: z.coerce
.number()
.int()
.min(INVOICE_PAGINATION.MIN_LIMIT)
.max(INVOICE_PAGINATION.MAX_LIMIT)
.optional()
.default(INVOICE_PAGINATION.DEFAULT_LIMIT),
status: invoiceListStatusSchema.optional(),
});
export type InvoiceListQuery = z.infer<typeof invoiceListQuerySchema>;
/**
* Schema for invoice SSO link query parameters
*/
export const invoiceSsoQuerySchema = z.object({
target: z.enum(["view", "download", "pay"]).optional().default("view"),
});
export type InvoiceSsoQuery = z.infer<typeof invoiceSsoQuerySchema>;
/**
* Schema for invoice payment link query parameters
*/
export const invoicePaymentLinkQuerySchema = z.object({
paymentMethodId: z.coerce.number().int().positive().optional(),
gatewayName: z.string().optional().default("stripe"),
});
export type InvoicePaymentLinkQuery = z.infer<typeof invoicePaymentLinkQuerySchema>;
// ============================================================================
// Inferred Types from Schemas (Schema-First Approach)
// ============================================================================
export type InvoiceStatus = z.infer<typeof invoiceStatusSchema>;
export type InvoiceItem = z.infer<typeof invoiceItemSchema>;
export type Invoice = z.infer<typeof invoiceSchema>;
export type InvoicePagination = z.infer<typeof invoicePaginationSchema>;
export type InvoiceList = z.infer<typeof invoiceListSchema>;
export type InvoiceSsoLink = z.infer<typeof invoiceSsoLinkSchema>;
export type PaymentInvoiceRequest = z.infer<typeof paymentInvoiceRequestSchema>;
export type BillingSummary = z.infer<typeof billingSummarySchema>;