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:
parent
3af18af502
commit
7c929eb4dc
557
docs/README.md
557
docs/README.md
@ -1,422 +1,199 @@
|
||||
# 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
|
||||
- [Catalog & Checkout](./portal-guides/catalog-and-checkout.md) — product source, eligibility, and checkout rules
|
||||
- [Orders & Provisioning](./portal-guides/orders-and-provisioning.md) — Salesforce orders and WHMCS fulfillment flow
|
||||
- [Billing & Payments](./portal-guides/billing-and-payments.md) — invoices, SSO pay links, and payment methods
|
||||
- [Subscriptions](./portal-guides/subscriptions.md) — how active services are read and refreshed
|
||||
- [Support Cases](./portal-guides/support-cases.md) — Salesforce case creation and visibility
|
||||
- [Complete Guide](./portal-guides/COMPLETE-GUIDE.md) — single, end-to-end explanation of how the portal works
|
||||
```
|
||||
docs/
|
||||
├── getting-started/ # Setup and running the project
|
||||
├── architecture/ # System design documents
|
||||
├── how-it-works/ # Feature guides (how the portal works)
|
||||
├── integrations/ # Salesforce, WHMCS, SIM integration docs
|
||||
├── development/ # BFF, Portal, Domain, Auth code docs
|
||||
├── 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**
|
||||
|
||||
- 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.
|
||||
See also: [Project Structure](./STRUCTURE.md)
|
||||
|
||||
---
|
||||
|
||||
### [Integration & Data Flow](./architecture/INTEGRATION-DATAFLOW.md)
|
||||
## 🏗️ Architecture
|
||||
|
||||
**External system integration patterns and data transformation**
|
||||
Core system design documents:
|
||||
|
||||
- Integration architecture overview
|
||||
- Salesforce integration (REST API + Platform Events via gRPC Pub/Sub)
|
||||
- WHMCS integration (REST API + Webhooks)
|
||||
- Freebit SIM management integration
|
||||
- Domain mapper pattern (Map Once, Use Everywhere)
|
||||
- Data transformation flows
|
||||
- Error handling and retry strategies
|
||||
- Caching strategies (CDC-driven + TTL-based)
|
||||
|
||||
**Read this** to understand how external systems are integrated.
|
||||
| Document | Description |
|
||||
| -------------------------------------------------------------- | ------------------------- |
|
||||
| [System Overview](./architecture/system-overview.md) | High-level architecture |
|
||||
| [Monorepo Structure](./architecture/monorepo.md) | Monorepo organization |
|
||||
| [Product Catalog](./architecture/product-catalog.md) | Catalog design |
|
||||
| [Modular Provisioning](./architecture/modular-provisioning.md) | Provisioning architecture |
|
||||
| [Domain Layer](./architecture/domain-layer.md) | Domain-driven design |
|
||||
| [Orders Architecture](./architecture/orders.md) | Order system design |
|
||||
|
||||
---
|
||||
|
||||
### [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
|
||||
- Provider pattern for multi-system abstraction
|
||||
- Type system architecture (unified domain package)
|
||||
- Schema-driven validation with Zod
|
||||
- Adding new domains step-by-step
|
||||
- Import patterns and best practices
|
||||
|
||||
**Read this** to understand the domain layer and type system.
|
||||
| Guide | Description |
|
||||
| ------------------------------------------------------------------ | --------------------------- |
|
||||
| [System Overview](./how-it-works/system-overview.md) | Systems and data ownership |
|
||||
| [Accounts & Identity](./how-it-works/accounts-and-identity.md) | Sign-up and WHMCS linking |
|
||||
| [Catalog & Checkout](./how-it-works/catalog-and-checkout.md) | Products and checkout rules |
|
||||
| [Orders & Provisioning](./how-it-works/orders-and-provisioning.md) | Order fulfillment flow |
|
||||
| [Billing & Payments](./how-it-works/billing-and-payments.md) | Invoices and payments |
|
||||
| [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)
|
||||
|
||||
**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
|
||||
## 🔌 Integrations
|
||||
|
||||
### Salesforce
|
||||
|
||||
- salesforce/SALESFORCE-ORDER-COMMUNICATION.md
|
||||
- salesforce/SALESFORCE-PORTAL-SECURITY-GUIDE.md
|
||||
- salesforce/SALESFORCE-PORTAL-SIMPLE-GUIDE.md
|
||||
- salesforce/SALESFORCE-PRODUCTS.md
|
||||
- salesforce/SALESFORCE-WHMCS-MAPPING-REFERENCE.md
|
||||
- salesforce/WHMCS_BILLING_ISSUES_RESOLUTION.md
|
||||
| Document | Description |
|
||||
| --------------------------------------------------------------------------- | ----------------------------- |
|
||||
| [Requirements](./integrations/salesforce/requirements.md) | Objects, fields, flows, setup |
|
||||
| [Overview](./integrations/salesforce/overview.md) | Getting started |
|
||||
| [Orders](./integrations/salesforce/orders.md) | Order communication |
|
||||
| [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
|
||||
- types/UNIFIED-PRODUCT-TYPES.md
|
||||
| Document | Description |
|
||||
| ---------------------------------------------------------- | ------------------------- |
|
||||
| [Troubleshooting](./integrations/whmcs/troubleshooting.md) | Billing issues resolution |
|
||||
|
||||
### Validation
|
||||
### SIM Management
|
||||
|
||||
- validation/SIGNUP_VALIDATION_RULES.md
|
||||
- validation/VALIDATION_CLEANUP_SUMMARY.md
|
||||
- validation/VALIDATION_PATTERNS.md
|
||||
- validation/bff-validation-migration.md
|
||||
| Document | Description |
|
||||
| ---------------------------------------------------- | ---------------------- |
|
||||
| [Freebit Integration](./integrations/sim/freebit.md) | Freebit SIM management |
|
||||
| [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
|
||||
|
||||
@ -172,26 +172,31 @@ External API Request
|
||||
## 🎨 Design Principles
|
||||
|
||||
### 1. **Co-location**
|
||||
|
||||
- Domain contracts, schemas, and provider logic live together
|
||||
- Easy to find related code
|
||||
- Clear ownership and responsibility
|
||||
|
||||
### 2. **Provider Isolation**
|
||||
|
||||
- Raw types and mappers nested in `providers/` subfolder
|
||||
- Each provider is self-contained
|
||||
- Easy to add/remove providers
|
||||
|
||||
### 3. **Type Safety**
|
||||
|
||||
- Zod schemas for runtime validation
|
||||
- TypeScript types inferred from schemas
|
||||
- Branded types for stronger type checking
|
||||
|
||||
### 4. **Clean Exports**
|
||||
|
||||
- Barrel exports (`index.ts`) control public API
|
||||
- Provider mappers exported as namespaces (`WhmcsBillingMapper.*`)
|
||||
- Predictable import paths
|
||||
|
||||
### 5. **Minimal Dependencies**
|
||||
|
||||
- Only depends on `zod` for runtime validation
|
||||
- No circular dependencies
|
||||
- Self-contained domain logic
|
||||
@ -199,40 +204,48 @@ External API Request
|
||||
## 📋 Domain Reference
|
||||
|
||||
### Billing
|
||||
|
||||
- **Contracts**: `Invoice`, `InvoiceItem`, `InvoiceList`
|
||||
- **Providers**: WHMCS
|
||||
- **Use Cases**: Display invoices, payment history, invoice details
|
||||
|
||||
### Subscriptions
|
||||
|
||||
- **Contracts**: `Subscription`, `SubscriptionList`
|
||||
- **Providers**: WHMCS
|
||||
- **Use Cases**: Display active services, manage subscriptions
|
||||
|
||||
### Payments
|
||||
|
||||
- **Contracts**: `PaymentMethod`, `PaymentGateway`
|
||||
- **Providers**: WHMCS
|
||||
- **Use Cases**: Payment method management, gateway configuration
|
||||
|
||||
### SIM
|
||||
|
||||
- **Contracts**: `SimDetails`, `SimUsage`, `SimTopUpHistory`
|
||||
- **Providers**: Freebit
|
||||
- **Use Cases**: SIM management, usage tracking, top-up history
|
||||
|
||||
### Orders
|
||||
|
||||
- **Contracts**: `OrderSummary`, `OrderDetails`, `FulfillmentOrderDetails`
|
||||
- **Providers**: WHMCS (provisioning), Salesforce (order management)
|
||||
- **Use Cases**: Order fulfillment, order history, order details
|
||||
|
||||
### Catalog
|
||||
|
||||
- **Contracts**: `InternetPlanCatalogItem`, `SimCatalogProduct`, `VpnCatalogProduct`
|
||||
- **Providers**: Salesforce (Product2)
|
||||
- **Use Cases**: Product catalog display, product selection
|
||||
|
||||
### Common
|
||||
|
||||
- **Types**: `IsoDateTimeString`, `UserId`, `AccountId`, `OrderId`, `ApiResponse`, `PaginatedResponse`
|
||||
- **Use Cases**: Shared utility types across all domains
|
||||
|
||||
### Toolkit
|
||||
|
||||
- **Formatting**: Currency, date, phone, text formatters
|
||||
- **Validation**: Email, URL, string validators
|
||||
- **Typing**: Type guards, assertions, helpers
|
||||
@ -243,6 +256,7 @@ External API Request
|
||||
### From Old Structure
|
||||
|
||||
**Before:**
|
||||
|
||||
```typescript
|
||||
import type { Invoice } from "@customer-portal/contracts/billing";
|
||||
import { invoiceSchema } from "@customer-portal/schemas/business/billing.schema";
|
||||
@ -250,6 +264,7 @@ import { transformWhmcsInvoice } from "@customer-portal/integrations-whmcs/mappe
|
||||
```
|
||||
|
||||
**After:**
|
||||
|
||||
```typescript
|
||||
import type { Invoice } from "@customer-portal/domain/billing";
|
||||
import { invoiceSchema } from "@customer-portal/domain/billing";
|
||||
@ -259,6 +274,7 @@ const invoice = WhmcsBillingMapper.transformWhmcsInvoice(data);
|
||||
```
|
||||
|
||||
### Benefits
|
||||
|
||||
- **Fewer imports**: Everything in one package
|
||||
- **Clearer intent**: Mapper namespace indicates provider
|
||||
- **Better DX**: Autocomplete shows all related exports
|
||||
@ -288,4 +304,3 @@ const invoice = WhmcsBillingMapper.transformWhmcsInvoice(data);
|
||||
- [Provider-Aware Structure](./DOMAIN-STRUCTURE.md)
|
||||
- [Type Cleanup Summary](./TYPE-CLEANUP-SUMMARY.md)
|
||||
- [Architecture Overview](./ARCHITECTURE.md)
|
||||
|
||||
@ -47,35 +47,40 @@ After comprehensive review and refactoring across all BFF integrations, the arch
|
||||
### Additional Improvements Beyond Orders
|
||||
|
||||
**6. ✅ Centralized DB Mappers**
|
||||
- **Created**: `apps/bff/src/infra/mappers/`
|
||||
- All Prisma → Domain mappings centralized
|
||||
- Clear naming: `mapPrismaUserToDomain()`, `mapPrismaMappingToDomain()`
|
||||
- **Documentation**: [DB-MAPPERS.md](./apps/bff/docs/DB-MAPPERS.md)
|
||||
|
||||
- **Created**: `apps/bff/src/infra/mappers/`
|
||||
- All Prisma → Domain mappings centralized
|
||||
- Clear naming: `mapPrismaUserToDomain()`, `mapPrismaMappingToDomain()`
|
||||
- **Documentation**: [DB-MAPPERS.md](./apps/bff/docs/DB-MAPPERS.md)
|
||||
|
||||
**7. ✅ Freebit Integration Cleaned**
|
||||
- **Deleted**: `FreebitMapperService` (redundant wrapper)
|
||||
- **Moved**: Provider utilities to domain (`Freebit.normalizeAccount()`, etc.)
|
||||
- Now uses domain mappers directly: `Freebit.transformFreebitAccountDetails()`
|
||||
|
||||
- **Deleted**: `FreebitMapperService` (redundant wrapper)
|
||||
- **Moved**: Provider utilities to domain (`Freebit.normalizeAccount()`, etc.)
|
||||
- Now uses domain mappers directly: `Freebit.transformFreebitAccountDetails()`
|
||||
|
||||
**8. ✅ WHMCS Transformer Services Removed**
|
||||
- **Deleted**: Entire `apps/bff/src/integrations/whmcs/transformers/` directory (6 files)
|
||||
- **Removed Services**:
|
||||
- `InvoiceTransformerService`
|
||||
- `SubscriptionTransformerService`
|
||||
- `PaymentTransformerService`
|
||||
- `WhmcsTransformerOrchestratorService`
|
||||
- **Why**: Thin wrappers that only injected currency then called domain mappers
|
||||
- **Now**: Integration services use domain mappers directly with currency context
|
||||
|
||||
- **Deleted**: Entire `apps/bff/src/integrations/whmcs/transformers/` directory (6 files)
|
||||
- **Removed Services**:
|
||||
- `InvoiceTransformerService`
|
||||
- `SubscriptionTransformerService`
|
||||
- `PaymentTransformerService`
|
||||
- `WhmcsTransformerOrchestratorService`
|
||||
- **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**
|
||||
- ✅ Salesforce: Uses domain mappers directly
|
||||
- ✅ WHMCS: Uses domain mappers directly
|
||||
- ✅ Freebit: Uses domain mappers directly
|
||||
- ✅ Catalog: Uses domain mappers directly
|
||||
|
||||
- ✅ Salesforce: Uses domain mappers directly
|
||||
- ✅ WHMCS: Uses domain mappers directly
|
||||
- ✅ Freebit: Uses domain mappers directly
|
||||
- ✅ Catalog: Uses domain mappers directly
|
||||
|
||||
### Files Changed Summary
|
||||
|
||||
**Created (5 files)**:
|
||||
|
||||
- `apps/bff/src/infra/mappers/user.mapper.ts`
|
||||
- `apps/bff/src/infra/mappers/mapping.mapper.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`
|
||||
|
||||
**Deleted (8 files)**:
|
||||
|
||||
- `apps/bff/src/integrations/freebit/services/freebit-mapper.service.ts`
|
||||
- `apps/bff/src/integrations/whmcs/transformers/` (6 files in directory)
|
||||
- `apps/bff/src/infra/utils/user-mapper.util.ts`
|
||||
|
||||
**Modified (17+ files)**:
|
||||
|
||||
- All WHMCS services (invoice, subscription, payment)
|
||||
- All Freebit services (operations, orchestrator)
|
||||
- MappingsService + all auth services
|
||||
- Module definitions (whmcs.module.ts, freebit.module.ts)
|
||||
- Domain exports (sim/providers/freebit/index.ts)
|
||||
|
||||
|
||||
|
||||
## 🎯 Current Architecture
|
||||
|
||||
### Clean Separation of Concerns
|
||||
@ -134,6 +139,7 @@ Flow: Query (BFF) → Raw Data → Domain Mapper → Domain Type → Use Directl
|
||||
### Domain Layer (`packages/domain/orders/`)
|
||||
|
||||
**Contains**:
|
||||
|
||||
- ✅ Business types (OrderDetails, OrderSummary)
|
||||
- ✅ Raw provider types (SalesforceOrderRecord)
|
||||
- ✅ Validation schemas (Zod)
|
||||
@ -141,6 +147,7 @@ Flow: Query (BFF) → Raw Data → Domain Mapper → Domain Type → Use Directl
|
||||
- ✅ Business validation functions
|
||||
|
||||
**Does NOT Contain**:
|
||||
|
||||
- ❌ Query builders (moved to BFF)
|
||||
- ❌ Field configuration
|
||||
- ❌ 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/`)
|
||||
|
||||
**Contains**:
|
||||
|
||||
- ✅ `SalesforceOrderService` - Encapsulates order operations
|
||||
- ✅ Query builders (`order-query-builder.ts`)
|
||||
- ✅ Connection services
|
||||
- ✅ Uses domain mappers for transformation
|
||||
|
||||
**Does NOT Contain**:
|
||||
|
||||
- ❌ Additional mapping logic (uses domain mappers)
|
||||
- ❌ Business validation
|
||||
|
||||
### Application Layer (`apps/bff/src/modules/orders/`)
|
||||
|
||||
**Contains**:
|
||||
|
||||
- ✅ `OrderOrchestrator` - Workflow coordination
|
||||
- ✅ `OrderFulfillmentOrchestrator` - Fulfillment workflows
|
||||
- ✅ Controllers (HTTP endpoints)
|
||||
@ -167,6 +177,7 @@ Flow: Query (BFF) → Raw Data → Domain Mapper → Domain Type → Use Directl
|
||||
- ✅ Uses domain mappers directly
|
||||
|
||||
**Does NOT Contain**:
|
||||
|
||||
- ❌ Direct Salesforce queries
|
||||
- ❌ Mapper service wrappers (deleted)
|
||||
- ❌ Double transformations
|
||||
@ -204,18 +215,21 @@ const whmcsItems = Providers.Whmcs.mapFulfillmentOrderItems(items);
|
||||
## ✅ Benefits Achieved
|
||||
|
||||
### Architecture Cleanliness
|
||||
|
||||
- ✅ Single source of truth for transformations (domain mappers)
|
||||
- ✅ Clear separation: domain = business, BFF = infrastructure
|
||||
- ✅ No redundant mapping layers
|
||||
- ✅ Query logic in correct layer (BFF integration)
|
||||
|
||||
### Code Quality
|
||||
|
||||
- ✅ Easier to test (clear boundaries)
|
||||
- ✅ Easier to maintain (no duplication)
|
||||
- ✅ Easier to understand (one transformation path)
|
||||
- ✅ Easier to swap providers (integration services encapsulate)
|
||||
|
||||
### Developer Experience
|
||||
|
||||
- ✅ Clear patterns to follow
|
||||
- ✅ No confusion about where code goes
|
||||
- ✅ Consistent with catalog services
|
||||
@ -226,11 +240,13 @@ const whmcsItems = Providers.Whmcs.mapFulfillmentOrderItems(items);
|
||||
## 📁 File Structure
|
||||
|
||||
### Created Files
|
||||
|
||||
1. ✅ `apps/bff/src/integrations/salesforce/services/salesforce-order.service.ts`
|
||||
2. ✅ `apps/bff/src/integrations/salesforce/utils/order-query-builder.ts`
|
||||
3. ✅ `docs/BFF-INTEGRATION-PATTERNS.md`
|
||||
|
||||
### Modified Files
|
||||
|
||||
1. ✅ `apps/bff/src/modules/orders/services/order-orchestrator.service.ts`
|
||||
2. ✅ `apps/bff/src/modules/orders/services/order-fulfillment-orchestrator.service.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`
|
||||
|
||||
### Deleted Files
|
||||
|
||||
1. ✅ `apps/bff/src/modules/orders/services/order-whmcs-mapper.service.ts`
|
||||
2. ✅ `packages/domain/orders/providers/salesforce/query.ts`
|
||||
|
||||
@ -249,6 +266,7 @@ const whmcsItems = Providers.Whmcs.mapFulfillmentOrderItems(items);
|
||||
### Single Transformation Principle
|
||||
|
||||
**One transformation path** - raw data flows through domain mapper exactly once:
|
||||
|
||||
```
|
||||
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
|
||||
|
||||
Integration services hide external system complexity:
|
||||
|
||||
```typescript
|
||||
// Application layer doesn't see Salesforce details
|
||||
const order = await this.salesforceOrderService.getOrderById(id);
|
||||
@ -264,6 +283,7 @@ const order = await this.salesforceOrderService.getOrderById(id);
|
||||
### Separation of Concerns
|
||||
|
||||
Each layer has a single responsibility:
|
||||
|
||||
- **Domain**: Business logic, types, validation
|
||||
- **Integration**: External system interaction
|
||||
- **Application**: Workflow coordination
|
||||
@ -369,6 +389,7 @@ The refactoring is complete. We now have:
|
||||
All domain and BFF layers now follow the "Map Once, Use Everywhere" principle.
|
||||
|
||||
The architecture now follows clean separation of concerns:
|
||||
|
||||
- **Domain**: Pure business logic (no infrastructure)
|
||||
- **Integration**: External system encapsulation (SF, WHMCS)
|
||||
- **Application**: Workflow coordination (orchestrators)
|
||||
@ -379,4 +400,3 @@ The architecture now follows clean separation of concerns:
|
||||
|
||||
**Last Updated**: October 2025
|
||||
**Refactored By**: Architecture Review Team
|
||||
|
||||
@ -19,16 +19,19 @@ packages/
|
||||
## 🎯 **Architecture Principles**
|
||||
|
||||
### **1. Separation of Concerns**
|
||||
|
||||
- **Dev vs Prod**: Clear separation with appropriate tooling
|
||||
- **Services vs Apps**: Development runs apps locally, production containerizes everything
|
||||
- **Configuration vs Code**: Environment variables for configuration, code for logic
|
||||
|
||||
### **2. Single Source of Truth**
|
||||
|
||||
- **One environment template**: `.env.example`
|
||||
- **One Docker Compose** per environment
|
||||
- **One script** per operation type
|
||||
|
||||
### **3. Clean Dependencies**
|
||||
|
||||
- **Portal**: Uses `@/lib/*` for shared utilities and services
|
||||
- **BFF**: Feature-aligned modules with shared concerns in `src/common/`
|
||||
- **Domain**: Framework-agnostic types and utilities
|
||||
@ -53,6 +56,7 @@ src/
|
||||
```
|
||||
|
||||
### **Conventions**
|
||||
|
||||
- Use `@/lib/*` for shared frontend utilities and services
|
||||
- Feature modules own their `components/`, `hooks/`, `services/`, and `types/`
|
||||
- Cross-feature UI belongs in `components/` (atomic design)
|
||||
@ -77,6 +81,7 @@ src/
|
||||
```
|
||||
|
||||
### **Conventions**
|
||||
|
||||
- Prefer `modules/*` over flat directories per domain
|
||||
- Keep DTOs and validators in-module
|
||||
- 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/`)**
|
||||
|
||||
- **Purpose**: Pure TypeScript interface definitions - single source of truth
|
||||
- **Contents**: Cross-layer contracts for billing, subscriptions, payments, SIM, orders
|
||||
- **Exports**: Organized by domain (e.g., `@customer-portal/contracts/billing`)
|
||||
- **Rule**: ZERO runtime dependencies, only pure types
|
||||
|
||||
#### **2. Schemas Package (`packages/schemas/`)**
|
||||
|
||||
- **Purpose**: Runtime validation schemas using Zod
|
||||
- **Contents**: Matching Zod validators for each contract + integration-specific payload schemas
|
||||
- **Exports**: Organized by domain and integration provider
|
||||
- **Usage**: Validate external API responses, request payloads, and user input
|
||||
|
||||
#### **3. Integration Packages (`packages/integrations/`)**
|
||||
|
||||
- **Purpose**: Transform raw provider data into shared contracts
|
||||
- **Structure**:
|
||||
- `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
|
||||
|
||||
#### **4. Application Layers**
|
||||
|
||||
- **BFF** (`apps/bff/`): Import from contracts/schemas, never define duplicate interfaces
|
||||
- **Portal** (`apps/portal/`): Import from contracts/schemas, use shared types everywhere
|
||||
- **Rule**: Applications only consume, never define domain types
|
||||
|
||||
### **Legacy: Domain Package (Deprecated)**
|
||||
|
||||
- **Status**: Being phased out in favor of contracts + schemas
|
||||
- **Migration**: Re-exports now point to contracts package for backward compatibility
|
||||
- **Rule**: New code should import from `@customer-portal/contracts` or `@customer-portal/schemas`
|
||||
|
||||
### **Logging Package**
|
||||
|
||||
- **Purpose**: Centralized structured logging
|
||||
- **Features**: Pino-based logging with correlation IDs
|
||||
- **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**
|
||||
|
||||
### **API Client**
|
||||
|
||||
- **Implementation**: Fetch wrapper using shared Zod schemas from `@customer-portal/domain`
|
||||
- **Features**: CSRF protection, auth handling, consistent `ApiResponse` helpers
|
||||
- **Location**: `apps/portal/src/lib/api/`
|
||||
|
||||
### **External Services**
|
||||
|
||||
- **WHMCS**: Billing system integration
|
||||
- **Salesforce**: CRM and order management
|
||||
- **Redis**: Caching and session storage
|
||||
@ -149,16 +162,19 @@ The codebase follows a strict layering pattern to ensure single source of truth
|
||||
## 🔒 **Security Architecture**
|
||||
|
||||
### **Authentication Flow**
|
||||
|
||||
- Portal-native authentication with JWT tokens
|
||||
- Optional MFA support
|
||||
- Secure token rotation with Redis backing
|
||||
|
||||
### **Error Handling**
|
||||
|
||||
- Never leak sensitive details to end users [[memory:6689308]]
|
||||
- Centralized error mapping to user-friendly messages
|
||||
- Comprehensive audit trails
|
||||
|
||||
### **Data Protection**
|
||||
|
||||
- PII minimization with encryption at rest/in transit
|
||||
- Row-level security (users can only access their data)
|
||||
- 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**
|
||||
|
||||
### **Path Aliases**
|
||||
|
||||
- **Portal**: `@/*`, `@/lib/*`, `@/features/*`, `@/components/*`
|
||||
- **BFF**: `@/*` mapped to `apps/bff/src`
|
||||
- **Domain**: Import via `@customer-portal/domain`
|
||||
|
||||
### **Code Quality**
|
||||
|
||||
- Strict TypeScript rules enforced repository-wide
|
||||
- ESLint and Prettier for consistent formatting
|
||||
- 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**
|
||||
|
||||
### **Caching Strategy**
|
||||
|
||||
- **Invoices**: 60-120s per page; bust on WHMCS webhook
|
||||
- **Cases**: 30-60s; bust after create/update
|
||||
- **Catalog**: 5-15m; manual bust on changes
|
||||
- **Keys include user_id** to prevent cross-user leakage
|
||||
|
||||
### **Database Optimization**
|
||||
|
||||
- Connection pooling with Prisma
|
||||
- Proper indexing on frequently queried fields
|
||||
- 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._
|
||||
@ -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
|
||||
|
||||
@ -6,16 +6,16 @@ The auth module now exposes a clean facade (`AuthFacade`) and a layered structur
|
||||
|
||||
## File Map
|
||||
|
||||
| Purpose | Location |
|
||||
| ---------------------- | ------------------------------------------------------ |
|
||||
| Express cookie helper | `apps/bff/src/app/bootstrap.ts` |
|
||||
| Auth controller | `modules/auth/presentation/http/auth.controller.ts` |
|
||||
| Guards/interceptors | `modules/auth/presentation/http/guards|interceptors` |
|
||||
| Passport strategies | `modules/auth/presentation/strategies` |
|
||||
| Facade (use-cases) | `modules/auth/application/auth.facade.ts` |
|
||||
| Token services | `modules/auth/infra/token` |
|
||||
| Rate limiter service | `modules/auth/infra/rate-limiting/auth-rate-limit.service.ts` |
|
||||
| Signup/password flows | `modules/auth/infra/workflows` |
|
||||
| Purpose | Location |
|
||||
| --------------------- | ------------------------------------------------------------- | ------------- |
|
||||
| Express cookie helper | `apps/bff/src/app/bootstrap.ts` |
|
||||
| Auth controller | `modules/auth/presentation/http/auth.controller.ts` |
|
||||
| Guards/interceptors | `modules/auth/presentation/http/guards | interceptors` |
|
||||
| Passport strategies | `modules/auth/presentation/strategies` |
|
||||
| Facade (use-cases) | `modules/auth/application/auth.facade.ts` |
|
||||
| Token services | `modules/auth/infra/token` |
|
||||
| Rate limiter service | `modules/auth/infra/rate-limiting/auth-rate-limit.service.ts` |
|
||||
| Signup/password flows | `modules/auth/infra/workflows` |
|
||||
|
||||
## Development Environment Flags
|
||||
|
||||
@ -18,6 +18,7 @@ Database mappers in the BFF layer are responsible for transforming **Prisma enti
|
||||
## Location
|
||||
|
||||
All DB mappers are centralized in:
|
||||
|
||||
```
|
||||
apps/bff/src/infra/mappers/
|
||||
```
|
||||
@ -25,11 +26,13 @@ apps/bff/src/infra/mappers/
|
||||
## Naming Convention
|
||||
|
||||
All DB mapper functions follow this pattern:
|
||||
|
||||
```typescript
|
||||
mapPrisma{EntityName}ToDomain(entity: Prisma{Entity}): Domain{Type}
|
||||
```
|
||||
|
||||
### Examples:
|
||||
|
||||
- `mapPrismaUserToDomain()` - User entity → AuthenticatedUser
|
||||
- `mapPrismaMappingToDomain()` - IdMapping entity → UserIdMapping
|
||||
- `mapPrismaOrderToDomain()` - Order entity → OrderDetails (if we had one)
|
||||
@ -124,7 +127,7 @@ import { mapPrismaMappingToDomain } from "@bff/infra/mappers";
|
||||
export class MappingsService {
|
||||
async findByUserId(userId: string): Promise<UserIdMapping | null> {
|
||||
const dbMapping = await this.prisma.idMapping.findUnique({
|
||||
where: { userId }
|
||||
where: { userId },
|
||||
});
|
||||
|
||||
if (!dbMapping) return null;
|
||||
@ -174,14 +177,14 @@ Providers.Salesforce.transformOrder(
|
||||
|
||||
## Key Differences
|
||||
|
||||
| Aspect | DB Mappers | Provider Mappers |
|
||||
|--------|-----------|------------------|
|
||||
| **Location** | `apps/bff/src/infra/mappers/` | `packages/domain/{domain}/providers/` |
|
||||
| **Input** | Prisma entity | External API response |
|
||||
| **Layer** | BFF Infrastructure | Domain |
|
||||
| **Purpose** | Hide ORM implementation | Transform business data |
|
||||
| **Import in Domain** | ❌ Never | ✅ Internal use only |
|
||||
| **Import in BFF** | ✅ Yes | ✅ Yes |
|
||||
| Aspect | DB Mappers | Provider Mappers |
|
||||
| -------------------- | ----------------------------- | ------------------------------------- |
|
||||
| **Location** | `apps/bff/src/infra/mappers/` | `packages/domain/{domain}/providers/` |
|
||||
| **Input** | Prisma entity | External API response |
|
||||
| **Layer** | BFF Infrastructure | Domain |
|
||||
| **Purpose** | Hide ORM implementation | Transform business data |
|
||||
| **Import in Domain** | ❌ Never | ✅ Internal use only |
|
||||
| **Import in BFF** | ✅ Yes | ✅ Yes |
|
||||
|
||||
## Best Practices
|
||||
|
||||
@ -207,25 +210,25 @@ Providers.Salesforce.transformOrder(
|
||||
DB mappers are pure functions and easy to test:
|
||||
|
||||
```typescript
|
||||
describe('mapPrismaUserToDomain', () => {
|
||||
it('should map Prisma user to domain user', () => {
|
||||
describe("mapPrismaUserToDomain", () => {
|
||||
it("should map Prisma user to domain user", () => {
|
||||
const prismaUser: PrismaUser = {
|
||||
id: 'user-123',
|
||||
email: 'test@example.com',
|
||||
role: 'USER',
|
||||
mfaSecret: 'secret',
|
||||
id: "user-123",
|
||||
email: "test@example.com",
|
||||
role: "USER",
|
||||
mfaSecret: "secret",
|
||||
emailVerified: true,
|
||||
lastLoginAt: new Date('2025-01-01'),
|
||||
createdAt: new Date('2024-01-01'),
|
||||
updatedAt: new Date('2024-01-01'),
|
||||
lastLoginAt: new Date("2025-01-01"),
|
||||
createdAt: new Date("2024-01-01"),
|
||||
updatedAt: new Date("2024-01-01"),
|
||||
// ... other fields
|
||||
};
|
||||
|
||||
const result = mapPrismaUserToDomain(prismaUser);
|
||||
|
||||
expect(result.id).toBe('user-123');
|
||||
expect(result.id).toBe("user-123");
|
||||
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
|
||||
|
||||
DB mappers are:
|
||||
|
||||
- ✅ **Infrastructure concerns** (BFF layer)
|
||||
- ✅ **Pure transformation functions** (Prisma → Domain)
|
||||
- ✅ **Centralized** in `/infra/mappers/`
|
||||
@ -241,4 +245,3 @@ DB mappers are:
|
||||
- ❌ **Never contain business logic**
|
||||
|
||||
This clear separation ensures the domain layer remains pure and independent of infrastructure choices.
|
||||
|
||||
@ -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.
|
||||
|
||||
---
|
||||
|
||||
## Core Principle: "Map Once, Use Everywhere"
|
||||
|
||||
**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!
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
|
||||
### Structure
|
||||
@ -84,32 +158,9 @@ export class SalesforceOrderService {
|
||||
}
|
||||
```
|
||||
|
||||
### ✅ Correct Pattern: Direct Domain Mapper Usage
|
||||
---
|
||||
|
||||
```typescript
|
||||
// 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 Builder Pattern
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
### 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
|
||||
import { UNIQUE } from "./soql.util";
|
||||
@ -132,23 +186,21 @@ import { UNIQUE } from "./soql.util";
|
||||
*/
|
||||
export function buildOrderSelectFields(additional: string[] = []): string[] {
|
||||
const fields = [
|
||||
"Id", "AccountId", "Status", "Type", "EffectiveDate",
|
||||
"OrderNumber", "TotalAmount", "CreatedDate"
|
||||
];
|
||||
return UNIQUE([...fields, ...additional]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build SOQL SELECT fields for OrderItem queries
|
||||
*/
|
||||
export function buildOrderItemSelectFields(additional: string[] = []): string[] {
|
||||
const fields = [
|
||||
"Id", "OrderId", "Quantity", "UnitPrice", "TotalPrice"
|
||||
"Id",
|
||||
"AccountId",
|
||||
"Status",
|
||||
"Type",
|
||||
"EffectiveDate",
|
||||
"OrderNumber",
|
||||
"TotalAmount",
|
||||
"CreatedDate",
|
||||
];
|
||||
return UNIQUE([...fields, ...additional]);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Integration Patterns
|
||||
|
||||
### Pattern 1: Simple Fetch and Transform
|
||||
@ -222,24 +274,7 @@ async getInvoices(clientId: number): Promise<Invoice[]> {
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 4: Create/Update Operations
|
||||
|
||||
```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
|
||||
### Pattern 4: Integration with Caching
|
||||
|
||||
```typescript
|
||||
async getInvoice(invoiceId: number, userId: string): Promise<Invoice> {
|
||||
@ -263,6 +298,8 @@ async getInvoice(invoiceId: number, userId: string): Promise<Invoice> {
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Context Injection Pattern
|
||||
|
||||
Some transformations need infrastructure context (like currency). Pass it explicitly:
|
||||
@ -277,10 +314,85 @@ const subscription = Providers.Whmcs.transformWhmcsSubscription(whmcsProduct, {
|
||||
```
|
||||
|
||||
**Why this is clean:**
|
||||
|
||||
- Domain mapper is pure (deterministic for same inputs)
|
||||
- Infrastructure concern (currency) is injected from BFF
|
||||
- No service wrapper needed
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Anti-Patterns to Avoid
|
||||
|
||||
### ❌ Transformer/Wrapper Services
|
||||
|
||||
```typescript
|
||||
// DON'T: Create wrapper services that just call domain mappers
|
||||
@Injectable()
|
||||
export class InvoiceTransformerService {
|
||||
transformInvoice(raw: WhmcsInvoice): Invoice {
|
||||
return Providers.Whmcs.transformWhmcsInvoice(raw, {...});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Why it's bad:**
|
||||
|
||||
- Adds unnecessary layer
|
||||
- No value beyond what domain mapper provides
|
||||
- Makes codebase harder to understand
|
||||
|
||||
**Instead:** Use domain mapper directly in integration service
|
||||
|
||||
### ❌ Multiple Transformations
|
||||
|
||||
```typescript
|
||||
// DON'T: Transform multiple times
|
||||
const rawData = await api.fetch();
|
||||
const intermediate = this.customTransform(rawData);
|
||||
const domainType = Providers.X.transform(intermediate);
|
||||
```
|
||||
|
||||
**Instead:** Transform once using domain mapper
|
||||
|
||||
---
|
||||
|
||||
## Module Organization
|
||||
|
||||
```typescript
|
||||
@ -305,62 +417,9 @@ const subscription = Providers.Whmcs.transformWhmcsSubscription(whmcsProduct, {
|
||||
export class SalesforceModule {}
|
||||
```
|
||||
|
||||
## Anti-Patterns to Avoid
|
||||
---
|
||||
|
||||
### ❌ Transformer Services
|
||||
|
||||
```typescript
|
||||
// DON'T: Create wrapper services that just call domain mappers
|
||||
@Injectable()
|
||||
export class InvoiceTransformerService {
|
||||
transformInvoice(raw: WhmcsInvoice): Invoice {
|
||||
return Providers.Whmcs.transformWhmcsInvoice(raw, {...});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Why it's bad:**
|
||||
- Adds unnecessary layer
|
||||
- No value beyond what domain mapper provides
|
||||
- Makes codebase harder to understand
|
||||
|
||||
**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
|
||||
|
||||
```typescript
|
||||
// DON'T: Transform multiple times
|
||||
const rawData = await api.fetch();
|
||||
const intermediate = this.customTransform(rawData);
|
||||
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
|
||||
|
||||
## Best Practices
|
||||
## Best Practices Summary
|
||||
|
||||
### ✅ DO
|
||||
|
||||
@ -381,19 +440,17 @@ const domainType = Providers.X.transform(intermediate);
|
||||
5. **Hard-code queries** - use query builders
|
||||
6. **Skip error handling** - always log failures
|
||||
|
||||
## Summary
|
||||
---
|
||||
|
||||
**Integration services** are responsible for:
|
||||
- 🔧 Building queries (SOQL, API params)
|
||||
- 🌐 Executing API calls
|
||||
- 🔄 Using domain mappers to transform responses
|
||||
- 📦 Returning domain types
|
||||
## Migration Checklist
|
||||
|
||||
**Integration services** should NOT:
|
||||
- ❌ 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.
|
||||
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
|
||||
@ -3,6 +3,7 @@
|
||||
## Current Problem
|
||||
|
||||
The frontend was polling order details every 5-15 seconds, causing:
|
||||
|
||||
- Unnecessary load on backend (Salesforce API calls, DB queries)
|
||||
- High API costs
|
||||
- Battery drain on mobile devices
|
||||
@ -117,20 +118,24 @@ WebSockets provide bidirectional communication but are more complex to implement
|
||||
## Migration Path
|
||||
|
||||
### Week 1: Remove Polling
|
||||
|
||||
- [x] Remove aggressive 5-second polling
|
||||
- [x] Document interim strategy
|
||||
|
||||
### Week 2: Implement SSE Infrastructure
|
||||
|
||||
- [x] Create `OrderEventsService` in BFF
|
||||
- [x] Expose SSE endpoint `GET /orders/:sfOrderId/events`
|
||||
- [x] Publish fulfillment lifecycle events (activating, completed, failed)
|
||||
|
||||
### Week 3: Frontend Integration
|
||||
|
||||
- [x] Create `useOrderUpdates` hook
|
||||
- [x] Wire `OrderDetail` to SSE (no timers)
|
||||
- [x] Auto-refetch details on push updates
|
||||
|
||||
### Week 4: Post-launch Monitoring
|
||||
|
||||
- [ ] Add observability for SSE connection counts
|
||||
- [ ] Track client error rates and reconnection attempts
|
||||
- [ ] Review UX analytics after rollout
|
||||
@ -8,6 +8,7 @@
|
||||
## 🎯 Core Principle: Domain Package = Pure Domain Logic
|
||||
|
||||
The `@customer-portal/domain` package should contain **ONLY** pure domain logic that is:
|
||||
|
||||
- ✅ Framework-agnostic
|
||||
- ✅ Reusable across frontend and backend
|
||||
- ✅ 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
|
||||
|
||||
| Type | Location | Examples | Reasoning |
|
||||
|------|----------|----------|-----------|
|
||||
| **Domain Types** | `packages/domain/*/contract.ts` | `Invoice`, `Order`, `Customer` | Pure business entities |
|
||||
| **Validation Schemas** | `packages/domain/*/schema.ts` | `invoiceSchema`, `orderQueryParamsSchema` | Runtime validation |
|
||||
| **Pure Utilities** | `packages/domain/toolkit/` | `formatCurrency()`, `parseDate()` | No framework dependencies |
|
||||
| **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 |
|
||||
| **Shared Infra** | `packages/validation`, `packages/logging` | `ZodPipe`, `useZodForm` | Framework bridges |
|
||||
| Type | Location | Examples | Reasoning |
|
||||
| ---------------------- | ----------------------------------------- | ----------------------------------------- | ------------------------- |
|
||||
| **Domain Types** | `packages/domain/*/contract.ts` | `Invoice`, `Order`, `Customer` | Pure business entities |
|
||||
| **Validation Schemas** | `packages/domain/*/schema.ts` | `invoiceSchema`, `orderQueryParamsSchema` | Runtime validation |
|
||||
| **Pure Utilities** | `packages/domain/toolkit/` | `formatCurrency()`, `parseDate()` | No framework dependencies |
|
||||
| **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 |
|
||||
| **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/`**
|
||||
|
||||
#### 1. Domain Types & Contracts
|
||||
|
||||
```typescript
|
||||
// packages/domain/billing/contract.ts
|
||||
export interface Invoice {
|
||||
@ -43,6 +45,7 @@ export interface Invoice {
|
||||
```
|
||||
|
||||
#### 2. Validation Schemas (Zod)
|
||||
|
||||
```typescript
|
||||
// packages/domain/billing/schema.ts
|
||||
export const invoiceSchema = z.object({
|
||||
@ -60,12 +63,10 @@ export const invoiceQueryParamsSchema = z.object({
|
||||
```
|
||||
|
||||
#### 3. Pure Utility Functions
|
||||
|
||||
```typescript
|
||||
// packages/domain/toolkit/formatting/currency.ts
|
||||
export function formatCurrency(
|
||||
amount: number,
|
||||
currency: SupportedCurrency
|
||||
): string {
|
||||
export function formatCurrency(amount: number, currency: SupportedCurrency): string {
|
||||
return new Intl.NumberFormat("en-US", {
|
||||
style: "currency",
|
||||
currency,
|
||||
@ -78,6 +79,7 @@ export function formatCurrency(
|
||||
✅ **Reusable** - both frontend and backend can use it
|
||||
|
||||
#### 4. Provider Mappers
|
||||
|
||||
```typescript
|
||||
// packages/domain/billing/providers/whmcs/mapper.ts
|
||||
export function transformWhmcsInvoice(raw: WhmcsInvoiceRaw): Invoice {
|
||||
@ -94,22 +96,25 @@ export function transformWhmcsInvoice(raw: WhmcsInvoiceRaw): Invoice {
|
||||
### ❌ **What Should NOT Be in `packages/domain/`**
|
||||
|
||||
#### 1. Framework-Specific API Clients
|
||||
|
||||
```typescript
|
||||
// ❌ DO NOT put in domain
|
||||
// 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({
|
||||
baseUrl: process.env.NEXT_PUBLIC_API_URL, // ← Next.js specific
|
||||
baseUrl: process.env.NEXT_PUBLIC_API_URL, // ← Next.js specific
|
||||
});
|
||||
```
|
||||
|
||||
**Why?**
|
||||
|
||||
- Depends on `@hey-api/client-fetch` (external library)
|
||||
- Uses Next.js environment variables
|
||||
- Runtime infrastructure code
|
||||
|
||||
#### 2. React Hooks
|
||||
|
||||
```typescript
|
||||
// ❌ DO NOT put in domain
|
||||
// apps/portal/src/lib/hooks/useInvoices.ts
|
||||
@ -123,13 +128,15 @@ export function useInvoices() {
|
||||
**Why?** React-specific - backend can't use this
|
||||
|
||||
#### 3. Error Handling with Framework Dependencies
|
||||
|
||||
```typescript
|
||||
// ❌ DO NOT put in domain
|
||||
// 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 {
|
||||
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
|
||||
|
||||
#### 4. NestJS-Specific Utilities
|
||||
|
||||
```typescript
|
||||
// ❌ DO NOT put in domain
|
||||
// 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:
|
||||
|
||||
- Only used by backend
|
||||
- Not needed for type definitions
|
||||
- Better to keep application-specific utils in apps
|
||||
@ -208,12 +217,14 @@ apps/
|
||||
Ask these questions:
|
||||
|
||||
### ✅ Move to Domain If:
|
||||
|
||||
1. Is it a **pure function** with no framework dependencies?
|
||||
2. Does it work with **domain types**?
|
||||
3. Could **both frontend and backend** use it?
|
||||
4. Is it **business logic** (not infrastructure)?
|
||||
|
||||
### ❌ Keep in Apps If:
|
||||
|
||||
1. Does it import **React**, **Next.js**, or **NestJS**?
|
||||
2. Does it use **environment variables**?
|
||||
3. Does it depend on **external libraries** (API clients, HTTP libs)?
|
||||
@ -224,16 +235,16 @@ Ask these questions:
|
||||
|
||||
## 📋 Examples with Decisions
|
||||
|
||||
| Utility | Decision | Location | Why |
|
||||
|---------|----------|----------|-----|
|
||||
| `formatCurrency(amount, currency)` | ✅ Domain | `domain/toolkit/formatting/` | Pure function, no deps |
|
||||
| `invoiceQueryParamsSchema` | ✅ Domain | `domain/billing/schema.ts` | Domain-specific validation |
|
||||
| `paginationParamsSchema` | ✅ Domain | `domain/common/schema.ts` | Truly generic |
|
||||
| `useInvoices()` React hook | ❌ Portal App | `portal/lib/hooks/` | React-specific |
|
||||
| `apiClient` instance | ❌ Portal App | `portal/lib/api/` | Framework-specific |
|
||||
| `ZodValidationPipe` | ✅ Validation Package | `packages/validation/` | Reusable bridge |
|
||||
| `getErrorMessage(error)` | ❌ BFF App | `bff/core/utils/` | App-specific utility |
|
||||
| `transformWhmcsInvoice()` | ✅ Domain | `domain/billing/providers/whmcs/` | Data transformation |
|
||||
| Utility | Decision | Location | Why |
|
||||
| ---------------------------------- | --------------------- | --------------------------------- | -------------------------- |
|
||||
| `formatCurrency(amount, currency)` | ✅ Domain | `domain/toolkit/formatting/` | Pure function, no deps |
|
||||
| `invoiceQueryParamsSchema` | ✅ Domain | `domain/billing/schema.ts` | Domain-specific validation |
|
||||
| `paginationParamsSchema` | ✅ Domain | `domain/common/schema.ts` | Truly generic |
|
||||
| `useInvoices()` React hook | ❌ Portal App | `portal/lib/hooks/` | React-specific |
|
||||
| `apiClient` instance | ❌ Portal App | `portal/lib/api/` | Framework-specific |
|
||||
| `ZodValidationPipe` | ✅ Validation Package | `packages/validation/` | Reusable bridge |
|
||||
| `getErrorMessage(error)` | ❌ BFF App | `bff/core/utils/` | App-specific utility |
|
||||
| `transformWhmcsInvoice()` | ✅ Domain | `domain/billing/providers/whmcs/` | Data transformation |
|
||||
|
||||
---
|
||||
|
||||
@ -242,6 +253,7 @@ Ask these questions:
|
||||
### Current Structure (✅ Correct!)
|
||||
|
||||
**Generic building blocks** in `domain/common/`:
|
||||
|
||||
```typescript
|
||||
// packages/domain/common/schema.ts
|
||||
export const paginationParamsSchema = z.object({
|
||||
@ -257,26 +269,28 @@ export const filterParamsSchema = z.object({
|
||||
```
|
||||
|
||||
**Domain-specific query params** in their own domains:
|
||||
|
||||
```typescript
|
||||
// packages/domain/billing/schema.ts
|
||||
export const invoiceQueryParamsSchema = z.object({
|
||||
page: z.coerce.number().int().positive().optional(),
|
||||
limit: z.coerce.number().int().positive().max(100).optional(),
|
||||
status: invoiceStatusSchema.optional(), // ← Domain-specific
|
||||
dateFrom: z.string().datetime().optional(), // ← Domain-specific
|
||||
dateTo: z.string().datetime().optional(), // ← Domain-specific
|
||||
status: invoiceStatusSchema.optional(), // ← Domain-specific
|
||||
dateFrom: z.string().datetime().optional(), // ← Domain-specific
|
||||
dateTo: z.string().datetime().optional(), // ← Domain-specific
|
||||
});
|
||||
|
||||
// packages/domain/subscriptions/schema.ts
|
||||
export const subscriptionQueryParamsSchema = z.object({
|
||||
page: z.coerce.number().int().positive().optional(),
|
||||
limit: z.coerce.number().int().positive().max(100).optional(),
|
||||
status: subscriptionStatusSchema.optional(), // ← Domain-specific
|
||||
type: z.string().optional(), // ← Domain-specific
|
||||
status: subscriptionStatusSchema.optional(), // ← Domain-specific
|
||||
type: z.string().optional(), // ← Domain-specific
|
||||
});
|
||||
```
|
||||
|
||||
**Why this works:**
|
||||
|
||||
- `common` has truly generic utilities
|
||||
- Each domain owns its specific query parameters
|
||||
- No duplication of business logic
|
||||
@ -286,7 +300,9 @@ export const subscriptionQueryParamsSchema = z.object({
|
||||
## 📖 Summary
|
||||
|
||||
### Domain Package (`packages/domain/`)
|
||||
|
||||
**Contains:**
|
||||
|
||||
- ✅ Domain types & interfaces
|
||||
- ✅ Zod validation schemas
|
||||
- ✅ Provider mappers (WHMCS, Salesforce, Freebit)
|
||||
@ -294,13 +310,16 @@ export const subscriptionQueryParamsSchema = z.object({
|
||||
- ✅ Domain-specific query parameter schemas
|
||||
|
||||
**Does NOT contain:**
|
||||
|
||||
- ❌ React hooks
|
||||
- ❌ API client instances
|
||||
- ❌ Framework-specific code
|
||||
- ❌ Infrastructure code
|
||||
|
||||
### App Lib/Core Directories
|
||||
|
||||
**Contains:**
|
||||
|
||||
- ✅ Framework-specific utilities
|
||||
- ✅ API clients & HTTP interceptors
|
||||
- ✅ 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!
|
||||
|
||||
@ -8,11 +8,13 @@
|
||||
## 🎯 Architecture Philosophy
|
||||
|
||||
**Core Principle**: Domain-first organization where each business domain owns its:
|
||||
|
||||
- **contract.ts** - Normalized types (provider-agnostic)
|
||||
- **schema.ts** - Runtime validation (Zod)
|
||||
- **providers/** - Provider-specific adapters (raw types + mappers)
|
||||
|
||||
**Why This Works**:
|
||||
|
||||
- Domain-centric matches business thinking
|
||||
- Provider isolation prevents leaking implementation details
|
||||
- Adding new providers = adding new folders (no refactoring)
|
||||
@ -103,6 +105,7 @@ packages/domain/
|
||||
## 📝 Import Patterns
|
||||
|
||||
### **Application Code (Domain Only)**
|
||||
|
||||
```typescript
|
||||
// Import normalized domain types
|
||||
import { Invoice, invoiceSchema, INVOICE_STATUS } from "@customer-portal/domain/billing";
|
||||
@ -121,12 +124,13 @@ const validated = invoiceSchema.parse(rawData);
|
||||
```
|
||||
|
||||
### **Integration Code (Needs Provider Specifics)**
|
||||
|
||||
```typescript
|
||||
// Import domain + provider
|
||||
import { Invoice, invoiceSchema } from "@customer-portal/domain/billing";
|
||||
import {
|
||||
transformWhmcsInvoice,
|
||||
type WhmcsInvoiceRaw
|
||||
type WhmcsInvoiceRaw,
|
||||
} from "@customer-portal/domain/billing/providers/whmcs/mapper";
|
||||
import { whmcsInvoiceRawSchema } from "@customer-portal/domain/billing/providers/whmcs/raw.types";
|
||||
|
||||
@ -140,6 +144,7 @@ const invoice: Invoice = transformWhmcsInvoice(whmcsData);
|
||||
## 🏗️ Domain File Templates
|
||||
|
||||
### **contract.ts**
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* {Domain} - Contract
|
||||
@ -164,6 +169,7 @@ export interface {Domain} {
|
||||
```
|
||||
|
||||
### **schema.ts**
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* {Domain} - Schemas
|
||||
@ -183,6 +189,7 @@ export const {domain}Schema = z.object({
|
||||
```
|
||||
|
||||
### **providers/{provider}/raw.types.ts**
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* {Provider} {Domain} Provider - Raw Types
|
||||
@ -200,6 +207,7 @@ export type {Provider}{Domain}Raw = z.infer<typeof {provider}{Domain}RawSchema>;
|
||||
```
|
||||
|
||||
### **providers/{provider}/mapper.ts**
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* {Provider} {Domain} Provider - Mapper
|
||||
@ -232,7 +240,9 @@ export function transform{Provider}{Domain}(raw: unknown): {Domain} {
|
||||
## 🎓 Key Patterns
|
||||
|
||||
### **1. Co-location**
|
||||
|
||||
Everything about a domain lives together:
|
||||
|
||||
```
|
||||
billing/
|
||||
├── contract.ts # What billing IS
|
||||
@ -241,7 +251,9 @@ billing/
|
||||
```
|
||||
|
||||
### **2. Provider Isolation**
|
||||
|
||||
Raw types and mappers stay in `providers/`:
|
||||
|
||||
```typescript
|
||||
// ✅ GOOD - Isolated
|
||||
import { transformWhmcsInvoice } from "@customer-portal/domain/billing/providers/whmcs/mapper";
|
||||
@ -251,7 +263,9 @@ import { WhmcsInvoiceRaw } from "@somewhere/global";
|
||||
```
|
||||
|
||||
### **3. Schema-Driven**
|
||||
|
||||
Domain schemas define the contract:
|
||||
|
||||
```typescript
|
||||
// Contract (types)
|
||||
export interface Invoice { ... }
|
||||
@ -264,7 +278,9 @@ return invoiceSchema.parse(transformedData);
|
||||
```
|
||||
|
||||
### **4. Provider Agnostic**
|
||||
|
||||
App code never knows about providers:
|
||||
|
||||
```typescript
|
||||
// ✅ App only knows domain
|
||||
function displayInvoice(invoice: Invoice) {
|
||||
@ -285,11 +301,13 @@ async function getInvoice(id: number): Promise<Invoice> {
|
||||
Example: Adding Stripe as an invoice provider
|
||||
|
||||
**1. Create provider folder:**
|
||||
|
||||
```bash
|
||||
mkdir -p packages/domain/billing/providers/stripe
|
||||
```
|
||||
|
||||
**2. Add raw types:**
|
||||
|
||||
```typescript
|
||||
// billing/providers/stripe/raw.types.ts
|
||||
export const stripeInvoiceRawSchema = z.object({
|
||||
@ -300,6 +318,7 @@ export const stripeInvoiceRawSchema = z.object({
|
||||
```
|
||||
|
||||
**3. Add mapper:**
|
||||
|
||||
```typescript
|
||||
// billing/providers/stripe/mapper.ts
|
||||
export function transformStripeInvoice(raw: unknown): Invoice {
|
||||
@ -313,6 +332,7 @@ export function transformStripeInvoice(raw: unknown): Invoice {
|
||||
```
|
||||
|
||||
**4. Use in service:**
|
||||
|
||||
```typescript
|
||||
// No changes to domain contract needed!
|
||||
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.
|
||||
|
||||
@ -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
|
||||
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
|
||||
## Core Principle
|
||||
|
||||
**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
|
||||
// packages/domain/src/entities/product.ts
|
||||
|
||||
// Maps directly to Salesforce Product2 fields
|
||||
interface BaseProduct {
|
||||
// Standard Salesforce fields
|
||||
@ -56,14 +49,13 @@ interface PricebookEntry {
|
||||
// Product with proper pricing structure
|
||||
interface ProductWithPricing extends BaseProduct {
|
||||
pricebookEntry?: PricebookEntry;
|
||||
// Convenience fields derived from pricebookEntry and billingCycle
|
||||
unitPrice?: number; // PricebookEntry.UnitPrice
|
||||
monthlyPrice?: number; // UnitPrice if billingCycle === "Monthly"
|
||||
oneTimePrice?: number; // UnitPrice if billingCycle === "Onetime"
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Specialized Product Types
|
||||
### Specialized Product Types
|
||||
|
||||
```typescript
|
||||
// Category-specific extensions
|
||||
@ -85,7 +77,11 @@ interface SimProduct extends ProductWithPricing {
|
||||
type Product = InternetProduct | SimProduct | VpnProduct | ProductWithPricing;
|
||||
```
|
||||
|
||||
### 3. Order Item Types
|
||||
---
|
||||
|
||||
## Order Types
|
||||
|
||||
### Order Item Types
|
||||
|
||||
```typescript
|
||||
// For new orders (before Salesforce creation)
|
||||
@ -109,7 +105,7 @@ interface SalesforceOrderItem {
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Order Types
|
||||
### Order Types
|
||||
|
||||
```typescript
|
||||
// Salesforce Order structure
|
||||
@ -143,34 +139,12 @@ type Order = WhmcsOrder | SalesforceOrder;
|
||||
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
|
||||
|
||||
### Salesforce to Domain
|
||||
|
||||
```typescript
|
||||
// Transform Salesforce Product2 + PricebookEntry to unified Product
|
||||
function fromSalesforceProduct2(sfProduct: any, pricebookEntry?: any): Product {
|
||||
@ -205,36 +179,42 @@ function fromSalesforceProduct2(sfProduct: any, pricebookEntry?: any): Product {
|
||||
unitPrice: unitPrice,
|
||||
monthlyPrice: billingCycle === "Monthly" ? 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
|
||||
|
||||
### Catalog Context
|
||||
@ -245,6 +225,11 @@ const product = fromSalesforceProduct2(salesforceProduct, pricebookEntry);
|
||||
if (isCatalogVisible(product)) {
|
||||
displayInCatalog(product);
|
||||
}
|
||||
|
||||
// Type-safe access to specific fields
|
||||
if (isInternetProduct(product)) {
|
||||
console.log(product.internetPlanTier); // TypeScript knows this exists
|
||||
}
|
||||
```
|
||||
|
||||
### Order Context
|
||||
@ -255,16 +240,6 @@ const orderItem: OrderItemRequest = {
|
||||
quantity: 1,
|
||||
autoAdded: false,
|
||||
};
|
||||
```
|
||||
|
||||
### Enhanced Order Summary
|
||||
|
||||
```typescript
|
||||
// Now uses consistent unified types
|
||||
interface OrderItem extends OrderItemRequest {
|
||||
id?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
// Access unified pricing fields
|
||||
const price =
|
||||
@ -273,44 +248,33 @@ const price =
|
||||
: item.oneTimePrice || item.unitPrice || 0;
|
||||
```
|
||||
|
||||
## Migration Strategy
|
||||
---
|
||||
|
||||
1. **Backward Compatibility**: Legacy type aliases maintained
|
||||
## Benefits
|
||||
|
||||
```typescript
|
||||
export type InternetPlan = InternetProduct;
|
||||
export type CatalogItem = Product;
|
||||
```
|
||||
1. **Single Source of Truth**: All types map directly to Salesforce object 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
|
||||
|
||||
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
|
||||
- `packages/domain/src/entities/order.ts` - Updated order types
|
||||
```typescript
|
||||
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
|
||||
- `apps/portal/src/features/catalog/components/base/EnhancedOrderSummary.tsx` - Updated to use unified 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.
|
||||
- `packages/domain/src/entities/order.ts` - Updated order types
|
||||
@ -13,14 +13,15 @@ When you run `prisma generate`, Prisma creates a client at `node_modules/.prisma
|
||||
```javascript
|
||||
// Inside generated client (simplified)
|
||||
const config = {
|
||||
schemaPath: "prisma/schema.prisma", // embedded at generate time
|
||||
schemaPath: "prisma/schema.prisma", // embedded at generate time
|
||||
// ...
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### 2. Monorepo vs Production Directory Structures
|
||||
|
||||
**During Build (Monorepo):**
|
||||
|
||||
```
|
||||
/app/
|
||||
├── apps/
|
||||
@ -32,6 +33,7 @@ const config = {
|
||||
```
|
||||
|
||||
**In Production (After pnpm deploy):**
|
||||
|
||||
```
|
||||
/app/
|
||||
├── prisma/schema.prisma ← Schema moved to root!
|
||||
@ -42,6 +44,7 @@ const config = {
|
||||
### 3. The Mismatch
|
||||
|
||||
In production, the schema is at:
|
||||
|
||||
```
|
||||
prisma/schema.prisma
|
||||
```
|
||||
@ -57,23 +60,29 @@ RUN pnpm dlx prisma@${PRISMA_VERSION} generate --schema=prisma/schema.prisma
|
||||
```
|
||||
|
||||
This regenerates the client with:
|
||||
|
||||
```javascript
|
||||
schemaPath: "prisma/schema.prisma" // ← Matches production!
|
||||
schemaPath: "prisma/schema.prisma"; // ← Matches production!
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Document the Behavior
|
||||
|
||||
Add comments in Dockerfile and schema.prisma explaining why regeneration is needed.
|
||||
|
||||
### 2. Version Lock Prisma
|
||||
|
||||
Use explicit version in Docker:
|
||||
|
||||
```dockerfile
|
||||
RUN pnpm dlx prisma@${PRISMA_VERSION} generate --schema=prisma/schema.prisma
|
||||
```
|
||||
|
||||
### 3. Match Prisma Versions
|
||||
|
||||
Ensure the version in Docker matches `package.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"dependencies": {
|
||||
@ -86,7 +95,9 @@ Ensure the version in Docker matches `package.json`:
|
||||
```
|
||||
|
||||
### 4. Include Binary Targets
|
||||
|
||||
Always include the production binary target in schema:
|
||||
|
||||
```prisma
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
@ -97,24 +108,28 @@ generator client {
|
||||
## Debugging
|
||||
|
||||
### Check Embedded Path
|
||||
|
||||
You can inspect the generated client's config:
|
||||
|
||||
```bash
|
||||
cat node_modules/.prisma/client/index.js | grep schemaPath
|
||||
```
|
||||
|
||||
### Verify Schema Location
|
||||
|
||||
In the container:
|
||||
|
||||
```bash
|
||||
ls -la /app/prisma/schema.prisma
|
||||
```
|
||||
|
||||
### Common Errors
|
||||
|
||||
| Error | Cause | Fix |
|
||||
|-------|-------|-----|
|
||||
| `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 |
|
||||
| `Query engine not found` | Missing binary target | Add correct `binaryTargets` to schema |
|
||||
| Error | Cause | Fix |
|
||||
| ----------------------------------- | ------------------------------------------- | ------------------------------------- |
|
||||
| `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 |
|
||||
| `Query engine not found` | Missing binary target | Add correct `binaryTargets` to schema |
|
||||
|
||||
## Further Reading
|
||||
|
||||
@ -173,10 +173,10 @@ OrderItems [
|
||||
|
||||
### OrderItem Status Updates
|
||||
|
||||
| Field | Source | Example | Notes |
|
||||
| --------------------- | -------------------- | --------- | ---------------------------- |
|
||||
| Field | Source | Example | Notes |
|
||||
| --------------------- | -------------------------------- | --------- | ---------------------------- |
|
||||
| `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
|
||||
|
||||
80
docs/integrations/sim/state-machine.md
Normal file
80
docs/integrations/sim/state-machine.md
Normal 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.
|
||||
@ -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)
|
||||
@ -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
331
docs/operations/logging.md
Normal 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
|
||||
@ -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.
|
||||
|
||||
@ -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.
|
||||
Loading…
x
Reference in New Issue
Block a user