import { Injectable, Inject, BadRequestException } from "@nestjs/common"; import { Logger } from "nestjs-pino"; import { SubscriptionsService } from "../../subscriptions.service"; import { getErrorMessage } from "@bff/core/utils/error.util"; import type { SimValidationResult } from "../interfaces/sim-base.interface"; import { MappingsService } from "@bff/modules/id-mappings/mappings.service"; import { WhmcsService } from "@bff/integrations/whmcs/whmcs.service"; @Injectable() export class SimValidationService { constructor( private readonly subscriptionsService: SubscriptionsService, private readonly mappingsService: MappingsService, private readonly whmcsService: WhmcsService, @Inject(Logger) private readonly logger: Logger ) {} /** * Check if a subscription is a SIM service and extract account identifier */ async validateSimSubscription( userId: string, subscriptionId: number ): Promise { 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 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 = ""; let accountSource = ""; // 1. Try domain field first if (subscription.domain && subscription.domain.trim()) { account = subscription.domain.trim(); accountSource = "subscription.domain"; this.logger.log(`Found SIM account in domain field: ${account}`, { userId, subscriptionId, source: accountSource, }); } // 2. If no domain, check custom fields for phone number/MSISDN if (!account && subscription.customFields) { account = this.extractAccountFromCustomFields(subscription.customFields, subscriptionId); if (account) { accountSource = "subscription.customFields"; this.logger.log(`Found SIM account in custom fields: ${account}`, { userId, subscriptionId, source: accountSource, }); } } // 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; accountSource = "subscription.orderNumber"; this.logger.log(`Found SIM account in order number: ${account}`, { userId, subscriptionId, source: accountSource, }); } } // 4. Final fallback - get phone number from WHMCS account if (!account) { try { const mapping = await this.mappingsService.findByUserId(userId); if (mapping?.whmcsClientId) { const client = await this.whmcsService.getClientDetails(mapping.whmcsClientId); if (client?.phonenumber) { account = client.phonenumber; accountSource = "whmcs.account.phonenumber"; this.logger.log( `Found SIM account in WHMCS account phone number: ${account}`, { userId, subscriptionId, productName: subscription.productName, whmcsClientId: mapping.whmcsClientId, source: accountSource, } ); } } } catch (error) { this.logger.warn( `Failed to retrieve phone number from WHMCS account for user ${userId}`, { error: getErrorMessage(error), userId, subscriptionId, } ); } // If still no account found, throw an error if (!account) { throw new BadRequestException( `No SIM account identifier (phone number) found for subscription ${subscriptionId}. ` + `Please ensure the subscription has a phone number in the domain field, custom fields, or in your WHMCS account profile.` ); } } // Clean up the account format (remove hyphens, spaces, etc.) account = account.replace(/[-\s()]/g, ""); // Skip phone number format validation for testing // In production, you might want to add validation back: // 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).`); // } // account = cleanAccount; this.logger.log(`Using SIM account: ${account} (from ${accountSource})`, { userId, subscriptionId, account, source: accountSource, note: "Phone number format validation skipped for testing", }); return { account }; } catch (error) { const sanitizedError = getErrorMessage(error); this.logger.error( `Failed to validate SIM subscription ${subscriptionId} for user ${userId}`, { error: sanitizedError, } ); throw error; } } /** * Extract account identifier from custom fields */ private extractAccountFromCustomFields( customFields: Record, subscriptionId: number ): string { // Common field names for SIM phone numbers in WHMCS const phoneFields = [ "phone", "msisdn", "phonenumber", "phone_number", "mobile", "sim_phone", "Phone Number", "MSISDN", "Phone", "Mobile", "SIM Phone", "PhoneNumber", "phone_number", "mobile_number", "sim_number", "account_number", "Account Number", "SIM Account", "Phone Number (SIM)", "Mobile Number", // Specific field names that might contain the SIM number "SIM Number", "SIM_Number", "sim_number", "SIM_Phone_Number", "Phone_Number_SIM", "Mobile_SIM_Number", "SIM_Account_Number", "ICCID", "iccid", "IMSI", "imsi", "EID", "eid", // Additional variations "SIM_Data", "SIM_Info", "SIM_Details", ]; for (const fieldName of phoneFields) { const rawValue = customFields[fieldName]; if (rawValue !== undefined && rawValue !== null && rawValue !== "") { const accountValue = this.formatCustomFieldValue(rawValue); this.logger.log(`Found SIM account in custom field '${fieldName}': ${accountValue}`, { subscriptionId, fieldName, account: accountValue, }); return accountValue; } } // If still no account found, log all available custom fields for debugging this.logger.warn(`No SIM account found in custom fields for subscription ${subscriptionId}`, { subscriptionId, availableFields: Object.keys(customFields), customFields, searchedFields: phoneFields, }); return ""; } /** * Debug method to check subscription data for SIM services */ async debugSimSubscription( userId: string, subscriptionId: number ): Promise> { try { const subscription = await this.subscriptionsService.getSubscriptionById( userId, subscriptionId ); // Get WHMCS account phone number for debugging let whmcsAccountPhone: string | undefined; try { const mapping = await this.mappingsService.findByUserId(userId); if (mapping?.whmcsClientId) { const client = await this.whmcsService.getClientDetails(mapping.whmcsClientId); whmcsAccountPhone = client?.phonenumber; } } catch (error) { this.logger.warn(`Failed to fetch WHMCS account phone for debugging`, { error: getErrorMessage(error), }); } 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, whmcsAccountPhone, allCustomFieldKeys: Object.keys(subscription.customFields || {}), allCustomFieldValues: subscription.customFields, }; } catch (error) { const sanitizedError = getErrorMessage(error); this.logger.error(`Failed to debug subscription ${subscriptionId}`, { error: sanitizedError, }); throw error; } } private formatCustomFieldValue(value: unknown): string { if (typeof value === "string") { return value; } if (typeof value === "number" || typeof value === "boolean") { return String(value); } if (value instanceof Date) { return value.toISOString(); } if (typeof value === "object" && value !== null) { try { return JSON.stringify(value); } catch { return "[unserializable]"; } } return ""; } }