# ADR-004: Domain Provider Isolation **Date**: 2025-01-15 **Status**: Accepted ## Context The Customer Portal uses multiple external providers: - **WHMCS** for billing (invoices, payment methods, subscriptions) - **Salesforce** for CRM (accounts, orders, cases, products) - **Freebit** for SIM management - **Japan Post** for address lookup Each provider has its own API response structures. Application code should not need to know these details. ## Decision **Isolate provider-specific code** in `providers/` subdirectories within each domain module. Only BFF integration code imports from providers; Portal and application code use only normalized domain types. ``` packages/domain/billing/ ├── contract.ts # ✅ Import everywhere ├── schema.ts # ✅ Import everywhere ├── index.ts # ✅ Import everywhere └── providers/ # ⚠️ BFF-only imports └── whmcs/ ├── raw.types.ts # WHMCS-specific structures └── mapper.ts # WHMCS → domain transformation ``` ## Rationale ### Why Provider Isolation? 1. **Portal stays provider-agnostic**: Frontend code never knows if data came from WHMCS, Salesforce, or elsewhere 2. **Easy to add/swap providers**: Adding Stripe as a billing provider = add `providers/stripe/` folder. No changes to domain contract or application code. 3. **Clear import boundaries**: ESLint enforces that Portal cannot import from `providers/` 4. **Single responsibility**: Raw types and mappers live together, making it clear what's provider-specific ### Import Rules (ESLint Enforced) ```typescript // ✅ Allowed everywhere (Portal + BFF) import { Invoice, invoiceSchema } from "@customer-portal/domain/billing"; // ✅ Allowed in BFF only import { transformWhmcsInvoice } from "@customer-portal/domain/billing/providers"; // ❌ Forbidden in Portal import { WhmcsInvoiceRaw } from "@customer-portal/domain/billing/providers/whmcs/raw.types"; ``` ### Alternatives Considered | Approach | Pros | Cons | | -------------------------------------- | ------------------------------------------------ | -------------------------------------------------- | | **Provider types in separate package** | Clear separation | Multiple packages to maintain, harder to co-locate | | **Provider types in BFF** | BFF-only by default | Types not reusable, duplicated if needed elsewhere | | **Provider isolation in domain** | Co-located, clear boundaries, ESLint enforceable | Requires import discipline | ## Consequences ### Positive - Portal remains provider-agnostic - Adding new providers is straightforward (new folder, no contract changes) - Clear, enforceable import boundaries - Co-located raw types + mappers ### Negative - Requires ESLint rules to enforce boundaries - Developers must understand import restrictions ## Implementation ### Directory Structure ``` packages/domain/billing/ ├── contract.ts # Normalized types (Invoice, InvoiceStatus) ├── schema.ts # Zod schemas (invoiceSchema) ├── index.ts # Public exports └── providers/ ├── index.ts # Re-exports all provider mappers └── whmcs/ ├── raw.types.ts # WhmcsInvoiceRaw (API response shape) └── mapper.ts # transformWhmcsInvoice() ``` ### Adding a New Provider ```bash # 1. Create provider folder mkdir -p packages/domain/billing/providers/stripe # 2. Add raw types # packages/domain/billing/providers/stripe/raw.types.ts export const stripeInvoiceRawSchema = z.object({ ... }); export type StripeInvoiceRaw = z.infer; # 3. Add mapper # packages/domain/billing/providers/stripe/mapper.ts export function transformStripeInvoice(raw: unknown): Invoice { ... } # 4. Export from providers/index.ts export * from "./stripe/mapper.js"; # 5. Use in BFF integration service - no other changes needed! ``` ### ESLint Configuration ```javascript // eslint.config.mjs { rules: { "no-restricted-imports": [ "error", { patterns: [ { group: ["@customer-portal/domain/*/providers/*"], message: "Portal cannot import provider-specific types. Use domain types instead.", }, ], }, ], }, } ``` ## Related - [Domain Structure](../development/domain/structure.md) - [Import Hygiene](../development/domain/import-hygiene.md) - [ADR-003: Map Once, Use Everywhere](./003-map-once-use-everywhere.md)