# Domain-First Structure with Providers

**Date**: October 3, 2025  
**Status**: ✅ Implementing

---

## 🎯 Architecture Philosophy

**Core Principle**: Domain-first organization where each business domain owns its:
- **contract.ts** - Normalized types (provider-agnostic)
- **schema.ts** - Runtime validation (Zod)
- **providers/** - Provider-specific adapters (raw types + mappers)

**Why This Works**:
- Domain-centric matches business thinking
- Provider isolation prevents leaking implementation details
- Adding new providers = adding new folders (no refactoring)
- Single package (`@customer-portal/domain`) for all types

---

## 📦 Package Structure

```
packages/domain/
├── billing/
│   ├── contract.ts              # Invoice, InvoiceItem, InvoiceStatus
│   ├── schema.ts                # invoiceSchema, INVOICE_STATUS const
│   ├── index.ts                 # Public exports
│   └── providers/
│       └── whmcs/
│           ├── raw.types.ts     # WhmcsInvoiceRaw (API response)
│           └── mapper.ts        # transformWhmcsInvoice()
│
├── subscriptions/
│   ├── contract.ts              # Subscription, SubscriptionStatus
│   ├── schema.ts
│   ├── index.ts
│   └── providers/
│       └── whmcs/
│           ├── raw.types.ts
│           └── mapper.ts
│
├── payments/
│   ├── contract.ts              # PaymentMethod, PaymentGateway
│   ├── schema.ts
│   ├── index.ts
│   └── providers/
│       └── whmcs/
│           ├── raw.types.ts
│           └── mapper.ts
│
├── sim/
│   ├── contract.ts              # SimDetails, SimUsage
│   ├── schema.ts
│   ├── index.ts
│   └── providers/
│       └── freebit/
│           ├── raw.types.ts
│           └── mapper.ts
│
├── orders/
│   ├── contract.ts              # Order, OrderItem
│   ├── schema.ts
│   ├── index.ts
│   └── providers/
│       ├── salesforce/          # Read orders
│       │   ├── raw.types.ts
│       │   └── mapper.ts
│       └── whmcs/               # Create orders
│           ├── raw.types.ts
│           └── mapper.ts
│
├── catalog/
│   ├── contract.ts              # CatalogProduct (UI view model)
│   ├── schema.ts
│   ├── index.ts
│   └── providers/
│       └── salesforce/
│           ├── raw.types.ts     # SalesforceProduct2
│           └── mapper.ts
│
├── common/
│   ├── types.ts                 # Address, Money, BaseEntity
│   ├── identifiers.ts           # UserId, OrderId (branded types)
│   ├── api.ts                   # ApiResponse, PaginatedResponse
│   ├── schema.ts                # Common schemas
│   └── index.ts
│
└── toolkit/
    ├── formatting/
    │   └── currency.ts          # formatCurrency()
    ├── validation/
    │   └── helpers.ts           # Validation utilities
    ├── typing/
    │   └── patterns.ts          # AsyncState, etc.
    └── index.ts
```

---

## 📝 Import Patterns

### **Application Code (Domain Only)**
```typescript
// Import normalized domain types
import { Invoice, invoiceSchema, INVOICE_STATUS } from "@customer-portal/domain/billing";
import { Subscription } from "@customer-portal/domain/subscriptions";
import { Address } from "@customer-portal/domain/customer";

// Use domain types
const invoice: Invoice = {
  id: 123,
  status: INVOICE_STATUS.PAID,
  // ...
};

// Validate
const validated = invoiceSchema.parse(rawData);
```

### **Integration Code (Needs Provider Specifics)**
```typescript
// Import domain + provider
import { Invoice, invoiceSchema } from "@customer-portal/domain/billing";
import { 
  transformWhmcsInvoice,
  type WhmcsInvoiceRaw 
} from "@customer-portal/domain/billing/providers/whmcs/mapper";
import { whmcsInvoiceRawSchema } from "@customer-portal/domain/billing/providers/whmcs/raw.types";

// Transform raw API data
const whmcsData: WhmcsInvoiceRaw = await whmcsApi.getInvoice(id);
const invoice: Invoice = transformWhmcsInvoice(whmcsData);
```

---

## 🏗️ Domain File Templates

### **contract.ts**
```typescript
/**
 * {Domain} - Contract
 * 
 * Normalized types for {domain} that all providers must map to.
 */

// Status enum (if applicable)
export const {DOMAIN}_STATUS = {
  ACTIVE: "Active",
  // ...
} as const;

export type {Domain}Status = (typeof {DOMAIN}_STATUS)[keyof typeof {DOMAIN}_STATUS];

// Main entity
export interface {Domain} {
  id: number;
  status: {Domain}Status;
  // ... normalized fields
}
```

### **schema.ts**
```typescript
/**
 * {Domain} - Schemas
 * 
 * Zod validation for {domain} types.
 */

import { z } from "zod";

export const {domain}StatusSchema = z.enum([...]);

export const {domain}Schema = z.object({
  id: z.number(),
  status: {domain}StatusSchema,
  // ... field validation
});
```

### **providers/{provider}/raw.types.ts**
```typescript
/**
 * {Provider} {Domain} Provider - Raw Types
 * 
 * Actual API response structure from {Provider}.
 */

import { z } from "zod";

export const {provider}{Domain}RawSchema = z.object({
  // Raw API fields (different naming, types, structure)
});

export type {Provider}{Domain}Raw = z.infer<typeof {provider}{Domain}RawSchema>;
```

### **providers/{provider}/mapper.ts**
```typescript
/**
 * {Provider} {Domain} Provider - Mapper
 * 
 * Transforms {Provider} raw data into normalized domain types.
 */

import type { {Domain} } from "../../contract";
import { {domain}Schema } from "../../schema";
import { type {Provider}{Domain}Raw, {provider}{Domain}RawSchema } from "./raw.types";

export function transform{Provider}{Domain}(raw: unknown): {Domain} {
  // 1. Validate raw data
  const validated = {provider}{Domain}RawSchema.parse(raw);
  
  // 2. Transform to domain model
  const result: {Domain} = {
    id: validated.someId,
    status: mapStatus(validated.rawStatus),
    // ... map all fields
  };
  
  // 3. Validate domain model
  return {domain}Schema.parse(result);
}
```

---

## 🎓 Key Patterns

### **1. Co-location**
Everything about a domain lives together:
```
billing/
  ├── contract.ts   # What billing IS
  ├── schema.ts     # How to validate it
  └── providers/    # Where it comes FROM
```

### **2. Provider Isolation**
Raw types and mappers stay in `providers/`:
```typescript
// ✅ GOOD - Isolated
import { transformWhmcsInvoice } from "@customer-portal/domain/billing/providers/whmcs/mapper";

// ❌ BAD - Would leak WHMCS details into app code
import { WhmcsInvoiceRaw } from "@somewhere/global";
```

### **3. Schema-Driven**
Domain schemas define the contract:
```typescript
// Contract (types)
export interface Invoice { ... }

// Schema (validation)
export const invoiceSchema = z.object({ ... });

// Provider mapper validates against schema
return invoiceSchema.parse(transformedData);
```

### **4. Provider Agnostic**
App code never knows about providers:
```typescript
// ✅ App only knows domain
function displayInvoice(invoice: Invoice) {
  // Doesn't care if it came from WHMCS, Salesforce, or Stripe
}

// ✅ Service layer handles providers
async function getInvoice(id: number): Promise<Invoice> {
  const raw = await whmcsClient.getInvoice(id);
  return transformWhmcsInvoice(raw); // Provider-specific
}
```

---

## 🔄 Adding a New Provider

Example: Adding Stripe as an invoice provider

**1. Create provider folder:**
```bash
mkdir -p packages/domain/billing/providers/stripe
```

**2. Add raw types:**
```typescript
// billing/providers/stripe/raw.types.ts
export const stripeInvoiceRawSchema = z.object({
  id: z.string(),
  status: z.enum(["draft", "open", "paid", "void"]),
  // ... Stripe's structure
});
```

**3. Add mapper:**
```typescript
// billing/providers/stripe/mapper.ts
export function transformStripeInvoice(raw: unknown): Invoice {
  const stripe = stripeInvoiceRawSchema.parse(raw);
  return invoiceSchema.parse({
    id: parseInt(stripe.id),
    status: mapStripeStatus(stripe.status),
    // ... transform to domain model
  });
}
```

**4. Use in service:**
```typescript
// No changes to domain contract needed!
import { transformStripeInvoice } from "@customer-portal/domain/billing/providers/stripe/mapper";

const invoice = transformStripeInvoice(stripeData);
```

---

## ✨ Benefits

1. **Domain-Centric** - Matches business thinking
2. **Provider Isolation** - No leaking of implementation details
3. **Co-location** - Everything about billing is in `billing/`
4. **Scalable** - New provider = new folder
5. **Single Package** - One `@customer-portal/domain`
6. **Type-Safe** - Schema validation at boundaries
7. **Provider-Agnostic** - App code doesn't know providers exist

---

## 📚 Related Documentation

- [TYPE-CLEANUP-GUIDE.md](./TYPE-CLEANUP-GUIDE.md) - Migration guide
- [ARCHITECTURE.md](./ARCHITECTURE.md) - Overall system architecture
- [TYPE-CLEANUP-SUMMARY.md](./TYPE-CLEANUP-SUMMARY.md) - Implementation summary

---

**Status**: Implementation in progress. See TODO list for remaining work.

