207 lines
6.3 KiB
TypeScript
207 lines
6.3 KiB
TypeScript
|
|
import { Injectable, Inject } from "@nestjs/common";
|
||
|
|
import { Logger } from "nestjs-pino";
|
||
|
|
import { FreebititService } from "../../vendors/freebit/freebit.service";
|
||
|
|
import { OrderDetailsDto } from "../types/order-details.dto";
|
||
|
|
import { getSalesforceFieldMap } from "../../common/config/field-map";
|
||
|
|
|
||
|
|
export interface SimFulfillmentRequest {
|
||
|
|
orderDetails: OrderDetailsDto;
|
||
|
|
configurations: Record<string, unknown>;
|
||
|
|
}
|
||
|
|
|
||
|
|
@Injectable()
|
||
|
|
export class SimFulfillmentService {
|
||
|
|
constructor(
|
||
|
|
private readonly freebit: FreebititService,
|
||
|
|
@Inject(Logger) private readonly logger: Logger
|
||
|
|
) {}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Handle SIM-specific fulfillment after WHMCs provisioning
|
||
|
|
*/
|
||
|
|
async fulfillSimOrder(request: SimFulfillmentRequest): Promise<void> {
|
||
|
|
const { orderDetails, configurations } = request;
|
||
|
|
|
||
|
|
this.logger.log("Starting SIM fulfillment", {
|
||
|
|
orderId: orderDetails.id,
|
||
|
|
orderType: orderDetails.orderType,
|
||
|
|
});
|
||
|
|
|
||
|
|
// Extract SIM-specific configurations
|
||
|
|
const simType = configurations.simType as "eSIM" | "Physical SIM" | undefined;
|
||
|
|
const eid = configurations.eid as string | undefined;
|
||
|
|
const activationType = configurations.activationType as "Immediate" | "Scheduled" | undefined;
|
||
|
|
const scheduledAt = configurations.scheduledAt as string | undefined;
|
||
|
|
const phoneNumber = configurations.mnpPhone as string | undefined;
|
||
|
|
const mnp = this.extractMnpConfig(configurations);
|
||
|
|
const addons = this.extractAddonConfig(configurations);
|
||
|
|
|
||
|
|
// Find the main SIM plan from order items
|
||
|
|
const simPlanItem = orderDetails.items.find(item =>
|
||
|
|
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");
|
||
|
|
}
|
||
|
|
|
||
|
|
// Validate required fields
|
||
|
|
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");
|
||
|
|
}
|
||
|
|
|
||
|
|
// Perform Freebit activation
|
||
|
|
await this.activateSim({
|
||
|
|
account: phoneNumber,
|
||
|
|
eid,
|
||
|
|
planSku,
|
||
|
|
simType: simType || "eSIM",
|
||
|
|
activationType: activationType || "Immediate",
|
||
|
|
scheduledAt,
|
||
|
|
mnp,
|
||
|
|
addons,
|
||
|
|
});
|
||
|
|
|
||
|
|
this.logger.log("SIM fulfillment completed successfully", {
|
||
|
|
orderId: orderDetails.id,
|
||
|
|
account: phoneNumber,
|
||
|
|
planSku,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Activate SIM via Freebit API
|
||
|
|
*/
|
||
|
|
private async activateSim(params: {
|
||
|
|
account: string;
|
||
|
|
eid?: string;
|
||
|
|
planSku: string;
|
||
|
|
simType: "eSIM" | "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;
|
||
|
|
};
|
||
|
|
addons?: {
|
||
|
|
voiceMail?: boolean;
|
||
|
|
callWaiting?: boolean;
|
||
|
|
};
|
||
|
|
}): Promise<void> {
|
||
|
|
const {
|
||
|
|
account,
|
||
|
|
eid,
|
||
|
|
planSku,
|
||
|
|
simType,
|
||
|
|
activationType,
|
||
|
|
scheduledAt,
|
||
|
|
mnp,
|
||
|
|
addons,
|
||
|
|
} = params;
|
||
|
|
|
||
|
|
try {
|
||
|
|
// Activate eSIM if applicable
|
||
|
|
if (simType === "eSIM") {
|
||
|
|
await this.freebit.activateEsimAccountNew({
|
||
|
|
account,
|
||
|
|
eid: eid!,
|
||
|
|
planCode: planSku,
|
||
|
|
contractLine: "5G",
|
||
|
|
shipDate: activationType === "Scheduled" ? scheduledAt : undefined,
|
||
|
|
mnp: mnp ? {
|
||
|
|
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 {
|
||
|
|
this.logger.warn("Physical SIM activation path is not implemented; skipping Freebit call", {
|
||
|
|
account,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// Apply add-ons (voice options) if selected
|
||
|
|
if (addons && (addons.voiceMail || addons.callWaiting)) {
|
||
|
|
await this.freebit.updateSimFeatures(account, {
|
||
|
|
voiceMailEnabled: !!addons.voiceMail,
|
||
|
|
callWaitingEnabled: !!addons.callWaiting,
|
||
|
|
});
|
||
|
|
|
||
|
|
this.logger.log("SIM add-ons applied", {
|
||
|
|
account,
|
||
|
|
voiceMail: addons.voiceMail,
|
||
|
|
callWaiting: addons.callWaiting,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
this.logger.error("SIM activation failed", {
|
||
|
|
account,
|
||
|
|
planSku,
|
||
|
|
error: error instanceof Error ? error.message : String(error),
|
||
|
|
});
|
||
|
|
throw error;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Extract MNP configuration from order configurations
|
||
|
|
*/
|
||
|
|
private extractMnpConfig(configurations: Record<string, unknown>) {
|
||
|
|
const isMnp = configurations.isMnp;
|
||
|
|
if (!isMnp || isMnp !== "true") {
|
||
|
|
return undefined;
|
||
|
|
}
|
||
|
|
|
||
|
|
return {
|
||
|
|
reserveNumber: configurations.mnpNumber as string | undefined,
|
||
|
|
reserveExpireDate: configurations.mnpExpiry as string | undefined,
|
||
|
|
account: configurations.mvnoAccountNumber as string | undefined,
|
||
|
|
firstnameKanji: configurations.portingFirstName as string | undefined,
|
||
|
|
lastnameKanji: configurations.portingLastName as string | undefined,
|
||
|
|
firstnameZenKana: configurations.portingFirstNameKatakana as string | undefined,
|
||
|
|
lastnameZenKana: configurations.portingLastNameKatakana as string | undefined,
|
||
|
|
gender: configurations.portingGender as string | undefined,
|
||
|
|
birthday: configurations.portingDateOfBirth as string | undefined,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Extract addon configuration from order configurations
|
||
|
|
*/
|
||
|
|
private extractAddonConfig(configurations: Record<string, unknown>) {
|
||
|
|
// Check if voice addons are present in the configurations
|
||
|
|
// This would need to be determined based on the order items or configurations
|
||
|
|
// For now, return undefined - this can be enhanced based on actual addon detection logic
|
||
|
|
return undefined;
|
||
|
|
}
|
||
|
|
}
|