diff --git a/apps/bff/src/integrations/freebit/services/freebit-auth.service.ts b/apps/bff/src/integrations/freebit/services/freebit-auth.service.ts index 405a0172..64f35883 100644 --- a/apps/bff/src/integrations/freebit/services/freebit-auth.service.ts +++ b/apps/bff/src/integrations/freebit/services/freebit-auth.service.ts @@ -69,7 +69,7 @@ export class FreebitAuthService { throw new Error("Freebit API not configured: FREEBIT_OEM_KEY is missing"); } - const request = FreebitProvider.schemas.auth.parse({ + const request: FreebitAuthRequest = FreebitProvider.schemas.auth.parse({ oemId: this.config.oemId, oemKey: this.config.oemKey, }); @@ -84,8 +84,9 @@ export class FreebitAuthService { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } - const json = (await response.json()) as unknown; - const data = FreebitProvider.mapper.transformFreebitAuthResponse(json); + const json: unknown = await response.json(); + const data: FreebitAuthResponse = + FreebitProvider.mapper.transformFreebitAuthResponse(json); if (data.resultCode !== "100" || !data.authKey) { throw new FreebitError( 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 2fa728f7..36354534 100644 --- a/apps/bff/src/integrations/whmcs/services/whmcs-currency.service.ts +++ b/apps/bff/src/integrations/whmcs/services/whmcs-currency.service.ts @@ -165,10 +165,12 @@ export class WhmcsCurrencyService implements OnModuleInit { ); // Extract currency indices - const currencyIndices = currencyKeys.map(key => { - const match = key.match(/currencies\[currency\]\[(\d+)\]\[id\]/); - return match ? parseInt(match[1]) : null; - }).filter(index => index !== null) as number[]; + const currencyIndices = currencyKeys + .map(key => { + const match = key.match(/currencies\[currency\]\[(\d+)\]\[id\]/); + return match ? parseInt(match[1], 10) : null; + }) + .filter((index): index is number => index !== null); // Build currency objects from the flat response for (const index of currencyIndices) { 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 90a65b2f..eb9acefd 100644 --- a/apps/bff/src/integrations/whmcs/utils/whmcs-client.utils.ts +++ b/apps/bff/src/integrations/whmcs/utils/whmcs-client.utils.ts @@ -31,7 +31,10 @@ const toNumber = (value: unknown): number | null => { const toOptionalString = (value: unknown): string | undefined => { if (value === undefined || value === null) return undefined; if (typeof value === "string") return value; - return String(value); + if (typeof value === "number" || typeof value === "boolean") { + return String(value); + } + return undefined; }; const toNullableBoolean = (value: unknown): boolean | null | undefined => { 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 94ed376f..9549cc36 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 @@ -138,16 +138,14 @@ export class WhmcsLinkWorkflowService { const createdUser = await this.usersService.create({ email, passwordHash: null, - firstName: String(clientDetails.firstname ?? ""), - lastName: String(clientDetails.lastname ?? ""), - company: String(clientDetails.companyname ?? ""), // Raw WHMCS field name + firstName: clientDetails.firstname ?? "", + lastName: clientDetails.lastname ?? "", + company: clientDetails.companyname ?? "", // Raw WHMCS field name phone: - String( - clientDetails.phonenumberformatted ?? - clientDetails.phonenumber ?? - clientDetails.telephoneNumber ?? - "" - ), // Raw WHMCS field names + clientDetails.phonenumberformatted ?? + clientDetails.phonenumber ?? + clientDetails.telephoneNumber ?? + "", emailVerified: true, }); 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 082749b7..41604d96 100644 --- a/apps/bff/src/modules/auth/presentation/http/auth.controller.ts +++ b/apps/bff/src/modules/auth/presentation/http/auth.controller.ts @@ -47,8 +47,9 @@ import { type AuthTokens, } from "@customer-portal/domain/auth"; -type RequestWithCookies = Request & { - cookies: Record; +type CookieValue = string | undefined; +type RequestWithCookies = Omit & { + cookies?: Record; }; const resolveAuthorizationHeader = (req: RequestWithCookies): string | undefined => { diff --git a/apps/bff/src/modules/auth/presentation/http/guards/global-auth.guard.ts b/apps/bff/src/modules/auth/presentation/http/guards/global-auth.guard.ts index b4d4f36c..dba8bd1f 100644 --- a/apps/bff/src/modules/auth/presentation/http/guards/global-auth.guard.ts +++ b/apps/bff/src/modules/auth/presentation/http/guards/global-auth.guard.ts @@ -15,8 +15,9 @@ import { TokenBlacklistService } from "../../../infra/token/token-blacklist.serv import { IS_PUBLIC_KEY } from "../../../decorators/public.decorator"; import { getErrorMessage } from "@bff/core/utils/error.util"; -type RequestWithCookies = Request & { - cookies: Record; +type CookieValue = string | undefined; +type RequestWithCookies = Omit & { + cookies?: Record; }; type RequestWithRoute = RequestWithCookies & { method: string; 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 6c12ca0e..f6272c19 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 @@ -22,14 +22,7 @@ import { Providers as OrderProviders, } from "@customer-portal/domain/orders"; -export interface OrderItemMappingResult { - whmcsItems: any[]; - summary: { - totalItems: number; - serviceItems: number; - activationItems: number; - }; -} +type WhmcsOrderItemMappingResult = ReturnType; export interface OrderFulfillmentStep { step: string; @@ -44,7 +37,7 @@ export interface OrderFulfillmentContext { idempotencyKey: string; validation: OrderFulfillmentValidationResult | null; orderDetails?: OrderDetails; - mappingResult?: OrderItemMappingResult; + mappingResult?: WhmcsOrderItemMappingResult; whmcsResult?: WhmcsOrderResult; steps: OrderFulfillmentStep[]; } @@ -134,7 +127,7 @@ export class OrderFulfillmentOrchestrator { } // Step 3: Execute the main fulfillment workflow as a distributed transaction - let mappingResult: OrderItemMappingResult | undefined; + let mappingResult: WhmcsOrderItemMappingResult | undefined; let whmcsCreateResult: { orderId: number } | undefined; let whmcsAcceptResult: WhmcsOrderResult | undefined; diff --git a/apps/bff/src/modules/subscriptions/sim-management/services/sim-plan.service.ts b/apps/bff/src/modules/subscriptions/sim-management/services/sim-plan.service.ts index ea45e71d..b9d9ad4f 100644 --- a/apps/bff/src/modules/subscriptions/sim-management/services/sim-plan.service.ts +++ b/apps/bff/src/modules/subscriptions/sim-management/services/sim-plan.service.ts @@ -60,9 +60,13 @@ export class SimPlanService { scheduleOrigin: request.scheduledAt ? "user-provided" : "auto-default", }); + if (!scheduledAt) { + throw new BadRequestException("Failed to determine schedule date for plan change"); + } + const result = await this.freebitService.changeSimPlan(account, request.newPlanCode, { assignGlobalIp, - scheduledAt: scheduledAt!, + scheduledAt, }); this.logger.log(`Successfully changed SIM plan for subscription ${subscriptionId}`, { diff --git a/apps/bff/src/modules/users/users.service.ts b/apps/bff/src/modules/users/users.service.ts index 214876d0..e54295f5 100644 --- a/apps/bff/src/modules/users/users.service.ts +++ b/apps/bff/src/modules/users/users.service.ts @@ -328,7 +328,10 @@ export class UsersService { const profile = await this.getProfile(userId); currency = profile.currency_code || currency; } catch (error) { - this.logger.warn("Could not fetch currency from profile", { userId }); + this.logger.warn("Could not fetch currency from profile", { + userId, + error: getErrorMessage(error), + }); } const summary: DashboardSummary = { @@ -529,14 +532,15 @@ export class UsersService { let currency = "JPY"; // Default try { const client = await this.whmcsService.getClientDetails(mapping.whmcsClientId); - if (client && typeof client === 'object' && 'currency_code' in client) { - const currency_code = (client as any).currency_code; - if (currency_code) { - currency = currency_code; - } + const currencyCode = client.currency_code ?? client.raw.currency_code ?? null; + if (currencyCode) { + currency = currencyCode; } } catch (error) { - this.logger.warn("Could not fetch currency from WHMCS client", { userId }); + this.logger.warn("Could not fetch currency from WHMCS client", { + userId, + error: getErrorMessage(error), + }); } const summary: DashboardSummary = { diff --git a/apps/bff/test/catalog-contract.spec.ts b/apps/bff/test/catalog-contract.spec.ts index 3659971b..58ada1f0 100644 --- a/apps/bff/test/catalog-contract.spec.ts +++ b/apps/bff/test/catalog-contract.spec.ts @@ -1,10 +1,23 @@ /// import request from "supertest"; -import { INestApplication } from "@nestjs/common"; +import type { INestApplication } from "@nestjs/common"; import { Test } from "@nestjs/testing"; +import type { Server } from "node:http"; import { AppModule } from "../src/app.module"; -import { parseInternetCatalog } from "@customer-portal/domain/catalog"; +import { + parseInternetCatalog, + internetCatalogResponseSchema, +} from "@customer-portal/domain/catalog"; +import { apiSuccessResponseSchema } from "@customer-portal/domain/common/schema"; + +const internetCatalogApiResponseSchema = apiSuccessResponseSchema(internetCatalogResponseSchema); + +const isRecord = (value: unknown): value is Record => + typeof value === "object" && value !== null; + +const isHttpServer = (value: unknown): value is Server => + isRecord(value) && typeof value.listen === "function" && typeof value.close === "function"; describe("Catalog contract", () => { let app: INestApplication; @@ -23,11 +36,15 @@ describe("Catalog contract", () => { }); it("should return internet catalog matching domain schema", async () => { - const response = await request(app.getHttpServer()).get("/catalog/internet/plans"); + const serverCandidate: unknown = app.getHttpServer(); + if (!isHttpServer(serverCandidate)) { + throw new Error("Expected Nest application to expose an HTTP server"); + } + + const response = await request(serverCandidate).get("/catalog/internet/plans"); expect(response.status).toBe(200); - expect(() => parseInternetCatalog(response.body.data)).not.toThrow(); + const payload = internetCatalogApiResponseSchema.parse(response.body); + expect(() => parseInternetCatalog(payload.data)).not.toThrow(); }); }); - - diff --git a/apps/bff/tsconfig.json b/apps/bff/tsconfig.json index f30f161f..5fa2f06b 100644 --- a/apps/bff/tsconfig.json +++ b/apps/bff/tsconfig.json @@ -27,6 +27,6 @@ "module": "commonjs" } }, - "include": ["src/**/*", "scripts/**/*"], - "exclude": ["node_modules", "dist", "**/*.spec.ts", "**/*.test.ts"] + "include": ["src/**/*", "scripts/**/*", "test/**/*"], + "exclude": ["node_modules", "dist"] } diff --git a/apps/portal/src/features/billing/components/InvoiceTable/InvoiceTable.tsx b/apps/portal/src/features/billing/components/InvoiceTable/InvoiceTable.tsx index eedf0bfb..ef1f1b9f 100644 --- a/apps/portal/src/features/billing/components/InvoiceTable/InvoiceTable.tsx +++ b/apps/portal/src/features/billing/components/InvoiceTable/InvoiceTable.tsx @@ -229,7 +229,9 @@ export function InvoiceTable({