2025-09-11 14:52:26 +09:00
|
|
|
import { Injectable, Inject } from "@nestjs/common";
|
|
|
|
|
import { Logger } from "nestjs-pino";
|
2025-09-25 16:38:21 +09:00
|
|
|
import { FreebitOrchestratorService } from "@bff/integrations/freebit/services/freebit-orchestrator.service";
|
2025-09-25 15:11:28 +09:00
|
|
|
import type { FulfillmentOrderDetails, FulfillmentOrderItem } from "../types/fulfillment.types";
|
|
|
|
|
import { getErrorMessage } from "@bff/core/utils/error.util";
|
2025-09-11 14:52:26 +09:00
|
|
|
|
|
|
|
|
export interface SimFulfillmentRequest {
|
2025-09-25 15:11:28 +09:00
|
|
|
orderDetails: FulfillmentOrderDetails;
|
2025-09-11 14:52:26 +09:00
|
|
|
configurations: Record<string, unknown>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Injectable()
|
|
|
|
|
export class SimFulfillmentService {
|
|
|
|
|
constructor(
|
2025-09-25 16:38:21 +09:00
|
|
|
private readonly freebit: FreebitOrchestratorService,
|
2025-09-11 14:52:26 +09:00
|
|
|
@Inject(Logger) private readonly logger: Logger
|
|
|
|
|
) {}
|
|
|
|
|
|
|
|
|
|
async fulfillSimOrder(request: SimFulfillmentRequest): Promise<void> {
|
|
|
|
|
const { orderDetails, configurations } = request;
|
2025-09-17 18:43:43 +09:00
|
|
|
|
2025-09-11 14:52:26 +09:00
|
|
|
this.logger.log("Starting SIM fulfillment", {
|
|
|
|
|
orderId: orderDetails.id,
|
|
|
|
|
orderType: orderDetails.orderType,
|
|
|
|
|
});
|
|
|
|
|
|
2025-09-25 15:11:28 +09:00
|
|
|
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);
|
2025-09-11 14:52:26 +09:00
|
|
|
const mnp = this.extractMnpConfig(configurations);
|
|
|
|
|
|
2025-09-17 18:43:43 +09:00
|
|
|
const simPlanItem = orderDetails.items.find(
|
2025-09-25 15:11:28 +09:00
|
|
|
(item: FulfillmentOrderItem) =>
|
|
|
|
|
item.product?.itemClass === "Plan" || item.product?.sku?.toLowerCase().includes("sim")
|
2025-09-11 14:52:26 +09:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (!simPlanItem) {
|
|
|
|
|
throw new Error("No SIM plan found in order items");
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-25 15:11:28 +09:00
|
|
|
const planSku = simPlanItem.product?.sku;
|
2025-09-11 14:52:26 +09:00
|
|
|
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");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await this.activateSim({
|
|
|
|
|
account: phoneNumber,
|
|
|
|
|
eid,
|
|
|
|
|
planSku,
|
2025-09-25 15:11:28 +09:00
|
|
|
simType,
|
|
|
|
|
activationType,
|
2025-09-11 14:52:26 +09:00
|
|
|
scheduledAt,
|
|
|
|
|
mnp,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this.logger.log("SIM fulfillment completed successfully", {
|
|
|
|
|
orderId: orderDetails.id,
|
|
|
|
|
account: phoneNumber,
|
|
|
|
|
planSku,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-25 15:11:28 +09:00
|
|
|
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;
|
2025-09-11 14:52:26 +09:00
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
if (simType === "eSIM") {
|
2025-09-25 15:11:28 +09:00
|
|
|
const { eid } = params;
|
2025-09-11 14:52:26 +09:00
|
|
|
await this.freebit.activateEsimAccountNew({
|
|
|
|
|
account,
|
2025-09-25 15:11:28 +09:00
|
|
|
eid,
|
2025-09-11 14:52:26 +09:00
|
|
|
planCode: planSku,
|
|
|
|
|
contractLine: "5G",
|
|
|
|
|
shipDate: activationType === "Scheduled" ? scheduledAt : undefined,
|
2025-09-11 16:21:54 +09:00
|
|
|
mnp:
|
|
|
|
|
mnp && mnp.reserveNumber && mnp.reserveExpireDate
|
|
|
|
|
? {
|
|
|
|
|
reserveNumber: mnp.reserveNumber,
|
|
|
|
|
reserveExpireDate: mnp.reserveExpireDate,
|
|
|
|
|
}
|
|
|
|
|
: undefined,
|
2025-09-17 18:43:43 +09:00
|
|
|
identity: mnp
|
|
|
|
|
? {
|
|
|
|
|
firstnameKanji: mnp.firstnameKanji,
|
|
|
|
|
lastnameKanji: mnp.lastnameKanji,
|
|
|
|
|
firstnameZenKana: mnp.firstnameZenKana,
|
|
|
|
|
lastnameZenKana: mnp.lastnameZenKana,
|
|
|
|
|
gender: mnp.gender,
|
|
|
|
|
birthday: mnp.birthday,
|
|
|
|
|
}
|
|
|
|
|
: undefined,
|
2025-09-11 14:52:26 +09:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this.logger.log("eSIM activated successfully", {
|
|
|
|
|
account,
|
|
|
|
|
planSku,
|
|
|
|
|
scheduled: activationType === "Scheduled",
|
|
|
|
|
});
|
|
|
|
|
} else {
|
2025-09-25 15:11:28 +09:00
|
|
|
await this.freebit.topUpSim(account, 0, {
|
|
|
|
|
scheduledAt: activationType === "Scheduled" ? scheduledAt : undefined,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this.logger.log("Physical SIM activation scheduled", {
|
2025-09-11 14:52:26 +09:00
|
|
|
account,
|
2025-09-25 15:11:28 +09:00
|
|
|
planSku,
|
2025-09-11 14:52:26 +09:00
|
|
|
});
|
|
|
|
|
}
|
2025-09-25 15:11:28 +09:00
|
|
|
} catch (error: unknown) {
|
2025-09-11 14:52:26 +09:00
|
|
|
this.logger.error("SIM activation failed", {
|
|
|
|
|
account,
|
|
|
|
|
planSku,
|
2025-09-25 15:11:28 +09:00
|
|
|
error: getErrorMessage(error),
|
2025-09-11 14:52:26 +09:00
|
|
|
});
|
|
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-25 15:11:28 +09:00
|
|
|
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
|
|
|
|
|
) {
|
2025-09-11 14:52:26 +09:00
|
|
|
return undefined;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
2025-09-25 15:11:28 +09:00
|
|
|
reserveNumber,
|
|
|
|
|
reserveExpireDate,
|
|
|
|
|
account,
|
|
|
|
|
firstnameKanji,
|
|
|
|
|
lastnameKanji,
|
|
|
|
|
firstnameZenKana,
|
|
|
|
|
lastnameZenKana,
|
|
|
|
|
gender,
|
|
|
|
|
birthday,
|
2025-09-11 14:52:26 +09:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|