436 lines
12 KiB
Markdown
Raw Permalink Normal View History

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