# 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; // ❌ 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 { 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 { // 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({ 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 { 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 ; } ``` **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; ``` ### 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({ 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