From de35397cf95915bb5e104a10740a1fcd8788d170 Mon Sep 17 00:00:00 2001 From: tema Date: Tue, 9 Sep 2025 15:59:30 +0900 Subject: [PATCH] Refactor order and subscription services for improved type safety and error handling - Updated billing cycle assignment in OrderOrchestrator to ensure proper type handling. - Enhanced error handling in SimManagementService and related components to use more specific types for exceptions. - Standardized error handling across various components to improve consistency and clarity. - Adjusted function signatures in multiple services and controllers to return more precise types, enhancing type safety. --- .../services/order-orchestrator.service.ts | 5 +- .../subscriptions/sim-management.service.ts | 13 ++- .../subscriptions/sim-usage-store.service.ts | 15 ++- .../subscriptions/subscriptions.controller.ts | 2 +- .../src/vendors/freebit/freebit.service.ts | 106 +++++++++++------- .../freebit/interfaces/freebit.types.ts | 26 ++--- .../whmcs/services/whmcs-invoice.service.ts | 10 +- apps/portal/scripts/dev-prep.mjs | 2 + apps/portal/src/app/catalog/sim/page.tsx | 2 +- apps/portal/src/app/orders/[id]/page.tsx | 2 - .../subscriptions/[id]/sim/cancel/page.tsx | 4 +- .../[id]/sim/change-plan/page.tsx | 4 +- .../subscriptions/[id]/sim/top-up/page.tsx | 4 +- .../components/ChangePlanModal.tsx | 4 +- .../components/DataUsageChart.tsx | 8 +- .../sim-management/components/SimActions.tsx | 8 +- .../components/SimFeatureToggles.tsx | 18 +-- .../components/SimManagementSection.tsx | 14 ++- .../sim-management/components/TopUpModal.tsx | 4 +- 19 files changed, 141 insertions(+), 110 deletions(-) diff --git a/apps/bff/src/orders/services/order-orchestrator.service.ts b/apps/bff/src/orders/services/order-orchestrator.service.ts index 37d0cb33..6b69f83e 100644 --- a/apps/bff/src/orders/services/order-orchestrator.service.ts +++ b/apps/bff/src/orders/services/order-orchestrator.service.ts @@ -261,7 +261,10 @@ export class OrderOrchestrator { quantity: item.Quantity, unitPrice: item.UnitPrice, totalPrice: item.TotalPrice, - billingCycle: String(item.PricebookEntry?.Product2?.Billing_Cycle__c || ""), + billingCycle: ((): string | undefined => { + const v = item.PricebookEntry?.Product2?.Billing_Cycle__c; + return typeof v === "string" ? v : undefined; + })(), }); return acc; }, diff --git a/apps/bff/src/subscriptions/sim-management.service.ts b/apps/bff/src/subscriptions/sim-management.service.ts index 1f73d5e0..8a69ab6c 100644 --- a/apps/bff/src/subscriptions/sim-management.service.ts +++ b/apps/bff/src/subscriptions/sim-management.service.ts @@ -1,4 +1,4 @@ -import { Injectable, Inject, BadRequestException, NotFoundException } from "@nestjs/common"; +import { Injectable, Inject, BadRequestException } from "@nestjs/common"; import { Logger } from "nestjs-pino"; import { FreebititService } from "../vendors/freebit/freebit.service"; import { WhmcsService } from "../vendors/whmcs/whmcs.service"; @@ -46,7 +46,10 @@ export class SimManagementService { /** * Debug method to check subscription data for SIM services */ - async debugSimSubscription(userId: string, subscriptionId: number): Promise { + async debugSimSubscription( + userId: string, + subscriptionId: number + ): Promise> { try { const subscription = await this.subscriptionsService.getSubscriptionById( userId, @@ -58,11 +61,11 @@ export class SimManagementService { const expectedEid = "89049032000001000000043598005455"; const simNumberField = Object.entries(subscription.customFields || {}).find( - ([key, value]) => value && value.toString().includes(expectedSimNumber) + ([_key, value]) => value && value.toString().includes(expectedSimNumber) ); const eidField = Object.entries(subscription.customFields || {}).find( - ([key, value]) => value && value.toString().includes(expectedEid) + ([_key, value]) => value && value.toString().includes(expectedEid) ); return { @@ -199,7 +202,7 @@ export class SimManagementService { // Check if any field contains the expected SIM number const expectedSimNumber = "02000331144508"; const foundSimNumber = Object.entries(subscription.customFields || {}).find( - ([key, value]) => value && value.toString().includes(expectedSimNumber) + ([_key, value]) => value && value.toString().includes(expectedSimNumber) ); if (foundSimNumber) { diff --git a/apps/bff/src/subscriptions/sim-usage-store.service.ts b/apps/bff/src/subscriptions/sim-usage-store.service.ts index 3628602f..3af5bc67 100644 --- a/apps/bff/src/subscriptions/sim-usage-store.service.ts +++ b/apps/bff/src/subscriptions/sim-usage-store.service.ts @@ -19,13 +19,16 @@ export class SimUsageStoreService { async upsertToday(account: string, usageMb: number, date?: Date): Promise { const day = this.normalizeDate(date); try { - await (this.prisma as any).simUsageDaily.upsert({ - where: { account_date: { account, date: day } as any }, + await this.prisma.simUsageDaily.upsert({ + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error composite unique input type depends on Prisma schema + where: { account_date: { account, date: day } as unknown }, update: { usageMb }, create: { account, date: day, usageMb }, }); - } catch (e: any) { - this.logger.error("Failed to upsert daily usage", { account, error: e?.message }); + } catch (e: unknown) { + const message = e instanceof Error ? e.message : String(e); + this.logger.error("Failed to upsert daily usage", { account, error: message }); } } @@ -36,7 +39,7 @@ export class SimUsageStoreService { const end = this.normalizeDate(); const start = new Date(end); start.setUTCDate(end.getUTCDate() - (days - 1)); - const rows = (await (this.prisma as any).simUsageDaily.findMany({ + const rows = (await this.prisma.simUsageDaily.findMany({ where: { account, date: { gte: start, lte: end } }, orderBy: { date: "desc" }, })) as Array<{ date: Date; usageMb: number }>; @@ -46,7 +49,7 @@ export class SimUsageStoreService { async cleanupPreviousMonths(): Promise { const now = new Date(); const firstOfMonth = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1)); - const result = await (this.prisma as any).simUsageDaily.deleteMany({ + const result = await this.prisma.simUsageDaily.deleteMany({ where: { date: { lt: firstOfMonth } }, }); return result.count; diff --git a/apps/bff/src/subscriptions/subscriptions.controller.ts b/apps/bff/src/subscriptions/subscriptions.controller.ts index 7a4de412..c1ae7346 100644 --- a/apps/bff/src/subscriptions/subscriptions.controller.ts +++ b/apps/bff/src/subscriptions/subscriptions.controller.ts @@ -204,7 +204,7 @@ export class SubscriptionsController { async debugSimSubscription( @Request() req: RequestWithUser, @Param("id", ParseIntPipe) subscriptionId: number - ) { + ): Promise> { return this.simManagementService.debugSimSubscription(req.user.id, subscriptionId); } diff --git a/apps/bff/src/vendors/freebit/freebit.service.ts b/apps/bff/src/vendors/freebit/freebit.service.ts index 854ad8db..c33910da 100644 --- a/apps/bff/src/vendors/freebit/freebit.service.ts +++ b/apps/bff/src/vendors/freebit/freebit.service.ts @@ -6,7 +6,7 @@ import { } from "@nestjs/common"; import { ConfigService } from "@nestjs/config"; import { Logger } from "nestjs-pino"; -import { +import type { FreebititConfig, FreebititAuthRequest, FreebititAuthResponse, @@ -22,14 +22,13 @@ import { FreebititPlanChangeResponse, FreebititCancelPlanRequest, FreebititCancelPlanResponse, - FreebititEsimReissueRequest, - FreebititEsimReissueResponse, FreebititEsimAddAccountRequest, FreebititEsimAddAccountResponse, + FreebititEsimAccountActivationRequest, + FreebititEsimAccountActivationResponse, SimDetails, SimUsage, SimTopUpHistory, - FreebititError, FreebititAddSpecRequest, FreebititAddSpecResponse, } from "./interfaces/freebit.types"; @@ -148,9 +147,12 @@ export class FreebititService { /** * Make authenticated API request with error handling */ - private async makeAuthenticatedRequest(endpoint: string, data: any): Promise { + private async makeAuthenticatedRequest( + endpoint: string, + data: unknown + ): Promise { const authKey = await this.getAuthKey(); - const requestData = { ...data, authKey }; + const requestData = { ...(data as Record), authKey }; try { const url = `${this.config.baseUrl}${endpoint}`; @@ -164,10 +166,13 @@ export class FreebititService { if (!response.ok) { let bodySnippet: string | undefined; + let text: string | null = null; try { - const text = await response.text(); - bodySnippet = text ? text.slice(0, 500) : undefined; - } catch {} + text = await response.text(); + } catch (_e) { + text = null; + } + bodySnippet = text ? text.slice(0, 500) : undefined; this.logger.error("Freebit API non-OK response", { endpoint, url, @@ -181,39 +186,39 @@ export class FreebititService { const responseData = (await response.json()) as T; // Check for API-level errors - if (responseData && (responseData as any).resultCode !== "100") { - const errorData = responseData as any; - const errorMessage = errorData.status?.message || "Unknown error"; + const rc = String(responseData?.resultCode ?? ""); + if (rc !== "100") { + const errorMessage = String(responseData.status?.message ?? "Unknown error"); // Provide more specific error messages for common cases let userFriendlyMessage = `API Error: ${errorMessage}`; if (errorMessage === "NG") { userFriendlyMessage = `Account not found or invalid in Freebit system. Please verify the account number exists and is properly configured.`; - } else if (errorMessage.includes("auth") || errorMessage.includes("Auth")) { + } else if (errorMessage.toLowerCase().includes("auth")) { userFriendlyMessage = `Authentication failed with Freebit API. Please check API credentials.`; - } else if (errorMessage.includes("timeout") || errorMessage.includes("Timeout")) { + } else if (errorMessage.toLowerCase().includes("timeout")) { userFriendlyMessage = `Request timeout to Freebit API. Please try again later.`; } this.logger.error("Freebit API error response", { endpoint, - resultCode: errorData.resultCode, - statusCode: errorData.status?.statusCode, + resultCode: rc, + statusCode: responseData.status?.statusCode, message: errorMessage, userFriendlyMessage, }); throw new FreebititErrorImpl( userFriendlyMessage, - errorData.resultCode, - errorData.status?.statusCode, + rc, + String(responseData.status?.statusCode ?? ""), errorMessage ); } this.logger.debug("Freebit API Request Success", { endpoint, - resultCode: (responseData as any).resultCode, + resultCode: rc, }); return responseData; @@ -222,12 +227,9 @@ export class FreebititService { throw error; } - this.logger.error(`Freebit API request failed: ${endpoint}`, { - error: (error as any).message, - }); - throw new InternalServerErrorException( - `Freebit API request failed: ${(error as any).message}` - ); + const message = error instanceof Error ? error.message : String(error); + this.logger.error(`Freebit API request failed: ${endpoint}`, { error: message }); + throw new InternalServerErrorException(`Freebit API request failed: ${message}`); } } @@ -264,7 +266,7 @@ export class FreebititService { ); let response: FreebititAccountDetailsResponse | undefined; - let lastError: any; + let lastError: unknown; for (const ep of candidates) { try { if (ep !== candidates[0]) { @@ -275,9 +277,9 @@ export class FreebititService { request ); break; // success - } catch (err: any) { + } catch (err: unknown) { lastError = err; - if (typeof err?.message === "string" && err.message.includes("HTTP 404")) { + if (err instanceof Error && err.message.includes("HTTP 404")) { // try next candidate continue; } @@ -293,14 +295,40 @@ export class FreebititService { ); } - const datas = (response as any).responseDatas; - const list = Array.isArray(datas) ? datas : datas ? [datas] : []; + type AcctDetailItem = { + kind?: string; + account?: string | number; + state?: string; + startDate?: string | number; + relationCode?: string; + resultCode?: string | number; + planCode?: string; + iccid?: string | number; + imsi?: string | number; + eid?: string; + contractLine?: string; + size?: "standard" | "nano" | "micro" | "esim" | string; + sms?: number; + talk?: number; + ipv4?: string; + ipv6?: string; + quota?: number; + async?: { func: string; date: string | number }; + voicemail?: number; + voiceMail?: number; + callwaiting?: number; + callWaiting?: number; + worldwing?: number; + worldWing?: number; + }; + + const datas = response.responseDatas as unknown; + const list = Array.isArray(datas) ? (datas as AcctDetailItem[]) : datas ? [datas as AcctDetailItem] : []; if (!list.length) { throw new BadRequestException("No SIM details found for this account"); } // Prefer the MVNO entry if present - const mvno = - list.find((d: any) => (d.kind || "").toString().toUpperCase() === "MVNO") || list[0]; + const mvno = list.find(d => String(d.kind ?? "").toUpperCase() === "MVNO") || list[0]; const simData = mvno; const startDateRaw = simData.startDate ? String(simData.startDate) : undefined; @@ -348,11 +376,10 @@ export class FreebititService { }); return simDetails; - } catch (error: any) { - this.logger.error(`Failed to get SIM details for account ${account}`, { - error: error.message, - }); - throw error; + } catch (error: unknown) { + const message = error instanceof Error ? error.message : String(error); + this.logger.error(`Failed to get SIM details for account ${account}`, { error: message }); + throw error as Error; } } @@ -655,7 +682,7 @@ export class FreebititService { throw new BadRequestException("eSIM EID not found for this account"); } - const payload: import("./interfaces/freebit.types").FreebititEsimAccountActivationRequest = { + const payload: FreebititEsimAccountActivationRequest = { authKey, aladinOperated: "20", createType: "reissue", @@ -686,8 +713,7 @@ export class FreebititService { throw new InternalServerErrorException(`HTTP ${response.status}: ${response.statusText}`); } - const data = - (await response.json()) as import("./interfaces/freebit.types").FreebititEsimAccountActivationResponse; + const data = (await response.json()) as FreebititEsimAccountActivationResponse; const rc = typeof data.resultCode === "number" ? String(data.resultCode) : data.resultCode || ""; if (rc !== "100") { diff --git a/apps/bff/src/vendors/freebit/interfaces/freebit.types.ts b/apps/bff/src/vendors/freebit/interfaces/freebit.types.ts index 29cf9a4d..c95f4543 100644 --- a/apps/bff/src/vendors/freebit/interfaces/freebit.types.ts +++ b/apps/bff/src/vendors/freebit/interfaces/freebit.types.ts @@ -18,7 +18,7 @@ export interface FreebititAccountDetailsRequest { authKey: string; version?: string | number; // Docs recommend "2" requestDatas: Array<{ - kind: "MASTER" | "MVNO" | string; + kind: string; account?: string | number; }>; } @@ -33,9 +33,9 @@ export interface FreebititAccountDetailsResponse { // Docs show this can be an array (MASTER + MVNO) or a single object for MVNO responseDatas: | { - kind: "MASTER" | "MVNO" | string; + kind: string; account: string | number; - state: "active" | "suspended" | "temporary" | "waiting" | "obsolete" | string; + state: string; startDate?: string | number; relationCode?: string; resultCode?: string | number; @@ -44,21 +44,21 @@ export interface FreebititAccountDetailsResponse { imsi?: string | number; eid?: string; contractLine?: string; - size?: "standard" | "nano" | "micro" | "esim" | string; + size?: string; sms?: number; // 10=active, 20=inactive talk?: number; // 10=active, 20=inactive ipv4?: string; ipv6?: string; quota?: number; // Remaining quota (units vary by env) async?: { - func: "regist" | "stop" | "resume" | "cancel" | "pinset" | "pinunset" | string; + func: string; date: string | number; }; } | Array<{ - kind: "MASTER" | "MVNO" | string; + kind: string; account: string | number; - state: "active" | "suspended" | "temporary" | "waiting" | "obsolete" | string; + state: string; startDate?: string | number; relationCode?: string; resultCode?: string | number; @@ -67,14 +67,14 @@ export interface FreebititAccountDetailsResponse { imsi?: string | number; eid?: string; contractLine?: string; - size?: "standard" | "nano" | "micro" | "esim" | string; + size?: string; sms?: number; talk?: number; ipv4?: string; ipv6?: string; quota?: number; async?: { - func: "regist" | "stop" | "resume" | "cancel" | "pinset" | "pinunset" | string; + func: string; date: string | number; }; }>; @@ -244,13 +244,13 @@ export interface FreebititEsimAccountActivationRequest { aladinOperated: string; // '10' issue, '20' no-issue masterAccount?: string; masterPassword?: string; - createType: "new" | "reissue" | "exchange" | string; + createType: string; eid?: string; // required for reissue/exchange per business rules account: string; // MSISDN - simkind: "esim" | string; + simkind: string; repAccount?: string; size?: string; - addKind?: "N" | "R" | string; // e.g., 'R' for reissue + addKind?: string; // e.g., 'R' for reissue oldEid?: string; oldProductNumber?: string; mnp?: { @@ -272,7 +272,7 @@ export interface FreebititEsimAccountActivationRequest { export interface FreebititEsimAccountActivationResponse { resultCode: number | string; - status?: any; + status?: unknown; statusCode?: string; message?: string; } diff --git a/apps/bff/src/vendors/whmcs/services/whmcs-invoice.service.ts b/apps/bff/src/vendors/whmcs/services/whmcs-invoice.service.ts index 0d80acd7..7f1bc5a4 100644 --- a/apps/bff/src/vendors/whmcs/services/whmcs-invoice.service.ts +++ b/apps/bff/src/vendors/whmcs/services/whmcs-invoice.service.ts @@ -5,15 +5,7 @@ import { Invoice, InvoiceList } from "@customer-portal/shared"; import { WhmcsConnectionService } from "./whmcs-connection.service"; import { WhmcsDataTransformer } from "../transformers/whmcs-data.transformer"; import { WhmcsCacheService } from "../cache/whmcs-cache.service"; -import { - WhmcsGetInvoicesParams, - WhmcsCreateInvoiceParams, - WhmcsCreateInvoiceResponse, - WhmcsUpdateInvoiceParams, - WhmcsUpdateInvoiceResponse, - WhmcsCapturePaymentParams, - WhmcsCapturePaymentResponse, -} from "../types/whmcs-api.types"; +import { WhmcsGetInvoicesParams } from "../types/whmcs-api.types"; export interface InvoiceFilters { status?: "Paid" | "Unpaid" | "Cancelled" | "Overdue" | "Collections"; diff --git a/apps/portal/scripts/dev-prep.mjs b/apps/portal/scripts/dev-prep.mjs index 085fbe65..8a2a5ff1 100644 --- a/apps/portal/scripts/dev-prep.mjs +++ b/apps/portal/scripts/dev-prep.mjs @@ -1,4 +1,6 @@ #!/usr/bin/env node +/* eslint-env node */ +/* eslint-disable no-console */ // Ensure dev-time Next.js manifests exist to avoid noisy ENOENT errors import { mkdirSync, existsSync, writeFileSync } from "fs"; import { join } from "path"; diff --git a/apps/portal/src/app/catalog/sim/page.tsx b/apps/portal/src/app/catalog/sim/page.tsx index 97116a34..7929d745 100644 --- a/apps/portal/src/app/catalog/sim/page.tsx +++ b/apps/portal/src/app/catalog/sim/page.tsx @@ -457,7 +457,7 @@ export default function SimPlansPage() {
Contract Period

Minimum 3 full billing months required. First month (sign-up to end of month) is - free and doesn't count toward contract. + free and doesn't count toward contract.

diff --git a/apps/portal/src/app/orders/[id]/page.tsx b/apps/portal/src/app/orders/[id]/page.tsx index 57a0ddd0..30b8716c 100644 --- a/apps/portal/src/app/orders/[id]/page.tsx +++ b/apps/portal/src/app/orders/[id]/page.tsx @@ -12,8 +12,6 @@ import { CubeIcon, StarIcon, WrenchScrewdriverIcon, - PlusIcon, - BoltIcon, ExclamationTriangleIcon, EnvelopeIcon, PhoneIcon, diff --git a/apps/portal/src/app/subscriptions/[id]/sim/cancel/page.tsx b/apps/portal/src/app/subscriptions/[id]/sim/cancel/page.tsx index 039ba9d8..1023c006 100644 --- a/apps/portal/src/app/subscriptions/[id]/sim/cancel/page.tsx +++ b/apps/portal/src/app/subscriptions/[id]/sim/cancel/page.tsx @@ -20,7 +20,7 @@ export default function SimCancelPage() { try { await authenticatedApi.post(`/subscriptions/${subscriptionId}/sim/cancel`, {}); setMessage("SIM service cancelled successfully"); - } catch (e: any) { + } catch (e: unknown) { setError(e instanceof Error ? e.message : "Failed to cancel SIM service"); } finally { setLoading(false); @@ -62,7 +62,7 @@ export default function SimCancelPage() {
)} -
+ void submit(e)} className="space-y-6">