diff --git a/apps/bff/src/integrations/freebit/interfaces/freebit.types.ts b/apps/bff/src/integrations/freebit/interfaces/freebit.types.ts index b8ff72c4..62fc96bf 100644 --- a/apps/bff/src/integrations/freebit/interfaces/freebit.types.ts +++ b/apps/bff/src/integrations/freebit/interfaces/freebit.types.ts @@ -92,6 +92,7 @@ export interface FreebitTopUpRequest { quota: number; // KB units (e.g., 102400 for 100MB) quotaCode?: string; // Campaign code expire?: string; // YYYYMMDD format + runTime?: string; // Scheduled execution time (YYYYMMDDHHmm) } export interface FreebitTopUpResponse { @@ -235,7 +236,13 @@ export interface FreebitCancelAccountResponse { export interface FreebitEsimReissueRequest { authKey: string; - account: string; + requestDatas: Array<{ + kind: "MVNO"; + account: string; + newEid?: string; + oldEid?: string; + planCode?: string; + }>; } export interface FreebitEsimReissueResponse { diff --git a/apps/bff/src/integrations/freebit/services/freebit-operations.service.ts b/apps/bff/src/integrations/freebit/services/freebit-operations.service.ts index 7be7f482..37a6393b 100644 --- a/apps/bff/src/integrations/freebit/services/freebit-operations.service.ts +++ b/apps/bff/src/integrations/freebit/services/freebit-operations.service.ts @@ -144,7 +144,7 @@ export class FreebitOperationsService { ): Promise { try { const quotaKb = Math.round(quotaMb * 1024); - const request: Omit = { + const baseRequest: Omit = { account, quota: quotaKb, quotaCode: options.campaignCode, @@ -153,9 +153,7 @@ export class FreebitOperationsService { const scheduled = !!options.scheduledAt; const endpoint = scheduled ? "/mvno/eachQuota/" : "/master/addSpec/"; - if (scheduled) { - (request as any).runTime = options.scheduledAt; - } + const request = scheduled ? { ...baseRequest, runTime: options.scheduledAt } : baseRequest; await this.client.makeAuthenticatedRequest( endpoint, @@ -348,7 +346,7 @@ export class FreebitOperationsService { try { const request: Omit = { requestDatas: [{ kind: "MVNO", account }], - } as any; + }; await this.client.makeAuthenticatedRequest( "/mvno/reissueEsim/", 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 0b4fcdf9..1b1a10e6 100644 --- a/apps/bff/src/integrations/freebit/services/freebit-orchestrator.service.ts +++ b/apps/bff/src/integrations/freebit/services/freebit-orchestrator.service.ts @@ -1,12 +1,7 @@ import { Injectable } from "@nestjs/common"; import { FreebitOperationsService } from "./freebit-operations.service"; import { FreebitMapperService } from "./freebit-mapper.service"; -import type { - SimDetails, - SimUsage, - SimTopUpHistory, - FreebitEsimAddAccountRequest, -} from "../interfaces/freebit.types"; +import type { SimDetails, SimUsage, SimTopUpHistory } from "../interfaces/freebit.types"; @Injectable() export class FreebitOrchestratorService { diff --git a/apps/bff/src/integrations/salesforce/events/pubsub.subscriber.ts b/apps/bff/src/integrations/salesforce/events/pubsub.subscriber.ts index e58fb0d3..9f962de6 100644 --- a/apps/bff/src/integrations/salesforce/events/pubsub.subscriber.ts +++ b/apps/bff/src/integrations/salesforce/events/pubsub.subscriber.ts @@ -15,12 +15,13 @@ import type { SalesforcePubSubError, SalesforcePubSubSubscription, SalesforcePubSubCallbackType, + SalesforcePubSubUnknownData, } from "../types/pubsub-events.types"; type SubscribeCallback = ( subscription: SalesforcePubSubSubscription, callbackType: SalesforcePubSubCallbackType, - data: SalesforcePubSubEvent | SalesforcePubSubError | unknown + data: SalesforcePubSubEvent | SalesforcePubSubError | SalesforcePubSubUnknownData ) => void | Promise; interface PubSubClient { diff --git a/apps/bff/src/integrations/salesforce/types/pubsub-events.types.ts b/apps/bff/src/integrations/salesforce/types/pubsub-events.types.ts index d4db9c62..948aaa35 100644 --- a/apps/bff/src/integrations/salesforce/types/pubsub-events.types.ts +++ b/apps/bff/src/integrations/salesforce/types/pubsub-events.types.ts @@ -33,8 +33,10 @@ export interface SalesforcePubSubError { export type SalesforcePubSubCallbackType = "data" | "event" | "grpcstatus" | "end" | "error"; +export type SalesforcePubSubUnknownData = Record | null | undefined; + export interface SalesforcePubSubCallback { subscription: SalesforcePubSubSubscription; callbackType: SalesforcePubSubCallbackType; - data: SalesforcePubSubEvent | SalesforcePubSubError | unknown; + data: SalesforcePubSubEvent | SalesforcePubSubError | SalesforcePubSubUnknownData; } diff --git a/apps/bff/src/integrations/whmcs/connection/services/whmcs-connection-orchestrator.service.ts b/apps/bff/src/integrations/whmcs/connection/services/whmcs-connection-orchestrator.service.ts index 6a504ad7..ea622763 100644 --- a/apps/bff/src/integrations/whmcs/connection/services/whmcs-connection-orchestrator.service.ts +++ b/apps/bff/src/integrations/whmcs/connection/services/whmcs-connection-orchestrator.service.ts @@ -6,7 +6,6 @@ import { WhmcsHttpClientService } from "./whmcs-http-client.service"; import { WhmcsErrorHandlerService } from "./whmcs-error-handler.service"; import { WhmcsApiMethodsService } from "./whmcs-api-methods.service"; import type { - WhmcsApiResponse, WhmcsErrorResponse, WhmcsAddClientParams, WhmcsValidateLoginParams, diff --git a/apps/bff/src/integrations/whmcs/connection/services/whmcs-error-handler.service.ts b/apps/bff/src/integrations/whmcs/connection/services/whmcs-error-handler.service.ts index ead43d6e..7b9fdab9 100644 --- a/apps/bff/src/integrations/whmcs/connection/services/whmcs-error-handler.service.ts +++ b/apps/bff/src/integrations/whmcs/connection/services/whmcs-error-handler.service.ts @@ -44,7 +44,7 @@ export class WhmcsErrorHandlerService { /** * Handle general request errors (network, timeout, etc.) */ - handleRequestError(error: unknown, action: string, params: Record): never { + handleRequestError(error: unknown, action: string, _params: Record): never { const message = getErrorMessage(error); if (this.isTimeoutError(error)) { diff --git a/apps/bff/src/integrations/whmcs/connection/services/whmcs-http-client.service.ts b/apps/bff/src/integrations/whmcs/connection/services/whmcs-http-client.service.ts index 17f0a8e4..7368b9f9 100644 --- a/apps/bff/src/integrations/whmcs/connection/services/whmcs-http-client.service.ts +++ b/apps/bff/src/integrations/whmcs/connection/services/whmcs-http-client.service.ts @@ -189,14 +189,53 @@ export class WhmcsHttpClientService { // Add parameters for (const [key, value] of Object.entries(params)) { - if (value !== undefined && value !== null) { - formData.append(key, String(value)); + if (value === undefined || value === null) { + continue; } + + const serialized = this.serializeParamValue(value); + formData.append(key, serialized); } return formData.toString(); } + private serializeParamValue(value: unknown): string { + if (typeof value === "string") { + return value; + } + + if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") { + return String(value); + } + + if (value instanceof Date) { + return value.toISOString(); + } + + if (Array.isArray(value)) { + return value.map(entry => this.serializeParamValue(entry)).join(","); + } + + if (typeof value === "object" && value !== null) { + try { + return JSON.stringify(value); + } catch { + return Object.prototype.toString.call(value); + } + } + + if (typeof value === "symbol") { + return value.description ? `Symbol(${value.description})` : "Symbol()"; + } + + if (typeof value === "function") { + return value.name ? `[Function ${value.name}]` : "[Function anonymous]"; + } + + return Object.prototype.toString.call(value); + } + /** * Parse WHMCS API response */ 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 14b66704..24f9904c 100644 --- a/apps/bff/src/integrations/whmcs/services/whmcs-invoice.service.ts +++ b/apps/bff/src/integrations/whmcs/services/whmcs-invoice.service.ts @@ -4,7 +4,7 @@ import { Injectable, NotFoundException, Inject } from "@nestjs/common"; import { Invoice, InvoiceList } from "@customer-portal/domain"; import { invoiceListSchema, - invoiceSchema as invoiceEntitySchema, + invoiceSchema, } from "@customer-portal/domain/validation/shared/entities"; import { WhmcsConnectionOrchestratorService } from "../connection/services/whmcs-connection-orchestrator.service"; import { InvoiceTransformerService } from "../transformers/services/invoice-transformer.service"; @@ -15,9 +15,6 @@ import { WhmcsCreateInvoiceParams, WhmcsUpdateInvoiceParams, WhmcsCapturePaymentParams, - WhmcsCreateInvoiceResponse, - WhmcsUpdateInvoiceResponse, - WhmcsCapturePaymentResponse, } from "../types/whmcs-api.types"; export interface InvoiceFilters { @@ -88,7 +85,7 @@ export class WhmcsInvoiceService { this.logger.log( `Fetched ${result.invoices.length} invoices for client ${clientId}, page ${page}` ); - return result; + return result as InvoiceList; } catch (error) { this.logger.error(`Failed to fetch invoices for client ${clientId}`, { error: getErrorMessage(error), @@ -117,7 +114,7 @@ export class WhmcsInvoiceService { try { // Get detailed invoice with items const detailedInvoice = await this.getInvoiceById(clientId, userId, invoice.id); - const parseResult = invoiceEntitySchema.safeParse(detailedInvoice); + const parseResult = invoiceSchema.safeParse(detailedInvoice); if (!parseResult.success) { this.logger.error("Failed to parse detailed invoice", { error: parseResult.error.issues, @@ -139,7 +136,7 @@ export class WhmcsInvoiceService { ); const result: InvoiceList = { - invoices: invoicesWithItems, + invoices: invoicesWithItems as Invoice[], pagination: invoiceList.pagination, }; @@ -184,7 +181,7 @@ export class WhmcsInvoiceService { // Transform invoice const invoice = this.invoiceTransformer.transformInvoice(response); - const parseResult = invoiceEntitySchema.safeParse(invoice); + const parseResult = invoiceSchema.safeParse(invoice); if (!parseResult.success) { throw new Error(`Invalid invoice data after transformation`); } @@ -232,8 +229,8 @@ export class WhmcsInvoiceService { for (const whmcsInvoice of response.invoices.invoice) { try { const transformed = this.invoiceTransformer.transformInvoice(whmcsInvoice); - const parsed = invoiceEntitySchema.parse(transformed); - invoices.push(parsed); + const parsed = invoiceSchema.parse(transformed); + invoices.push(parsed as Invoice); } catch (error) { this.logger.error(`Failed to transform invoice ${whmcsInvoice.id}`, { error: getErrorMessage(error), diff --git a/apps/bff/src/integrations/whmcs/transformers/services/invoice-transformer.service.ts b/apps/bff/src/integrations/whmcs/transformers/services/invoice-transformer.service.ts index cbbc03e3..0c8a2595 100644 --- a/apps/bff/src/integrations/whmcs/transformers/services/invoice-transformer.service.ts +++ b/apps/bff/src/integrations/whmcs/transformers/services/invoice-transformer.service.ts @@ -1,11 +1,7 @@ import { Injectable, Inject } from "@nestjs/common"; import { Logger } from "nestjs-pino"; import { Invoice, InvoiceItem as BaseInvoiceItem } from "@customer-portal/domain"; -import type { - WhmcsInvoice, - WhmcsInvoiceItems, - WhmcsCustomField, -} from "../../types/whmcs-api.types"; +import type { WhmcsInvoice, WhmcsInvoiceItems } from "../../types/whmcs-api.types"; import { DataUtils } from "../utils/data-utils"; import { StatusNormalizer } from "../utils/status-normalizer"; import { TransformationValidator } from "../validators/transformation-validator"; diff --git a/apps/bff/src/integrations/whmcs/transformers/services/subscription-transformer.service.ts b/apps/bff/src/integrations/whmcs/transformers/services/subscription-transformer.service.ts index 43248b4b..01b4bbba 100644 --- a/apps/bff/src/integrations/whmcs/transformers/services/subscription-transformer.service.ts +++ b/apps/bff/src/integrations/whmcs/transformers/services/subscription-transformer.service.ts @@ -131,7 +131,7 @@ export class SubscriptionTransformerService { private normalizeFieldName(name: string): string { return name .toLowerCase() - .replace(/[^a-z0-9]+(.)/g, (_, char) => char.toUpperCase()) + .replace(/[^a-z0-9]+(.)/g, (_match: string, char: string) => char.toUpperCase()) .replace(/^[^a-z]+/, ""); } diff --git a/apps/bff/src/integrations/whmcs/transformers/services/whmcs-transformer-orchestrator.service.ts b/apps/bff/src/integrations/whmcs/transformers/services/whmcs-transformer-orchestrator.service.ts index 48f4d849..3721fbdf 100644 --- a/apps/bff/src/integrations/whmcs/transformers/services/whmcs-transformer-orchestrator.service.ts +++ b/apps/bff/src/integrations/whmcs/transformers/services/whmcs-transformer-orchestrator.service.ts @@ -30,7 +30,7 @@ export class WhmcsTransformerOrchestratorService { /** * Transform WHMCS invoice to our standard Invoice format */ - async transformInvoice(whmcsInvoice: WhmcsInvoice): Promise { + transformInvoice(whmcsInvoice: WhmcsInvoice): Invoice { try { return this.invoiceTransformer.transformInvoice(whmcsInvoice); } catch (error) { @@ -60,7 +60,7 @@ export class WhmcsTransformerOrchestratorService { /** * Transform WHMCS product/service to our standard Subscription format */ - async transformSubscription(whmcsProduct: WhmcsProduct): Promise { + transformSubscription(whmcsProduct: WhmcsProduct): Subscription { try { return this.subscriptionTransformer.transformSubscription(whmcsProduct); } catch (error) { @@ -90,7 +90,7 @@ export class WhmcsTransformerOrchestratorService { /** * Transform WHMCS payment gateway to shared PaymentGateway interface */ - async transformPaymentGateway(whmcsGateway: WhmcsPaymentGateway): Promise { + transformPaymentGateway(whmcsGateway: WhmcsPaymentGateway): PaymentGateway { try { return this.paymentTransformer.transformPaymentGateway(whmcsGateway); } catch (error) { @@ -120,7 +120,7 @@ export class WhmcsTransformerOrchestratorService { /** * Transform WHMCS payment method to shared PaymentMethod interface */ - async transformPaymentMethod(whmcsPayMethod: WhmcsPaymentMethod): Promise { + transformPaymentMethod(whmcsPayMethod: WhmcsPaymentMethod): PaymentMethod { try { return this.paymentTransformer.transformPaymentMethod(whmcsPayMethod); } catch (error) { @@ -150,16 +150,16 @@ export class WhmcsTransformerOrchestratorService { /** * Transform multiple invoices in batch with error handling */ - async transformInvoices(whmcsInvoices: WhmcsInvoice[]): Promise<{ + transformInvoices(whmcsInvoices: WhmcsInvoice[]): { successful: Invoice[]; failed: Array<{ invoice: WhmcsInvoice; error: string }>; - }> { + } { const successful: Invoice[] = []; const failed: Array<{ invoice: WhmcsInvoice; error: string }> = []; for (const whmcsInvoice of whmcsInvoices) { try { - const transformed = await this.transformInvoice(whmcsInvoice); + const transformed = this.transformInvoice(whmcsInvoice); successful.push(transformed); } catch (error) { failed.push({ @@ -181,16 +181,16 @@ export class WhmcsTransformerOrchestratorService { /** * Transform multiple subscriptions in batch with error handling */ - async transformSubscriptions(whmcsProducts: WhmcsProduct[]): Promise<{ + transformSubscriptions(whmcsProducts: WhmcsProduct[]): { successful: Subscription[]; failed: Array<{ product: WhmcsProduct; error: string }>; - }> { + } { const successful: Subscription[] = []; const failed: Array<{ product: WhmcsProduct; error: string }> = []; for (const whmcsProduct of whmcsProducts) { try { - const transformed = await this.transformSubscription(whmcsProduct); + const transformed = this.transformSubscription(whmcsProduct); successful.push(transformed); } catch (error) { failed.push({ @@ -212,16 +212,16 @@ export class WhmcsTransformerOrchestratorService { /** * Transform multiple payment methods in batch with error handling */ - async transformPaymentMethods(whmcsPayMethods: WhmcsPaymentMethod[]): Promise<{ + transformPaymentMethods(whmcsPayMethods: WhmcsPaymentMethod[]): { successful: PaymentMethod[]; failed: Array<{ payMethod: WhmcsPaymentMethod; error: string }>; - }> { + } { const successful: PaymentMethod[] = []; const failed: Array<{ payMethod: WhmcsPaymentMethod; error: string }> = []; for (const whmcsPayMethod of whmcsPayMethods) { try { - const transformed = await this.transformPaymentMethod(whmcsPayMethod); + const transformed = this.transformPaymentMethod(whmcsPayMethod); successful.push(transformed); } catch (error) { failed.push({ @@ -243,16 +243,16 @@ export class WhmcsTransformerOrchestratorService { /** * Transform multiple payment gateways in batch with error handling */ - async transformPaymentGateways(whmcsGateways: WhmcsPaymentGateway[]): Promise<{ + transformPaymentGateways(whmcsGateways: WhmcsPaymentGateway[]): { successful: PaymentGateway[]; failed: Array<{ gateway: WhmcsPaymentGateway; error: string }>; - }> { + } { const successful: PaymentGateway[] = []; const failed: Array<{ gateway: WhmcsPaymentGateway; error: string }> = []; for (const whmcsGateway of whmcsGateways) { try { - const transformed = await this.transformPaymentGateway(whmcsGateway); + const transformed = this.transformPaymentGateway(whmcsGateway); successful.push(transformed); } catch (error) { failed.push({ diff --git a/apps/bff/src/integrations/whmcs/transformers/utils/data-utils.ts b/apps/bff/src/integrations/whmcs/transformers/utils/data-utils.ts index 43a23aad..d119b353 100644 --- a/apps/bff/src/integrations/whmcs/transformers/utils/data-utils.ts +++ b/apps/bff/src/integrations/whmcs/transformers/utils/data-utils.ts @@ -126,18 +126,59 @@ export class DataUtils { if (!customFields) return undefined; // Try exact match first - if (customFields[fieldName]) { - return String(customFields[fieldName]); + const directValue = DataUtils.toStringValue(customFields[fieldName]); + if (directValue !== undefined) { + return directValue; } // Try case-insensitive match const lowerFieldName = fieldName.toLowerCase(); for (const [key, value] of Object.entries(customFields)) { if (key.toLowerCase() === lowerFieldName) { - return String(value); + return DataUtils.toStringValue(value); } } return undefined; } + + private static toStringValue(value: unknown): string | undefined { + if (value === undefined || value === null) { + return undefined; + } + + if (typeof value === "string") { + return value; + } + + if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") { + return String(value); + } + + if (value instanceof Date) { + return value.toISOString(); + } + + if (Array.isArray(value)) { + return value.map(entry => DataUtils.toStringValue(entry) ?? "").join(","); + } + + if (typeof value === "object") { + try { + return JSON.stringify(value); + } catch { + return Object.prototype.toString.call(value); + } + } + + if (typeof value === "symbol") { + return value.description ? `Symbol(${value.description})` : "Symbol()"; + } + + if (typeof value === "function") { + return value.name ? `[Function ${value.name}]` : "[Function anonymous]"; + } + + return Object.prototype.toString.call(value); + } } diff --git a/apps/bff/src/main.ts b/apps/bff/src/main.ts index f08a3bca..283f2d5d 100644 --- a/apps/bff/src/main.ts +++ b/apps/bff/src/main.ts @@ -1,3 +1,4 @@ +import "tsconfig-paths/register"; import { Logger, type INestApplication } from "@nestjs/common"; import { bootstrap } from "./app/bootstrap"; diff --git a/apps/bff/src/modules/invoices/invoices.controller.ts b/apps/bff/src/modules/invoices/invoices.controller.ts index 360ccc32..cc57346e 100644 --- a/apps/bff/src/modules/invoices/invoices.controller.ts +++ b/apps/bff/src/modules/invoices/invoices.controller.ts @@ -173,10 +173,10 @@ export class InvoicesController { @ApiParam({ name: "id", type: Number, description: "Invoice ID" }) @ApiOkResponse({ description: "List of related subscriptions" }) @ApiResponse({ status: 404, description: "Invoice not found" }) - async getInvoiceSubscriptions( + getInvoiceSubscriptions( @Request() req: AuthenticatedRequest, @Param("id", ParseIntPipe) invoiceId: number - ): Promise { + ): Subscription[] { if (invoiceId <= 0) { throw new BadRequestException("Invoice ID must be a positive number"); } 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 267f2e16..d3ded4ff 100644 --- a/apps/bff/src/modules/invoices/services/invoices-orchestrator.service.ts +++ b/apps/bff/src/modules/invoices/services/invoices-orchestrator.service.ts @@ -1,13 +1,6 @@ import { Injectable, Inject } from "@nestjs/common"; import { Logger } from "nestjs-pino"; -import { - Invoice, - InvoiceList, - InvoiceSsoLink, - InvoicePaymentLink, - PaymentMethodList, - PaymentGatewayList, -} from "@customer-portal/domain"; +import { Invoice, InvoiceList } from "@customer-portal/domain"; import { InvoiceRetrievalService } from "./invoice-retrieval.service"; import { InvoiceHealthService } from "./invoice-health.service"; import { InvoiceValidatorService } from "../validators/invoice-validator.service"; diff --git a/apps/bff/src/modules/subscriptions/sim-management.service.ts b/apps/bff/src/modules/subscriptions/sim-management.service.ts index b63ab6b5..a1fc6bad 100644 --- a/apps/bff/src/modules/subscriptions/sim-management.service.ts +++ b/apps/bff/src/modules/subscriptions/sim-management.service.ts @@ -13,6 +13,7 @@ import type { SimTopUpHistoryRequest, SimFeaturesUpdateRequest, } from "./sim-management/types/sim-requests.types"; +import type { SimNotificationContext } from "./sim-management/interfaces/sim-base.interface"; @Injectable() export class SimManagementService { @@ -25,9 +26,9 @@ export class SimManagementService { private async notifySimAction( action: string, status: "SUCCESS" | "ERROR", - context: Record + context: SimNotificationContext ): Promise { - return this.simNotification.notifySimAction(action, status, context as any); + return this.simNotification.notifySimAction(action, status, context); } /** diff --git a/apps/bff/src/modules/subscriptions/subscriptions.service.ts b/apps/bff/src/modules/subscriptions/subscriptions.service.ts index 4b750f2d..89bd7aef 100644 --- a/apps/bff/src/modules/subscriptions/subscriptions.service.ts +++ b/apps/bff/src/modules/subscriptions/subscriptions.service.ts @@ -6,7 +6,7 @@ import { WhmcsService } from "@bff/integrations/whmcs/whmcs.service"; import { MappingsService } from "@bff/modules/id-mappings/mappings.service"; import { Logger } from "nestjs-pino"; import { z } from "zod"; -import { subscriptionSchema } from "@customer-portal/domain/validation/shared/entities"; +import { subscriptionSchema } from "@customer-portal/domain"; import type { WhmcsProduct, WhmcsProductsResponse, diff --git a/apps/bff/tsconfig.build.json b/apps/bff/tsconfig.build.json index b0b3023c..3796e8d6 100644 --- a/apps/bff/tsconfig.build.json +++ b/apps/bff/tsconfig.build.json @@ -13,7 +13,6 @@ "include": ["src/**/*"], "exclude": ["node_modules", "dist", "test", "**/*.spec.ts"], "references": [ - { "path": "../../packages/domain" }, - { "path": "../../packages/validation" } + { "path": "../../packages/domain" } ] } diff --git a/apps/portal/scripts/stubs/core-api.ts b/apps/portal/scripts/stubs/core-api.ts index df6d3690..a21dca65 100644 --- a/apps/portal/scripts/stubs/core-api.ts +++ b/apps/portal/scripts/stubs/core-api.ts @@ -3,14 +3,14 @@ export type PostCall = [path: string, options?: unknown]; export const postCalls: PostCall[] = []; export const apiClient = { - POST: async (path: string, options?: unknown) => { + POST: (path: string, options?: unknown) => { postCalls.push([path, options]); - return { data: null } as const; + return Promise.resolve({ data: null } as const); }, - GET: async () => ({ data: null }) as const, - PUT: async () => ({ data: null }) as const, - PATCH: async () => ({ data: null }) as const, - DELETE: async () => ({ data: null }) as const, + GET: () => Promise.resolve({ data: null } as const), + PUT: () => Promise.resolve({ data: null } as const), + PATCH: () => Promise.resolve({ data: null } as const), + DELETE: () => Promise.resolve({ data: null } as const), }; export const configureApiClientAuth = () => undefined; diff --git a/apps/portal/scripts/test-request-password-reset.cjs b/apps/portal/scripts/test-request-password-reset.cjs index 9f2e75b3..784cb2be 100755 --- a/apps/portal/scripts/test-request-password-reset.cjs +++ b/apps/portal/scripts/test-request-password-reset.cjs @@ -1,4 +1,6 @@ #!/usr/bin/env node +/* eslint-env node */ +/* global __dirname, console, process */ const fs = require("node:fs"); const path = require("node:path"); @@ -94,7 +96,7 @@ const { useAuthStore } = require("../src/features/auth/services/auth.store.ts"); const [endpoint, options] = coreApiStub.postCalls[0]; if (endpoint !== "/auth/request-password-reset") { throw new Error( - `Expected endpoint \"/auth/request-password-reset\" but received \"${endpoint}\"` + `Expected endpoint "/auth/request-password-reset" but received "${endpoint}"` ); } @@ -109,7 +111,7 @@ const { useAuthStore } = require("../src/features/auth/services/auth.store.ts"); if (body.email !== payload.email) { throw new Error( - `Expected request body email to be \"${payload.email}\" but received \"${body.email}\"` + `Expected request body email to be "${payload.email}" but received "${body.email}"` ); } diff --git a/apps/portal/src/components/organisms/AppShell/Sidebar.tsx b/apps/portal/src/components/organisms/AppShell/Sidebar.tsx index 5adc3ec9..2b2ab08b 100644 --- a/apps/portal/src/components/organisms/AppShell/Sidebar.tsx +++ b/apps/portal/src/components/organisms/AppShell/Sidebar.tsx @@ -140,10 +140,9 @@ const NavigationItem = memo(function NavigationItem({ href={child.href} prefetch onMouseEnter={() => { - try { - // Warm up code/data for faster clicks - if (child.href) router.prefetch(child.href); - } catch {} + if (child.href) { + void router.prefetch(child.href); + } }} className={`group flex items-center px-3 py-2 text-sm rounded-md transition-all duration-200 relative ${ isChildActive @@ -194,9 +193,9 @@ const NavigationItem = memo(function NavigationItem({ href={item.href || "#"} prefetch onMouseEnter={() => { - try { - if (item.href && item.href !== "#") router.prefetch(item.href); - } catch {} + if (item.href && item.href !== "#") { + void router.prefetch(item.href); + } }} className={`group w-full flex items-center px-3 py-2.5 text-sm font-medium rounded-lg transition-all duration-200 relative ${ isActive diff --git a/apps/portal/src/features/account/hooks/useAddressEdit.ts b/apps/portal/src/features/account/hooks/useAddressEdit.ts index e696b13e..ccd7b46e 100644 --- a/apps/portal/src/features/account/hooks/useAddressEdit.ts +++ b/apps/portal/src/features/account/hooks/useAddressEdit.ts @@ -11,12 +11,8 @@ import { useZodForm } from "@customer-portal/validation"; export function useAddressEdit(initial: AddressFormData) { const handleSave = useCallback(async (formData: AddressFormData) => { - try { - const requestData = addressFormToRequest(formData); - await accountService.updateAddress(requestData); - } catch (error) { - throw error; // Let useZodForm handle the error state - } + const requestData = addressFormToRequest(formData); + await accountService.updateAddress(requestData); }, []); return useZodForm({ diff --git a/apps/portal/src/features/account/hooks/useProfileEdit.ts b/apps/portal/src/features/account/hooks/useProfileEdit.ts index aa8539e8..32e9f043 100644 --- a/apps/portal/src/features/account/hooks/useProfileEdit.ts +++ b/apps/portal/src/features/account/hooks/useProfileEdit.ts @@ -12,17 +12,13 @@ import { useZodForm } from "@customer-portal/validation"; export function useProfileEdit(initial: ProfileEditFormData) { const handleSave = useCallback(async (formData: ProfileEditFormData) => { - try { - const requestData = profileFormToRequest(formData); - const updated = await accountService.updateProfile(requestData); + const requestData = profileFormToRequest(formData); + const updated = await accountService.updateProfile(requestData); - useAuthStore.setState(state => ({ - ...state, - user: state.user ? { ...state.user, ...updated } : state.user, - })); - } catch (error) { - throw error; // Let useZodForm handle the error state - } + useAuthStore.setState(state => ({ + ...state, + user: state.user ? { ...state.user, ...updated } : state.user, + })); }, []); return useZodForm({ diff --git a/apps/portal/src/features/auth/components/LinkWhmcsForm/LinkWhmcsForm.tsx b/apps/portal/src/features/auth/components/LinkWhmcsForm/LinkWhmcsForm.tsx index 2eb34c95..7317d7ce 100644 --- a/apps/portal/src/features/auth/components/LinkWhmcsForm/LinkWhmcsForm.tsx +++ b/apps/portal/src/features/auth/components/LinkWhmcsForm/LinkWhmcsForm.tsx @@ -22,17 +22,12 @@ export function LinkWhmcsForm({ onTransferred, className = "" }: LinkWhmcsFormPr const handleLink = useCallback( async (formData: LinkWhmcsFormData) => { clearError(); - try { - const payload: LinkWhmcsRequestInput = { - email: formData.email, - password: formData.password, - }; - const result = await linkWhmcs(payload); - onTransferred?.({ ...result, email: formData.email }); - } catch (err) { - // Error is handled by useZodForm - throw err; - } + const payload: LinkWhmcsRequestInput = { + email: formData.email, + password: formData.password, + }; + const result = await linkWhmcs(payload); + onTransferred?.({ ...result, email: formData.email }); }, [linkWhmcs, onTransferred, clearError] ); @@ -56,7 +51,7 @@ export function LinkWhmcsForm({ onTransferred, className = "" }: LinkWhmcsFormPr

-
+ void handleSubmit(event)} className="space-y-4"> { clearError(); + const requestData = loginFormToRequest(formData); try { - const requestData = loginFormToRequest(formData); await login(requestData); onSuccess?.(); } catch (err) { const message = err instanceof Error ? err.message : "Login failed"; onError?.(message); - throw err; // Re-throw to let useZodForm handle the error state + throw err; } }, [login, onSuccess, onError, clearError] @@ -61,7 +61,6 @@ export function LoginForm({ setValue, setTouchedField, handleSubmit, - validateField, } = useZodForm({ schema: loginSchema, initialValues: { @@ -74,7 +73,7 @@ export function LoginForm({ return (
- + void handleSubmit(event)} className="space-y-6">

- Don't have an account?{" "} + Don't have an account?{" "}

Reset your password

- Enter your email address and we'll send you a link to reset your password. + Enter your email address and we'll send you a link to reset your password.

- + void requestForm.handleSubmit(event)} className="space-y-4"> Enter your new password below.

- + void resetForm.handleSubmit(event)} className="space-y-4"> - + void form.handleSubmit(event)} className="space-y-4"> , - "values" | "errors" | "touched" | "setValue" | "setTouchedField" - > {} +type PasswordStepProps = Pick< + UseZodFormReturn, + "values" | "errors" | "touched" | "setValue" | "setTouchedField" +>; export function PasswordStep({ values, diff --git a/apps/portal/src/features/auth/services/auth.store.ts b/apps/portal/src/features/auth/services/auth.store.ts index 3064d22a..4fff470d 100644 --- a/apps/portal/src/features/auth/services/auth.store.ts +++ b/apps/portal/src/features/auth/services/auth.store.ts @@ -319,8 +319,10 @@ export const useAuthStore = create()( set({ user: profile }); } catch (error) { // Token might be expired, try to refresh - handleAuthError(error, get().logout); - await get().refreshTokens(); + const shouldLogout = handleAuthError(error, get().logout); + if (!shouldLogout) { + await get().refreshTokens(); + } } }, @@ -328,7 +330,7 @@ export const useAuthStore = create()( const { tokens } = get(); if (!tokens?.refreshToken) { // No refresh token available, logout - get().logout(); + await get().logout(); return; } @@ -349,7 +351,10 @@ export const useAuthStore = create()( set({ tokens: newTokens, isAuthenticated: true }); } catch (error) { // Refresh failed, logout - handleAuthError(error, get().logout); + const shouldLogout = handleAuthError(error, get().logout); + if (!shouldLogout) { + await get().logout(); + } } }, diff --git a/apps/portal/src/features/billing/views/PaymentMethods.tsx b/apps/portal/src/features/billing/views/PaymentMethods.tsx index 04679df8..23762971 100644 --- a/apps/portal/src/features/billing/views/PaymentMethods.tsx +++ b/apps/portal/src/features/billing/views/PaymentMethods.tsx @@ -7,7 +7,7 @@ import { SubCard } from "@/components/molecules/SubCard/SubCard"; import { useSession } from "@/features/auth/hooks"; import { useAuthStore } from "@/features/auth/services/auth.store"; -import { apiClient, getDataOrThrow } from "@/lib/api"; +import { apiClient, getDataOrThrow, isApiError } from "@/lib/api"; import { openSsoLink } from "@/features/billing/utils/sso"; import { usePaymentRefresh } from "@/features/billing/hooks/usePaymentRefresh"; import { PaymentMethodCard, usePaymentMethods } from "@/features/billing"; @@ -62,9 +62,9 @@ export function PaymentMethodsContainer() { }); const sso = getDataOrThrow(response, "Failed to open payment methods portal"); openSsoLink(sso.url, { newTab: true }); - } catch (err) { + } catch (err: unknown) { logger.error(err, "Failed to open payment methods"); - if (err && typeof err === "object" && "status" in err && (err as any).status === 401) { + if (isApiError(err) && err.response.status === 401) { setError("Authentication failed. Please log in again."); } else { setError("Unable to access payment methods. Please try again later."); diff --git a/apps/portal/src/features/catalog/components/internet/configure/steps/ServiceConfigurationStep.tsx b/apps/portal/src/features/catalog/components/internet/configure/steps/ServiceConfigurationStep.tsx index d7c8d7b7..c48da8be 100644 --- a/apps/portal/src/features/catalog/components/internet/configure/steps/ServiceConfigurationStep.tsx +++ b/apps/portal/src/features/catalog/components/internet/configure/steps/ServiceConfigurationStep.tsx @@ -46,8 +46,8 @@ export function ServiceConfigurationStep({ plan, mode, setMode, isTransitioning, from our tech team for details.

- * Will appear on the invoice as "Platinum Base Plan". Device subscriptions will be added - later. + * Will appear on the invoice as "Platinum Base Plan". Device subscriptions will be + added later.

)} diff --git a/apps/portal/src/features/catalog/hooks/useSimConfigure.ts b/apps/portal/src/features/catalog/hooks/useSimConfigure.ts index 88b86647..22eb6ad8 100644 --- a/apps/portal/src/features/catalog/hooks/useSimConfigure.ts +++ b/apps/portal/src/features/catalog/hooks/useSimConfigure.ts @@ -93,8 +93,7 @@ export function useSimConfigure(planId?: string): UseSimConfigureResult { const { values, errors, setValue, validate } = useZodForm({ schema: simConfigureFormSchema, initialValues, - onSubmit: async data => { - // This hook doesn't submit directly, just validates + onSubmit: data => { simConfigureFormToRequest(data); }, }); @@ -120,7 +119,7 @@ export function useSimConfigure(planId?: string): UseSimConfigureResult { // Initialize from URL params useEffect(() => { let mounted = true; - async function initializeFromParams() { + const initializeFromParams = () => { if (simLoading || !simData) return; if (mounted) { @@ -136,9 +135,9 @@ export function useSimConfigure(planId?: string): UseSimConfigureResult { setScheduledActivationDate(searchParams.get("scheduledDate") || ""); setWantsMnp(searchParams.get("wantsMnp") === "true"); } - } + }; - void initializeFromParams(); + initializeFromParams(); return () => { mounted = false; }; @@ -173,25 +172,37 @@ export function useSimConfigure(planId?: string): UseSimConfigureResult { // Add addon pricing if (simData?.addons) { - values.selectedAddons.forEach((addonId: string) => { + values.selectedAddons.forEach(addonId => { const addon = simData.addons.find(a => a.id === addonId); - if (addon) { - if ((addon as any).billingType === "monthly") { - monthly += (addon as any).price || 0; - } else { - oneTime += (addon as any).price || 0; - } + if (!addon) return; + + const billingType = + ("billingType" in addon && typeof (addon as { billingType?: string }).billingType === "string" + ? (addon as { billingType?: string }).billingType + : addon.billingCycle) ?? ""; + const normalizedBilling = billingType.toLowerCase(); + const recurringValue = addon.monthlyPrice ?? addon.unitPrice ?? 0; + const oneTimeValue = addon.oneTimePrice ?? addon.unitPrice ?? addon.monthlyPrice ?? 0; + + if (normalizedBilling === "monthly") { + monthly += recurringValue; + } else { + oneTime += oneTimeValue; } }); } // Add activation fees if (simData?.activationFees) { - const activationFee = simData.activationFees.find( - fee => (fee as any).simType === values.simType - ); + const activationFee = simData.activationFees.find(fee => { + const rawSimType = + "simType" in fee && typeof (fee as { simType?: string }).simType === "string" + ? (fee as { simType?: string }).simType + : undefined; + return (rawSimType ?? fee.simPlanType) === values.simType; + }); if (activationFee) { - oneTime += (activationFee as any).amount || 0; + oneTime += activationFee.oneTimePrice ?? activationFee.unitPrice ?? activationFee.monthlyPrice ?? 0; } } diff --git a/apps/portal/src/features/orders/services/orders.service.ts b/apps/portal/src/features/orders/services/orders.service.ts index 4cd25b4a..7d35df49 100644 --- a/apps/portal/src/features/orders/services/orders.service.ts +++ b/apps/portal/src/features/orders/services/orders.service.ts @@ -3,6 +3,14 @@ import type { CreateOrderRequest } from "@customer-portal/domain"; const API_BASE = process.env.NEXT_PUBLIC_API_BASE || "http://localhost:4000"; +interface AuthStoreSnapshot { + state?: { + tokens?: { + accessToken?: unknown; + }; + }; +} + const getAuthHeader = (): string | undefined => { if (typeof window === "undefined") return undefined; @@ -10,9 +18,9 @@ const getAuthHeader = (): string | undefined => { if (!authStore) return undefined; try { - const parsed = JSON.parse(authStore); + const parsed = JSON.parse(authStore) as AuthStoreSnapshot; const token = parsed?.state?.tokens?.accessToken; - return token ? `Bearer ${token}` : undefined; + return typeof token === "string" && token ? `Bearer ${token}` : undefined; } catch { return undefined; } diff --git a/apps/portal/src/lib/api/index.ts b/apps/portal/src/lib/api/index.ts index 5b807dc4..40759e4f 100644 --- a/apps/portal/src/lib/api/index.ts +++ b/apps/portal/src/lib/api/index.ts @@ -19,17 +19,17 @@ export const queryKeys = { session: () => ["auth", "session"] as const, }, billing: { - invoices: (params?: Record) => ["billing", "invoices", params] as const, + invoices: (params?: Record) => ["billing", "invoices", params] as const, invoice: (id: string) => ["billing", "invoice", id] as const, paymentMethods: () => ["billing", "payment-methods"] as const, }, subscriptions: { all: () => ["subscriptions"] as const, - list: (params?: Record) => ["subscriptions", "list", params] as const, + list: (params?: Record) => ["subscriptions", "list", params] as const, active: () => ["subscriptions", "active"] as const, stats: () => ["subscriptions", "stats"] as const, detail: (id: string) => ["subscriptions", "detail", id] as const, - invoices: (id: number, params?: Record) => + invoices: (id: number, params?: Record) => ["subscriptions", "invoices", id, params] as const, }, dashboard: { diff --git a/apps/portal/src/lib/api/runtime/client.ts b/apps/portal/src/lib/api/runtime/client.ts index 8531544a..88248e09 100644 --- a/apps/portal/src/lib/api/runtime/client.ts +++ b/apps/portal/src/lib/api/runtime/client.ts @@ -85,6 +85,21 @@ export interface CreateClientOptions { handleError?: (response: Response) => void | Promise; } +const getBodyMessage = (body: unknown): string | null => { + if (typeof body === "string") { + return body; + } + + if (typeof body === "object" && body !== null && "message" in body) { + const maybeMessage = (body as { message?: unknown }).message; + if (typeof maybeMessage === "string") { + return maybeMessage; + } + } + + return null; +}; + async function defaultHandleError(response: Response) { if (response.ok) return; @@ -96,13 +111,9 @@ async function defaultHandleError(response: Response) { const contentType = cloned.headers.get("content-type"); if (contentType?.includes("application/json")) { body = await cloned.json(); - if ( - body && - typeof body === "object" && - "message" in body && - typeof (body as any).message === "string" - ) { - message = (body as any).message; + const jsonMessage = getBodyMessage(body); + if (jsonMessage) { + message = jsonMessage; } } else { const text = await cloned.text(); diff --git a/apps/portal/src/lib/hooks/useLocalStorage.ts b/apps/portal/src/lib/hooks/useLocalStorage.ts index 760fb74d..51194b78 100644 --- a/apps/portal/src/lib/hooks/useLocalStorage.ts +++ b/apps/portal/src/lib/hooks/useLocalStorage.ts @@ -26,7 +26,8 @@ export function useLocalStorage( try { const item = window.localStorage.getItem(key); if (item) { - setStoredValue(JSON.parse(item)); + const parsed: unknown = JSON.parse(item); + setStoredValue(parsed as T); } } catch (error) { logger.warn( diff --git a/apps/portal/src/lib/utils/error-handling.ts b/apps/portal/src/lib/utils/error-handling.ts index c7f628ce..f735e8c3 100644 --- a/apps/portal/src/lib/utils/error-handling.ts +++ b/apps/portal/src/lib/utils/error-handling.ts @@ -116,12 +116,15 @@ export function getUserFriendlyMessage(error: unknown): string { /** * Handle authentication errors consistently */ -export function handleAuthError(error: unknown, logout: () => void): void { +export function handleAuthError(error: unknown, logout: () => void | Promise): boolean { const errorInfo = getErrorInfo(error); if (errorInfo.shouldLogout) { - logout(); + void logout(); + return true; } + + return false; } /** diff --git a/packages/domain/tsconfig.json b/packages/domain/tsconfig.json index 458aada3..af092737 100644 --- a/packages/domain/tsconfig.json +++ b/packages/domain/tsconfig.json @@ -1,8 +1,8 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "module": "ESNext", - "moduleResolution": "bundler", + "module": "NodeNext", + "moduleResolution": "nodenext", "lib": ["ES2022"], "outDir": "./dist", "rootDir": "./src",