Assist_Design/docs/TYPE-CLEANUP-GUIDE.md

357 lines
12 KiB
Markdown
Raw Normal View History

# Type Cleanup & Architecture Guide
## 🎯 Goal
Establish a single source of truth for every cross-layer contract so backend integrations, internal services, and the Portal all share identical type definitions and runtime validation.
**No business code should re-declare data shapes, and every external payload must be validated exactly once at the boundary.**
---
## 📐 Ideal State
### Layer Architecture
```
┌─────────────────────────────────────────────────┐
@customer-portal/contracts │
│ Pure TypeScript interfaces (no runtime deps) │
│ → billing, subscriptions, payments, SIM, etc. │
└────────────────┬────────────────────────────────┘
┌─────────────────────────────────────────────────┐
@customer-portal/schemas │
│ Zod validators for each contract │
│ → Billing, SIM, Payments, Integrations │
└────────────────┬────────────────────────────────┘
┌─────────────────────────────────────────────────┐
│ Integration Packages │
│ → WHMCS mappers (billing, orders, payments) │
│ → Freebit mappers (SIM operations) │
│ Transform raw API data → contracts │
└────────────────┬────────────────────────────────┘
┌─────────────────────────────────────────────────┐
│ Application Layers │
│ → BFF (NestJS): Orchestrates integrations │
│ → Portal (Next.js): UI and client logic │
│ Only import from contracts/schemas │
└─────────────────────────────────────────────────┘
```
---
## 🗂️ Package Structure
### 1. Contracts (`packages/contracts/src/`)
**Purpose**: Pure TypeScript types - the single source of truth.
```
packages/contracts/src/
├── billing/
│ ├── invoice.ts # Invoice, InvoiceItem, InvoiceList
│ └── index.ts
├── subscriptions/
│ ├── subscription.ts # Subscription, SubscriptionList
│ └── index.ts
├── payments/
│ ├── payment.ts # PaymentMethod, PaymentGateway
│ └── index.ts
├── sim/
│ ├── sim-details.ts # SimDetails, SimUsage, SimTopUpHistory
│ └── index.ts
├── orders/
│ ├── order.ts # Order, OrderItem, FulfillmentOrderItem
│ └── index.ts
└── freebit/
├── requests.ts # Freebit request payloads
└── index.ts
```
**Usage**:
```typescript
import type { Invoice, InvoiceItem } from "@customer-portal/contracts/billing";
import type { SimDetails } from "@customer-portal/contracts/sim";
```
---
### 2. Schemas (`packages/schemas/src/`)
**Purpose**: Runtime validation using Zod schemas.
```
packages/schemas/src/
├── billing/
│ ├── invoice.schema.ts # invoiceSchema, invoiceListSchema
│ └── index.ts
├── subscriptions/
│ ├── subscription.schema.ts
│ └── index.ts
├── payments/
│ ├── payment.schema.ts
│ └── index.ts
├── sim/
│ ├── sim.schema.ts
│ └── index.ts
└── integrations/
├── whmcs/
│ ├── invoice.schema.ts # Raw WHMCS invoice schemas
│ ├── payment.schema.ts
│ ├── product.schema.ts
│ ├── order.schema.ts # NEW: WHMCS AddOrder schemas
│ └── index.ts
└── freebit/
├── account.schema.ts # Raw Freebit response schemas
├── traffic.schema.ts
├── quota.schema.ts
└── requests/
├── topup.schema.ts
├── plan-change.schema.ts
├── esim-activation.schema.ts # NEW
├── features.schema.ts # NEW
└── index.ts
```
**Usage**:
```typescript
import { invoiceSchema } from "@customer-portal/schemas/billing";
import { whmcsAddOrderParamsSchema } from "@customer-portal/schemas/integrations/whmcs/order.schema";
import { freebitEsimActivationParamsSchema } from "@customer-portal/schemas/integrations/freebit/requests/esim-activation.schema";
// Validate at the boundary
const validated = invoiceSchema.parse(externalData);
```
---
### 3. Integration Packages
#### WHMCS Integration (`packages/integrations/whmcs/`)
```
packages/integrations/whmcs/src/
├── mappers/
│ ├── invoice.mapper.ts # transformWhmcsInvoice()
│ ├── subscription.mapper.ts # transformWhmcsSubscription()
│ ├── payment.mapper.ts # transformWhmcsPaymentMethod()
│ ├── order.mapper.ts # mapFulfillmentOrderItems(), buildWhmcsAddOrderPayload()
│ └── index.ts
├── utils/
│ └── index.ts
└── index.ts
```
**Key Functions**:
- `transformWhmcsInvoice(raw)``Invoice`
- `transformWhmcsSubscription(raw)``Subscription`
- `mapFulfillmentOrderItems(items)``{ whmcsItems, summary }`
- `buildWhmcsAddOrderPayload(params)` → WHMCS API payload
- `createOrderNotes(sfOrderId, notes)` → formatted order notes
**Usage in BFF**:
```typescript
import { transformWhmcsInvoice, buildWhmcsAddOrderPayload } from "@customer-portal/integrations-whmcs/mappers";
// Transform WHMCS raw data
const invoice = transformWhmcsInvoice(whmcsResponse);
// Build order payload
const payload = buildWhmcsAddOrderPayload({
clientId: 123,
items: mappedItems,
paymentMethod: "stripe",
});
```
#### Freebit Integration (`packages/integrations/freebit/`)
```
packages/integrations/freebit/src/
├── mappers/
│ ├── sim.mapper.ts # transformFreebitAccountDetails(), transformFreebitTrafficInfo()
│ └── index.ts
├── utils/
│ ├── normalize.ts # normalizeAccount()
│ └── index.ts
└── index.ts
```
**Key Functions**:
- `transformFreebitAccountDetails(raw)``SimDetails`
- `transformFreebitTrafficInfo(raw)``SimUsage`
- `transformFreebitQuotaHistory(raw)``SimTopUpHistory[]`
- `normalizeAccount(account)` → normalized MSISDN
**Usage in BFF**:
```typescript
import { transformFreebitAccountDetails } from "@customer-portal/integrations-freebit/mappers";
import { normalizeAccount } from "@customer-portal/integrations-freebit/utils";
const simDetails = transformFreebitAccountDetails(freebitResponse);
const account = normalizeAccount(msisdn);
```
---
## 🚫 Anti-Patterns to Avoid
### ❌ Don't re-declare types in application code
```typescript
// BAD - duplicating contract in BFF
export interface Invoice {
id: string;
amount: number;
// ...
}
```
```typescript
// GOOD - import from contracts
import type { Invoice } from "@customer-portal/contracts/billing";
```
### ❌ Don't skip schema validation at boundaries
```typescript
// BAD - trusting external data
const invoice = whmcsResponse as Invoice;
```
```typescript
// GOOD - validate with schema
import { transformWhmcsInvoice } from "@customer-portal/integrations-whmcs/mappers";
const invoice = transformWhmcsInvoice(whmcsResponse); // Validates internally
```
### ❌ Don't use legacy domain imports
```typescript
// BAD - old path
import type { Invoice } from "@customer-portal/domain";
```
```typescript
// GOOD - new path
import type { Invoice } from "@customer-portal/contracts/billing";
```
---
## 🔧 Migration Checklist
### For WHMCS Order Workflows
- [x] Create `whmcs/order.schema.ts` with `WhmcsOrderItem`, `WhmcsAddOrderParams`, etc.
- [x] Move `buildWhmcsAddOrderPayload()` to `whmcs/mappers/order.mapper.ts`
- [x] Update `WhmcsOrderService` to use shared mapper
- [x] Update BFF order orchestrator to consume mapper outputs
- [ ] Add unit tests for mapper functions
### For Freebit Requests
- [x] Create `freebit/requests/esim-activation.schema.ts`
- [x] Create `freebit/requests/features.schema.ts`
- [x] Update `FreebitOperationsService` to validate requests through schemas
- [ ] Centralize options normalization in integration package
- [ ] Add regression tests for schema validation
### For Portal Alignment
- [x] Update SIM components to import from `@customer-portal/contracts/sim`
- [ ] Remove lingering `@customer-portal/domain` imports
- [ ] Update API client typings to use shared contracts
### For Governance
- [x] Document layer rules in `ARCHITECTURE.md`
- [x] Create this `TYPE-CLEANUP-GUIDE.md`
- [ ] Add ESLint rules preventing deep imports from legacy paths
---
## 📚 Import Examples
### BFF (NestJS)
```typescript
// Controllers
import type { Invoice, InvoiceList } from "@customer-portal/contracts/billing";
import { invoiceListSchema } from "@customer-portal/schemas/billing";
// Services
import { transformWhmcsInvoice } from "@customer-portal/integrations-whmcs/mappers";
// Operations
const invoices = rawInvoices.map(transformWhmcsInvoice);
const validated = invoiceListSchema.parse({ invoices, total });
```
### Portal (Next.js)
```typescript
// Components
import type { SimDetails } from "@customer-portal/contracts/sim";
import type { Invoice } from "@customer-portal/contracts/billing";
// API Client
export async function fetchInvoices(): Promise<InvoiceList> {
const response = await api.get<InvoiceList>("/api/invoices");
return response.data;
}
```
---
## 🎓 Best Practices
1. **Always validate at boundaries**: Use schemas when receiving data from external APIs
2. **Import from subpaths**: Use `@customer-portal/contracts/billing` not `@customer-portal/contracts`
3. **Use mappers in integrations**: Keep transformation logic in integration packages
4. **Don't export Zod schemas from contracts**: Contracts are type-only, schemas are runtime
5. **Keep integrations thin in BFF**: Let integration packages handle complex mapping
---
## 🔍 Finding the Right Import
| What you need | Where to import from |
|---------------|---------------------|
| TypeScript type definition | `@customer-portal/contracts/{domain}` |
| Runtime validation | `@customer-portal/schemas/{domain}` |
| External API validation | `@customer-portal/schemas/integrations/{provider}` |
| Transform external data | `@customer-portal/integrations-{provider}/mappers` |
| Normalize/format helpers | `@customer-portal/integrations-{provider}/utils` |
---
## 💡 Quick Reference
```typescript
// ✅ CORRECT Usage Pattern
// 1. Import types from contracts
import type { Invoice } from "@customer-portal/contracts/billing";
// 2. Import schema for validation
import { invoiceSchema } from "@customer-portal/schemas/billing";
// 3. Import mapper from integration
import { transformWhmcsInvoice } from "@customer-portal/integrations-whmcs/mappers";
// 4. Use in service
const invoice: Invoice = transformWhmcsInvoice(rawData); // Auto-validates
const validated = invoiceSchema.parse(invoice); // Optional explicit validation
```
---
**Last Updated**: 2025-10-03
For questions or clarifications, refer to `docs/ARCHITECTURE.md` or the package README files.