# Schema-First Migration Guide **Status**: ๐Ÿšง In Progress **Date**: October 2025 **Objective**: Standardize all domain types to be derived from Zod schemas --- ## ๐ŸŽฏ Migration Strategy ### **Chosen Approach: Schema-First** All TypeScript types will be **derived from Zod schemas** using `z.infer`. **Benefits:** - โœ… Single source of truth (schema defines structure) - โœ… Impossible for types to drift from validation - โœ… Less maintenance (update schema, type auto-updates) - โœ… Runtime + compile-time safety guaranteed --- ## ๐Ÿ“‹ File Structure Pattern ### **Before (Mixed Approach):** ```typescript // contract.ts export interface Invoice { id: number; status: InvoiceStatus; // ... } // schema.ts export const invoiceSchema = z.object({ id: z.number(), status: invoiceStatusSchema, // ... }); ``` ### **After (Schema-First):** ```typescript // schema.ts export const invoiceSchema = z.object({ id: z.number().int().positive(), status: invoiceStatusSchema, // ... }); // Derive type from schema export type Invoice = z.infer; // contract.ts // Only for: // 1. Constants (e.g., INVOICE_STATUS) // 2. Complex business types without schemas // 3. Union types // 4. Provider-specific types export const INVOICE_STATUS = { PAID: "Paid", UNPAID: "Unpaid", // ... } as const; export type InvoiceStatus = (typeof INVOICE_STATUS)[keyof typeof INVOICE_STATUS]; // Re-export inferred type export type { Invoice } from './schema'; ``` --- ## ๐Ÿ”„ Migration Checklist ### **Phase 1: Core Domains** - [ ] **billing** - invoices, billing summary - [ ] **orders** - order creation, order queries - [ ] **customer** - customer profile, address - [ ] **auth** - login, signup, password reset ### **Phase 2: Secondary Domains** - [ ] **subscriptions** - service subscriptions - [ ] **payments** - payment methods, gateways - [ ] **sim** - SIM details, usage, management - [ ] **catalog** - product catalog ### **Phase 3: Supporting Domains** - [ ] **common** - shared schemas - [ ] **mappings** - ID mappings - [ ] **dashboard** - dashboard types --- ## ๐Ÿ“ Migration Steps (Per Domain) For each domain, follow these steps: ### 1. **Audit Current State** - Identify all types in `contract.ts` - Identify all schemas in `schema.ts` - Find which types already have corresponding schemas ### 2. **Convert Schema to Type** ```typescript // Before export interface Invoice { ... } // After (in schema.ts) export const invoiceSchema = z.object({ ... }); export type Invoice = z.infer; ``` ### 3. **Update contract.ts** - Remove duplicate interfaces - Keep only constants and business logic types - Re-export types from schema.ts ### 4. **Update index.ts** - Export both schemas and types - Maintain backward compatibility ### 5. **Verify Imports** - Check no breaking changes for consumers - Types still importable from domain package --- ## ๐Ÿ› ๏ธ Special Cases ### **Case 1: Constants + Inferred Types** ```typescript // schema.ts export const invoiceStatusSchema = z.enum(["Paid", "Unpaid", "Overdue"]); export type InvoiceStatus = z.infer; // contract.ts export const INVOICE_STATUS = { PAID: "Paid", UNPAID: "Unpaid", OVERDUE: "Overdue", } as const; // Keep both - they serve different purposes ``` ### **Case 2: Schemas Referencing Contract Types** ```typescript // โŒ Bad - circular dependency import type { SimTopUpRequest } from "./contract"; export const simTopUpRequestSchema: z.ZodType = z.object({ ... }); // โœ… Good - derive from schema export const simTopUpRequestSchema = z.object({ quotaMb: z.number().int().min(100).max(51200), }); export type SimTopUpRequest = z.infer; ``` ### **Case 3: Complex Business Types** ```typescript // contract.ts - Keep types that have no validation schema export interface OrderBusinessValidation extends CreateOrderRequest { userId: string; opportunityId?: string; } // These are internal domain types, not API contracts ``` ### **Case 4: Provider-Specific Types** ```typescript // providers/whmcs/raw.types.ts // Keep as-is - these are provider-specific, not validated export interface WhmcsInvoiceRaw { id: string; status: string; // Raw WHMCS response shape } ``` --- ## โœ… Success Criteria After migration, each domain should have: 1. **schema.ts** - All Zod schemas - Exported types derived from schemas (`z.infer`) 2. **contract.ts** - Constants (e.g., `INVOICE_STATUS`) - Business logic types (not validated) - Re-exports of types from schema.ts 3. **index.ts** - Exports all schemas - Exports all types - Maintains backward compatibility 4. **No Type Drift** - Every validated type has a corresponding schema - All types are derived from schemas --- ## ๐Ÿงช Testing Strategy ### **After Each Domain Migration:** 1. **Type Check** ```bash pnpm run type-check ``` 2. **Build Domain Package** ```bash cd packages/domain && pnpm build ``` 3. **Test Imports in Apps** ```typescript // Verify exports still work import { Invoice, invoiceSchema } from '@customer-portal/domain/billing'; ``` 4. **Run Schema Tests** (if added) ```bash pnpm test packages/domain ``` --- ## ๐Ÿ“š References - **Zod Documentation**: https://zod.dev/ - **Type Inference**: https://zod.dev/?id=type-inference - **Schema Composition**: https://zod.dev/?id=objects --- ## ๐Ÿš€ Next Steps 1. Start with **billing** domain (most straightforward) 2. Apply pattern to **orders** domain 3. Replicate across all domains 4. Update documentation 5. Create linting rules to enforce pattern