Assist_Design/docs/TYPE-CLEANUP-GUIDE.md

12 KiB

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:

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:

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:

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:

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

// BAD - duplicating contract in BFF
export interface Invoice {
  id: string;
  amount: number;
  // ...
}
// GOOD - import from contracts
import type { Invoice } from "@customer-portal/contracts/billing";

Don't skip schema validation at boundaries

// BAD - trusting external data
const invoice = whmcsResponse as Invoice;
// GOOD - validate with schema
import { transformWhmcsInvoice } from "@customer-portal/integrations-whmcs/mappers";

const invoice = transformWhmcsInvoice(whmcsResponse); // Validates internally

Don't use legacy domain imports

// BAD - old path
import type { Invoice } from "@customer-portal/domain";
// GOOD - new path
import type { Invoice } from "@customer-portal/contracts/billing";

🔧 Migration Checklist

For WHMCS Order Workflows

  • Create whmcs/order.schema.ts with WhmcsOrderItem, WhmcsAddOrderParams, etc.
  • Move buildWhmcsAddOrderPayload() to whmcs/mappers/order.mapper.ts
  • Update WhmcsOrderService to use shared mapper
  • Update BFF order orchestrator to consume mapper outputs
  • Add unit tests for mapper functions

For Freebit Requests

  • Create freebit/requests/esim-activation.schema.ts
  • Create freebit/requests/features.schema.ts
  • Update FreebitOperationsService to validate requests through schemas
  • Centralize options normalization in integration package
  • Add regression tests for schema validation

For Portal Alignment

  • 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

  • Document layer rules in ARCHITECTURE.md
  • Create this TYPE-CLEANUP-GUIDE.md
  • Add ESLint rules preventing deep imports from legacy paths

📚 Import Examples

BFF (NestJS)

// 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)

// 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

// ✅ 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.