diff --git a/apps/bff/package.json b/apps/bff/package.json index 817ac42b..6b946aa5 100644 --- a/apps/bff/package.json +++ b/apps/bff/package.json @@ -48,7 +48,6 @@ "class-transformer": "^0.5.1", "class-validator": "^0.14.2", "cookie-parser": "^1.4.7", - "csurf": "^1.11.0", "express": "^4.21.2", "helmet": "^8.1.0", "ioredis": "^5.7.0", @@ -67,7 +66,7 @@ "salesforce-pubsub-api-client": "^5.5.0", "speakeasy": "^2.0.0", "uuid": "^11.1.0", - "zod": "^4.0.17" + "zod": "^4.1.9" }, "devDependencies": { "@nestjs/cli": "^11.0.10", diff --git a/apps/bff/src/app/bootstrap.ts b/apps/bff/src/app/bootstrap.ts index 17849fef..5d0e5d4a 100644 --- a/apps/bff/src/app/bootstrap.ts +++ b/apps/bff/src/app/bootstrap.ts @@ -110,7 +110,6 @@ export async function bootstrap(): Promise { "Accept", "Authorization", "X-API-Key", - "X-CSRF-Token", ], exposedHeaders: ["X-Total-Count", "X-Page-Count"], maxAge: 86400, // 24 hours diff --git a/apps/bff/src/integrations/freebit/interfaces/freebit.types.ts b/apps/bff/src/integrations/freebit/interfaces/freebit.types.ts index 410aba6d..3237ce8a 100644 --- a/apps/bff/src/integrations/freebit/interfaces/freebit.types.ts +++ b/apps/bff/src/integrations/freebit/interfaces/freebit.types.ts @@ -54,12 +54,9 @@ export interface FreebitAccountDetailsResponse { responseDatas: FreebitAccountDetail[]; } -export interface FreebitTrafficInfoResponseEntry { +export interface FreebitTrafficInfoRequest { + authKey: string; account: string; - todayUsageMb?: number | string; - todayUsageKb?: number | string; - monthlyUsageMb?: number | string; - monthlyUsageKb?: number | string; } export interface FreebitTrafficInfoResponse { @@ -68,7 +65,12 @@ export interface FreebitTrafficInfoResponse { message: string; statusCode: string | number; }; - responseDatas: FreebitTrafficInfoResponseEntry[]; + account: string; + traffic: { + today: string; // Today's usage in KB + inRecentDays: string; // Comma-separated recent days usage + blackList: string; // 10=blacklisted, 20=not blacklisted + }; } export interface FreebitTopUpRequest { @@ -104,22 +106,26 @@ export interface FreebitAddSpecResponse { status: { message: string; statusCode: string | number }; } -export interface FreebitQuotaAddition { - date?: string; - quotaMb?: number | string; - quotaKb?: number | string; - description?: string; +export interface FreebitQuotaHistoryRequest { + authKey: string; + account: string; + fromDate: string; + toDate: string; } -export interface FreebitQuotaHistoryResponseEntry { - account: string; - additions?: FreebitQuotaAddition[]; +export interface FreebitQuotaHistoryItem { + quota: string; // KB as string + date: string; + expire: string; + quotaCode: string; } export interface FreebitQuotaHistoryResponse { resultCode: string; status: { message: string; statusCode: string | number }; - responseDatas: FreebitQuotaHistoryResponseEntry[]; + total: string | number; + count: string | number; + quotaHistory: FreebitQuotaHistoryItem[]; } export interface FreebitPlanChangeRequest { @@ -226,15 +232,17 @@ export interface FreebitEsimReissueResponse { export interface FreebitEsimAddAccountRequest { authKey: string; - aladinOperated?: string; + aladinOperated: string; // '10' for issue, '20' for no-issue account: string; eid: string; addKind: "N" | "R"; // N = new, R = reissue - createType?: string; - simKind?: string; + shipDate?: string; planCode?: string; contractLine?: string; - reissue?: { oldProductNumber?: string; oldEid?: string }; + mnp?: { + reserveNumber: string; + reserveExpireDate: string; + }; } export interface FreebitEsimAddAccountResponse { @@ -272,8 +280,11 @@ export interface FreebitEsimAccountActivationRequest { } export interface FreebitEsimAccountActivationResponse { - resultCode: number | string; - status?: unknown; + resultCode: string; + status?: { + message?: string; + statusCode?: string | number; + }; statusCode?: string | number; message?: string; } @@ -303,20 +314,23 @@ export interface SimUsage { account: string; todayUsageMb: number; todayUsageKb: number; - monthlyUsageMb: number; - monthlyUsageKb: number; - recentDaysUsage?: Array<{ date: string; usageKb: number; usageMb: number }>; - lastUpdated: string; + monthlyUsageMb?: number; + monthlyUsageKb?: number; + recentDaysUsage: Array<{ date: string; usageKb: number; usageMb: number }>; + isBlacklisted: boolean; + lastUpdated?: string; } export interface SimTopUpHistory { account: string; totalAdditions: number; - additions: Array<{ - date: string; - quotaMb: number; + additionCount: number; + history: Array<{ quotaKb: number; - description: string; + quotaMb: number; + addedDate: string; + expiryDate: string; + campaignCode: string; }>; } diff --git a/apps/bff/src/integrations/freebit/services/freebit-client.service.ts b/apps/bff/src/integrations/freebit/services/freebit-client.service.ts index dd4d847b..23e406a8 100644 --- a/apps/bff/src/integrations/freebit/services/freebit-client.service.ts +++ b/apps/bff/src/integrations/freebit/services/freebit-client.service.ts @@ -109,6 +109,93 @@ export class FreebitClientService { throw new FreebitError("Request failed after all retry attempts"); } + /** + * Make an authenticated JSON request to Freebit API (for PA05-41) + */ + async makeAuthenticatedJsonRequest< + TResponse extends FreebitResponseBase, + TPayload extends object, + >(endpoint: string, payload: TPayload): Promise { + const config = this.authService.getConfig(); + const url = `${config.baseUrl}${endpoint}`; + + for (let attempt = 1; attempt <= config.retryAttempts; attempt++) { + try { + this.logger.debug(`Freebit JSON API request (attempt ${attempt}/${config.retryAttempts})`, { + url, + payload: this.sanitizePayload(payload as Record), + }); + + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), config.timeout); + + const response = await fetch(url, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + signal: controller.signal, + }); + + clearTimeout(timeout); + + if (!response.ok) { + throw new FreebitError( + `HTTP ${response.status}: ${response.statusText}`, + response.status.toString() + ); + } + + const responseData = (await response.json()) as TResponse; + + if (responseData.resultCode && responseData.resultCode !== "100") { + throw new FreebitError( + `API Error: ${responseData.status?.message || "Unknown error"}`, + responseData.resultCode, + responseData.status?.statusCode, + responseData.status?.message + ); + } + + this.logger.debug("Freebit JSON API request successful", { + url, + resultCode: responseData.resultCode, + }); + + return responseData; + } catch (error: unknown) { + if (error instanceof FreebitError) { + if (error.isAuthError() && attempt === 1) { + this.logger.warn("Auth error detected, clearing cache and retrying"); + this.authService.clearAuthCache(); + continue; + } + if (!error.isRetryable() || attempt === config.retryAttempts) { + throw error; + } + } + + if (attempt === config.retryAttempts) { + const message = getErrorMessage(error); + this.logger.error(`Freebit JSON API request failed after ${config.retryAttempts} attempts`, { + url, + error: message, + }); + throw new FreebitError(`Request failed: ${message}`); + } + + const delay = Math.min(1000 * Math.pow(2, attempt - 1), 10000); + this.logger.warn(`Freebit JSON API request failed, retrying in ${delay}ms`, { + url, + attempt, + error: getErrorMessage(error), + }); + await new Promise(resolve => setTimeout(resolve, delay)); + } + } + + throw new FreebitError("Request failed after all retry attempts"); + } + /** * Make a simple request without authentication (for health checks) */ diff --git a/apps/bff/src/integrations/freebit/services/freebit-mapper.service.ts b/apps/bff/src/integrations/freebit/services/freebit-mapper.service.ts index 4eb58074..7f04eea3 100644 --- a/apps/bff/src/integrations/freebit/services/freebit-mapper.service.ts +++ b/apps/bff/src/integrations/freebit/services/freebit-mapper.service.ts @@ -70,41 +70,48 @@ export class FreebitMapperService { * Map Freebit traffic info response to SimUsage */ mapToSimUsage(response: FreebitTrafficInfoResponse): SimUsage { - const traffic = response.responseDatas[0]; - if (!traffic) { + if (!response.traffic) { throw new Error("No traffic data in response"); } + 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], + usageKb: parseInt(usage, 10) || 0, + usageMb: Math.round(((parseInt(usage, 10) || 0) / 1024) * 100) / 100, + })); + return { - account: String(traffic.account ?? ""), - todayUsageMb: Number(traffic.todayUsageMb ?? 0), - todayUsageKb: Number(traffic.todayUsageKb ?? 0), - monthlyUsageMb: Number(traffic.monthlyUsageMb ?? 0), - monthlyUsageKb: Number(traffic.monthlyUsageKb ?? 0), - recentDaysUsage: [], - lastUpdated: new Date().toISOString(), + account: String(response.account ?? ""), + todayUsageMb: Math.round((todayUsageKb / 1024) * 100) / 100, + todayUsageKb, + recentDaysUsage: recentDaysData, + isBlacklisted: response.traffic.blackList === "10", }; } /** * Map Freebit quota history response to SimTopUpHistory */ - mapToSimTopUpHistory(response: FreebitQuotaHistoryResponse): SimTopUpHistory { - const history = response.responseDatas[0]; - if (!history) { + mapToSimTopUpHistory(response: FreebitQuotaHistoryResponse, account: string): SimTopUpHistory { + if (!response.quotaHistory) { throw new Error("No history data in response"); } - const additions = Array.isArray(history.additions) ? history.additions : []; - return { - account: String(history.account ?? ""), - totalAdditions: additions.length, - additions: additions.map(addition => ({ - date: String(addition?.date ?? ""), - quotaMb: Number(addition?.quotaMb ?? 0), - quotaKb: Number(addition?.quotaKb ?? 0), - description: String(addition?.description ?? ""), + account, + 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, + expiryDate: item.expire, + campaignCode: item.quotaCode, })), }; } 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 2428c03f..cd95c18d 100644 --- a/apps/bff/src/integrations/freebit/services/freebit-operations.service.ts +++ b/apps/bff/src/integrations/freebit/services/freebit-operations.service.ts @@ -15,14 +15,16 @@ import type { FreebitQuotaHistoryResponse, FreebitPlanChangeRequest, FreebitPlanChangeResponse, + FreebitAddSpecRequest, + FreebitAddSpecResponse, FreebitCancelPlanRequest, FreebitCancelPlanResponse, FreebitEsimReissueRequest, FreebitEsimReissueResponse, - FreebitAddSpecRequest, - FreebitAddSpecResponse, - FreebitPlanChangePayload, - FreebitAddSpecPayload, + FreebitEsimAddAccountRequest, + FreebitEsimAddAccountResponse, + FreebitEsimAccountActivationRequest, + FreebitEsimAccountActivationResponse, SimDetails, SimUsage, SimTopUpHistory, @@ -114,9 +116,7 @@ export class FreebitOperationsService { */ async getSimUsage(account: string): Promise { try { - const request: Omit = { - requestDatas: [{ kind: "MVNO", account }], - } as any; + const request: Omit = { account }; const response = await this.client.makeAuthenticatedRequest< FreebitTrafficInfoResponse, @@ -140,26 +140,35 @@ export class FreebitOperationsService { async topUpSim( account: string, quotaMb: number, - options: { description?: string } = {} + options: { campaignCode?: string; expiryDate?: string; scheduledAt?: string } = {} ): Promise { try { + const quotaKb = Math.round(quotaMb * 1024); const request: Omit = { - requestDatas: [ - { - kind: "MVNO", - account, - quotaMb, - description: options.description || `Data top-up: ${quotaMb}MB`, - }, - ], - } as any; + account, + quota: quotaKb, + quotaCode: options.campaignCode, + expire: options.expiryDate, + }; + + const scheduled = !!options.scheduledAt; + const endpoint = scheduled ? "/mvno/eachQuota/" : "/master/addSpec/"; + if (scheduled) { + (request as any).runTime = options.scheduledAt; + } await this.client.makeAuthenticatedRequest( - "/mvno/addQuota/", + endpoint, request ); - this.logger.log(`Successfully topped up ${quotaMb}MB for account ${account}`); + this.logger.log(`Successfully topped up ${quotaMb}MB for account ${account}`, { + account, + endpoint, + quotaMb, + quotaKb, + scheduled, + }); } catch (error) { const message = getErrorMessage(error); this.logger.error(`Failed to top up SIM for account ${account}`, { @@ -180,23 +189,18 @@ export class FreebitOperationsService { toDate: string ): Promise { try { - const request: Omit = { - requestDatas: [ - { - kind: "MVNO", - account, - fromDate, - toDate, - }, - ], - } as any; + const request: Omit = { + account, + fromDate, + toDate + }; const response = await this.client.makeAuthenticatedRequest< FreebitQuotaHistoryResponse, typeof request >("/mvno/getQuotaHistory/", request); - return this.mapper.mapToSimTopUpHistory(response); + return this.mapper.mapToSimTopUpHistory(response, account); } catch (error) { const message = getErrorMessage(error); this.logger.error(`Failed to get SIM top-up history for account ${account}`, { @@ -218,30 +222,28 @@ export class FreebitOperationsService { options: { assignGlobalIp?: boolean; scheduledAt?: string } = {} ): Promise<{ ipv4?: string; ipv6?: string }> { try { - const request: FreebitPlanChangePayload = { - requestDatas: [ - { - kind: "MVNO", - account, - newPlanCode, - assignGlobalIp: options.assignGlobalIp ?? false, - scheduledAt: options.scheduledAt, - }, - ], + const request: Omit = { + account, + plancode: newPlanCode, + globalip: options.assignGlobalIp ? "1" : "0", + runTime: options.scheduledAt, }; const response = await this.client.makeAuthenticatedRequest< FreebitPlanChangeResponse, - FreebitPlanChangePayload + typeof request >("/mvno/changePlan/", request); - const result = response.responseDatas?.[0] ?? {}; - - this.logger.log(`Successfully changed plan for account ${account} to ${newPlanCode}`); + this.logger.log(`Successfully changed plan for account ${account} to ${newPlanCode}`, { + account, + newPlanCode, + assignGlobalIp: options.assignGlobalIp, + scheduled: !!options.scheduledAt, + }); return { - ipv4: result.ipv4, - ipv6: result.ipv6, + ipv4: response.ipv4, + ipv6: response.ipv6, }; } catch (error) { const message = getErrorMessage(error); @@ -267,51 +269,37 @@ export class FreebitOperationsService { } ): Promise { try { - const requests: FreebitAddSpecPayload[] = []; + const request: Omit = { account }; - const createSpecPayload = ( - specCode: string, - additional: Partial = {} - ): FreebitAddSpecPayload => ({ - requestDatas: [ - { - kind: "MVNO", - account, - specCode, - ...additional, - }, - ], - }); - - // Voice options (PA05-06) + // Use both variations for compatibility if (typeof features.voiceMailEnabled === "boolean") { - requests.push(createSpecPayload("PA05-06", { enabled: features.voiceMailEnabled })); + request.voiceMail = features.voiceMailEnabled ? "10" : "20"; + request.voicemail = request.voiceMail; } - if (typeof features.callWaitingEnabled === "boolean") { - requests.push(createSpecPayload("PA05-06", { enabled: features.callWaitingEnabled })); + request.callWaiting = features.callWaitingEnabled ? "10" : "20"; + request.callwaiting = request.callWaiting; } - if (typeof features.internationalRoamingEnabled === "boolean") { - requests.push( - createSpecPayload("PA05-06", { enabled: features.internationalRoamingEnabled }) - ); + request.worldWing = features.internationalRoamingEnabled ? "10" : "20"; + request.worldwing = request.worldWing; } - - // Network type (PA05-38 for contract line change) if (features.networkType) { - requests.push(createSpecPayload("PA05-38", { networkType: features.networkType })); + request.contractLine = features.networkType; } - // Execute all requests - for (const request of requests) { - await this.client.makeAuthenticatedRequest( - "/mvno/addSpec/", - request - ); - } + await this.client.makeAuthenticatedRequest( + "/master/addSpec/", + request + ); - this.logger.log(`Successfully updated SIM features for account ${account}`, { features }); + this.logger.log(`Successfully updated SIM features for account ${account}`, { + account, + voiceMailEnabled: features.voiceMailEnabled, + callWaitingEnabled: features.callWaitingEnabled, + internationalRoamingEnabled: features.internationalRoamingEnabled, + networkType: features.networkType, + }); } catch (error) { const message = getErrorMessage(error); this.logger.error(`Failed to update SIM features for account ${account}`, { @@ -328,22 +316,20 @@ export class FreebitOperationsService { */ async cancelSim(account: string, scheduledAt?: string): Promise { try { - const request: FreebitCancelPlanPayload = { - requestDatas: [ - { - kind: "MVNO", - account, - runDate: scheduledAt || this.mapper.formatDateForApi(new Date()), - }, - ], + const request: Omit = { + account, + runTime: scheduledAt }; - await this.client.makeAuthenticatedRequest( - "/mvno/cancelPlan/", + await this.client.makeAuthenticatedRequest( + "/mvno/releasePlan/", request ); - this.logger.log(`Successfully cancelled SIM for account ${account}`); + this.logger.log(`Successfully cancelled SIM for account ${account}`, { + account, + runTime: scheduledAt + }); } catch (error) { const message = getErrorMessage(error); this.logger.error(`Failed to cancel SIM for account ${account}`, { @@ -386,33 +372,31 @@ export class FreebitOperationsService { async reissueEsimProfileEnhanced( account: string, newEid: string, - options: { oldEid?: string; planCode?: string } = {} + options: { oldProductNumber?: string; oldEid?: string; planCode?: string } = {} ): Promise { try { - const request: FreebitEsimReissuePayload = { - requestDatas: [ - { - kind: "MVNO", - account, - newEid, - oldEid: options.oldEid, - planCode: options.planCode, - }, - ], + const request: Omit = { + aladinOperated: "20", + account, + eid: newEid, + addKind: "R", + planCode: options.planCode, }; - await this.client.makeAuthenticatedRequest( - "/mvno/reissueEsim/", + await this.client.makeAuthenticatedRequest( + "/mvno/esim/addAcnt/", request ); - this.logger.log(`Successfully reissued eSIM profile with new EID for account ${account}`, { + this.logger.log(`Successfully reissued eSIM profile via addAcnt for account ${account}`, { + account, newEid, + oldProductNumber: options.oldProductNumber, oldEid: options.oldEid, }); } catch (error) { const message = getErrorMessage(error); - this.logger.error(`Failed to reissue eSIM profile with new EID for account ${account}`, { + this.logger.error(`Failed to reissue eSIM profile via addAcnt for account ${account}`, { account, newEid, error: message, @@ -421,6 +405,75 @@ export class FreebitOperationsService { } } + /** + * Activate new eSIM account using PA05-41 (addAcct) + */ + async activateEsimAccountNew(params: { + account: string; + eid: string; + planCode?: string; + contractLine?: "4G" | "5G"; + aladinOperated?: "10" | "20"; + shipDate?: string; + mnp?: { reserveNumber: string; reserveExpireDate: string }; + identity?: { + firstnameKanji?: string; + lastnameKanji?: string; + firstnameZenKana?: string; + lastnameZenKana?: string; + gender?: string; + birthday?: string; + }; + }): Promise { + const { account, eid, planCode, contractLine, aladinOperated = "10", shipDate, mnp, identity } = params; + + if (!account || !eid) { + throw new BadRequestException("activateEsimAccountNew requires account and eid"); + } + + try { + const payload: FreebitEsimAccountActivationRequest = { + authKey: await this.auth.getAuthKey(), + aladinOperated, + createType: "new", + eid, + account, + simkind: "esim", + planCode, + contractLine, + shipDate, + ...(mnp ? { mnp } : {}), + ...(identity ? identity : {}), + } as FreebitEsimAccountActivationRequest; + + // Use JSON request for PA05-41 + await this.client.makeAuthenticatedJsonRequest< + FreebitEsimAccountActivationResponse, + FreebitEsimAccountActivationRequest + >( + "/mvno/esim/addAcct/", + payload + ); + + this.logger.log("Successfully activated new eSIM account via PA05-41", { + account, + planCode, + contractLine, + scheduled: !!shipDate, + mnp: !!mnp, + }); + } catch (error) { + const message = getErrorMessage(error); + this.logger.error(`Failed to activate new eSIM account ${account}`, { + account, + eid, + planCode, + error: message, + }); + throw new BadRequestException(`Failed to activate new eSIM account: ${message}`); + } + } + /** * Health check - test API connectivity */ 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 f834a43f..0b4fcdf9 100644 --- a/apps/bff/src/integrations/freebit/services/freebit-orchestrator.service.ts +++ b/apps/bff/src/integrations/freebit/services/freebit-orchestrator.service.ts @@ -5,6 +5,7 @@ import type { SimDetails, SimUsage, SimTopUpHistory, + FreebitEsimAddAccountRequest, } from "../interfaces/freebit.types"; @Injectable() @@ -36,7 +37,7 @@ export class FreebitOrchestratorService { async topUpSim( account: string, quotaMb: number, - options: { description?: string } = {} + options: { campaignCode?: string; expiryDate?: string; scheduledAt?: string } = {} ): Promise { const normalizedAccount = this.mapper.normalizeAccount(account); return this.operations.topUpSim(normalizedAccount, quotaMb, options); @@ -110,6 +111,39 @@ export class FreebitOrchestratorService { return this.operations.reissueEsimProfileEnhanced(normalizedAccount, newEid, options); } + /** + * Activate new eSIM account + */ + async activateEsimAccountNew(params: { + account: string; + eid: string; + planCode?: string; + contractLine?: "4G" | "5G"; + aladinOperated?: "10" | "20"; + shipDate?: string; + mnp?: { reserveNumber: string; reserveExpireDate: string }; + identity?: { + firstnameKanji?: string; + lastnameKanji?: string; + firstnameZenKana?: string; + lastnameZenKana?: string; + gender?: string; + birthday?: string; + }; + }): Promise { + const normalizedAccount = this.mapper.normalizeAccount(params.account); + return this.operations.activateEsimAccountNew({ + account: normalizedAccount, + eid: params.eid, + planCode: params.planCode, + contractLine: params.contractLine, + aladinOperated: params.aladinOperated, + shipDate: params.shipDate, + mnp: params.mnp, + identity: params.identity, + }); + } + /** * Health check */ diff --git a/apps/bff/src/integrations/whmcs/transformers/utils/status-normalizer.ts b/apps/bff/src/integrations/whmcs/transformers/utils/status-normalizer.ts index 510e0c85..73401f18 100644 --- a/apps/bff/src/integrations/whmcs/transformers/utils/status-normalizer.ts +++ b/apps/bff/src/integrations/whmcs/transformers/utils/status-normalizer.ts @@ -50,11 +50,12 @@ export class StatusNormalizer { monthly: "Monthly", quarterly: "Quarterly", semiannually: "Semi-Annually", + "semi-annually": "Semi-Annually", annually: "Annually", biennially: "Biennially", triennially: "Triennially", - onetime: "One Time", - "one time": "One Time", + onetime: "One-time", + "one time": "One-time", free: "Free", }; @@ -65,7 +66,7 @@ export class StatusNormalizer { * Check if billing cycle represents a one-time payment */ static isOneTimeBilling(cycle: string): boolean { - const oneTimeCycles = ["onetime", "one time", "free"]; + const oneTimeCycles = ["onetime", "one time", "one-time", "free"]; return oneTimeCycles.includes(cycle?.toLowerCase()); } diff --git a/apps/bff/src/modules/orders/services/sim-fulfillment.service.ts b/apps/bff/src/modules/orders/services/sim-fulfillment.service.ts index 667c689f..88647ccc 100644 --- a/apps/bff/src/modules/orders/services/sim-fulfillment.service.ts +++ b/apps/bff/src/modules/orders/services/sim-fulfillment.service.ts @@ -54,15 +54,29 @@ export class SimFulfillmentService { throw new Error("Phone number is required for SIM activation"); } - await this.activateSim({ - account: phoneNumber, - eid, - planSku, - simType, - activationType, - scheduledAt, - mnp, - }); + if (simType === "eSIM") { + if (!eid) { + throw new Error("EID is required for eSIM activation"); + } + await this.activateSim({ + account: phoneNumber, + eid, + planSku, + simType: "eSIM", + activationType, + scheduledAt, + mnp, + }); + } else { + await this.activateSim({ + account: phoneNumber, + planSku, + simType: "Physical SIM", + activationType, + scheduledAt, + mnp, + }); + } this.logger.log("SIM fulfillment completed successfully", { orderId: orderDetails.id, diff --git a/apps/bff/src/modules/subscriptions/sim-order-activation.service.ts b/apps/bff/src/modules/subscriptions/sim-order-activation.service.ts index 4ec279d6..24ba9498 100644 --- a/apps/bff/src/modules/subscriptions/sim-order-activation.service.ts +++ b/apps/bff/src/modules/subscriptions/sim-order-activation.service.ts @@ -87,33 +87,15 @@ export class SimOrderActivationService { await this.freebit.activateEsimAccountNew({ account: req.msisdn, eid: req.eid!, - planSku: req.planSku, + planCode: req.planSku, contractLine: "5G", shipDate: req.activationType === "Scheduled" ? req.scheduledAt : undefined, mnp: req.mnp ? { - reservationNumber: req.mnp.reserveNumber, - expiryDate: req.mnp.reserveExpireDate, - phoneNumber: req.mnp.account || "", - mvnoAccountNumber: "", - portingLastName: "", - portingFirstName: "", - portingLastNameKatakana: "", - portingFirstNameKatakana: "", - portingGender: "" as const, - portingDateOfBirth: "" + reserveNumber: req.mnp.reserveNumber || "", + reserveExpireDate: req.mnp.reserveExpireDate || "" } - : undefined, - identity: req.mnp - ? { - firstnameKanji: req.mnp.firstnameKanji, - lastnameKanji: req.mnp.lastnameKanji, - firstnameZenKana: req.mnp.firstnameZenKana, - lastnameZenKana: req.mnp.lastnameZenKana, - gender: req.mnp.gender, - birthday: req.mnp.birthday, - } - : undefined, + : undefined }); } else { this.logger.warn("Physical SIM activation path is not implemented; skipping Freebit call", { diff --git a/apps/portal/package.json b/apps/portal/package.json index 55472d5b..26f640df 100644 --- a/apps/portal/package.json +++ b/apps/portal/package.json @@ -33,7 +33,7 @@ "tailwind-merge": "^3.3.1", "tw-animate-css": "^1.3.7", "world-countries": "^5.1.0", - "zod": "^4.0.17", + "zod": "^4.1.9", "zustand": "^5.0.8" }, "devDependencies": { diff --git a/package.json b/package.json index a98980f9..e4d5289f 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,6 @@ "zod": "^4.1.9" }, "dependencies": { - "@sendgrid/mail": "^8.1.5", - "csurf": "^1.11.0" + "@sendgrid/mail": "^8.1.5" } } diff --git a/packages/domain/src/validation/shared/entities.ts b/packages/domain/src/validation/shared/entities.ts index bcbca52e..fc2c4fe3 100644 --- a/packages/domain/src/validation/shared/entities.ts +++ b/packages/domain/src/validation/shared/entities.ts @@ -71,6 +71,7 @@ const subscriptionCycleSchema = z.enum([ "Biennially", "Triennially", "One-time", + "Free", ]); // ===================================================== diff --git a/packages/domain/src/validation/shared/primitives.ts b/packages/domain/src/validation/shared/primitives.ts index 0dfe3c69..f2de834e 100644 --- a/packages/domain/src/validation/shared/primitives.ts +++ b/packages/domain/src/validation/shared/primitives.ts @@ -98,7 +98,16 @@ export const categoryEnum = z.enum(["technical", "billing", "account", "general" // Billing cycle enums export const billingCycleEnum = z.enum(["Monthly", "Quarterly", "Annually", "Onetime", "Free"]); -export const subscriptionBillingCycleEnum = z.enum(["Monthly", "Quarterly", "Annually", "Biennially", "Triennially"]); +export const subscriptionBillingCycleEnum = z.enum([ + "Monthly", + "Quarterly", + "Semi-Annually", + "Annually", + "Biennially", + "Triennially", + "One-time", + "Free" +]); // ===================================================== // TYPE EXPORTS diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3f869e99..91287763 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,9 +11,6 @@ importers: '@sendgrid/mail': specifier: ^8.1.5 version: 8.1.5 - csurf: - specifier: ^1.11.0 - version: 1.11.0 devDependencies: '@eslint/eslintrc': specifier: ^3.3.1 @@ -114,9 +111,6 @@ importers: cookie-parser: specifier: ^1.4.7 version: 1.4.7 - csurf: - specifier: ^1.11.0 - version: 1.11.0 express: specifier: ^4.21.2 version: 4.21.2 @@ -137,7 +131,7 @@ importers: version: 4.4.0(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(pino-http@10.5.0)(pino@9.9.5)(rxjs@7.8.2) nestjs-zod: specifier: ^5.0.1 - version: 5.0.1(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/swagger@11.2.0(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6)(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2))(rxjs@7.8.2)(zod@4.1.5) + version: 5.0.1(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/swagger@11.2.0(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6)(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2))(rxjs@7.8.2)(zod@4.1.9) passport: specifier: ^0.7.0 version: 0.7.0 @@ -172,8 +166,8 @@ importers: specifier: ^11.1.0 version: 11.1.0 zod: - specifier: ^4.0.17 - version: 4.1.5 + specifier: ^4.1.9 + version: 4.1.9 devDependencies: '@nestjs/cli': specifier: ^11.0.10 @@ -302,8 +296,8 @@ importers: specifier: ^5.1.0 version: 5.1.0 zod: - specifier: ^4.0.17 - version: 4.1.5 + specifier: ^4.1.9 + version: 4.1.9 zustand: specifier: ^5.0.8 version: 5.0.8(@types/react@19.1.12)(react@19.1.1) @@ -2540,10 +2534,6 @@ packages: resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} engines: {node: '>=6.6.0'} - cookie@0.4.0: - resolution: {integrity: sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==} - engines: {node: '>= 0.6'} - cookie@0.7.1: resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==} engines: {node: '>= 0.6'} @@ -2592,18 +2582,9 @@ packages: resolution: {integrity: sha512-D3WAbvvgUVIqSxUfdvLeGjuotsB32bvfVPd+AaaTWMtyUeC9zgCnw5xs94no89yFLVsafvY9dMZEhTwsY/ZecA==} engines: {node: '>=0.6.0'} - csrf@3.1.0: - resolution: {integrity: sha512-uTqEnCvWRk042asU6JtapDTcJeeailFy4ydOQS28bj1hcLnYRiqi8SsD2jS412AY1I/4qdOwWZun774iqywf9w==} - engines: {node: '>= 0.8'} - csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} - csurf@1.11.0: - resolution: {integrity: sha512-UCtehyEExKTxgiu8UHdGvHj4tnpE/Qctue03Giq5gPgMQ9cg/ciod5blZQ5a4uCEenNQjxyGuzygLdKUmee/bQ==} - engines: {node: '>= 0.8.0'} - deprecated: This package is archived and no longer maintained. For support, visit https://github.com/expressjs/express/discussions - csv-parse@5.6.0: resolution: {integrity: sha512-l3nz3euub2QMg5ouu5U09Ew9Wf6/wQ8I++ch1loQ0ljmzhmfZYrH9fflS22i/PQEvsPvxCwxgz5q7UB8K1JO4Q==} @@ -2697,10 +2678,6 @@ packages: resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} engines: {node: '>=0.10'} - depd@1.1.2: - resolution: {integrity: sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==} - engines: {node: '>= 0.6'} - depd@2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} @@ -3325,10 +3302,6 @@ packages: html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} - http-errors@1.7.3: - resolution: {integrity: sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==} - engines: {node: '>= 0.6'} - http-errors@2.0.0: resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} engines: {node: '>= 0.8'} @@ -4519,10 +4492,6 @@ packages: quick-format-unescaped@4.0.4: resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} - random-bytes@1.0.0: - resolution: {integrity: sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==} - engines: {node: '>= 0.8'} - randombytes@2.1.0: resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} @@ -4637,9 +4606,6 @@ packages: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - rndm@1.2.0: - resolution: {integrity: sha512-fJhQQI5tLrQvYIYFpOnFinzv9dwmR7hRnUz1XqP3OJ1jIweTNOd6aTO4jwQSgcBSFUB+/KHJxuGneime+FdzOw==} - router@2.2.0: resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} engines: {node: '>= 18'} @@ -4743,9 +4709,6 @@ packages: resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} engines: {node: '>= 0.4'} - setprototypeof@1.1.1: - resolution: {integrity: sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==} - setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} @@ -4833,10 +4796,6 @@ packages: standard-as-callback@2.1.0: resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} - statuses@1.5.0: - resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==} - engines: {node: '>= 0.6'} - statuses@2.0.1: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'} @@ -5033,10 +4992,6 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} - toidentifier@1.0.0: - resolution: {integrity: sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==} - engines: {node: '>=0.6'} - toidentifier@1.0.1: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} @@ -5117,10 +5072,6 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - tsscmp@1.0.6: - resolution: {integrity: sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==} - engines: {node: '>=0.6.x'} - tsx@4.20.5: resolution: {integrity: sha512-+wKjMNU9w/EaQayHXb7WA7ZaHY6hN8WgfvHNQ3t1PnU91/7O8TcTnIhCDYTZwnt8JsO9IBqZ30Ln1r7pPF52Aw==} engines: {node: '>=18.0.0'} @@ -5197,10 +5148,6 @@ packages: engines: {node: '>=0.8.0'} hasBin: true - uid-safe@2.1.5: - resolution: {integrity: sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==} - engines: {node: '>= 0.8'} - uid@2.0.2: resolution: {integrity: sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==} engines: {node: '>=8'} @@ -5408,9 +5355,6 @@ packages: resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==} engines: {node: '>=18'} - zod@4.1.5: - resolution: {integrity: sha512-rcUUZqlLJgBC33IT3PNMgsCq6TzLQEG/Ei/KTCU0PedSWRMAXoOUN+4t/0H+Q8bdnLPdqUYnvboJT0bn/229qg==} - zod@4.1.9: resolution: {integrity: sha512-HI32jTq0AUAC125z30E8bQNz0RQ+9Uc+4J7V97gLYjZVKRjeydPgGt6dvQzFrav7MYOUGFqqOGiHpA/fdbd0cQ==} @@ -7717,8 +7661,6 @@ snapshots: cookie-signature@1.2.2: {} - cookie@0.4.0: {} - cookie@0.7.1: {} cookie@0.7.2: {} @@ -7761,21 +7703,8 @@ snapshots: dependencies: sequin: 0.1.1 - csrf@3.1.0: - dependencies: - rndm: 1.2.0 - tsscmp: 1.0.6 - uid-safe: 2.1.5 - csstype@3.1.3: {} - csurf@1.11.0: - dependencies: - cookie: 0.4.0 - cookie-signature: 1.0.6 - csrf: 3.1.0 - http-errors: 1.7.3 - csv-parse@5.6.0: {} csv-stringify@6.6.0: {} @@ -7846,8 +7775,6 @@ snapshots: denque@2.1.0: {} - depd@1.1.2: {} - depd@2.0.0: {} destr@2.0.5: {} @@ -8711,14 +8638,6 @@ snapshots: html-escaper@2.0.2: {} - http-errors@1.7.3: - dependencies: - depd: 1.1.2 - inherits: 2.0.4 - setprototypeof: 1.1.1 - statuses: 1.5.0 - toidentifier: 1.0.0 - http-errors@2.0.0: dependencies: depd: 2.0.0 @@ -9695,12 +9614,12 @@ snapshots: pino-http: 10.5.0 rxjs: 7.8.2 - nestjs-zod@5.0.1(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/swagger@11.2.0(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6)(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2))(rxjs@7.8.2)(zod@4.1.5): + nestjs-zod@5.0.1(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/swagger@11.2.0(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6)(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2))(rxjs@7.8.2)(zod@4.1.9): dependencies: '@nestjs/common': 11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) deepmerge: 4.3.1 rxjs: 7.8.2 - zod: 4.1.5 + zod: 4.1.9 optionalDependencies: '@nestjs/swagger': 11.2.0(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6)(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2) @@ -10097,8 +10016,6 @@ snapshots: quick-format-unescaped@4.0.4: {} - random-bytes@1.0.0: {} - randombytes@2.1.0: dependencies: safe-buffer: 5.2.1 @@ -10212,8 +10129,6 @@ snapshots: reusify@1.1.0: {} - rndm@1.2.0: {} - router@2.2.0: dependencies: debug: 4.4.1 @@ -10378,8 +10293,6 @@ snapshots: es-errors: 1.3.0 es-object-atoms: 1.1.1 - setprototypeof@1.1.1: {} - setprototypeof@1.2.0: {} sharp@0.34.3: @@ -10491,8 +10404,6 @@ snapshots: standard-as-callback@2.1.0: {} - statuses@1.5.0: {} - statuses@2.0.1: {} statuses@2.0.2: {} @@ -10705,8 +10616,6 @@ snapshots: dependencies: is-number: 7.0.0 - toidentifier@1.0.0: {} - toidentifier@1.0.1: {} token-types@6.1.1: @@ -10787,8 +10696,6 @@ snapshots: tslib@2.8.1: {} - tsscmp@1.0.6: {} - tsx@4.20.5: dependencies: esbuild: 0.25.10 @@ -10876,10 +10783,6 @@ snapshots: uglify-js@3.19.3: optional: true - uid-safe@2.1.5: - dependencies: - random-bytes: 1.0.0 - uid@2.0.2: dependencies: '@lukeed/csprng': 1.1.0 @@ -11130,8 +11033,6 @@ snapshots: yoctocolors-cjs@2.1.3: {} - zod@4.1.5: {} - zod@4.1.9: {} zustand@5.0.8(@types/react@19.1.12)(react@19.1.1):