- Adjusted .prettierrc to ensure consistent formatting with a newline at the end of the file. - Reformatted eslint.config.mjs for improved readability by aligning array elements. - Updated pnpm-lock.yaml to use single quotes for consistency across dependencies. - Simplified worktree setup in .cursor/worktrees.json for cleaner configuration. - Enhanced documentation in .cursor/plans to clarify architecture refactoring. - Refactored various service files for improved readability and maintainability, including rate-limiting and auth services. - Updated imports and exports across multiple files for consistency and clarity. - Improved error handling and logging in service methods to enhance debugging capabilities. - Streamlined utility functions for better performance and maintainability across the domain packages.
436 lines
12 KiB
Markdown
436 lines
12 KiB
Markdown
# @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
|