- Added a new script to check domain imports, promoting better import hygiene across the codebase. - Refactored multiple domain index files to remove unnecessary type re-exports, streamlining the module structure. - Expanded documentation on import patterns and validation processes to provide clearer guidance for developers. - Included an architecture diagram to illustrate the relationships between the Portal, BFF, and Domain packages.
531 lines
15 KiB
Plaintext
531 lines
15 KiB
Plaintext
# Customer Portal - AI Agent Coding Rules
|
|
|
|
This document defines the coding standards, architecture patterns, and conventions for the Customer Portal project. AI agents (Cursor, Copilot, etc.) must follow these rules when generating or modifying code.
|
|
|
|
---
|
|
|
|
## 📚 Documentation First
|
|
|
|
**Before writing any code, read and understand the relevant documentation:**
|
|
|
|
- Start from `docs/README.md` for navigation
|
|
- Check `docs/development/` for implementation patterns
|
|
- Review `docs/architecture/` for system design
|
|
- Check `docs/integrations/` for external API details
|
|
|
|
**Never guess API response structures or endpoint signatures.** Always read the documentation or existing implementations first.
|
|
|
|
---
|
|
|
|
## 🏗️ Monorepo Architecture
|
|
|
|
```
|
|
apps/
|
|
portal/ # Next.js 15 frontend (React 19)
|
|
bff/ # NestJS 11 Backend-for-Frontend
|
|
packages/
|
|
domain/ # Pure domain types/schemas/utils (isomorphic)
|
|
```
|
|
|
|
### Technology Stack
|
|
|
|
- **Frontend**: Next.js 15, React 19, Tailwind CSS 4, shadcn/ui, TanStack Query, Zustand
|
|
- **Backend**: NestJS 11, Prisma 6, PostgreSQL 17, Redis 7, Pino
|
|
- **Integrations**: Salesforce (jsforce + Pub/Sub API), WHMCS, Freebit
|
|
- **Validation**: Zod (shared between frontend and backend)
|
|
|
|
---
|
|
|
|
## 📦 Domain Package (`@customer-portal/domain`)
|
|
|
|
The domain package is the **single source of truth** for all types and validation.
|
|
|
|
### Structure
|
|
|
|
```
|
|
packages/domain/
|
|
├── {domain}/
|
|
│ ├── contract.ts # Normalized types (provider-agnostic)
|
|
│ ├── schema.ts # Zod schemas + derived types
|
|
│ ├── constants.ts # Domain constants
|
|
│ ├── providers/ # Provider-specific adapters (BFF only!)
|
|
│ │ └── {provider}/
|
|
│ │ ├── raw.types.ts # Raw API response types
|
|
│ │ └── mapper.ts # Transform raw → domain
|
|
│ └── index.ts # Public exports
|
|
├── common/ # Shared types (ApiResponse, pagination)
|
|
└── toolkit/ # Utilities (formatting, validation helpers)
|
|
```
|
|
|
|
### Import Rules
|
|
|
|
```typescript
|
|
// ✅ CORRECT: Import from domain module
|
|
import type { Invoice, InvoiceList } from "@customer-portal/domain/billing";
|
|
import { invoiceSchema, invoiceListSchema } from "@customer-portal/domain/billing";
|
|
|
|
// ✅ CORRECT (BFF only): Import provider mappers
|
|
import { Providers } from "@customer-portal/domain/billing/providers";
|
|
|
|
// ❌ WRONG: Never import from domain root
|
|
import { Invoice } from "@customer-portal/domain";
|
|
|
|
// ❌ WRONG: Never deep-import internals
|
|
import { Invoice } from "@customer-portal/domain/billing/contract";
|
|
import { mapper } from "@customer-portal/domain/billing/providers/whmcs/mapper";
|
|
|
|
// ❌ WRONG: Portal must NEVER import providers
|
|
// (in apps/portal/**)
|
|
import { Providers } from "@customer-portal/domain/billing/providers"; // FORBIDDEN
|
|
```
|
|
|
|
### Schema-First Approach
|
|
|
|
Always define Zod schemas first, then derive TypeScript types:
|
|
|
|
```typescript
|
|
// ✅ CORRECT: Schema-first
|
|
export const invoiceSchema = z.object({
|
|
id: z.number(),
|
|
status: z.enum(["Paid", "Unpaid", "Cancelled"]),
|
|
total: z.number(),
|
|
});
|
|
export type Invoice = z.infer<typeof invoiceSchema>;
|
|
|
|
// ❌ WRONG: Type-only (no runtime validation)
|
|
export interface Invoice {
|
|
id: number;
|
|
status: string;
|
|
total: number;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 🖥️ BFF (NestJS Backend)
|
|
|
|
### Directory Structure
|
|
|
|
```
|
|
apps/bff/src/
|
|
├── modules/ # Feature-aligned modules
|
|
│ └── {feature}/
|
|
│ ├── {feature}.controller.ts
|
|
│ ├── {feature}.module.ts
|
|
│ └── services/
|
|
│ ├── {feature}-orchestrator.service.ts
|
|
│ └── {feature}.service.ts
|
|
├── integrations/ # External system clients
|
|
│ └── {provider}/
|
|
│ ├── services/
|
|
│ │ ├── {provider}-connection.service.ts
|
|
│ │ └── {provider}-{entity}.service.ts
|
|
│ ├── utils/
|
|
│ │ └── {entity}-query-builder.ts
|
|
│ └── {provider}.module.ts
|
|
├── core/ # Framework setup (guards, filters, config)
|
|
├── infra/ # Infrastructure (Redis, queues, logging)
|
|
└── main.ts
|
|
```
|
|
|
|
### Controller Pattern
|
|
|
|
Controllers use Zod DTOs via `createZodDto()` and the global `ZodValidationPipe`:
|
|
|
|
```typescript
|
|
import { Controller, Get, Query, Request } from "@nestjs/common";
|
|
import { createZodDto, ZodResponse } from "nestjs-zod";
|
|
import type { RequestWithUser } from "@bff/modules/auth/auth.types.js";
|
|
import type { InvoiceList } from "@customer-portal/domain/billing";
|
|
import {
|
|
invoiceListQuerySchema,
|
|
invoiceListSchema,
|
|
} from "@customer-portal/domain/billing";
|
|
|
|
class InvoiceListQueryDto extends createZodDto(invoiceListQuerySchema) {}
|
|
class InvoiceListDto extends createZodDto(invoiceListSchema) {}
|
|
|
|
@Controller("invoices")
|
|
export class InvoicesController {
|
|
constructor(private readonly orchestrator: InvoicesOrchestratorService) {}
|
|
|
|
@Get()
|
|
@ZodResponse({ description: "List invoices", type: InvoiceListDto })
|
|
async getInvoices(
|
|
@Request() req: RequestWithUser,
|
|
@Query() query: InvoiceListQueryDto
|
|
): Promise<InvoiceList> {
|
|
return this.orchestrator.getInvoices(req.user.id, query);
|
|
}
|
|
}
|
|
```
|
|
|
|
**Controller Rules:**
|
|
- ❌ Never import `zod` directly in controllers
|
|
- ✅ Use schemas from `@customer-portal/domain/{module}`
|
|
- ✅ Use `createZodDto(schema)` for DTO classes
|
|
- ✅ Delegate all business logic to orchestrator services
|
|
|
|
### Integration Service Pattern
|
|
|
|
Integration services handle external API communication:
|
|
|
|
```typescript
|
|
import { Injectable } from "@nestjs/common";
|
|
import { SalesforceConnection } from "./salesforce-connection.service";
|
|
import { buildOrderSelectFields } from "../utils/order-query-builder";
|
|
import {
|
|
Providers,
|
|
type OrderDetails,
|
|
} from "@customer-portal/domain/orders";
|
|
|
|
@Injectable()
|
|
export class SalesforceOrderService {
|
|
constructor(private readonly sf: SalesforceConnection) {}
|
|
|
|
async getOrderById(orderId: string): Promise<OrderDetails | null> {
|
|
// 1. Build query (infrastructure concern)
|
|
const fields = buildOrderSelectFields().join(", ");
|
|
const soql = `SELECT ${fields} FROM Order WHERE Id = '${orderId}'`;
|
|
|
|
// 2. Execute query
|
|
const result = await this.sf.query(soql);
|
|
if (!result.records?.[0]) return null;
|
|
|
|
// 3. Transform with domain mapper (SINGLE transformation!)
|
|
return Providers.Salesforce.transformOrderDetails(result.records[0], []);
|
|
}
|
|
}
|
|
```
|
|
|
|
**Integration Service Rules:**
|
|
- ✅ Build queries in utils (query builders)
|
|
- ✅ Execute API calls
|
|
- ✅ Use domain mappers for transformation
|
|
- ✅ Return domain types
|
|
- ❌ Never add business logic
|
|
- ❌ Never create wrapper/transformer services
|
|
- ❌ Never transform data twice
|
|
|
|
### Error Handling
|
|
|
|
**Never expose sensitive information to customers:**
|
|
|
|
```typescript
|
|
// ✅ CORRECT: Generic user-facing message
|
|
throw new HttpException(
|
|
"Unable to process your request. Please try again.",
|
|
HttpStatus.BAD_REQUEST
|
|
);
|
|
|
|
// ❌ WRONG: Exposes internal details
|
|
throw new HttpException(
|
|
`WHMCS API error: ${error.message}`,
|
|
HttpStatus.BAD_REQUEST
|
|
);
|
|
```
|
|
|
|
Log detailed errors server-side, return generic messages to clients.
|
|
|
|
---
|
|
|
|
## 🌐 Portal (Next.js Frontend)
|
|
|
|
### Directory Structure
|
|
|
|
```
|
|
apps/portal/src/
|
|
├── app/ # Next.js App Router (thin route wrappers)
|
|
│ ├── (public)/ # Marketing + auth routes
|
|
│ ├── (authenticated)/ # Signed-in portal routes
|
|
│ └── api/ # API routes
|
|
├── components/ # Shared UI (design system)
|
|
│ ├── ui/ # Atoms (Button, Input, Card)
|
|
│ ├── layout/ # Layout components
|
|
│ └── common/ # Molecules (DataTable, SearchBar)
|
|
├── features/ # Feature modules
|
|
│ └── {feature}/
|
|
│ ├── components/ # Feature-specific UI
|
|
│ ├── hooks/ # React Query hooks
|
|
│ ├── services/ # API service functions
|
|
│ ├── views/ # Page-level views
|
|
│ └── index.ts # Public exports
|
|
├── lib/ # Core utilities
|
|
│ ├── api/ # HTTP client
|
|
│ ├── hooks/ # Shared hooks
|
|
│ ├── services/ # Shared services
|
|
│ └── utils/ # Utility functions
|
|
└── styles/ # Global styles
|
|
```
|
|
|
|
### Feature Module Pattern
|
|
|
|
```typescript
|
|
// features/billing/hooks/use-invoices.ts
|
|
import { useQuery } from "@tanstack/react-query";
|
|
import type { InvoiceList } from "@customer-portal/domain/billing";
|
|
import { billingService } from "../services";
|
|
|
|
export function useInvoices(params?: { status?: string }) {
|
|
return useQuery<InvoiceList>({
|
|
queryKey: ["invoices", params],
|
|
queryFn: () => billingService.getInvoices(params),
|
|
});
|
|
}
|
|
|
|
// features/billing/services/billing.service.ts
|
|
import { apiClient } from "@/lib/api";
|
|
import type { InvoiceList } from "@customer-portal/domain/billing";
|
|
|
|
export const billingService = {
|
|
async getInvoices(params?: { status?: string }): Promise<InvoiceList> {
|
|
const response = await apiClient.get("/invoices", { params });
|
|
return response.data;
|
|
},
|
|
};
|
|
|
|
// features/billing/index.ts
|
|
export * from "./hooks";
|
|
export * from "./components";
|
|
```
|
|
|
|
### Page Component Rules
|
|
|
|
Pages are thin shells that compose features:
|
|
|
|
```typescript
|
|
// app/(authenticated)/billing/page.tsx
|
|
import { InvoicesView } from "@/features/billing";
|
|
|
|
export default function BillingPage() {
|
|
return <InvoicesView />;
|
|
}
|
|
```
|
|
|
|
**Frontend Rules:**
|
|
- ✅ Pages delegate to feature views
|
|
- ✅ Data fetching lives in feature hooks
|
|
- ✅ Business logic lives in feature services
|
|
- ✅ Use `@/` path aliases
|
|
- ❌ Never call APIs directly in page components
|
|
- ❌ Never import provider types (only domain contracts)
|
|
|
|
### Import Patterns
|
|
|
|
```typescript
|
|
// Feature imports
|
|
import { LoginForm, useAuth } from "@/features/auth";
|
|
|
|
// Component imports
|
|
import { Button, Input } from "@/components/ui";
|
|
import { DataTable } from "@/components/common";
|
|
|
|
// Type imports (domain types only!)
|
|
import type { Invoice } from "@customer-portal/domain/billing";
|
|
|
|
// Utility imports
|
|
import { apiClient } from "@/lib/api";
|
|
```
|
|
|
|
---
|
|
|
|
## ✅ Validation Patterns
|
|
|
|
### Query Parameters
|
|
|
|
Use `z.coerce` for URL query strings:
|
|
|
|
```typescript
|
|
// ✅ CORRECT: Coerce string to number
|
|
export const paginationSchema = z.object({
|
|
page: z.coerce.number().int().positive().optional(),
|
|
limit: z.coerce.number().int().positive().max(100).optional(),
|
|
});
|
|
|
|
// ❌ WRONG: Will fail on URL strings
|
|
export const paginationSchema = z.object({
|
|
page: z.number().optional(), // "1" !== 1
|
|
});
|
|
```
|
|
|
|
### Request Body Validation
|
|
|
|
```typescript
|
|
// In domain schema
|
|
export const createOrderRequestSchema = z.object({
|
|
items: z.array(orderItemSchema).min(1),
|
|
shippingAddressId: z.string().uuid(),
|
|
});
|
|
export type CreateOrderRequest = z.infer<typeof createOrderRequestSchema>;
|
|
```
|
|
|
|
### Form Validation (Frontend)
|
|
|
|
```typescript
|
|
import { useForm } from "react-hook-form";
|
|
import { zodResolver } from "@hookform/resolvers/zod";
|
|
import { loginRequestSchema, type LoginRequest } from "@customer-portal/domain/auth";
|
|
|
|
function LoginForm() {
|
|
const form = useForm<LoginRequest>({
|
|
resolver: zodResolver(loginRequestSchema),
|
|
});
|
|
// ...
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 🎨 UI Design Guidelines
|
|
|
|
Follow minimal, clean UI design principles:
|
|
|
|
- **Avoid excessive animations** - Use subtle, purposeful transitions
|
|
- **Clean typography** - Use system fonts or carefully chosen web fonts
|
|
- **Consistent spacing** - Follow the design system tokens
|
|
- **Accessibility first** - Proper ARIA labels, keyboard navigation
|
|
- **Mobile responsive** - Test on all breakpoints
|
|
|
|
Use shadcn/ui components and Tailwind CSS utilities.
|
|
|
|
---
|
|
|
|
## 🔒 Security Rules
|
|
|
|
1. **Never expose sensitive data** in error messages or responses
|
|
2. **Validate all inputs** at API boundaries (Zod schemas)
|
|
3. **Use parameterized queries** (Prisma handles this)
|
|
4. **Sanitize user content** before display
|
|
5. **Check authorization** in every endpoint
|
|
|
|
---
|
|
|
|
## 📝 Naming Conventions
|
|
|
|
### Files
|
|
|
|
- **Components**: PascalCase (`InvoiceCard.tsx`)
|
|
- **Hooks**: camelCase with `use` prefix (`useInvoices.ts`)
|
|
- **Services**: kebab-case (`billing.service.ts`)
|
|
- **Types/Schemas**: kebab-case (`invoice.schema.ts`)
|
|
- **Utils**: kebab-case (`format-currency.ts`)
|
|
|
|
### Code
|
|
|
|
- **Types/Interfaces**: PascalCase (`Invoice`, `OrderDetails`)
|
|
- **Schemas**: camelCase with `Schema` suffix (`invoiceSchema`)
|
|
- **Constants**: SCREAMING_SNAKE_CASE (`INVOICE_STATUS`)
|
|
- **Functions**: camelCase (`getInvoices`, `transformOrder`)
|
|
- **React Components**: PascalCase (`InvoiceList`)
|
|
|
|
### Services
|
|
|
|
- ❌ Avoid `V2` suffix in service names
|
|
- ✅ Use clear, descriptive names (`InvoicesOrchestratorService`)
|
|
|
|
---
|
|
|
|
## 🚫 Anti-Patterns to Avoid
|
|
|
|
### Domain Layer
|
|
|
|
- ❌ Framework-specific imports (no React/NestJS in domain)
|
|
- ❌ Circular dependencies
|
|
- ❌ Exposing raw provider types to application code
|
|
|
|
### BFF
|
|
|
|
- ❌ Business logic in controllers
|
|
- ❌ Direct Zod imports in controllers (use schemas from domain)
|
|
- ❌ Creating transformer/wrapper services
|
|
- ❌ Multiple data transformations
|
|
- ❌ Exposing internal error details
|
|
|
|
### Portal
|
|
|
|
- ❌ API calls in page components
|
|
- ❌ Business logic in UI components
|
|
- ❌ Importing provider types/mappers
|
|
- ❌ Duplicate type definitions
|
|
- ❌ Using `window.location` for navigation (use `next/link` or `useRouter`)
|
|
|
|
### General
|
|
|
|
- ❌ Using `any` type (especially in public APIs)
|
|
- ❌ Unsafe type assertions
|
|
- ❌ Console.log in production code (use proper logger)
|
|
- ❌ Guessing API response structures
|
|
|
|
---
|
|
|
|
## 🔄 Data Flow Summary
|
|
|
|
### Inbound (External API → Application)
|
|
|
|
```
|
|
External API Response
|
|
↓
|
|
Raw Provider Types (domain/*/providers/*/raw.types.ts)
|
|
↓
|
|
Provider Mapper (domain/*/providers/*/mapper.ts) [BFF only]
|
|
↓
|
|
Zod Schema Validation (domain/*/schema.ts)
|
|
↓
|
|
Domain Contract (domain/*/contract.ts)
|
|
↓
|
|
Application Code (BFF services, Portal hooks)
|
|
```
|
|
|
|
### Outbound (Application → External API)
|
|
|
|
```
|
|
Application Intent
|
|
↓
|
|
Domain Contract
|
|
↓
|
|
Provider Mapper (BFF only)
|
|
↓
|
|
Raw Provider Payload
|
|
↓
|
|
External API Request
|
|
```
|
|
|
|
---
|
|
|
|
## 📋 Checklist for New Features
|
|
|
|
1. [ ] Define types in `packages/domain/{feature}/contract.ts`
|
|
2. [ ] Add Zod schemas in `packages/domain/{feature}/schema.ts`
|
|
3. [ ] Export from `packages/domain/{feature}/index.ts`
|
|
4. [ ] (If provider needed) Add raw types and mapper in `providers/{provider}/`
|
|
5. [ ] Create BFF module in `apps/bff/src/modules/{feature}/`
|
|
6. [ ] Create controller with Zod DTOs
|
|
7. [ ] Create orchestrator service
|
|
8. [ ] Create portal feature in `apps/portal/src/features/{feature}/`
|
|
9. [ ] Add hooks, services, and components
|
|
10. [ ] Create page in `apps/portal/src/app/`
|
|
|
|
---
|
|
|
|
## 🛠️ Development Commands
|
|
|
|
```bash
|
|
# Development
|
|
pnpm dev # Start all apps
|
|
pnpm dev:bff # Start BFF only
|
|
pnpm dev:portal # Start Portal only
|
|
|
|
# Type checking
|
|
pnpm typecheck # Check all packages
|
|
pnpm lint # Run ESLint
|
|
|
|
# Database
|
|
pnpm db:migrate # Run migrations
|
|
pnpm db:generate # Generate Prisma client
|
|
```
|
|
|
|
---
|
|
|
|
**Last Updated**: December 2025
|
|
|