/** * 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; // 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; // 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; 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; /** * 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; /** * 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; // ============================================================================ // Inferred Types from Schemas (Schema-First Approach) // ============================================================================ export type InvoiceStatus = z.infer; export type InvoiceItem = z.infer; export type Invoice = z.infer; export type InvoicePagination = z.infer; export type InvoiceList = z.infer; export type InvoiceSsoLink = z.infer; export type PaymentInvoiceRequest = z.infer; export type BillingSummary = z.infer;