- Deleted migration file that removed cached profile fields from the users table, centralizing profile data retrieval from WHMCS. - Updated CsrfMiddleware to include new public authentication endpoints for password reset, setting password, and WHMCS account linking. - Enhanced error handling in password and WHMCS linking workflows to provide clearer feedback on missing mappings and improve user experience. - Adjusted user creation and update methods in UsersFacade to handle cases where WHMCS mappings are not yet available, ensuring smoother account setup.
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 "";
|
|
}
|
|
}
|