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.
This commit is contained in:
parent
a3dbd07183
commit
3030d12138
530
.cursorrules
Normal file
530
.cursorrules
Normal file
@ -0,0 +1,530 @@
|
||||
# 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
|
||||
|
||||
@ -14,6 +14,44 @@
|
||||
- **BFF-only (integration/infrastructure)**:
|
||||
- `@customer-portal/domain/<module>/providers`
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Context | Import Pattern | Example |
|
||||
| ----------------------- | ---------------------------------- | ------------------------------------------------------------------- |
|
||||
| Portal/BFF domain types | `@customer-portal/domain/<module>` | `import { Invoice } from "@customer-portal/domain/billing"` |
|
||||
| BFF provider adapters | `.../<module>/providers` | `import { Whmcs } from "@customer-portal/domain/billing/providers"` |
|
||||
| Toolkit utilities | `@customer-portal/domain/toolkit` | `import { Formatting } from "@customer-portal/domain/toolkit"` |
|
||||
|
||||
## Architecture Diagram
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
subgraph Portal [Portal App]
|
||||
PC[PortalComponents]
|
||||
end
|
||||
|
||||
subgraph BFF [BFF App]
|
||||
BC[BFFControllers]
|
||||
BI[BFFIntegrations]
|
||||
end
|
||||
|
||||
subgraph Domain [Domain Package]
|
||||
DM[ModuleEntrypoints]
|
||||
DP[ProviderEntrypoints]
|
||||
DT[Toolkit]
|
||||
end
|
||||
|
||||
PC -->|allowed| DM
|
||||
PC -->|allowed| DT
|
||||
PC -.->|BLOCKED| DP
|
||||
|
||||
BC -->|allowed| DM
|
||||
BC -->|allowed| DT
|
||||
BI -->|allowed| DM
|
||||
BI -->|allowed| DP
|
||||
BI -->|allowed| DT
|
||||
```
|
||||
|
||||
### Never
|
||||
|
||||
- `@customer-portal/domain/<module>/**` (anything deeper than the module entrypoint)
|
||||
@ -51,3 +89,11 @@ Where to export it:
|
||||
- No `@customer-portal/domain/*/*` imports (except exact `.../<module>/providers` in BFF).
|
||||
- Portal has **zero** `.../providers` imports.
|
||||
- No wildcard subpath exports added to `packages/domain/package.json#exports`.
|
||||
|
||||
## Validation
|
||||
|
||||
After changes:
|
||||
|
||||
1. Run `pnpm lint`
|
||||
2. Run `pnpm type-check`
|
||||
3. Run `pnpm build`
|
||||
|
||||
@ -24,6 +24,7 @@
|
||||
"format:check": "prettier -c .",
|
||||
"prepare": "husky",
|
||||
"type-check": "pnpm --filter @customer-portal/domain run type-check && pnpm --filter @customer-portal/bff --filter @customer-portal/portal run type-check",
|
||||
"check:imports": "node scripts/check-domain-imports.mjs",
|
||||
"clean": "pnpm --recursive run clean",
|
||||
"dev:start": "./scripts/dev/manage.sh start",
|
||||
"dev:stop": "./scripts/dev/manage.sh stop",
|
||||
|
||||
@ -12,21 +12,3 @@ export * from "./constants.js";
|
||||
|
||||
// Schemas (includes derived types)
|
||||
export * from "./schema.js";
|
||||
|
||||
// Re-export types for convenience
|
||||
export type {
|
||||
Currency,
|
||||
InvoiceStatus,
|
||||
InvoiceItem,
|
||||
Invoice,
|
||||
InvoiceIdParam,
|
||||
InvoicePagination,
|
||||
InvoiceList,
|
||||
InvoiceSsoLink,
|
||||
PaymentInvoiceRequest,
|
||||
BillingSummary,
|
||||
InvoiceQueryParams,
|
||||
InvoiceListQuery,
|
||||
InvoiceSsoQuery,
|
||||
InvoicePaymentLinkQuery,
|
||||
} from "./schema.js";
|
||||
|
||||
@ -5,12 +5,3 @@
|
||||
export * from "./contract.js";
|
||||
export * from "./schema.js";
|
||||
export * from "./validation.js";
|
||||
|
||||
// Re-export types for convenience
|
||||
export type {
|
||||
MappingSearchFilters,
|
||||
MappingStats,
|
||||
BulkMappingOperation,
|
||||
BulkMappingResult,
|
||||
} from "./schema.js";
|
||||
export type { MappingValidationResult } from "./contract.js";
|
||||
|
||||
@ -58,28 +58,6 @@ export {
|
||||
calculateOrderTotals,
|
||||
formatScheduledDate,
|
||||
} from "./helpers.js";
|
||||
// Re-export types for convenience
|
||||
export type {
|
||||
// Order item types
|
||||
OrderItemSummary,
|
||||
OrderItemDetails,
|
||||
// Order types
|
||||
OrderSummary,
|
||||
OrderDetails,
|
||||
// Query and creation types
|
||||
OrderQueryParams,
|
||||
OrderConfigurationsAddress,
|
||||
OrderConfigurations,
|
||||
CreateOrderRequest,
|
||||
OrderBusinessValidation,
|
||||
SfOrderIdParam,
|
||||
OrderListResponse,
|
||||
// Display types
|
||||
OrderDisplayItem,
|
||||
OrderDisplayItemCategory,
|
||||
OrderDisplayItemCharge,
|
||||
OrderDisplayItemChargeKind,
|
||||
} from "./schema.js";
|
||||
|
||||
// Provider adapters
|
||||
// NOTE: Provider adapters are intentionally not exported from the module root.
|
||||
|
||||
@ -11,13 +11,3 @@ export { PAYMENT_METHOD_TYPE, PAYMENT_GATEWAY_TYPE } from "./contract.js";
|
||||
|
||||
// Schemas (includes derived types)
|
||||
export * from "./schema.js";
|
||||
|
||||
// Re-export types for convenience
|
||||
export type {
|
||||
PaymentMethodType,
|
||||
PaymentMethod,
|
||||
PaymentMethodList,
|
||||
PaymentGatewayType,
|
||||
PaymentGateway,
|
||||
PaymentGatewayList,
|
||||
} from "./schema.js";
|
||||
|
||||
@ -11,28 +11,5 @@ export { type PricingTier, type CatalogPriceInfo } from "./contract.js";
|
||||
// Schemas (includes derived types)
|
||||
export * from "./schema.js";
|
||||
|
||||
// Re-export types for convenience
|
||||
export type {
|
||||
CatalogProductBase,
|
||||
CatalogPricebookEntry,
|
||||
// Internet products
|
||||
InternetCatalogProduct,
|
||||
InternetPlanTemplate,
|
||||
InternetPlanCatalogItem,
|
||||
InternetInstallationCatalogItem,
|
||||
InternetAddonCatalogItem,
|
||||
InternetEligibilityStatus,
|
||||
InternetEligibilityDetails,
|
||||
InternetEligibilityRequest,
|
||||
InternetEligibilityRequestResponse,
|
||||
// SIM products
|
||||
SimCatalogProduct,
|
||||
SimActivationFeeCatalogItem,
|
||||
// VPN products
|
||||
VpnCatalogProduct,
|
||||
// Union type
|
||||
CatalogProduct,
|
||||
} from "./schema.js";
|
||||
|
||||
// Utilities
|
||||
export * from "./utils.js";
|
||||
|
||||
@ -25,62 +25,5 @@ export {
|
||||
getSimPlanLabel,
|
||||
buildSimFeaturesUpdatePayload,
|
||||
} from "./helpers.js";
|
||||
|
||||
// Re-export types for convenience
|
||||
export type {
|
||||
SimStatus,
|
||||
SimType,
|
||||
SimDetails,
|
||||
RecentDayUsage,
|
||||
SimUsage,
|
||||
SimTopUpHistoryEntry,
|
||||
SimTopUpHistory,
|
||||
SimInfo,
|
||||
// Portal-facing DTOs
|
||||
SimAvailablePlan,
|
||||
SimAvailablePlanArray,
|
||||
SimCancellationMonth,
|
||||
SimCancellationPreview,
|
||||
SimReissueFullRequest,
|
||||
SimCallHistoryPagination,
|
||||
SimDomesticCallRecord,
|
||||
SimDomesticCallHistoryResponse,
|
||||
SimInternationalCallRecord,
|
||||
SimInternationalCallHistoryResponse,
|
||||
SimSmsRecord,
|
||||
SimSmsHistoryResponse,
|
||||
SimHistoryMonth,
|
||||
SimHistoryAvailableMonths,
|
||||
SimCallHistoryImportResult,
|
||||
SimSftpFiles,
|
||||
SimSftpListResult,
|
||||
// Request types
|
||||
SimTopUpRequest,
|
||||
SimPlanChangeRequest,
|
||||
SimCancelRequest,
|
||||
SimTopUpHistoryRequest,
|
||||
SimFeaturesUpdateRequest,
|
||||
SimReissueRequest,
|
||||
SimConfigureFormData,
|
||||
SimCardType,
|
||||
ActivationType,
|
||||
MnpData,
|
||||
// Enhanced request types
|
||||
SimCancelFullRequest,
|
||||
SimTopUpFullRequest,
|
||||
SimChangePlanFullRequest,
|
||||
SimHistoryQuery,
|
||||
SimSftpListQuery,
|
||||
SimCallHistoryImportQuery,
|
||||
SimReissueEsimRequest,
|
||||
// Activation types
|
||||
SimOrderActivationRequest,
|
||||
SimOrderActivationMnp,
|
||||
SimOrderActivationAddons,
|
||||
// Pricing types
|
||||
SimTopUpPricing,
|
||||
SimTopUpPricingPreviewRequest,
|
||||
SimTopUpPricingPreviewResponse,
|
||||
} from "./schema.js";
|
||||
export type { SimPlanCode } from "./contract.js";
|
||||
export type { SimPlanOption, SimFeatureToggleSnapshot } from "./helpers.js";
|
||||
|
||||
@ -12,25 +12,6 @@ export { SUBSCRIPTION_STATUS, SUBSCRIPTION_CYCLE } from "./contract.js";
|
||||
// Schemas (includes derived types)
|
||||
export * from "./schema.js";
|
||||
|
||||
// Re-export types for convenience
|
||||
export type {
|
||||
SubscriptionStatus,
|
||||
SubscriptionCycle,
|
||||
Subscription,
|
||||
SubscriptionArray,
|
||||
SubscriptionList,
|
||||
SubscriptionIdParam,
|
||||
SubscriptionQueryParams,
|
||||
SubscriptionQuery,
|
||||
SubscriptionStats,
|
||||
SimActionResponse,
|
||||
SimPlanChangeResult,
|
||||
// Internet cancellation types
|
||||
InternetCancellationMonth,
|
||||
InternetCancellationPreview,
|
||||
InternetCancelRequest,
|
||||
} from "./schema.js";
|
||||
|
||||
// Re-export schemas for validation
|
||||
export {
|
||||
internetCancellationMonthSchema,
|
||||
|
||||
146
scripts/check-domain-imports.mjs
Normal file
146
scripts/check-domain-imports.mjs
Normal file
@ -0,0 +1,146 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Domain Import Boundary Checker
|
||||
*
|
||||
* Validates:
|
||||
* 1. No @customer-portal/domain (root) imports
|
||||
* 2. No deep imports beyond module/providers
|
||||
* 3. Portal has zero provider imports
|
||||
*/
|
||||
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
const ROOT = process.cwd();
|
||||
|
||||
const APPS_DIR = path.join(ROOT, "apps");
|
||||
const BFF_SRC_DIR = path.join(APPS_DIR, "bff", "src");
|
||||
const PORTAL_SRC_DIR = path.join(APPS_DIR, "portal", "src");
|
||||
|
||||
const FILE_EXTS = new Set([".ts", ".tsx", ".js", ".jsx"]);
|
||||
const IGNORE_DIRS = new Set(["node_modules", "dist", ".next", ".turbo", ".cache"]);
|
||||
|
||||
function toPos(text, idx) {
|
||||
// 1-based line/column
|
||||
let line = 1;
|
||||
let col = 1;
|
||||
for (let i = 0; i < idx; i += 1) {
|
||||
if (text.charCodeAt(i) === 10) {
|
||||
line += 1;
|
||||
col = 1;
|
||||
} else {
|
||||
col += 1;
|
||||
}
|
||||
}
|
||||
return { line, col };
|
||||
}
|
||||
|
||||
async function* walk(dir) {
|
||||
const entries = await fs.readdir(dir, { withFileTypes: true });
|
||||
for (const e of entries) {
|
||||
const p = path.join(dir, e.name);
|
||||
if (e.isDirectory()) {
|
||||
if (IGNORE_DIRS.has(e.name) || e.name.startsWith(".")) continue;
|
||||
yield* walk(p);
|
||||
continue;
|
||||
}
|
||||
if (!e.isFile()) continue;
|
||||
if (!FILE_EXTS.has(path.extname(e.name))) continue;
|
||||
yield p;
|
||||
}
|
||||
}
|
||||
|
||||
function collectDomainImports(code) {
|
||||
const results = [];
|
||||
|
||||
const patterns = [
|
||||
{ kind: "from", re: /\bfrom\s+['"]([^'"]+)['"]/g },
|
||||
{ kind: "import", re: /\bimport\s+['"]([^'"]+)['"]/g },
|
||||
{ kind: "dynamicImport", re: /\bimport\(\s*['"]([^'"]+)['"]\s*\)/g },
|
||||
{ kind: "require", re: /\brequire\(\s*['"]([^'"]+)['"]\s*\)/g },
|
||||
];
|
||||
|
||||
for (const { kind, re } of patterns) {
|
||||
for (const m of code.matchAll(re)) {
|
||||
const spec = m[1];
|
||||
if (!spec || !spec.startsWith("@customer-portal/domain")) continue;
|
||||
const idx = typeof m.index === "number" ? m.index : 0;
|
||||
results.push({ kind, spec, idx });
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
function validateSpecifier({ spec, isPortal }) {
|
||||
if (spec === "@customer-portal/domain") {
|
||||
return "Do not import @customer-portal/domain (root). Use @customer-portal/domain/<module> instead.";
|
||||
}
|
||||
|
||||
if (spec.includes("/src/")) {
|
||||
return "Import from @customer-portal/domain/<module> instead of internals.";
|
||||
}
|
||||
|
||||
if (spec.startsWith("@customer-portal/domain/toolkit/")) {
|
||||
return "Do not deep-import toolkit internals. Import from @customer-portal/domain/toolkit only.";
|
||||
}
|
||||
|
||||
if (/^@customer-portal\/domain\/[^/]+\/providers\/.+/.test(spec)) {
|
||||
return "Do not deep-import provider internals. Import from @customer-portal/domain/<module>/providers only.";
|
||||
}
|
||||
|
||||
if (/^@customer-portal\/domain\/[^/]+\/providers$/.test(spec)) {
|
||||
if (isPortal) {
|
||||
return "Portal must not import provider adapters/types. Import normalized domain models from @customer-portal/domain/<module> instead.";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Any 2+ segment import like @customer-portal/domain/a/b is illegal everywhere
|
||||
// (except the explicit .../<module>/providers entrypoint handled above).
|
||||
if (/^@customer-portal\/domain\/[^/]+\/[^/]+/.test(spec)) {
|
||||
return "No deep @customer-portal/domain imports. Use @customer-portal/domain/<module> (or BFF-only: .../<module>/providers).";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const errors = [];
|
||||
|
||||
// Broad scan: both apps for root/deep imports
|
||||
for (const baseDir of [BFF_SRC_DIR, PORTAL_SRC_DIR]) {
|
||||
for await (const file of walk(baseDir)) {
|
||||
const code = await fs.readFile(file, "utf8");
|
||||
const isPortal = file.startsWith(PORTAL_SRC_DIR + path.sep);
|
||||
|
||||
for (const imp of collectDomainImports(code)) {
|
||||
const message = validateSpecifier({ spec: imp.spec, isPortal });
|
||||
if (!message) continue;
|
||||
const pos = toPos(code, imp.idx);
|
||||
errors.push({
|
||||
file: path.relative(ROOT, file),
|
||||
line: pos.line,
|
||||
col: pos.col,
|
||||
spec: imp.spec,
|
||||
message,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
console.error(`[domain] ERROR: illegal domain imports detected (${errors.length})`);
|
||||
for (const e of errors) {
|
||||
console.error(`[domain] ${e.file}:${e.line}:${e.col} ${e.spec}`);
|
||||
console.error(` ${e.message}`);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log("[domain] OK: import contract checks passed.");
|
||||
}
|
||||
|
||||
await main();
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user