diff --git a/apps/bff/src/orders/services/order-fulfillment-orchestrator.service.ts b/apps/bff/src/orders/services/order-fulfillment-orchestrator.service.ts index eee3d024..4fe522ff 100644 --- a/apps/bff/src/orders/services/order-fulfillment-orchestrator.service.ts +++ b/apps/bff/src/orders/services/order-fulfillment-orchestrator.service.ts @@ -180,7 +180,8 @@ export class OrderFulfillmentOrchestrator { } // Extract configurations from the original payload - const configurations = payload.configurations || {}; + const configurations: Record = + (payload.configurations as Record | undefined) ?? {}; await this.simFulfillmentService.fulfillSimOrder({ orderDetails: context.orderDetails, diff --git a/apps/bff/src/orders/services/sim-fulfillment.service.ts b/apps/bff/src/orders/services/sim-fulfillment.service.ts index bed657e2..7aa8ab07 100644 --- a/apps/bff/src/orders/services/sim-fulfillment.service.ts +++ b/apps/bff/src/orders/services/sim-fulfillment.service.ts @@ -89,8 +89,8 @@ export class SimFulfillmentService { activationType: "Immediate" | "Scheduled"; scheduledAt?: string; mnp?: { - reserveNumber: string; - reserveExpireDate: string; + reserveNumber?: string; + reserveExpireDate?: string; account?: string; firstnameKanji?: string; lastnameKanji?: string; @@ -124,10 +124,13 @@ export class SimFulfillmentService { planCode: planSku, contractLine: "5G", shipDate: activationType === "Scheduled" ? scheduledAt : undefined, - mnp: mnp ? { - reserveNumber: mnp.reserveNumber, - reserveExpireDate: mnp.reserveExpireDate, - } : undefined, + mnp: + mnp && mnp.reserveNumber && mnp.reserveExpireDate + ? { + reserveNumber: mnp.reserveNumber, + reserveExpireDate: mnp.reserveExpireDate, + } + : undefined, identity: mnp ? { firstnameKanji: mnp.firstnameKanji, lastnameKanji: mnp.lastnameKanji, diff --git a/apps/bff/src/vendors/freebit/freebit.service.ts b/apps/bff/src/vendors/freebit/freebit.service.ts index 1b35bbab..c1409470 100644 --- a/apps/bff/src/vendors/freebit/freebit.service.ts +++ b/apps/bff/src/vendors/freebit/freebit.service.ts @@ -1,17 +1,4 @@ -import { - Injectable, - Inject, - BadRequestException, - InternalServerErrorException, -} from "@nestjs/common"; -import { ConfigService } from "@nestjs/config"; -import { Logger } from "nestjs-pino"; -import { - Injectable, - Inject, - BadRequestException, - InternalServerErrorException, -} from "@nestjs/common"; +import { Inject, Injectable, BadRequestException, InternalServerErrorException } from "@nestjs/common"; import { ConfigService } from "@nestjs/config"; import { Logger } from "nestjs-pino"; import type { @@ -30,46 +17,41 @@ import type { FreebititPlanChangeResponse, FreebititCancelPlanRequest, FreebititCancelPlanResponse, + FreebititEsimReissueRequest, + FreebititEsimReissueResponse, FreebititEsimAddAccountRequest, FreebititEsimAddAccountResponse, + FreebititEsimAccountActivationRequest, + FreebititEsimAccountActivationResponse, + FreebititAddSpecRequest, + FreebititAddSpecResponse, SimDetails, SimUsage, SimTopUpHistory, - FreebititAddSpecRequest, - FreebititAddSpecResponse, } from "./interfaces/freebit.types"; @Injectable() export class FreebititService { private readonly config: FreebititConfig; - private authKeyCache: { - token: string; - expiresAt: number; - } | null = null; + private authKeyCache: { token: string; expiresAt: number } | null = null; constructor( private readonly configService: ConfigService, @Inject(Logger) private readonly logger: Logger - @Inject(Logger) private readonly logger: Logger ) { this.config = { - baseUrl: - this.configService.get("FREEBIT_BASE_URL") || "https://i1.mvno.net/emptool/api", + baseUrl: this.configService.get("FREEBIT_BASE_URL") || "https://i1.mvno.net/emptool/api", oemId: this.configService.get("FREEBIT_OEM_ID") || "PASI", oemKey: this.configService.get("FREEBIT_OEM_KEY") || "", timeout: this.configService.get("FREEBIT_TIMEOUT") || 30000, retryAttempts: this.configService.get("FREEBIT_RETRY_ATTEMPTS") || 3, - detailsEndpoint: - this.configService.get("FREEBIT_DETAILS_ENDPOINT") || "/master/getAcnt/", + detailsEndpoint: this.configService.get("FREEBIT_DETAILS_ENDPOINT") || "/master/getAcnt/", }; - // Warn if critical configuration is missing if (!this.config.oemKey) { this.logger.warn("FREEBIT_OEM_KEY is not configured. SIM management features will not work."); - this.logger.warn("FREEBIT_OEM_KEY is not configured. SIM management features will not work."); } - this.logger.debug("Freebit service initialized", { this.logger.debug("Freebit service initialized", { baseUrl: this.config.baseUrl, oemId: this.config.oemId, @@ -77,21 +59,8 @@ export class FreebititService { }); } - /** - * Map Freebit SIM status to portal status - */ - private mapSimStatus(freebititStatus: string): "active" | "suspended" | "cancelled" | "pending" { - private mapSimStatus(freebititStatus: string): "active" | "suspended" | "cancelled" | "pending" { - switch (freebititStatus) { - case "active": - return "active"; - case "suspended": - return "suspended"; - case "temporary": - case "waiting": - return "pending"; - case "obsolete": - return "cancelled"; + private mapSimStatus(status: string): "active" | "suspended" | "cancelled" | "pending" { + switch (status) { case "active": return "active"; case "suspended": @@ -103,38 +72,24 @@ export class FreebititService { return "cancelled"; default: return "pending"; - return "pending"; } } - /** - * Get or refresh authentication token - */ private async getAuthKey(): Promise { - // Check if we have a valid cached token if (this.authKeyCache && this.authKeyCache.expiresAt > Date.now()) { return this.authKeyCache.token; } try { - // Check if configuration is available if (!this.config.oemKey) { throw new Error("Freebit API not configured: FREEBIT_OEM_KEY is missing"); - throw new Error("Freebit API not configured: FREEBIT_OEM_KEY is missing"); } - const request: FreebititAuthRequest = { - oemId: this.config.oemId, - oemKey: this.config.oemKey, - }; + const request: FreebititAuthRequest = { oemId: this.config.oemId, oemKey: this.config.oemKey }; const response = await fetch(`${this.config.baseUrl}/authOem/`, { method: "POST", - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - "Content-Type": "application/x-www-form-urlencoded", - }, + headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: `json=${JSON.stringify(request)}`, }); @@ -143,9 +98,6 @@ export class FreebititService { } const data = (await response.json()) as FreebititAuthResponse; - const data = (await response.json()) as FreebititAuthResponse; - - if (data.resultCode !== "100") { if (data.resultCode !== "100") { throw new FreebititErrorImpl( `Authentication failed: ${data.status.message}`, @@ -155,13 +107,7 @@ export class FreebititService { ); } - // Cache the token for 50 minutes (assuming 60min expiry) - this.authKeyCache = { - token: data.authKey, - expiresAt: Date.now() + 50 * 60 * 1000, - }; - - this.logger.log("Successfully authenticated with Freebit API"); + this.authKeyCache = { token: data.authKey, expiresAt: Date.now() + 50 * 60 * 1000 }; this.logger.log("Successfully authenticated with Freebit API"); return data.authKey; } catch (error: any) { @@ -170,22 +116,15 @@ export class FreebititService { } } - /** - * Make authenticated API request with error handling - */ - private async makeAuthenticatedRequest(endpoint: string, data: any): Promise { + private async makeAuthenticatedRequest(endpoint: string, data: Record): Promise { const authKey = await this.getAuthKey(); - const requestData = { ...(data as Record), authKey }; + const requestData = { ...data, authKey }; try { const url = `${this.config.baseUrl}${endpoint}`; const response = await fetch(url, { method: "POST", - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - "Content-Type": "application/x-www-form-urlencoded", - }, + headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: `json=${JSON.stringify(requestData)}`, }); @@ -205,50 +144,57 @@ export class FreebititService { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } - 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 responseData = (await response.json()) as any; + if (responseData && responseData.resultCode && responseData.resultCode !== "100") { throw new FreebititErrorImpl( - `API Error: ${errorData.status?.message || "Unknown error"}`, - errorData.resultCode, - errorData.status?.statusCode, - errorData.status?.message + `API Error: ${responseData.status?.message || "Unknown error"}`, + responseData.resultCode, + responseData.status?.statusCode, + responseData.status?.message ); } - this.logger.debug("Freebit API Request Success", { - this.logger.debug("Freebit API Request Success", { - endpoint, - resultCode: rc, - }); - - return responseData; + this.logger.debug("Freebit API Request Success", { endpoint }); + return responseData as T; } catch (error) { if (error instanceof FreebititErrorImpl) { 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}` - ); + this.logger.error(`Freebit API request failed: ${endpoint}`, { error: (error as any).message }); + throw new InternalServerErrorException(`Freebit API request failed: ${(error as any).message}`); + } + } + + private async makeAuthenticatedJsonRequest(endpoint: string, payload: Record): Promise { + const url = `${this.config.baseUrl}${endpoint}`; + try { + const response = await fetch(url, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + }); + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + const responseData = (await response.json()) as any; + if (responseData && responseData.resultCode && responseData.resultCode !== "100") { + throw new FreebititErrorImpl( + `API Error: ${responseData.status?.message || "Unknown error"}`, + responseData.resultCode, + responseData.status?.statusCode, + responseData.status?.message + ); + } + this.logger.debug("Freebit JSON API Request Success", { endpoint }); + return responseData as T; + } catch (error) { + this.logger.error(`Freebit JSON API request failed: ${endpoint}`, { error: (error as any).message }); + throw new InternalServerErrorException(`Freebit JSON API request failed: ${(error as any).message}`); } - this.logger.debug("Freebit JSON API Request Success", { endpoint, resultCode: rc }); - return data; } - /** - * Get detailed SIM account information - */ async getSimDetails(account: string): Promise { try { - const request: Omit = { - version: "2", - requestDatas: [{ kind: "MVNO", account }], const request: Omit = { version: "2", requestDatas: [{ kind: "MVNO", account }], @@ -276,28 +222,6 @@ export class FreebititService { ]) ); - const configured = this.config.detailsEndpoint || "/master/getAcnt/"; - const candidates = Array.from( - new Set([ - configured, - configured.replace(/\/$/, ""), - "/master/getAcnt/", - "/master/getAcnt", - "/mvno/getAccountDetail/", - "/mvno/getAccountDetail", - "/mvno/getAcntDetail/", - "/mvno/getAcntDetail", - "/mvno/getAccountInfo/", - "/mvno/getAccountInfo", - "/mvno/getSubscriberInfo/", - "/mvno/getSubscriberInfo", - "/mvno/getInfo/", - "/mvno/getInfo", - "/master/getDetail/", - "/master/getDetail", - ]) - ); - let response: FreebititAccountDetailsResponse | undefined; let lastError: unknown; for (const ep of candidates) { @@ -305,132 +229,87 @@ export class FreebititService { if (ep !== candidates[0]) { this.logger.warn(`Retrying Freebit account details with alternative endpoint: ${ep}`); } - response = await this.makeAuthenticatedRequest( - ep, - request - ); - response = await this.makeAuthenticatedRequest( - ep, - request - ); - break; // success - } catch (err: unknown) { + response = await this.makeAuthenticatedRequest(ep, request as any); + break; + } catch (err: any) { lastError = err; if (typeof err?.message === "string" && err.message.includes("HTTP 404")) { - // try next candidate - continue; + continue; // try next } - // non-404 error, rethrow - throw err; } } - if (!response) { - throw ( - lastError || - new InternalServerErrorException("Failed to fetch SIM details: all endpoints failed") - ); + throw lastError || new Error("Failed to fetch account details"); } - const datas = (response as any).responseDatas; - const list = Array.isArray(datas) ? datas : datas ? [datas] : []; - if (!list.length) { - throw new BadRequestException("No SIM details found for this account"); - 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 simData = mvno; + const resp = response.responseDatas as any; + const simData = Array.isArray(resp) + ? (resp.find((d) => String(d.kind).toUpperCase() === "MVNO") || resp[0]) + : resp; - const startDateRaw = simData.startDate ? String(simData.startDate) : undefined; - const startDate = - startDateRaw && /^\d{8}$/.test(startDateRaw) - ? `${startDateRaw.slice(0, 4)}-${startDateRaw.slice(4, 6)}-${startDateRaw.slice(6, 8)}` - : startDateRaw; - const startDate = - startDateRaw && /^\d{8}$/.test(startDateRaw) - ? `${startDateRaw.slice(0, 4)}-${startDateRaw.slice(4, 6)}-${startDateRaw.slice(6, 8)}` - : startDateRaw; + const size = String(simData.size || "").toLowerCase(); + const isEsim = size === "esim" || !!simData.eid; + const planCode = String(simData.planCode || ""); + const status = this.mapSimStatus(String(simData.state || "")); - const simDetails: SimDetails = { - account: String(simData.account ?? account), - msisdn: String(simData.account ?? account), + const remainingKb = Number(simData.quota) || 0; + const details: SimDetails = { + account: String(simData.account || account), + msisdn: String(simData.account || account), iccid: simData.iccid ? String(simData.iccid) : undefined, imsi: simData.imsi ? String(simData.imsi) : undefined, - eid: simData.eid, - planCode: simData.planCode, - status: this.mapSimStatus(String(simData.state || "pending")), - simType: simData.eid ? "esim" : "physical", - size: simData.size, - hasVoice: simData.talk === 10, - hasSms: simData.sms === 10, - remainingQuotaKb: typeof simData.quota === "number" ? simData.quota : 0, - remainingQuotaMb: - typeof simData.quota === "number" ? Math.round((simData.quota / 1024) * 100) / 100 : 0, - startDate, + eid: simData.eid ? String(simData.eid) : undefined, + planCode, + status, + simType: isEsim ? "esim" : "physical", + size: (size as any) || (isEsim ? "esim" : "nano"), + hasVoice: Number(simData.talk) === 10, + hasSms: Number(simData.sms) === 10, + remainingQuotaKb: remainingKb, + remainingQuotaMb: Math.round((remainingKb / 1024) * 100) / 100, + startDate: simData.startDate ? String(simData.startDate) : undefined, ipv4: simData.ipv4, ipv6: simData.ipv6, - voiceMailEnabled: simData.voicemail === 10 || simData.voiceMail === 10, - callWaitingEnabled: simData.callwaiting === 10 || simData.callWaiting === 10, - internationalRoamingEnabled: simData.worldwing === 10 || simData.worldWing === 10, + voiceMailEnabled: Number(simData.voicemail ?? simData.voiceMail) === 10, + callWaitingEnabled: Number(simData.callwaiting ?? simData.callWaiting) === 10, + internationalRoamingEnabled: Number(simData.worldwing ?? simData.worldWing) === 10, networkType: simData.contractLine || undefined, pendingOperations: simData.async - ? [ - { - operation: simData.async.func, - scheduledDate: String(simData.async.date), - }, - ] - : undefined, - pendingOperations: simData.async - ? [ - { - operation: simData.async.func, - scheduledDate: String(simData.async.date), - }, - ] + ? [{ operation: String(simData.async.func), scheduledDate: String(simData.async.date) }] : undefined, }; this.logger.log(`Retrieved SIM details for account ${account}`, { account, - status: simDetails.status, - planCode: simDetails.planCode, + status: details.status, + planCode: details.planCode, }); - return simDetails; + return details; } catch (error: any) { - this.logger.error(`Failed to get SIM details for account ${account}`, { - error: error.message, - }); + this.logger.error(`Failed to get SIM details for account ${account}`, { error: error.message }); throw error; } } - /** - * Get SIM data usage information - */ async getSimUsage(account: string): Promise { try { const request: Omit = { account }; - - const request: Omit = { account }; - const response = await this.makeAuthenticatedRequest( "/mvno/getTrafficInfo/", - "/mvno/getTrafficInfo/", - request + request as any ); const todayUsageKb = parseInt(response.traffic.today, 10) || 0; - const recentDaysData = response.traffic.inRecentDays.split(",").map((usage, index) => ({ - date: new Date(Date.now() - (index + 1) * 24 * 60 * 60 * 1000).toISOString().split("T")[0], - const recentDaysData = response.traffic.inRecentDays.split(",").map((usage, index) => ({ - date: new Date(Date.now() - (index + 1) * 24 * 60 * 60 * 1000).toISOString().split("T")[0], - usageKb: parseInt(usage, 10) || 0, - usageMb: Math.round((parseInt(usage, 10) / 1024) * 100) / 100, - })); + const recentDaysData = response.traffic.inRecentDays + .split(",") + .map((usage, index) => ({ + date: new Date(Date.now() - (index + 1) * 24 * 60 * 60 * 1000) + .toISOString() + .split("T")[0], + usageKb: parseInt(usage, 10) || 0, + usageMb: Math.round(((parseInt(usage, 10) || 0) / 1024) * 100) / 100, + })); const simUsage: SimUsage = { account, @@ -438,7 +317,6 @@ export class FreebititService { todayUsageMb: Math.round((todayUsageKb / 1024) * 100) / 100, recentDaysUsage: recentDaysData, isBlacklisted: response.traffic.blackList === "10", - isBlacklisted: response.traffic.blackList === "10", }; this.logger.log(`Retrieved SIM usage for account ${account}`, { @@ -455,30 +333,13 @@ export class FreebititService { } } - /** - * Top up SIM data quota - */ async topUpSim( account: string, quotaMb: number, - options: { - campaignCode?: string; - expiryDate?: string; - scheduledAt?: string; - } = {} - ): Promise { - async topUpSim( - account: string, - quotaMb: number, - options: { - campaignCode?: string; - expiryDate?: string; - scheduledAt?: string; - } = {} + options: { campaignCode?: string; expiryDate?: string; scheduledAt?: string } = {} ): Promise { try { - const quotaKb = quotaMb * 1024; - + const quotaKb = Math.round(quotaMb * 1024); const request: Omit = { account, quota: quotaKb, @@ -486,23 +347,19 @@ export class FreebititService { expire: options.expiryDate, }; - // Use PA05-22 for scheduled top-ups, PA04-04 for immediate - const endpoint = options.scheduledAt ? "/mvno/eachQuota/" : "/master/addSpec/"; - - if (options.scheduledAt && endpoint === "/mvno/eachQuota/") { + const scheduled = !!options.scheduledAt; + const endpoint = scheduled ? "/mvno/eachQuota/" : "/master/addSpec/"; + if (scheduled) { (request as any).runTime = options.scheduledAt; } - await this.makeAuthenticatedRequest(endpoint, request); - + await this.makeAuthenticatedRequest(endpoint, request as any); this.logger.log(`Successfully topped up SIM ${account}`, { account, endpoint, quotaMb, quotaKb, - units: isScheduled ? "KB (PA05-22)" : "MB (PA04-04)", - campaignCode: options.campaignCode, - scheduled: isScheduled, + scheduled, }); } catch (error: any) { this.logger.error(`Failed to top up SIM ${account}`, { @@ -514,38 +371,19 @@ export class FreebititService { } } - /** - * Get SIM top-up history - */ - async getSimTopUpHistory( - account: string, - fromDate: string, - toDate: string - ): Promise { - async getSimTopUpHistory( - account: string, - fromDate: string, - toDate: string - ): Promise { + async getSimTopUpHistory(account: string, fromDate: string, toDate: string): Promise { try { - const request: Omit = { - const request: Omit = { - account, - fromDate, - toDate, - }; - + const request: Omit = { account, fromDate, toDate }; const response = await this.makeAuthenticatedRequest( "/mvno/getQuotaHistory/", - "/mvno/getQuotaHistory/", - request + request as any ); const history: SimTopUpHistory = { account, - totalAdditions: response.total, - additionCount: response.count, - history: response.quotaHistory.map(item => ({ + totalAdditions: Number(response.total) || 0, + additionCount: Number(response.count) || 0, + history: response.quotaHistory.map((item) => ({ quotaKb: parseInt(item.quota, 10), quotaMb: Math.round((parseInt(item.quota, 10) / 1024) * 100) / 100, addedDate: item.date, @@ -569,27 +407,12 @@ export class FreebititService { } } - /** - * Change SIM plan - */ async changeSimPlan( account: string, newPlanCode: string, - options: { - assignGlobalIp?: boolean; - scheduledAt?: string; - } = {} - ): Promise<{ ipv4?: string; ipv6?: string }> { - async changeSimPlan( - account: string, - newPlanCode: string, - options: { - assignGlobalIp?: boolean; - scheduledAt?: string; - } = {} + options: { assignGlobalIp?: boolean; scheduledAt?: string } = {} ): Promise<{ ipv4?: string; ipv6?: string }> { try { - const request: Omit = { const request: Omit = { account, plancode: newPlanCode, @@ -599,8 +422,7 @@ export class FreebititService { const response = await this.makeAuthenticatedRequest( "/mvno/changePlan/", - "/mvno/changePlan/", - request + request as any ); this.logger.log(`Successfully changed SIM plan for account ${account}`, { @@ -610,10 +432,7 @@ export class FreebititService { scheduled: !!options.scheduledAt, }); - return { - ipv4: response.ipv4, - ipv6: response.ipv6, - }; + return { ipv4: response.ipv4, ipv6: response.ipv6 }; } catch (error: any) { this.logger.error(`Failed to change SIM plan for account ${account}`, { error: error.message, @@ -624,53 +443,30 @@ export class FreebititService { } } - /** - * Update SIM optional features (voicemail, call waiting, international roaming, network type) - * Uses AddSpec endpoint for immediate changes - */ async updateSimFeatures( account: string, - features: { - voiceMailEnabled?: boolean; - callWaitingEnabled?: boolean; - internationalRoamingEnabled?: boolean; - networkType?: string; // '4G' | '5G' - } - ): Promise { - async updateSimFeatures( - account: string, - features: { - voiceMailEnabled?: boolean; - callWaitingEnabled?: boolean; - internationalRoamingEnabled?: boolean; - networkType?: string; // '4G' | '5G' - } + features: { voiceMailEnabled?: boolean; callWaitingEnabled?: boolean; internationalRoamingEnabled?: boolean; networkType?: string } ): Promise { try { - const request: Omit = { - account, - }; + const request: Omit = { account }; if (typeof features.voiceMailEnabled === "boolean") { - request.voiceMail = features.voiceMailEnabled ? ("10" as const) : ("20" as const); - request.voicemail = request.voiceMail; // include alternate casing for compatibility + request.voiceMail = features.voiceMailEnabled ? "10" : "20"; + request.voicemail = request.voiceMail; } if (typeof features.callWaitingEnabled === "boolean") { - request.callWaiting = features.callWaitingEnabled ? ("10" as const) : ("20" as const); + request.callWaiting = features.callWaitingEnabled ? "10" : "20"; request.callwaiting = request.callWaiting; } if (typeof features.internationalRoamingEnabled === "boolean") { - request.worldWing = features.internationalRoamingEnabled - ? ("10" as const) - : ("20" as const); + request.worldWing = features.internationalRoamingEnabled ? "10" : "20"; request.worldwing = request.worldWing; } if (features.networkType) { request.contractLine = features.networkType; } - await this.makeAuthenticatedRequest("/master/addSpec/", request); - + await this.makeAuthenticatedRequest("/master/addSpec/", request as any); this.logger.log(`Updated SIM features for account ${account}`, { account, voiceMailEnabled: features.voiceMailEnabled, @@ -680,104 +476,53 @@ export class FreebititService { }); } catch (error: unknown) { const message = error instanceof Error ? error.message : String(error); - this.logger.error(`Failed to update SIM features for account ${account}`, { - error: message, - account, - }); + this.logger.error(`Failed to update SIM features for account ${account}`, { error: message, account }); throw error as Error; } } - /** - * Cancel SIM service via PA02-04 (master/cnclAcnt) - */ async cancelSim(account: string, scheduledAt?: string): Promise { try { - const request: Omit = { - account, - runDate: scheduledAt, - }; - - await this.makeAuthenticatedRequest( - "/mvno/releasePlan/", - request - ); - - this.logger.log(`Successfully cancelled SIM for account ${account}`, { - account, - runDate: scheduledAt, - }); + const request: Omit = { account, runTime: scheduledAt }; + await this.makeAuthenticatedRequest("/mvno/releasePlan/", request as any); + this.logger.log(`Successfully cancelled SIM for account ${account}`, { account, runTime: scheduledAt }); } catch (error: any) { - this.logger.error(`Failed to cancel SIM for account ${account}`, { - error: error.message, - account, - }); + this.logger.error(`Failed to cancel SIM for account ${account}`, { error: error.message, account }); throw error as Error; } } - /** - * Reissue eSIM profile using reissueProfile endpoint - */ async reissueEsimProfile(account: string): Promise { try { const request: Omit = { account }; - - await this.makeAuthenticatedRequest( - "/esim/reissueProfile/", - request - ); - - this.logger.log(`Successfully reissued eSIM profile for account ${account}`, { account }); + await this.makeAuthenticatedRequest("/esim/reissueProfile/", request as any); + this.logger.log(`Successfully requested eSIM reissue for account ${account}`); } catch (error: any) { - this.logger.error(`Failed to reissue eSIM profile for account ${account}`, { - error: error.message, - account, - }); + this.logger.error(`Failed to reissue eSIM profile for account ${account}`, { error: error.message, account }); throw error as Error; } } - /** - * Reissue eSIM profile using addAcnt endpoint (enhanced method based on Salesforce implementation) - */ async reissueEsimProfileEnhanced( - account: string, account: string, newEid: string, - options: { - oldProductNumber?: string; - oldEid?: string; - planCode?: string; - } = {} + options: { oldProductNumber?: string; oldEid?: string; planCode?: string } = {} ): Promise { try { - const request: Omit = { - aladinOperated: "20", const request: Omit = { aladinOperated: "20", account, eid: newEid, - addKind: "R", // R = reissue - addKind: "R", // R = reissue - reissue: { - oldProductNumber: options.oldProductNumber, - oldEid: options.oldEid, - }, + addKind: "R", + reissue: { oldProductNumber: options.oldProductNumber, oldEid: options.oldEid }, + planCode: options.planCode, }; - // Add optional fields - if (options.planCode) { - request.planCode = options.planCode; - } - await this.makeAuthenticatedRequest( "/mvno/esim/addAcnt/", - "/mvno/esim/addAcnt/", - request + request as any ); - this.logger.log(`Successfully reissued eSIM profile via addAcnt for account ${account}`, { this.logger.log(`Successfully reissued eSIM profile via addAcnt for account ${account}`, { account, newEid, @@ -794,17 +539,13 @@ export class FreebititService { } } - /** - * Activate a new eSIM account via PA05-41 addAcct (JSON API) - * This supports optional scheduling (shipDate) and MNP payload. - */ async activateEsimAccountNew(params: { - account: string; // MSISDN to be activated (required by Freebit) - eid: string; // 32-digit EID + account: string; + eid: string; planCode?: string; contractLine?: "4G" | "5G"; aladinOperated?: "10" | "20"; - shipDate?: string; // YYYYMMDD; if provided we send as scheduled activation date + shipDate?: string; mnp?: { reserveNumber: string; reserveExpireDate: string }; identity?: { firstnameKanji?: string; @@ -815,16 +556,7 @@ export class FreebititService { birthday?: string; }; }): Promise { - const { - account, - eid, - planCode, - contractLine, - aladinOperated = "10", - shipDate, - mnp, - identity, - } = params; + const { account, eid, planCode, contractLine, aladinOperated = "10", shipDate, mnp, identity } = params; if (!account || !eid) { throw new BadRequestException("activateEsimAccountNew requires account and eid"); @@ -858,9 +590,6 @@ export class FreebititService { }); } - /** - * Health check for Freebit API - */ async healthCheck(): Promise { try { await this.getAuthKey(); @@ -872,19 +601,17 @@ export class FreebititService { } } -// Custom error class for Freebit API errors class FreebititErrorImpl extends Error { public readonly resultCode: string; - public readonly statusCode: string; + public readonly statusCode: string | number; public readonly freebititMessage: string; - constructor(message: string, resultCode: string, statusCode: string, freebititMessage: string) { - constructor(message: string, resultCode: string, statusCode: string, freebititMessage: string) { + constructor(message: string, resultCode: string | number, statusCode: string | number, freebititMessage: string) { super(message); this.name = "FreebititError"; - this.name = "FreebititError"; - this.resultCode = resultCode; + this.resultCode = String(resultCode); this.statusCode = statusCode; - this.freebititMessage = freebititMessage; + this.freebititMessage = String(freebititMessage); } } + diff --git a/apps/bff/src/vendors/freebit/interfaces/freebit.types.ts b/apps/bff/src/vendors/freebit/interfaces/freebit.types.ts index 0dfbd5b7..541ec245 100644 --- a/apps/bff/src/vendors/freebit/interfaces/freebit.types.ts +++ b/apps/bff/src/vendors/freebit/interfaces/freebit.types.ts @@ -1,20 +1,17 @@ -// Freebit API Type Definitions +// Freebit API Type Definitions (cleaned) export interface FreebititAuthRequest { oemId: string; // 4-char alphanumeric ISP identifier oemKey: string; // 32-char auth key - oemId: string; // 4-char alphanumeric ISP identifier - oemKey: string; // 32-char auth key } export interface FreebititAuthResponse { resultCode: string; status: { message: string; - statusCode: string; + statusCode: string | number; }; authKey: string; // Token for subsequent API calls - authKey: string; // Token for subsequent API calls } export interface FreebititAccountDetailsRequest { @@ -33,7 +30,6 @@ export interface FreebititAccountDetailsResponse { statusCode: string | number; }; masterAccount?: string; - // Docs show this can be an array (MASTER + MVNO) or a single object for MVNO responseDatas: | { kind: "MASTER" | "MVNO" | string; @@ -52,11 +48,8 @@ export interface FreebititAccountDetailsResponse { 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; - date: string | number; - }; + quota?: number; // Remaining quota + async?: { func: string; date: string | number }; } | Array<{ kind: "MASTER" | "MVNO" | string; @@ -76,11 +69,7 @@ export interface FreebititAccountDetailsResponse { ipv4?: string; ipv6?: string; quota?: number; - async?: { - func: "regist" | "stop" | "resume" | "cancel" | "pinset" | "pinunset" | string; - date: string | number; - }; - }>; + async?: { func: string; date: string | number }; }>; } @@ -93,15 +82,13 @@ export interface FreebititTrafficInfoResponse { resultCode: string; status: { message: string; - statusCode: string; + statusCode: string | number; }; account: string; traffic: { - today: string; // Today's usage in KB today: string; // Today's usage in KB inRecentDays: string; // Comma-separated recent days usage blackList: string; // 10=blacklisted, 20=not blacklisted - blackList: string; // 10=blacklisted, 20=not blacklisted }; } @@ -115,17 +102,14 @@ export interface FreebititTopUpRequest { export interface FreebititTopUpResponse { resultCode: string; - status: { - message: string; - statusCode: string; - }; + status: { message: string; statusCode: string | number }; } // AddSpec request for updating SIM options/features immediately export interface FreebititAddSpecRequest { authKey: string; account: string; - kind?: string; // Required by PA04-04 (/master/addSpec/), e.g. 'MVNO' + kind?: string; // e.g. 'MVNO' // Feature flags: 10 = enabled, 20 = disabled voiceMail?: "10" | "20"; voicemail?: "10" | "20"; @@ -133,21 +117,12 @@ export interface FreebititAddSpecRequest { callwaiting?: "10" | "20"; worldWing?: "10" | "20"; worldwing?: "10" | "20"; - voiceMail?: "10" | "20"; - voicemail?: "10" | "20"; - callWaiting?: "10" | "20"; - callwaiting?: "10" | "20"; - worldWing?: "10" | "20"; - worldwing?: "10" | "20"; contractLine?: string; // '4G' or '5G' } export interface FreebititAddSpecResponse { resultCode: string; - status: { - message: string; - statusCode: string; - }; + status: { message: string; statusCode: string | number }; } export interface FreebititQuotaHistoryRequest { @@ -155,25 +130,15 @@ export interface FreebititQuotaHistoryRequest { account: string; fromDate: string; // YYYYMMDD toDate: string; // YYYYMMDD - fromDate: string; // YYYYMMDD - toDate: string; // YYYYMMDD } export interface FreebititQuotaHistoryResponse { resultCode: string; - status: { - message: string; - statusCode: string; - }; + status: { message: string; statusCode: string | number }; account: string; total: number; count: number; - quotaHistory: Array<{ - quota: string; - expire: string; - date: string; - quotaCode: string; - }>; + quotaHistory: Array<{ quota: string; expire: string; date: string; quotaCode: string }>; } export interface FreebititPlanChangeRequest { @@ -181,46 +146,16 @@ export interface FreebititPlanChangeRequest { account: string; plancode: string; globalip?: "0" | "1"; // 0=no IP, 1=assign global IP - runTime?: string; // YYYYMMDD - optional, immediate if omitted + runTime?: string; // YYYYMMDD - optional } export interface FreebititPlanChangeResponse { resultCode: string; - status: { - message: string; - statusCode: string; - }; + status: { message: string; statusCode: string | number }; ipv4?: string; ipv6?: string; } -// PA05-06: MVNO Voice Option Change -export interface FreebititVoiceOptionChangeRequest { - authKey: string; - account: string; - userConfirmed: "10" | "20"; - aladinOperated: "10" | "20"; - talkOption: { - voiceMail?: "10" | "20"; - callWaiting?: "10" | "20"; - worldWing?: "10" | "20"; - worldCall?: "10" | "20"; - callTransfer?: "10" | "20"; - callTransferNoId?: "10" | "20"; - worldCallCreditLimit?: string; - worldWingCreditLimit?: string; - }; -} - -export interface FreebititVoiceOptionChangeResponse { - resultCode: string; - status: { - message: string; - statusCode: string; - }; -} - -// PA05-38: MVNO Contract Change (4G/5G) export interface FreebititContractLineChangeRequest { authKey: string; account: string; @@ -239,16 +174,12 @@ export interface FreebititContractLineChangeResponse { export interface FreebititCancelPlanRequest { authKey: string; account: string; - runTime?: string; // YYYYMMDD - optional, immediate if omitted - runTime?: string; // YYYYMMDD - optional, immediate if omitted + runTime?: string; // YYYYMMDD - optional } export interface FreebititCancelPlanResponse { resultCode: string; - status: { - message: string; - statusCode: string; - }; + status: { message: string; statusCode: string | number }; } // PA02-04: Account Cancellation (master/cnclAcnt) @@ -261,10 +192,7 @@ export interface FreebititCancelAccountRequest { export interface FreebititCancelAccountResponse { resultCode: string; - status: { - message: string; - statusCode: string; - }; + status: { message: string; statusCode: string | number }; } export interface FreebititEsimReissueRequest { @@ -274,10 +202,7 @@ export interface FreebititEsimReissueRequest { export interface FreebititEsimReissueResponse { resultCode: string; - status: { - message: string; - statusCode: string; - }; + status: { message: string; statusCode: string | number }; } export interface FreebititEsimAddAccountRequest { @@ -286,23 +211,16 @@ export interface FreebititEsimAddAccountRequest { account: string; eid: string; addKind: "N" | "R"; // N = new, R = reissue - addKind: "N" | "R"; // N = new, R = reissue createType?: string; simKind?: string; planCode?: string; contractLine?: string; - reissue?: { - oldProductNumber?: string; - oldEid?: string; - }; + reissue?: { oldProductNumber?: string; oldEid?: string }; } export interface FreebititEsimAddAccountResponse { resultCode: string; - status: { - message: string; - statusCode: string; - }; + status: { message: string; statusCode: string | number }; } // PA05-41 eSIM Account Activation (addAcct) @@ -320,10 +238,7 @@ export interface FreebititEsimAccountActivationRequest { addKind?: string; // e.g., 'R' for reissue oldEid?: string; oldProductNumber?: string; - mnp?: { - reserveNumber: string; - reserveExpireDate: string; // YYYYMMDD - }; + mnp?: { reserveNumber: string; reserveExpireDate: string }; firstnameKanji?: string; lastnameKanji?: string; firstnameZenKana?: string; @@ -340,7 +255,7 @@ export interface FreebititEsimAccountActivationRequest { export interface FreebititEsimAccountActivationResponse { resultCode: number | string; status?: unknown; - statusCode?: string; + statusCode?: string | number; message?: string; } @@ -355,9 +270,6 @@ export interface SimDetails { status: "active" | "suspended" | "cancelled" | "pending"; simType: "physical" | "esim"; size: "standard" | "nano" | "micro" | "esim"; - status: "active" | "suspended" | "cancelled" | "pending"; - simType: "physical" | "esim"; - size: "standard" | "nano" | "micro" | "esim"; hasVoice: boolean; hasSms: boolean; remainingQuotaKb: number; @@ -365,26 +277,18 @@ export interface SimDetails { startDate?: string; ipv4?: string; ipv6?: string; - // Optional extended service features voiceMailEnabled?: boolean; callWaitingEnabled?: boolean; internationalRoamingEnabled?: boolean; networkType?: string; // e.g., '4G' or '5G' - pendingOperations?: Array<{ - operation: string; - scheduledDate: string; - }>; + pendingOperations?: Array<{ operation: string; scheduledDate: string }>; } export interface SimUsage { account: string; todayUsageKb: number; todayUsageMb: number; - recentDaysUsage: Array<{ - date: string; - usageKb: number; - usageMb: number; - }>; + recentDaysUsage: Array<{ date: string; usageKb: number; usageMb: number }>; isBlacklisted: boolean; } @@ -404,7 +308,7 @@ export interface SimTopUpHistory { // Error handling export interface FreebititError extends Error { resultCode: string; - statusCode: string; + statusCode: string | number; freebititMessage: string; } diff --git a/apps/bff/src/vendors/vendors.module.ts b/apps/bff/src/vendors/vendors.module.ts index b6f97fdb..dd077c2c 100644 --- a/apps/bff/src/vendors/vendors.module.ts +++ b/apps/bff/src/vendors/vendors.module.ts @@ -1,10 +1,11 @@ import { Module } from "@nestjs/common"; import { WhmcsModule } from "./whmcs/whmcs.module"; import { SalesforceModule } from "./salesforce/salesforce.module"; +import { FreebititModule } from "./freebit/freebit.module"; @Module({ - imports: [WhmcsModule, SalesforceModule], + imports: [WhmcsModule, SalesforceModule, FreebititModule], providers: [], - exports: [WhmcsModule, SalesforceModule], + exports: [WhmcsModule, SalesforceModule, FreebititModule], }) export class VendorsModule {} diff --git a/apps/portal/src/app/(portal)/subscriptions/[id]/sim/cancel/page.tsx b/apps/portal/src/app/(portal)/subscriptions/[id]/sim/cancel/page.tsx index ab533df1..28832d05 100644 --- a/apps/portal/src/app/(portal)/subscriptions/[id]/sim/cancel/page.tsx +++ b/apps/portal/src/app/(portal)/subscriptions/[id]/sim/cancel/page.tsx @@ -1,14 +1,32 @@ "use client"; import Link from "next/link"; -import { useParams } from "next/navigation"; -import { useState } from "react"; +import { useParams, useRouter } from "next/navigation"; +import { useEffect, useMemo, useState, type ReactNode } from "react"; import { authenticatedApi } from "@/lib/api"; import type { SimDetails } from "@/features/sim-management/components/SimDetailsCard"; import { formatPlanShort } from "@/lib/plan"; type Step = 1 | 2 | 3; +function Notice({ title, children }: { title: string; children: ReactNode }) { + return ( +
+
{title}
+
{children}
+
+ ); +} + +function InfoRow({ label, value }: { label: string; value: string }) { + return ( +
+
{label}
+
{value}
+
+ ); +} + export default function SimCancelPage() { const params = useParams(); const router = useRouter(); @@ -162,7 +180,6 @@ export default function SimCancelPage() { requested from this online form. Please contact Assist Solutions Customer Support (info@asolutions.co.jp) for more information. - 4
diff --git a/apps/portal/src/app/(portal)/subscriptions/[id]/sim/change-plan/page.tsx b/apps/portal/src/app/(portal)/subscriptions/[id]/sim/change-plan/page.tsx index d928a4ed..5d14bd4a 100644 --- a/apps/portal/src/app/(portal)/subscriptions/[id]/sim/change-plan/page.tsx +++ b/apps/portal/src/app/(portal)/subscriptions/[id]/sim/change-plan/page.tsx @@ -2,7 +2,6 @@ import { useState, useMemo } from "react"; import Link from "next/link"; -import { DashboardLayout } from "@/components/layout/dashboard-layout"; import { useParams } from "next/navigation"; import { authenticatedApi } from "@/lib/api"; @@ -55,8 +54,7 @@ export default function SimChangePlanPage() { }; return ( - -
+
- ); } diff --git a/apps/portal/src/app/subscriptions/[id]/page.tsx b/apps/portal/src/app/subscriptions/[id]/page.tsx deleted file mode 100644 index 447a34ba..00000000 --- a/apps/portal/src/app/subscriptions/[id]/page.tsx +++ /dev/null @@ -1,506 +0,0 @@ -"use client"; - -import { useEffect, useState } from "react"; -import { useParams, useSearchParams } from "next/navigation"; -import Link from "next/link"; -import { DashboardLayout } from "@/components/layout/dashboard-layout"; -import { - ArrowLeftIcon, - ServerIcon, - CheckCircleIcon, - ExclamationTriangleIcon, - ClockIcon, - XCircleIcon, - CalendarIcon, - DocumentTextIcon, - ArrowTopRightOnSquareIcon, -} from "@heroicons/react/24/outline"; -import { format } from "date-fns"; -import { useSubscription, useSubscriptionInvoices } from "@/hooks/useSubscriptions"; -import { formatCurrency as sharedFormatCurrency, getCurrencyLocale } from "@/utils/currency"; -import { SimManagementSection } from "@/features/sim-management"; - -export default function SubscriptionDetailPage() { - const params = useParams(); - const searchParams = useSearchParams(); - const [currentPage, setCurrentPage] = useState(1); - const itemsPerPage = 10; - const [showInvoices, setShowInvoices] = useState(true); - const [showSimManagement, setShowSimManagement] = useState(false); - - const subscriptionId = parseInt(params.id as string); - const { data: subscription, isLoading, error } = useSubscription(subscriptionId); - const { - data: invoiceData, - isLoading: invoicesLoading, - error: invoicesError, - } = useSubscriptionInvoices(subscriptionId, { page: currentPage, limit: itemsPerPage }); - - const invoices = invoiceData?.invoices || []; - const pagination = invoiceData?.pagination; - - // Control what sections to show based on URL hash - useEffect(() => { - const updateVisibility = () => { - const hash = typeof window !== "undefined" ? window.location.hash : ""; - const service = (searchParams.get("service") || "").toLowerCase(); - const isSimContext = hash.includes("sim-management") || service === "sim"; - - if (isSimContext) { - // Show only SIM management, hide invoices - setShowInvoices(false); - setShowSimManagement(true); - } else { - // Show only invoices, hide SIM management - setShowInvoices(true); - setShowSimManagement(false); - } - }; - updateVisibility(); - if (typeof window !== "undefined") { - window.addEventListener("hashchange", updateVisibility); - return () => window.removeEventListener("hashchange", updateVisibility); - } - return; - }, [searchParams]); - - const getStatusIcon = (status: string) => { - switch (status) { - case "Active": - return ; - case "Suspended": - return ; - case "Terminated": - return ; - case "Cancelled": - return ; - case "Pending": - return ; - default: - return ; - } - }; - - 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 { - return format(new Date(dateString), "MMM d, yyyy"); - } catch { - return "Invalid date"; - } - }; - - const formatCurrency = (amount: number) => - sharedFormatCurrency(amount || 0, { currency: "JPY", locale: getCurrencyLocale("JPY") }); - - const formatBillingLabel = (cycle: string) => { - switch (cycle) { - case "Monthly": - return "Monthly Billing"; - case "Annually": - return "Annual Billing"; - case "Quarterly": - return "Quarterly Billing"; - case "Semi-Annually": - return "Semi-Annual Billing"; - case "Biennially": - return "Biennial Billing"; - case "Triennially": - return "Triennial Billing"; - case "One-time": - return "One-time Payment"; - default: - return "One-time Payment"; - } - }; - - if (isLoading) { - return ( - -
-
-
-

Loading subscription...

-
-
-
- ); - } - - if (error || !subscription) { - return ( - -
-
-
-
- -
-
-

Error loading subscription

-
- {error instanceof Error ? error.message : "Subscription not found"} -
-
- - ← Back to subscriptions - -
-
-
-
-
-
- ); - } - - return ( - -
-
- {/* Header */} -
-
-
- - - -
- -
-

{subscription.productName}

-

Service ID: {subscription.serviceId}

-
-
-
-
-
- - {/* Subscription Summary Card */} -
-
-
-
- {getStatusIcon(subscription.status)} -
-

Subscription Details

-

Service subscription information

-
-
- - {subscription.status} - -
-
- -
-
-
-

- Billing Amount -

-

- {formatCurrency(subscription.amount)} -

-

{formatBillingLabel(subscription.cycle)}

-
-
-

- Next Due Date -

-

{formatDate(subscription.nextDue)}

-
- - Due date -
-
-
-

- Registration Date -

-

- {formatDate(subscription.registrationDate)} -

- Service created -
-
-
-
- - {/* Navigation tabs for SIM services - More visible and mobile-friendly */} - {subscription.productName.toLowerCase().includes("sim") && ( -
-
-
-
-

Service Management

-

- Switch between billing and SIM management views -

-
-
- - - SIM Management - - - - Billing - -
-
-
-
- )} - - {/* SIM Management Section - Only show when in SIM context and for SIM services */} - {showSimManagement && subscription.productName.toLowerCase().includes("sim") && ( - - )} - - {/* Related Invoices (hidden when viewing SIM management directly) */} - {showInvoices && ( -
-
-
- -

Related Invoices

-
-

- Invoices containing charges for this subscription -

-
- - {invoicesLoading ? ( -
-
-

Loading invoices...

-
- ) : invoicesError ? ( -
- -

Error loading invoices

-

- {invoicesError instanceof Error - ? invoicesError.message - : "Failed to load related invoices"} -

-
- ) : invoices.length === 0 ? ( -
- -

No invoices found

-

- No invoices have been generated for this subscription yet. -

-
- ) : ( - <> -
-
- {invoices.map(invoice => ( -
-
-
-
- {getInvoiceStatusIcon(invoice.status)} -
-
-

- Invoice {invoice.number} -

-

- Issued{" "} - {invoice.issuedAt && - format(new Date(invoice.issuedAt), "MMM d, yyyy")} -

-
-
-
- - {invoice.status} - - - {formatCurrency(invoice.total)} - -
-
-
-
- - Due:{" "} - {invoice.dueDate - ? format(new Date(invoice.dueDate), "MMM d, yyyy") - : "N/A"} - -
- -
-
- ))} -
-
- - {/* Pagination */} - {pagination && pagination.totalPages > 1 && ( -
-
- - -
-
-
-

- Showing{" "} - - {(currentPage - 1) * itemsPerPage + 1} - {" "} - to{" "} - - {Math.min(currentPage * itemsPerPage, pagination.totalItems)} - {" "} - of {pagination.totalItems} results -

-
-
- -
-
-
- )} - - )} -
- )} -
-
-
- ); -} diff --git a/apps/portal/src/app/subscriptions/[id]/sim/reissue/page.tsx b/apps/portal/src/app/subscriptions/[id]/sim/reissue/page.tsx deleted file mode 100644 index 7fd24269..00000000 --- a/apps/portal/src/app/subscriptions/[id]/sim/reissue/page.tsx +++ /dev/null @@ -1,133 +0,0 @@ -"use client"; - -import { useEffect, useState } from "react"; -import Link from "next/link"; -import { useParams, useRouter } from "next/navigation"; -import { DashboardLayout } from "@/components/layout/dashboard-layout"; -import { authenticatedApi } from "@/lib/api"; - -export default function EsimReissuePage() { - const params = useParams(); - const router = useRouter(); - const subscriptionId = parseInt(params.id as string); - const [loading, setLoading] = useState(false); - const [detailsLoading, setDetailsLoading] = useState(true); - const [error, setError] = useState(null); - const [message, setMessage] = useState(null); - const [oldEid, setOldEid] = useState(null); - const [newEid, setNewEid] = useState(""); - - useEffect(() => { - const fetchDetails = async () => { - try { - setDetailsLoading(true); - const data = await authenticatedApi.get<{ eid?: string }>( - `/subscriptions/${subscriptionId}/sim/details` - ); - setOldEid(data?.eid || null); - } catch (e: unknown) { - setError(e instanceof Error ? e.message : "Failed to load SIM details"); - } finally { - setDetailsLoading(false); - } - }; - void fetchDetails(); - }, [subscriptionId]); - - const validEid = (val: string) => /^\d{32}$/.test(val); - - const submit = async (e: React.FormEvent) => { - e.preventDefault(); - setError(null); - setMessage(null); - if (!validEid(newEid)) { - setError("Please enter a valid 32-digit EID"); - return; - } - setLoading(true); - try { - await authenticatedApi.post(`/subscriptions/${subscriptionId}/sim/reissue-esim`, { newEid }); - setMessage("eSIM reissue requested successfully. You will receive the new profile shortly."); - setTimeout(() => router.push(`/subscriptions/${subscriptionId}#sim-management`), 1500); - } catch (e: unknown) { - setError(e instanceof Error ? e.message : "Failed to submit eSIM reissue"); - } finally { - setLoading(false); - } - }; - - return ( - -
-
- - ← Back to SIM Management - -
-
-

Reissue eSIM

-

- Enter the new EID to transfer this eSIM to. We will show your current EID for - confirmation. -

- - {detailsLoading ? ( -
Loading current eSIM details…
- ) : ( -
- -
- {oldEid || "—"} -
-
- )} - - {message && ( -
- {message} -
- )} - {error && ( -
- {error} -
- )} - -
void submit(e)} className="space-y-4"> -
- - setNewEid(e.target.value.trim())} - placeholder="32-digit EID (e.g., 8904….)" - className="mt-1 w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 font-mono" - maxLength={32} - /> -

Must be exactly 32 digits.

-
-
- - - Cancel - -
-
-
-
-
- ); -} diff --git a/apps/portal/src/components/layout/dashboard-layout.tsx b/apps/portal/src/components/layout/dashboard-layout.tsx index 5e9821eb..255439dd 100644 --- a/apps/portal/src/components/layout/dashboard-layout.tsx +++ b/apps/portal/src/components/layout/dashboard-layout.tsx @@ -413,15 +413,10 @@ const NavigationItem = memo(function NavigationItem({ const hasChildren = item.children && item.children.length > 0; const isActive = hasChildren - ? item.children?.some((child: NavigationChild) => + ? (item.children?.some((child: NavigationChild) => pathname.startsWith((child.href || "").split(/[?#]/)[0]) - ) || false - ? item.children?.some((child: NavigationChild) => - pathname.startsWith((child.href || "").split(/[?#]/)[0]) - ) || false - : item.href - ? pathname === item.href - : false; + ) || false) + : (item.href ? pathname === item.href : false); const handleLogout = () => { void logout().then(() => { diff --git a/docs/SUBSCRIPTION-SERVICE-MANAGEMENT.md b/docs/SUBSCRIPTION-SERVICE-MANAGEMENT.md index a8ecb090..6b256e02 100644 --- a/docs/SUBSCRIPTION-SERVICE-MANAGEMENT.md +++ b/docs/SUBSCRIPTION-SERVICE-MANAGEMENT.md @@ -30,7 +30,7 @@ apps/portal/src/features/service-management/ ## Integration -- Entry point: `apps/portal/src/app/subscriptions/[id]/page.tsx` renders `ServiceManagementSection` +- Entry point: `apps/portal/src/app/(portal)/subscriptions/[id]/page.tsx` renders `ServiceManagementSection` - Detection: SIM availability is inferred from `subscription.productName` including `sim` (case-insensitive) ## Future Expansion