304 lines
9.5 KiB
TypeScript
304 lines
9.5 KiB
TypeScript
|
|
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<SimValidationResult> {
|
||
|
|
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<string, unknown>,
|
||
|
|
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<Record<string, unknown>> {
|
||
|
|
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 "";
|
||
|
|
}
|
||
|
|
}
|