Assist_Design/apps/bff/src/modules/orders/services/sim-fulfillment.service.ts

243 lines
7.3 KiB
TypeScript
Raw Normal View History

import { Injectable, Inject } from "@nestjs/common";
import { Logger } from "nestjs-pino";
import { FreebitOrchestratorService } from "@bff/integrations/freebit/services/freebit-orchestrator.service";
import type { OrderDetails, OrderItemDetails } from "@customer-portal/domain/orders";
import { getErrorMessage } from "@bff/core/utils/error.util";
export interface SimFulfillmentRequest {
orderDetails: OrderDetails;
configurations: Record<string, unknown>;
}
@Injectable()
export class SimFulfillmentService {
constructor(
private readonly freebit: FreebitOrchestratorService,
@Inject(Logger) private readonly logger: Logger
) {}
async fulfillSimOrder(request: SimFulfillmentRequest): Promise<void> {
const { orderDetails, configurations } = request;
this.logger.log("Starting SIM fulfillment", {
orderId: orderDetails.id,
orderType: orderDetails.orderType,
});
const simType = this.readEnum(configurations.simType, ["eSIM", "Physical SIM"]) ?? "eSIM";
const eid = this.readString(configurations.eid);
const activationType =
this.readEnum(configurations.activationType, ["Immediate", "Scheduled"]) ?? "Immediate";
const scheduledAt = this.readString(configurations.scheduledAt);
const phoneNumber = this.readString(configurations.mnpPhone);
const mnp = this.extractMnpConfig(configurations);
const simPlanItem = orderDetails.items.find(
(item: OrderItemDetails) =>
item.product?.itemClass === "Plan" || item.product?.sku?.toLowerCase().includes("sim")
);
if (!simPlanItem) {
throw new Error("No SIM plan found in order items");
}
const planSku = simPlanItem.product?.sku;
if (!planSku) {
throw new Error("SIM plan SKU not found");
}
if (simType === "eSIM" && (!eid || eid.length < 15)) {
throw new Error("EID is required for eSIM and must be valid");
}
if (!phoneNumber) {
throw new Error("Phone number is required for SIM activation");
}
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,
account: phoneNumber,
planSku,
});
}
private async activateSim(
params:
| {
account: string;
eid: string;
planSku: string;
simType: "eSIM";
activationType: "Immediate" | "Scheduled";
scheduledAt?: string;
mnp?: {
reserveNumber?: string;
reserveExpireDate?: string;
account?: string;
firstnameKanji?: string;
lastnameKanji?: string;
firstnameZenKana?: string;
lastnameZenKana?: string;
gender?: string;
birthday?: string;
};
}
| {
account: string;
eid?: string;
planSku: string;
simType: "Physical SIM";
activationType: "Immediate" | "Scheduled";
scheduledAt?: string;
mnp?: {
reserveNumber?: string;
reserveExpireDate?: string;
account?: string;
firstnameKanji?: string;
lastnameKanji?: string;
firstnameZenKana?: string;
lastnameZenKana?: string;
gender?: string;
birthday?: string;
};
}
): Promise<void> {
const { account, planSku, simType, activationType, scheduledAt, mnp } = params;
try {
if (simType === "eSIM") {
const { eid } = params;
await this.freebit.activateEsimAccountNew({
account,
eid,
planCode: planSku,
contractLine: "5G",
shipDate: activationType === "Scheduled" ? scheduledAt : undefined,
mnp:
mnp && mnp.reserveNumber && mnp.reserveExpireDate
? {
reserveNumber: mnp.reserveNumber,
reserveExpireDate: mnp.reserveExpireDate,
}
: undefined,
identity: mnp
? {
firstnameKanji: mnp.firstnameKanji,
lastnameKanji: mnp.lastnameKanji,
firstnameZenKana: mnp.firstnameZenKana,
lastnameZenKana: mnp.lastnameZenKana,
gender: mnp.gender,
birthday: mnp.birthday,
}
: undefined,
});
this.logger.log("eSIM activated successfully", {
account,
planSku,
scheduled: activationType === "Scheduled",
});
} else {
await this.freebit.topUpSim(account, 0, {
scheduledAt: activationType === "Scheduled" ? scheduledAt : undefined,
});
this.logger.log("Physical SIM activation scheduled", {
account,
planSku,
});
}
} catch (error: unknown) {
this.logger.error("SIM activation failed", {
account,
planSku,
error: getErrorMessage(error),
});
throw error;
}
}
private readString(value: unknown): string | undefined {
return typeof value === "string" ? value : undefined;
}
private readEnum<T extends string>(value: unknown, allowed: readonly T[]): T | undefined {
return typeof value === "string" && allowed.includes(value as T) ? (value as T) : undefined;
}
private extractMnpConfig(config: Record<string, unknown>) {
const nested = config.mnp;
const source =
nested && typeof nested === "object" ? (nested as Record<string, unknown>) : config;
const isMnpFlag = this.readString(source.isMnp ?? config.isMnp);
if (isMnpFlag && isMnpFlag !== "true") {
return undefined;
}
const reserveNumber = this.readString(source.mnpNumber ?? source.reserveNumber);
const reserveExpireDate = this.readString(source.mnpExpiry ?? source.reserveExpireDate);
const account = this.readString(source.mvnoAccountNumber ?? source.account);
const firstnameKanji = this.readString(source.portingFirstName ?? source.firstnameKanji);
const lastnameKanji = this.readString(source.portingLastName ?? source.lastnameKanji);
const firstnameZenKana = this.readString(
source.portingFirstNameKatakana ?? source.firstnameZenKana
);
const lastnameZenKana = this.readString(
source.portingLastNameKatakana ?? source.lastnameZenKana
);
const gender = this.readString(source.portingGender ?? source.gender);
const birthday = this.readString(source.portingDateOfBirth ?? source.birthday);
if (
!reserveNumber &&
!reserveExpireDate &&
!account &&
!firstnameKanji &&
!lastnameKanji &&
!firstnameZenKana &&
!lastnameZenKana &&
!gender &&
!birthday
) {
return undefined;
}
return {
reserveNumber,
reserveExpireDate,
account,
firstnameKanji,
lastnameKanji,
firstnameZenKana,
lastnameZenKana,
gender,
birthday,
};
}
}