Assist_Design/apps/bff/src/subscriptions/sim-management.service.ts

430 lines
14 KiB
TypeScript
Raw Normal View History

import { Injectable, Inject, BadRequestException, NotFoundException } from '@nestjs/common';
import { Logger } from 'nestjs-pino';
import { FreebititService } from '../vendors/freebit/freebit.service';
import { MappingsService } from '../mappings/mappings.service';
import { SubscriptionsService } from './subscriptions.service';
import { SimDetails, SimUsage, SimTopUpHistory } from '../vendors/freebit/interfaces/freebit.types';
import { getErrorMessage } from '../common/utils/error.util';
export interface SimTopUpRequest {
quotaMb: number;
campaignCode?: string;
expiryDate?: string; // YYYYMMDD
scheduledAt?: string; // YYYYMMDD or YYYY-MM-DD HH:MM:SS
}
export interface SimPlanChangeRequest {
newPlanCode: string;
assignGlobalIp?: boolean;
scheduledAt?: string; // YYYYMMDD
}
export interface SimCancelRequest {
scheduledAt?: string; // YYYYMMDD - optional, immediate if omitted
}
export interface SimTopUpHistoryRequest {
fromDate: string; // YYYYMMDD
toDate: string; // YYYYMMDD
}
@Injectable()
export class SimManagementService {
constructor(
private readonly freebititService: FreebititService,
private readonly mappingsService: MappingsService,
private readonly subscriptionsService: SubscriptionsService,
@Inject(Logger) private readonly logger: Logger,
) {}
/**
* Debug method to check subscription data for SIM services
*/
async debugSimSubscription(userId: string, subscriptionId: number): Promise<any> {
try {
const subscription = await this.subscriptionsService.getSubscriptionById(userId, subscriptionId);
return {
subscriptionId,
productName: subscription.productName,
domain: subscription.domain,
orderNumber: subscription.orderNumber,
customFields: subscription.customFields,
isSimService: subscription.productName.toLowerCase().includes('sim') ||
subscription.groupName?.toLowerCase().includes('sim'),
groupName: subscription.groupName,
status: subscription.status,
};
} catch (error) {
this.logger.error(`Failed to debug subscription ${subscriptionId}`, {
error: getErrorMessage(error),
});
throw error;
}
}
/**
* Check if a subscription is a SIM service
*/
private async validateSimSubscription(userId: string, subscriptionId: number): Promise<{ account: string }> {
try {
// Get subscription details to verify it's a SIM service
const subscription = await this.subscriptionsService.getSubscriptionById(userId, subscriptionId);
// Check if this is a SIM service (you may need to adjust this logic based on your product naming)
const isSimService = subscription.productName.toLowerCase().includes('sim') ||
subscription.groupName?.toLowerCase().includes('sim');
if (!isSimService) {
throw new BadRequestException('This subscription is not a SIM service');
}
// For SIM services, the account identifier (phone number) can be stored in multiple places
let account = '';
// 1. Try domain field first
if (subscription.domain && subscription.domain.trim()) {
account = subscription.domain.trim();
}
// 2. If no domain, check custom fields for phone number/MSISDN
if (!account && subscription.customFields) {
const phoneFields = ['phone', 'msisdn', 'phonenumber', 'phone_number', 'mobile', 'sim_phone'];
for (const fieldName of phoneFields) {
if (subscription.customFields[fieldName]) {
account = subscription.customFields[fieldName];
break;
}
}
}
// 3. If still no account, check if subscription ID looks like a phone number
if (!account && subscription.orderNumber) {
const orderNum = subscription.orderNumber.toString();
if (/^\d{10,11}$/.test(orderNum)) {
account = orderNum;
}
}
// 4. Final fallback - for testing, use a dummy phone number based on subscription ID
if (!account) {
// Generate a test phone number: 080 + last 8 digits of subscription ID
const subIdStr = subscriptionId.toString().padStart(8, '0');
account = `080${subIdStr.slice(-8)}`;
this.logger.warn(`No SIM account identifier found for subscription ${subscriptionId}, using generated number: ${account}`, {
userId,
subscriptionId,
productName: subscription.productName,
domain: subscription.domain,
customFields: subscription.customFields ? Object.keys(subscription.customFields) : [],
});
}
// Clean up the account format (remove hyphens, spaces, etc.)
account = account.replace(/[-\s()]/g, '');
// Validate phone number format (10-11 digits, optionally starting with +81)
const cleanAccount = account.replace(/^\+81/, '0'); // Convert +81 to 0
if (!/^0\d{9,10}$/.test(cleanAccount)) {
throw new BadRequestException(`Invalid SIM account format: ${account}. Expected Japanese phone number format (10-11 digits starting with 0).`);
}
// Use the cleaned format
account = cleanAccount;
return { account };
} catch (error) {
this.logger.error(`Failed to validate SIM subscription ${subscriptionId} for user ${userId}`, {
error: getErrorMessage(error),
});
throw error;
}
}
/**
* Get SIM details for a subscription
*/
async getSimDetails(userId: string, subscriptionId: number): Promise<SimDetails> {
try {
const { account } = await this.validateSimSubscription(userId, subscriptionId);
const simDetails = await this.freebititService.getSimDetails(account);
this.logger.log(`Retrieved SIM details for subscription ${subscriptionId}`, {
userId,
subscriptionId,
account,
status: simDetails.status,
});
return simDetails;
} catch (error) {
this.logger.error(`Failed to get SIM details for subscription ${subscriptionId}`, {
error: getErrorMessage(error),
userId,
subscriptionId,
});
throw error;
}
}
/**
* Get SIM data usage for a subscription
*/
async getSimUsage(userId: string, subscriptionId: number): Promise<SimUsage> {
try {
const { account } = await this.validateSimSubscription(userId, subscriptionId);
const simUsage = await this.freebititService.getSimUsage(account);
this.logger.log(`Retrieved SIM usage for subscription ${subscriptionId}`, {
userId,
subscriptionId,
account,
todayUsageMb: simUsage.todayUsageMb,
});
return simUsage;
} catch (error) {
this.logger.error(`Failed to get SIM usage for subscription ${subscriptionId}`, {
error: getErrorMessage(error),
userId,
subscriptionId,
});
throw error;
}
}
/**
* Top up SIM data quota
*/
async topUpSim(userId: string, subscriptionId: number, request: SimTopUpRequest): Promise<void> {
try {
const { account } = await this.validateSimSubscription(userId, subscriptionId);
// Validate quota amount
if (request.quotaMb <= 0 || request.quotaMb > 100000) {
throw new BadRequestException('Quota must be between 1MB and 100GB');
}
// Validate date formats if provided
if (request.expiryDate && !/^\d{8}$/.test(request.expiryDate)) {
throw new BadRequestException('Expiry date must be in YYYYMMDD format');
}
if (request.scheduledAt && !/^\d{8}$/.test(request.scheduledAt.replace(/[-:\s]/g, ''))) {
throw new BadRequestException('Scheduled date must be in YYYYMMDD format');
}
await this.freebititService.topUpSim(account, request.quotaMb, {
campaignCode: request.campaignCode,
expiryDate: request.expiryDate,
scheduledAt: request.scheduledAt,
});
this.logger.log(`Successfully topped up SIM for subscription ${subscriptionId}`, {
userId,
subscriptionId,
account,
quotaMb: request.quotaMb,
scheduled: !!request.scheduledAt,
});
} catch (error) {
this.logger.error(`Failed to top up SIM for subscription ${subscriptionId}`, {
error: getErrorMessage(error),
userId,
subscriptionId,
quotaMb: request.quotaMb,
});
throw error;
}
}
/**
* Get SIM top-up history
*/
async getSimTopUpHistory(
userId: string,
subscriptionId: number,
request: SimTopUpHistoryRequest
): Promise<SimTopUpHistory> {
try {
const { account } = await this.validateSimSubscription(userId, subscriptionId);
// Validate date format
if (!/^\d{8}$/.test(request.fromDate) || !/^\d{8}$/.test(request.toDate)) {
throw new BadRequestException('Dates must be in YYYYMMDD format');
}
const history = await this.freebititService.getSimTopUpHistory(
account,
request.fromDate,
request.toDate
);
this.logger.log(`Retrieved SIM top-up history for subscription ${subscriptionId}`, {
userId,
subscriptionId,
account,
totalAdditions: history.totalAdditions,
});
return history;
} catch (error) {
this.logger.error(`Failed to get SIM top-up history for subscription ${subscriptionId}`, {
error: getErrorMessage(error),
userId,
subscriptionId,
});
throw error;
}
}
/**
* Change SIM plan
*/
async changeSimPlan(
userId: string,
subscriptionId: number,
request: SimPlanChangeRequest
): Promise<{ ipv4?: string; ipv6?: string }> {
try {
const { account } = await this.validateSimSubscription(userId, subscriptionId);
// Validate plan code format
if (!request.newPlanCode || request.newPlanCode.length < 3) {
throw new BadRequestException('Invalid plan code');
}
// Validate scheduled date if provided
if (request.scheduledAt && !/^\d{8}$/.test(request.scheduledAt)) {
throw new BadRequestException('Scheduled date must be in YYYYMMDD format');
}
const result = await this.freebititService.changeSimPlan(account, request.newPlanCode, {
assignGlobalIp: request.assignGlobalIp,
scheduledAt: request.scheduledAt,
});
this.logger.log(`Successfully changed SIM plan for subscription ${subscriptionId}`, {
userId,
subscriptionId,
account,
newPlanCode: request.newPlanCode,
scheduled: !!request.scheduledAt,
});
return result;
} catch (error) {
this.logger.error(`Failed to change SIM plan for subscription ${subscriptionId}`, {
error: getErrorMessage(error),
userId,
subscriptionId,
newPlanCode: request.newPlanCode,
});
throw error;
}
}
/**
* Cancel SIM service
*/
async cancelSim(userId: string, subscriptionId: number, request: SimCancelRequest = {}): Promise<void> {
try {
const { account } = await this.validateSimSubscription(userId, subscriptionId);
// Validate scheduled date if provided
if (request.scheduledAt && !/^\d{8}$/.test(request.scheduledAt)) {
throw new BadRequestException('Scheduled date must be in YYYYMMDD format');
}
await this.freebititService.cancelSim(account, request.scheduledAt);
this.logger.log(`Successfully cancelled SIM for subscription ${subscriptionId}`, {
userId,
subscriptionId,
account,
scheduled: !!request.scheduledAt,
});
} catch (error) {
this.logger.error(`Failed to cancel SIM for subscription ${subscriptionId}`, {
error: getErrorMessage(error),
userId,
subscriptionId,
});
throw error;
}
}
/**
* Reissue eSIM profile
*/
async reissueEsimProfile(userId: string, subscriptionId: number): Promise<void> {
try {
const { account } = await this.validateSimSubscription(userId, subscriptionId);
// First check if this is actually an eSIM
const simDetails = await this.freebititService.getSimDetails(account);
if (simDetails.simType !== 'esim') {
throw new BadRequestException('This operation is only available for eSIM subscriptions');
}
await this.freebititService.reissueEsimProfile(account);
this.logger.log(`Successfully reissued eSIM profile for subscription ${subscriptionId}`, {
userId,
subscriptionId,
account,
});
} catch (error) {
this.logger.error(`Failed to reissue eSIM profile for subscription ${subscriptionId}`, {
error: getErrorMessage(error),
userId,
subscriptionId,
});
throw error;
}
}
/**
* Get comprehensive SIM information (details + usage combined)
*/
async getSimInfo(userId: string, subscriptionId: number): Promise<{
details: SimDetails;
usage: SimUsage;
}> {
try {
const [details, usage] = await Promise.all([
this.getSimDetails(userId, subscriptionId),
this.getSimUsage(userId, subscriptionId),
]);
// If Freebit doesn't return remaining quota, derive it from plan code (e.g., PASI_50G)
// by subtracting measured usage (today + recentDays) from the plan cap.
const normalizeNumber = (n: number) => (isFinite(n) && n > 0 ? n : 0);
const usedMb = normalizeNumber(usage.todayUsageMb) + usage.recentDaysUsage.reduce((sum, d) => sum + normalizeNumber(d.usageMb), 0);
const planCapMatch = (details.planCode || '').match(/(\d+)\s*G/i);
if ((details.remainingQuotaMb === 0 || details.remainingQuotaMb == null) && planCapMatch) {
const capGb = parseInt(planCapMatch[1], 10);
if (!isNaN(capGb) && capGb > 0) {
const capMb = capGb * 1024;
const remainingMb = Math.max(capMb - usedMb, 0);
details.remainingQuotaMb = Math.round(remainingMb * 100) / 100;
details.remainingQuotaKb = Math.round(details.remainingQuotaMb * 1024);
}
}
return { details, usage };
} catch (error) {
this.logger.error(`Failed to get comprehensive SIM info for subscription ${subscriptionId}`, {
error: getErrorMessage(error),
userId,
subscriptionId,
});
throw error;
}
}
}