Update Customer Portal Documentation and Remove Deprecated Files

- Streamlined the README.md for clarity and conciseness.
- Deleted outdated documentation files related to Freebit SIM management, SIM management API data flow, and various architectural guides to reduce clutter and improve maintainability.
- Updated the last modified date in the README to reflect the latest changes.
This commit is contained in:
barsa 2025-12-23 15:43:36 +09:00
parent 3af18af502
commit 7c929eb4dc
66 changed files with 1077 additions and 1830 deletions

View File

@ -1,422 +1,199 @@
# Customer Portal Documentation # Customer Portal Documentation
This directory contains comprehensive system design documentation for the Customer Portal project. Comprehensive documentation for the Customer Portal project.
--- ---
## 👀 Portal Guides (How It Works) ## 📁 Documentation Structure
- [Overview](./portal-guides/system-overview.md) — systems, data ownership, and caching quick reference ```
- [Accounts & Identity](./portal-guides/accounts-and-identity.md) — sign-up, WHMCS linking, and address/profile handling docs/
- [Catalog & Checkout](./portal-guides/catalog-and-checkout.md) — product source, eligibility, and checkout rules ├── getting-started/ # Setup and running the project
- [Orders & Provisioning](./portal-guides/orders-and-provisioning.md) — Salesforce orders and WHMCS fulfillment flow ├── architecture/ # System design documents
- [Billing & Payments](./portal-guides/billing-and-payments.md) — invoices, SSO pay links, and payment methods ├── how-it-works/ # Feature guides (how the portal works)
- [Subscriptions](./portal-guides/subscriptions.md) — how active services are read and refreshed ├── integrations/ # Salesforce, WHMCS, SIM integration docs
- [Support Cases](./portal-guides/support-cases.md) — Salesforce case creation and visibility ├── development/ # BFF, Portal, Domain, Auth code docs
- [Complete Guide](./portal-guides/COMPLETE-GUIDE.md) — single, end-to-end explanation of how the portal works ├── operations/ # Logging, provisioning, monitoring
└── _archive/ # Historical documents
```
--- ---
## 📚 Core System Design Documents ## 🚀 Getting Started
### [System Architecture](./architecture/SYSTEM-ARCHITECTURE.md) | Document | Description |
| ----------------------------------------------------- | ------------------------ |
| [Setup Guide](./getting-started/setup.md) | Initial project setup |
| [Running the App](./getting-started/running.md) | Local development |
| [Deployment](./getting-started/deployment.md) | Deployment instructions |
| [Docker & Prisma](./getting-started/docker-prisma.md) | Docker setup with Prisma |
| [Address System](./getting-started/address-system.md) | Address handling |
**Comprehensive overview of the entire system** See also: [Project Structure](./STRUCTURE.md)
- System overview and high-level architecture
- Architecture principles (Clean Architecture, DDD)
- Monorepo structure and organization
- Application layers (Portal, BFF, Domain)
- Technology stack and infrastructure
- Data flow and integration patterns
- Deployment architecture
**Start here** for a complete understanding of the system.
--- ---
### [Integration & Data Flow](./architecture/INTEGRATION-DATAFLOW.md) ## 🏗️ Architecture
**External system integration patterns and data transformation** Core system design documents:
- Integration architecture overview | Document | Description |
- Salesforce integration (REST API + Platform Events via gRPC Pub/Sub) | -------------------------------------------------------------- | ------------------------- |
- WHMCS integration (REST API + Webhooks) | [System Overview](./architecture/system-overview.md) | High-level architecture |
- Freebit SIM management integration | [Monorepo Structure](./architecture/monorepo.md) | Monorepo organization |
- Domain mapper pattern (Map Once, Use Everywhere) | [Product Catalog](./architecture/product-catalog.md) | Catalog design |
- Data transformation flows | [Modular Provisioning](./architecture/modular-provisioning.md) | Provisioning architecture |
- Error handling and retry strategies | [Domain Layer](./architecture/domain-layer.md) | Domain-driven design |
- Caching strategies (CDC-driven + TTL-based) | [Orders Architecture](./architecture/orders.md) | Order system design |
**Read this** to understand how external systems are integrated.
--- ---
### [Domain Layer Design](./architecture/DOMAIN-LAYER-DESIGN.md) ## 📖 How It Works
**Framework-agnostic type system and business logic** Feature guides explaining how the portal functions:
- Domain-driven design principles | Guide | Description |
- Provider pattern for multi-system abstraction | ------------------------------------------------------------------ | --------------------------- |
- Type system architecture (unified domain package) | [System Overview](./how-it-works/system-overview.md) | Systems and data ownership |
- Schema-driven validation with Zod | [Accounts & Identity](./how-it-works/accounts-and-identity.md) | Sign-up and WHMCS linking |
- Adding new domains step-by-step | [Catalog & Checkout](./how-it-works/catalog-and-checkout.md) | Products and checkout rules |
- Import patterns and best practices | [Orders & Provisioning](./how-it-works/orders-and-provisioning.md) | Order fulfillment flow |
| [Billing & Payments](./how-it-works/billing-and-payments.md) | Invoices and payments |
**Read this** to understand the domain layer and type system. | [Subscriptions](./how-it-works/subscriptions.md) | Active services |
| [Support Cases](./how-it-works/support-cases.md) | Salesforce case creation |
| [Order Fulfillment](./how-it-works/order-fulfillment.md) | Complete order flow |
| [Add-on Installation](./how-it-works/addon-installation.md) | Add-on handling logic |
| [Complete Guide](./how-it-works/COMPLETE-GUIDE.md) | End-to-end explanation |
--- ---
### [Authentication & Security](./architecture/AUTHENTICATION-SECURITY.md) ## 🔌 Integrations
**Security architecture and implementation**
- Authentication flow (JWT access + refresh tokens)
- Token management and rotation with Redis blacklist
- Authorization and access control
- Rate limiting strategies (auth endpoints + external APIs)
- Password security (Bcrypt with configurable rounds)
- CSRF protection
- PII redaction and data protection
- Audit logging
**Read this** for security implementation details.
---
## 📖 Feature-Specific Documentation
### Portal (Frontend)
- [Portal Architecture](./portal/PORTAL-ARCHITECTURE.md) - Frontend structure
- [Performance Optimization](./portal/PERFORMANCE.md) - Performance best practices
- [Portal Data Model](./portal/PORTAL-DATA-MODEL.md) - Data structures
- [Portal Integration Overview](./portal/PORTAL-INTEGRATION-OVERVIEW.md) - API integration
- [Portal Roadmap](./portal/PORTAL-ROADMAP.md) - Future plans
- [Recommended Library Structure](./portal/RECOMMENDED-LIB-STRUCTURE.md) - Code organization
### Orders & Catalog
- [Order Fulfillment Guide](./orders/ORDER-FULFILLMENT-COMPLETE-GUIDE.md) - Order processing flow
- [Portal Ordering & Provisioning](./orders/PORTAL-ORDERING-PROVISIONING.md) - Order management
- [Product Catalog Architecture](./architecture/PRODUCT-CATALOG-ARCHITECTURE.md) - Catalog design
- [Add-on Installation Logic](./products/ADDON-INSTALLATION-LOGIC.md) - Add-on handling
- [Bundle Analysis](./products/BUNDLE_ANALYSIS.md) - Product bundles
### Integration Guides
**BFF Integration Layer:**
- [BFF Integration Patterns (Architecture)](./bff/BFF-INTEGRATION-PATTERNS-ARCHITECTURE.md) - Integration architecture
- [BFF Integration Patterns (Guide)](./bff/BFF-INTEGRATION-PATTERNS-GUIDE.md) - Implementation guide
- [DB Mappers](./bff/DB-MAPPERS.md) - Database mapping patterns
**Salesforce:**
- [Salesforce Portal Simple Guide](./salesforce/SALESFORCE-PORTAL-SIMPLE-GUIDE.md) - Getting started
- [Salesforce Order Communication](./salesforce/SALESFORCE-ORDER-COMMUNICATION.md) - Order integration
- [Salesforce Portal Security Guide](./salesforce/SALESFORCE-PORTAL-SECURITY-GUIDE.md) - Security setup
- [Salesforce Products](./salesforce/SALESFORCE-PRODUCTS.md) - Product catalog
- [Salesforce-WHMCS Mapping Reference](./salesforce/SALESFORCE-WHMCS-MAPPING-REFERENCE.md) - Data mapping
- [WHMCS Billing Issues Resolution](./salesforce/WHMCS_BILLING_ISSUES_RESOLUTION.md) - Troubleshooting
**API Integration:**
- [SIM Management API Data Flow](./api/SIM-MANAGEMENT-API-DATA-FLOW.md) - SIM API integration
- [Freebit SIM Management](./api/FREEBIT-SIM-MANAGEMENT.md) - Freebit integration
### Domain & Architecture
- [Domain Structure](./domain/DOMAIN-STRUCTURE.md) - Domain organization
- [Package Organization](./domain/PACKAGE-ORGANIZATION.md) - Package structure
- [Monorepo Architecture](./architecture/MONOREPO-ARCHITECTURE.md) - Monorepo setup
- [Modular Provisioning Architecture](./architecture/MODULAR-PROVISIONING-ARCHITECTURE.md) - Provisioning design
- [New Domain Architecture](./architecture/NEW-DOMAIN-ARCHITECTURE.md) - Domain layer design
### Validation & Data
- [Validation Patterns](./validation/VALIDATION_PATTERNS.md) - Validation strategies
- [Validation Cleanup Summary](./validation/VALIDATION_CLEANUP_SUMMARY.md) - Validation improvements
- [BFF Validation Migration](./validation/bff-validation-migration.md) - Migration guide
- [Signup Validation Rules](./validation/SIGNUP_VALIDATION_RULES.md) - Signup validation
- [Unified Product Types](./types/UNIFIED-PRODUCT-TYPES.md) - Type unification
- [Consolidated Type System](./types/CONSOLIDATED-TYPE-SYSTEM.md) - Type system design
### Operations & Deployment
**Getting Started:**
- [Getting Started](./guides/GETTING_STARTED.md) - Setup instructions
- [Running the Application](./guides/RUN.md) - How to run
- [Deployment Guide](./guides/DEPLOY.md) - Deployment instructions
- [Project Structure](./guides/STRUCTURE.md) - Directory structure
- [Address System](./guides/ADDRESS_SYSTEM.md) - Address handling
**Provisioning:**
- [Provisioning Runbook](./provisioning/RUNBOOK_PROVISIONING.md) - Provisioning procedures
- [Subscription Service Management](./provisioning/SUBSCRIPTION-SERVICE-MANAGEMENT.md) - Service management
- [Temporarily Disabled Modules](./provisioning/TEMPORARY-DISABLED-MODULES.md) - Disabled features
**Logging:**
- [Logging Guide](./logging/LOGGING.md) - Logging implementation
- [Logging Levels](./logging/LOGGING_LEVELS.md) - Log level configuration
### Other Documentation
- [Changelog](./CHANGELOG.md) - Project changelog
- [Portal Non-Tech Presentation](./portal/PORTAL-NONTECH-PRESENTATION.md) - Non-technical overview
---
## 🗂️ Archived Documentation
Status reports and temporary implementation documents have been moved to `_archive/`:
- Migration progress reports
- Refactoring completion documents
- Cleanup and audit documents
- Priority tracking documents
These are kept for historical reference but are not part of the active system design.
---
## 🎯 Quick Start Guide
### For New Developers
1. **Start with Core Design Documents:**
- [System Architecture](./architecture/SYSTEM-ARCHITECTURE.md) - Overall system (if available)
- [Domain Layer Design](./architecture/DOMAIN-LAYER-DESIGN.md) - Type system (if available)
- [Integration & Data Flow](./architecture/INTEGRATION-DATAFLOW.md) - External systems (if available)
2. **Set Up Your Environment:**
- [Getting Started](./guides/GETTING_STARTED.md) - Setup
- [Running the Application](./guides/RUN.md) - Local development
3. **Understand Key Features:**
- [Order Fulfillment Guide](./orders/ORDER-FULFILLMENT-COMPLETE-GUIDE.md)
- [Product Catalog Architecture](./architecture/PRODUCT-CATALOG-ARCHITECTURE.md)
- [Portal Architecture](./portal/PORTAL-ARCHITECTURE.md)
### For Backend Developers
1. [System Architecture](./architecture/SYSTEM-ARCHITECTURE.md) - BFF architecture (if available)
2. [Integration & Data Flow](./architecture/INTEGRATION-DATAFLOW.md) - Integration patterns (if available)
3. [Authentication & Security](./architecture/AUTHENTICATION-SECURITY.md) - Auth implementation (if available)
4. [BFF Integration Patterns](./bff/BFF-INTEGRATION-PATTERNS-GUIDE.md) - Best practices
### For Frontend Developers
1. [Portal Architecture](./portal/PORTAL-ARCHITECTURE.md) - Frontend structure
2. [Domain Layer Design](./architecture/DOMAIN-LAYER-DESIGN.md) - Type system (if available)
3. [Portal Integration Overview](./portal/PORTAL-INTEGRATION-OVERVIEW.md) - API integration
4. [Performance Optimization](./portal/PERFORMANCE.md) - Performance tips
### For Integration Work
1. [Integration & Data Flow](./architecture/INTEGRATION-DATAFLOW.md) - Integration architecture (if available)
2. [Salesforce Portal Simple Guide](./salesforce/SALESFORCE-PORTAL-SIMPLE-GUIDE.md)
3. [Salesforce-WHMCS Mapping Reference](./salesforce/SALESFORCE-WHMCS-MAPPING-REFERENCE.md)
4. [SIM Management API Data Flow](./api/SIM-MANAGEMENT-API-DATA-FLOW.md)
### For DevOps/Deployment
1. [Deployment Guide](./guides/DEPLOY.md)
2. [Provisioning Runbook](./provisioning/RUNBOOK_PROVISIONING.md)
3. [Logging Guide](./logging/LOGGING.md)
---
## 📋 Documentation Standards
### When Adding New Documentation
1. **Place in appropriate category folder**
2. **Use clear, descriptive filenames**
3. **Update this README index**
4. **Follow markdown best practices**
5. **Include date and author information**
6. **Keep documentation synchronized with code**
### Documentation Types
- **Design Documents**: Architecture, patterns, and design decisions
- **Guides**: Step-by-step instructions and tutorials
- **References**: API documentation, data mappings, configuration
- **Status Reports**: Move to `_archive/` when outdated
### Naming Conventions
- Use `UPPERCASE-WITH-DASHES.md` for design documents
- Use descriptive names that indicate content
- Avoid version numbers in filenames
- Use consistent prefixes for related docs
---
## 🤝 Contributing
When updating documentation:
- Keep files organized in their respective categories
- Update the changelog for significant changes
- Ensure all links work correctly
- Use relative links for internal documentation
- Keep the README index updated
- Archive outdated status reports
---
## 📞 Support
For questions about the system:
1. Check the relevant design document
2. Review related guides and references
3. Check archived documents for historical context
4. Contact the development team
## 🏗️ Technology Stack
### Frontend
- Next.js 15 (App Router) with React 19
- Tailwind CSS 4 with shadcn/ui components
- TanStack Query for data fetching and caching
- Zustand for client state management
- React Hook Form + Zod for form validation
### Backend (BFF)
- NestJS 11 with TypeScript
- Prisma 6 ORM with PostgreSQL 17
- p-queue for request throttling
- Redis 7 for caching and token blacklist
- Pino for structured logging
### External Integrations
- **WHMCS**: Custom API client for billing and subscriptions
- **Salesforce**: jsforce for REST API + salesforce-pubsub-api-client for Platform Events
- **Freebit**: Custom SIM management integration
### Infrastructure
- Docker for local development
- pnpm workspaces for monorepo management
- TypeScript project references for build optimization
---
**Last Updated**: November 2025
**Maintained By**: Development Team
---
## 📑 Complete File Index
<details>
<summary>View all documentation files</summary>
### Core Design
- SYSTEM-ARCHITECTURE.md (check if exists in architecture/)
- INTEGRATION-DATAFLOW.md (check if exists in architecture/)
- DOMAIN-LAYER-DESIGN.md (check if exists in architecture/)
- AUTHENTICATION-SECURITY.md (check if exists in architecture/)
### Architecture
- architecture/MONOREPO-ARCHITECTURE.md
- architecture/MODULAR-PROVISIONING-ARCHITECTURE.md
- architecture/NEW-DOMAIN-ARCHITECTURE.md
- architecture/ORDERS-ARCHITECTURE-REVIEW.md
- architecture/PRODUCT-CATALOG-ARCHITECTURE.md
### API & Integration
- api/FREEBIT-SIM-MANAGEMENT.md
- api/SIM-MANAGEMENT-API-DATA-FLOW.md
### Authentication
- auth/AUTH-MODULE-ARCHITECTURE.md
- auth/AUTH-SCHEMA-IMPROVEMENTS.md
- auth/DEVELOPMENT-AUTH-SETUP.md
- auth/REDIS-TOKEN-FLOW-IMPLEMENTATION.md
### BFF
- bff/BFF-INTEGRATION-PATTERNS-ARCHITECTURE.md
- bff/BFF-INTEGRATION-PATTERNS-GUIDE.md
- bff/DB-MAPPERS.md
### Domain
- domain/DOMAIN-STRUCTURE.md
- domain/PACKAGE-ORGANIZATION.md
### Guides
- guides/ADDRESS_SYSTEM.md
- guides/DEPLOY.md
- guides/GETTING_STARTED.md
- guides/RUN.md
- guides/STRUCTURE.md
### Logging
- logging/LOGGING.md
- logging/LOGGING_LEVELS.md
### Orders
- orders/ORDER-FULFILLMENT-COMPLETE-GUIDE.md
- orders/PORTAL-ORDERING-PROVISIONING.md
### Portal
- portal/PERFORMANCE.md
- portal/PORTAL-ARCHITECTURE.md
- portal/PORTAL-DATA-MODEL.md
- portal/PORTAL-INTEGRATION-OVERVIEW.md
- portal/PORTAL-NONTECH-PRESENTATION.md
- portal/PORTAL-ROADMAP.md
- portal/RECOMMENDED-LIB-STRUCTURE.md
### Products
- products/ADDON-INSTALLATION-LOGIC.md
- products/BUNDLE_ANALYSIS.md
### Provisioning
- provisioning/RUNBOOK_PROVISIONING.md
- provisioning/SUBSCRIPTION-SERVICE-MANAGEMENT.md
- provisioning/TEMPORARY-DISABLED-MODULES.md
### Salesforce ### Salesforce
- salesforce/SALESFORCE-ORDER-COMMUNICATION.md | Document | Description |
- salesforce/SALESFORCE-PORTAL-SECURITY-GUIDE.md | --------------------------------------------------------------------------- | ----------------------------- |
- salesforce/SALESFORCE-PORTAL-SIMPLE-GUIDE.md | [Requirements](./integrations/salesforce/requirements.md) | Objects, fields, flows, setup |
- salesforce/SALESFORCE-PRODUCTS.md | [Overview](./integrations/salesforce/overview.md) | Getting started |
- salesforce/SALESFORCE-WHMCS-MAPPING-REFERENCE.md | [Orders](./integrations/salesforce/orders.md) | Order communication |
- salesforce/WHMCS_BILLING_ISSUES_RESOLUTION.md | [Products](./integrations/salesforce/products.md) | Product catalog |
| [Security](./integrations/salesforce/security.md) | Security setup |
| [Opportunity Lifecycle](./integrations/salesforce/opportunity-lifecycle.md) | Opportunity stages |
| [WHMCS Mapping](./integrations/salesforce/whmcs-mapping.md) | SF-WHMCS data mapping |
### Types ### WHMCS
- types/CONSOLIDATED-TYPE-SYSTEM.md | Document | Description |
- types/UNIFIED-PRODUCT-TYPES.md | ---------------------------------------------------------- | ------------------------- |
| [Troubleshooting](./integrations/whmcs/troubleshooting.md) | Billing issues resolution |
### Validation ### SIM Management
- validation/SIGNUP_VALIDATION_RULES.md | Document | Description |
- validation/VALIDATION_CLEANUP_SUMMARY.md | ---------------------------------------------------- | ---------------------- |
- validation/VALIDATION_PATTERNS.md | [Freebit Integration](./integrations/sim/freebit.md) | Freebit SIM management |
- validation/bff-validation-migration.md | [Data Flow](./integrations/sim/data-flow.md) | SIM API data flow |
| [State Machine](./integrations/sim/state-machine.md) | SIM state transitions |
</details> ---
## 💻 Development
### BFF (Backend for Frontend)
| Document | Description |
| ----------------------------------------------------------------- | --------------------------- |
| [Integration Patterns](./development/bff/integration-patterns.md) | Clean architecture patterns |
| [DB Mappers](./development/bff/db-mappers.md) | Database mapping |
| [Order Status Updates](./development/bff/order-status-updates.md) | Status update strategy |
### Portal (Frontend)
| Document | Description |
| -------------------------------------------------------------------- | ------------------------ |
| [Architecture](./development/portal/architecture.md) | Frontend structure |
| [Performance](./development/portal/performance.md) | Performance optimization |
| [Data Model](./development/portal/data-model.md) | Data structures |
| [Integration Overview](./development/portal/integration-overview.md) | API integration |
| [Lib Structure](./development/portal/lib-structure.md) | Code organization |
| [UI Design System](./development/portal/ui-design-system.md) | Design system |
### Domain Layer
| Document | Description |
| ---------------------------------------------- | ------------------- |
| [Structure](./development/domain/structure.md) | Domain organization |
| [Packages](./development/domain/packages.md) | Package structure |
| [Types](./development/domain/types.md) | Unified type system |
### Authentication
| Document | Description |
| ------------------------------------------------------------ | ------------------------- |
| [Module Architecture](./development/auth/module.md) | Auth module design |
| [Development Setup](./development/auth/development-setup.md) | Auth setup for dev |
| [Redis Tokens](./development/auth/redis-tokens.md) | Token flow implementation |
---
## 🛠️ Operations
| Document | Description |
| ------------------------------------------------------------------ | ----------------------------- |
| [Logging](./operations/logging.md) | Centralized logging system |
| [Security Monitoring](./operations/security-monitoring.md) | Security monitoring setup |
| [Provisioning Runbook](./operations/provisioning-runbook.md) | Provisioning procedures |
| [Subscription Management](./operations/subscription-management.md) | Service management |
| [Disabled Modules](./operations/disabled-modules.md) | Temporarily disabled features |
---
## 🗂️ Archive
Historical documents kept for reference:
- `_archive/planning/` — Development plans and task lists
- `_archive/refactoring/` — Completed refactoring summaries
- `_archive/reviews/` — Point-in-time code reviews
---
## 🎯 Quick Start by Role
### New Developer
1. [Setup Guide](./getting-started/setup.md)
2. [System Overview](./architecture/system-overview.md)
3. [Complete Guide](./how-it-works/COMPLETE-GUIDE.md)
### Backend Developer
1. [Integration Patterns](./development/bff/integration-patterns.md)
2. [Salesforce Requirements](./integrations/salesforce/requirements.md)
3. [Order Fulfillment](./how-it-works/order-fulfillment.md)
### Frontend Developer
1. [Portal Architecture](./development/portal/architecture.md)
2. [Domain Types](./development/domain/types.md)
3. [Performance](./development/portal/performance.md)
### DevOps
1. [Deployment](./getting-started/deployment.md)
2. [Logging](./operations/logging.md)
3. [Provisioning Runbook](./operations/provisioning-runbook.md)
---
## 🏗️ Technology Stack
**Frontend**: Next.js 15, React 19, Tailwind CSS 4, shadcn/ui, TanStack Query, Zustand
**Backend**: NestJS 11, Prisma 6, PostgreSQL 17, Redis 7, Pino
**Integrations**: Salesforce (jsforce + Pub/Sub API), WHMCS, Freebit
---
**Last Updated**: December 2025

View File

@ -172,26 +172,31 @@ External API Request
## 🎨 Design Principles ## 🎨 Design Principles
### 1. **Co-location** ### 1. **Co-location**
- Domain contracts, schemas, and provider logic live together - Domain contracts, schemas, and provider logic live together
- Easy to find related code - Easy to find related code
- Clear ownership and responsibility - Clear ownership and responsibility
### 2. **Provider Isolation** ### 2. **Provider Isolation**
- Raw types and mappers nested in `providers/` subfolder - Raw types and mappers nested in `providers/` subfolder
- Each provider is self-contained - Each provider is self-contained
- Easy to add/remove providers - Easy to add/remove providers
### 3. **Type Safety** ### 3. **Type Safety**
- Zod schemas for runtime validation - Zod schemas for runtime validation
- TypeScript types inferred from schemas - TypeScript types inferred from schemas
- Branded types for stronger type checking - Branded types for stronger type checking
### 4. **Clean Exports** ### 4. **Clean Exports**
- Barrel exports (`index.ts`) control public API - Barrel exports (`index.ts`) control public API
- Provider mappers exported as namespaces (`WhmcsBillingMapper.*`) - Provider mappers exported as namespaces (`WhmcsBillingMapper.*`)
- Predictable import paths - Predictable import paths
### 5. **Minimal Dependencies** ### 5. **Minimal Dependencies**
- Only depends on `zod` for runtime validation - Only depends on `zod` for runtime validation
- No circular dependencies - No circular dependencies
- Self-contained domain logic - Self-contained domain logic
@ -199,40 +204,48 @@ External API Request
## 📋 Domain Reference ## 📋 Domain Reference
### Billing ### Billing
- **Contracts**: `Invoice`, `InvoiceItem`, `InvoiceList` - **Contracts**: `Invoice`, `InvoiceItem`, `InvoiceList`
- **Providers**: WHMCS - **Providers**: WHMCS
- **Use Cases**: Display invoices, payment history, invoice details - **Use Cases**: Display invoices, payment history, invoice details
### Subscriptions ### Subscriptions
- **Contracts**: `Subscription`, `SubscriptionList` - **Contracts**: `Subscription`, `SubscriptionList`
- **Providers**: WHMCS - **Providers**: WHMCS
- **Use Cases**: Display active services, manage subscriptions - **Use Cases**: Display active services, manage subscriptions
### Payments ### Payments
- **Contracts**: `PaymentMethod`, `PaymentGateway` - **Contracts**: `PaymentMethod`, `PaymentGateway`
- **Providers**: WHMCS - **Providers**: WHMCS
- **Use Cases**: Payment method management, gateway configuration - **Use Cases**: Payment method management, gateway configuration
### SIM ### SIM
- **Contracts**: `SimDetails`, `SimUsage`, `SimTopUpHistory` - **Contracts**: `SimDetails`, `SimUsage`, `SimTopUpHistory`
- **Providers**: Freebit - **Providers**: Freebit
- **Use Cases**: SIM management, usage tracking, top-up history - **Use Cases**: SIM management, usage tracking, top-up history
### Orders ### Orders
- **Contracts**: `OrderSummary`, `OrderDetails`, `FulfillmentOrderDetails` - **Contracts**: `OrderSummary`, `OrderDetails`, `FulfillmentOrderDetails`
- **Providers**: WHMCS (provisioning), Salesforce (order management) - **Providers**: WHMCS (provisioning), Salesforce (order management)
- **Use Cases**: Order fulfillment, order history, order details - **Use Cases**: Order fulfillment, order history, order details
### Catalog ### Catalog
- **Contracts**: `InternetPlanCatalogItem`, `SimCatalogProduct`, `VpnCatalogProduct` - **Contracts**: `InternetPlanCatalogItem`, `SimCatalogProduct`, `VpnCatalogProduct`
- **Providers**: Salesforce (Product2) - **Providers**: Salesforce (Product2)
- **Use Cases**: Product catalog display, product selection - **Use Cases**: Product catalog display, product selection
### Common ### Common
- **Types**: `IsoDateTimeString`, `UserId`, `AccountId`, `OrderId`, `ApiResponse`, `PaginatedResponse` - **Types**: `IsoDateTimeString`, `UserId`, `AccountId`, `OrderId`, `ApiResponse`, `PaginatedResponse`
- **Use Cases**: Shared utility types across all domains - **Use Cases**: Shared utility types across all domains
### Toolkit ### Toolkit
- **Formatting**: Currency, date, phone, text formatters - **Formatting**: Currency, date, phone, text formatters
- **Validation**: Email, URL, string validators - **Validation**: Email, URL, string validators
- **Typing**: Type guards, assertions, helpers - **Typing**: Type guards, assertions, helpers
@ -243,6 +256,7 @@ External API Request
### From Old Structure ### From Old Structure
**Before:** **Before:**
```typescript ```typescript
import type { Invoice } from "@customer-portal/contracts/billing"; import type { Invoice } from "@customer-portal/contracts/billing";
import { invoiceSchema } from "@customer-portal/schemas/business/billing.schema"; import { invoiceSchema } from "@customer-portal/schemas/business/billing.schema";
@ -250,6 +264,7 @@ import { transformWhmcsInvoice } from "@customer-portal/integrations-whmcs/mappe
``` ```
**After:** **After:**
```typescript ```typescript
import type { Invoice } from "@customer-portal/domain/billing"; import type { Invoice } from "@customer-portal/domain/billing";
import { invoiceSchema } from "@customer-portal/domain/billing"; import { invoiceSchema } from "@customer-portal/domain/billing";
@ -259,6 +274,7 @@ const invoice = WhmcsBillingMapper.transformWhmcsInvoice(data);
``` ```
### Benefits ### Benefits
- **Fewer imports**: Everything in one package - **Fewer imports**: Everything in one package
- **Clearer intent**: Mapper namespace indicates provider - **Clearer intent**: Mapper namespace indicates provider
- **Better DX**: Autocomplete shows all related exports - **Better DX**: Autocomplete shows all related exports
@ -288,4 +304,3 @@ const invoice = WhmcsBillingMapper.transformWhmcsInvoice(data);
- [Provider-Aware Structure](./DOMAIN-STRUCTURE.md) - [Provider-Aware Structure](./DOMAIN-STRUCTURE.md)
- [Type Cleanup Summary](./TYPE-CLEANUP-SUMMARY.md) - [Type Cleanup Summary](./TYPE-CLEANUP-SUMMARY.md)
- [Architecture Overview](./ARCHITECTURE.md) - [Architecture Overview](./ARCHITECTURE.md)

View File

@ -47,35 +47,40 @@ After comprehensive review and refactoring across all BFF integrations, the arch
### Additional Improvements Beyond Orders ### Additional Improvements Beyond Orders
**6. ✅ Centralized DB Mappers** **6. ✅ Centralized DB Mappers**
- **Created**: `apps/bff/src/infra/mappers/`
- All Prisma → Domain mappings centralized - **Created**: `apps/bff/src/infra/mappers/`
- Clear naming: `mapPrismaUserToDomain()`, `mapPrismaMappingToDomain()` - All Prisma → Domain mappings centralized
- **Documentation**: [DB-MAPPERS.md](./apps/bff/docs/DB-MAPPERS.md) - Clear naming: `mapPrismaUserToDomain()`, `mapPrismaMappingToDomain()`
- **Documentation**: [DB-MAPPERS.md](./apps/bff/docs/DB-MAPPERS.md)
**7. ✅ Freebit Integration Cleaned** **7. ✅ Freebit Integration Cleaned**
- **Deleted**: `FreebitMapperService` (redundant wrapper)
- **Moved**: Provider utilities to domain (`Freebit.normalizeAccount()`, etc.) - **Deleted**: `FreebitMapperService` (redundant wrapper)
- Now uses domain mappers directly: `Freebit.transformFreebitAccountDetails()` - **Moved**: Provider utilities to domain (`Freebit.normalizeAccount()`, etc.)
- Now uses domain mappers directly: `Freebit.transformFreebitAccountDetails()`
**8. ✅ WHMCS Transformer Services Removed** **8. ✅ WHMCS Transformer Services Removed**
- **Deleted**: Entire `apps/bff/src/integrations/whmcs/transformers/` directory (6 files)
- **Removed Services**: - **Deleted**: Entire `apps/bff/src/integrations/whmcs/transformers/` directory (6 files)
- `InvoiceTransformerService` - **Removed Services**:
- `SubscriptionTransformerService` - `InvoiceTransformerService`
- `PaymentTransformerService` - `SubscriptionTransformerService`
- `WhmcsTransformerOrchestratorService` - `PaymentTransformerService`
- **Why**: Thin wrappers that only injected currency then called domain mappers - `WhmcsTransformerOrchestratorService`
- **Now**: Integration services use domain mappers directly with currency context - **Why**: Thin wrappers that only injected currency then called domain mappers
- **Now**: Integration services use domain mappers directly with currency context
**9. ✅ Consistent Pattern Across ALL Integrations** **9. ✅ Consistent Pattern Across ALL Integrations**
- ✅ Salesforce: Uses domain mappers directly
- ✅ WHMCS: Uses domain mappers directly - ✅ Salesforce: Uses domain mappers directly
- ✅ Freebit: Uses domain mappers directly - ✅ WHMCS: Uses domain mappers directly
- ✅ Catalog: Uses domain mappers directly - ✅ Freebit: Uses domain mappers directly
- ✅ Catalog: Uses domain mappers directly
### Files Changed Summary ### Files Changed Summary
**Created (5 files)**: **Created (5 files)**:
- `apps/bff/src/infra/mappers/user.mapper.ts` - `apps/bff/src/infra/mappers/user.mapper.ts`
- `apps/bff/src/infra/mappers/mapping.mapper.ts` - `apps/bff/src/infra/mappers/mapping.mapper.ts`
- `apps/bff/src/infra/mappers/index.ts` - `apps/bff/src/infra/mappers/index.ts`
@ -84,19 +89,19 @@ After comprehensive review and refactoring across all BFF integrations, the arch
- `apps/bff/docs/BFF-INTEGRATION-PATTERNS.md` - `apps/bff/docs/BFF-INTEGRATION-PATTERNS.md`
**Deleted (8 files)**: **Deleted (8 files)**:
- `apps/bff/src/integrations/freebit/services/freebit-mapper.service.ts` - `apps/bff/src/integrations/freebit/services/freebit-mapper.service.ts`
- `apps/bff/src/integrations/whmcs/transformers/` (6 files in directory) - `apps/bff/src/integrations/whmcs/transformers/` (6 files in directory)
- `apps/bff/src/infra/utils/user-mapper.util.ts` - `apps/bff/src/infra/utils/user-mapper.util.ts`
**Modified (17+ files)**: **Modified (17+ files)**:
- All WHMCS services (invoice, subscription, payment) - All WHMCS services (invoice, subscription, payment)
- All Freebit services (operations, orchestrator) - All Freebit services (operations, orchestrator)
- MappingsService + all auth services - MappingsService + all auth services
- Module definitions (whmcs.module.ts, freebit.module.ts) - Module definitions (whmcs.module.ts, freebit.module.ts)
- Domain exports (sim/providers/freebit/index.ts) - Domain exports (sim/providers/freebit/index.ts)
## 🎯 Current Architecture ## 🎯 Current Architecture
### Clean Separation of Concerns ### Clean Separation of Concerns
@ -134,6 +139,7 @@ Flow: Query (BFF) → Raw Data → Domain Mapper → Domain Type → Use Directl
### Domain Layer (`packages/domain/orders/`) ### Domain Layer (`packages/domain/orders/`)
**Contains**: **Contains**:
- ✅ Business types (OrderDetails, OrderSummary) - ✅ Business types (OrderDetails, OrderSummary)
- ✅ Raw provider types (SalesforceOrderRecord) - ✅ Raw provider types (SalesforceOrderRecord)
- ✅ Validation schemas (Zod) - ✅ Validation schemas (Zod)
@ -141,6 +147,7 @@ Flow: Query (BFF) → Raw Data → Domain Mapper → Domain Type → Use Directl
- ✅ Business validation functions - ✅ Business validation functions
**Does NOT Contain**: **Does NOT Contain**:
- ❌ Query builders (moved to BFF) - ❌ Query builders (moved to BFF)
- ❌ Field configuration - ❌ Field configuration
- ❌ HTTP/API concerns - ❌ HTTP/API concerns
@ -148,18 +155,21 @@ Flow: Query (BFF) → Raw Data → Domain Mapper → Domain Type → Use Directl
### Integration Layer (`apps/bff/src/integrations/salesforce/`) ### Integration Layer (`apps/bff/src/integrations/salesforce/`)
**Contains**: **Contains**:
- ✅ `SalesforceOrderService` - Encapsulates order operations - ✅ `SalesforceOrderService` - Encapsulates order operations
- ✅ Query builders (`order-query-builder.ts`) - ✅ Query builders (`order-query-builder.ts`)
- ✅ Connection services - ✅ Connection services
- ✅ Uses domain mappers for transformation - ✅ Uses domain mappers for transformation
**Does NOT Contain**: **Does NOT Contain**:
- ❌ Additional mapping logic (uses domain mappers) - ❌ Additional mapping logic (uses domain mappers)
- ❌ Business validation - ❌ Business validation
### Application Layer (`apps/bff/src/modules/orders/`) ### Application Layer (`apps/bff/src/modules/orders/`)
**Contains**: **Contains**:
- ✅ `OrderOrchestrator` - Workflow coordination - ✅ `OrderOrchestrator` - Workflow coordination
- ✅ `OrderFulfillmentOrchestrator` - Fulfillment workflows - ✅ `OrderFulfillmentOrchestrator` - Fulfillment workflows
- ✅ Controllers (HTTP endpoints) - ✅ Controllers (HTTP endpoints)
@ -167,6 +177,7 @@ Flow: Query (BFF) → Raw Data → Domain Mapper → Domain Type → Use Directl
- ✅ Uses domain mappers directly - ✅ Uses domain mappers directly
**Does NOT Contain**: **Does NOT Contain**:
- ❌ Direct Salesforce queries - ❌ Direct Salesforce queries
- ❌ Mapper service wrappers (deleted) - ❌ Mapper service wrappers (deleted)
- ❌ Double transformations - ❌ Double transformations
@ -204,18 +215,21 @@ const whmcsItems = Providers.Whmcs.mapFulfillmentOrderItems(items);
## ✅ Benefits Achieved ## ✅ Benefits Achieved
### Architecture Cleanliness ### Architecture Cleanliness
- ✅ Single source of truth for transformations (domain mappers) - ✅ Single source of truth for transformations (domain mappers)
- ✅ Clear separation: domain = business, BFF = infrastructure - ✅ Clear separation: domain = business, BFF = infrastructure
- ✅ No redundant mapping layers - ✅ No redundant mapping layers
- ✅ Query logic in correct layer (BFF integration) - ✅ Query logic in correct layer (BFF integration)
### Code Quality ### Code Quality
- ✅ Easier to test (clear boundaries) - ✅ Easier to test (clear boundaries)
- ✅ Easier to maintain (no duplication) - ✅ Easier to maintain (no duplication)
- ✅ Easier to understand (one transformation path) - ✅ Easier to understand (one transformation path)
- ✅ Easier to swap providers (integration services encapsulate) - ✅ Easier to swap providers (integration services encapsulate)
### Developer Experience ### Developer Experience
- ✅ Clear patterns to follow - ✅ Clear patterns to follow
- ✅ No confusion about where code goes - ✅ No confusion about where code goes
- ✅ Consistent with catalog services - ✅ Consistent with catalog services
@ -226,11 +240,13 @@ const whmcsItems = Providers.Whmcs.mapFulfillmentOrderItems(items);
## 📁 File Structure ## 📁 File Structure
### Created Files ### Created Files
1. ✅ `apps/bff/src/integrations/salesforce/services/salesforce-order.service.ts` 1. ✅ `apps/bff/src/integrations/salesforce/services/salesforce-order.service.ts`
2. ✅ `apps/bff/src/integrations/salesforce/utils/order-query-builder.ts` 2. ✅ `apps/bff/src/integrations/salesforce/utils/order-query-builder.ts`
3. ✅ `docs/BFF-INTEGRATION-PATTERNS.md` 3. ✅ `docs/BFF-INTEGRATION-PATTERNS.md`
### Modified Files ### Modified Files
1. ✅ `apps/bff/src/modules/orders/services/order-orchestrator.service.ts` 1. ✅ `apps/bff/src/modules/orders/services/order-orchestrator.service.ts`
2. ✅ `apps/bff/src/modules/orders/services/order-fulfillment-orchestrator.service.ts` 2. ✅ `apps/bff/src/modules/orders/services/order-fulfillment-orchestrator.service.ts`
3. ✅ `apps/bff/src/modules/orders/orders.module.ts` 3. ✅ `apps/bff/src/modules/orders/orders.module.ts`
@ -239,6 +255,7 @@ const whmcsItems = Providers.Whmcs.mapFulfillmentOrderItems(items);
6. ✅ `packages/domain/orders/index.ts` 6. ✅ `packages/domain/orders/index.ts`
### Deleted Files ### Deleted Files
1. ✅ `apps/bff/src/modules/orders/services/order-whmcs-mapper.service.ts` 1. ✅ `apps/bff/src/modules/orders/services/order-whmcs-mapper.service.ts`
2. ✅ `packages/domain/orders/providers/salesforce/query.ts` 2. ✅ `packages/domain/orders/providers/salesforce/query.ts`
@ -249,6 +266,7 @@ const whmcsItems = Providers.Whmcs.mapFulfillmentOrderItems(items);
### Single Transformation Principle ### Single Transformation Principle
**One transformation path** - raw data flows through domain mapper exactly once: **One transformation path** - raw data flows through domain mapper exactly once:
``` ```
Query (BFF) → Raw Data → Domain Mapper → Domain Type → Use Directly Query (BFF) → Raw Data → Domain Mapper → Domain Type → Use Directly
``` ```
@ -256,6 +274,7 @@ Query (BFF) → Raw Data → Domain Mapper → Domain Type → Use Directly
### Encapsulation Principle ### Encapsulation Principle
Integration services hide external system complexity: Integration services hide external system complexity:
```typescript ```typescript
// Application layer doesn't see Salesforce details // Application layer doesn't see Salesforce details
const order = await this.salesforceOrderService.getOrderById(id); const order = await this.salesforceOrderService.getOrderById(id);
@ -264,6 +283,7 @@ const order = await this.salesforceOrderService.getOrderById(id);
### Separation of Concerns ### Separation of Concerns
Each layer has a single responsibility: Each layer has a single responsibility:
- **Domain**: Business logic, types, validation - **Domain**: Business logic, types, validation
- **Integration**: External system interaction - **Integration**: External system interaction
- **Application**: Workflow coordination - **Application**: Workflow coordination
@ -286,10 +306,10 @@ async getOrder(orderId: string): Promise<OrderDetails | null> {
async getOrderById(orderId: string): Promise<OrderDetails | null> { async getOrderById(orderId: string): Promise<OrderDetails | null> {
// 1. Build query (infrastructure) // 1. Build query (infrastructure)
const soql = buildOrderQuery(orderId); const soql = buildOrderQuery(orderId);
// 2. Execute query // 2. Execute query
const rawData = await this.sf.query(soql); const rawData = await this.sf.query(soql);
// 3. Use domain mapper (single transformation!) // 3. Use domain mapper (single transformation!)
return Providers.Salesforce.transformSalesforceOrderDetails(rawData); return Providers.Salesforce.transformSalesforceOrderDetails(rawData);
} }
@ -359,7 +379,7 @@ The refactoring is complete. We now have:
1. ✅ **Zero redundant wrappers** - No mapper/transformer services wrapping domain mappers 1. ✅ **Zero redundant wrappers** - No mapper/transformer services wrapping domain mappers
2. ✅ **Single source of truth** - Domain mappers are the ONLY transformation point 2. ✅ **Single source of truth** - Domain mappers are the ONLY transformation point
3. ✅ **Clear separation** - Domain = business, BFF = infrastructure 3. ✅ **Clear separation** - Domain = business, BFF = infrastructure
4. ✅ **Consistent patterns** - All integrations follow the same clean approach 4. ✅ **Consistent patterns** - All integrations follow the same clean approach
5. ✅ **Centralized DB mappers** - Prisma transformations in one location 5. ✅ **Centralized DB mappers** - Prisma transformations in one location
6. ✅ **Comprehensive documentation** - Patterns documented for future developers 6. ✅ **Comprehensive documentation** - Patterns documented for future developers
@ -369,6 +389,7 @@ The refactoring is complete. We now have:
All domain and BFF layers now follow the "Map Once, Use Everywhere" principle. All domain and BFF layers now follow the "Map Once, Use Everywhere" principle.
The architecture now follows clean separation of concerns: The architecture now follows clean separation of concerns:
- **Domain**: Pure business logic (no infrastructure) - **Domain**: Pure business logic (no infrastructure)
- **Integration**: External system encapsulation (SF, WHMCS) - **Integration**: External system encapsulation (SF, WHMCS)
- **Application**: Workflow coordination (orchestrators) - **Application**: Workflow coordination (orchestrators)
@ -379,4 +400,3 @@ The architecture now follows clean separation of concerns:
**Last Updated**: October 2025 **Last Updated**: October 2025
**Refactored By**: Architecture Review Team **Refactored By**: Architecture Review Team

View File

@ -19,16 +19,19 @@ packages/
## 🎯 **Architecture Principles** ## 🎯 **Architecture Principles**
### **1. Separation of Concerns** ### **1. Separation of Concerns**
- **Dev vs Prod**: Clear separation with appropriate tooling - **Dev vs Prod**: Clear separation with appropriate tooling
- **Services vs Apps**: Development runs apps locally, production containerizes everything - **Services vs Apps**: Development runs apps locally, production containerizes everything
- **Configuration vs Code**: Environment variables for configuration, code for logic - **Configuration vs Code**: Environment variables for configuration, code for logic
### **2. Single Source of Truth** ### **2. Single Source of Truth**
- **One environment template**: `.env.example` - **One environment template**: `.env.example`
- **One Docker Compose** per environment - **One Docker Compose** per environment
- **One script** per operation type - **One script** per operation type
### **3. Clean Dependencies** ### **3. Clean Dependencies**
- **Portal**: Uses `@/lib/*` for shared utilities and services - **Portal**: Uses `@/lib/*` for shared utilities and services
- **BFF**: Feature-aligned modules with shared concerns in `src/common/` - **BFF**: Feature-aligned modules with shared concerns in `src/common/`
- **Domain**: Framework-agnostic types and utilities - **Domain**: Framework-agnostic types and utilities
@ -53,6 +56,7 @@ src/
``` ```
### **Conventions** ### **Conventions**
- Use `@/lib/*` for shared frontend utilities and services - Use `@/lib/*` for shared frontend utilities and services
- Feature modules own their `components/`, `hooks/`, `services/`, and `types/` - Feature modules own their `components/`, `hooks/`, `services/`, and `types/`
- Cross-feature UI belongs in `components/` (atomic design) - Cross-feature UI belongs in `components/` (atomic design)
@ -77,6 +81,7 @@ src/
``` ```
### **Conventions** ### **Conventions**
- Prefer `modules/*` over flat directories per domain - Prefer `modules/*` over flat directories per domain
- Keep DTOs and validators in-module - Keep DTOs and validators in-module
- Reuse `packages/domain` for domain types - Reuse `packages/domain` for domain types
@ -99,18 +104,21 @@ The codebase follows a strict layering pattern to ensure single source of truth
``` ```
#### **1. Contracts Package (`packages/contracts/`)** #### **1. Contracts Package (`packages/contracts/`)**
- **Purpose**: Pure TypeScript interface definitions - single source of truth - **Purpose**: Pure TypeScript interface definitions - single source of truth
- **Contents**: Cross-layer contracts for billing, subscriptions, payments, SIM, orders - **Contents**: Cross-layer contracts for billing, subscriptions, payments, SIM, orders
- **Exports**: Organized by domain (e.g., `@customer-portal/contracts/billing`) - **Exports**: Organized by domain (e.g., `@customer-portal/contracts/billing`)
- **Rule**: ZERO runtime dependencies, only pure types - **Rule**: ZERO runtime dependencies, only pure types
#### **2. Schemas Package (`packages/schemas/`)** #### **2. Schemas Package (`packages/schemas/`)**
- **Purpose**: Runtime validation schemas using Zod - **Purpose**: Runtime validation schemas using Zod
- **Contents**: Matching Zod validators for each contract + integration-specific payload schemas - **Contents**: Matching Zod validators for each contract + integration-specific payload schemas
- **Exports**: Organized by domain and integration provider - **Exports**: Organized by domain and integration provider
- **Usage**: Validate external API responses, request payloads, and user input - **Usage**: Validate external API responses, request payloads, and user input
#### **3. Integration Packages (`packages/integrations/`)** #### **3. Integration Packages (`packages/integrations/`)**
- **Purpose**: Transform raw provider data into shared contracts - **Purpose**: Transform raw provider data into shared contracts
- **Structure**: - **Structure**:
- `packages/integrations/whmcs/` - WHMCS billing integration - `packages/integrations/whmcs/` - WHMCS billing integration
@ -119,16 +127,19 @@ The codebase follows a strict layering pattern to ensure single source of truth
- **Rule**: Must use `@customer-portal/schemas` for validation at boundaries - **Rule**: Must use `@customer-portal/schemas` for validation at boundaries
#### **4. Application Layers** #### **4. Application Layers**
- **BFF** (`apps/bff/`): Import from contracts/schemas, never define duplicate interfaces - **BFF** (`apps/bff/`): Import from contracts/schemas, never define duplicate interfaces
- **Portal** (`apps/portal/`): Import from contracts/schemas, use shared types everywhere - **Portal** (`apps/portal/`): Import from contracts/schemas, use shared types everywhere
- **Rule**: Applications only consume, never define domain types - **Rule**: Applications only consume, never define domain types
### **Legacy: Domain Package (Deprecated)** ### **Legacy: Domain Package (Deprecated)**
- **Status**: Being phased out in favor of contracts + schemas - **Status**: Being phased out in favor of contracts + schemas
- **Migration**: Re-exports now point to contracts package for backward compatibility - **Migration**: Re-exports now point to contracts package for backward compatibility
- **Rule**: New code should import from `@customer-portal/contracts` or `@customer-portal/schemas` - **Rule**: New code should import from `@customer-portal/contracts` or `@customer-portal/schemas`
### **Logging Package** ### **Logging Package**
- **Purpose**: Centralized structured logging - **Purpose**: Centralized structured logging
- **Features**: Pino-based logging with correlation IDs - **Features**: Pino-based logging with correlation IDs
- **Security**: Automatic PII redaction [[memory:6689308]] - **Security**: Automatic PII redaction [[memory:6689308]]
@ -136,11 +147,13 @@ The codebase follows a strict layering pattern to ensure single source of truth
## 🔗 **Integration Architecture** ## 🔗 **Integration Architecture**
### **API Client** ### **API Client**
- **Implementation**: Fetch wrapper using shared Zod schemas from `@customer-portal/domain` - **Implementation**: Fetch wrapper using shared Zod schemas from `@customer-portal/domain`
- **Features**: CSRF protection, auth handling, consistent `ApiResponse` helpers - **Features**: CSRF protection, auth handling, consistent `ApiResponse` helpers
- **Location**: `apps/portal/src/lib/api/` - **Location**: `apps/portal/src/lib/api/`
### **External Services** ### **External Services**
- **WHMCS**: Billing system integration - **WHMCS**: Billing system integration
- **Salesforce**: CRM and order management - **Salesforce**: CRM and order management
- **Redis**: Caching and session storage - **Redis**: Caching and session storage
@ -149,16 +162,19 @@ The codebase follows a strict layering pattern to ensure single source of truth
## 🔒 **Security Architecture** ## 🔒 **Security Architecture**
### **Authentication Flow** ### **Authentication Flow**
- Portal-native authentication with JWT tokens - Portal-native authentication with JWT tokens
- Optional MFA support - Optional MFA support
- Secure token rotation with Redis backing - Secure token rotation with Redis backing
### **Error Handling** ### **Error Handling**
- Never leak sensitive details to end users [[memory:6689308]] - Never leak sensitive details to end users [[memory:6689308]]
- Centralized error mapping to user-friendly messages - Centralized error mapping to user-friendly messages
- Comprehensive audit trails - Comprehensive audit trails
### **Data Protection** ### **Data Protection**
- PII minimization with encryption at rest/in transit - PII minimization with encryption at rest/in transit
- Row-level security (users can only access their data) - Row-level security (users can only access their data)
- Idempotency keys on all mutating operations - Idempotency keys on all mutating operations
@ -166,11 +182,13 @@ The codebase follows a strict layering pattern to ensure single source of truth
## 🚀 **Development Workflow** ## 🚀 **Development Workflow**
### **Path Aliases** ### **Path Aliases**
- **Portal**: `@/*`, `@/lib/*`, `@/features/*`, `@/components/*` - **Portal**: `@/*`, `@/lib/*`, `@/features/*`, `@/components/*`
- **BFF**: `@/*` mapped to `apps/bff/src` - **BFF**: `@/*` mapped to `apps/bff/src`
- **Domain**: Import via `@customer-portal/domain` - **Domain**: Import via `@customer-portal/domain`
### **Code Quality** ### **Code Quality**
- Strict TypeScript rules enforced repository-wide - Strict TypeScript rules enforced repository-wide
- ESLint and Prettier for consistent formatting - ESLint and Prettier for consistent formatting
- Pre-commit hooks for quality gates - Pre-commit hooks for quality gates
@ -178,16 +196,18 @@ The codebase follows a strict layering pattern to ensure single source of truth
## 📈 **Performance & Scalability** ## 📈 **Performance & Scalability**
### **Caching Strategy** ### **Caching Strategy**
- **Invoices**: 60-120s per page; bust on WHMCS webhook - **Invoices**: 60-120s per page; bust on WHMCS webhook
- **Cases**: 30-60s; bust after create/update - **Cases**: 30-60s; bust after create/update
- **Catalog**: 5-15m; manual bust on changes - **Catalog**: 5-15m; manual bust on changes
- **Keys include user_id** to prevent cross-user leakage - **Keys include user_id** to prevent cross-user leakage
### **Database Optimization** ### **Database Optimization**
- Connection pooling with Prisma - Connection pooling with Prisma
- Proper indexing on frequently queried fields - Proper indexing on frequently queried fields
- Optional mirrors for external system data - Optional mirrors for external system data
--- ---
*This architecture supports clean, maintainable code with clear separation of concerns and production-ready security.* _This architecture supports clean, maintainable code with clear separation of concerns and production-ready security._

View File

@ -1,405 +0,0 @@
# BFF Integration Layer Patterns
## Overview
The BFF (Backend for Frontend) integration layer encapsulates all external system interactions and infrastructure concerns. This document defines the patterns and best practices for creating integration services.
---
## Architecture Principles
### Domain Layer vs Integration Layer
**Domain Layer** (`packages/domain/`):
- Business types and validation schemas
- Raw provider types (data structures from external systems)
- Transformation mappers (raw → domain)
- Business validation logic
- **No infrastructure concerns**
**Integration Layer** (`apps/bff/src/integrations/`):
- Query builders (SOQL, GraphQL, etc.)
- Connection services
- Integration services
- HTTP/API clients
- **No business logic**
---
## Integration Service Pattern
### Purpose
Integration services encapsulate all interactions with a specific external system feature, providing a clean abstraction for the application layer.
### Structure
```typescript
@Injectable()
export class SalesforceOrderService {
constructor(
private readonly sf: SalesforceConnection,
@Inject(Logger) private readonly logger: Logger
) {}
async getOrderById(orderId: string): Promise<OrderDetails | null> {
// 1. Build query (infrastructure concern)
const soql = this.buildOrderQuery(orderId);
// 2. Execute query
const rawData = await this.sf.query(soql);
// 3. Use domain mapper (single transformation!)
return DomainProviders.Salesforce.transformOrder(rawData);
}
}
```
### Key Characteristics
1. **Encapsulation**: All system-specific logic hidden from application layer
2. **Single Transformation**: Uses domain mappers, no additional mapping
3. **Returns Domain Types**: Application layer works with domain types only
4. **Infrastructure Details**: Query building, field selection, etc. stay here
---
## Query Builder Pattern
### Location
Query builders belong in **integration layer utils**, not in domain.
**Correct**: `apps/bff/src/integrations/salesforce/utils/order-query-builder.ts`
**Wrong**: `packages/domain/orders/providers/salesforce/query.ts`
### Example
```typescript
/**
* Build field list for Order queries
* Infrastructure concern - not business logic
*/
export function buildOrderSelectFields(
additional: string[] = []
): string[] {
const fields = [
"Id",
"AccountId",
"Status",
// ... all Salesforce field names
];
return UNIQUE([...fields, ...additional]);
}
```
---
## Data Flow
### Clean Architecture Flow
```
┌─────────────────────────────────────────┐
│ Controller (HTTP) │
│ - API endpoints │
│ - Request/Response formatting │
└──────────────┬──────────────────────────┘
┌──────────────▼──────────────────────────┐
│ Orchestrator (Application) │
│ - Workflow coordination │
│ - Uses integration services │
│ - Works with domain types │
└──────────────┬──────────────────────────┘
┌──────────┴──────────┐
│ │
┌───▼───────────┐ ┌──────▼──────────────┐
│ Domain │ │ Integration │
│ (Business) │ │ (Infrastructure) │
│ │ │ │
│ • Types │ │ • SF OrderService │
│ • Schemas │ │ • Query Builders │
│ • Mappers ────┼──┤ • Connections │
│ • Validators │ │ • API Clients │
└───────────────┘ └─────────────────────┘
```
### Single Transformation Principle
**One transformation path**:
```
Query (BFF) → Raw Data → Domain Mapper → Domain Type → Use Directly
└────────── Single transformation ──────────┘
```
**No double transformation**:
```
❌ Query → Raw Data → Domain Mapper → Domain Type → BFF Mapper → ???
```
---
## Integration Service Examples
### Example 1: Salesforce Order Service
```typescript
import { SalesforceConnection } from "./salesforce-connection.service";
import { buildOrderSelectFields } from "../utils/order-query-builder";
import { Providers } from "@customer-portal/domain/orders";
@Injectable()
export class SalesforceOrderService {
constructor(
private readonly sf: SalesforceConnection,
@Inject(Logger) private readonly logger: Logger
) {}
async getOrderById(orderId: string): Promise<OrderDetails | null> {
// Build query using integration layer utils
const fields = buildOrderSelectFields(["Account.Name"]).join(", ");
const soql = `SELECT ${fields} FROM Order WHERE Id = '${orderId}'`;
// Execute query
const result = await this.sf.query(soql);
if (!result.records?.[0]) return null;
// Use domain mapper - single transformation!
return Providers.Salesforce.transformSalesforceOrderDetails(
result.records[0],
[]
);
}
}
```
### Example 2: WHMCS Client Service
```typescript
import { WhmcsConnection } from "./whmcs-connection.service";
import { Providers } from "@customer-portal/domain/customer";
@Injectable()
export class WhmcsClientService {
constructor(
private readonly whmcs: WhmcsConnection,
@Inject(Logger) private readonly logger: Logger
) {}
async getClientById(clientId: number): Promise<CustomerProfile | null> {
// Build request parameters
const params = { clientid: clientId };
// Execute API call
const rawClient = await this.whmcs.call("GetClientsDetails", params);
if (!rawClient) return null;
// Use domain mapper - single transformation!
return Providers.Whmcs.transformWhmcsClient(rawClient);
}
}
```
---
## When to Create an Integration Service
### Create When
✅ You need to query/update data from an external system
✅ The logic involves system-specific query construction
✅ Multiple orchestrators need the same external data
✅ You want to encapsulate provider-specific complexity
### Don't Create When
❌ Simple pass-through without any system-specific logic
❌ One-time queries that won't be reused
❌ Logic that belongs in domain (business rules)
---
## Orchestrator Usage Pattern
### Before (Direct Queries - Wrong)
```typescript
@Injectable()
export class OrderOrchestrator {
constructor(private readonly sf: SalesforceConnection) {}
async getOrder(orderId: string) {
// ❌ Building queries in orchestrator
const soql = `SELECT Id, Status FROM Order WHERE Id = '${orderId}'`;
const result = await this.sf.query(soql);
// ❌ Orchestrator knows about Salesforce structure
return DomainMapper.transform(result.records[0]);
}
}
```
### After (Integration Service - Correct)
```typescript
@Injectable()
export class OrderOrchestrator {
constructor(
private readonly salesforceOrderService: SalesforceOrderService
) {}
async getOrder(orderId: string) {
// ✅ Clean delegation to integration service
return this.salesforceOrderService.getOrderById(orderId);
// ✅ Receives domain type, uses directly
}
}
```
---
## Mapper Usage Pattern
### Direct Domain Mapper Usage (Correct)
```typescript
import { Providers } from "@customer-portal/domain/orders";
// ✅ Use domain mapper directly - no wrapper service
const whmcsItems = Providers.Whmcs.mapFulfillmentOrderItems(items);
```
### Redundant Service Wrapper (Wrong)
```typescript
// ❌ Don't create service wrappers that just delegate
@Injectable()
export class OrderWhmcsMapper {
mapOrderItemsToWhmcs(items: OrderItem[]) {
// Just wraps domain mapper - no value added!
return Providers.Whmcs.mapFulfillmentOrderItems(items);
}
}
```
---
## Module Configuration
### Integration Module
```typescript
@Module({
imports: [ConfigModule],
providers: [
SalesforceConnection,
SalesforceOrderService,
SalesforceAccountService,
],
exports: [
SalesforceConnection,
SalesforceOrderService, // Export for use in application layer
],
})
export class SalesforceModule {}
```
### Application Module
```typescript
@Module({
imports: [
SalesforceModule, // Import integration modules
WhmcsModule,
],
providers: [
OrderOrchestrator, // Uses integration services
],
})
export class OrdersModule {}
```
---
## Testing Integration Services
### Unit Test Pattern
```typescript
describe("SalesforceOrderService", () => {
let service: SalesforceOrderService;
let mockConnection: jest.Mocked<SalesforceConnection>;
beforeEach(() => {
mockConnection = {
query: jest.fn(),
} as any;
service = new SalesforceOrderService(mockConnection, mockLogger);
});
it("should transform raw SF data using domain mapper", async () => {
const mockRawOrder = { Id: "123", Status: "Active" };
mockConnection.query.mockResolvedValue({
records: [mockRawOrder],
});
const result = await service.getOrderById("123");
// Verify domain mapper was used (single transformation)
expect(result).toBeDefined();
expect(result.id).toBe("123");
});
});
```
---
## Benefits of This Pattern
### Architecture Cleanliness
- ✅ Clear separation of concerns
- ✅ Domain stays pure (no infrastructure)
- ✅ Integration complexity encapsulated
### Code Quality
- ✅ Easier to test (mock integration services)
- ✅ Easier to swap providers (change integration layer only)
- ✅ No duplication (single transformation path)
### Developer Experience
- ✅ Clear patterns to follow
- ✅ No confusion about where code belongs
- ✅ Self-documenting architecture
---
## Migration Checklist
When adding new external system integration:
- [ ] Create integration service in `apps/bff/src/integrations/{provider}/services/`
- [ ] Move query builders to `apps/bff/src/integrations/{provider}/utils/`
- [ ] Define raw types in domain `packages/domain/{feature}/providers/{provider}/raw.types.ts`
- [ ] Define mappers in domain `packages/domain/{feature}/providers/{provider}/mapper.ts`
- [ ] Export integration service from provider module
- [ ] Use integration service in orchestrators
- [ ] Use domain mappers directly (no wrapper services)
- [ ] Test integration service independently
---
## Related Documentation
- [Domain Package README](../../packages/domain/README.md)
- [ORDERS-ARCHITECTURE-REVIEW.md](../ORDERS-ARCHITECTURE-REVIEW.md)
- [Schema-First Approach](../../packages/domain/SCHEMA-FIRST-COMPLETE.md)
---
**Last Updated**: October 2025
**Status**: ✅ Active Pattern

View File

@ -6,16 +6,16 @@ The auth module now exposes a clean facade (`AuthFacade`) and a layered structur
## File Map ## File Map
| Purpose | Location | | Purpose | Location |
| ---------------------- | ------------------------------------------------------ | | --------------------- | ------------------------------------------------------------- | ------------- |
| Express cookie helper | `apps/bff/src/app/bootstrap.ts` | | Express cookie helper | `apps/bff/src/app/bootstrap.ts` |
| Auth controller | `modules/auth/presentation/http/auth.controller.ts` | | Auth controller | `modules/auth/presentation/http/auth.controller.ts` |
| Guards/interceptors | `modules/auth/presentation/http/guards|interceptors` | | Guards/interceptors | `modules/auth/presentation/http/guards | interceptors` |
| Passport strategies | `modules/auth/presentation/strategies` | | Passport strategies | `modules/auth/presentation/strategies` |
| Facade (use-cases) | `modules/auth/application/auth.facade.ts` | | Facade (use-cases) | `modules/auth/application/auth.facade.ts` |
| Token services | `modules/auth/infra/token` | | Token services | `modules/auth/infra/token` |
| Rate limiter service | `modules/auth/infra/rate-limiting/auth-rate-limit.service.ts` | | Rate limiter service | `modules/auth/infra/rate-limiting/auth-rate-limit.service.ts` |
| Signup/password flows | `modules/auth/infra/workflows` | | Signup/password flows | `modules/auth/infra/workflows` |
## Development Environment Flags ## Development Environment Flags

View File

@ -18,6 +18,7 @@ Database mappers in the BFF layer are responsible for transforming **Prisma enti
## Location ## Location
All DB mappers are centralized in: All DB mappers are centralized in:
``` ```
apps/bff/src/infra/mappers/ apps/bff/src/infra/mappers/
``` ```
@ -25,11 +26,13 @@ apps/bff/src/infra/mappers/
## Naming Convention ## Naming Convention
All DB mapper functions follow this pattern: All DB mapper functions follow this pattern:
```typescript ```typescript
mapPrisma{EntityName}ToDomain(entity: Prisma{Entity}): Domain{Type} mapPrisma{EntityName}ToDomain(entity: Prisma{Entity}): Domain{Type}
``` ```
### Examples: ### Examples:
- `mapPrismaUserToDomain()` - User entity → AuthenticatedUser - `mapPrismaUserToDomain()` - User entity → AuthenticatedUser
- `mapPrismaMappingToDomain()` - IdMapping entity → UserIdMapping - `mapPrismaMappingToDomain()` - IdMapping entity → UserIdMapping
- `mapPrismaOrderToDomain()` - Order entity → OrderDetails (if we had one) - `mapPrismaOrderToDomain()` - Order entity → OrderDetails (if we had one)
@ -59,7 +62,7 @@ import type { AuthenticatedUser } from "@customer-portal/domain/auth";
/** /**
* Maps Prisma User entity to Domain AuthenticatedUser type * Maps Prisma User entity to Domain AuthenticatedUser type
* *
* NOTE: Profile fields must be fetched from WHMCS - this only maps auth state * NOTE: Profile fields must be fetched from WHMCS - this only maps auth state
*/ */
export function mapPrismaUserToDomain(user: PrismaUser): AuthenticatedUser { export function mapPrismaUserToDomain(user: PrismaUser): AuthenticatedUser {
@ -67,16 +70,16 @@ export function mapPrismaUserToDomain(user: PrismaUser): AuthenticatedUser {
id: user.id, id: user.id,
email: user.email, email: user.email,
role: user.role, role: user.role,
// Transform database booleans // Transform database booleans
mfaEnabled: !!user.mfaSecret, mfaEnabled: !!user.mfaSecret,
emailVerified: user.emailVerified, emailVerified: user.emailVerified,
// Transform Date objects to ISO strings // Transform Date objects to ISO strings
lastLoginAt: user.lastLoginAt?.toISOString(), lastLoginAt: user.lastLoginAt?.toISOString(),
createdAt: user.createdAt.toISOString(), createdAt: user.createdAt.toISOString(),
updatedAt: user.updatedAt.toISOString(), updatedAt: user.updatedAt.toISOString(),
// Profile fields are null - must be fetched from WHMCS // Profile fields are null - must be fetched from WHMCS
firstname: null, firstname: null,
lastname: null, lastname: null,
@ -123,12 +126,12 @@ import { mapPrismaMappingToDomain } from "@bff/infra/mappers";
@Injectable() @Injectable()
export class MappingsService { export class MappingsService {
async findByUserId(userId: string): Promise<UserIdMapping | null> { async findByUserId(userId: string): Promise<UserIdMapping | null> {
const dbMapping = await this.prisma.idMapping.findUnique({ const dbMapping = await this.prisma.idMapping.findUnique({
where: { userId } where: { userId },
}); });
if (!dbMapping) return null; if (!dbMapping) return null;
// Use centralized DB mapper // Use centralized DB mapper
return mapPrismaMappingToDomain(dbMapping); return mapPrismaMappingToDomain(dbMapping);
} }
@ -174,14 +177,14 @@ Providers.Salesforce.transformOrder(
## Key Differences ## Key Differences
| Aspect | DB Mappers | Provider Mappers | | Aspect | DB Mappers | Provider Mappers |
|--------|-----------|------------------| | -------------------- | ----------------------------- | ------------------------------------- |
| **Location** | `apps/bff/src/infra/mappers/` | `packages/domain/{domain}/providers/` | | **Location** | `apps/bff/src/infra/mappers/` | `packages/domain/{domain}/providers/` |
| **Input** | Prisma entity | External API response | | **Input** | Prisma entity | External API response |
| **Layer** | BFF Infrastructure | Domain | | **Layer** | BFF Infrastructure | Domain |
| **Purpose** | Hide ORM implementation | Transform business data | | **Purpose** | Hide ORM implementation | Transform business data |
| **Import in Domain** | ❌ Never | ✅ Internal use only | | **Import in Domain** | ❌ Never | ✅ Internal use only |
| **Import in BFF** | ✅ Yes | ✅ Yes | | **Import in BFF** | ✅ Yes | ✅ Yes |
## Best Practices ## Best Practices
@ -207,25 +210,25 @@ Providers.Salesforce.transformOrder(
DB mappers are pure functions and easy to test: DB mappers are pure functions and easy to test:
```typescript ```typescript
describe('mapPrismaUserToDomain', () => { describe("mapPrismaUserToDomain", () => {
it('should map Prisma user to domain user', () => { it("should map Prisma user to domain user", () => {
const prismaUser: PrismaUser = { const prismaUser: PrismaUser = {
id: 'user-123', id: "user-123",
email: 'test@example.com', email: "test@example.com",
role: 'USER', role: "USER",
mfaSecret: 'secret', mfaSecret: "secret",
emailVerified: true, emailVerified: true,
lastLoginAt: new Date('2025-01-01'), lastLoginAt: new Date("2025-01-01"),
createdAt: new Date('2024-01-01'), createdAt: new Date("2024-01-01"),
updatedAt: new Date('2024-01-01'), updatedAt: new Date("2024-01-01"),
// ... other fields // ... other fields
}; };
const result = mapPrismaUserToDomain(prismaUser); const result = mapPrismaUserToDomain(prismaUser);
expect(result.id).toBe('user-123'); expect(result.id).toBe("user-123");
expect(result.mfaEnabled).toBe(true); expect(result.mfaEnabled).toBe(true);
expect(result.lastLoginAt).toBe('2025-01-01T00:00:00.000Z'); expect(result.lastLoginAt).toBe("2025-01-01T00:00:00.000Z");
}); });
}); });
``` ```
@ -233,6 +236,7 @@ describe('mapPrismaUserToDomain', () => {
## Summary ## Summary
DB mappers are: DB mappers are:
- ✅ **Infrastructure concerns** (BFF layer) - ✅ **Infrastructure concerns** (BFF layer)
- ✅ **Pure transformation functions** (Prisma → Domain) - ✅ **Pure transformation functions** (Prisma → Domain)
- ✅ **Centralized** in `/infra/mappers/` - ✅ **Centralized** in `/infra/mappers/`
@ -241,4 +245,3 @@ DB mappers are:
- ❌ **Never contain business logic** - ❌ **Never contain business logic**
This clear separation ensures the domain layer remains pure and independent of infrastructure choices. This clear separation ensures the domain layer remains pure and independent of infrastructure choices.

View File

@ -4,6 +4,8 @@
The BFF Integration Layer is responsible for communicating with external systems (Salesforce, WHMCS, Freebit, etc.) and transforming their responses into domain types. This document describes the clean architecture patterns we follow. The BFF Integration Layer is responsible for communicating with external systems (Salesforce, WHMCS, Freebit, etc.) and transforming their responses into domain types. This document describes the clean architecture patterns we follow.
---
## Core Principle: "Map Once, Use Everywhere" ## Core Principle: "Map Once, Use Everywhere"
**Domain mappers are the ONLY place that transforms raw provider data to domain types.** **Domain mappers are the ONLY place that transforms raw provider data to domain types.**
@ -20,6 +22,78 @@ Everything else just uses the domain types directly.
in domain layer! in domain layer!
``` ```
---
## Architecture Principles
### Domain Layer vs Integration Layer
**Domain Layer** (`packages/domain/`):
- Business types and validation schemas
- Raw provider types (data structures from external systems)
- Transformation mappers (raw → domain)
- Business validation logic
- **No infrastructure concerns**
**Integration Layer** (`apps/bff/src/integrations/`):
- Query builders (SOQL, GraphQL, etc.)
- Connection services
- Integration services
- HTTP/API clients
- **No business logic**
---
## Data Flow
### Clean Architecture Flow
```
┌─────────────────────────────────────────┐
│ Controller (HTTP) │
│ - API endpoints │
│ - Request/Response formatting │
└──────────────┬──────────────────────────┘
┌──────────────▼──────────────────────────┐
│ Orchestrator (Application) │
│ - Workflow coordination │
│ - Uses integration services │
│ - Works with domain types │
└──────────────┬──────────────────────────┘
┌──────────┴──────────┐
│ │
┌───▼───────────┐ ┌──────▼──────────────┐
│ Domain │ │ Integration │
│ (Business) │ │ (Infrastructure) │
│ │ │ │
│ • Types │ │ • SF OrderService │
│ • Schemas │ │ • Query Builders │
│ • Mappers ────┼──┤ • Connections │
│ • Validators │ │ • API Clients │
└───────────────┘ └─────────────────────┘
```
### Single Transformation Principle
**One transformation path**:
```
Query (BFF) → Raw Data → Domain Mapper → Domain Type → Use Directly
└────────── Single transformation ──────────┘
```
**No double transformation**:
```
❌ Query → Raw Data → Domain Mapper → Domain Type → BFF Mapper → ???
```
---
## Integration Service Pattern ## Integration Service Pattern
### Structure ### Structure
@ -75,7 +149,7 @@ export class SalesforceOrderService {
// 2. Execute query // 2. Execute query
const result = await this.sf.query(soql); const result = await this.sf.query(soql);
const order = result.records?.[0]; const order = result.records?.[0];
if (!order) return null; if (!order) return null;
// 3. Use domain mapper (SINGLE transformation!) // 3. Use domain mapper (SINGLE transformation!)
@ -84,32 +158,9 @@ export class SalesforceOrderService {
} }
``` ```
### ✅ Correct Pattern: Direct Domain Mapper Usage ---
```typescript ## Query Builder Pattern
// In integration service
const defaultCurrency = this.currencyService.getDefaultCurrency();
const invoice = Providers.Whmcs.transformWhmcsInvoice(rawInvoice, {
defaultCurrencyCode: defaultCurrency.code,
defaultCurrencySymbol: defaultCurrency.prefix || defaultCurrency.suffix,
});
```
### ❌ Anti-Pattern: Wrapper Services
```typescript
// DON'T DO THIS - Redundant wrapper!
@Injectable()
export class InvoiceTransformerService {
transformInvoice(whmcsInvoice: WhmcsInvoice): Invoice {
return Providers.Whmcs.transformWhmcsInvoice(whmcsInvoice, {...});
}
}
// This just adds an extra layer with no value!
```
## Query Builders
Query builders belong in the BFF integration layer because they are **infrastructure concerns**. Query builders belong in the BFF integration layer because they are **infrastructure concerns**.
@ -122,7 +173,10 @@ apps/bff/src/integrations/{provider}/utils/
└── soql.util.ts └── soql.util.ts
``` ```
### Example: Order Query Builder **Correct**: `apps/bff/src/integrations/salesforce/utils/order-query-builder.ts`
**Wrong**: `packages/domain/orders/providers/salesforce/query.ts`
### Example
```typescript ```typescript
import { UNIQUE } from "./soql.util"; import { UNIQUE } from "./soql.util";
@ -132,23 +186,21 @@ import { UNIQUE } from "./soql.util";
*/ */
export function buildOrderSelectFields(additional: string[] = []): string[] { export function buildOrderSelectFields(additional: string[] = []): string[] {
const fields = [ const fields = [
"Id", "AccountId", "Status", "Type", "EffectiveDate", "Id",
"OrderNumber", "TotalAmount", "CreatedDate" "AccountId",
]; "Status",
return UNIQUE([...fields, ...additional]); "Type",
} "EffectiveDate",
"OrderNumber",
/** "TotalAmount",
* Build SOQL SELECT fields for OrderItem queries "CreatedDate",
*/
export function buildOrderItemSelectFields(additional: string[] = []): string[] {
const fields = [
"Id", "OrderId", "Quantity", "UnitPrice", "TotalPrice"
]; ];
return UNIQUE([...fields, ...additional]); return UNIQUE([...fields, ...additional]);
} }
``` ```
---
## Common Integration Patterns ## Common Integration Patterns
### Pattern 1: Simple Fetch and Transform ### Pattern 1: Simple Fetch and Transform
@ -157,12 +209,12 @@ export function buildOrderItemSelectFields(additional: string[] = []): string[]
async getEntity(id: string): Promise<DomainType | null> { async getEntity(id: string): Promise<DomainType | null> {
// 1. Build query // 1. Build query
const soql = buildQuery(id); const soql = buildQuery(id);
// 2. Execute // 2. Execute
const result = await this.connection.query(soql); const result = await this.connection.query(soql);
if (!result.records?.[0]) return null; if (!result.records?.[0]) return null;
// 3. Transform with domain mapper // 3. Transform with domain mapper
return Providers.{Provider}.transform{Entity}(result.records[0]); return Providers.{Provider}.transform{Entity}(result.records[0]);
} }
@ -175,18 +227,18 @@ async getOrderWithItems(orderId: string): Promise<OrderDetails | null> {
// 1. Build queries for both order and items // 1. Build queries for both order and items
const orderSoql = buildOrderQuery(orderId); const orderSoql = buildOrderQuery(orderId);
const itemsSoql = buildOrderItemsQuery(orderId); const itemsSoql = buildOrderItemsQuery(orderId);
// 2. Execute in parallel // 2. Execute in parallel
const [orderResult, itemsResult] = await Promise.all([ const [orderResult, itemsResult] = await Promise.all([
this.sf.query(orderSoql), this.sf.query(orderSoql),
this.sf.query(itemsSoql), this.sf.query(itemsSoql),
]); ]);
const order = orderResult.records?.[0]; const order = orderResult.records?.[0];
if (!order) return null; if (!order) return null;
const items = itemsResult.records ?? []; const items = itemsResult.records ?? [];
// 3. Transform with domain mapper (handles both order and items) // 3. Transform with domain mapper (handles both order and items)
return OrderProviders.Salesforce.transformSalesforceOrderDetails(order, items); return OrderProviders.Salesforce.transformSalesforceOrderDetails(order, items);
} }
@ -198,12 +250,12 @@ async getOrderWithItems(orderId: string): Promise<OrderDetails | null> {
async getInvoices(clientId: number): Promise<Invoice[]> { async getInvoices(clientId: number): Promise<Invoice[]> {
// 1. Fetch from API // 1. Fetch from API
const response = await this.whmcsClient.getInvoices({ clientId }); const response = await this.whmcsClient.getInvoices({ clientId });
if (!response.invoices?.invoice) return []; if (!response.invoices?.invoice) return [];
// 2. Get infrastructure context (currency) // 2. Get infrastructure context (currency)
const defaultCurrency = this.currencyService.getDefaultCurrency(); const defaultCurrency = this.currencyService.getDefaultCurrency();
// 3. Transform batch with error handling // 3. Transform batch with error handling
const invoices: Invoice[] = []; const invoices: Invoice[] = [];
for (const whmcsInvoice of response.invoices.invoice) { for (const whmcsInvoice of response.invoices.invoice) {
@ -217,52 +269,37 @@ async getInvoices(clientId: number): Promise<Invoice[]> {
this.logger.error(`Failed to transform invoice ${whmcsInvoice.id}`, { error }); this.logger.error(`Failed to transform invoice ${whmcsInvoice.id}`, { error });
} }
} }
return invoices; return invoices;
} }
``` ```
### Pattern 4: Create/Update Operations ### Pattern 4: Integration with Caching
```typescript
async createOrder(orderFields: Record<string, unknown>): Promise<{ id: string }> {
this.logger.log({ orderType: orderFields.Type }, "Creating Salesforce Order");
try {
const created = await this.sf.sobject("Order").create(orderFields);
this.logger.log({ orderId: created.id }, "Salesforce Order created successfully");
return created;
} catch (error) {
this.logger.error("Failed to create Salesforce Order", { error });
throw error;
}
}
```
## Integration with Caching
```typescript ```typescript
async getInvoice(invoiceId: number, userId: string): Promise<Invoice> { async getInvoice(invoiceId: number, userId: string): Promise<Invoice> {
// 1. Check cache // 1. Check cache
const cached = await this.cacheService.getInvoice(userId, invoiceId); const cached = await this.cacheService.getInvoice(userId, invoiceId);
if (cached) return cached; if (cached) return cached;
// 2. Fetch from API // 2. Fetch from API
const response = await this.whmcsClient.getInvoice({ invoiceid: invoiceId }); const response = await this.whmcsClient.getInvoice({ invoiceid: invoiceId });
// 3. Transform with domain mapper // 3. Transform with domain mapper
const defaultCurrency = this.currencyService.getDefaultCurrency(); const defaultCurrency = this.currencyService.getDefaultCurrency();
const invoice = Providers.Whmcs.transformWhmcsInvoice(response, { const invoice = Providers.Whmcs.transformWhmcsInvoice(response, {
defaultCurrencyCode: defaultCurrency.code, defaultCurrencyCode: defaultCurrency.code,
defaultCurrencySymbol: defaultCurrency.prefix || defaultCurrency.suffix, defaultCurrencySymbol: defaultCurrency.prefix || defaultCurrency.suffix,
}); });
// 4. Cache and return // 4. Cache and return
await this.cacheService.setInvoice(userId, invoiceId, invoice); await this.cacheService.setInvoice(userId, invoiceId, invoice);
return invoice; return invoice;
} }
``` ```
---
## Context Injection Pattern ## Context Injection Pattern
Some transformations need infrastructure context (like currency). Pass it explicitly: Some transformations need infrastructure context (like currency). Pass it explicitly:
@ -277,37 +314,53 @@ const subscription = Providers.Whmcs.transformWhmcsSubscription(whmcsProduct, {
``` ```
**Why this is clean:** **Why this is clean:**
- Domain mapper is pure (deterministic for same inputs) - Domain mapper is pure (deterministic for same inputs)
- Infrastructure concern (currency) is injected from BFF - Infrastructure concern (currency) is injected from BFF
- No service wrapper needed - No service wrapper needed
## Module Organization ---
## Orchestrator Usage Pattern
### Before (Direct Queries - Wrong)
```typescript ```typescript
@Module({ @Injectable()
imports: [ConfigModule], export class OrderOrchestrator {
providers: [ constructor(private readonly sf: SalesforceConnection) {}
// Connection services
SalesforceConnection, async getOrder(orderId: string) {
// ❌ Building queries in orchestrator
// Entity-specific integration services const soql = `SELECT Id, Status FROM Order WHERE Id = '${orderId}'`;
SalesforceOrderService, const result = await this.sf.query(soql);
SalesforceAccountService,
// ❌ Orchestrator knows about Salesforce structure
// Main service return DomainMapper.transform(result.records[0]);
SalesforceService, }
], }
exports: [
SalesforceService,
SalesforceOrderService, // Export for use in other modules
],
})
export class SalesforceModule {}
``` ```
### After (Integration Service - Correct)
```typescript
@Injectable()
export class OrderOrchestrator {
constructor(private readonly salesforceOrderService: SalesforceOrderService) {}
async getOrder(orderId: string) {
// ✅ Clean delegation to integration service
return this.salesforceOrderService.getOrderById(orderId);
// ✅ Receives domain type, uses directly
}
}
```
---
## Anti-Patterns to Avoid ## Anti-Patterns to Avoid
### ❌ Transformer Services ### ❌ Transformer/Wrapper Services
```typescript ```typescript
// DON'T: Create wrapper services that just call domain mappers // DON'T: Create wrapper services that just call domain mappers
@ -320,30 +373,13 @@ export class InvoiceTransformerService {
``` ```
**Why it's bad:** **Why it's bad:**
- Adds unnecessary layer - Adds unnecessary layer
- No value beyond what domain mapper provides - No value beyond what domain mapper provides
- Makes codebase harder to understand - Makes codebase harder to understand
**Instead:** Use domain mapper directly in integration service **Instead:** Use domain mapper directly in integration service
### ❌ Mapper Services
```typescript
// DON'T: Wrap domain mappers in injectable services
@Injectable()
export class FreebitMapperService {
mapToSimDetails(response: unknown): SimDetails {
return FreebitProvider.transformFreebitAccountDetails(response);
}
}
```
**Why it's bad:**
- Just wraps domain mapper
- Adds injection complexity for no benefit
**Instead:** Import domain mapper directly and use it
### ❌ Multiple Transformations ### ❌ Multiple Transformations
```typescript ```typescript
@ -353,14 +389,37 @@ const intermediate = this.customTransform(rawData);
const domainType = Providers.X.transform(intermediate); const domainType = Providers.X.transform(intermediate);
``` ```
**Why it's bad:**
- Multiple transformation points
- Risk of data inconsistency
- Harder to maintain
**Instead:** Transform once using domain mapper **Instead:** Transform once using domain mapper
## Best Practices ---
## Module Organization
```typescript
@Module({
imports: [ConfigModule],
providers: [
// Connection services
SalesforceConnection,
// Entity-specific integration services
SalesforceOrderService,
SalesforceAccountService,
// Main service
SalesforceService,
],
exports: [
SalesforceService,
SalesforceOrderService, // Export for use in other modules
],
})
export class SalesforceModule {}
```
---
## Best Practices Summary
### ✅ DO ### ✅ DO
@ -381,19 +440,17 @@ const domainType = Providers.X.transform(intermediate);
5. **Hard-code queries** - use query builders 5. **Hard-code queries** - use query builders
6. **Skip error handling** - always log failures 6. **Skip error handling** - always log failures
## Summary ---
**Integration services** are responsible for: ## Migration Checklist
- 🔧 Building queries (SOQL, API params)
- 🌐 Executing API calls
- 🔄 Using domain mappers to transform responses
- 📦 Returning domain types
**Integration services** should NOT: When adding new external system integration:
- ❌ Wrap domain mappers in services
- ❌ Add additional transformation layers
- ❌ Contain business logic
- ❌ Expose raw provider types
**Remember:** "Map Once, Use Everywhere" - domain mappers are the single source of truth for transformations.
- [ ] Create integration service in `apps/bff/src/integrations/{provider}/services/`
- [ ] Move query builders to `apps/bff/src/integrations/{provider}/utils/`
- [ ] Define raw types in domain `packages/domain/{feature}/providers/{provider}/raw.types.ts`
- [ ] Define mappers in domain `packages/domain/{feature}/providers/{provider}/mapper.ts`
- [ ] Export integration service from provider module
- [ ] Use integration service in orchestrators
- [ ] Use domain mappers directly (no wrapper services)
- [ ] Test integration service independently

View File

@ -3,6 +3,7 @@
## Current Problem ## Current Problem
The frontend was polling order details every 5-15 seconds, causing: The frontend was polling order details every 5-15 seconds, causing:
- Unnecessary load on backend (Salesforce API calls, DB queries) - Unnecessary load on backend (Salesforce API calls, DB queries)
- High API costs - High API costs
- Battery drain on mobile devices - Battery drain on mobile devices
@ -117,20 +118,24 @@ WebSockets provide bidirectional communication but are more complex to implement
## Migration Path ## Migration Path
### Week 1: Remove Polling ### Week 1: Remove Polling
- [x] Remove aggressive 5-second polling - [x] Remove aggressive 5-second polling
- [x] Document interim strategy - [x] Document interim strategy
### Week 2: Implement SSE Infrastructure ### Week 2: Implement SSE Infrastructure
- [x] Create `OrderEventsService` in BFF - [x] Create `OrderEventsService` in BFF
- [x] Expose SSE endpoint `GET /orders/:sfOrderId/events` - [x] Expose SSE endpoint `GET /orders/:sfOrderId/events`
- [x] Publish fulfillment lifecycle events (activating, completed, failed) - [x] Publish fulfillment lifecycle events (activating, completed, failed)
### Week 3: Frontend Integration ### Week 3: Frontend Integration
- [x] Create `useOrderUpdates` hook - [x] Create `useOrderUpdates` hook
- [x] Wire `OrderDetail` to SSE (no timers) - [x] Wire `OrderDetail` to SSE (no timers)
- [x] Auto-refetch details on push updates - [x] Auto-refetch details on push updates
### Week 4: Post-launch Monitoring ### Week 4: Post-launch Monitoring
- [ ] Add observability for SSE connection counts - [ ] Add observability for SSE connection counts
- [ ] Track client error rates and reconnection attempts - [ ] Track client error rates and reconnection attempts
- [ ] Review UX analytics after rollout - [ ] Review UX analytics after rollout

View File

@ -8,6 +8,7 @@
## 🎯 Core Principle: Domain Package = Pure Domain Logic ## 🎯 Core Principle: Domain Package = Pure Domain Logic
The `@customer-portal/domain` package should contain **ONLY** pure domain logic that is: The `@customer-portal/domain` package should contain **ONLY** pure domain logic that is:
- ✅ Framework-agnostic - ✅ Framework-agnostic
- ✅ Reusable across frontend and backend - ✅ Reusable across frontend and backend
- ✅ Pure TypeScript (no React, no NestJS, no Next.js) - ✅ Pure TypeScript (no React, no NestJS, no Next.js)
@ -17,14 +18,14 @@ The `@customer-portal/domain` package should contain **ONLY** pure domain logic
## 📦 Package Structure Matrix ## 📦 Package Structure Matrix
| Type | Location | Examples | Reasoning | | Type | Location | Examples | Reasoning |
|------|----------|----------|-----------| | ---------------------- | ----------------------------------------- | ----------------------------------------- | ------------------------- |
| **Domain Types** | `packages/domain/*/contract.ts` | `Invoice`, `Order`, `Customer` | Pure business entities | | **Domain Types** | `packages/domain/*/contract.ts` | `Invoice`, `Order`, `Customer` | Pure business entities |
| **Validation Schemas** | `packages/domain/*/schema.ts` | `invoiceSchema`, `orderQueryParamsSchema` | Runtime validation | | **Validation Schemas** | `packages/domain/*/schema.ts` | `invoiceSchema`, `orderQueryParamsSchema` | Runtime validation |
| **Pure Utilities** | `packages/domain/toolkit/` | `formatCurrency()`, `parseDate()` | No framework dependencies | | **Pure Utilities** | `packages/domain/toolkit/` | `formatCurrency()`, `parseDate()` | No framework dependencies |
| **Provider Mappers** | `packages/domain/*/providers/` | `transformWhmcsInvoice()` | Data transformation logic | | **Provider Mappers** | `packages/domain/*/providers/` | `transformWhmcsInvoice()` | Data transformation logic |
| **Framework Utils** | `apps/*/src/lib/` or `apps/*/src/core/` | API clients, React hooks | Framework-specific code | | **Framework Utils** | `apps/*/src/lib/` or `apps/*/src/core/` | API clients, React hooks | Framework-specific code |
| **Shared Infra** | `packages/validation`, `packages/logging` | `ZodPipe`, `useZodForm` | Framework bridges | | **Shared Infra** | `packages/validation`, `packages/logging` | `ZodPipe`, `useZodForm` | Framework bridges |
--- ---
@ -33,6 +34,7 @@ The `@customer-portal/domain` package should contain **ONLY** pure domain logic
### ✅ **What Belongs in `packages/domain/`** ### ✅ **What Belongs in `packages/domain/`**
#### 1. Domain Types & Contracts #### 1. Domain Types & Contracts
```typescript ```typescript
// packages/domain/billing/contract.ts // packages/domain/billing/contract.ts
export interface Invoice { export interface Invoice {
@ -43,6 +45,7 @@ export interface Invoice {
``` ```
#### 2. Validation Schemas (Zod) #### 2. Validation Schemas (Zod)
```typescript ```typescript
// packages/domain/billing/schema.ts // packages/domain/billing/schema.ts
export const invoiceSchema = z.object({ export const invoiceSchema = z.object({
@ -60,12 +63,10 @@ export const invoiceQueryParamsSchema = z.object({
``` ```
#### 3. Pure Utility Functions #### 3. Pure Utility Functions
```typescript ```typescript
// packages/domain/toolkit/formatting/currency.ts // packages/domain/toolkit/formatting/currency.ts
export function formatCurrency( export function formatCurrency(amount: number, currency: SupportedCurrency): string {
amount: number,
currency: SupportedCurrency
): string {
return new Intl.NumberFormat("en-US", { return new Intl.NumberFormat("en-US", {
style: "currency", style: "currency",
currency, currency,
@ -78,6 +79,7 @@ export function formatCurrency(
**Reusable** - both frontend and backend can use it **Reusable** - both frontend and backend can use it
#### 4. Provider Mappers #### 4. Provider Mappers
```typescript ```typescript
// packages/domain/billing/providers/whmcs/mapper.ts // packages/domain/billing/providers/whmcs/mapper.ts
export function transformWhmcsInvoice(raw: WhmcsInvoiceRaw): Invoice { export function transformWhmcsInvoice(raw: WhmcsInvoiceRaw): Invoice {
@ -94,22 +96,25 @@ export function transformWhmcsInvoice(raw: WhmcsInvoiceRaw): Invoice {
### ❌ **What Should NOT Be in `packages/domain/`** ### ❌ **What Should NOT Be in `packages/domain/`**
#### 1. Framework-Specific API Clients #### 1. Framework-Specific API Clients
```typescript ```typescript
// ❌ DO NOT put in domain // ❌ DO NOT put in domain
// apps/portal/src/lib/api/client.ts // apps/portal/src/lib/api/client.ts
import { ApiClient } from "@hey-api/client-fetch"; // ← Framework dependency import { ApiClient } from "@hey-api/client-fetch"; // ← Framework dependency
export const apiClient = new ApiClient({ export const apiClient = new ApiClient({
baseUrl: process.env.NEXT_PUBLIC_API_URL, // ← Next.js specific baseUrl: process.env.NEXT_PUBLIC_API_URL, // ← Next.js specific
}); });
``` ```
**Why?** **Why?**
- Depends on `@hey-api/client-fetch` (external library) - Depends on `@hey-api/client-fetch` (external library)
- Uses Next.js environment variables - Uses Next.js environment variables
- Runtime infrastructure code - Runtime infrastructure code
#### 2. React Hooks #### 2. React Hooks
```typescript ```typescript
// ❌ DO NOT put in domain // ❌ DO NOT put in domain
// apps/portal/src/lib/hooks/useInvoices.ts // apps/portal/src/lib/hooks/useInvoices.ts
@ -123,13 +128,15 @@ export function useInvoices() {
**Why?** React-specific - backend can't use this **Why?** React-specific - backend can't use this
#### 3. Error Handling with Framework Dependencies #### 3. Error Handling with Framework Dependencies
```typescript ```typescript
// ❌ DO NOT put in domain // ❌ DO NOT put in domain
// apps/portal/src/lib/utils/error-handling.ts // apps/portal/src/lib/utils/error-handling.ts
import { ApiError as ClientApiError } from "@/lib/api"; // ← Framework client import { ApiError as ClientApiError } from "@/lib/api"; // ← Framework client
export function getErrorInfo(error: unknown): ApiErrorInfo { export function getErrorInfo(error: unknown): ApiErrorInfo {
if (error instanceof ClientApiError) { // ← Framework-specific error type if (error instanceof ClientApiError) {
// ← Framework-specific error type
// ... // ...
} }
} }
@ -138,6 +145,7 @@ export function getErrorInfo(error: unknown): ApiErrorInfo {
**Why?** Depends on the API client implementation **Why?** Depends on the API client implementation
#### 4. NestJS-Specific Utilities #### 4. NestJS-Specific Utilities
```typescript ```typescript
// ❌ DO NOT put in domain // ❌ DO NOT put in domain
// apps/bff/src/core/utils/error.util.ts // apps/bff/src/core/utils/error.util.ts
@ -151,6 +159,7 @@ export function getErrorMessage(error: unknown): string {
``` ```
**This one is borderline** - it's generic enough it COULD be in domain, but: **This one is borderline** - it's generic enough it COULD be in domain, but:
- Only used by backend - Only used by backend
- Not needed for type definitions - Not needed for type definitions
- Better to keep application-specific utils in apps - Better to keep application-specific utils in apps
@ -208,12 +217,14 @@ apps/
Ask these questions: Ask these questions:
### ✅ Move to Domain If: ### ✅ Move to Domain If:
1. Is it a **pure function** with no framework dependencies? 1. Is it a **pure function** with no framework dependencies?
2. Does it work with **domain types**? 2. Does it work with **domain types**?
3. Could **both frontend and backend** use it? 3. Could **both frontend and backend** use it?
4. Is it **business logic** (not infrastructure)? 4. Is it **business logic** (not infrastructure)?
### ❌ Keep in Apps If: ### ❌ Keep in Apps If:
1. Does it import **React**, **Next.js**, or **NestJS**? 1. Does it import **React**, **Next.js**, or **NestJS**?
2. Does it use **environment variables**? 2. Does it use **environment variables**?
3. Does it depend on **external libraries** (API clients, HTTP libs)? 3. Does it depend on **external libraries** (API clients, HTTP libs)?
@ -224,16 +235,16 @@ Ask these questions:
## 📋 Examples with Decisions ## 📋 Examples with Decisions
| Utility | Decision | Location | Why | | Utility | Decision | Location | Why |
|---------|----------|----------|-----| | ---------------------------------- | --------------------- | --------------------------------- | -------------------------- |
| `formatCurrency(amount, currency)` | ✅ Domain | `domain/toolkit/formatting/` | Pure function, no deps | | `formatCurrency(amount, currency)` | ✅ Domain | `domain/toolkit/formatting/` | Pure function, no deps |
| `invoiceQueryParamsSchema` | ✅ Domain | `domain/billing/schema.ts` | Domain-specific validation | | `invoiceQueryParamsSchema` | ✅ Domain | `domain/billing/schema.ts` | Domain-specific validation |
| `paginationParamsSchema` | ✅ Domain | `domain/common/schema.ts` | Truly generic | | `paginationParamsSchema` | ✅ Domain | `domain/common/schema.ts` | Truly generic |
| `useInvoices()` React hook | ❌ Portal App | `portal/lib/hooks/` | React-specific | | `useInvoices()` React hook | ❌ Portal App | `portal/lib/hooks/` | React-specific |
| `apiClient` instance | ❌ Portal App | `portal/lib/api/` | Framework-specific | | `apiClient` instance | ❌ Portal App | `portal/lib/api/` | Framework-specific |
| `ZodValidationPipe` | ✅ Validation Package | `packages/validation/` | Reusable bridge | | `ZodValidationPipe` | ✅ Validation Package | `packages/validation/` | Reusable bridge |
| `getErrorMessage(error)` | ❌ BFF App | `bff/core/utils/` | App-specific utility | | `getErrorMessage(error)` | ❌ BFF App | `bff/core/utils/` | App-specific utility |
| `transformWhmcsInvoice()` | ✅ Domain | `domain/billing/providers/whmcs/` | Data transformation | | `transformWhmcsInvoice()` | ✅ Domain | `domain/billing/providers/whmcs/` | Data transformation |
--- ---
@ -242,6 +253,7 @@ Ask these questions:
### Current Structure (✅ Correct!) ### Current Structure (✅ Correct!)
**Generic building blocks** in `domain/common/`: **Generic building blocks** in `domain/common/`:
```typescript ```typescript
// packages/domain/common/schema.ts // packages/domain/common/schema.ts
export const paginationParamsSchema = z.object({ export const paginationParamsSchema = z.object({
@ -257,26 +269,28 @@ export const filterParamsSchema = z.object({
``` ```
**Domain-specific query params** in their own domains: **Domain-specific query params** in their own domains:
```typescript ```typescript
// packages/domain/billing/schema.ts // packages/domain/billing/schema.ts
export const invoiceQueryParamsSchema = z.object({ export const invoiceQueryParamsSchema = z.object({
page: z.coerce.number().int().positive().optional(), page: z.coerce.number().int().positive().optional(),
limit: z.coerce.number().int().positive().max(100).optional(), limit: z.coerce.number().int().positive().max(100).optional(),
status: invoiceStatusSchema.optional(), // ← Domain-specific status: invoiceStatusSchema.optional(), // ← Domain-specific
dateFrom: z.string().datetime().optional(), // ← Domain-specific dateFrom: z.string().datetime().optional(), // ← Domain-specific
dateTo: z.string().datetime().optional(), // ← Domain-specific dateTo: z.string().datetime().optional(), // ← Domain-specific
}); });
// packages/domain/subscriptions/schema.ts // packages/domain/subscriptions/schema.ts
export const subscriptionQueryParamsSchema = z.object({ export const subscriptionQueryParamsSchema = z.object({
page: z.coerce.number().int().positive().optional(), page: z.coerce.number().int().positive().optional(),
limit: z.coerce.number().int().positive().max(100).optional(), limit: z.coerce.number().int().positive().max(100).optional(),
status: subscriptionStatusSchema.optional(), // ← Domain-specific status: subscriptionStatusSchema.optional(), // ← Domain-specific
type: z.string().optional(), // ← Domain-specific type: z.string().optional(), // ← Domain-specific
}); });
``` ```
**Why this works:** **Why this works:**
- `common` has truly generic utilities - `common` has truly generic utilities
- Each domain owns its specific query parameters - Each domain owns its specific query parameters
- No duplication of business logic - No duplication of business logic
@ -286,7 +300,9 @@ export const subscriptionQueryParamsSchema = z.object({
## 📖 Summary ## 📖 Summary
### Domain Package (`packages/domain/`) ### Domain Package (`packages/domain/`)
**Contains:** **Contains:**
- ✅ Domain types & interfaces - ✅ Domain types & interfaces
- ✅ Zod validation schemas - ✅ Zod validation schemas
- ✅ Provider mappers (WHMCS, Salesforce, Freebit) - ✅ Provider mappers (WHMCS, Salesforce, Freebit)
@ -294,13 +310,16 @@ export const subscriptionQueryParamsSchema = z.object({
- ✅ Domain-specific query parameter schemas - ✅ Domain-specific query parameter schemas
**Does NOT contain:** **Does NOT contain:**
- ❌ React hooks - ❌ React hooks
- ❌ API client instances - ❌ API client instances
- ❌ Framework-specific code - ❌ Framework-specific code
- ❌ Infrastructure code - ❌ Infrastructure code
### App Lib/Core Directories ### App Lib/Core Directories
**Contains:** **Contains:**
- ✅ Framework-specific utilities - ✅ Framework-specific utilities
- ✅ API clients & HTTP interceptors - ✅ API clients & HTTP interceptors
- ✅ React hooks & custom hooks - ✅ React hooks & custom hooks
@ -310,4 +329,3 @@ export const subscriptionQueryParamsSchema = z.object({
--- ---
**Key Takeaway**: The domain package is your **single source of truth for types and validation**. Everything else stays in apps where it belongs! **Key Takeaway**: The domain package is your **single source of truth for types and validation**. Everything else stays in apps where it belongs!

View File

@ -8,11 +8,13 @@
## 🎯 Architecture Philosophy ## 🎯 Architecture Philosophy
**Core Principle**: Domain-first organization where each business domain owns its: **Core Principle**: Domain-first organization where each business domain owns its:
- **contract.ts** - Normalized types (provider-agnostic) - **contract.ts** - Normalized types (provider-agnostic)
- **schema.ts** - Runtime validation (Zod) - **schema.ts** - Runtime validation (Zod)
- **providers/** - Provider-specific adapters (raw types + mappers) - **providers/** - Provider-specific adapters (raw types + mappers)
**Why This Works**: **Why This Works**:
- Domain-centric matches business thinking - Domain-centric matches business thinking
- Provider isolation prevents leaking implementation details - Provider isolation prevents leaking implementation details
- Adding new providers = adding new folders (no refactoring) - Adding new providers = adding new folders (no refactoring)
@ -103,6 +105,7 @@ packages/domain/
## 📝 Import Patterns ## 📝 Import Patterns
### **Application Code (Domain Only)** ### **Application Code (Domain Only)**
```typescript ```typescript
// Import normalized domain types // Import normalized domain types
import { Invoice, invoiceSchema, INVOICE_STATUS } from "@customer-portal/domain/billing"; import { Invoice, invoiceSchema, INVOICE_STATUS } from "@customer-portal/domain/billing";
@ -121,12 +124,13 @@ const validated = invoiceSchema.parse(rawData);
``` ```
### **Integration Code (Needs Provider Specifics)** ### **Integration Code (Needs Provider Specifics)**
```typescript ```typescript
// Import domain + provider // Import domain + provider
import { Invoice, invoiceSchema } from "@customer-portal/domain/billing"; import { Invoice, invoiceSchema } from "@customer-portal/domain/billing";
import { import {
transformWhmcsInvoice, transformWhmcsInvoice,
type WhmcsInvoiceRaw type WhmcsInvoiceRaw,
} from "@customer-portal/domain/billing/providers/whmcs/mapper"; } from "@customer-portal/domain/billing/providers/whmcs/mapper";
import { whmcsInvoiceRawSchema } from "@customer-portal/domain/billing/providers/whmcs/raw.types"; import { whmcsInvoiceRawSchema } from "@customer-portal/domain/billing/providers/whmcs/raw.types";
@ -140,10 +144,11 @@ const invoice: Invoice = transformWhmcsInvoice(whmcsData);
## 🏗️ Domain File Templates ## 🏗️ Domain File Templates
### **contract.ts** ### **contract.ts**
```typescript ```typescript
/** /**
* {Domain} - Contract * {Domain} - Contract
* *
* Normalized types for {domain} that all providers must map to. * Normalized types for {domain} that all providers must map to.
*/ */
@ -164,10 +169,11 @@ export interface {Domain} {
``` ```
### **schema.ts** ### **schema.ts**
```typescript ```typescript
/** /**
* {Domain} - Schemas * {Domain} - Schemas
* *
* Zod validation for {domain} types. * Zod validation for {domain} types.
*/ */
@ -183,10 +189,11 @@ export const {domain}Schema = z.object({
``` ```
### **providers/{provider}/raw.types.ts** ### **providers/{provider}/raw.types.ts**
```typescript ```typescript
/** /**
* {Provider} {Domain} Provider - Raw Types * {Provider} {Domain} Provider - Raw Types
* *
* Actual API response structure from {Provider}. * Actual API response structure from {Provider}.
*/ */
@ -200,10 +207,11 @@ export type {Provider}{Domain}Raw = z.infer<typeof {provider}{Domain}RawSchema>;
``` ```
### **providers/{provider}/mapper.ts** ### **providers/{provider}/mapper.ts**
```typescript ```typescript
/** /**
* {Provider} {Domain} Provider - Mapper * {Provider} {Domain} Provider - Mapper
* *
* Transforms {Provider} raw data into normalized domain types. * Transforms {Provider} raw data into normalized domain types.
*/ */
@ -214,14 +222,14 @@ import { type {Provider}{Domain}Raw, {provider}{Domain}RawSchema } from "./raw.t
export function transform{Provider}{Domain}(raw: unknown): {Domain} { export function transform{Provider}{Domain}(raw: unknown): {Domain} {
// 1. Validate raw data // 1. Validate raw data
const validated = {provider}{Domain}RawSchema.parse(raw); const validated = {provider}{Domain}RawSchema.parse(raw);
// 2. Transform to domain model // 2. Transform to domain model
const result: {Domain} = { const result: {Domain} = {
id: validated.someId, id: validated.someId,
status: mapStatus(validated.rawStatus), status: mapStatus(validated.rawStatus),
// ... map all fields // ... map all fields
}; };
// 3. Validate domain model // 3. Validate domain model
return {domain}Schema.parse(result); return {domain}Schema.parse(result);
} }
@ -232,7 +240,9 @@ export function transform{Provider}{Domain}(raw: unknown): {Domain} {
## 🎓 Key Patterns ## 🎓 Key Patterns
### **1. Co-location** ### **1. Co-location**
Everything about a domain lives together: Everything about a domain lives together:
``` ```
billing/ billing/
├── contract.ts # What billing IS ├── contract.ts # What billing IS
@ -241,7 +251,9 @@ billing/
``` ```
### **2. Provider Isolation** ### **2. Provider Isolation**
Raw types and mappers stay in `providers/`: Raw types and mappers stay in `providers/`:
```typescript ```typescript
// ✅ GOOD - Isolated // ✅ GOOD - Isolated
import { transformWhmcsInvoice } from "@customer-portal/domain/billing/providers/whmcs/mapper"; import { transformWhmcsInvoice } from "@customer-portal/domain/billing/providers/whmcs/mapper";
@ -251,7 +263,9 @@ import { WhmcsInvoiceRaw } from "@somewhere/global";
``` ```
### **3. Schema-Driven** ### **3. Schema-Driven**
Domain schemas define the contract: Domain schemas define the contract:
```typescript ```typescript
// Contract (types) // Contract (types)
export interface Invoice { ... } export interface Invoice { ... }
@ -264,7 +278,9 @@ return invoiceSchema.parse(transformedData);
``` ```
### **4. Provider Agnostic** ### **4. Provider Agnostic**
App code never knows about providers: App code never knows about providers:
```typescript ```typescript
// ✅ App only knows domain // ✅ App only knows domain
function displayInvoice(invoice: Invoice) { function displayInvoice(invoice: Invoice) {
@ -285,11 +301,13 @@ async function getInvoice(id: number): Promise<Invoice> {
Example: Adding Stripe as an invoice provider Example: Adding Stripe as an invoice provider
**1. Create provider folder:** **1. Create provider folder:**
```bash ```bash
mkdir -p packages/domain/billing/providers/stripe mkdir -p packages/domain/billing/providers/stripe
``` ```
**2. Add raw types:** **2. Add raw types:**
```typescript ```typescript
// billing/providers/stripe/raw.types.ts // billing/providers/stripe/raw.types.ts
export const stripeInvoiceRawSchema = z.object({ export const stripeInvoiceRawSchema = z.object({
@ -300,6 +318,7 @@ export const stripeInvoiceRawSchema = z.object({
``` ```
**3. Add mapper:** **3. Add mapper:**
```typescript ```typescript
// billing/providers/stripe/mapper.ts // billing/providers/stripe/mapper.ts
export function transformStripeInvoice(raw: unknown): Invoice { export function transformStripeInvoice(raw: unknown): Invoice {
@ -313,6 +332,7 @@ export function transformStripeInvoice(raw: unknown): Invoice {
``` ```
**4. Use in service:** **4. Use in service:**
```typescript ```typescript
// No changes to domain contract needed! // No changes to domain contract needed!
import { transformStripeInvoice } from "@customer-portal/domain/billing/providers/stripe/mapper"; import { transformStripeInvoice } from "@customer-portal/domain/billing/providers/stripe/mapper";
@ -343,4 +363,3 @@ const invoice = transformStripeInvoice(stripeData);
--- ---
**Status**: Implementation in progress. See TODO list for remaining work. **Status**: Implementation in progress. See TODO list for remaining work.

View File

@ -1,29 +1,22 @@
# Consolidated Type System - Complete Solution # Domain Type System
## Problem Analysis This document describes the unified type system for the Customer Portal, focusing on how types map to Salesforce and WHMCS data structures.
You identified a critical issue: **order items, catalog items, and product types were all trying to encapsulate the same Salesforce Product2 data**, leading to: ---
1. **Type duplication** across contexts ## Core Principle
2. **Inconsistent pricing models** (sometimes `price`, sometimes `monthlyPrice`/`oneTimePrice`)
3. **Enhanced Order Summary** creating its own conflicting `OrderItem` interface
4. **Missing PricebookEntry representation** in the type system
## Solution: Unified Type Architecture
### Core Principle
**One Salesforce Object = One TypeScript Type Structure** **One Salesforce Object = One TypeScript Type Structure**
Since both catalog items and order items ultimately represent Salesforce `Product2` objects (with `PricebookEntry` for pricing), we created a unified type system that directly maps to the Salesforce/WHMCS object structure. Since both catalog items and order items ultimately represent Salesforce `Product2` objects (with `PricebookEntry` for pricing), we have a unified type system that directly maps to the Salesforce/WHMCS object structure.
## New Type Hierarchy ---
### 1. Base Product Structure ## Product Types
### Base Product Structure
```typescript ```typescript
// packages/domain/src/entities/product.ts
// Maps directly to Salesforce Product2 fields // Maps directly to Salesforce Product2 fields
interface BaseProduct { interface BaseProduct {
// Standard Salesforce fields // Standard Salesforce fields
@ -56,14 +49,13 @@ interface PricebookEntry {
// Product with proper pricing structure // Product with proper pricing structure
interface ProductWithPricing extends BaseProduct { interface ProductWithPricing extends BaseProduct {
pricebookEntry?: PricebookEntry; pricebookEntry?: PricebookEntry;
// Convenience fields derived from pricebookEntry and billingCycle
unitPrice?: number; // PricebookEntry.UnitPrice unitPrice?: number; // PricebookEntry.UnitPrice
monthlyPrice?: number; // UnitPrice if billingCycle === "Monthly" monthlyPrice?: number; // UnitPrice if billingCycle === "Monthly"
oneTimePrice?: number; // UnitPrice if billingCycle === "Onetime" oneTimePrice?: number; // UnitPrice if billingCycle === "Onetime"
} }
``` ```
### 2. Specialized Product Types ### Specialized Product Types
```typescript ```typescript
// Category-specific extensions // Category-specific extensions
@ -85,7 +77,11 @@ interface SimProduct extends ProductWithPricing {
type Product = InternetProduct | SimProduct | VpnProduct | ProductWithPricing; type Product = InternetProduct | SimProduct | VpnProduct | ProductWithPricing;
``` ```
### 3. Order Item Types ---
## Order Types
### Order Item Types
```typescript ```typescript
// For new orders (before Salesforce creation) // For new orders (before Salesforce creation)
@ -109,7 +105,7 @@ interface SalesforceOrderItem {
} }
``` ```
### 4. Order Types ### Order Types
```typescript ```typescript
// Salesforce Order structure // Salesforce Order structure
@ -143,34 +139,12 @@ type Order = WhmcsOrder | SalesforceOrder;
type OrderItem = WhmcsOrderItem | OrderItemRequest | SalesforceOrderItem; type OrderItem = WhmcsOrderItem | OrderItemRequest | SalesforceOrderItem;
``` ```
## Key Benefits ---
### 1. **Single Source of Truth**
- All types map directly to Salesforce object structure
- No more guessing which type to use in which context
- Consistent field names across the application
### 2. **Proper PricebookEntry Representation**
- Pricing is now properly modeled as PricebookEntry structure
- Convenience fields (`monthlyPrice`, `oneTimePrice`) derived from `unitPrice` and `billingCycle`
- No more confusion between `price` vs `monthlyPrice` vs `unitPrice`
### 3. **Type Safety**
- TypeScript discrimination based on `category` field
- Proper type guards for business logic
- Compile-time validation of field access
### 4. **Maintainability**
- Changes to Salesforce fields only need updates in one place
- Clear transformation functions between Salesforce API and TypeScript types
- Backward compatibility through type aliases
## Transformation Functions ## Transformation Functions
### Salesforce to Domain
```typescript ```typescript
// Transform Salesforce Product2 + PricebookEntry to unified Product // Transform Salesforce Product2 + PricebookEntry to unified Product
function fromSalesforceProduct2(sfProduct: any, pricebookEntry?: any): Product { function fromSalesforceProduct2(sfProduct: any, pricebookEntry?: any): Product {
@ -205,36 +179,42 @@ function fromSalesforceProduct2(sfProduct: any, pricebookEntry?: any): Product {
unitPrice: unitPrice, unitPrice: unitPrice,
monthlyPrice: billingCycle === "Monthly" ? unitPrice : undefined, monthlyPrice: billingCycle === "Monthly" ? unitPrice : undefined,
oneTimePrice: billingCycle === "Onetime" ? unitPrice : undefined, oneTimePrice: billingCycle === "Onetime" ? unitPrice : undefined,
// Include all other fields for dynamic access
...sfProduct,
};
}
// Transform Salesforce OrderItem to unified structure
function fromSalesforceOrderItem(sfOrderItem: any): SalesforceOrderItem {
const product = fromSalesforceProduct2(
sfOrderItem.PricebookEntry?.Product2,
sfOrderItem.PricebookEntry
);
return {
id: sfOrderItem.Id,
orderId: sfOrderItem.OrderId,
quantity: sfOrderItem.Quantity,
unitPrice: sfOrderItem.UnitPrice,
totalPrice: sfOrderItem.TotalPrice,
pricebookEntry: {
...product.pricebookEntry!,
product2: product,
},
whmcsServiceId: sfOrderItem.WHMCS_Service_ID__c,
billingCycle: product.billingCycle,
...sfOrderItem,
}; };
} }
``` ```
### Type Guards
```typescript
// Type-safe product checking
function isInternetProduct(product: Product): product is InternetProduct {
return product.category === "Internet";
}
function isSimProduct(product: Product): product is SimProduct {
return product.category === "SIM";
}
function isVpnProduct(product: Product): product is VpnProduct {
return product.category === "VPN";
}
// Business logic helpers
function isCatalogVisible(product: Product): boolean {
return product.portalCatalog && product.portalAccessible;
}
function isServiceProduct(product: Product): boolean {
return product.itemClass === "Service";
}
function isAddonProduct(product: Product): boolean {
return product.itemClass === "Add-on";
}
```
---
## Usage Examples ## Usage Examples
### Catalog Context ### Catalog Context
@ -245,6 +225,11 @@ const product = fromSalesforceProduct2(salesforceProduct, pricebookEntry);
if (isCatalogVisible(product)) { if (isCatalogVisible(product)) {
displayInCatalog(product); displayInCatalog(product);
} }
// Type-safe access to specific fields
if (isInternetProduct(product)) {
console.log(product.internetPlanTier); // TypeScript knows this exists
}
``` ```
### Order Context ### Order Context
@ -255,16 +240,6 @@ const orderItem: OrderItemRequest = {
quantity: 1, quantity: 1,
autoAdded: false, autoAdded: false,
}; };
```
### Enhanced Order Summary
```typescript
// Now uses consistent unified types
interface OrderItem extends OrderItemRequest {
id?: string;
description?: string;
}
// Access unified pricing fields // Access unified pricing fields
const price = const price =
@ -273,44 +248,33 @@ const price =
: item.oneTimePrice || item.unitPrice || 0; : item.oneTimePrice || item.unitPrice || 0;
``` ```
## Migration Strategy ---
1. **Backward Compatibility**: Legacy type aliases maintained ## Benefits
```typescript 1. **Single Source of Truth**: All types map directly to Salesforce object structure
export type InternetPlan = InternetProduct; 2. **No Duplication**: Same type used in catalog, orders, and business logic
export type CatalogItem = Product; 3. **Type Safety**: Proper TypeScript discrimination based on `category` field
``` 4. **Maintainability**: Changes to Salesforce fields only need updates in one place
5. **Consistency**: Same field names and structure across all contexts
2. **Gradual Adoption**: Existing code continues to work while new code uses unified types ---
3. **Clear Documentation**: This document explains the new structure and migration path ## Backward Compatibility
## Files Modified Legacy type aliases are maintained for existing code:
- `packages/domain/src/entities/product.ts` - New unified product types ```typescript
- `packages/domain/src/entities/order.ts` - Updated order types export type InternetPlan = InternetProduct;
export type CatalogItem = Product;
```
This allows gradual migration while new code uses unified types.
---
## Implementation Files
- `packages/domain/src/entities/product.ts` - Core unified types
- `packages/domain/src/entities/catalog.ts` - Re-exports with legacy aliases - `packages/domain/src/entities/catalog.ts` - Re-exports with legacy aliases
- `apps/portal/src/features/catalog/components/base/EnhancedOrderSummary.tsx` - Updated to use unified types - `packages/domain/src/entities/order.ts` - Updated order types
## Enhanced Order Summary
The **Enhanced Order Summary** was a UI component that created its own `OrderItem` interface, leading to type conflicts. It has been updated to:
1. **Extend unified types**: Now extends `OrderItemRequest` instead of creating conflicting interfaces
2. **Use consistent pricing**: Uses `monthlyPrice`/`oneTimePrice`/`unitPrice` from unified structure
3. **Proper field access**: Uses `itemClass`, `billingCycle` from unified product structure
This eliminates the confusion about "which OrderItem type should I use?" because there's now a clear hierarchy:
- `OrderItemRequest` - for new orders in the portal
- `SalesforceOrderItem` - for existing orders from Salesforce
- `WhmcsOrderItem` - for existing orders from WHMCS
The Enhanced Order Summary now properly represents the unified product structure while adding UI-specific fields like `description` and optional `id`.
## Conclusion
This consolidated type system eliminates the original problem of multiple types trying to represent the same Salesforce data. Now there's **one unified type system** that properly maps to your Salesforce/WHMCS object structure, with clear transformation functions and proper PricebookEntry representation.
The solution maintains backward compatibility while providing a clear path forward for consistent type usage across your entire application.

View File

@ -13,14 +13,15 @@ When you run `prisma generate`, Prisma creates a client at `node_modules/.prisma
```javascript ```javascript
// Inside generated client (simplified) // Inside generated client (simplified)
const config = { const config = {
schemaPath: "prisma/schema.prisma", // embedded at generate time schemaPath: "prisma/schema.prisma", // embedded at generate time
// ... // ...
} };
``` ```
### 2. Monorepo vs Production Directory Structures ### 2. Monorepo vs Production Directory Structures
**During Build (Monorepo):** **During Build (Monorepo):**
``` ```
/app/ /app/
├── apps/ ├── apps/
@ -32,6 +33,7 @@ const config = {
``` ```
**In Production (After pnpm deploy):** **In Production (After pnpm deploy):**
``` ```
/app/ /app/
├── prisma/schema.prisma ← Schema moved to root! ├── prisma/schema.prisma ← Schema moved to root!
@ -42,6 +44,7 @@ const config = {
### 3. The Mismatch ### 3. The Mismatch
In production, the schema is at: In production, the schema is at:
``` ```
prisma/schema.prisma prisma/schema.prisma
``` ```
@ -57,23 +60,29 @@ RUN pnpm dlx prisma@${PRISMA_VERSION} generate --schema=prisma/schema.prisma
``` ```
This regenerates the client with: This regenerates the client with:
```javascript ```javascript
schemaPath: "prisma/schema.prisma" // ← Matches production! schemaPath: "prisma/schema.prisma"; // ← Matches production!
``` ```
## Best Practices ## Best Practices
### 1. Document the Behavior ### 1. Document the Behavior
Add comments in Dockerfile and schema.prisma explaining why regeneration is needed. Add comments in Dockerfile and schema.prisma explaining why regeneration is needed.
### 2. Version Lock Prisma ### 2. Version Lock Prisma
Use explicit version in Docker: Use explicit version in Docker:
```dockerfile ```dockerfile
RUN pnpm dlx prisma@${PRISMA_VERSION} generate --schema=prisma/schema.prisma RUN pnpm dlx prisma@${PRISMA_VERSION} generate --schema=prisma/schema.prisma
``` ```
### 3. Match Prisma Versions ### 3. Match Prisma Versions
Ensure the version in Docker matches `package.json`: Ensure the version in Docker matches `package.json`:
```json ```json
{ {
"dependencies": { "dependencies": {
@ -86,7 +95,9 @@ Ensure the version in Docker matches `package.json`:
``` ```
### 4. Include Binary Targets ### 4. Include Binary Targets
Always include the production binary target in schema: Always include the production binary target in schema:
```prisma ```prisma
generator client { generator client {
provider = "prisma-client-js" provider = "prisma-client-js"
@ -97,24 +108,28 @@ generator client {
## Debugging ## Debugging
### Check Embedded Path ### Check Embedded Path
You can inspect the generated client's config: You can inspect the generated client's config:
```bash ```bash
cat node_modules/.prisma/client/index.js | grep schemaPath cat node_modules/.prisma/client/index.js | grep schemaPath
``` ```
### Verify Schema Location ### Verify Schema Location
In the container: In the container:
```bash ```bash
ls -la /app/prisma/schema.prisma ls -la /app/prisma/schema.prisma
``` ```
### Common Errors ### Common Errors
| Error | Cause | Fix | | Error | Cause | Fix |
|-------|-------|-----| | ----------------------------------- | ------------------------------------------- | ------------------------------------- |
| `Could not load --schema from path` | Embedded path doesn't match actual location | Regenerate from production structure | | `Could not load --schema from path` | Embedded path doesn't match actual location | Regenerate from production structure |
| `Prisma Client not found` | Client not generated or not copied | Run `prisma generate` in Dockerfile | | `Prisma Client not found` | Client not generated or not copied | Run `prisma generate` in Dockerfile |
| `Query engine not found` | Missing binary target | Add correct `binaryTargets` to schema | | `Query engine not found` | Missing binary target | Add correct `binaryTargets` to schema |
## Further Reading ## Further Reading

View File

@ -173,10 +173,10 @@ OrderItems [
### OrderItem Status Updates ### OrderItem Status Updates
| Field | Source | Example | Notes | | Field | Source | Example | Notes |
| --------------------- | -------------------- | --------- | ---------------------------- | | --------------------- | -------------------------------- | --------- | ---------------------------- |
| `WHMCS_Service_ID__c` | AddOrder response (`serviceids`) | "67890" | Individual service ID | | `WHMCS_Service_ID__c` | AddOrder response (`serviceids`) | "67890" | Individual service ID |
| `Billing_Cycle__c` | Already set | "Monthly" | No change during fulfillment | | `Billing_Cycle__c` | Already set | "Monthly" | No change during fulfillment |
## 🔧 Data Transformation Rules ## 🔧 Data Transformation Rules

View File

@ -0,0 +1,80 @@
# SIM Order & Lifecycle States
This document stays in sync with the strongly typed lifecycle definitions that live in the SIM
domain package. When workflow steps change, update the types first and then revise this doc so
deployments, QA, and onboarding all use the same vocabulary.
## Core Types
The lifecycle exports below are the single source of truth for state names and transitions.
```1:120:packages/domain/sim/lifecycle.ts
export const SIM_LIFECYCLE_STAGE = {
CHECKOUT: "checkout",
ORDER_PENDING_REVIEW: "order.pendingReview",
ACTIVATION_PROCESSING: "activation.processing",
ACTIVATION_FAILED_PAYMENT: "activation.failedPayment",
ACTIVATION_PROVISIONING: "activation.provisioning",
SERVICE_ACTIVE: "service.active",
PLAN_CHANGE_SCHEDULED: "planChange.scheduled",
PLAN_CHANGE_APPLIED: "planChange.applied",
CANCELLATION_SCHEDULED: "cancellation.scheduled",
SERVICE_CANCELLED: "service.cancelled",
} as const;
export const SIM_MANAGEMENT_ACTION = {
TOP_UP_DATA: "topUpData",
CHANGE_PLAN: "changePlan",
UPDATE_FEATURES: "updateFeatures",
CANCEL_SIM: "cancelSim",
REISSUE_ESIM: "reissueEsim",
} as const;
```
- `SimLifecycleStage` — union of the keys above.
- `SimManagementAction` — canonical action names referenced by BFF services, workers, and docs.
- `SIM_ORDER_FLOW` — ordered list of activation transitions.
- `SIM_MANAGEMENT_FLOW` — per-action transitions that describe how a request moves between states.
## Order + Activation Flow (`SIM_ORDER_FLOW`)
| Step | Transition | Details |
| ---- | ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 1 | `checkout.createOrder`: `checkout → order.pendingReview` | `orderWithSkuValidationSchema` ensures the cart is valid before the BFF writes a Salesforce order. |
| 2 | `orders.activateSim`: `order.pendingReview → activation.processing` | `SimOrderActivationService` locks the request (cache key) and kicks off billing. |
| 3a | `payments.failure`: `activation.processing → activation.failedPayment` | WHMCS capture failed. Invoice is cancelled via `SimBillingService`, user must retry. |
| 3b | `payments.success`: `activation.processing → activation.provisioning` | Payment succeeded; Freebit provisioning and add-on updates run. |
| 4 | `provisioning.complete`: `activation.provisioning → service.active` | Freebit returns success, cache records the idempotent result, and the monthly WHMCS subscription is scheduled for the first of next month via `SimScheduleService`. |
## SIM Management Actions (`SIM_MANAGEMENT_FLOW`)
All customer-facing actions execute through `SimActionRunnerService`, guaranteeing consistent
notifications and structured logs. The table below maps directly to the typed transitions.
| Action (`SimManagementAction`) | Transition(s) | Notes |
| ------------------------------ | ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| `topUpData` | `service.active → service.active` | One-time invoice captured through `SimBillingService`; Freebit quota increases immediately. |
| `changePlan` | `service.active → planChange.scheduled → planChange.applied → service.active` | `SimScheduleService.resolveScheduledDate` auto-picks the first day of next month unless the user provides `scheduledAt`. |
| `updateFeatures` | `service.active → service.active` | Voice toggles run instantly. If `networkType` changes, the second phase is queued (see below). |
| `cancelSim` | `service.active → cancellation.scheduled → service.cancelled` | Default schedule = first day of next month; users can override by passing a valid `YYYYMMDD` date. |
| `reissueEsim` | `service.active → service.active` | Freebit eSIM profile reissued; lifecycle stage does not change. |
## Deferred Jobs
Network-type changes are persisted as BullMQ jobs so they survive restarts and include retry
telemetry.
- Enqueue: `SimPlanService``SimManagementQueueService.scheduleNetworkTypeChange`
- Worker: `SimManagementProcessor` consumes the `sim-management` queue and calls
`FreebitOrchestratorService.updateSimFeatures` when the delay expires.
## Validation & Feedback Layers
| Layer | Enforcement | Customer feedback |
| --------------- | ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
| Frontend | Domain schemas (`orderWithSkuValidationSchema`, SIM configure schemas) | Immediate form errors (e.g., missing activation fee, invalid EID). |
| BFF | `SimOrderActivationService`, `SimPlanService`, `SimTopUpService` | Sanitized business errors (`VAL_001`, payment failures) routed through Secure Error Mapper. |
| Background jobs | `SimManagementProcessor` | Logged with request metadata; failures fan out via `SimNotificationService`. |
Keep this file and `packages/domain/sim/lifecycle.ts` in lockstep. When adding a new stage or action,
update the domain types first, then describe the change here so every team shares the same model.

View File

@ -1,343 +0,0 @@
# 📝 Centralized Logging System
This guide covers the **centralized, high-performance logging system** implemented using **Pino** - the fastest JSON logger for Node.js applications.
## 🎯 **Architecture Overview**
### **Single Logger System**
- ✅ **Backend (BFF)**: `nestjs-pino` with Pino
- ✅ **Frontend**: Custom structured logger compatible with backend
- ✅ **Shared**: Common interfaces and configurations
- ❌ **No more mixed logging systems**
### **Benefits of Centralization**
- 🚀 **Performance**: Pino is 5x faster than winston
- 🔒 **Security**: Automatic sensitive data sanitization
- 📊 **Structured**: JSON logging for better parsing
- 🌐 **Correlation**: Request tracking across services
- 📈 **Monitoring**: Easy integration with log aggregation'
- this implemetnatin requires more type safety faeteur
## 🏗️ **Implementation**
### **Backend (BFF) - NestJS + Pino**
```typescript
// ✅ CORRECT: Use nestjs-pino Logger
import { Logger } from "nestjs-pino";
@Injectable()
export class UserService {
constructor(@Inject(Logger) private readonly logger: Logger) {}
async findUser(id: string) {
this.logger.info(`Finding user ${id}`);
// ... implementation
}
}
```
### **Frontend - Structured Logger**
```typescript
// ✅ CORRECT: Use shared logger interface
import { logger } from "@/lib/logger";
export function handleApiCall() {
logger.logApiCall("/api/users", "GET", 200, 150);
logger.logUserAction("user123", "login");
}
```
## 🚫 **What NOT to Do**
### **❌ Don't Use Multiple Logging Systems**
```typescript
// ❌ WRONG: Mixing logging systems
import { Logger } from "@nestjs/common"; // Don't use this - REMOVED
import { Logger } from "nestjs-pino"; // ✅ Use this - CENTRALIZED
// ❌ WRONG: Console logging in production
console.log("User logged in"); // Don't use console.log
logger.info("User logged in"); // Use structured logger
```
### **✅ Current Status: FULLY CENTRALIZED**
- **All BFF services** now use `nestjs-pino` Logger
- **No more** `@nestjs/common` Logger imports
- **No more** `new Logger()` instantiations
- **Single logging system** throughout the entire backend
### **❌ Don't Use Console Methods**
```typescript
// ❌ WRONG: Direct console usage
console.log("Debug info");
console.error("Error occurred");
console.warn("Warning message");
// ✅ CORRECT: Structured logging
logger.debug("Debug info");
logger.error("Error occurred");
logger.warn("Warning message");
```
## 🔧 **Configuration**
### **Environment Variables**
```bash
# Logging configuration
LOG_LEVEL=info # error, warn, info, debug, trace
APP_NAME=customer-portal-bff # Service identifier
NODE_ENV=development # Environment context
```
### **Log Levels**
| Level | Numeric | Description |
| ------- | ------- | ------------------------------------ |
| `error` | 0 | Errors that need immediate attention |
| `warn` | 1 | Warnings that should be monitored |
| `info` | 2 | General information about operations |
| `debug` | 3 | Detailed debugging information |
| `trace` | 4 | Very detailed tracing information |
## 📝 **Usage Examples**
### **Basic Logging**
```typescript
// Simple messages
logger.info("User authentication successful");
logger.warn("Rate limit approaching");
logger.error("Database connection failed");
// With structured data
logger.info("Invoice created", {
invoiceId: "INV-001",
amount: 99.99,
userId: "user123",
});
```
### **API Call Logging**
```typescript
// Automatic API call logging
logger.logApiCall("/api/invoices", "POST", 201, 250, {
userId: "user123",
invoiceId: "INV-001",
});
```
### **User Action Logging**
```typescript
// User activity tracking
logger.logUserAction("user123", "password_change", {
ipAddress: "192.168.1.1",
userAgent: "Mozilla/5.0...",
});
```
### **Error Logging**
```typescript
// Comprehensive error logging
try {
// ... operation
} catch (error) {
logger.logError(error, "user_creation", {
userId: "user123",
email: "user@example.com",
});
}
```
## 🔍 **Request Correlation**
### **Automatic Correlation IDs**
```typescript
// Every request gets a unique correlation ID
// Headers: x-correlation-id: 1703123456789-abc123def
logger.info("Processing request", {
correlationId: req.headers["x-correlation-id"],
endpoint: req.url,
method: req.method,
});
```
### **Manual Correlation**
```typescript
// Set correlation context
logger.setCorrelationId("req-123");
logger.setUserId("user-456");
logger.setRequestId("req-789");
// All subsequent logs include this context
logger.info("User action completed");
```
## 📊 **Production Logging**
### **File Logging**
```typescript
// Automatic log rotation
// - Combined logs: logs/customer-portal-bff-combined.log
// - Error logs: logs/customer-portal-bff-error.log
// - Console output: stdout (for container logs)
```
### **Log Aggregation**
```typescript
// Structured JSON output for easy parsing
{
"timestamp": "2025-01-22T10:30:00.000Z",
"level": "info",
"service": "customer-portal-bff",
"environment": "production",
"message": "User authentication successful",
"correlationId": "req-123",
"userId": "user-456",
"data": {
"ipAddress": "192.168.1.1",
"userAgent": "Mozilla/5.0..."
}
}
```
## 🚀 **Performance Benefits**
### **Benchmarks (Pino vs Winston)**
| Operation | Pino | Winston | Improvement |
| -------------------- | ---- | ------- | ------------- |
| JSON serialization | 1x | 5x | **5x faster** |
| Object serialization | 1x | 3x | **3x faster** |
| String interpolation | 1x | 2x | **2x faster** |
| Overall performance | 1x | 5x | **5x faster** |
### **Memory Usage**
- **Pino**: Minimal memory footprint
- **Winston**: Higher memory usage due to object retention
- **Console**: No memory overhead but unstructured
## 🔒 **Security Features**
### **Automatic Data Sanitization**
```typescript
// Sensitive data is automatically redacted
logger.info("User login", {
email: "user@example.com",
password: "[REDACTED]", // Automatically sanitized
token: "[REDACTED]", // Automatically sanitized
ipAddress: "192.168.1.1", // Safe to log
});
```
### **Sanitized Headers**
```typescript
// Request headers are automatically cleaned
{
"authorization": "[REDACTED]",
"cookie": "[REDACTED]",
"x-api-key": "[REDACTED]",
"user-agent": "Mozilla/5.0...", // Safe to log
"accept": "application/json" // Safe to log
}
```
## 🔄 **Migration Guide**
### **From @nestjs/common Logger**
```typescript
// ❌ OLD: @nestjs/common Logger
import { Logger } from "@nestjs/common";
private readonly logger = new Logger(ServiceName.name);
// ✅ NEW: nestjs-pino Logger
import { Logger } from "nestjs-pino";
constructor(@Inject(Logger) private readonly logger: Logger) {}
```
### **From Console Logging**
```typescript
// ❌ OLD: Console methods
console.log("Debug info");
console.error("Error occurred");
// ✅ NEW: Structured logger
logger.debug("Debug info");
logger.error("Error occurred");
```
### **From Winston**
```typescript
// ❌ OLD: Winston logger
import * as winston from 'winston';
const logger = winston.createLogger({...});
// ✅ NEW: Pino logger
import { Logger } from "nestjs-pino";
constructor(@Inject(Logger) private readonly logger: Logger) {}
```
## 📋 **Best Practices**
### **✅ Do's**
- Use structured logging with context
- Include correlation IDs in all logs
- Log at appropriate levels
- Sanitize sensitive data
- Use consistent message formats
### **❌ Don'ts**
- Don't mix logging systems
- Don't use console methods in production
- Don't log sensitive information
- Don't log large objects unnecessarily
- Don't use string interpolation for logging
## 🛠️ **Troubleshooting**
### **Common Issues**
1. **Multiple Logger Instances**: Ensure single logger per service
2. **Missing Correlation IDs**: Check middleware configuration
3. **Performance Issues**: Verify log level settings
4. **Missing Logs**: Check file permissions and disk space
### **Debug Mode**
```bash
# Enable debug logging
LOG_LEVEL=debug pnpm dev
# Check logger configuration
LOG_LEVEL=trace pnpm dev
```
## 📚 **Additional Resources**
- [Pino Documentation](https://getpino.io/)
- [NestJS Pino Module](https://github.com/iamolegga/nestjs-pino)
- [Structured Logging Best Practices](https://12factor.net/logs)
- [Log Correlation Patterns](https://microservices.io/patterns/observability/distributed-tracing.html)

View File

@ -1,107 +0,0 @@
# 📊 Logging Configuration Guide
## Quick Log Level Changes
### Using the Script (Recommended)
```bash
# Check current level
./scripts/set-log-level.sh
# Set to minimal logging (production-like)
./scripts/set-log-level.sh warn
# Set to normal development logging
./scripts/set-log-level.sh info
# Set to detailed debugging
./scripts/set-log-level.sh debug
```
### Manual Configuration
Edit `.env` file:
```bash
LOG_LEVEL="info" # Change this value
```
## Log Levels Explained
| Level | Numeric | What You'll See | Best For |
| ------- | ------- | -------------------- | ------------------------- |
| `error` | 0 | Only critical errors | Production monitoring |
| `warn` | 1 | Warnings + errors | Quiet development |
| `info` | 2 | General operations | **Normal development** ⭐ |
| `debug` | 3 | Detailed debugging | Troubleshooting issues |
| `trace` | 4 | Very verbose tracing | Deep debugging |
## What's Been Optimized
### ✅ Reduced Noise
- **HTTP requests/responses**: Filtered out health checks, static assets
- **Request bodies**: Hidden by default (security + noise reduction)
- **Response bodies**: Hidden by default (reduces overwhelming output)
- **Session checks**: Frequent `/api/auth/session` calls ignored
### ✅ Cleaner Output
- **Pretty formatting**: Colored, timestamped logs in development
- **Message focus**: Emphasizes actual log messages over metadata
- **Structured data**: Still available but not overwhelming
### ✅ Security Enhanced
- **Sensitive data**: Automatically redacted (tokens, passwords, etc.)
- **Production ready**: No debug info exposed to customers
## Common Scenarios
### 🔇 Too Much Noise?
```bash
./scripts/set-log-level.sh warn
```
### 🐛 Debugging Issues?
```bash
./scripts/set-log-level.sh debug
```
### 🚀 Normal Development?
```bash
./scripts/set-log-level.sh info
```
### 📊 Production Monitoring?
```bash
./scripts/set-log-level.sh error
```
## Environment Variables
```bash
# Core logging
LOG_LEVEL="info" # Main log level
DISABLE_HTTP_LOGGING="false" # Set to "true" to disable HTTP logs entirely
# Application context
APP_NAME="customer-portal-bff" # Service name in logs
NODE_ENV="development" # Affects log formatting
```
## Restart Required
After changing log levels, restart your development server:
```bash
# Stop current server (Ctrl+C)
# Then restart
pnpm dev
```
The new log level will take effect immediately.

331
docs/operations/logging.md Normal file
View File

@ -0,0 +1,331 @@
# Centralized Logging System
This guide covers the centralized, high-performance logging system implemented using **Pino** - the fastest JSON logger for Node.js applications.
---
## Architecture Overview
### Single Logger System
- **Backend (BFF)**: `nestjs-pino` with Pino
- **Frontend**: Custom structured logger compatible with backend
- **Shared**: Common interfaces and configurations
### Benefits
- **Performance**: Pino is 5x faster than winston
- **Security**: Automatic sensitive data sanitization
- **Structured**: JSON logging for better parsing
- **Correlation**: Request tracking across services
- **Monitoring**: Easy integration with log aggregation
---
## Log Levels
| Level | Numeric | What You'll See | Best For |
| ------- | ------- | -------------------- | ---------------------- |
| `error` | 0 | Only critical errors | Production monitoring |
| `warn` | 1 | Warnings + errors | Quiet development |
| `info` | 2 | General operations | **Normal development** |
| `debug` | 3 | Detailed debugging | Troubleshooting issues |
| `trace` | 4 | Very verbose tracing | Deep debugging |
---
## Configuration
### Environment Variables
```bash
# Core logging
LOG_LEVEL="info" # Main log level
DISABLE_HTTP_LOGGING="false" # Set to "true" to disable HTTP logs entirely
# Application context
APP_NAME="customer-portal-bff" # Service name in logs
NODE_ENV="development" # Affects log formatting
```
### Using the Script
```bash
# Check current level
./scripts/set-log-level.sh
# Set to minimal logging (production-like)
./scripts/set-log-level.sh warn
# Set to normal development logging
./scripts/set-log-level.sh info
# Set to detailed debugging
./scripts/set-log-level.sh debug
```
---
## Implementation
### Backend (BFF) - NestJS + Pino
```typescript
// ✅ CORRECT: Use nestjs-pino Logger
import { Logger } from "nestjs-pino";
@Injectable()
export class UserService {
constructor(@Inject(Logger) private readonly logger: Logger) {}
async findUser(id: string) {
this.logger.info(`Finding user ${id}`);
// ... implementation
}
}
```
### Frontend - Structured Logger
```typescript
// ✅ CORRECT: Use shared logger interface
import { logger } from "@/lib/logger";
export function handleApiCall() {
logger.logApiCall("/api/users", "GET", 200, 150);
logger.logUserAction("user123", "login");
}
```
---
## Usage Examples
### Basic Logging
```typescript
// Simple messages
logger.info("User authentication successful");
logger.warn("Rate limit approaching");
logger.error("Database connection failed");
// With structured data
logger.info("Invoice created", {
invoiceId: "INV-001",
amount: 99.99,
userId: "user123",
});
```
### API Call Logging
```typescript
logger.logApiCall("/api/invoices", "POST", 201, 250, {
userId: "user123",
invoiceId: "INV-001",
});
```
### User Action Logging
```typescript
logger.logUserAction("user123", "password_change", {
ipAddress: "192.168.1.1",
userAgent: "Mozilla/5.0...",
});
```
### Error Logging
```typescript
try {
// ... operation
} catch (error) {
logger.logError(error, "user_creation", {
userId: "user123",
email: "user@example.com",
});
}
```
---
## Request Correlation
### Automatic Correlation IDs
```typescript
// Every request gets a unique correlation ID
// Headers: x-correlation-id: 1703123456789-abc123def
logger.info("Processing request", {
correlationId: req.headers["x-correlation-id"],
endpoint: req.url,
method: req.method,
});
```
### Manual Correlation
```typescript
// Set correlation context
logger.setCorrelationId("req-123");
logger.setUserId("user-456");
logger.setRequestId("req-789");
// All subsequent logs include this context
logger.info("User action completed");
```
---
## What's Been Optimized
### Reduced Noise
- **HTTP requests/responses**: Filtered out health checks, static assets
- **Request bodies**: Hidden by default (security + noise reduction)
- **Response bodies**: Hidden by default (reduces overwhelming output)
- **Session checks**: Frequent `/api/auth/session` calls ignored
### Cleaner Output
- **Pretty formatting**: Colored, timestamped logs in development
- **Message focus**: Emphasizes actual log messages over metadata
- **Structured data**: Still available but not overwhelming
### Security Enhanced
- **Sensitive data**: Automatically redacted (tokens, passwords, etc.)
- **Production ready**: No debug info exposed to customers
---
## Production Logging
### Structured JSON Output
```json
{
"timestamp": "2025-01-22T10:30:00.000Z",
"level": "info",
"service": "customer-portal-bff",
"environment": "production",
"message": "User authentication successful",
"correlationId": "req-123",
"userId": "user-456",
"data": {
"ipAddress": "192.168.1.1",
"userAgent": "Mozilla/5.0..."
}
}
```
### File Logging
```
# Automatic log rotation
- Combined logs: logs/customer-portal-bff-combined.log
- Error logs: logs/customer-portal-bff-error.log
- Console output: stdout (for container logs)
```
---
## Security Features
### Automatic Data Sanitization
```typescript
// Sensitive data is automatically redacted
logger.info("User login", {
email: "user@example.com",
password: "[REDACTED]", // Automatically sanitized
token: "[REDACTED]", // Automatically sanitized
ipAddress: "192.168.1.1", // Safe to log
});
```
### Sanitized Headers
```json
{
"authorization": "[REDACTED]",
"cookie": "[REDACTED]",
"x-api-key": "[REDACTED]",
"user-agent": "Mozilla/5.0...",
"accept": "application/json"
}
```
---
## What NOT to Do
### Don't Use Multiple Logging Systems
```typescript
// ❌ WRONG: Mixing logging systems
import { Logger } from "@nestjs/common"; // Don't use this
// ✅ CORRECT: Use centralized logger
import { Logger } from "nestjs-pino";
```
### Don't Use Console Methods
```typescript
// ❌ WRONG: Direct console usage
console.log("Debug info");
console.error("Error occurred");
// ✅ CORRECT: Structured logging
logger.debug("Debug info");
logger.error("Error occurred");
```
---
## Performance Benefits
### Benchmarks (Pino vs Winston)
| Operation | Pino | Winston | Improvement |
| -------------------- | ---- | ------- | ------------- |
| JSON serialization | 1x | 5x | **5x faster** |
| Object serialization | 1x | 3x | **3x faster** |
| String interpolation | 1x | 2x | **2x faster** |
| Overall performance | 1x | 5x | **5x faster** |
---
## Common Scenarios
| Need | Command |
| --------------------- | ---------------------------------- |
| Too much noise | `./scripts/set-log-level.sh warn` |
| Debugging issues | `./scripts/set-log-level.sh debug` |
| Normal development | `./scripts/set-log-level.sh info` |
| Production monitoring | `./scripts/set-log-level.sh error` |
After changing log levels, restart your development server for changes to take effect.
---
## Best Practices
### Do's
- Use structured logging with context
- Include correlation IDs in all logs
- Log at appropriate levels
- Sanitize sensitive data
- Use consistent message formats
### Don'ts
- Don't mix logging systems
- Don't use console methods in production
- Don't log sensitive information
- Don't log large objects unnecessarily
- Don't use string interpolation for logging

View File

@ -1,81 +0,0 @@
# SIM Order & Lifecycle States
This document stays in sync with the strongly typed lifecycle definitions that live in the SIM
domain package. When workflow steps change, update the types first and then revise this doc so
deployments, QA, and onboarding all use the same vocabulary.
## Core Types
The lifecycle exports below are the single source of truth for state names and transitions.
```1:120:packages/domain/sim/lifecycle.ts
export const SIM_LIFECYCLE_STAGE = {
CHECKOUT: "checkout",
ORDER_PENDING_REVIEW: "order.pendingReview",
ACTIVATION_PROCESSING: "activation.processing",
ACTIVATION_FAILED_PAYMENT: "activation.failedPayment",
ACTIVATION_PROVISIONING: "activation.provisioning",
SERVICE_ACTIVE: "service.active",
PLAN_CHANGE_SCHEDULED: "planChange.scheduled",
PLAN_CHANGE_APPLIED: "planChange.applied",
CANCELLATION_SCHEDULED: "cancellation.scheduled",
SERVICE_CANCELLED: "service.cancelled",
} as const;
export const SIM_MANAGEMENT_ACTION = {
TOP_UP_DATA: "topUpData",
CHANGE_PLAN: "changePlan",
UPDATE_FEATURES: "updateFeatures",
CANCEL_SIM: "cancelSim",
REISSUE_ESIM: "reissueEsim",
} as const;
```
- `SimLifecycleStage` — union of the keys above.
- `SimManagementAction` — canonical action names referenced by BFF services, workers, and docs.
- `SIM_ORDER_FLOW` — ordered list of activation transitions.
- `SIM_MANAGEMENT_FLOW` — per-action transitions that describe how a request moves between states.
## Order + Activation Flow (`SIM_ORDER_FLOW`)
| Step | Transition | Details |
|------|------------|---------|
| 1 | `checkout.createOrder`: `checkout → order.pendingReview` | `orderWithSkuValidationSchema` ensures the cart is valid before the BFF writes a Salesforce order. |
| 2 | `orders.activateSim`: `order.pendingReview → activation.processing` | `SimOrderActivationService` locks the request (cache key) and kicks off billing. |
| 3a | `payments.failure`: `activation.processing → activation.failedPayment` | WHMCS capture failed. Invoice is cancelled via `SimBillingService`, user must retry. |
| 3b | `payments.success`: `activation.processing → activation.provisioning` | Payment succeeded; Freebit provisioning and add-on updates run. |
| 4 | `provisioning.complete`: `activation.provisioning → service.active` | Freebit returns success, cache records the idempotent result, and the monthly WHMCS subscription is scheduled for the first of next month via `SimScheduleService`. |
## SIM Management Actions (`SIM_MANAGEMENT_FLOW`)
All customer-facing actions execute through `SimActionRunnerService`, guaranteeing consistent
notifications and structured logs. The table below maps directly to the typed transitions.
| Action (`SimManagementAction`) | Transition(s) | Notes |
|--------------------------------|---------------|-------|
| `topUpData` | `service.active → service.active` | One-time invoice captured through `SimBillingService`; Freebit quota increases immediately. |
| `changePlan` | `service.active → planChange.scheduled → planChange.applied → service.active` | `SimScheduleService.resolveScheduledDate` auto-picks the first day of next month unless the user provides `scheduledAt`. |
| `updateFeatures` | `service.active → service.active` | Voice toggles run instantly. If `networkType` changes, the second phase is queued (see below). |
| `cancelSim` | `service.active → cancellation.scheduled → service.cancelled` | Default schedule = first day of next month; users can override by passing a valid `YYYYMMDD` date. |
| `reissueEsim` | `service.active → service.active` | Freebit eSIM profile reissued; lifecycle stage does not change. |
## Deferred Jobs
Network-type changes are persisted as BullMQ jobs so they survive restarts and include retry
telemetry.
- Enqueue: `SimPlanService``SimManagementQueueService.scheduleNetworkTypeChange`
- Worker: `SimManagementProcessor` consumes the `sim-management` queue and calls
`FreebitOrchestratorService.updateSimFeatures` when the delay expires.
## Validation & Feedback Layers
| Layer | Enforcement | Customer feedback |
|-------|-------------|-------------------|
| Frontend | Domain schemas (`orderWithSkuValidationSchema`, SIM configure schemas) | Immediate form errors (e.g., missing activation fee, invalid EID). |
| BFF | `SimOrderActivationService`, `SimPlanService`, `SimTopUpService` | Sanitized business errors (`VAL_001`, payment failures) routed through Secure Error Mapper. |
| Background jobs | `SimManagementProcessor` | Logged with request metadata; failures fan out via `SimNotificationService`. |
Keep this file and `packages/domain/sim/lifecycle.ts` in lockstep. When adding a new stage or action,
update the domain types first, then describe the change here so every team shares the same model.

View File

@ -1,141 +0,0 @@
# Unified Product Types - Solution to Type Duplication
## Problem Statement
Previously, we had multiple type definitions trying to represent the same underlying Salesforce `Product2` data:
- `CatalogItem` - for catalog display
- `CatalogOrderItem` - for order items in checkout
- `InternetPlan`, `SimPlan`, `VpnPlan` - for specific product types
- Various interfaces in Salesforce order types
This led to:
- **Type duplication** across the codebase
- **Inconsistent field names** between contexts
- **Maintenance overhead** when Salesforce fields change
- **Confusion** about which type to use when
## Solution: Unified Product Types
### Core Principle
**One Salesforce Product2 = One TypeScript Type**
Since order items get their data from Salesforce `Product2` objects (via `PricebookEntry.Product2`), and catalog items are also representing the same `Product2` objects, we should have unified types that directly map to the Salesforce structure.
### New Type Hierarchy
```typescript
// Base product interface mapping directly to Salesforce Product2
interface BaseProduct {
// Standard Salesforce fields
id: string; // Product2.Id
name: string; // Product2.Name
sku: string; // Product2.StockKeepingUnit
// Custom fields for portal
category: ProductCategory; // Product2Categories1__c
itemClass: ItemClass; // Item_Class__c
billingCycle: BillingCycle; // Billing_Cycle__c
portalCatalog: boolean; // Portal_Catalog__c
portalAccessible: boolean; // Portal_Accessible__c
// WHMCS integration
whmcsProductId: number; // WH_Product_ID__c
whmcsProductName: string; // WH_Product_Name__c
// Pricing from PricebookEntry
monthlyPrice?: number;
oneTimePrice?: number;
}
// Specialized types for each product category
interface InternetProduct extends BaseProduct {
category: "Internet";
internetPlanTier?: "Silver" | "Gold" | "Platinum";
internetOfferingType?: string;
// ... other Internet-specific fields
}
interface SimProduct extends BaseProduct {
category: "SIM";
simDataSize?: string;
simPlanType?: "DataOnly" | "DataSmsVoice" | "VoiceOnly";
simHasFamilyDiscount?: boolean;
// ... other SIM-specific fields
}
// Union type for all products
type Product = InternetProduct | SimProduct | VpnProduct | BaseProduct;
// Order item with quantity
interface ProductOrderItem extends BaseProduct {
quantity: number;
unitPrice: number;
totalPrice: number;
autoAdded?: boolean;
}
```
### Benefits
1. **Single Source of Truth**: All types map directly to Salesforce Product2 structure
2. **No Duplication**: Same type used in catalog, orders, and business logic
3. **Type Safety**: Proper TypeScript discrimination based on `category` field
4. **Maintainability**: Changes to Salesforce fields only need updates in one place
5. **Consistency**: Same field names and structure across all contexts
### Usage Examples
```typescript
// Transform Salesforce data to unified type
const product = fromSalesforceProduct2(salesforceProduct, pricebookEntry);
// Use in catalog context
if (isCatalogVisible(product)) {
displayInCatalog(product);
}
// Use in order context
const orderItem: ProductOrderItem = {
...product,
quantity: 1,
unitPrice: product.monthlyPrice || 0,
totalPrice: product.monthlyPrice || 0,
};
// Type-safe access to specific fields
if (isInternetProduct(product)) {
console.log(product.internetPlanTier); // TypeScript knows this exists
}
```
### Migration Strategy
1. **Backward Compatibility**: Legacy type aliases maintained
```typescript
export type InternetPlan = InternetProduct; // For existing code
export type CatalogItem = Product; // Unified representation
```
2. **Gradual Migration**: Existing code continues to work while new code uses unified types
3. **Field Mapping**: The `fromSalesforceProduct2()` function handles transformation from Salesforce API responses
### Implementation Files
- `packages/domain/src/entities/product.ts` - Core unified types
- `packages/domain/src/entities/catalog.ts` - Re-exports with legacy aliases
- `packages/domain/src/entities/order.ts` - Updated order types
- `packages/domain/src/entities/examples/unified-product-usage.ts` - Usage examples
### Key Functions
- `fromSalesforceProduct2()` - Transform Salesforce data to unified types
- `isInternetProduct()`, `isSimProduct()`, `isVpnProduct()` - Type guards
- `isCatalogVisible()`, `isOrderable()` - Business logic helpers
- `isServiceProduct()`, `isAddonProduct()` - Classification helpers
This unified approach eliminates the confusion about which type to use and ensures consistency across the entire application while maintaining direct mapping to the underlying Salesforce data structure.