From e5ce4e166ce6cab27de238f82c1fc27aa3ee0c9c Mon Sep 17 00:00:00 2001 From: barsa Date: Wed, 22 Oct 2025 10:58:16 +0900 Subject: [PATCH] Refactor mappers and services for improved type safety and code clarity - Updated export statements in user and mapping mappers for consistency. - Enhanced FreebitAuthService to explicitly define response types for better type inference. - Refactored various services to improve error handling and response structure. - Cleaned up unused code and comments across multiple files to enhance readability. - Improved type annotations in invoice and subscription services for better validation and consistency. --- DOMAIN_VIOLATIONS_REPORT.md | 374 ++++++++++++++++++ apps/bff/src/infra/mappers/index.ts | 9 +- apps/bff/src/infra/mappers/mapping.mapper.ts | 5 +- apps/bff/src/infra/mappers/user.mapper.ts | 9 +- .../freebit/services/freebit-auth.service.ts | 3 +- .../services/freebit-operations.service.ts | 90 +++-- .../services/freebit-orchestrator.service.ts | 4 +- .../salesforce/salesforce.service.ts | 4 +- .../services/salesforce-account.service.ts | 2 +- .../services/salesforce-order.service.ts | 24 +- .../salesforce/utils/catalog-query-builder.ts | 13 +- .../salesforce/utils/order-query-builder.ts | 12 +- .../services/whmcs-api-methods.service.ts | 16 +- .../whmcs/services/whmcs-client.service.ts | 7 +- .../whmcs/services/whmcs-currency.service.ts | 52 +-- .../whmcs/services/whmcs-invoice.service.ts | 21 +- .../whmcs/services/whmcs-order.service.ts | 7 +- .../whmcs/services/whmcs-payment.service.ts | 11 +- .../whmcs/services/whmcs-sso.service.ts | 10 +- .../whmcs/utils/whmcs-client.utils.ts | 49 +-- .../src/integrations/whmcs/whmcs.service.ts | 18 +- .../modules/auth/application/auth.facade.ts | 13 +- .../workflows/password-workflow.service.ts | 2 - .../workflows/signup-workflow.service.ts | 5 +- .../workflows/whmcs-link-workflow.service.ts | 5 +- .../auth/presentation/http/auth.controller.ts | 5 +- .../catalog/services/base-catalog.service.ts | 8 +- .../services/internet-catalog.service.ts | 1 - .../catalog/services/vpn-catalog.service.ts | 10 +- .../src/modules/currency/currency.module.ts | 3 +- .../modules/id-mappings/mappings.service.ts | 22 +- apps/bff/src/modules/invoices/index.ts | 5 +- .../modules/invoices/invoices.controller.ts | 17 +- .../src/modules/invoices/invoices.module.ts | 8 +- .../services/invoice-retrieval.service.ts | 19 +- .../services/invoices-orchestrator.service.ts | 20 +- .../types/invoice-monitoring.types.ts | 3 +- apps/bff/src/modules/orders/orders.module.ts | 2 +- .../orders/queue/provisioning.processor.ts | 2 +- .../orders/services/order-builder.service.ts | 20 +- .../order-fulfillment-error.service.ts | 2 +- .../order-fulfillment-orchestrator.service.ts | 21 +- .../order-fulfillment-validator.service.ts | 6 +- .../services/order-orchestrator.service.ts | 7 +- .../services/order-pricebook.service.ts | 2 - .../services/order-validator.service.ts | 2 +- .../services/payment-validator.service.ts | 19 +- .../subscriptions/sim-management.service.ts | 5 +- .../services/sim-orchestrator.service.ts | 5 +- .../services/sim-validation.service.ts | 8 +- .../subscriptions/subscriptions.service.ts | 5 +- .../bff/src/modules/users/users.controller.ts | 7 +- apps/bff/src/modules/users/users.service.ts | 33 +- apps/portal/next.config.mjs | 4 +- apps/portal/src/components/atoms/button.tsx | 2 + .../molecules/AsyncBlock/AsyncBlock.tsx | 6 +- .../molecules/FormField/FormField.tsx | 9 +- .../organisms/AppShell/AppShell.tsx | 7 +- .../organisms/AppShell/navigation.ts | 1 - .../account/components/AddressCard.tsx | 5 +- .../account/views/ProfileContainer.tsx | 8 +- .../auth/components/LoginForm/LoginForm.tsx | 3 +- .../PasswordResetForm/PasswordResetForm.tsx | 14 +- .../SetPasswordForm/SetPasswordForm.tsx | 6 +- .../components/SignupForm/AccountStep.tsx | 1 - .../auth/components/SignupForm/SignupForm.tsx | 8 +- .../src/features/auth/services/auth.store.ts | 2 +- .../BillingStatusBadge/BillingStatusBadge.tsx | 1 - .../BillingSummary/BillingSummary.tsx | 6 +- .../InvoiceDetail/InvoiceHeader.tsx | 19 +- .../components/InvoiceDetail/InvoiceItems.tsx | 78 ++-- .../InvoiceDetail/InvoiceSummaryBar.tsx | 48 +-- .../InvoiceDetail/InvoiceTotals.tsx | 2 +- .../components/InvoiceList/InvoiceList.tsx | 15 +- .../components/InvoiceTable/InvoiceTable.tsx | 49 +-- .../billing/components/PaymentMethodCard.tsx | 50 +-- .../PaymentMethodCard/PaymentMethodCard.tsx | 12 +- .../src/features/billing/hooks/useBilling.ts | 8 +- .../features/billing/views/InvoiceDetail.tsx | 2 +- .../features/billing/views/PaymentMethods.tsx | 41 +- .../components/base/AddressConfirmation.tsx | 11 +- .../components/base/EnhancedOrderSummary.tsx | 4 +- .../catalog/components/base/OrderSummary.tsx | 7 +- .../components/base/PricingDisplay.tsx | 1 - .../configure/steps/ReviewOrderStep.tsx | 7 +- .../components/sim/SimConfigureView.tsx | 3 +- .../catalog/hooks/useConfigureParams.ts | 24 +- .../catalog/hooks/useInternetConfigure.ts | 4 +- .../features/catalog/hooks/useSimConfigure.ts | 37 +- .../catalog/services/catalog.service.ts | 12 +- .../src/features/catalog/utils/pricing.ts | 1 - .../features/catalog/views/InternetPlans.tsx | 186 +++++---- .../features/checkout/hooks/useCheckout.ts | 22 +- .../components/UpcomingPaymentBanner.tsx | 2 +- .../dashboard/hooks/useDashboardSummary.ts | 5 +- .../orders/services/orders.service.ts | 5 +- .../src/features/orders/views/OrderDetail.tsx | 4 +- .../components/SimDetailsCard.tsx | 12 +- .../components/SubscriptionCard.tsx | 10 +- .../components/SubscriptionDetails.tsx | 2 +- .../services/sim-actions.service.ts | 4 +- .../views/SubscriptionDetail.tsx | 52 +-- .../subscriptions/views/SubscriptionsList.tsx | 6 +- apps/portal/src/lib/api/helpers.ts | 6 +- apps/portal/src/lib/api/index.ts | 8 +- apps/portal/src/lib/api/response-helpers.ts | 19 +- apps/portal/src/lib/api/runtime/client.ts | 9 +- apps/portal/src/lib/constants/countries.ts | 6 +- .../portal/src/lib/hooks/useFormatCurrency.ts | 2 +- apps/portal/src/lib/providers.tsx | 2 +- apps/portal/src/lib/utils/error-handling.ts | 2 +- 111 files changed, 1091 insertions(+), 823 deletions(-) create mode 100644 DOMAIN_VIOLATIONS_REPORT.md diff --git a/DOMAIN_VIOLATIONS_REPORT.md b/DOMAIN_VIOLATIONS_REPORT.md new file mode 100644 index 00000000..a2c435f6 --- /dev/null +++ b/DOMAIN_VIOLATIONS_REPORT.md @@ -0,0 +1,374 @@ +# Domain Violations Report + +**Customer Portal - BFF and Portal Domain Violations Analysis** + +--- + +## Executive Summary + +This report identifies cases where the BFF (Backend for Frontend) and Portal applications are not following the domain package as the single source of truth for types, validation, and business logic. The analysis reveals several categories of violations that need to be addressed to maintain clean architecture principles. + +## Architecture Overview + +The customer portal follows a clean architecture pattern with: +- **Domain Package** (`packages/domain/`): Single source of truth for types, schemas, and business logic +- **BFF** (`apps/bff/`): NestJS backend that should consume domain types +- **Portal** (`apps/portal/`): Next.js frontend that should consume domain types + +## Key Findings + +### ✅ **Good Practices Found** + +1. **Domain Package Structure**: Well-organized with clear separation of concerns +2. **Type Imports**: Most services correctly import domain types +3. **Validation Patterns**: Many components properly extend domain schemas +4. **Provider Adapters**: Clean separation between domain contracts and provider implementations + +### ❌ **Domain Violations Identified** + +## 1. BFF Layer Violations + +### 1.1 Duplicate Validation Schemas + +**Location**: `apps/bff/src/modules/orders/services/order-fulfillment-validator.service.ts` +```typescript +// ❌ VIOLATION: Duplicate validation schema +const salesforceAccountIdSchema = z.string().min(1, "Salesforce AccountId is required"); +``` + +**Issue**: Creating local Zod schemas instead of using domain validation +**Impact**: Duplication of validation logic, potential inconsistencies + +### 1.2 Infrastructure-Specific Types + +**Location**: `apps/bff/src/modules/orders/services/order-fulfillment-validator.service.ts` +```typescript +// ❌ VIOLATION: BFF-specific interface that could be domain type +export interface OrderFulfillmentValidationResult { + sfOrder: SalesforceOrderRecord; + clientId: number; + isAlreadyProvisioned: boolean; + whmcsOrderId?: string; +} +``` + +**Issue**: Business logic types defined in BFF instead of domain +**Impact**: Business logic scattered across layers + +### 1.3 Local Type Definitions + +**Location**: `apps/bff/src/modules/invoices/services/invoice-retrieval.service.ts` +```typescript +// ❌ VIOLATION: Local interface instead of domain type +interface UserMappingInfo { + userId: string; + whmcsClientId: number; +} +``` + +**Issue**: Infrastructure types that could be standardized in domain +**Impact**: Inconsistent type definitions across services + +### 1.4 Response Schema Definitions + +**Location**: `apps/bff/src/modules/orders/orders.controller.ts` +```typescript +// ❌ VIOLATION: Controller-specific response schema +private readonly createOrderResponseSchema = apiSuccessResponseSchema( + z.object({ + sfOrderId: z.string(), + status: z.string(), + message: z.string(), + }) +); +``` + +**Issue**: Response schemas defined in controllers instead of domain +**Impact**: API contract not centralized + +### 1.5 SOQL Utility Schemas + +**Location**: `apps/bff/src/integrations/salesforce/utils/soql.util.ts` +```typescript +// ❌ VIOLATION: Utility-specific schemas +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"); +``` + +**Issue**: Common validation patterns not centralized +**Impact**: Repeated validation logic + +## 2. Portal Layer Violations + +### 2.1 Component-Specific Types + +**Location**: `apps/portal/src/features/catalog/components/base/PricingDisplay.tsx` +```typescript +// ❌ VIOLATION: UI-specific types that could be domain types +export interface PricingTier { + name: string; + price: number; + billingCycle: "Monthly" | "Onetime" | "Annual"; + description?: string; + features?: string[]; + isRecommended?: boolean; + originalPrice?: number; +} +``` + +**Issue**: Business concepts defined in UI components +**Impact**: Business logic in presentation layer + +### 2.2 Checkout-Specific Types + +**Location**: `apps/portal/src/features/checkout/hooks/useCheckout.ts` +```typescript +// ❌ VIOLATION: Business logic types in UI layer +type CheckoutItemType = "plan" | "installation" | "addon" | "activation" | "vpn"; + +interface CheckoutItem extends CatalogProductBase { + quantity: number; + itemType: CheckoutItemType; + autoAdded?: boolean; +} + +interface CheckoutTotals { + monthlyTotal: number; + oneTimeTotal: number; +} +``` + +**Issue**: Business domain types defined in UI hooks +**Impact**: Business logic scattered across layers + +### 2.3 Catalog Utility Types + +**Location**: `apps/portal/src/features/catalog/utils/catalog.utils.ts` +```typescript +// ❌ VIOLATION: TODO comment indicates missing domain type +// TODO: Define CatalogFilter type properly +type CatalogFilter = { + category?: string; + priceMin?: number; + priceMax?: number; + search?: string; +}; +``` + +**Issue**: Explicitly acknowledged missing domain type +**Impact**: Business concepts not properly modeled + +### 2.4 Form Extension Patterns + +**Location**: `apps/portal/src/features/auth/components/SignupForm/SignupForm.tsx` +```typescript +// ⚠️ ACCEPTABLE: Extending domain schema with UI concerns +export const signupFormSchema = signupInputSchema + .extend({ + confirmPassword: z.string().min(1, "Please confirm your password"), + }) + .refine(data => data.acceptTerms === true, { + message: "You must accept the terms and conditions", + path: ["acceptTerms"], + }) +``` + +**Status**: This is actually a good pattern - extending domain schemas with UI-specific fields + +## 3. Cross-Cutting Concerns + +### 3.1 Environment Configuration + +**Location**: `apps/bff/src/core/config/env.validation.ts` +```typescript +// ⚠️ ACCEPTABLE: Infrastructure configuration +export const envSchema = z.object({ + NODE_ENV: z.enum(["development", "test", "production"]).default("development"), + // ... many environment variables +}); +``` + +**Status**: Environment configuration is appropriately infrastructure-specific + +### 3.2 Database Mapping Types + +**Location**: `apps/bff/src/infra/mappers/mapping.mapper.ts` +```typescript +// ✅ GOOD: Infrastructure mapping with clear documentation +export function mapPrismaMappingToDomain(mapping: PrismaIdMapping): UserIdMapping { + // Maps Prisma entity to Domain type +} +``` + +**Status**: Proper infrastructure-to-domain mapping + +## Recommendations + +### 1. Immediate Actions (High Priority) + +#### 1.1 Move Business Types to Domain + +**Action**: Move the following types to appropriate domain modules: + +```typescript +// Move to packages/domain/orders/ +export interface OrderFulfillmentValidationResult { + sfOrder: SalesforceOrderRecord; + clientId: number; + isAlreadyProvisioned: boolean; + whmcsOrderId?: string; +} + +// Move to packages/domain/catalog/ +export interface PricingTier { + name: string; + price: number; + billingCycle: "Monthly" | "Onetime" | "Annual"; + description?: string; + features?: string[]; + isRecommended?: boolean; + originalPrice?: number; +} + +// Move to packages/domain/orders/ +export interface CheckoutItem { + // Define based on CatalogProductBase + quantity + itemType +} +``` + +#### 1.2 Centralize Validation Schemas + +**Action**: Move common validation patterns to domain: + +```typescript +// Add to packages/domain/common/schema.ts +export const salesforceAccountIdSchema = z.string().min(1, "Salesforce AccountId is required"); +export const nonEmptyStringSchema = z.string().min(1, "Value cannot be empty").trim(); +export const soqlFieldNameSchema = z.string().trim().regex(/^[A-Za-z0-9_.]+$/, "Invalid SOQL field name"); +``` + +#### 1.3 Define Missing Domain Types + +**Action**: Complete the TODO items and define missing types: + +```typescript +// Add to packages/domain/catalog/ +export interface CatalogFilter { + category?: string; + priceMin?: number; + priceMax?: number; + search?: string; +} + +// Add to packages/domain/orders/ +export interface OrderCreateResponse { + sfOrderId: string; + status: string; + message: string; +} +``` + +### 2. Medium Priority Actions + +#### 2.1 Standardize Response Types + +**Action**: Create standardized API response types in domain: + +```typescript +// Add to packages/domain/common/ +export interface OrderCreateResponse { + sfOrderId: string; + status: string; + message: string; +} + +export const orderCreateResponseSchema = z.object({ + sfOrderId: z.string(), + status: z.string(), + message: z.string(), +}); +``` + +#### 2.2 Refactor Checkout Logic + +**Action**: Move checkout business logic to domain: + +```typescript +// Add to packages/domain/orders/ +export interface CheckoutCart { + items: CheckoutItem[]; + totals: CheckoutTotals; + configuration: OrderConfigurations; +} + +export function calculateCheckoutTotals(items: CheckoutItem[]): CheckoutTotals { + // Move calculation logic from Portal to Domain +} +``` + +### 3. Long-term Improvements + +#### 3.1 Domain Service Layer + +**Action**: Consider creating domain services for complex business logic: + +```typescript +// Add to packages/domain/orders/services/ +export class OrderValidationService { + static validateFulfillmentRequest( + sfOrderId: string, + idempotencyKey: string + ): Promise { + // Move validation logic from BFF to Domain + } +} +``` + +#### 3.2 Type Safety Improvements + +**Action**: Enhance type safety across layers: + +```typescript +// Ensure all domain types are properly exported +// Add runtime validation for all domain types +// Implement proper error handling with domain error types +``` + +## Implementation Plan + +### Phase 1: Critical Violations (Week 1-2) +1. Move `OrderFulfillmentValidationResult` to domain +2. Move `PricingTier` to domain +3. Centralize validation schemas +4. Define missing `CatalogFilter` type + +### Phase 2: Business Logic Consolidation (Week 3-4) +1. Move checkout types to domain +2. Standardize API response types +3. Refactor validation services +4. Update imports across applications + +### Phase 3: Architecture Improvements (Week 5-6) +1. Create domain service layer +2. Implement proper error handling +3. Add comprehensive type exports +4. Update documentation + +## Success Metrics + +- **Type Reuse**: 90%+ of business types defined in domain +- **Validation Consistency**: Single validation schema per business rule +- **Import Clarity**: Clear domain imports in BFF and Portal +- **Documentation**: Updated architecture documentation + +## Conclusion + +The analysis reveals that while the domain package is well-structured, there are several violations where business logic and types are scattered across the BFF and Portal layers. The recommended actions will help establish the domain package as the true single source of truth, improving maintainability and consistency across the application. + +The violations are primarily in business logic types and validation schemas that should be centralized in the domain layer. The form extension patterns in the Portal are actually good examples of how to properly extend domain schemas with UI-specific concerns. + +--- + +**Report Generated**: $(date) +**Analysis Scope**: BFF and Portal applications +**Domain Package**: `packages/domain/` diff --git a/apps/bff/src/infra/mappers/index.ts b/apps/bff/src/infra/mappers/index.ts index 88d5fe56..dfb41495 100644 --- a/apps/bff/src/infra/mappers/index.ts +++ b/apps/bff/src/infra/mappers/index.ts @@ -1,14 +1,13 @@ /** * Centralized DB Mappers - * + * * All mappers that transform Prisma entities to Domain types - * + * * Pattern: * - mapPrisma{Entity}ToDomain() - converts Prisma type to domain type * - These are infrastructure concerns (Prisma is BFF's implementation detail) * - Domain layer should never import these */ -export * from './user.mapper'; -export * from './mapping.mapper'; - +export * from "./user.mapper"; +export * from "./mapping.mapper"; diff --git a/apps/bff/src/infra/mappers/mapping.mapper.ts b/apps/bff/src/infra/mappers/mapping.mapper.ts index 0da65d2b..12fa7d5b 100644 --- a/apps/bff/src/infra/mappers/mapping.mapper.ts +++ b/apps/bff/src/infra/mappers/mapping.mapper.ts @@ -1,8 +1,8 @@ /** * ID Mapping DB Mapper - * + * * Maps Prisma IdMapping entity to Domain UserIdMapping type - * + * * NOTE: This is an infrastructure concern - Prisma is BFF's ORM implementation detail. * Domain layer should not know about Prisma types. */ @@ -23,4 +23,3 @@ export function mapPrismaMappingToDomain(mapping: PrismaIdMapping): UserIdMappin updatedAt: mapping.updatedAt, }; } - diff --git a/apps/bff/src/infra/mappers/user.mapper.ts b/apps/bff/src/infra/mappers/user.mapper.ts index 590ceaf9..4a2f6ba6 100644 --- a/apps/bff/src/infra/mappers/user.mapper.ts +++ b/apps/bff/src/infra/mappers/user.mapper.ts @@ -1,8 +1,8 @@ /** * User DB Mapper - * + * * Adapts @prisma/client User to domain UserAuth type - * + * * NOTE: This is an infrastructure adapter - Prisma is BFF's ORM implementation detail. * The domain provider handles the actual mapping logic. */ @@ -15,10 +15,10 @@ type PrismaUserRaw = Parameters( - ep, - request - ); + response = await this.client.makeAuthenticatedRequest< + FreebitAccountDetailsRaw, + typeof request + >(ep, request); break; } catch (err: unknown) { lastError = err; @@ -116,7 +116,9 @@ export class FreebitOperationsService { */ async getSimUsage(account: string): Promise { try { - const request: FreebitTrafficInfoRequest = FreebitProvider.schemas.trafficInfo.parse({ account }); + const request: FreebitTrafficInfoRequest = FreebitProvider.schemas.trafficInfo.parse({ + account, + }); const response = await this.client.makeAuthenticatedRequest< FreebitTrafficInfoRaw, @@ -143,7 +145,11 @@ export class FreebitOperationsService { options: { campaignCode?: string; expiryDate?: string; scheduledAt?: string } = {} ): Promise { try { - const payload: FreebitTopUpRequest = FreebitProvider.schemas.topUp.parse({ account, quotaMb, options }); + const payload: FreebitTopUpRequest = FreebitProvider.schemas.topUp.parse({ + account, + quotaMb, + options, + }); const quotaKb = Math.round(payload.quotaMb * 1024); const baseRequest = { account: payload.account, @@ -158,10 +164,7 @@ export class FreebitOperationsService { ? { ...baseRequest, runTime: payload.options?.scheduledAt } : baseRequest; - await this.client.makeAuthenticatedRequest( - endpoint, - request - ); + await this.client.makeAuthenticatedRequest(endpoint, request); this.logger.log(`Successfully topped up ${quotaMb}MB for account ${account}`, { account, @@ -238,17 +241,20 @@ export class FreebitOperationsService { runTime: parsed.scheduledAt, }; - const response = await this.client.makeAuthenticatedRequest( - "/mvno/changePlan/", - request - ); + const response = await this.client.makeAuthenticatedRequest< + PlanChangeResponse, + typeof request + >("/mvno/changePlan/", request); - this.logger.log(`Successfully changed plan for account ${parsed.account} to ${parsed.newPlanCode}`, { - account: parsed.account, - newPlanCode: parsed.newPlanCode, - assignGlobalIp: parsed.assignGlobalIp, - scheduled: Boolean(parsed.scheduledAt), - }); + this.logger.log( + `Successfully changed plan for account ${parsed.account} to ${parsed.newPlanCode}`, + { + account: parsed.account, + newPlanCode: parsed.newPlanCode, + assignGlobalIp: parsed.assignGlobalIp, + scheduled: Boolean(parsed.scheduledAt), + } + ); return { ipv4: response.ipv4, @@ -426,17 +432,20 @@ export class FreebitOperationsService { authKey: await this.auth.getAuthKey(), }; - await this.client.makeAuthenticatedRequest( - "/mvno/esim/addAcnt/", - payload - ); + await this.client.makeAuthenticatedRequest< + EsimAddAccountResponse, + FreebitEsimAddAccountRequest + >("/mvno/esim/addAcnt/", payload); - this.logger.log(`Successfully reissued eSIM profile via addAcnt for account ${parsed.account}`, { - account: parsed.account, - newEid: parsed.newEid, - oldProductNumber: parsed.oldProductNumber, - oldEid: parsed.oldEid, - }); + this.logger.log( + `Successfully reissued eSIM profile via addAcnt for account ${parsed.account}`, + { + account: parsed.account, + newEid: parsed.newEid, + oldProductNumber: parsed.oldProductNumber, + oldEid: parsed.oldEid, + } + ); } catch (error) { const message = getErrorMessage(error); this.logger.error(`Failed to reissue eSIM profile via addAcnt for account ${account}`, { @@ -480,16 +489,17 @@ export class FreebitOperationsService { } = params; // Import schemas dynamically to avoid circular dependencies - const validatedParams: FreebitEsimActivationParams = FreebitProvider.schemas.esimActivationParams.parse({ - account, - eid, - planCode, - contractLine, - aladinOperated, - shipDate, - mnp, - identity, - }); + const validatedParams: FreebitEsimActivationParams = + FreebitProvider.schemas.esimActivationParams.parse({ + account, + eid, + planCode, + contractLine, + aladinOperated, + shipDate, + mnp, + identity, + }); if (!validatedParams.account || !validatedParams.eid) { throw new BadRequestException("activateEsimAccountNew requires account and eid"); diff --git a/apps/bff/src/integrations/freebit/services/freebit-orchestrator.service.ts b/apps/bff/src/integrations/freebit/services/freebit-orchestrator.service.ts index ae918071..20dbe3ee 100644 --- a/apps/bff/src/integrations/freebit/services/freebit-orchestrator.service.ts +++ b/apps/bff/src/integrations/freebit/services/freebit-orchestrator.service.ts @@ -5,9 +5,7 @@ import type { SimDetails, SimUsage, SimTopUpHistory } from "@customer-portal/dom @Injectable() export class FreebitOrchestratorService { - constructor( - private readonly operations: FreebitOperationsService - ) {} + constructor(private readonly operations: FreebitOperationsService) {} /** * Get SIM account details diff --git a/apps/bff/src/integrations/salesforce/salesforce.service.ts b/apps/bff/src/integrations/salesforce/salesforce.service.ts index e2288626..f34979cc 100644 --- a/apps/bff/src/integrations/salesforce/salesforce.service.ts +++ b/apps/bff/src/integrations/salesforce/salesforce.service.ts @@ -12,11 +12,11 @@ import type { SalesforceOrderRecord } from "@customer-portal/domain/orders"; * Account Methods (Actually Used): * - findAccountByCustomerNumber() - Used in signup/WHMCS linking workflows * - getAccountDetails() - Used in signup to check WH_Account__c field - * + * * Order Methods: * - updateOrder() - Used in order provisioning * - getOrder() - Used to fetch order details - * + * * Note: Internet Eligibility checking happens in internet-catalog.service.ts */ @Injectable() diff --git a/apps/bff/src/integrations/salesforce/services/salesforce-account.service.ts b/apps/bff/src/integrations/salesforce/services/salesforce-account.service.ts index d81e96f8..51d45717 100644 --- a/apps/bff/src/integrations/salesforce/services/salesforce-account.service.ts +++ b/apps/bff/src/integrations/salesforce/services/salesforce-account.service.ts @@ -8,7 +8,7 @@ import { customerNumberSchema, salesforceIdSchema } from "@customer-portal/domai /** * Salesforce Account Service - * + * * Only contains methods that are actually used in the codebase: * - findByCustomerNumber() - Used in signup/WHMCS linking workflows * - getAccountDetails() - Used in signup to check WH_Account__c field diff --git a/apps/bff/src/integrations/salesforce/services/salesforce-order.service.ts b/apps/bff/src/integrations/salesforce/services/salesforce-order.service.ts index 7efd3e3f..dc6eb223 100644 --- a/apps/bff/src/integrations/salesforce/services/salesforce-order.service.ts +++ b/apps/bff/src/integrations/salesforce/services/salesforce-order.service.ts @@ -55,10 +55,9 @@ export class SalesforceOrderService { const orderItemProduct2Fields = buildOrderItemProduct2Fields().map( f => `PricebookEntry.Product2.${f}` ); - const orderItemSelect = [ - ...buildOrderItemSelectFields(), - ...orderItemProduct2Fields, - ].join(", "); + const orderItemSelect = [...buildOrderItemSelectFields(), ...orderItemProduct2Fields].join( + ", " + ); const orderSoql = ` SELECT ${orderQueryFields} @@ -96,10 +95,7 @@ export class SalesforceOrderService { ); // Use domain mapper - single transformation! - return OrderProviders.Salesforce.transformSalesforceOrderDetails( - order, - orderItems - ); + return OrderProviders.Salesforce.transformSalesforceOrderDetails(order, orderItems); } catch (error: unknown) { this.logger.error("Failed to fetch order with items", { error: getErrorMessage(error), @@ -140,10 +136,9 @@ export class SalesforceOrderService { const orderItemProduct2Fields = buildOrderItemProduct2Fields().map( f => `PricebookEntry.Product2.${f}` ); - const orderItemSelect = [ - ...buildOrderItemSelectFields(), - ...orderItemProduct2Fields, - ].join(", "); + const orderItemSelect = [...buildOrderItemSelectFields(), ...orderItemProduct2Fields].join( + ", " + ); const ordersSoql = ` SELECT ${orderQueryFields} @@ -211,8 +206,8 @@ export class SalesforceOrderService { // Use domain mapper for each order - single transformation! return orders - .filter((order): order is SalesforceOrderRecord & { Id: string } => - typeof order.Id === "string" + .filter( + (order): order is SalesforceOrderRecord & { Id: string } => typeof order.Id === "string" ) .map(order => OrderProviders.Salesforce.transformSalesforceOrderSummary( @@ -229,4 +224,3 @@ export class SalesforceOrderService { } } } - diff --git a/apps/bff/src/integrations/salesforce/utils/catalog-query-builder.ts b/apps/bff/src/integrations/salesforce/utils/catalog-query-builder.ts index 091a3c55..0ab98c2f 100644 --- a/apps/bff/src/integrations/salesforce/utils/catalog-query-builder.ts +++ b/apps/bff/src/integrations/salesforce/utils/catalog-query-builder.ts @@ -17,17 +17,8 @@ export function buildProductQuery( additionalFields: string[] = [], additionalConditions: string = "" ): string { - const categoryField = assertSoqlFieldName( - portalCategoryField, - "PRODUCT_PORTAL_CATEGORY_FIELD" - ); - const baseFields = [ - "Id", - "Name", - "StockKeepingUnit", - categoryField, - "Item_Class__c", - ]; + const categoryField = assertSoqlFieldName(portalCategoryField, "PRODUCT_PORTAL_CATEGORY_FIELD"); + const baseFields = ["Id", "Name", "StockKeepingUnit", categoryField, "Item_Class__c"]; const allFields = [...baseFields, ...additionalFields].join(", "); const safeCategory = sanitizeSoqlLiteral(category); diff --git a/apps/bff/src/integrations/salesforce/utils/order-query-builder.ts b/apps/bff/src/integrations/salesforce/utils/order-query-builder.ts index 9376f44a..5c57fc3c 100644 --- a/apps/bff/src/integrations/salesforce/utils/order-query-builder.ts +++ b/apps/bff/src/integrations/salesforce/utils/order-query-builder.ts @@ -10,9 +10,7 @@ const UNIQUE = (values: T[]): T[] => Array.from(new Set(values)); /** * Build field list for Order queries */ -export function buildOrderSelectFields( - additional: string[] = [] -): string[] { +export function buildOrderSelectFields(additional: string[] = []): string[] { const fields = [ "Id", "AccountId", @@ -50,9 +48,7 @@ export function buildOrderSelectFields( /** * Build field list for OrderItem queries */ -export function buildOrderItemSelectFields( - additional: string[] = [] -): string[] { +export function buildOrderItemSelectFields(additional: string[] = []): string[] { const fields = [ "Id", "OrderId", @@ -69,9 +65,7 @@ export function buildOrderItemSelectFields( /** * Build field list for Product2 fields within OrderItem queries */ -export function buildOrderItemProduct2Fields( - additional: string[] = [] -): string[] { +export function buildOrderItemProduct2Fields(additional: string[] = []): string[] { const fields = [ "Id", "Name", diff --git a/apps/bff/src/integrations/whmcs/connection/services/whmcs-api-methods.service.ts b/apps/bff/src/integrations/whmcs/connection/services/whmcs-api-methods.service.ts index b8b7676c..77c9317c 100644 --- a/apps/bff/src/integrations/whmcs/connection/services/whmcs-api-methods.service.ts +++ b/apps/bff/src/integrations/whmcs/connection/services/whmcs-api-methods.service.ts @@ -25,16 +25,12 @@ import type { WhmcsValidateLoginResponse, WhmcsSsoResponse, } from "@customer-portal/domain/customer"; -import type { - WhmcsProductListResponse, -} from "@customer-portal/domain/subscriptions"; +import type { WhmcsProductListResponse } from "@customer-portal/domain/subscriptions"; import type { WhmcsPaymentMethodListResponse, WhmcsPaymentGatewayListResponse, } from "@customer-portal/domain/payments"; -import type { - WhmcsCatalogProductListResponse, -} from "@customer-portal/domain/catalog"; +import type { WhmcsCatalogProductListResponse } from "@customer-portal/domain/catalog"; import { WhmcsHttpClientService } from "./whmcs-http-client.service"; import { WhmcsConfigService } from "../config/whmcs-config.service"; import type { WhmcsRequestOptions } from "../types/connection.types"; @@ -132,7 +128,9 @@ export class WhmcsApiMethodsService { // PRODUCT/SUBSCRIPTION API METHODS // ========================================== - async getClientsProducts(params: WhmcsGetClientsProductsParams): Promise { + async getClientsProducts( + params: WhmcsGetClientsProductsParams + ): Promise { return this.makeRequest("GetClientsProducts", params); } @@ -144,7 +142,9 @@ export class WhmcsApiMethodsService { // PAYMENT API METHODS // ========================================== - async getPaymentMethods(params: WhmcsGetPayMethodsParams): Promise { + async getPaymentMethods( + params: WhmcsGetPayMethodsParams + ): Promise { return this.makeRequest("GetPayMethods", params); } diff --git a/apps/bff/src/integrations/whmcs/services/whmcs-client.service.ts b/apps/bff/src/integrations/whmcs/services/whmcs-client.service.ts index c29e3f92..99b1f830 100644 --- a/apps/bff/src/integrations/whmcs/services/whmcs-client.service.ts +++ b/apps/bff/src/integrations/whmcs/services/whmcs-client.service.ts @@ -7,8 +7,6 @@ import type { WhmcsValidateLoginParams, WhmcsAddClientParams, WhmcsClientResponse, -} from "@customer-portal/domain/customer"; -import type { WhmcsAddClientResponse, WhmcsValidateLoginResponse, } from "@customer-portal/domain/customer"; @@ -38,7 +36,8 @@ export class WhmcsClientService { password2: password, }; - const response = await this.connectionService.validateLogin(params); + const response: WhmcsValidateLoginResponse = + await this.connectionService.validateLogin(params); this.logger.log(`Validated login for email: ${email}`); return { @@ -146,7 +145,7 @@ export class WhmcsClientService { */ async addClient(clientData: WhmcsAddClientParams): Promise<{ clientId: number }> { try { - const response = await this.connectionService.addClient(clientData); + const response: WhmcsAddClientResponse = await this.connectionService.addClient(clientData); this.logger.log(`Created new client: ${response.clientid}`); return { clientId: response.clientid }; diff --git a/apps/bff/src/integrations/whmcs/services/whmcs-currency.service.ts b/apps/bff/src/integrations/whmcs/services/whmcs-currency.service.ts index 36354534..2607d11c 100644 --- a/apps/bff/src/integrations/whmcs/services/whmcs-currency.service.ts +++ b/apps/bff/src/integrations/whmcs/services/whmcs-currency.service.ts @@ -19,7 +19,7 @@ export class WhmcsCurrencyService implements OnModuleInit { // Check if WHMCS is available before trying to load currencies this.logger.debug("Checking WHMCS availability before loading currencies"); const isAvailable = await this.connectionService.isAvailable(); - + if (!isAvailable) { this.logger.warn("WHMCS service is not available, using fallback currency configuration"); this.setFallbackCurrency(); @@ -49,9 +49,9 @@ export class WhmcsCurrencyService implements OnModuleInit { format: "1", rate: "1.00000", }; - + this.currencies = [this.defaultCurrency]; - + this.logger.log("Using fallback currency configuration", { defaultCurrency: this.defaultCurrency.code, }); @@ -94,13 +94,13 @@ export class WhmcsCurrencyService implements OnModuleInit { try { // The connection service returns the raw WHMCS API response data // (the WhmcsResponse wrapper is unwrapped by the API methods service) - const response = await this.connectionService.getCurrencies() as WhmcsCurrenciesResponse; + const response = (await this.connectionService.getCurrencies()) as WhmcsCurrenciesResponse; // Check if response has currencies data (success case) or error fields if (response.result === "success" || (response.currencies && !response.error)) { // Parse the WHMCS response format into currency objects this.currencies = this.parseWhmcsCurrenciesResponse(response); - + if (this.currencies.length > 0) { // Set first currency as default (WHMCS typically returns the primary currency first) this.defaultCurrency = this.currencies[0]; @@ -120,7 +120,9 @@ export class WhmcsCurrencyService implements OnModuleInit { errorcode: response?.errorcode, fullResponse: JSON.stringify(response, null, 2), }); - throw new Error(`WHMCS GetCurrencies error: ${response?.message || response?.error || "Unknown error"}`); + throw new Error( + `WHMCS GetCurrencies error: ${response?.message || response?.error || "Unknown error"}` + ); } } catch (error) { this.logger.error("Failed to load currencies from WHMCS", { @@ -136,21 +138,25 @@ export class WhmcsCurrencyService implements OnModuleInit { */ private parseWhmcsCurrenciesResponse(response: WhmcsCurrenciesResponse): WhmcsCurrency[] { const currencies: WhmcsCurrency[] = []; - + // Check if response has nested currency structure - if (response.currencies && typeof response.currencies === 'object' && 'currency' in response.currencies) { - const currencyArray = Array.isArray(response.currencies.currency) - ? response.currencies.currency + if ( + response.currencies && + typeof response.currencies === "object" && + "currency" in response.currencies + ) { + const currencyArray = Array.isArray(response.currencies.currency) + ? response.currencies.currency : [response.currencies.currency]; - + for (const currencyData of currencyArray) { const currency: WhmcsCurrency = { id: parseInt(String(currencyData.id)) || 0, - code: String(currencyData.code || ''), - prefix: String(currencyData.prefix || ''), - suffix: String(currencyData.suffix || ''), - format: String(currencyData.format || '1'), - rate: String(currencyData.rate || '1.00000'), + code: String(currencyData.code || ""), + prefix: String(currencyData.prefix || ""), + suffix: String(currencyData.suffix || ""), + format: String(currencyData.format || "1"), + rate: String(currencyData.rate || "1.00000"), }; // Validate that we have essential currency data @@ -160,8 +166,8 @@ export class WhmcsCurrencyService implements OnModuleInit { } } else { // Fallback: try to parse flat format (currencies[currency][0][id], etc.) - const currencyKeys = Object.keys(response).filter(key => - key.startsWith('currencies[currency][') && key.includes('][id]') + const currencyKeys = Object.keys(response).filter( + key => key.startsWith("currencies[currency][") && key.includes("][id]") ); // Extract currency indices @@ -176,11 +182,11 @@ export class WhmcsCurrencyService implements OnModuleInit { for (const index of currencyIndices) { const currency: WhmcsCurrency = { id: parseInt(String(response[`currencies[currency][${index}][id]`])) || 0, - code: String(response[`currencies[currency][${index}][code]`] || ''), - prefix: String(response[`currencies[currency][${index}][prefix]`] || ''), - suffix: String(response[`currencies[currency][${index}][suffix]`] || ''), - format: String(response[`currencies[currency][${index}][format]`] || '1'), - rate: String(response[`currencies[currency][${index}][rate]`] || '1.00000'), + code: String(response[`currencies[currency][${index}][code]`] || ""), + prefix: String(response[`currencies[currency][${index}][prefix]`] || ""), + suffix: String(response[`currencies[currency][${index}][suffix]`] || ""), + format: String(response[`currencies[currency][${index}][format]`] || "1"), + rate: String(response[`currencies[currency][${index}][rate]`] || "1.00000"), }; // Validate that we have essential currency data diff --git a/apps/bff/src/integrations/whmcs/services/whmcs-invoice.service.ts b/apps/bff/src/integrations/whmcs/services/whmcs-invoice.service.ts index a055245b..2120b708 100644 --- a/apps/bff/src/integrations/whmcs/services/whmcs-invoice.service.ts +++ b/apps/bff/src/integrations/whmcs/services/whmcs-invoice.service.ts @@ -1,7 +1,13 @@ import { getErrorMessage } from "@bff/core/utils/error.util"; import { Logger } from "nestjs-pino"; import { Injectable, NotFoundException, Inject } from "@nestjs/common"; -import { Invoice, InvoiceList, invoiceListSchema, invoiceSchema, Providers } from "@customer-portal/domain/billing"; +import { + Invoice, + InvoiceList, + invoiceListSchema, + invoiceSchema, + Providers, +} from "@customer-portal/domain/billing"; import { WhmcsConnectionOrchestratorService } from "../connection/services/whmcs-connection-orchestrator.service"; import { WhmcsCurrencyService } from "./whmcs-currency.service"; import { WhmcsCacheService } from "../cache/whmcs-cache.service"; @@ -65,7 +71,7 @@ export class WhmcsInvoiceService { ...(status && { status: status as WhmcsGetInvoicesParams["status"] }), }; - const response = await this.connectionService.getInvoices(params); + const response: WhmcsInvoiceListResponse = await this.connectionService.getInvoices(params); const transformed = this.transformInvoicesResponse(response, clientId, page, limit); const result = invoiceListSchema.parse(transformed as unknown); @@ -148,7 +154,7 @@ export class WhmcsInvoiceService { } // Fetch from WHMCS API - const response = await this.connectionService.getInvoice(invoiceId); + const response: WhmcsInvoiceResponse = await this.connectionService.getInvoice(invoiceId); if (!response.invoiceid) { throw new NotFoundException(`Invoice ${invoiceId} not found`); @@ -291,7 +297,8 @@ export class WhmcsInvoiceService { itemtaxed1: false, // No tax for data top-ups for now }; - const response = await this.connectionService.createInvoice(whmcsParams); + const response: WhmcsCreateInvoiceResponse = + await this.connectionService.createInvoice(whmcsParams); if (response.result !== "success") { throw new Error(`WHMCS invoice creation failed: ${response.message}`); @@ -350,7 +357,8 @@ export class WhmcsInvoiceService { notes: params.notes, }; - const response = await this.connectionService.updateInvoice(whmcsParams); + const response: WhmcsUpdateInvoiceResponse = + await this.connectionService.updateInvoice(whmcsParams); if (response.result !== "success") { throw new Error(`WHMCS invoice update failed: ${response.message}`); @@ -388,7 +396,8 @@ export class WhmcsInvoiceService { invoiceid: params.invoiceId, }; - const response = await this.connectionService.capturePayment(whmcsParams); + const response: WhmcsCapturePaymentResponse = + await this.connectionService.capturePayment(whmcsParams); if (response.result === "success") { this.logger.log(`Successfully captured payment for invoice ${params.invoiceId}`, { diff --git a/apps/bff/src/integrations/whmcs/services/whmcs-order.service.ts b/apps/bff/src/integrations/whmcs/services/whmcs-order.service.ts index c385ef54..0523b68d 100644 --- a/apps/bff/src/integrations/whmcs/services/whmcs-order.service.ts +++ b/apps/bff/src/integrations/whmcs/services/whmcs-order.service.ts @@ -3,10 +3,7 @@ import { Logger } from "nestjs-pino"; import { WhmcsConnectionOrchestratorService } from "../connection/services/whmcs-connection-orchestrator.service"; import { getErrorMessage } from "@bff/core/utils/error.util"; -import type { - WhmcsOrderItem, - WhmcsAddOrderParams, -} from "@customer-portal/domain/orders"; +import type { WhmcsOrderItem, WhmcsAddOrderParams } from "@customer-portal/domain/orders"; import { Providers } from "@customer-portal/domain/orders"; export type { WhmcsOrderItem, WhmcsAddOrderParams }; @@ -180,7 +177,7 @@ export class WhmcsOrderService { /** * Build WHMCS AddOrder payload from our parameters * Following official WHMCS API documentation format - * + * * Delegates to shared mapper function from integration package */ private buildAddOrderPayload(params: WhmcsAddOrderParams): Record { diff --git a/apps/bff/src/integrations/whmcs/services/whmcs-payment.service.ts b/apps/bff/src/integrations/whmcs/services/whmcs-payment.service.ts index 9ecd7a98..5647d957 100644 --- a/apps/bff/src/integrations/whmcs/services/whmcs-payment.service.ts +++ b/apps/bff/src/integrations/whmcs/services/whmcs-payment.service.ts @@ -46,9 +46,11 @@ export class WhmcsPaymentService { } // Fetch pay methods (use the documented WHMCS structure) - const response: WhmcsPaymentMethodListResponse = await this.connectionService.getPaymentMethods({ + const params: WhmcsGetPayMethodsParams = { clientid: clientId, - }); + }; + const response: WhmcsPaymentMethodListResponse = + await this.connectionService.getPaymentMethods(params); const paymentMethodsArray: WhmcsPaymentMethod[] = Array.isArray(response.paymethods) ? response.paymethods @@ -117,7 +119,8 @@ export class WhmcsPaymentService { } // Fetch from WHMCS API - const response = await this.connectionService.getPaymentGateways(); + const response: WhmcsPaymentGatewayListResponse = + await this.connectionService.getPaymentGateways(); if (!response.gateways?.gateway) { this.logger.warn("No payment gateways found"); @@ -129,7 +132,7 @@ export class WhmcsPaymentService { // Transform payment gateways const gateways = response.gateways.gateway - .map(whmcsGateway => { + .map((whmcsGateway: WhmcsPaymentGateway) => { try { return Providers.Whmcs.transformWhmcsPaymentGateway(whmcsGateway); } catch (error) { diff --git a/apps/bff/src/integrations/whmcs/services/whmcs-sso.service.ts b/apps/bff/src/integrations/whmcs/services/whmcs-sso.service.ts index 8d2bfaa2..03c328d3 100644 --- a/apps/bff/src/integrations/whmcs/services/whmcs-sso.service.ts +++ b/apps/bff/src/integrations/whmcs/services/whmcs-sso.service.ts @@ -27,7 +27,7 @@ export class WhmcsSsoService { ...(ssoRedirectPath && { sso_redirect_path: ssoRedirectPath }), }; - const response = await this.connectionService.createSsoToken(params); + const response: WhmcsSsoResponse = await this.connectionService.createSsoToken(params); const url = this.resolveRedirectUrl(response.redirect_url); this.debugLogRedirectHost(url); @@ -83,7 +83,7 @@ export class WhmcsSsoService { sso_redirect_path: path, }; - const response = await this.connectionService.createSsoToken(params); + const response: WhmcsSsoResponse = await this.connectionService.createSsoToken(params); // Return the 60s, one-time URL (resolved to absolute) const url = this.resolveRedirectUrl(response.redirect_url); @@ -104,7 +104,7 @@ export class WhmcsSsoService { destination: adminPath || "clientarea.php", }; - const response = await this.connectionService.createSsoToken(params); + const response: WhmcsSsoResponse = await this.connectionService.createSsoToken(params); const url = this.resolveRedirectUrl(response.redirect_url); this.debugLogRedirectHost(url); @@ -159,7 +159,9 @@ export class WhmcsSsoService { sso_redirect_path: modulePath, }; - const response = await this.connectionService.createSsoToken(ssoParams); + const response: WhmcsSsoResponse = await this.connectionService.createSsoToken( + ssoParams + ); const url = this.resolveRedirectUrl(response.redirect_url); this.debugLogRedirectHost(url); diff --git a/apps/bff/src/integrations/whmcs/utils/whmcs-client.utils.ts b/apps/bff/src/integrations/whmcs/utils/whmcs-client.utils.ts index eb9acefd..03bc4d5a 100644 --- a/apps/bff/src/integrations/whmcs/utils/whmcs-client.utils.ts +++ b/apps/bff/src/integrations/whmcs/utils/whmcs-client.utils.ts @@ -59,9 +59,7 @@ const isRecordOfStrings = (value: unknown): value is Record => !Array.isArray(value) && Object.values(value).every(v => typeof v === "string"); -const normalizeCustomFields = ( - raw: RawCustomFields -): Record | undefined => { +const normalizeCustomFields = (raw: RawCustomFields): Record | undefined => { if (!raw) return undefined; if (Array.isArray(raw)) { @@ -69,10 +67,7 @@ const normalizeCustomFields = ( if (!field) return acc; const idKey = toOptionalString(field.id)?.trim(); const nameKey = toOptionalString(field.name)?.trim(); - const value = - field.value === undefined || field.value === null - ? "" - : String(field.value); + const value = field.value === undefined || field.value === null ? "" : String(field.value); if (idKey) acc[idKey] = value; if (nameKey) acc[nameKey] = value; @@ -84,15 +79,12 @@ const normalizeCustomFields = ( if (isRecordOfStrings(raw)) { const stringRecord = raw as Record; - const map = Object.entries(stringRecord).reduce>( - (acc, [key, value]) => { - const trimmedKey = key.trim(); - if (!trimmedKey) return acc; - acc[trimmedKey] = value; - return acc; - }, - {} - ); + const map = Object.entries(stringRecord).reduce>((acc, [key, value]) => { + const trimmedKey = key.trim(); + if (!trimmedKey) return acc; + acc[trimmedKey] = value; + return acc; + }, {}); return Object.keys(map).length ? map : undefined; } @@ -177,9 +169,7 @@ export interface NormalizedWhmcsClient raw: WhmcsClient; } -export const deriveAddressFromClient = ( - client: WhmcsClient -): Address | undefined => { +export const deriveAddressFromClient = (client: WhmcsClient): Address | undefined => { const address = addressSchema.parse({ address1: client.address1 ?? null, address2: client.address2 ?? null, @@ -200,9 +190,7 @@ export const deriveAddressFromClient = ( return hasValues ? address : undefined; }; -export const normalizeWhmcsClient = ( - rawClient: WhmcsClient -): NormalizedWhmcsClient => { +export const normalizeWhmcsClient = (rawClient: WhmcsClient): NormalizedWhmcsClient => { const id = toNumber(rawClient.id); if (id === null) { throw new Error("WHMCS client ID missing or invalid."); @@ -222,8 +210,7 @@ export const normalizeWhmcsClient = ( users: normalizeUsers(rawClient.users), allowSingleSignOn: toNullableBoolean(rawClient.allowSingleSignOn) ?? null, email_verified: toNullableBoolean(rawClient.email_verified) ?? null, - marketing_emails_opt_in: - toNullableBoolean(rawClient.marketing_emails_opt_in) ?? null, + marketing_emails_opt_in: toNullableBoolean(rawClient.marketing_emails_opt_in) ?? null, defaultpaymethodid: toNumber(rawClient.defaultpaymethodid), currency: toNumber(rawClient.currency), address: deriveAddressFromClient(rawClient), @@ -254,10 +241,7 @@ export const getCustomFieldValue = ( return undefined; }; -export const buildUserProfile = ( - userAuth: UserAuth, - client: NormalizedWhmcsClient -): User => { +export const buildUserProfile = (userAuth: UserAuth, client: NormalizedWhmcsClient): User => { const payload = { id: userAuth.id, email: userAuth.email, @@ -272,10 +256,7 @@ export const buildUserProfile = ( fullname: client.fullname ?? null, companyname: client.companyname ?? null, phonenumber: - client.phonenumberformatted ?? - client.phonenumber ?? - client.telephoneNumber ?? - null, + client.phonenumberformatted ?? client.phonenumber ?? client.telephoneNumber ?? null, language: client.language ?? null, currency_code: client.currency_code ?? null, address: client.address ?? undefined, @@ -284,6 +265,4 @@ export const buildUserProfile = ( return userSchema.parse(payload); }; -export const getNumericClientId = ( - client: NormalizedWhmcsClient -): number => client.id; +export const getNumericClientId = (client: NormalizedWhmcsClient): number => client.id; diff --git a/apps/bff/src/integrations/whmcs/whmcs.service.ts b/apps/bff/src/integrations/whmcs/whmcs.service.ts index 5b4c2275..772baca8 100644 --- a/apps/bff/src/integrations/whmcs/whmcs.service.ts +++ b/apps/bff/src/integrations/whmcs/whmcs.service.ts @@ -1,4 +1,3 @@ -import { getErrorMessage } from "@bff/core/utils/error.util"; import { Injectable, Inject } from "@nestjs/common"; import type { Invoice, InvoiceList } from "@customer-portal/domain/billing"; import type { Subscription, SubscriptionList } from "@customer-portal/domain/subscriptions"; @@ -14,17 +13,10 @@ import { WhmcsClientService } from "./services/whmcs-client.service"; import { WhmcsPaymentService } from "./services/whmcs-payment.service"; import { WhmcsSsoService } from "./services/whmcs-sso.service"; import { WhmcsOrderService } from "./services/whmcs-order.service"; -import type { - WhmcsAddClientParams, - WhmcsClientResponse, -} from "@customer-portal/domain/customer"; +import type { WhmcsAddClientParams, WhmcsClientResponse } from "@customer-portal/domain/customer"; import type { WhmcsGetClientsProductsParams } from "@customer-portal/domain/subscriptions"; -import type { - WhmcsProductListResponse, -} from "@customer-portal/domain/subscriptions"; -import type { - WhmcsCatalogProductListResponse, -} from "@customer-portal/domain/catalog"; +import type { WhmcsProductListResponse } from "@customer-portal/domain/subscriptions"; +import type { WhmcsCatalogProductListResponse } from "@customer-portal/domain/catalog"; import { Logger } from "nestjs-pino"; import { deriveAddressFromClient, type NormalizedWhmcsClient } from "./utils/whmcs-client.utils"; @@ -282,7 +274,9 @@ export class WhmcsService { return this.connectionService.getSystemInfo(); } - async getClientsProducts(params: WhmcsGetClientsProductsParams): Promise { + async getClientsProducts( + params: WhmcsGetClientsProductsParams + ): Promise { return this.connectionService.getClientsProducts(params); } diff --git a/apps/bff/src/modules/auth/application/auth.facade.ts b/apps/bff/src/modules/auth/application/auth.facade.ts index 02d0db1c..80b62a88 100644 --- a/apps/bff/src/modules/auth/application/auth.facade.ts +++ b/apps/bff/src/modules/auth/application/auth.facade.ts @@ -17,14 +17,6 @@ import { type SetPasswordRequest, type ChangePasswordRequest, type SsoLinkResponse, - type CheckPasswordNeededResponse, - signupRequestSchema, - validateSignupRequestSchema, - linkWhmcsRequestSchema, - setPasswordRequestSchema, - updateProfileRequestSchema, - updateAddressRequestSchema, - changePasswordRequestSchema, } from "@customer-portal/domain/auth"; import type { User as PrismaUser } from "@prisma/client"; @@ -326,10 +318,7 @@ export class AuthFacade { /** * Create SSO link to WHMCS for general access */ - async createSsoLink( - userId: string, - destination?: string - ): Promise { + async createSsoLink(userId: string, destination?: string): Promise { try { // Production-safe logging - no sensitive data this.logger.log("Creating SSO link request"); diff --git a/apps/bff/src/modules/auth/infra/workflows/workflows/password-workflow.service.ts b/apps/bff/src/modules/auth/infra/workflows/workflows/password-workflow.service.ts index abb263c2..ec2737a7 100644 --- a/apps/bff/src/modules/auth/infra/workflows/workflows/password-workflow.service.ts +++ b/apps/bff/src/modules/auth/infra/workflows/workflows/password-workflow.service.ts @@ -11,12 +11,10 @@ import { getErrorMessage } from "@bff/core/utils/error.util"; import { AuthTokenService } from "../../token/token.service"; import { AuthRateLimitService } from "../../rate-limiting/auth-rate-limit.service"; import { - type AuthTokens, type PasswordChangeResult, type ChangePasswordRequest, changePasswordRequestSchema, } from "@customer-portal/domain/auth"; -import type { User } from "@customer-portal/domain/customer"; import { mapPrismaUserToDomain } from "@bff/infra/mappers"; @Injectable() diff --git a/apps/bff/src/modules/auth/infra/workflows/workflows/signup-workflow.service.ts b/apps/bff/src/modules/auth/infra/workflows/workflows/signup-workflow.service.ts index 6a541bf5..a76fc9cf 100644 --- a/apps/bff/src/modules/auth/infra/workflows/workflows/signup-workflow.service.ts +++ b/apps/bff/src/modules/auth/infra/workflows/workflows/signup-workflow.service.ts @@ -23,9 +23,7 @@ import { type SignupRequest, type SignupResult, type ValidateSignupRequest, - type AuthTokens, } from "@customer-portal/domain/auth"; -import type { User } from "@customer-portal/domain/customer"; import { mapPrismaUserToDomain } from "@bff/infra/mappers"; import type { User as PrismaUser } from "@prisma/client"; @@ -137,7 +135,7 @@ export class SignupWorkflowService { if (request) { await this.authRateLimitService.consumeSignupAttempt(request); } - + // Validate signup data using schema (throws on validation error) signupRequestSchema.parse(signupData); @@ -474,5 +472,4 @@ export class SignupWorkflowService { result.messages.push("All checks passed. Ready to create your account."); return result; } - } diff --git a/apps/bff/src/modules/auth/infra/workflows/workflows/whmcs-link-workflow.service.ts b/apps/bff/src/modules/auth/infra/workflows/workflows/whmcs-link-workflow.service.ts index 9549cc36..380ed587 100644 --- a/apps/bff/src/modules/auth/infra/workflows/workflows/whmcs-link-workflow.service.ts +++ b/apps/bff/src/modules/auth/infra/workflows/workflows/whmcs-link-workflow.service.ts @@ -13,7 +13,10 @@ import { SalesforceService } from "@bff/integrations/salesforce/salesforce.servi import { getErrorMessage } from "@bff/core/utils/error.util"; import { mapPrismaUserToDomain } from "@bff/infra/mappers"; import type { User } from "@customer-portal/domain/customer"; -import { getCustomFieldValue, getNumericClientId } from "@bff/integrations/whmcs/utils/whmcs-client.utils"; +import { + getCustomFieldValue, + getNumericClientId, +} from "@bff/integrations/whmcs/utils/whmcs-client.utils"; // No direct Customer import - use inferred type from WHMCS service @Injectable() diff --git a/apps/bff/src/modules/auth/presentation/http/auth.controller.ts b/apps/bff/src/modules/auth/presentation/http/auth.controller.ts index 41604d96..9597b04a 100644 --- a/apps/bff/src/modules/auth/presentation/http/auth.controller.ts +++ b/apps/bff/src/modules/auth/presentation/http/auth.controller.ts @@ -255,7 +255,10 @@ export class AuthController { @Post("reset-password") @HttpCode(200) @UsePipes(new ZodValidationPipe(passwordResetSchema)) - async resetPassword(@Body() body: ResetPasswordRequest, @Res({ passthrough: true }) res: Response) { + async resetPassword( + @Body() body: ResetPasswordRequest, + @Res({ passthrough: true }) res: Response + ) { await this.authFacade.resetPassword(body.token, body.password); // Clear auth cookies after password reset to force re-login diff --git a/apps/bff/src/modules/catalog/services/base-catalog.service.ts b/apps/bff/src/modules/catalog/services/base-catalog.service.ts index 2121762e..6d2ae570 100644 --- a/apps/bff/src/modules/catalog/services/base-catalog.service.ts +++ b/apps/bff/src/modules/catalog/services/base-catalog.service.ts @@ -30,11 +30,9 @@ export class BaseCatalogService { ) { const portalPricebook = this.configService.get("PORTAL_PRICEBOOK_ID")!; this.portalPriceBookId = assertSalesforceId(portalPricebook, "PORTAL_PRICEBOOK_ID"); - const portalCategory = this.configService.get("PRODUCT_PORTAL_CATEGORY_FIELD") ?? "Product2Categories1__c"; - this.portalCategoryField = assertSoqlFieldName( - portalCategory, - "PRODUCT_PORTAL_CATEGORY_FIELD" - ); + const portalCategory = + this.configService.get("PRODUCT_PORTAL_CATEGORY_FIELD") ?? "Product2Categories1__c"; + this.portalCategoryField = assertSoqlFieldName(portalCategory, "PRODUCT_PORTAL_CATEGORY_FIELD"); } protected async executeQuery( diff --git a/apps/bff/src/modules/catalog/services/internet-catalog.service.ts b/apps/bff/src/modules/catalog/services/internet-catalog.service.ts index d025e960..531b0032 100644 --- a/apps/bff/src/modules/catalog/services/internet-catalog.service.ts +++ b/apps/bff/src/modules/catalog/services/internet-catalog.service.ts @@ -179,5 +179,4 @@ export class InternetCatalogService extends BaseCatalogService { // e.g., eligibility "Home 1G" matches plan.internetOfferingType "Home 1G" return plan.internetOfferingType === eligibility; } - } diff --git a/apps/bff/src/modules/catalog/services/vpn-catalog.service.ts b/apps/bff/src/modules/catalog/services/vpn-catalog.service.ts index 4dd77cbf..2381cd5b 100644 --- a/apps/bff/src/modules/catalog/services/vpn-catalog.service.ts +++ b/apps/bff/src/modules/catalog/services/vpn-catalog.service.ts @@ -3,7 +3,10 @@ import { ConfigService } from "@nestjs/config"; import { Logger } from "nestjs-pino"; import { SalesforceConnection } from "@bff/integrations/salesforce/services/salesforce-connection.service"; import { BaseCatalogService } from "./base-catalog.service"; -import type { SalesforceProduct2WithPricebookEntries, VpnCatalogProduct } from "@customer-portal/domain/catalog"; +import type { + SalesforceProduct2WithPricebookEntries, + VpnCatalogProduct, +} from "@customer-portal/domain/catalog"; import { Providers as CatalogProviders } from "@customer-portal/domain/catalog"; @Injectable() @@ -16,10 +19,7 @@ export class VpnCatalogService extends BaseCatalogService { super(sf, configService, logger); } async getPlans(): Promise { - const soql = this.buildCatalogServiceQuery("VPN", [ - "VPN_Region__c", - "Catalog_Order__c", - ]); + const soql = this.buildCatalogServiceQuery("VPN", ["VPN_Region__c", "Catalog_Order__c"]); const records = await this.executeQuery( soql, "VPN Plans" diff --git a/apps/bff/src/modules/currency/currency.module.ts b/apps/bff/src/modules/currency/currency.module.ts index 2e6c1a8f..e618b9b6 100644 --- a/apps/bff/src/modules/currency/currency.module.ts +++ b/apps/bff/src/modules/currency/currency.module.ts @@ -6,6 +6,7 @@ import { WhmcsModule } from "../../integrations/whmcs/whmcs.module"; @Module({ imports: [WhmcsModule], controllers: [CurrencyController], - providers: [], + providers: [WhmcsCurrencyService], + exports: [WhmcsCurrencyService], }) export class CurrencyModule {} diff --git a/apps/bff/src/modules/id-mappings/mappings.service.ts b/apps/bff/src/modules/id-mappings/mappings.service.ts index 695f1811..80450c0e 100644 --- a/apps/bff/src/modules/id-mappings/mappings.service.ts +++ b/apps/bff/src/modules/id-mappings/mappings.service.ts @@ -53,7 +53,6 @@ export class MappingsService { throw error; } - const warnings = checkMappingCompleteness(validatedRequest); const sanitizedRequest = validatedRequest; const [byUser, byWhmcs, bySf] = await Promise.all([ @@ -68,18 +67,15 @@ export class MappingsService { : Promise.resolve(null), ]); - if (byUser) { - throw new ConflictException(`User ${sanitizedRequest.userId} already has a mapping`); - } - if (byWhmcs) { - throw new ConflictException( - `WHMCS client ${sanitizedRequest.whmcsClientId} is already mapped to user ${byWhmcs.userId}` - ); - } - if (bySf) { - this.logger.warn( - `Salesforce account ${sanitizedRequest.sfAccountId} is already mapped to user ${bySf.userId}` - ); + const existingMappings = [byUser, byWhmcs, bySf] + .filter((mapping): mapping is Prisma.IdMapping => mapping !== null) + .map(mapPrismaMappingToDomain); + + const conflictCheck = validateNoConflicts(sanitizedRequest, existingMappings); + const warnings = [...checkMappingCompleteness(sanitizedRequest), ...conflictCheck.warnings]; + + if (!conflictCheck.isValid) { + throw new ConflictException(conflictCheck.errors.join("; ")); } let created; diff --git a/apps/bff/src/modules/invoices/index.ts b/apps/bff/src/modules/invoices/index.ts index a081dad0..4ab7139c 100644 --- a/apps/bff/src/modules/invoices/index.ts +++ b/apps/bff/src/modules/invoices/index.ts @@ -9,7 +9,4 @@ export * from "./services/invoice-retrieval.service"; export * from "./services/invoice-health.service"; // Export monitoring types (infrastructure concerns) -export type { - InvoiceHealthStatus, - InvoiceServiceStats, -} from "./types/invoice-monitoring.types"; +export type { InvoiceHealthStatus, InvoiceServiceStats } from "./types/invoice-monitoring.types"; diff --git a/apps/bff/src/modules/invoices/invoices.controller.ts b/apps/bff/src/modules/invoices/invoices.controller.ts index 3b4fe890..4f5a4b72 100644 --- a/apps/bff/src/modules/invoices/invoices.controller.ts +++ b/apps/bff/src/modules/invoices/invoices.controller.ts @@ -16,14 +16,23 @@ import { MappingsService } from "@bff/modules/id-mappings/mappings.service"; import { ZodValidationPipe } from "@bff/core/validation"; import type { RequestWithUser } from "@bff/modules/auth/auth.types"; -import type { Invoice, InvoiceList, InvoiceSsoLink, InvoiceListQuery } from "@customer-portal/domain/billing"; +import type { + Invoice, + InvoiceList, + InvoiceSsoLink, + InvoiceListQuery, +} from "@customer-portal/domain/billing"; import { invoiceListQuerySchema, invoiceSchema } from "@customer-portal/domain/billing"; import type { Subscription } from "@customer-portal/domain/subscriptions"; -import type { PaymentMethodList, PaymentGatewayList, InvoicePaymentLink } from "@customer-portal/domain/payments"; +import type { + PaymentMethodList, + PaymentGatewayList, + InvoicePaymentLink, +} from "@customer-portal/domain/payments"; /** * Invoice Controller - * + * * All request validation is handled by Zod schemas via ZodValidationPipe. * Business logic is delegated to service layer. */ @@ -88,7 +97,7 @@ export class InvoicesController { ): Subscription[] { // Validate using domain schema invoiceSchema.shape.id.parse(invoiceId); - + // This functionality has been moved to WHMCS directly // For now, return empty array as subscriptions are managed in WHMCS return []; diff --git a/apps/bff/src/modules/invoices/invoices.module.ts b/apps/bff/src/modules/invoices/invoices.module.ts index a9f3c55c..17b0a343 100644 --- a/apps/bff/src/modules/invoices/invoices.module.ts +++ b/apps/bff/src/modules/invoices/invoices.module.ts @@ -9,18 +9,14 @@ import { InvoiceHealthService } from "./services/invoice-health.service"; /** * Invoice Module - * + * * Validation is now handled by Zod schemas via ZodValidationPipe in controller. * No separate validator service needed. */ @Module({ imports: [WhmcsModule, MappingsModule], controllers: [InvoicesController], - providers: [ - InvoicesOrchestratorService, - InvoiceRetrievalService, - InvoiceHealthService, - ], + providers: [InvoicesOrchestratorService, InvoiceRetrievalService, InvoiceHealthService], exports: [InvoicesOrchestratorService], }) export class InvoicesModule {} diff --git a/apps/bff/src/modules/invoices/services/invoice-retrieval.service.ts b/apps/bff/src/modules/invoices/services/invoice-retrieval.service.ts index 46650fb7..7c3157a5 100644 --- a/apps/bff/src/modules/invoices/services/invoice-retrieval.service.ts +++ b/apps/bff/src/modules/invoices/services/invoice-retrieval.service.ts @@ -25,7 +25,7 @@ interface UserMappingInfo { /** * Service responsible for retrieving invoices from WHMCS - * + * * Validation is handled by Zod schemas at the entry point (controller). * This service focuses on business logic and data fetching. */ @@ -146,21 +146,30 @@ export class InvoiceRetrievalService { /** * Get unpaid invoices for a user */ - async getUnpaidInvoices(userId: string, options: Partial = {}): Promise { + async getUnpaidInvoices( + userId: string, + options: Partial = {} + ): Promise { return this.getInvoicesByStatus(userId, "Unpaid", options); } /** * Get overdue invoices for a user */ - async getOverdueInvoices(userId: string, options: Partial = {}): Promise { + async getOverdueInvoices( + userId: string, + options: Partial = {} + ): Promise { return this.getInvoicesByStatus(userId, "Overdue", options); } /** * Get paid invoices for a user */ - async getPaidInvoices(userId: string, options: Partial = {}): Promise { + async getPaidInvoices( + userId: string, + options: Partial = {} + ): Promise { return this.getInvoicesByStatus(userId, "Paid", options); } @@ -190,7 +199,7 @@ export class InvoiceRetrievalService { private async getUserMapping(userId: string): Promise { // Validate userId is a valid UUID validateUuidV4OrThrow(userId); - + const mapping = await this.mappingsService.findByUserId(userId); if (!mapping?.whmcsClientId) { diff --git a/apps/bff/src/modules/invoices/services/invoices-orchestrator.service.ts b/apps/bff/src/modules/invoices/services/invoices-orchestrator.service.ts index 29cbab74..013a7cc0 100644 --- a/apps/bff/src/modules/invoices/services/invoices-orchestrator.service.ts +++ b/apps/bff/src/modules/invoices/services/invoices-orchestrator.service.ts @@ -10,10 +10,7 @@ import { } from "@customer-portal/domain/billing"; import { InvoiceRetrievalService } from "./invoice-retrieval.service"; import { InvoiceHealthService } from "./invoice-health.service"; -import type { - InvoiceHealthStatus, - InvoiceServiceStats, -} from "../types/invoice-monitoring.types"; +import type { InvoiceHealthStatus, InvoiceServiceStats } from "../types/invoice-monitoring.types"; /** * Main orchestrator service for invoice operations @@ -86,21 +83,30 @@ export class InvoicesOrchestratorService { /** * Get unpaid invoices for a user */ - async getUnpaidInvoices(userId: string, options: Partial = {}): Promise { + async getUnpaidInvoices( + userId: string, + options: Partial = {} + ): Promise { return this.retrievalService.getUnpaidInvoices(userId, options); } /** * Get overdue invoices for a user */ - async getOverdueInvoices(userId: string, options: Partial = {}): Promise { + async getOverdueInvoices( + userId: string, + options: Partial = {} + ): Promise { return this.retrievalService.getOverdueInvoices(userId, options); } /** * Get paid invoices for a user */ - async getPaidInvoices(userId: string, options: Partial = {}): Promise { + async getPaidInvoices( + userId: string, + options: Partial = {} + ): Promise { return this.retrievalService.getPaidInvoices(userId, options); } diff --git a/apps/bff/src/modules/invoices/types/invoice-monitoring.types.ts b/apps/bff/src/modules/invoices/types/invoice-monitoring.types.ts index 1ca40be9..7d40e236 100644 --- a/apps/bff/src/modules/invoices/types/invoice-monitoring.types.ts +++ b/apps/bff/src/modules/invoices/types/invoice-monitoring.types.ts @@ -1,6 +1,6 @@ /** * BFF Invoice Monitoring Types - * + * * Infrastructure types for monitoring, health checks, and statistics. * These are BFF-specific and do not belong in the domain layer. */ @@ -24,4 +24,3 @@ export interface InvoiceHealthStatus { timestamp: string; }; } - diff --git a/apps/bff/src/modules/orders/orders.module.ts b/apps/bff/src/modules/orders/orders.module.ts index e8bee884..0d84f3d3 100644 --- a/apps/bff/src/modules/orders/orders.module.ts +++ b/apps/bff/src/modules/orders/orders.module.ts @@ -28,7 +28,7 @@ import { ProvisioningProcessor } from "./queue/provisioning.processor"; providers: [ // Shared services PaymentValidatorService, - + // Order creation services (modular) OrderValidator, OrderBuilder, diff --git a/apps/bff/src/modules/orders/queue/provisioning.processor.ts b/apps/bff/src/modules/orders/queue/provisioning.processor.ts index d1353e0b..151749c6 100644 --- a/apps/bff/src/modules/orders/queue/provisioning.processor.ts +++ b/apps/bff/src/modules/orders/queue/provisioning.processor.ts @@ -32,7 +32,7 @@ export class ProvisioningProcessor extends WorkerHost { }); // Guard: Only process if Salesforce Order is currently 'Activating' - + const order = await this.salesforceService.getOrder(sfOrderId); const status = order?.Activation_Status__c ?? ""; const lastErrorCode = order?.Activation_Error_Code__c ?? ""; diff --git a/apps/bff/src/modules/orders/services/order-builder.service.ts b/apps/bff/src/modules/orders/services/order-builder.service.ts index db05ca77..b0891089 100644 --- a/apps/bff/src/modules/orders/services/order-builder.service.ts +++ b/apps/bff/src/modules/orders/services/order-builder.service.ts @@ -74,10 +74,7 @@ export class OrderBuilder { assignIfString(orderFields, "Access_Mode__c", config.accessMode); } - private addSimFields( - orderFields: Record, - body: OrderBusinessValidation - ): void { + private addSimFields(orderFields: Record, body: OrderBusinessValidation): void { const config = body.configurations || {}; assignIfString(orderFields, "SIM_Type__c", config.simType); assignIfString(orderFields, "EID__c", config.eid); @@ -91,7 +88,11 @@ export class OrderBuilder { assignIfString(orderFields, "Porting_Last_Name__c", config.portingLastName); assignIfString(orderFields, "Porting_First_Name__c", config.portingFirstName); assignIfString(orderFields, "Porting_Last_Name_Katakana__c", config.portingLastNameKatakana); - assignIfString(orderFields, "Porting_First_Name_Katakana__c", config.portingFirstNameKatakana); + assignIfString( + orderFields, + "Porting_First_Name_Katakana__c", + config.portingFirstNameKatakana + ); assignIfString(orderFields, "Porting_Gender__c", config.portingGender); assignIfString(orderFields, "Porting_Date_Of_Birth__c", config.portingDateOfBirth); } @@ -124,9 +125,12 @@ export class OrderBuilder { orderFields.Billing_Street__c = fullStreet; orderFields.Billing_City__c = typeof addressToUse?.city === "string" ? addressToUse.city : ""; - orderFields.Billing_State__c = typeof addressToUse?.state === "string" ? addressToUse.state : ""; - orderFields.Billing_Postal_Code__c = typeof addressToUse?.postcode === "string" ? addressToUse.postcode : ""; - orderFields.Billing_Country__c = typeof addressToUse?.country === "string" ? addressToUse.country : ""; + orderFields.Billing_State__c = + typeof addressToUse?.state === "string" ? addressToUse.state : ""; + orderFields.Billing_Postal_Code__c = + typeof addressToUse?.postcode === "string" ? addressToUse.postcode : ""; + orderFields.Billing_Country__c = + typeof addressToUse?.country === "string" ? addressToUse.country : ""; orderFields.Address_Changed__c = addressChanged; if (addressChanged) { diff --git a/apps/bff/src/modules/orders/services/order-fulfillment-error.service.ts b/apps/bff/src/modules/orders/services/order-fulfillment-error.service.ts index 510420ca..b65dcb2a 100644 --- a/apps/bff/src/modules/orders/services/order-fulfillment-error.service.ts +++ b/apps/bff/src/modules/orders/services/order-fulfillment-error.service.ts @@ -5,7 +5,7 @@ import type { OrderFulfillmentErrorCode } from "@customer-portal/domain/orders"; /** * Centralized error code determination and error handling for order fulfillment * Eliminates duplicate error code logic across services - * + * * Note: Error codes are now defined in @customer-portal/domain/orders as business constants */ @Injectable() diff --git a/apps/bff/src/modules/orders/services/order-fulfillment-orchestrator.service.ts b/apps/bff/src/modules/orders/services/order-fulfillment-orchestrator.service.ts index f6272c19..4d0cda02 100644 --- a/apps/bff/src/modules/orders/services/order-fulfillment-orchestrator.service.ts +++ b/apps/bff/src/modules/orders/services/order-fulfillment-orchestrator.service.ts @@ -14,13 +14,7 @@ import { OrderFulfillmentErrorService } from "./order-fulfillment-error.service" import { SimFulfillmentService } from "./sim-fulfillment.service"; import { DistributedTransactionService } from "@bff/core/database/services/distributed-transaction.service"; import { getErrorMessage } from "@bff/core/utils/error.util"; -import { - type OrderSummary, - type OrderDetails, - type SalesforceOrderRecord, - type SalesforceOrderItemRecord, - Providers as OrderProviders, -} from "@customer-portal/domain/orders"; +import { type OrderDetails, Providers as OrderProviders } from "@customer-portal/domain/orders"; type WhmcsOrderItemMappingResult = ReturnType; @@ -138,14 +132,12 @@ export class OrderFulfillmentOrchestrator { id: "sf_status_update", description: "Update Salesforce order status to Activating", execute: async () => { - return await this.salesforceService.updateOrder({ Id: sfOrderId, Activation_Status__c: "Activating", }); }, rollback: async () => { - await this.salesforceService.updateOrder({ Id: sfOrderId, Activation_Status__c: "Failed", @@ -169,13 +161,13 @@ export class OrderFulfillmentOrchestrator { // Use domain mapper directly - single transformation! const result = OrderProviders.Whmcs.mapOrderToWhmcsItems(context.orderDetails); mappingResult = result; - + this.logger.log("OrderItems mapped to WHMCS", { totalItems: result.summary.totalItems, serviceItems: result.summary.serviceItems, activationItems: result.summary.activationItems, }); - + return Promise.resolve(result); }, critical: true, @@ -281,8 +273,6 @@ export class OrderFulfillmentOrchestrator { id: "sf_success_update", description: "Update Salesforce with success", execute: async () => { - - return await this.salesforceService.updateOrder({ Id: sfOrderId, Status: "Completed", @@ -291,7 +281,6 @@ export class OrderFulfillmentOrchestrator { }); }, rollback: async () => { - await this.salesforceService.updateOrder({ Id: sfOrderId, Activation_Status__c: "Failed", @@ -330,7 +319,6 @@ export class OrderFulfillmentOrchestrator { return context; } - /** * Initialize fulfillment steps */ @@ -353,7 +341,6 @@ export class OrderFulfillmentOrchestrator { return steps; } - private extractConfigurations(value: unknown): Record { if (value && typeof value === "object") { return value as Record; @@ -361,7 +348,6 @@ export class OrderFulfillmentOrchestrator { return {}; } - /** * Handle fulfillment errors and update Salesforce */ @@ -371,7 +357,6 @@ export class OrderFulfillmentOrchestrator { ): Promise { const errorCode = this.orderFulfillmentErrorService.determineErrorCode(error); const userMessage = error.message; - this.logger.error("Fulfillment orchestration failed", { sfOrderId: context.sfOrderId, diff --git a/apps/bff/src/modules/orders/services/order-fulfillment-validator.service.ts b/apps/bff/src/modules/orders/services/order-fulfillment-validator.service.ts index 9864991a..729b49e3 100644 --- a/apps/bff/src/modules/orders/services/order-fulfillment-validator.service.ts +++ b/apps/bff/src/modules/orders/services/order-fulfillment-validator.service.ts @@ -1,4 +1,4 @@ -import { Injectable, BadRequestException, ConflictException, Inject } from "@nestjs/common"; +import { Injectable, BadRequestException, Inject } from "@nestjs/common"; import { Logger } from "nestjs-pino"; import { z } from "zod"; import { SalesforceService } from "@bff/integrations/salesforce/salesforce.service"; @@ -7,7 +7,6 @@ import { getErrorMessage } from "@bff/core/utils/error.util"; import type { SalesforceOrderRecord } from "@customer-portal/domain/orders"; import { sfOrderIdParamSchema } from "@customer-portal/domain/orders"; import { PaymentValidatorService } from "./payment-validator.service"; -type OrderStringFieldKey = "activationStatus"; // Schema for validating Salesforce Account ID const salesforceAccountIdSchema = z.string().min(1, "Salesforce AccountId is required"); @@ -52,7 +51,7 @@ export class OrderFulfillmentValidator { const sfOrder = await this.validateSalesforceOrder(sfOrderId); // 2. Check if already provisioned (idempotency) - + const existingWhmcsOrderId = sfOrder.WHMCS_Order_ID__c; if (existingWhmcsOrderId) { this.logger.log("Order already provisioned", { @@ -115,7 +114,6 @@ export class OrderFulfillmentValidator { throw new BadRequestException(`Cannot provision cancelled order ${sfOrderId}`); } - this.logger.log("Salesforce order validated", { sfOrderId, status: order.Status, diff --git a/apps/bff/src/modules/orders/services/order-orchestrator.service.ts b/apps/bff/src/modules/orders/services/order-orchestrator.service.ts index 330ad643..be8d1b52 100644 --- a/apps/bff/src/modules/orders/services/order-orchestrator.service.ts +++ b/apps/bff/src/modules/orders/services/order-orchestrator.service.ts @@ -4,10 +4,7 @@ import { SalesforceOrderService } from "@bff/integrations/salesforce/services/sa import { OrderValidator } from "./order-validator.service"; import { OrderBuilder } from "./order-builder.service"; import { OrderItemBuilder } from "./order-item-builder.service"; -import { - type OrderDetails, - type OrderSummary, -} from "@customer-portal/domain/orders"; +import { type OrderDetails, type OrderSummary } from "@customer-portal/domain/orders"; import { assertSalesforceId } from "@bff/integrations/salesforce/utils/soql.util"; type OrderDetailsResponse = OrderDetails; @@ -101,7 +98,7 @@ export class OrderOrchestrator { const sfAccountId = userMapping.sfAccountId ? assertSalesforceId(userMapping.sfAccountId, "sfAccountId") : undefined; - + if (!sfAccountId) { this.logger.warn({ userId }, "User mapping missing Salesforce account ID"); return []; diff --git a/apps/bff/src/modules/orders/services/order-pricebook.service.ts b/apps/bff/src/modules/orders/services/order-pricebook.service.ts index 3c9f8a0c..902de01a 100644 --- a/apps/bff/src/modules/orders/services/order-pricebook.service.ts +++ b/apps/bff/src/modules/orders/services/order-pricebook.service.ts @@ -2,7 +2,6 @@ import { Inject, Injectable, NotFoundException } from "@nestjs/common"; import { ConfigService } from "@nestjs/config"; import { Logger } from "nestjs-pino"; import { SalesforceConnection } from "@bff/integrations/salesforce/services/salesforce-connection.service"; -import { z } from "zod"; import type { SalesforceProduct2Record, SalesforcePricebookEntryRecord, @@ -78,7 +77,6 @@ export class OrderPricebookService { return new Map(); } - const meta = new Map(); for (let i = 0; i < uniqueSkus.length; i += this.chunkSize) { diff --git a/apps/bff/src/modules/orders/services/order-validator.service.ts b/apps/bff/src/modules/orders/services/order-validator.service.ts index e8a3b5f4..7491dff6 100644 --- a/apps/bff/src/modules/orders/services/order-validator.service.ts +++ b/apps/bff/src/modules/orders/services/order-validator.service.ts @@ -18,7 +18,7 @@ import { PaymentValidatorService } from "./payment-validator.service"; /** * Handles all order validation logic - both format and business rules - * + * * Note: Business validation rules have been moved to @customer-portal/domain/orders/validation * This service now focuses on infrastructure concerns (DB lookups, API calls, etc.) */ diff --git a/apps/bff/src/modules/orders/services/payment-validator.service.ts b/apps/bff/src/modules/orders/services/payment-validator.service.ts index 16e37694..741b5625 100644 --- a/apps/bff/src/modules/orders/services/payment-validator.service.ts +++ b/apps/bff/src/modules/orders/services/payment-validator.service.ts @@ -5,7 +5,7 @@ import { getErrorMessage } from "@bff/core/utils/error.util"; /** * Shared Payment Validation Service - * + * * Provides consistent payment method validation across order workflows. * Used by both OrderValidator and OrderFulfillmentValidator. */ @@ -18,35 +18,34 @@ export class PaymentValidatorService { /** * Validate that a payment method exists for the WHMCS client - * + * * @param userId - User ID for logging purposes * @param whmcsClientId - WHMCS client ID to check * @throws BadRequestException if no payment method exists or verification fails */ - async validatePaymentMethodExists( - userId: string, - whmcsClientId: number - ): Promise { + async validatePaymentMethodExists(userId: string, whmcsClientId: number): Promise { try { const pay = await this.whmcs.getPaymentMethods({ clientid: whmcsClientId }); const paymentMethods = Array.isArray(pay.paymethods) ? pay.paymethods : []; - + if (paymentMethods.length === 0) { this.logger.warn({ userId, whmcsClientId }, "No payment method on file"); throw new BadRequestException("A payment method is required before ordering"); } - this.logger.log({ userId, whmcsClientId, count: paymentMethods.length }, "Payment method verified"); + this.logger.log( + { userId, whmcsClientId, count: paymentMethods.length }, + "Payment method verified" + ); } catch (e: unknown) { // Re-throw BadRequestException as-is if (e instanceof BadRequestException) { throw e; } - + const err = getErrorMessage(e); this.logger.error({ err, userId, whmcsClientId }, "Payment method verification failed"); throw new BadRequestException("Unable to verify payment method. Please try again later."); } } } - diff --git a/apps/bff/src/modules/subscriptions/sim-management.service.ts b/apps/bff/src/modules/subscriptions/sim-management.service.ts index cb019fc3..fbbeda88 100644 --- a/apps/bff/src/modules/subscriptions/sim-management.service.ts +++ b/apps/bff/src/modules/subscriptions/sim-management.service.ts @@ -120,10 +120,7 @@ export class SimManagementService { /** * Get comprehensive SIM information (details + usage combined) */ - async getSimInfo( - userId: string, - subscriptionId: number - ): Promise { + async getSimInfo(userId: string, subscriptionId: number): Promise { return this.simOrchestrator.getSimInfo(userId, subscriptionId); } diff --git a/apps/bff/src/modules/subscriptions/sim-management/services/sim-orchestrator.service.ts b/apps/bff/src/modules/subscriptions/sim-management/services/sim-orchestrator.service.ts index 0cbbf0c0..df1fbb3d 100644 --- a/apps/bff/src/modules/subscriptions/sim-management/services/sim-orchestrator.service.ts +++ b/apps/bff/src/modules/subscriptions/sim-management/services/sim-orchestrator.service.ts @@ -111,10 +111,7 @@ export class SimOrchestratorService { /** * Get comprehensive SIM information (details + usage combined) */ - async getSimInfo( - userId: string, - subscriptionId: number - ): Promise { + async getSimInfo(userId: string, subscriptionId: number): Promise { try { const [details, usage] = await Promise.all([ this.getSimDetails(userId, subscriptionId), diff --git a/apps/bff/src/modules/subscriptions/sim-management/services/sim-validation.service.ts b/apps/bff/src/modules/subscriptions/sim-management/services/sim-validation.service.ts index 26e2688e..0fb19d3c 100644 --- a/apps/bff/src/modules/subscriptions/sim-management/services/sim-validation.service.ts +++ b/apps/bff/src/modules/subscriptions/sim-management/services/sim-validation.service.ts @@ -3,7 +3,11 @@ import { Logger } from "nestjs-pino"; import { SubscriptionsService } from "../../subscriptions.service"; import { getErrorMessage } from "@bff/core/utils/error.util"; import type { SimValidationResult } from "../interfaces/sim-base.interface"; -import { cleanSimAccount, extractSimAccountFromSubscription, isSimSubscription } from "@customer-portal/domain/sim"; +import { + cleanSimAccount, + extractSimAccountFromSubscription, + isSimSubscription, +} from "@customer-portal/domain/sim"; @Injectable() export class SimValidationService { @@ -14,7 +18,7 @@ export class SimValidationService { /** * Check if a subscription is a SIM service and extract account identifier - * + * * Uses domain validation functions for business logic. */ async validateSimSubscription( diff --git a/apps/bff/src/modules/subscriptions/subscriptions.service.ts b/apps/bff/src/modules/subscriptions/subscriptions.service.ts index 4b828823..0355df9b 100644 --- a/apps/bff/src/modules/subscriptions/subscriptions.service.ts +++ b/apps/bff/src/modules/subscriptions/subscriptions.service.ts @@ -139,7 +139,10 @@ export class SubscriptionsService { /** * Get subscriptions by status */ - async getSubscriptionsByStatus(userId: string, status: SubscriptionStatus): Promise { + async getSubscriptionsByStatus( + userId: string, + status: SubscriptionStatus + ): Promise { try { const normalizedStatus = subscriptionStatusSchema.parse(status); const subscriptionList = await this.getSubscriptions(userId, { status: normalizedStatus }); diff --git a/apps/bff/src/modules/users/users.controller.ts b/apps/bff/src/modules/users/users.controller.ts index 5e3aea11..41605dc4 100644 --- a/apps/bff/src/modules/users/users.controller.ts +++ b/apps/bff/src/modules/users/users.controller.ts @@ -63,7 +63,7 @@ export class UsersController { * PATCH /me - Update customer profile (can update profile fields and/or address) * All fields optional - only send what needs to be updated * Updates stored in WHMCS (single source of truth) - * + * * Examples: * - Update name only: { firstname: "John", lastname: "Doe" } * - Update address only: { address1: "123 Main St", city: "Tokyo" } @@ -71,7 +71,10 @@ export class UsersController { */ @Patch() @UsePipes(new ZodValidationPipe(updateCustomerProfileRequestSchema)) - async updateProfile(@Req() req: RequestWithUser, @Body() updateData: UpdateCustomerProfileRequest) { + async updateProfile( + @Req() req: RequestWithUser, + @Body() updateData: UpdateCustomerProfileRequest + ) { return this.usersService.updateProfile(req.user.id, updateData); } } diff --git a/apps/bff/src/modules/users/users.service.ts b/apps/bff/src/modules/users/users.service.ts index e54295f5..e1594a59 100644 --- a/apps/bff/src/modules/users/users.service.ts +++ b/apps/bff/src/modules/users/users.service.ts @@ -25,13 +25,7 @@ import { buildUserProfile } from "@bff/integrations/whmcs/utils/whmcs-client.uti // Use a subset of PrismaUser for auth-related updates only type UserUpdateData = Partial< - Pick< - PrismaUser, - | "passwordHash" - | "failedLoginAttempts" - | "lastLoginAt" - | "lockedUntil" - > + Pick >; @Injectable() @@ -54,7 +48,7 @@ export class UsersService { const user = await this.prisma.user.findUnique({ where: { email: validEmail }, }); - + if (!user) return null; // Return full profile with WHMCS data @@ -134,10 +128,10 @@ export class UsersService { try { // Get WHMCS client data (source of truth for profile) const whmcsClient = await this.whmcsService.getClientDetails(mapping.whmcsClientId); - + // Map Prisma user to UserAuth const userAuth = CustomerProviders.Portal.mapPrismaUserToUserAuth(user); - + return buildUserProfile(userAuth, whmcsClient); } catch (error) { this.logger.error("Failed to fetch client profile from WHMCS", { @@ -223,7 +217,7 @@ export class UsersService { const createdUser = await this.prisma.user.create({ data: normalizedData, }); - + // Return full profile from WHMCS return this.getProfile(createdUser.id); } catch (error) { @@ -274,15 +268,18 @@ export class UsersService { // Update in WHMCS (all fields optional) await this.whmcsService.updateClient(mapping.whmcsClientId, parsed); - + this.logger.log({ userId: validId }, "Successfully updated customer profile in WHMCS"); // Return fresh profile return this.getProfile(validId); } catch (error) { const msg = getErrorMessage(error); - this.logger.error({ userId: validId, error: msg }, "Failed to update customer profile in WHMCS"); - + this.logger.error( + { userId: validId, error: msg }, + "Failed to update customer profile in WHMCS" + ); + if (msg.includes("WHMCS API Error")) { throw new BadRequestException(msg.replace("WHMCS API Error: ", "")); } @@ -321,7 +318,7 @@ export class UsersService { const mapping = await this.mappingsService.findByUserId(userId); if (!mapping?.whmcsClientId) { this.logger.warn(`No WHMCS mapping found for user ${userId}`); - + // Get currency from WHMCS profile if available let currency = "JPY"; // Default try { @@ -333,7 +330,7 @@ export class UsersService { error: getErrorMessage(error), }); } - + const summary: DashboardSummary = { stats: { activeSubscriptions: 0, @@ -505,7 +502,8 @@ export class UsersService { productName: subscription.productName, status: subscription.status, } as Record; - if (subscription.registrationDate) metadata.registrationDate = subscription.registrationDate; + if (subscription.registrationDate) + metadata.registrationDate = subscription.registrationDate; activities.push({ id: `service-activated-${subscription.id}`, type: "service_activated", @@ -561,5 +559,4 @@ export class UsersService { throw new BadRequestException("Unable to retrieve dashboard summary"); } } - } diff --git a/apps/portal/next.config.mjs b/apps/portal/next.config.mjs index 0bef2fa2..5b344b9a 100644 --- a/apps/portal/next.config.mjs +++ b/apps/portal/next.config.mjs @@ -124,9 +124,7 @@ const nextConfig = { }; const preferredExtensions = [".ts", ".tsx", ".mts", ".cts"]; const existingExtensions = config.resolve.extensions || []; - config.resolve.extensions = [ - ...new Set([...preferredExtensions, ...existingExtensions]), - ]; + config.resolve.extensions = [...new Set([...preferredExtensions, ...existingExtensions])]; config.resolve.extensionAlias = { ...(config.resolve.extensionAlias || {}), ".js": [".ts", ".tsx", ".js"], diff --git a/apps/portal/src/components/atoms/button.tsx b/apps/portal/src/components/atoms/button.tsx index b178436d..3dcc311a 100644 --- a/apps/portal/src/components/atoms/button.tsx +++ b/apps/portal/src/components/atoms/button.tsx @@ -68,6 +68,7 @@ const Button = forwardRef((p if (props.as === "a") { const { className, variant, size, as: _as, href, ...anchorProps } = rest as ButtonAsAnchorProps; + void _as; return ( ((p disabled, ...buttonProps } = rest as ButtonAsButtonProps; + void _as; return ( )} - + {/* Download Button - Always available and always on the right */} -

- Opens in a new tab for security -

+

Opens in a new tab for security

- +
- {paymentMethodsData.paymentMethods.map((paymentMethod) => ( + {paymentMethodsData.paymentMethods.map(paymentMethod => ( {createPaymentMethodsSsoLink.isPending ? "Opening..." : "Manage Cards"} -

- Opens in a new tab for security -

+

Opens in a new tab for security

)} @@ -232,7 +239,9 @@ export function PaymentMethodsContainer() {
-

Supported Payment Methods

+

+ Supported Payment Methods +

  • • Credit Cards (Visa, MasterCard, American Express)
  • • Debit Cards
  • diff --git a/apps/portal/src/features/catalog/components/base/AddressConfirmation.tsx b/apps/portal/src/features/catalog/components/base/AddressConfirmation.tsx index 3824767c..65825d85 100644 --- a/apps/portal/src/features/catalog/components/base/AddressConfirmation.tsx +++ b/apps/portal/src/features/catalog/components/base/AddressConfirmation.tsx @@ -241,8 +241,9 @@ export function AddressConfirmation({ if (!billingInfo) return null; const address = billingInfo.address; - const countryLabel = - address?.country ? getCountryName(address.country) ?? address.country : null; + const countryLabel = address?.country + ? (getCountryName(address.country) ?? address.country) + : null; return wrap( <> @@ -368,7 +369,7 @@ export function AddressConfirmation({ {option.name} ))} - +
@@ -396,9 +397,7 @@ export function AddressConfirmation({

{address.address1}

- {address.address2 ? ( -

{address.address2}

- ) : null} + {address.address2 ?

{address.address2}

: null}

{[address.city, address.state].filter(Boolean).join(", ")} {address.postcode ? ` ${address.postcode}` : ""} diff --git a/apps/portal/src/features/catalog/components/base/EnhancedOrderSummary.tsx b/apps/portal/src/features/catalog/components/base/EnhancedOrderSummary.tsx index 2e40d1f7..a6fb09b5 100644 --- a/apps/portal/src/features/catalog/components/base/EnhancedOrderSummary.tsx +++ b/apps/portal/src/features/catalog/components/base/EnhancedOrderSummary.tsx @@ -256,7 +256,9 @@ export function EnhancedOrderSummary({

Tax (10%) - ¥{formatCurrency(totals.taxAmount)} + + ¥{formatCurrency(totals.taxAmount)} +
)} diff --git a/apps/portal/src/features/catalog/components/base/OrderSummary.tsx b/apps/portal/src/features/catalog/components/base/OrderSummary.tsx index 5e9b325e..a5756a89 100644 --- a/apps/portal/src/features/catalog/components/base/OrderSummary.tsx +++ b/apps/portal/src/features/catalog/components/base/OrderSummary.tsx @@ -196,10 +196,9 @@ export function OrderSummary({ {String(addon.name)} ¥ - {( - addon.billingCycle === "Monthly" - ? addon.monthlyPrice ?? 0 - : addon.oneTimePrice ?? 0 + {(addon.billingCycle === "Monthly" + ? (addon.monthlyPrice ?? 0) + : (addon.oneTimePrice ?? 0) ).toLocaleString()} {addon.billingCycle === "Monthly" ? "/mo" : " one-time"} diff --git a/apps/portal/src/features/catalog/components/base/PricingDisplay.tsx b/apps/portal/src/features/catalog/components/base/PricingDisplay.tsx index 844bd34f..d8d0e70d 100644 --- a/apps/portal/src/features/catalog/components/base/PricingDisplay.tsx +++ b/apps/portal/src/features/catalog/components/base/PricingDisplay.tsx @@ -60,7 +60,6 @@ export function PricingDisplay({ infoText, children, }: PricingDisplayProps) { - const getCurrencyIcon = () => { if (!showCurrencySymbol) return null; return currency === "JPY" ? : "$"; diff --git a/apps/portal/src/features/catalog/components/internet/configure/steps/ReviewOrderStep.tsx b/apps/portal/src/features/catalog/components/internet/configure/steps/ReviewOrderStep.tsx index d1a7106b..b7ad0e79 100644 --- a/apps/portal/src/features/catalog/components/internet/configure/steps/ReviewOrderStep.tsx +++ b/apps/portal/src/features/catalog/components/internet/configure/steps/ReviewOrderStep.tsx @@ -110,14 +110,17 @@ function OrderSummary({ {mode &&

Access Mode: {mode}

}
-

¥{(plan.monthlyPrice ?? 0).toLocaleString()}

+

+ ¥{(plan.monthlyPrice ?? 0).toLocaleString()} +

per month

{/* Installation */} - {(selectedInstallation.monthlyPrice ?? 0) > 0 || (selectedInstallation.oneTimePrice ?? 0) > 0 ? ( + {(selectedInstallation.monthlyPrice ?? 0) > 0 || + (selectedInstallation.oneTimePrice ?? 0) > 0 ? (

Installation

diff --git a/apps/portal/src/features/catalog/components/sim/SimConfigureView.tsx b/apps/portal/src/features/catalog/components/sim/SimConfigureView.tsx index c3a8af2a..bedf6240 100644 --- a/apps/portal/src/features/catalog/components/sim/SimConfigureView.tsx +++ b/apps/portal/src/features/catalog/components/sim/SimConfigureView.tsx @@ -174,7 +174,8 @@ export function SimConfigureView({
- ¥{(plan.monthlyPrice ?? plan.unitPrice ?? plan.oneTimePrice ?? 0).toLocaleString()}/mo + ¥{(plan.monthlyPrice ?? plan.unitPrice ?? plan.oneTimePrice ?? 0).toLocaleString()} + /mo
{plan.simHasFamilyDiscount && (
Discounted Price
diff --git a/apps/portal/src/features/catalog/hooks/useConfigureParams.ts b/apps/portal/src/features/catalog/hooks/useConfigureParams.ts index 8c7fba93..37b79d84 100644 --- a/apps/portal/src/features/catalog/hooks/useConfigureParams.ts +++ b/apps/portal/src/features/catalog/hooks/useConfigureParams.ts @@ -19,9 +19,7 @@ const parseActivationType = (value: string | null): ActivationType | null => { return null; }; -const parsePortingGender = ( - value: string | null -): MnpData["portingGender"] | undefined => { +const parsePortingGender = (value: string | null): MnpData["portingGender"] | undefined => { if (value === "Male" || value === "Female" || value === "Corporate/Other") { return value; } @@ -41,9 +39,7 @@ export function useInternetConfigureParams() { const params = useSearchParams(); const accessModeParam = params.get("accessMode"); const accessMode: AccessMode | null = - accessModeParam === "IPoE-BYOR" || accessModeParam === "PPPoE" - ? accessModeParam - : null; + accessModeParam === "IPoE-BYOR" || accessModeParam === "PPPoE" ? accessModeParam : null; const installationSku = params.get("installationSku"); const addonSkus = params.getAll("addonSku"); @@ -59,11 +55,9 @@ export function useSimConfigureParams() { const simType = parseSimCardType(params.get("simType")); const activationType = parseActivationType(params.get("activationType")); - const scheduledAt = - coalesce(params.get("scheduledAt"), params.get("scheduledDate")) ?? null; + const scheduledAt = coalesce(params.get("scheduledAt"), params.get("scheduledDate")) ?? null; const addonSkus = params.getAll("addonSku"); - const isMnp = - coalesce(params.get("isMnp"), params.get("wantsMnp"))?.toLowerCase() === "true"; + const isMnp = coalesce(params.get("isMnp"), params.get("wantsMnp"))?.toLowerCase() === "true"; const eid = params.get("eid") ?? null; const mnp: Partial = { @@ -86,14 +80,8 @@ export function useSimConfigureParams() { params.get("mvnoAccountNumber"), params.get("mnp_mvnoAccountNumber") ), - portingLastName: coalesce( - params.get("portingLastName"), - params.get("mnp_portingLastName") - ), - portingFirstName: coalesce( - params.get("portingFirstName"), - params.get("mnp_portingFirstName") - ), + portingLastName: coalesce(params.get("portingLastName"), params.get("mnp_portingLastName")), + portingFirstName: coalesce(params.get("portingFirstName"), params.get("mnp_portingFirstName")), portingLastNameKatakana: coalesce( params.get("portingLastNameKatakana"), params.get("mnp_portingLastNameKatakana") diff --git a/apps/portal/src/features/catalog/hooks/useInternetConfigure.ts b/apps/portal/src/features/catalog/hooks/useInternetConfigure.ts index 618692a3..6aad88e2 100644 --- a/apps/portal/src/features/catalog/hooks/useInternetConfigure.ts +++ b/apps/portal/src/features/catalog/hooks/useInternetConfigure.ts @@ -10,9 +10,7 @@ import type { } from "@customer-portal/domain/catalog"; type InstallationTerm = NonNullable< - NonNullable[ - "installationTerm" - ] + NonNullable["installationTerm"] >; type InternetAccessMode = "IPoE-BYOR" | "PPPoE"; diff --git a/apps/portal/src/features/catalog/hooks/useSimConfigure.ts b/apps/portal/src/features/catalog/hooks/useSimConfigure.ts index fca51102..3e0c43e7 100644 --- a/apps/portal/src/features/catalog/hooks/useSimConfigure.ts +++ b/apps/portal/src/features/catalog/hooks/useSimConfigure.ts @@ -12,7 +12,10 @@ import { type MnpData, } from "@customer-portal/domain/sim"; import { buildSimOrderConfigurations } from "@customer-portal/domain/orders"; -import type { SimCatalogProduct, SimActivationFeeCatalogItem } from "@customer-portal/domain/catalog"; +import type { + SimCatalogProduct, + SimActivationFeeCatalogItem, +} from "@customer-portal/domain/catalog"; export type UseSimConfigureResult = { // data @@ -73,9 +76,7 @@ const parseActivationTypeParam = (value: string | null): ActivationType | null = return null; }; -const parsePortingGenderParam = ( - value: string | null -): MnpData["portingGender"] | undefined => { +const parsePortingGenderParam = (value: string | null): MnpData["portingGender"] | undefined => { if (value === "Male" || value === "Female" || value === "Corporate/Other") { return value; } @@ -161,9 +162,7 @@ export function useSimConfigure(planId?: string): UseSimConfigureResult { const resolvedParams = useMemo(() => { const initialSimType = - configureParams.simType ?? - parseSimCardTypeParam(parsedSearchParams.get("simType")) ?? - "eSIM"; + configureParams.simType ?? parseSimCardTypeParam(parsedSearchParams.get("simType")) ?? "eSIM"; const initialActivationType = configureParams.activationType ?? parseActivationTypeParam(parsedSearchParams.get("activationType")) ?? @@ -203,14 +202,20 @@ export function useSimConfigure(planId?: string): UseSimConfigureResult { const mnp = configureParams.mnp; const resolvedMnpData: MnpData = wantsMnp ? { - reservationNumber: paramFallback(mnp.reservationNumber, parsedSearchParams.get("mnpNumber")), + reservationNumber: paramFallback( + mnp.reservationNumber, + parsedSearchParams.get("mnpNumber") + ), expiryDate: paramFallback(mnp.expiryDate, parsedSearchParams.get("mnpExpiry")), phoneNumber: paramFallback(mnp.phoneNumber, parsedSearchParams.get("mnpPhone")), mvnoAccountNumber: paramFallback( mnp.mvnoAccountNumber, parsedSearchParams.get("mvnoAccountNumber") ), - portingLastName: paramFallback(mnp.portingLastName, parsedSearchParams.get("portingLastName")), + portingLastName: paramFallback( + mnp.portingLastName, + parsedSearchParams.get("portingLastName") + ), portingFirstName: paramFallback( mnp.portingFirstName, parsedSearchParams.get("portingFirstName") @@ -413,10 +418,7 @@ export function useSimConfigure(planId?: string): UseSimConfigureResult { params.set("activationType", values.activationType); if (values.scheduledActivationDate) { params.set("scheduledDate", values.scheduledActivationDate); - params.set( - "scheduledAt", - values.scheduledActivationDate.replace(/-/g, "") - ); + params.set("scheduledAt", values.scheduledActivationDate.replace(/-/g, "")); } else { params.delete("scheduledDate"); params.delete("scheduledAt"); @@ -438,16 +440,13 @@ export function useSimConfigure(planId?: string): UseSimConfigureResult { if (simConfig.mnpPhone) params.set("mnpPhone", simConfig.mnpPhone); if (simConfig.mvnoAccountNumber) params.set("mvnoAccountNumber", simConfig.mvnoAccountNumber); - if (simConfig.portingLastName) - params.set("portingLastName", simConfig.portingLastName); - if (simConfig.portingFirstName) - params.set("portingFirstName", simConfig.portingFirstName); + if (simConfig.portingLastName) params.set("portingLastName", simConfig.portingLastName); + if (simConfig.portingFirstName) params.set("portingFirstName", simConfig.portingFirstName); if (simConfig.portingLastNameKatakana) params.set("portingLastNameKatakana", simConfig.portingLastNameKatakana); if (simConfig.portingFirstNameKatakana) params.set("portingFirstNameKatakana", simConfig.portingFirstNameKatakana); - if (simConfig.portingGender) - params.set("portingGender", simConfig.portingGender); + if (simConfig.portingGender) params.set("portingGender", simConfig.portingGender); if (simConfig.portingDateOfBirth) params.set("portingDateOfBirth", simConfig.portingDateOfBirth); } else { diff --git a/apps/portal/src/features/catalog/services/catalog.service.ts b/apps/portal/src/features/catalog/services/catalog.service.ts index 57a28ddf..8e315c33 100644 --- a/apps/portal/src/features/catalog/services/catalog.service.ts +++ b/apps/portal/src/features/catalog/services/catalog.service.ts @@ -33,13 +33,17 @@ export const catalogService = { }, async getInternetInstallations(): Promise { - const response = await apiClient.GET("/api/catalog/internet/installations"); + const response = await apiClient.GET( + "/api/catalog/internet/installations" + ); const data = getDataOrDefault(response, []); return internetInstallationCatalogItemSchema.array().parse(data); }, async getInternetAddons(): Promise { - const response = await apiClient.GET("/api/catalog/internet/addons"); + const response = await apiClient.GET( + "/api/catalog/internet/addons" + ); const data = getDataOrDefault(response, []); return internetAddonCatalogItemSchema.array().parse(data); }, @@ -51,7 +55,9 @@ export const catalogService = { }, async getSimActivationFees(): Promise { - const response = await apiClient.GET("/api/catalog/sim/activation-fees"); + const response = await apiClient.GET( + "/api/catalog/sim/activation-fees" + ); const data = getDataOrDefault(response, []); return simActivationFeeCatalogItemSchema.array().parse(data); }, diff --git a/apps/portal/src/features/catalog/utils/pricing.ts b/apps/portal/src/features/catalog/utils/pricing.ts index 858b66c9..e348f6e7 100644 --- a/apps/portal/src/features/catalog/utils/pricing.ts +++ b/apps/portal/src/features/catalog/utils/pricing.ts @@ -30,4 +30,3 @@ export function getDisplayPrice(item: CatalogProductBase): PriceInfo | null { currency, }; } - diff --git a/apps/portal/src/features/catalog/views/InternetPlans.tsx b/apps/portal/src/features/catalog/views/InternetPlans.tsx index 5faf12a0..a23f0ece 100644 --- a/apps/portal/src/features/catalog/views/InternetPlans.tsx +++ b/apps/portal/src/features/catalog/views/InternetPlans.tsx @@ -118,11 +118,11 @@ export function InternetPlansContainer() {
{/* Enhanced Back Button */}
-
- {/* Enhanced Header */} -
- {/* Background decoration */} -
-
-
-
- -

- Choose Your Internet Plan -

-

- High-speed fiber internet with reliable connectivity for your home or business -

+ {/* Enhanced Header */} +
+ {/* Background decoration */} +
+
+
+
- {eligibility && ( -
-
- {getEligibilityIcon(eligibility)} - Available for: {eligibility} +

+ Choose Your Internet Plan +

+

+ High-speed fiber internet with reliable connectivity for your home or business +

+ + {eligibility && ( +
+
+ {getEligibilityIcon(eligibility)} + Available for: {eligibility} +
+

+ Plans shown are tailored to your house type and local infrastructure +

-

- Plans shown are tailored to your house type and local infrastructure + )} +

+ + {hasActiveInternet && ( + +

+ You already have an Internet subscription with us. If you want another subscription + for a different residence, please{" "} + + contact us + + .

+ + )} + + {plans.length > 0 ? ( + <> +
+ {plans.map(plan => ( + + ))} +
+ + +
    +
  • Theoretical internet speed is the same for all three packages
  • +
  • + One-time fee (¥22,800) can be paid upfront or in 12- or 24-month installments +
  • +
  • + Home phone line (Hikari Denwa) can be added to GOLD or PLATINUM plans + (¥450/month + ¥1,000-3,000 one-time) +
  • +
  • In-home technical assistance available (¥15,000 onsite visiting fee)
  • +
+
+ + ) : ( +
+ +

No Plans Available

+

+ We couldn't find any internet plans available for your location at this time. +

+
)}
- - {hasActiveInternet && ( - -

- You already have an Internet subscription with us. If you want another subscription - for a different residence, please{" "} - - contact us - - . -

-
- )} - - {plans.length > 0 ? ( - <> -
- {plans.map(plan => ( - - ))} -
- - -
    -
  • Theoretical internet speed is the same for all three packages
  • -
  • - One-time fee (¥22,800) can be paid upfront or in 12- or 24-month installments -
  • -
  • - Home phone line (Hikari Denwa) can be added to GOLD or PLATINUM plans (¥450/month - + ¥1,000-3,000 one-time) -
  • -
  • In-home technical assistance available (¥15,000 onsite visiting fee)
  • -
-
- - ) : ( -
- -

No Plans Available

-

- We couldn't find any internet plans available for your location at this time. -

- -
- )} -
); diff --git a/apps/portal/src/features/checkout/hooks/useCheckout.ts b/apps/portal/src/features/checkout/hooks/useCheckout.ts index 80119a42..99bc3c53 100644 --- a/apps/portal/src/features/checkout/hooks/useCheckout.ts +++ b/apps/portal/src/features/checkout/hooks/useCheckout.ts @@ -7,7 +7,11 @@ import { ordersService } from "@/features/orders/services/orders.service"; import { usePaymentMethods } from "@/features/billing/hooks/useBilling"; import { usePaymentRefresh } from "@/features/billing/hooks/usePaymentRefresh"; import type { CatalogProductBase } from "@customer-portal/domain/catalog"; -import { createLoadingState, createSuccessState, createErrorState } from "@customer-portal/domain/toolkit"; +import { + createLoadingState, + createSuccessState, + createErrorState, +} from "@customer-portal/domain/toolkit"; import type { AsyncState } from "@customer-portal/domain/toolkit"; import { useActiveSubscriptions } from "@/features/subscriptions/hooks/useSubscriptions"; import { @@ -261,8 +265,7 @@ export function useCheckout() { ); } catch (error) { if (mounted) { - const reason = - error instanceof Error ? error.message : "Failed to load checkout data"; + const reason = error instanceof Error ? error.message : "Failed to load checkout data"; setCheckoutState(createErrorState(new Error(reason))); } } @@ -301,7 +304,10 @@ export function useCheckout() { ? { simType: selections.simType as OrderConfigurations["simType"] } : {}), ...(selections.activationType - ? { activationType: selections.activationType as OrderConfigurations["activationType"] } + ? { + activationType: + selections.activationType as OrderConfigurations["activationType"], + } : {}), ...(selections.scheduledAt ? { scheduledAt: selections.scheduledAt } : {}), ...(selections.eid ? { eid: selections.eid } : {}), @@ -313,7 +319,9 @@ export function useCheckout() { ? { mvnoAccountNumber: selections.mvnoAccountNumber } : {}), ...(selections.portingLastName ? { portingLastName: selections.portingLastName } : {}), - ...(selections.portingFirstName ? { portingFirstName: selections.portingFirstName } : {}), + ...(selections.portingFirstName + ? { portingFirstName: selections.portingFirstName } + : {}), ...(selections.portingLastNameKatakana ? { portingLastNameKatakana: selections.portingLastNameKatakana } : {}), @@ -366,7 +374,9 @@ export function useCheckout() { if (orderType === ORDER_TYPE.SIM) { if (!configurations) { - throw new Error("SIM configuration is incomplete. Please restart the SIM configuration flow."); + throw new Error( + "SIM configuration is incomplete. Please restart the SIM configuration flow." + ); } if (configurations?.simType === "eSIM" && !configurations.eid) { throw new Error( diff --git a/apps/portal/src/features/dashboard/components/UpcomingPaymentBanner.tsx b/apps/portal/src/features/dashboard/components/UpcomingPaymentBanner.tsx index 2a1fbef3..e2425018 100644 --- a/apps/portal/src/features/dashboard/components/UpcomingPaymentBanner.tsx +++ b/apps/portal/src/features/dashboard/components/UpcomingPaymentBanner.tsx @@ -14,7 +14,7 @@ interface UpcomingPaymentBannerProps { export function UpcomingPaymentBanner({ invoice, onPay, loading }: UpcomingPaymentBannerProps) { const { formatCurrency } = useFormatCurrency(); - + return (
diff --git a/apps/portal/src/features/dashboard/hooks/useDashboardSummary.ts b/apps/portal/src/features/dashboard/hooks/useDashboardSummary.ts index 58b20d55..214a2114 100644 --- a/apps/portal/src/features/dashboard/hooks/useDashboardSummary.ts +++ b/apps/portal/src/features/dashboard/hooks/useDashboardSummary.ts @@ -42,10 +42,7 @@ export function useDashboardSummary() { try { const response = await apiClient.GET("/api/me/summary"); if (!response.data) { - throw new DashboardDataError( - "FETCH_ERROR", - "Dashboard summary response was empty" - ); + throw new DashboardDataError("FETCH_ERROR", "Dashboard summary response was empty"); } const parsed = dashboardSummarySchema.safeParse(response.data); if (!parsed.success) { diff --git a/apps/portal/src/features/orders/services/orders.service.ts b/apps/portal/src/features/orders/services/orders.service.ts index 13e0a8f7..20aca527 100644 --- a/apps/portal/src/features/orders/services/orders.service.ts +++ b/apps/portal/src/features/orders/services/orders.service.ts @@ -6,10 +6,7 @@ import { type OrderDetails, type OrderSummary, } from "@customer-portal/domain/orders"; -import { - assertSuccess, - type DomainApiResponse, -} from "@/lib/api/response-helpers"; +import { assertSuccess, type DomainApiResponse } from "@/lib/api/response-helpers"; async function createOrder(payload: CreateOrderRequest): Promise<{ sfOrderId: string }> { const body: CreateOrderRequest = { diff --git a/apps/portal/src/features/orders/views/OrderDetail.tsx b/apps/portal/src/features/orders/views/OrderDetail.tsx index 415beeb6..722f617f 100644 --- a/apps/portal/src/features/orders/views/OrderDetail.tsx +++ b/apps/portal/src/features/orders/views/OrderDetail.tsx @@ -186,9 +186,7 @@ export function OrderDetailContainer() {
{productName}
SKU: {sku}
- {billingCycle && ( -
{billingCycle}
- )} + {billingCycle &&
{billingCycle}
}
Qty: {item.quantity}
diff --git a/apps/portal/src/features/sim-management/components/SimDetailsCard.tsx b/apps/portal/src/features/sim-management/components/SimDetailsCard.tsx index 62676b78..12b6d913 100644 --- a/apps/portal/src/features/sim-management/components/SimDetailsCard.tsx +++ b/apps/portal/src/features/sim-management/components/SimDetailsCard.tsx @@ -48,13 +48,7 @@ const formatQuota = (remainingMb: number) => { return `${remainingMb.toFixed(0)} MB`; }; -const FeatureToggleRow = ({ - label, - enabled, -}: { - label: string; - enabled: boolean; -}) => ( +const FeatureToggleRow = ({ label, enabled }: { label: string; enabled: boolean }) => (
{label} ); const statusClass = statusBadgeClass[normalizedStatus] ?? "bg-gray-100 text-gray-800"; - const containerClasses = embedded - ? "" - : "bg-white shadow-lg rounded-xl border border-gray-100"; + const containerClasses = embedded ? "" : "bg-white shadow-lg rounded-xl border border-gray-100"; return (
diff --git a/apps/portal/src/features/subscriptions/components/SubscriptionCard.tsx b/apps/portal/src/features/subscriptions/components/SubscriptionCard.tsx index 06961dea..90edffc0 100644 --- a/apps/portal/src/features/subscriptions/components/SubscriptionCard.tsx +++ b/apps/portal/src/features/subscriptions/components/SubscriptionCard.tsx @@ -78,7 +78,7 @@ const getBillingCycleLabel = (cycle: string) => { export const SubscriptionCard = forwardRef( ({ subscription, variant = "list", showActions = true, onViewClick, className }, ref) => { const { formatCurrency } = useFormatCurrency(); - + const handleViewClick = () => { if (onViewClick) { onViewClick(subscription); @@ -111,9 +111,7 @@ export const SubscriptionCard = forwardRef

Price

-

- {formatCurrency(subscription.amount)} -

+

{formatCurrency(subscription.amount)}

{getBillingCycleLabel(subscription.cycle)}

@@ -171,9 +169,7 @@ export const SubscriptionCard = forwardRef
-

- {formatCurrency(subscription.amount)} -

+

{formatCurrency(subscription.amount)}

{getBillingCycleLabel(subscription.cycle)}

diff --git a/apps/portal/src/features/subscriptions/components/SubscriptionDetails.tsx b/apps/portal/src/features/subscriptions/components/SubscriptionDetails.tsx index 478ad2e9..e807fb31 100644 --- a/apps/portal/src/features/subscriptions/components/SubscriptionDetails.tsx +++ b/apps/portal/src/features/subscriptions/components/SubscriptionDetails.tsx @@ -104,7 +104,7 @@ const isVpnService = (productName: string) => { export const SubscriptionDetails = forwardRef( ({ subscription, showServiceSpecificSections = true, className }, ref) => { const { formatCurrency } = useFormatCurrency(); - + return (
{/* Main Details Card */} diff --git a/apps/portal/src/features/subscriptions/services/sim-actions.service.ts b/apps/portal/src/features/subscriptions/services/sim-actions.service.ts index 43d7c0bf..46720201 100644 --- a/apps/portal/src/features/subscriptions/services/sim-actions.service.ts +++ b/apps/portal/src/features/subscriptions/services/sim-actions.service.ts @@ -1,9 +1,9 @@ import { apiClient } from "@/lib/api"; import { simInfoSchema, type SimInfo } from "@customer-portal/domain/sim"; -import type { +import type { SimTopUpRequest, SimPlanChangeRequest, - SimCancelRequest + SimCancelRequest, } from "@customer-portal/domain/sim"; // Types imported from domain - no duplication diff --git a/apps/portal/src/features/subscriptions/views/SubscriptionDetail.tsx b/apps/portal/src/features/subscriptions/views/SubscriptionDetail.tsx index 2e044850..df3cf060 100644 --- a/apps/portal/src/features/subscriptions/views/SubscriptionDetail.tsx +++ b/apps/portal/src/features/subscriptions/views/SubscriptionDetail.tsx @@ -22,14 +22,12 @@ import { useSubscription } from "@/features/subscriptions/hooks"; import { InvoicesList } from "@/features/billing/components/InvoiceList/InvoiceList"; import { Formatting } from "@customer-portal/domain/toolkit"; -const { formatCurrency: sharedFormatCurrency, getCurrencyLocale } = Formatting; +const { formatCurrency: sharedFormatCurrency } = Formatting; import { SimManagementSection } from "@/features/sim-management"; export function SubscriptionDetailContainer() { const params = useParams(); const searchParams = useSearchParams(); - const [currentPage, setCurrentPage] = useState(1); - const itemsPerPage = 10; const [showInvoices, setShowInvoices] = useState(true); const [showSimManagement, setShowSimManagement] = useState(false); @@ -74,51 +72,6 @@ export function SubscriptionDetailContainer() { } }; - const getStatusColor = (status: string) => { - switch (status) { - case "Active": - return "bg-green-100 text-green-800"; - case "Suspended": - return "bg-yellow-100 text-yellow-800"; - case "Terminated": - return "bg-red-100 text-red-800"; - case "Cancelled": - return "bg-gray-100 text-gray-800"; - case "Pending": - return "bg-blue-100 text-blue-800"; - default: - return "bg-gray-100 text-gray-800"; - } - }; - - const getInvoiceStatusIcon = (status: string) => { - switch (status) { - case "Paid": - return ; - case "Overdue": - return ; - case "Unpaid": - return ; - default: - return ; - } - }; - - const getInvoiceStatusColor = (status: string) => { - switch (status) { - case "Paid": - return "bg-green-100 text-green-800"; - case "Overdue": - return "bg-red-100 text-red-800"; - case "Unpaid": - return "bg-yellow-100 text-yellow-800"; - case "Cancelled": - return "bg-gray-100 text-gray-800"; - default: - return "bg-gray-100 text-gray-800"; - } - }; - const formatDate = (dateString: string | undefined) => { if (!dateString) return "N/A"; try { @@ -128,8 +81,7 @@ export function SubscriptionDetailContainer() { } }; - const formatCurrency = (amount: number) => - sharedFormatCurrency(amount || 0); + const formatCurrency = (amount: number) => sharedFormatCurrency(amount || 0); const formatBillingLabel = (cycle: string) => { switch (cycle) { diff --git a/apps/portal/src/features/subscriptions/views/SubscriptionsList.tsx b/apps/portal/src/features/subscriptions/views/SubscriptionsList.tsx index ff3176df..ff033054 100644 --- a/apps/portal/src/features/subscriptions/views/SubscriptionsList.tsx +++ b/apps/portal/src/features/subscriptions/views/SubscriptionsList.tsx @@ -11,7 +11,6 @@ import { StatusPill } from "@/components/atoms/status-pill"; import { SubCard } from "@/components/molecules/SubCard/SubCard"; import { SearchFilterBar } from "@/components/molecules/SearchFilterBar/SearchFilterBar"; import { LoadingTable } from "@/components/atoms/loading-skeleton"; -import { ErrorState } from "@/components/atoms/error-state"; import { AsyncBlock } from "@/components/molecules/AsyncBlock/AsyncBlock"; import { ServerIcon, @@ -23,7 +22,6 @@ import { } from "@heroicons/react/24/outline"; import { format } from "date-fns"; import { useSubscriptions, useSubscriptionStats } from "@/features/subscriptions/hooks"; -import { Formatting } from "@customer-portal/domain/toolkit"; import type { Subscription } from "@customer-portal/domain/subscriptions"; import { useFormatCurrency } from "@/lib/hooks/useFormatCurrency"; @@ -135,9 +133,7 @@ export function SubscriptionsListContainer() { header: "Price", render: (s: Subscription) => (
- - {formatCurrency(s.amount)} - + {formatCurrency(s.amount)}
{s.cycle === "Monthly" ? "per month" diff --git a/apps/portal/src/lib/api/helpers.ts b/apps/portal/src/lib/api/helpers.ts index d51213e6..3ddf8706 100644 --- a/apps/portal/src/lib/api/helpers.ts +++ b/apps/portal/src/lib/api/helpers.ts @@ -19,10 +19,7 @@ export function getDataOrThrow( /** * Extract data from API response or return default value */ -export function getDataOrDefault( - response: { data?: T; error?: unknown }, - defaultValue: T -): T { +export function getDataOrDefault(response: { data?: T; error?: unknown }, defaultValue: T): T { return response.data ?? defaultValue; } @@ -32,4 +29,3 @@ export function getDataOrDefault( export function isApiError(error: unknown): error is Error { return error instanceof Error; } - diff --git a/apps/portal/src/lib/api/index.ts b/apps/portal/src/lib/api/index.ts index 66e88497..b5734ba9 100644 --- a/apps/portal/src/lib/api/index.ts +++ b/apps/portal/src/lib/api/index.ts @@ -1,5 +1,11 @@ export { createClient, resolveBaseUrl } from "./runtime/client"; -export type { ApiClient, AuthHeaderResolver, CreateClientOptions, QueryParams, PathParams } from "./runtime/client"; +export type { + ApiClient, + AuthHeaderResolver, + CreateClientOptions, + QueryParams, + PathParams, +} from "./runtime/client"; export { ApiError, isApiError } from "./runtime/client"; // Re-export API helpers diff --git a/apps/portal/src/lib/api/response-helpers.ts b/apps/portal/src/lib/api/response-helpers.ts index e0b1e5a2..cd453585 100644 --- a/apps/portal/src/lib/api/response-helpers.ts +++ b/apps/portal/src/lib/api/response-helpers.ts @@ -8,7 +8,7 @@ import type { ZodTypeAny, infer as ZodInfer } from "zod"; /** * API Response Helper Types and Functions - * + * * Generic utilities for working with API responses */ @@ -34,12 +34,9 @@ export function getNullableData(response: ApiResponse): T | null { /** * Extract data from API response or throw error */ -export function getDataOrThrow( - response: ApiResponse, - errorMessage?: string -): T { +export function getDataOrThrow(response: ApiResponse, errorMessage?: string): T { if (response.error || response.data === undefined) { - throw new Error(errorMessage || 'Failed to fetch data'); + throw new Error(errorMessage || "Failed to fetch data"); } return response.data; } @@ -47,10 +44,7 @@ export function getDataOrThrow( /** * Extract data from API response or return default value */ -export function getDataOrDefault( - response: ApiResponse, - defaultValue: T -): T { +export function getDataOrDefault(response: ApiResponse, defaultValue: T): T { return response.data ?? defaultValue; } @@ -77,7 +71,10 @@ export function assertSuccess(response: DomainApiResponse): ApiSuccessResp throw new Error(response.error.message); } -export function parseDomainResponse(response: DomainApiResponse, parser?: (payload: T) => T): T { +export function parseDomainResponse( + response: DomainApiResponse, + parser?: (payload: T) => T +): T { const success = assertSuccess(response); return parser ? parser(success.data) : success.data; } diff --git a/apps/portal/src/lib/api/runtime/client.ts b/apps/portal/src/lib/api/runtime/client.ts index 13dfeef5..6587e4ef 100644 --- a/apps/portal/src/lib/api/runtime/client.ts +++ b/apps/portal/src/lib/api/runtime/client.ts @@ -13,14 +13,7 @@ export class ApiError extends Error { export const isApiError = (error: unknown): error is ApiError => error instanceof ApiError; -export type HttpMethod = - | "GET" - | "POST" - | "PUT" - | "PATCH" - | "DELETE" - | "HEAD" - | "OPTIONS"; +export type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD" | "OPTIONS"; export type PathParams = Record; export type QueryPrimitive = string | number | boolean; diff --git a/apps/portal/src/lib/constants/countries.ts b/apps/portal/src/lib/constants/countries.ts index d1dfbe98..1b1968a5 100644 --- a/apps/portal/src/lib/constants/countries.ts +++ b/apps/portal/src/lib/constants/countries.ts @@ -13,11 +13,7 @@ const normalizedCountries = countries return { code, name: commonName, - searchKeys: [ - commonName, - country.name.official, - ...(country.altSpellings ?? []), - ] + searchKeys: [commonName, country.name.official, ...(country.altSpellings ?? [])] .filter(Boolean) .map(entry => entry.toLowerCase()), }; diff --git a/apps/portal/src/lib/hooks/useFormatCurrency.ts b/apps/portal/src/lib/hooks/useFormatCurrency.ts index 183906a0..dde4cd9f 100644 --- a/apps/portal/src/lib/hooks/useFormatCurrency.ts +++ b/apps/portal/src/lib/hooks/useFormatCurrency.ts @@ -11,7 +11,7 @@ export function useFormatCurrency() { // Show loading state or fallback return "¥" + amount.toLocaleString(); } - + if (error) { // Fallback to JPY if there's an error return baseFormatCurrency(amount, "JPY", "¥"); diff --git a/apps/portal/src/lib/providers.tsx b/apps/portal/src/lib/providers.tsx index 3896b790..5b2b6e4a 100644 --- a/apps/portal/src/lib/providers.tsx +++ b/apps/portal/src/lib/providers.tsx @@ -8,7 +8,7 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; import { useState } from "react"; -import { ApiError, isApiError } from "@/lib/api/runtime/client"; +import { isApiError } from "@/lib/api/runtime/client"; export function QueryProvider({ children }: { children: React.ReactNode }) { const [queryClient] = useState( diff --git a/apps/portal/src/lib/utils/error-handling.ts b/apps/portal/src/lib/utils/error-handling.ts index 056dafeb..ed9b7cc1 100644 --- a/apps/portal/src/lib/utils/error-handling.ts +++ b/apps/portal/src/lib/utils/error-handling.ts @@ -139,7 +139,7 @@ function parseClientApiError(error: ClientApiError): ApiErrorInfo | null { const status = error.response?.status; const parsedBody = parseRawErrorBody(error.body); -const payloadInfo = parsedBody ? deriveInfoFromPayload(parsedBody, status) : null; + const payloadInfo = parsedBody ? deriveInfoFromPayload(parsedBody, status) : null; if (payloadInfo) { return payloadInfo;