142 lines
4.7 KiB
Markdown
142 lines
4.7 KiB
Markdown
# 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<typeof stripeInvoiceRawSchema>;
|
|
|
|
# 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)
|