- Updated CardBadge to support additional size options and enhanced styling for better visual consistency. - Refactored InternetPlanCard to utilize the new CardBadge size options and improved layout for badge display. - Enhanced state management in InternetPlanCard to utilize Zustand for better performance and maintainability. - Streamlined rendering logic in InternetConfigureContainer for improved step transitions and user experience. - Updated catalog utility functions to remove deprecated filtering and sorting methods, focusing on server-side handling.
25 KiB
Comprehensive Code Review Report
Customer Portal - Architecture & Code Quality Analysis
Date: October 28, 2025
Reviewer: AI Code Reviewer
Scope: Full codebase review for separation of concerns, domain design, and code quality
Executive Summary
The customer portal codebase demonstrates good overall architecture with a clear BFF (Backend for Frontend) pattern and a dedicated domain package. However, there are several critical architectural violations and technical debt that need to be addressed to achieve true separation of concerns and maintainable code.
Key Metrics
- Architecture Score: 7/10 (Good foundation, needs refinement)
- Separation of Concerns: 6/10 (Domain exists but inconsistently used)
- Code Quality: 7.5/10 (Generally clean with some issues)
- File Structure: 8/10 (Well-organized, some redundancy)
✅ Strengths (What's Done Well)
1. Domain Package Architecture
- Well-structured domain package at
packages/domain/with clear separation by business domain (catalog, orders, sim, billing, etc.) - Schema-first approach using Zod for runtime validation with TypeScript type inference
- Provider adapters properly isolated (Salesforce, WHMCS, Freebit) with clean mapping logic
- Centralized validation schemas in domain package
2. BFF Layer Implementation
- Clean controller layer with proper use of NestJS decorators and guards
- Service-oriented architecture with clear separation (orchestrators, validators, builders)
- Proper dependency injection patterns throughout
- Structured logging using nestjs-pino
- Rate limiting on sensitive endpoints (orders, catalog)
3. Frontend Architecture
- Feature-based organization in
apps/portal/src/features/ - Component hierarchy (atoms, molecules, organisms) following atomic design
- Zustand state management for complex state (auth, catalog configuration)
- React Query integration for data fetching
- Proper hooks abstraction for business logic encapsulation
4. Type Safety
- Strong TypeScript usage throughout the codebase
- Type inference from Zod schemas (schema-first approach)
- Minimal use of
anytypes
❌ Critical Issues (Must Fix)
1. Business Logic in Frontend ⚠️ CRITICAL
Location: apps/portal/src/features/catalog/utils/catalog.utils.ts
Issue: Frontend contains business logic for filtering, sorting, and calculating product values.
// ❌ BAD: Frontend has business logic
export function filterProducts(
products: CatalogProduct[],
filters: { category?: string; priceMin?: number; priceMax?: number; search?: string; }
): CatalogProduct[] {
return products.filter(product => {
if (typeof filters.priceMin === "number") {
const price = product.monthlyPrice ?? product.oneTimePrice ?? 0;
if (price < filters.priceMin) return false;
}
// ... more filtering logic
});
}
export function sortProducts(products: CatalogProduct[], sortBy: "name" | "price" = "name"): CatalogProduct[] {
// ... sorting logic
}
Why This Is Wrong:
- Separation of concerns violated: Frontend should only display data, not transform it
- Inconsistent business rules: Client-side logic can diverge from server-side
- Security risk: Business rules can be bypassed by modifying frontend code
- Performance: Heavy operations on client-side
Solution:
// ✅ GOOD: Move to BFF
// apps/bff/src/modules/catalog/services/catalog.service.ts
async getFilteredProducts(
filters: CatalogFilter
): Promise<CatalogProduct[]> {
// Apply filtering server-side
// Return pre-filtered, pre-calculated results
}
Impact: HIGH - Violates fundamental architectural principles
2. Business Validation Logic Split Between Layers ⚠️ HIGH
Location:
apps/portal/src/features/checkout/hooks/useCheckout.ts(lines 51-59, 211-213)apps/bff/src/modules/orders/services/order-validator.service.ts
Issue: Business validation duplicated in frontend and backend.
// ❌ BAD: Business validation in frontend hook
const hasActiveInternetSubscription = useMemo(() => {
if (!Array.isArray(activeSubs)) return false;
return activeSubs.some(
subscription =>
String(subscription.groupName || subscription.productName || "")
.toLowerCase()
.includes("internet") &&
String(subscription.status || "").toLowerCase() === "active"
);
}, [activeSubs]);
// Client-side guard: prevent Internet orders if an Internet subscription already exists
if (orderType === ORDER_TYPE.INTERNET && hasActiveInternetSubscription && !isDevEnvironment) {
throw new Error(ACTIVE_INTERNET_WARNING_MESSAGE);
}
Why This Is Wrong:
- Duplicate validation: Same rule exists in BFF (
validateInternetDuplication) - Inconsistency risk: Frontend and backend rules can diverge
- Security: Client-side checks can be bypassed
- Development mode override: Business rules shouldn't be environment-dependent
Solution:
// ✅ GOOD: Single source of truth in BFF
// Frontend only displays warnings/hints
// BFF enforces business rules absolutely
// Remove all business rule enforcement from frontend
Impact: HIGH - Security and consistency risk
3. Frontend Stores Contain Business Logic ⚠️ MEDIUM-HIGH
Location: apps/portal/src/features/catalog/services/catalog.store.ts
Issue: Store contains complex business logic for building checkout parameters, validating forms, and transforming data.
// ❌ BAD: Complex transformation logic in frontend store (lines 196-304)
buildSimCheckoutParams: () => {
const { sim } = get();
if (!sim.planSku) return null;
const rawSelections: Record<string, string> = {
plan: sim.planSku,
simType: sim.simType,
activationType: sim.activationType,
};
// 50+ lines of complex transformation logic
// Building MNP data, normalizing selections, etc.
}
Why This Is Wrong:
- Business logic in presentation layer: Stores should only manage state, not transform it
- Complex transformations: Should be in domain package
- Tight coupling: Frontend tightly coupled to domain implementation details
Solution:
// ✅ GOOD: Move to domain package
// packages/domain/orders/helpers.ts
export function buildSimCheckoutSelections(config: SimConfigState): OrderSelections {
// Transformation logic here
}
// Frontend store only calls helper
buildSimCheckoutParams: () => {
const { sim } = get();
return buildSimCheckoutSelections(sim);
}
Impact: MEDIUM-HIGH - Maintainability and testability issues
4. Validation Schema Duplication ⚠️ MEDIUM
Location: Multiple files
Issue: Validation patterns duplicated across layers instead of using domain schemas.
// ❌ BAD: Local validation schema in BFF
// apps/bff/src/integrations/salesforce/utils/soql.util.ts
const nonEmptyStringSchema = z.string().min(1, "Value cannot be empty").trim();
const soqlFieldNameSchema = z.string().trim().regex(/^[A-Za-z0-9_.]+$/, "Invalid SOQL field name");
// ❌ BAD: Controller-specific response schema
// apps/bff/src/modules/orders/orders.controller.ts
private readonly createOrderResponseSchema = apiSuccessResponseSchema(
z.object({
sfOrderId: z.string(),
status: z.string(),
message: z.string(),
})
);
Why This Is Wrong:
- Duplication: Same validation logic defined in multiple places
- Inconsistency: Validation rules can diverge
- Not DRY: Violates Don't Repeat Yourself principle
Solution:
// ✅ GOOD: Define once in domain
// packages/domain/common/validation.ts
export const nonEmptyStringSchema = z.string().min(1).trim();
export const salesforceIdSchema = z.string().length(18);
// packages/domain/orders/schema.ts
export const orderCreateResponseSchema = z.object({
sfOrderId: z.string(),
status: z.string(),
message: z.string(),
});
Impact: MEDIUM - Code duplication and maintenance burden
5. Infrastructure Types in Business Layer ⚠️ MEDIUM
Location: Various service files
Issue: Infrastructure-specific types defined in business services instead of proper boundaries.
// ❌ BAD: Infrastructure type in service
// apps/bff/src/modules/invoices/services/invoice-retrieval.service.ts
interface UserMappingInfo {
userId: string;
whmcsClientId: number;
}
// ❌ BAD: BFF-specific interface that could be domain type
// apps/bff/src/modules/orders/services/order-fulfillment-validator.service.ts
export interface OrderFulfillmentValidationResult {
sfOrder: SalesforceOrderRecord;
clientId: number;
isAlreadyProvisioned: boolean;
whmcsOrderId?: string;
}
Why This Is Wrong:
- Blurred boundaries: Infrastructure concerns mixed with business logic
- Tight coupling: Services coupled to specific infrastructure details
- Hard to test: Infrastructure types make unit testing harder
Solution:
// ✅ GOOD: Define in appropriate domain or create proper abstraction
// packages/domain/orders/types.ts
export interface OrderFulfillmentValidation {
orderId: string;
customerAccountId: string;
isProvisioned: boolean;
externalOrderId?: string;
}
Impact: MEDIUM - Architectural clarity and testability
6. Complex Business Logic in Frontend Hooks ⚠️ MEDIUM
Location: apps/portal/src/features/checkout/hooks/useCheckout.ts (lines 90-123)
Issue: Complex parsing and configuration building in frontend hooks.
// ❌ BAD: Complex business logic in hook (lines 90-123)
const { selections, configurations } = useMemo(() => {
const rawSelections: Record<string, string> = {};
params.forEach((value, key) => {
if (key !== "type") {
rawSelections[key] = value;
}
});
try {
const normalizedSelections = normalizeOrderSelections(rawSelections);
let configuration: OrderConfigurations | undefined;
try {
configuration = buildOrderConfigurations(normalizedSelections);
} catch (error) {
logger.warn("Failed to derive order configurations from selections", {
error: error instanceof Error ? error.message : String(error),
});
}
return {
selections: normalizedSelections,
configurations: configuration,
};
} catch (error) {
// ... error handling
}
}, [params]);
Why This Is Wrong:
- Complex logic in UI layer: Hooks should be simple state managers
- Silent error swallowing: Try-catch with fallbacks hide issues
- Hard to test: Logic tied to React hooks ecosystem
Solution:
// ✅ GOOD: Move to service layer
// apps/portal/src/features/checkout/services/checkout-params.service.ts
export class CheckoutParamsService {
static parseSelections(params: URLSearchParams): OrderSelections {
// Parsing logic
}
static buildConfigurations(selections: OrderSelections): OrderConfigurations {
// Building logic
}
}
// Hook becomes simple
const selections = CheckoutParamsService.parseSelections(params);
const configurations = CheckoutParamsService.buildConfigurations(selections);
Impact: MEDIUM - Maintainability and testability
🔴 Architectural Issues
1. Inconsistent Domain Package Usage
Issue: Not all business types are in the domain package. Some are scattered across BFF and Portal.
Examples:
CheckoutItemused in Portal but not defined in domainPricingTierdefined in UI componentsCatalogFilterhas TODO comment indicating missing domain type
Solution:
- Move ALL business types to domain package
- Frontend and BFF should ONLY import from domain
- Domain package should be the single source of truth
2. Lack of Domain Services
Issue: Business logic spread across BFF services without clear domain service layer.
Observation:
packages/domain/
catalog/
schema.ts ✅
contract.ts ✅
providers/ ✅
index.ts ✅
services/ ❌ MISSING
validation.ts ❌ MISSING
Solution:
// packages/domain/orders/services/order-validation.service.ts
export class OrderValidationService {
static validateOrderFulfillment(order: Order): ValidationResult {
// Pure business logic here
}
}
Impact: Architectural clarity and testability
3. Mixed Responsibility in Controllers
Issue: Controllers doing more than HTTP request/response handling.
Example:
// ❌ BAD: Controller handles parsing logic
@Get("internet/plans")
async getInternetPlans(@Request() req: RequestWithUser) {
const userId = req.user?.id;
if (!userId) {
const catalog = await this.internetCatalog.getCatalogData();
return parseInternetCatalog(catalog); // Parsing in controller
}
// ...
}
Solution:
// ✅ GOOD: Service handles all logic
@Get("internet/plans")
async getInternetPlans(@Request() req: RequestWithUser) {
return this.catalogService.getInternetPlansForUser(req.user?.id);
}
4. Tight Coupling to External Services
Issue: Business logic services directly coupled to integration services (WHMCS, Salesforce).
Example:
// apps/bff/src/modules/orders/services/order-validator.service.ts
constructor(
private readonly whmcs: WhmcsConnectionOrchestratorService,
private readonly mappings: MappingsService,
// ... directly depends on infrastructure
)
Solution: Introduce repository pattern or ports/adapters to decouple business logic from infrastructure.
🟡 Code Quality Issues
1. Inconsistent Error Handling
Issue: Mix of generic throw new Error() and typed exceptions.
Found:
- Some services use typed exceptions (
SimActivationException,OrderValidationException) - Many still use generic
throw new Error() - Inconsistent error message formatting
Solution: Standardize on typed exception hierarchy across all services.
2. Missing Input Validation in Some Services
Issue: Some service methods don't validate inputs before processing.
Example:
// apps/bff/src/modules/orders/services/sim-fulfillment.service.ts (lines 28-34)
const simType = this.readEnum(configurations.simType, ["eSIM", "Physical SIM"]) ?? "eSIM";
const eid = this.readString(configurations.eid);
// ... no validation that configurations is not null/undefined
Solution: Add input validation at service boundaries.
3. Complex Service Methods
Issue: Some service methods are very long (100+ lines) with complex logic.
Examples:
OrderFulfillmentOrchestrator.fulfillOrder()(likely 200+ lines)SimFulfillmentService.fulfillSimOrder()(100 lines with nested conditionals)
Solution: Break down into smaller, focused methods with single responsibility.
4. Inadequate Logging Context
Issue: Some log statements lack sufficient context for debugging.
Example:
this.logger.log("SIM fulfillment completed successfully", {
orderId: orderDetails.id,
account: phoneNumber,
planSku,
});
// Missing: simType, activationType, whether MNP was used, etc.
Solution: Add comprehensive context to all log statements.
5. Magic Strings and Numbers
Issue: Various magic strings and numbers scattered throughout code.
Examples:
// apps/portal/src/features/checkout/hooks/useCheckout.ts
const ACTIVE_INTERNET_WARNING_MESSAGE = "You already have an active..."; // Should be in constants
const isDevEnvironment = process.env.NODE_ENV === "development"; // Direct env check
// apps/bff/src/modules/orders/controllers/checkout.service.ts (line 55)
if (simType === "eSIM" && (!eid || eid.length < 15)) { // Magic number 15
Solution: Extract to constants or configuration.
📁 File Structure Issues
1. Inconsistent Module Organization
Issue: Some modules have deep nesting, others are flat.
Example:
modules/auth/
application/
infra/
workflows/
workflows/ ← Redundant nesting
presentation/
services/
vs.
modules/catalog/
services/
catalog.controller.ts
catalog.module.ts
Solution: Standardize on consistent depth and organization.
2. Mixed Concerns in Directories
Issue: Some directories contain both business logic and infrastructure code.
Example:
modules/orders/
controllers/ ← HTTP layer
services/ ← Mix of business logic and orchestration
queue/ ← Infrastructure
types/ ← Shared types
Solution: Separate by architectural layer (domain, application, infrastructure, presentation).
3. Redundant Type Directories
Issue: Types scattered across multiple locations.
Found:
apps/bff/src/types/apps/bff/src/modules/*/types/packages/domain/*/
Solution: All business types in domain, infrastructure types in their respective modules.
🔧 Technical Debt
1. Build Artifacts in Source ⚠️ RESOLVED
Status: Fixed (per CODEBASE_ANALYSIS.md)
- Previously: 224 generated files in source tree
- Now: Build outputs isolated to
dist/
2. Console.log Statements
Issue: 3 console.log instances found in portal (should use logger)
Found: apps/portal/src/lib/logger.ts:2
Solution: Replace with proper logging infrastructure.
3. Incomplete Type Safety
Issue: Some areas still have loose typing.
Examples:
unknownandanyusage in error handling- Generic
Record<string, unknown>for complex objects - Missing discriminated unions for variant types
4. Missing Tests
Observation: No test files found in search (likely in separate test directories).
Recommendation: Ensure comprehensive test coverage, especially for:
- Domain validation logic
- Business rules
- Order fulfillment workflows
- Integration adapters
📊 Detailed Findings by Domain
Authentication & Authorization
Strengths:
- ✅ Clean store implementation with Zustand
- ✅ Proper JWT handling with httpOnly cookies
- ✅ Session management with refresh tokens
Issues:
- ⚠️ Complex workflow logic in frontend store (auth.store.ts)
- ⚠️ Nested directory structure (
infra/workflows/workflows/)
Catalog & Product Management
Strengths:
- ✅ Domain types well-defined (CatalogProduct, InternetPlan, SimProduct, etc.)
- ✅ Provider adapters cleanly separated
- ✅ Caching implemented (CatalogCacheService)
Issues:
- ❌ Filtering and sorting logic in frontend utils
- ❌ Business logic in catalog store
- ⚠️ Parsing logic in controllers instead of services
Orders & Checkout
Strengths:
- ✅ Clear orchestration pattern (OrderOrchestrator)
- ✅ Separate validation service (OrderValidator)
- ✅ Domain schemas for order types
- ✅ Business validation in domain package
Issues:
- ❌ Frontend validation duplicating backend validation
- ❌ Complex checkout hook with business logic
- ⚠️ Long service methods that could be broken down
Subscriptions & Billing
Strengths:
- ✅ Clean data fetching with React Query
- ✅ Proper type definitions
Issues:
- ⚠️ Minimal business logic encapsulation
- ⚠️ Direct WHMCS coupling in some areas
🎯 Recommendations
Immediate Actions (High Priority)
1. Remove Business Logic from Frontend ⚠️ CRITICAL
- Timeline: 1-2 weeks
- Impact: HIGH
- Action:
- Move
filterProducts,sortProductsfromcatalog.utils.tsto BFF - Remove client-side business validation from
useCheckouthook - Simplify frontend stores to only manage UI state
- Move
2. Consolidate Validation Schemas ⚠️ HIGH
- Timeline: 1 week
- Impact: MEDIUM
- Action:
- Move all validation schemas to domain package
- Remove duplicate schemas from BFF and Portal
- Update imports across codebase
3. Extract Business Types to Domain ⚠️ HIGH
- Timeline: 1 week
- Impact: MEDIUM
- Action:
- Move
OrderFulfillmentValidationResult,PricingTier,CheckoutItemto domain - Define missing types (
CatalogFilter, etc.) - Update all imports
- Move
Medium Priority Actions
4. Introduce Domain Services Layer
- Timeline: 2-3 weeks
- Impact: MEDIUM
- Action:
- Create
packages/domain/*/services/directories - Move pure business logic from BFF services to domain services
- BFF services become thin orchestration layer
- Create
5. Standardize Error Handling
- Timeline: 1-2 weeks
- Impact: MEDIUM
- Action:
- Complete typed exception hierarchy
- Replace all generic
throw new Error()with typed exceptions - Add proper error context
6. Refactor Long Service Methods
- Timeline: 2 weeks
- Impact: LOW-MEDIUM
- Action:
- Break down methods >50 lines into smaller functions
- Apply Single Responsibility Principle
- Improve testability
Long-term Improvements
7. Implement Repository Pattern
- Timeline: 3-4 weeks
- Impact: MEDIUM
- Action:
- Introduce repositories to decouple from Salesforce/WHMCS
- Define ports/adapters architecture
- Improve testability with mocking
8. Comprehensive Testing Strategy
- Timeline: Ongoing
- Impact: HIGH
- Action:
- Unit tests for all domain services
- Integration tests for BFF services
- E2E tests for critical user flows
9. Documentation Improvements
- Timeline: 1-2 weeks
- Impact: LOW
- Action:
- Architecture Decision Records (ADRs)
- API documentation
- Code comments for complex business logic
📋 Implementation Plan
Phase 1: Critical Fixes (Weeks 1-3)
- Week 1: Remove business logic from frontend utilities
- Week 2: Consolidate validation schemas in domain
- Week 3: Extract business types to domain package
Phase 2: Architectural Improvements (Weeks 4-6)
- Week 4-5: Introduce domain services layer
- Week 6: Standardize error handling across codebase
Phase 3: Quality Improvements (Weeks 7-9)
- Week 7: Refactor long service methods
- Week 8-9: Implement repository pattern
Phase 4: Long-term Excellence (Weeks 10+)
- Week 10+: Comprehensive testing strategy
- Ongoing: Documentation improvements
📈 Success Metrics
Code Quality Metrics
- Type Safety: 95%+ of code strongly typed
- Business Logic in Domain: 90%+ of business logic in domain package
- Validation Consistency: Single validation schema per business rule
- Test Coverage: 80%+ for domain and business logic
Architectural Metrics
- Separation of Concerns: Clear boundaries between layers
- Coupling: Low coupling between modules
- Cohesion: High cohesion within modules
- Import Clarity: All business types imported from domain
🎓 Learning & Best Practices
What to Continue
- Schema-first approach: Continue using Zod schemas as source of truth
- Provider adapters: Continue clean separation of external service integration
- Feature-based organization: Frontend feature structure is good
- Dependency injection: Continue using DI in BFF
What to Stop
- Business logic in frontend: Stop putting business rules in UI components/hooks
- Duplicate validation: Stop defining validation schemas in multiple places
- Direct external service coupling: Stop directly coupling business logic to infrastructure
- Magic values: Stop using magic strings/numbers without constants
What to Start
- Domain services: Start creating pure business logic services in domain
- Repository pattern: Start abstracting data access
- Typed exceptions: Start using typed exceptions everywhere
- Comprehensive logging: Start adding rich context to all logs
🔍 Conclusion
The customer portal has a solid architectural foundation with good separation between BFF and Portal, and a well-structured domain package. However, there are critical violations of separation of concerns, particularly business logic leaking into the frontend.
Summary Assessment
- Architecture: 7/10 - Good foundation but needs refinement
- Domain Design: 6/10 - Well-structured but underutilized
- Code Quality: 7.5/10 - Generally clean with some technical debt
- Maintainability: 6.5/10 - Will improve significantly after addressing issues
Priority Focus Areas
- Remove all business logic from frontend (Critical)
- Consolidate validation in domain (High)
- Complete domain type migration (High)
- Introduce domain services layer (Medium)
By addressing these issues systematically, the codebase will achieve:
- ✅ True separation of concerns
- ✅ Single source of truth for business logic
- ✅ Improved testability
- ✅ Better maintainability
- ✅ Cleaner architecture
Next Steps: Prioritize Phase 1 actions and create detailed task tickets for implementation.
Review Schedule: Re-assess after Phase 1 completion (3 weeks) to measure progress.