- Removed deprecated files and components from the BFF application, including various auth and catalog services, enhancing code clarity. - Updated package.json scripts for better organization and streamlined development processes. - Refactored portal components to improve structure and maintainability, including the removal of unused files and components. - Enhanced type definitions and imports across the application for consistency and clarity.
172 lines
6.4 KiB
TypeScript
172 lines
6.4 KiB
TypeScript
import { Injectable, BadRequestException, Inject } from "@nestjs/common";
|
|
import { Logger } from "nestjs-pino";
|
|
import { FreebititService } from "@bff/integrations/freebit/freebit.service";
|
|
import { WhmcsService } from "@bff/integrations/whmcs/whmcs.service";
|
|
import { MappingsService } from "@bff/modules/id-mappings/mappings.service";
|
|
import { getErrorMessage } from "@bff/core/utils/error.util";
|
|
|
|
export interface SimOrderActivationRequest {
|
|
planSku: string;
|
|
simType: "eSIM" | "Physical SIM";
|
|
eid?: string;
|
|
activationType: "Immediate" | "Scheduled";
|
|
scheduledAt?: string; // YYYYMMDD
|
|
addons?: { voiceMail?: boolean; callWaiting?: boolean };
|
|
mnp?: {
|
|
reserveNumber: string;
|
|
reserveExpireDate: string; // YYYYMMDD
|
|
account?: string; // phone to port
|
|
firstnameKanji?: string;
|
|
lastnameKanji?: string;
|
|
firstnameZenKana?: string;
|
|
lastnameZenKana?: string;
|
|
gender?: string;
|
|
birthday?: string; // YYYYMMDD
|
|
};
|
|
msisdn: string; // phone number for the new/ported account
|
|
oneTimeAmountJpy: number; // Activation fee charged immediately
|
|
monthlyAmountJpy: number; // Monthly subscription fee
|
|
}
|
|
|
|
@Injectable()
|
|
export class SimOrderActivationService {
|
|
constructor(
|
|
private readonly freebit: FreebititService,
|
|
private readonly whmcs: WhmcsService,
|
|
private readonly mappings: MappingsService,
|
|
@Inject(Logger) private readonly logger: Logger
|
|
) {}
|
|
|
|
async activate(
|
|
userId: string,
|
|
req: SimOrderActivationRequest
|
|
): Promise<{ success: boolean; invoiceId: number; transactionId?: string }> {
|
|
if (req.simType === "eSIM" && (!req.eid || req.eid.length < 15)) {
|
|
throw new BadRequestException("EID is required for eSIM and must be valid");
|
|
}
|
|
if (!req.msisdn || req.msisdn.trim() === "") {
|
|
throw new BadRequestException("Phone number (msisdn) is required for SIM activation");
|
|
}
|
|
if (!/^\d{8}$/.test(req.scheduledAt || "") && req.activationType === "Scheduled") {
|
|
throw new BadRequestException("scheduledAt must be YYYYMMDD when scheduling activation");
|
|
}
|
|
|
|
const mapping = await this.mappings.findByUserId(userId);
|
|
if (!mapping?.whmcsClientId) {
|
|
throw new BadRequestException("WHMCS client mapping not found");
|
|
}
|
|
|
|
// 1) Create invoice for one-time activation fee only
|
|
const invoice = await this.whmcs.createInvoice({
|
|
clientId: mapping.whmcsClientId,
|
|
description: `SIM Activation Fee (${req.planSku}) for ${req.msisdn}`,
|
|
amount: req.oneTimeAmountJpy,
|
|
currency: "JPY",
|
|
dueDate: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
|
|
notes: `SIM activation fee for ${req.msisdn}, plan ${req.planSku}. Monthly billing will start on the 1st of next month.`,
|
|
});
|
|
|
|
const paymentResult = await this.whmcs.capturePayment({
|
|
invoiceId: invoice.id,
|
|
amount: req.oneTimeAmountJpy,
|
|
currency: "JPY",
|
|
});
|
|
|
|
if (!paymentResult.success) {
|
|
await this.whmcs.updateInvoice({
|
|
invoiceId: invoice.id,
|
|
status: "Cancelled",
|
|
notes: `Payment failed: ${paymentResult.error || "unknown"}`,
|
|
});
|
|
throw new BadRequestException(`Payment failed: ${paymentResult.error || "unknown"}`);
|
|
}
|
|
|
|
// 2) Freebit activation
|
|
try {
|
|
if (req.simType === "eSIM") {
|
|
await this.freebit.activateEsimAccountNew({
|
|
account: req.msisdn,
|
|
eid: req.eid!,
|
|
planCode: req.planSku,
|
|
contractLine: "5G",
|
|
shipDate: req.activationType === "Scheduled" ? req.scheduledAt : undefined,
|
|
mnp: req.mnp
|
|
? { 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,
|
|
});
|
|
} else {
|
|
this.logger.warn("Physical SIM activation path is not implemented; skipping Freebit call", {
|
|
account: req.msisdn,
|
|
});
|
|
}
|
|
|
|
// 3) Add-ons (voice options) immediately after activation if selected
|
|
if (req.addons && (req.addons.voiceMail || req.addons.callWaiting)) {
|
|
await this.freebit.updateSimFeatures(req.msisdn, {
|
|
voiceMailEnabled: !!req.addons.voiceMail,
|
|
callWaitingEnabled: !!req.addons.callWaiting,
|
|
});
|
|
}
|
|
|
|
// 4) Create monthly subscription for recurring billing
|
|
if (req.monthlyAmountJpy > 0) {
|
|
const nextMonth = new Date();
|
|
nextMonth.setMonth(nextMonth.getMonth() + 1);
|
|
nextMonth.setDate(1); // First day of next month
|
|
nextMonth.setHours(0, 0, 0, 0);
|
|
|
|
// Create a monthly subscription order using the order service
|
|
const orderService = this.whmcs.getOrderService();
|
|
await orderService.addOrder({
|
|
clientId: mapping.whmcsClientId,
|
|
items: [
|
|
{
|
|
productId: req.planSku, // Use the plan SKU as product ID
|
|
billingCycle: "monthly",
|
|
quantity: 1,
|
|
configOptions: {
|
|
phone_number: req.msisdn,
|
|
activation_date: nextMonth.toISOString().split("T")[0],
|
|
},
|
|
customFields: {
|
|
sim_type: req.simType,
|
|
eid: req.eid || "",
|
|
},
|
|
},
|
|
],
|
|
paymentMethod: "mailin", // Default payment method
|
|
notes: `Monthly SIM plan billing for ${req.msisdn}, plan ${req.planSku}. Billing starts on the 1st of next month.`,
|
|
noinvoice: false, // Create invoice
|
|
noinvoiceemail: true, // Suppress invoice email for now
|
|
noemail: true, // Suppress order emails
|
|
});
|
|
|
|
this.logger.log("Monthly subscription created", {
|
|
account: req.msisdn,
|
|
amount: req.monthlyAmountJpy,
|
|
nextDueDate: nextMonth.toISOString().split("T")[0],
|
|
});
|
|
}
|
|
|
|
this.logger.log("SIM activation completed", { account: req.msisdn, invoiceId: invoice.id });
|
|
return { success: true, invoiceId: invoice.id, transactionId: paymentResult.transactionId };
|
|
} catch (err) {
|
|
await this.whmcs.updateInvoice({
|
|
invoiceId: invoice.id,
|
|
notes: `Freebit activation failed after payment: ${getErrorMessage(err)}`,
|
|
});
|
|
throw err;
|
|
}
|
|
}
|
|
}
|