# 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<typeof schema>`.

**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<typeof invoiceSchema>;

// 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<typeof invoiceSchema>;
```

### 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<typeof invoiceStatusSchema>;

// 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<SimTopUpRequest> = z.object({ ... });

// ✅ Good - derive from schema
export const simTopUpRequestSchema = z.object({
  quotaMb: z.number().int().min(100).max(51200),
});
export type SimTopUpRequest = z.infer<typeof simTopUpRequestSchema>;
```

### **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

