# Domain-First Structure with Providers

**Last Updated**: January 2026
**Status**: ✅ Implemented

---

## Quick Reference

| Aspect               | Location                                                     |
| -------------------- | ------------------------------------------------------------ |
| **Domain package**   | `packages/domain/`                                           |
| **Schemas**          | `packages/domain/<module>/schema.ts`                         |
| **Provider mappers** | `packages/domain/<module>/providers/<provider>/mapper.ts`    |
| **Raw types**        | `packages/domain/<module>/providers/<provider>/raw.types.ts` |

**Import patterns**:

```typescript
// App/Portal code - domain types only
import { Invoice, invoiceSchema } from "@customer-portal/domain/billing";

// BFF integration code - includes provider mappers
import { transformWhmcsInvoice } from "@customer-portal/domain/billing/providers";
```

**Key principle**: Transform once in domain mappers, use domain types everywhere else.

---

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

**Import rules**: See [`docs/development/domain/import-hygiene.md`](import-hygiene.md).

---

## 📦 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
│
├── services/
│   ├── contract.ts              # Service catalog products (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,
  whmcsInvoiceRawSchema,
  type WhmcsInvoiceRaw,
} from "@customer-portal/domain/billing/providers";

// 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";

// ❌ 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";

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

---

## 📋 Domain Module Inventory

| Module          | Key Types                            | Key Schemas               | Provider(s)       | Purpose               |
| --------------- | ------------------------------------ | ------------------------- | ----------------- | --------------------- |
| `auth`          | `AuthenticatedUser`, `Session`       | `authenticatedUserSchema` | —                 | Authentication types  |
| `address`       | `Address`, `JapanAddress`            | `addressSchema`           | japanpost         | Address management    |
| `billing`       | `Invoice`, `InvoiceItem`             | `invoiceSchema`           | whmcs             | Invoice management    |
| `checkout`      | `CheckoutSession`, `CartItem`        | `checkoutSchema`          | —                 | Checkout flow         |
| `common`        | `Money`, `ApiResponse`               | `moneySchema`             | —                 | Shared utilities      |
| `customer`      | `Customer`, `CustomerProfile`        | `customerSchema`          | salesforce        | Customer data         |
| `dashboard`     | `DashboardStats`                     | `dashboardSchema`         | —                 | Dashboard data        |
| `get-started`   | `GetStartedFlow`, `EligibilityCheck` | `getStartedSchema`        | —                 | Signup flow           |
| `notifications` | `Notification`                       | `notificationSchema`      | —                 | Notifications         |
| `opportunity`   | `Opportunity`                        | `opportunitySchema`       | salesforce        | Opportunity lifecycle |
| `orders`        | `Order`, `OrderDetails`, `OrderItem` | `orderSchema`             | salesforce, whmcs | Order management      |
| `payments`      | `PaymentMethod`, `PaymentGateway`    | `paymentMethodSchema`     | whmcs             | Payment methods       |
| `services`      | `CatalogProduct`, `ServiceCategory`  | `catalogProductSchema`    | salesforce        | Product catalog       |
| `sim`           | `SimDetails`, `SimUsage`             | `simDetailsSchema`        | freebit           | SIM management        |
| `subscriptions` | `Subscription`, `SubscriptionStatus` | `subscriptionSchema`      | whmcs, salesforce | Active services       |
| `support`       | `Case`, `CaseMessage`                | `caseSchema`              | salesforce        | Support cases         |
| `toolkit`       | `Formatting`, `AsyncState`           | —                         | —                 | Utilities             |

---

## 📚 Related Documentation

- [Import Hygiene](./import-hygiene.md) - Import rules and ESLint enforcement
- [Domain Packages](./packages.md) - Package internal structure
- [Domain Types](./types.md) - Unified type system
- [BFF Integration Patterns](../bff/integration-patterns.md) - How BFF uses domain mappers
- [Architecture Decisions](../../decisions/README.md) - Why these patterns exist
