357 lines
12 KiB
Markdown
357 lines
12 KiB
Markdown
|
|
# 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.
|
||
|
|
|