Assist_Design/docs/PACKAGE-ORGANIZATION.md

314 lines
10 KiB
Markdown
Raw Normal View History

# Package Organization & "Lib" Files Strategy
**Status**: ✅ Implemented
**Date**: October 2025
---
## 🎯 Core Principle: Domain Package = Pure Domain Logic
The `@customer-portal/domain` package should contain **ONLY** pure domain logic that is:
- ✅ Framework-agnostic
- ✅ Reusable across frontend and backend
- ✅ Pure TypeScript (no React, no NestJS, no Next.js)
- ✅ No external infrastructure dependencies
---
## 📦 Package Structure Matrix
| Type | Location | Examples | Reasoning |
|------|----------|----------|-----------|
| **Domain Types** | `packages/domain/*/contract.ts` | `Invoice`, `Order`, `Customer` | Pure business entities |
| **Validation Schemas** | `packages/domain/*/schema.ts` | `invoiceSchema`, `orderQueryParamsSchema` | Runtime validation |
| **Pure Utilities** | `packages/domain/toolkit/` | `formatCurrency()`, `parseDate()` | No framework dependencies |
| **Provider Mappers** | `packages/domain/*/providers/` | `transformWhmcsInvoice()` | Data transformation logic |
| **Framework Utils** | `apps/*/src/lib/` or `apps/*/src/core/` | API clients, React hooks | Framework-specific code |
| **Shared Infra** | `packages/validation`, `packages/logging` | `ZodPipe`, `useZodForm` | Framework bridges |
---
## 📂 Detailed Breakdown
### ✅ **What Belongs in `packages/domain/`**
#### 1. Domain Types & Contracts
```typescript
// packages/domain/billing/contract.ts
export interface Invoice {
id: number;
status: InvoiceStatus;
total: number;
}
```
#### 2. Validation Schemas (Zod)
```typescript
// packages/domain/billing/schema.ts
export const invoiceSchema = z.object({
id: z.number(),
status: invoiceStatusSchema,
total: z.number(),
});
// Domain-specific query params
export const invoiceQueryParamsSchema = z.object({
page: z.coerce.number().int().positive().optional(),
status: invoiceStatusSchema.optional(),
dateFrom: z.string().datetime().optional(),
});
```
#### 3. Pure Utility Functions
```typescript
// packages/domain/toolkit/formatting/currency.ts
export function formatCurrency(
amount: number,
currency: SupportedCurrency
): string {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency,
}).format(amount);
}
```
**Pure function** - no React, no DOM, no framework
**Business logic** - directly related to domain entities
**Reusable** - both frontend and backend can use it
#### 4. Provider Mappers
```typescript
// packages/domain/billing/providers/whmcs/mapper.ts
export function transformWhmcsInvoice(raw: WhmcsInvoiceRaw): Invoice {
return {
id: raw.invoiceid,
status: mapStatus(raw.status),
total: parseFloat(raw.total),
};
}
```
---
### ❌ **What Should NOT Be in `packages/domain/`**
#### 1. Framework-Specific API Clients
```typescript
// ❌ DO NOT put in domain
// apps/portal/src/lib/api/client.ts
import { ApiClient } from "@hey-api/client-fetch"; // ← Framework dependency
export const apiClient = new ApiClient({
baseUrl: process.env.NEXT_PUBLIC_API_URL, // ← Next.js specific
});
```
**Why?**
- Depends on `@hey-api/client-fetch` (external library)
- Uses Next.js environment variables
- Runtime infrastructure code
#### 2. React Hooks
```typescript
// ❌ DO NOT put in domain
// apps/portal/src/lib/hooks/useInvoices.ts
import { useQuery } from "@tanstack/react-query"; // ← React dependency
export function useInvoices() {
return useQuery({ ... }); // ← React-specific
}
```
**Why?** React-specific - backend can't use this
#### 3. Error Handling with Framework Dependencies
```typescript
// ❌ DO NOT put in domain
// apps/portal/src/lib/utils/error-handling.ts
import { ApiError as ClientApiError } from "@/lib/api"; // ← Framework client
export function getErrorInfo(error: unknown): ApiErrorInfo {
if (error instanceof ClientApiError) { // ← Framework-specific error type
// ...
}
}
```
**Why?** Depends on the API client implementation
#### 4. NestJS-Specific Utilities
```typescript
// ❌ DO NOT put in domain
// apps/bff/src/core/utils/error.util.ts
export function getErrorMessage(error: unknown): string {
// Generic error extraction - could be in domain
if (error instanceof Error) {
return error.message;
}
return String(error);
}
```
**This one is borderline** - it's generic enough it COULD be in domain, but:
- Only used by backend
- Not needed for type definitions
- Better to keep application-specific utils in apps
---
## 🎨 Current Architecture (Correct!)
```
packages/
├── domain/ # ✅ Pure domain logic
│ ├── billing/
│ │ ├── contract.ts # ✅ Types
│ │ ├── schema.ts # ✅ Zod schemas + domain query params
│ │ └── providers/whmcs/ # ✅ Data mappers
│ ├── common/
│ │ ├── types.ts # ✅ Truly generic types (ApiResponse, PaginationParams)
│ │ └── schema.ts # ✅ Truly generic schemas (paginationParamsSchema, emailSchema)
│ └── toolkit/
│ ├── formatting/ # ✅ Pure functions (formatCurrency, formatDate)
│ └── validation/ # ✅ Pure validation helpers
├── validation/ # ✅ Framework bridges
│ ├── src/zod-pipe.ts # NestJS Zod pipe
│ └── src/zod-form.ts # React Zod form hook
└── logging/ # ✅ Infrastructure
└── src/logger.ts # Logging utilities
apps/
├── portal/ (Next.js)
│ └── src/lib/
│ ├── api/ # ❌ Framework-specific
│ │ ├── client.ts # API client instance
│ │ └── runtime/ # Generated client code
│ ├── hooks/ # ❌ React-specific
│ │ └── useAuth.ts # React Query hooks
│ └── utils/ # ❌ App-specific
│ ├── error-handling.ts # Portal error handling
│ └── cn.ts # Tailwind utility
└── bff/ (NestJS)
└── src/core/
├── validation/ # ❌ NestJS-specific
│ └── zod-validation.filter.ts
└── utils/ # ❌ App-specific
├── error.util.ts # BFF error utilities
└── validation.util.ts # BFF validation helpers
```
---
## 🔄 Decision Framework: "Should This Be in Domain?"
Ask these questions:
### ✅ Move to Domain If:
1. Is it a **pure function** with no framework dependencies?
2. Does it work with **domain types**?
3. Could **both frontend and backend** use it?
4. Is it **business logic** (not infrastructure)?
### ❌ Keep in Apps If:
1. Does it import **React**, **Next.js**, or **NestJS**?
2. Does it use **environment variables**?
3. Does it depend on **external libraries** (API clients, HTTP libs)?
4. Is it **UI-specific** or **framework-specific**?
5. Is it only used in **one app**?
---
## 📋 Examples with Decisions
| Utility | Decision | Location | Why |
|---------|----------|----------|-----|
| `formatCurrency(amount, currency)` | ✅ Domain | `domain/toolkit/formatting/` | Pure function, no deps |
| `invoiceQueryParamsSchema` | ✅ Domain | `domain/billing/schema.ts` | Domain-specific validation |
| `paginationParamsSchema` | ✅ Domain | `domain/common/schema.ts` | Truly generic |
| `useInvoices()` React hook | ❌ Portal App | `portal/lib/hooks/` | React-specific |
| `apiClient` instance | ❌ Portal App | `portal/lib/api/` | Framework-specific |
| `ZodValidationPipe` | ✅ Validation Package | `packages/validation/` | Reusable bridge |
| `getErrorMessage(error)` | ❌ BFF App | `bff/core/utils/` | App-specific utility |
| `transformWhmcsInvoice()` | ✅ Domain | `domain/billing/providers/whmcs/` | Data transformation |
---
## 🚀 What About Query Parameters?
### Current Structure (✅ Correct!)
**Generic building blocks** in `domain/common/`:
```typescript
// packages/domain/common/schema.ts
export const paginationParamsSchema = z.object({
page: z.coerce.number().int().positive().optional(),
limit: z.coerce.number().int().positive().max(100).optional(),
});
export const filterParamsSchema = z.object({
search: z.string().optional(),
sortBy: z.string().optional(),
sortOrder: z.enum(["asc", "desc"]).optional(),
});
```
**Domain-specific query params** in their own domains:
```typescript
// packages/domain/billing/schema.ts
export const invoiceQueryParamsSchema = z.object({
page: z.coerce.number().int().positive().optional(),
limit: z.coerce.number().int().positive().max(100).optional(),
status: invoiceStatusSchema.optional(), // ← Domain-specific
dateFrom: z.string().datetime().optional(), // ← Domain-specific
dateTo: z.string().datetime().optional(), // ← Domain-specific
});
// packages/domain/subscriptions/schema.ts
export const subscriptionQueryParamsSchema = z.object({
page: z.coerce.number().int().positive().optional(),
limit: z.coerce.number().int().positive().max(100).optional(),
status: subscriptionStatusSchema.optional(), // ← Domain-specific
type: z.string().optional(), // ← Domain-specific
});
```
**Why this works:**
- `common` has truly generic utilities
- Each domain owns its specific query parameters
- No duplication of business logic
---
## 📖 Summary
### Domain Package (`packages/domain/`)
**Contains:**
- ✅ Domain types & interfaces
- ✅ Zod validation schemas
- ✅ Provider mappers (WHMCS, Salesforce, Freebit)
- ✅ Pure utility functions (formatting, parsing)
- ✅ Domain-specific query parameter schemas
**Does NOT contain:**
- ❌ React hooks
- ❌ API client instances
- ❌ Framework-specific code
- ❌ Infrastructure code
### App Lib/Core Directories
**Contains:**
- ✅ Framework-specific utilities
- ✅ API clients & HTTP interceptors
- ✅ React hooks & custom hooks
- ✅ Error handling with framework dependencies
- ✅ Application-specific helpers
---
**Key Takeaway**: The domain package is your **single source of truth for types and validation**. Everything else stays in apps where it belongs!