Assist_Design/.cursorrules
barsa 3030d12138 Update Domain Import Practices and Enhance Documentation
- 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.
2025-12-26 15:07:47 +09:00

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