# @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<typeof schema>` 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<Invoice> = {
  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<LoginRequest>({
    schema: loginRequestSchema,
    initialValues: { email: "", password: "" },
    onSubmit: async (data) => {
      // data is validated and typed
      await login(data);
    },
  });

  return (
    <form onSubmit={form.handleSubmit}>
      {/* form fields */}
    </form>
  );
}
```

### **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<typeof myEntityQueryParamsSchema>;

// 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<MyEntityList> {
    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<typeof userSchema>;

// ❌ 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<typeof invoiceQueryParamsSchema>;

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