# @customer-portal/domain **Single Source of Truth for Types and Validation** The `@customer-portal/domain` package is the **centralized domain layer** containing all types, Zod validation schemas, and provider-specific adapters for the customer portal application. --- ## 📦 Package Structure ``` packages/domain/ ├── common/ # Shared types and utilities │ ├── types.ts # Common types, API responses, pagination │ ├── schema.ts # Zod schemas for validation │ └── index.ts │ ├── auth/ # Authentication & authorization │ ├── contract.ts # User, AuthTokens, AuthResponse types │ ├── schema.ts # Login, signup, password validation │ └── index.ts │ ├── billing/ # Invoices and billing │ ├── contract.ts # Invoice, InvoiceItem, InvoiceList │ ├── schema.ts # Zod schemas + query params │ ├── providers/whmcs/ # WHMCS adapter │ └── index.ts │ ├── subscriptions/ # Service subscriptions │ ├── contract.ts # Subscription, SubscriptionStatus │ ├── schema.ts # Zod schemas + query params │ ├── providers/whmcs/ # WHMCS adapter │ └── index.ts │ ├── orders/ # Order management │ ├── contract.ts # OrderSummary, OrderDetails │ ├── schema.ts # Zod schemas + query params │ ├── providers/ │ │ ├── salesforce/ # Read orders from Salesforce │ │ └── whmcs/ # Create orders in WHMCS │ └── index.ts │ ├── payments/ # Payment methods & gateways │ ├── contract.ts # PaymentMethod, PaymentGateway │ ├── schema.ts # Zod validation schemas │ └── index.ts │ ├── sim/ # SIM card management │ ├── contract.ts # SimDetails, SimUsage │ ├── schema.ts # Zod schemas + activation │ ├── providers/freebit/ # Freebit adapter │ └── index.ts │ ├── catalog/ # Product catalog │ ├── contract.ts # CatalogProduct types │ ├── schema.ts # Product validation │ └── index.ts │ ├── customer/ # Customer data │ ├── contract.ts # Customer, CustomerAddress │ ├── schema.ts # Customer validation │ └── index.ts │ └── toolkit/ # Utilities ├── formatting/ # Currency, date formatting ├── validation/ # Validation helpers └── index.ts ``` --- ## 🎯 Design Principles ### 1. **Domain-First Organization** Each business domain owns its: - **`contract.ts`** - TypeScript interfaces (provider-agnostic) - **`schema.ts`** - Zod validation schemas (runtime safety) - **`providers/`** - Provider-specific adapters (WHMCS, Salesforce, Freebit) ### 2. **Single Source of Truth** - ✅ All types defined in domain package - ✅ All validation schemas in domain package - ✅ No duplicate type definitions in apps - ✅ Shared between frontend (Next.js) and backend (NestJS) ### 3. **Type Safety + Runtime Validation** - TypeScript provides compile-time type checking - Zod schemas provide runtime validation - Use `z.infer` to derive types from schemas --- ## 📚 Usage Guide ### **Basic Import Pattern** ```typescript // Import domain types and schemas import { Invoice, invoiceSchema, InvoiceQueryParams } from "@customer-portal/domain/billing"; import { Subscription, subscriptionSchema } from "@customer-portal/domain/subscriptions"; import { ApiResponse, PaginationParams } from "@customer-portal/domain/common"; ``` ### **API Response Handling** ```typescript import { ApiResponse, ApiSuccessResponse, ApiErrorResponse, apiResponseSchema } from "@customer-portal/domain/common"; // Type-safe API responses const response: ApiResponse = { success: true, data: { /* invoice data */ } }; // With validation const validated = apiResponseSchema(invoiceSchema).parse(rawResponse); ``` ### **Query Parameters with Validation** ```typescript import { InvoiceQueryParams, invoiceQueryParamsSchema } from "@customer-portal/domain/billing"; // In BFF controller @Get() @UsePipes(new ZodValidationPipe(invoiceQueryParamsSchema)) async getInvoices(@Query() query: InvoiceQueryParams) { // query is validated and typed } // In frontend const params: InvoiceQueryParams = { page: 1, limit: 20, status: "Unpaid" }; ``` ### **Form Validation (Frontend)** ```typescript import { useZodForm } from "@customer-portal/validation"; import { loginRequestSchema, type LoginRequest } from "@customer-portal/domain/auth"; function LoginForm() { const form = useZodForm({ schema: loginRequestSchema, initialValues: { email: "", password: "" }, onSubmit: async (data) => { // data is validated and typed await login(data); }, }); return (
{/* form fields */}
); } ``` ### **Backend Validation (BFF)** ```typescript import { ZodValidationPipe } from "@bff/core/validation"; import { createOrderRequestSchema, type CreateOrderRequest } from "@customer-portal/domain/orders"; @Controller("orders") export class OrdersController { @Post() @UsePipes(new ZodValidationPipe(createOrderRequestSchema)) async create(@Body() body: CreateOrderRequest) { // body is validated by Zod before reaching here return this.orderService.create(body); } } ``` --- ## 🔧 Common Schemas Reference ### **API Responses** | Schema | Description | |--------|-------------| | `apiSuccessResponseSchema(dataSchema)` | Successful API response wrapper | | `apiErrorResponseSchema` | Error API response with code/message | | `apiResponseSchema(dataSchema)` | Discriminated union of success/error | ### **Pagination & Queries** | Schema | Description | |--------|-------------| | `paginationParamsSchema` | Page, limit, offset parameters | | `paginatedResponseSchema(itemSchema)` | Paginated list response | | `filterParamsSchema` | Search, sortBy, sortOrder | | `queryParamsSchema` | Combined pagination + filters | ### **Domain-Specific Query Params** | Schema | Description | |--------|-------------| | `invoiceQueryParamsSchema` | Invoice list filtering (status, dates) | | `subscriptionQueryParamsSchema` | Subscription filtering (status, type) | | `orderQueryParamsSchema` | Order filtering (status, orderType) | ### **Validation Primitives** | Schema | Description | |--------|-------------| | `emailSchema` | Email validation (lowercase, trimmed) | | `passwordSchema` | Strong password (8+ chars, mixed case, number, special) | | `nameSchema` | Name validation (1-100 chars) | | `phoneSchema` | Phone number validation | | `timestampSchema` | ISO datetime string | | `dateSchema` | ISO date string | --- ## 🚀 Adding New Domain Types ### 1. Create Domain Files ```typescript // packages/domain/my-domain/contract.ts export interface MyEntity { id: string; name: string; status: "active" | "inactive"; } export interface MyEntityList { entities: MyEntity[]; totalCount: number; } // packages/domain/my-domain/schema.ts import { z } from "zod"; export const myEntityStatusSchema = z.enum(["active", "inactive"]); export const myEntitySchema = z.object({ id: z.string(), name: z.string().min(1), status: myEntityStatusSchema, }); export const myEntityListSchema = z.object({ entities: z.array(myEntitySchema), totalCount: z.number().int().nonnegative(), }); // Query params export const myEntityQueryParamsSchema = z.object({ page: z.coerce.number().int().positive().optional(), limit: z.coerce.number().int().positive().max(100).optional(), status: myEntityStatusSchema.optional(), }); export type MyEntityQueryParams = z.infer; // packages/domain/my-domain/index.ts export * from "./contract"; export * from "./schema"; ``` ### 2. Use in Backend (BFF) ```typescript import { ZodValidationPipe } from "@bff/core/validation"; import { myEntitySchema, myEntityQueryParamsSchema, type MyEntity, type MyEntityQueryParams } from "@customer-portal/domain/my-domain"; @Controller("my-entities") export class MyEntitiesController { @Get() @UsePipes(new ZodValidationPipe(myEntityQueryParamsSchema)) async list(@Query() query: MyEntityQueryParams): Promise { return this.service.list(query); } } ``` ### 3. Use in Frontend ```typescript import { useQuery } from "@tanstack/react-query"; import { myEntitySchema, type MyEntity } from "@customer-portal/domain/my-domain"; function useMyEntities() { return useQuery({ queryKey: ["my-entities"], queryFn: async () => { const response = await apiClient.get("/my-entities"); return myEntitySchema.array().parse(response.data); }, }); } ``` --- ## ✅ Validation Best Practices ### 1. **Always Define Both Type and Schema** ```typescript // ✅ Good - Type and schema together export const userSchema = z.object({ id: z.string(), email: emailSchema, }); export type User = z.infer; // ❌ Bad - Type only (no runtime validation) export interface User { id: string; email: string; } ``` ### 2. **Use Zod Schema Composition** ```typescript // Base schema const baseProductSchema = z.object({ id: z.string(), name: z.string(), }); // Extended schema export const fullProductSchema = baseProductSchema.extend({ description: z.string(), price: z.number().positive(), }); ``` ### 3. **Query Params Use `z.coerce` for URL Strings** ```typescript // ✅ Good - coerce string params to numbers export const paginationSchema = z.object({ page: z.coerce.number().int().positive().optional(), limit: z.coerce.number().int().positive().optional(), }); // ❌ Bad - will fail on URL query strings export const paginationSchema = z.object({ page: z.number().int().positive().optional(), // "1" !== 1 }); ``` ### 4. **Use Refinements for Complex Validation** ```typescript export const simActivationSchema = z.object({ simType: z.enum(["eSIM", "Physical SIM"]), eid: z.string().optional(), }).refine( (data) => data.simType !== "eSIM" || (data.eid && data.eid.length >= 15), { message: "EID required for eSIM", path: ["eid"] } ); ``` --- ## 🔄 Migration from Local Types If you find types defined locally in apps, migrate them to domain: ```typescript // ❌ Before: apps/bff/src/modules/invoices/types.ts export interface InvoiceQuery { status?: string; page?: number; } // ✅ After: packages/domain/billing/schema.ts export const invoiceQueryParamsSchema = z.object({ status: invoiceStatusSchema.optional(), page: z.coerce.number().int().positive().optional(), }); export type InvoiceQueryParams = z.infer; // Update imports import { InvoiceQueryParams } from "@customer-portal/domain/billing"; ``` --- ## 📖 Additional Resources - **Zod Documentation**: https://zod.dev/ - **Provider-Aware Architecture**: See `docs/DOMAIN-STRUCTURE.md` - **Type System**: See `docs/CONSOLIDATED-TYPE-SYSTEM.md` --- ## 🤝 Contributing When adding new types or schemas: 1. ✅ Define types in `contract.ts` 2. ✅ Add Zod schemas in `schema.ts` 3. ✅ Export from `index.ts` 4. ✅ Update this README if adding new patterns 5. ✅ Remove any duplicate types from apps 6. ✅ Update imports to use domain package --- **Maintained by**: Customer Portal Team **Last Updated**: October 2025