# 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 { const response = await api.get("/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.