Assist_Design/apps/bff/src/users/users.service.ts

710 lines
24 KiB
TypeScript
Raw Normal View History

2025-08-21 15:24:40 +09:00
import { getErrorMessage } from "../common/utils/error.util";
import type { UpdateAddressDto } from "./dto/update-address.dto";
import { Injectable, Inject, NotFoundException, BadRequestException } from "@nestjs/common";
2025-08-21 15:24:40 +09:00
import { Logger } from "nestjs-pino";
import { PrismaService } from "../common/prisma/prisma.service";
import { User, Activity } from "@customer-portal/shared";
import type { Subscription, Invoice } from "@customer-portal/shared";
2025-08-23 18:02:05 +09:00
import { User as PrismaUser } from "@prisma/client";
2025-08-21 15:24:40 +09:00
import { WhmcsService } from "../vendors/whmcs/whmcs.service";
import { SalesforceService } from "../vendors/salesforce/salesforce.service";
import { WhmcsClientResponse } from "../vendors/whmcs/types/whmcs-api.types";
2025-08-28 16:57:57 +09:00
// Removed unused import: getSalesforceFieldMap
2025-08-21 15:24:40 +09:00
import { MappingsService } from "../mappings/mappings.service";
import { UpdateBillingDto } from "./dto/update-billing.dto";
// Enhanced type definitions for better type safety
2025-08-21 15:24:40 +09:00
export interface EnhancedUser extends Omit<User, "createdAt" | "updatedAt"> {
createdAt: Date;
updatedAt: Date;
mailingAddress?: {
street?: string | null;
city?: string | null;
state?: string | null;
postalCode?: string | null;
country?: string | null;
buildingName?: string | null;
roomNumber?: string | null;
};
salesforceHealthy?: boolean;
}
2025-08-23 18:02:05 +09:00
// Salesforce Account interface based on the data model
interface SalesforceAccount {
Id: string;
Name?: string;
PersonMailingStreet?: string;
PersonMailingCity?: string;
PersonMailingState?: string;
PersonMailingPostalCode?: string;
PersonMailingCountry?: string;
BillingStreet?: string;
BillingCity?: string;
BillingState?: string;
BillingPostalCode?: string;
BillingCountry?: string;
BuildingName__pc?: string;
BuildingName__c?: string;
RoomNumber__pc?: string;
RoomNumber__c?: string;
PersonMobilePhone?: string;
Mobile?: string;
}
interface UserUpdateData {
firstName?: string;
lastName?: string;
company?: string;
phone?: string;
passwordHash?: string;
failedLoginAttempts?: number;
lastLoginAt?: Date;
lockedUntil?: Date | null;
mailingAddress?: {
street?: string;
city?: string;
state?: string;
postalCode?: string;
country?: string;
buildingName?: string;
roomNumber?: string;
};
}
@Injectable()
export class UsersService {
constructor(
private prisma: PrismaService,
private whmcsService: WhmcsService,
private salesforceService: SalesforceService,
private mappingsService: MappingsService,
2025-08-22 17:02:49 +09:00
@Inject(Logger) private readonly logger: Logger
) {}
// Helper function to convert Prisma user to EnhancedUser type
private toEnhancedUser(
user: PrismaUser,
extras: Partial<EnhancedUser> = {},
salesforceHealthy: boolean = true
): EnhancedUser {
return {
id: user.id,
email: user.email,
2025-08-23 18:02:05 +09:00
firstName: user.firstName || undefined,
lastName: user.lastName || undefined,
company: user.company || undefined,
phone: user.phone || undefined,
mfaEnabled: !!user.mfaSecret, // Derive from mfaSecret existence
emailVerified: user.emailVerified,
createdAt: user.createdAt,
updatedAt: user.updatedAt,
salesforceHealthy,
2025-08-21 15:24:40 +09:00
...extras,
};
}
// Helper function to convert Prisma user to shared User type
2025-08-23 18:02:05 +09:00
private toUser(user: PrismaUser): User {
return {
id: user.id,
email: user.email,
2025-08-23 18:02:05 +09:00
firstName: user.firstName || undefined,
lastName: user.lastName || undefined,
company: user.company || undefined,
phone: user.phone || undefined,
mfaEnabled: !!user.mfaSecret, // Derive from mfaSecret existence
emailVerified: user.emailVerified,
createdAt: user.createdAt.toISOString(),
updatedAt: user.updatedAt.toISOString(),
};
}
private validateEmail(email: string): string {
const trimmed = email?.toLowerCase().trim();
2025-08-21 15:24:40 +09:00
if (!trimmed) throw new Error("Email is required");
2025-08-22 17:02:49 +09:00
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(trimmed)) throw new Error("Invalid email format");
return trimmed;
}
private validateUserId(id: string): string {
const trimmed = id?.trim();
2025-08-21 15:24:40 +09:00
if (!trimmed) throw new Error("User ID is required");
if (
2025-08-22 17:02:49 +09:00
!/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(trimmed)
2025-08-21 15:24:40 +09:00
) {
throw new Error("Invalid user ID format");
}
return trimmed;
}
async findByEmail(email: string): Promise<User | null> {
const validEmail = this.validateEmail(email);
try {
2025-08-21 15:24:40 +09:00
const user = await this.prisma.user.findUnique({
where: { email: validEmail },
});
return user ? this.toUser(user) : null;
} catch (error) {
2025-08-21 15:24:40 +09:00
this.logger.error("Failed to find user by email", {
error: getErrorMessage(error),
});
throw new Error("Failed to find user");
}
}
// Internal method for auth service - returns raw user with sensitive fields
2025-08-23 18:02:05 +09:00
async findByEmailInternal(email: string): Promise<PrismaUser | null> {
const validEmail = this.validateEmail(email);
try {
2025-08-21 15:24:40 +09:00
return await this.prisma.user.findUnique({
where: { email: validEmail },
});
} catch (error) {
2025-08-21 15:24:40 +09:00
this.logger.error("Failed to find user by email (internal)", {
error: getErrorMessage(error),
});
throw new Error("Failed to find user");
}
}
// Internal method for auth service - returns raw user by ID with sensitive fields
async findByIdInternal(id: string): Promise<PrismaUser | null> {
const validId = this.validateUserId(id);
try {
return await this.prisma.user.findUnique({ where: { id: validId } });
} catch (error) {
this.logger.error("Failed to find user by ID (internal)", {
error: getErrorMessage(error),
});
throw new Error("Failed to find user");
}
}
async findById(id: string): Promise<EnhancedUser | null> {
const validId = this.validateUserId(id);
try {
2025-08-21 15:24:40 +09:00
const user = await this.prisma.user.findUnique({
where: { id: validId },
});
if (!user) return null;
// Try to enhance with Salesforce data, fallback to basic user data
try {
return await this.getEnhancedProfile(validId);
} catch (error) {
2025-08-22 17:02:49 +09:00
this.logger.warn("Failed to fetch Salesforce data, returning basic user data", {
error: getErrorMessage(error),
userId: validId,
});
return this.toEnhancedUser(user, {}, false);
}
} catch (error) {
2025-08-21 15:24:40 +09:00
this.logger.error("Failed to find user by ID", {
error: getErrorMessage(error),
});
throw new Error("Failed to find user");
}
}
async getEnhancedProfile(userId: string): Promise<EnhancedUser> {
const user = await this.prisma.user.findUnique({ where: { id: userId } });
2025-08-21 15:24:40 +09:00
if (!user) throw new Error("User not found");
const mapping = await this.mappingsService.findByUserId(userId);
if (!mapping?.sfAccountId) return this.toEnhancedUser(user, {}, true);
let salesforceHealthy = true;
try {
2025-08-23 18:02:05 +09:00
const account = (await this.salesforceService.getAccount(
mapping.sfAccountId
)) as SalesforceAccount | null;
if (!account) return this.toEnhancedUser(user, undefined, salesforceHealthy);
return this.toEnhancedUser(
user,
{
company: account.Name?.trim() || user.company || undefined,
email: user.email, // Keep original email for now
phone: user.phone || undefined, // Keep original phone for now
// Address temporarily disabled until field issues resolved
},
salesforceHealthy
);
} catch (error) {
salesforceHealthy = false;
2025-08-21 15:24:40 +09:00
this.logger.error("Failed to fetch Salesforce account data", {
error: getErrorMessage(error),
userId,
sfAccountId: mapping.sfAccountId,
2025-08-21 15:24:40 +09:00
});
return this.toEnhancedUser(user, undefined, salesforceHealthy);
}
}
2025-08-23 18:02:05 +09:00
private hasAddress(_account: SalesforceAccount): boolean {
// Temporarily disabled until field mapping is resolved
return false;
}
2025-08-23 18:02:05 +09:00
private extractAddress(account: SalesforceAccount): {
street: string | null;
city: string | null;
state: string | null;
postalCode: string | null;
country: string | null;
buildingName: string | null;
roomNumber: string | null;
} {
// Prefer Person Account fields (Contact), fallback to Business Account fields
return {
street: account.PersonMailingStreet || account.BillingStreet || null,
city: account.PersonMailingCity || account.BillingCity || null,
state: account.PersonMailingState || account.BillingState || null,
2025-08-22 17:02:49 +09:00
postalCode: account.PersonMailingPostalCode || account.BillingPostalCode || null,
country: account.PersonMailingCountry || account.BillingCountry || null,
buildingName: account.BuildingName__pc || account.BuildingName__c || null,
roomNumber: account.RoomNumber__pc || account.RoomNumber__c || null,
};
}
2025-08-23 18:02:05 +09:00
async create(userData: Partial<PrismaUser>): Promise<User> {
const validEmail = this.validateEmail(userData.email!);
try {
const normalizedData = { ...userData, email: validEmail };
2025-08-21 15:24:40 +09:00
const createdUser = await this.prisma.user.create({
data: normalizedData,
});
return this.toUser(createdUser);
} catch (error) {
2025-08-21 15:24:40 +09:00
this.logger.error("Failed to create user", {
error: getErrorMessage(error),
});
throw new Error("Failed to create user");
}
}
async update(id: string, userData: UserUpdateData): Promise<User> {
const validId = this.validateUserId(id);
const sanitizedData = this.sanitizeUserData(userData);
try {
const updatedUser = await this.prisma.user.update({
where: { id: validId },
data: sanitizedData,
});
// Do not mutate Salesforce Account from the portal. Salesforce remains authoritative.
return this.toUser(updatedUser);
} catch (error) {
2025-08-21 15:24:40 +09:00
this.logger.error("Failed to update user", {
error: getErrorMessage(error),
});
throw new Error("Failed to update user");
}
}
2025-08-23 18:02:05 +09:00
private sanitizeUserData(userData: UserUpdateData): Partial<PrismaUser> {
const sanitized: Partial<PrismaUser> = {};
2025-08-21 15:24:40 +09:00
if (userData.firstName !== undefined)
sanitized.firstName = userData.firstName?.trim().substring(0, 50) || null;
if (userData.lastName !== undefined)
sanitized.lastName = userData.lastName?.trim().substring(0, 50) || null;
if (userData.company !== undefined)
sanitized.company = userData.company?.trim().substring(0, 100) || null;
if (userData.phone !== undefined)
sanitized.phone = userData.phone?.trim().substring(0, 20) || null;
// Handle authentication-related fields
2025-08-22 17:02:49 +09:00
if (userData.passwordHash !== undefined) sanitized.passwordHash = userData.passwordHash;
2025-08-21 15:24:40 +09:00
if (userData.failedLoginAttempts !== undefined)
sanitized.failedLoginAttempts = userData.failedLoginAttempts;
2025-08-22 17:02:49 +09:00
if (userData.lastLoginAt !== undefined) sanitized.lastLoginAt = userData.lastLoginAt;
if (userData.lockedUntil !== undefined) sanitized.lockedUntil = userData.lockedUntil;
2025-08-21 15:24:40 +09:00
return sanitized;
}
2025-08-22 17:02:49 +09:00
async syncToSalesforce(userId: string, userData: UserUpdateData): Promise<void> {
const mapping = await this.mappingsService.findByUserId(userId);
if (!mapping?.sfAccountId) return;
const salesforceUpdate = this.buildSalesforceUpdate(userData);
if (Object.keys(salesforceUpdate).length === 0) return;
try {
2025-08-22 17:02:49 +09:00
await this.salesforceService.updateAccount(mapping.sfAccountId, salesforceUpdate);
2025-08-21 15:24:40 +09:00
this.logger.debug("Successfully synced to Salesforce", {
fieldsUpdated: Object.keys(salesforceUpdate),
});
} catch (error) {
2025-08-21 15:24:40 +09:00
this.logger.error("Failed to sync to Salesforce", {
error: getErrorMessage(error),
});
throw error;
}
}
2025-08-23 18:02:05 +09:00
private buildSalesforceUpdate(userData: UserUpdateData): Partial<SalesforceAccount> {
2025-08-28 16:57:57 +09:00
// const fields = getSalesforceFieldMap(); // Unused variable
2025-08-23 18:02:05 +09:00
const update: Partial<SalesforceAccount> = {};
2025-08-21 15:24:40 +09:00
if (userData.company !== undefined) update.Name = userData.company;
if (userData.phone !== undefined) {
// Update both mobile fields for maximum compatibility
update.PersonMobilePhone = userData.phone;
update.Mobile = userData.phone;
}
2025-08-21 15:24:40 +09:00
if (userData.mailingAddress) {
const addr = userData.mailingAddress;
// Update both Person Account and Business Account address fields
if (addr.street !== undefined) {
update.PersonMailingStreet = addr.street;
update.BillingStreet = addr.street;
}
if (addr.city !== undefined) {
update.PersonMailingCity = addr.city;
update.BillingCity = addr.city;
}
if (addr.state !== undefined) {
update.PersonMailingState = addr.state;
update.BillingState = addr.state;
}
if (addr.postalCode !== undefined) {
update.PersonMailingPostalCode = addr.postalCode;
update.BillingPostalCode = addr.postalCode;
}
if (addr.country !== undefined) {
update.PersonMailingCountry = addr.country;
update.BillingCountry = addr.country;
}
if (addr.buildingName !== undefined) {
2025-08-28 16:57:57 +09:00
(update as Record<string, unknown>)["BuildingName__pc"] = addr.buildingName;
(update as Record<string, unknown>)["BuildingName__c"] = addr.buildingName;
}
if (addr.roomNumber !== undefined) {
2025-08-28 16:57:57 +09:00
(update as Record<string, unknown>)["RoomNumber__pc"] = addr.roomNumber;
(update as Record<string, unknown>)["RoomNumber__c"] = addr.roomNumber;
}
}
2025-08-21 15:24:40 +09:00
return update;
}
async getUserSummary(userId: string) {
try {
// Verify user exists
const user = await this.prisma.user.findUnique({ where: { id: userId } });
if (!user) {
2025-08-21 15:24:40 +09:00
throw new Error("User not found");
}
// Check if user has WHMCS mapping
const mapping = await this.mappingsService.findByUserId(userId);
if (!mapping?.whmcsClientId) {
this.logger.warn(`No WHMCS mapping found for user ${userId}`);
return {
stats: {
activeSubscriptions: 0,
unpaidInvoices: 0,
openCases: 0,
2025-08-21 15:24:40 +09:00
currency: "JPY",
},
nextInvoice: null,
2025-08-21 15:24:40 +09:00
recentActivity: [],
};
}
// Fetch live data from WHMCS in parallel
const [subscriptionsData, invoicesData] = await Promise.allSettled([
this.whmcsService.getSubscriptions(mapping.whmcsClientId, userId),
2025-08-21 15:24:40 +09:00
this.whmcsService.getInvoices(mapping.whmcsClientId, userId, {
limit: 50,
}),
]);
// Process subscriptions
let activeSubscriptions = 0;
2025-08-23 18:02:05 +09:00
let recentSubscriptions: Array<{
id: string;
status: string;
registrationDate: string;
productName: string;
}> = [];
2025-08-21 15:24:40 +09:00
if (subscriptionsData.status === "fulfilled") {
const subscriptions: Subscription[] = subscriptionsData.value.subscriptions;
activeSubscriptions = subscriptions.filter(
(sub: Subscription) => sub.status === "Active"
).length;
recentSubscriptions = subscriptions
.filter((sub: Subscription) => sub.status === "Active")
2025-08-21 15:24:40 +09:00
.sort(
(a: Subscription, b: Subscription) =>
2025-08-22 17:02:49 +09:00
new Date(b.registrationDate).getTime() - new Date(a.registrationDate).getTime()
2025-08-21 15:24:40 +09:00
)
2025-08-23 18:02:05 +09:00
.slice(0, 3)
.map((sub: Subscription) => ({
2025-08-23 18:02:05 +09:00
id: sub.id.toString(),
status: sub.status,
registrationDate: sub.registrationDate,
productName: sub.productName,
}));
} else {
2025-08-21 15:24:40 +09:00
this.logger.error(
`Failed to fetch subscriptions for user ${userId}:`,
2025-08-22 17:02:49 +09:00
subscriptionsData.reason
2025-08-21 15:24:40 +09:00
);
}
// Process invoices
let unpaidInvoices = 0;
let nextInvoice = null;
2025-08-23 18:02:05 +09:00
let recentInvoices: Array<{
id: string;
status: string;
dueDate?: string;
total: number;
number: string;
issuedAt?: string;
paidDate?: string;
}> = [];
2025-08-21 15:24:40 +09:00
if (invoicesData.status === "fulfilled") {
const invoices: Invoice[] = invoicesData.value.invoices;
2025-08-21 15:24:40 +09:00
// Count unpaid invoices
2025-08-21 15:24:40 +09:00
unpaidInvoices = invoices.filter(
(inv: Invoice) => inv.status === "Unpaid" || inv.status === "Overdue"
2025-08-21 15:24:40 +09:00
).length;
// Find next due invoice
const upcomingInvoices = invoices
.filter(
(inv: Invoice) => (inv.status === "Unpaid" || inv.status === "Overdue") && inv.dueDate
)
.sort(
(a: Invoice, b: Invoice) =>
new Date(a.dueDate!).getTime() - new Date(b.dueDate!).getTime()
);
2025-08-21 15:24:40 +09:00
if (upcomingInvoices.length > 0) {
const invoice = upcomingInvoices[0];
nextInvoice = {
2025-08-23 18:02:05 +09:00
id: invoice.id.toString(),
dueDate: invoice.dueDate,
amount: invoice.total,
2025-08-21 15:24:40 +09:00
currency: "JPY",
};
}
// Recent invoices for activity
recentInvoices = invoices
2025-08-21 15:24:40 +09:00
.sort(
(a: Invoice, b: Invoice) =>
new Date(b.issuedAt || "").getTime() - new Date(a.issuedAt || "").getTime()
2025-08-21 15:24:40 +09:00
)
2025-08-23 18:02:05 +09:00
.slice(0, 5)
.map((inv: Invoice) => ({
2025-08-23 18:02:05 +09:00
id: inv.id.toString(),
status: inv.status,
dueDate: inv.dueDate,
total: inv.total,
number: inv.number,
issuedAt: inv.issuedAt,
}));
} else {
2025-08-22 17:02:49 +09:00
this.logger.error(`Failed to fetch invoices for user ${userId}`, {
reason: getErrorMessage(invoicesData.reason),
});
}
// Build activity feed
const activities: Activity[] = [];
// Add invoice activities
2025-08-22 17:02:49 +09:00
recentInvoices.forEach(invoice => {
2025-08-21 15:24:40 +09:00
if (invoice.status === "Paid") {
activities.push({
id: `invoice-paid-${invoice.id}`,
2025-08-21 15:24:40 +09:00
type: "invoice_paid",
title: `Invoice #${invoice.number} paid`,
description: `Payment of ¥${invoice.total.toLocaleString()} processed`,
2025-08-22 17:02:49 +09:00
date: invoice.paidDate || invoice.issuedAt || new Date().toISOString(),
2025-08-23 18:02:05 +09:00
relatedId: Number(invoice.id),
});
2025-08-22 17:02:49 +09:00
} else if (invoice.status === "Unpaid" || invoice.status === "Overdue") {
activities.push({
id: `invoice-created-${invoice.id}`,
2025-08-21 15:24:40 +09:00
type: "invoice_created",
title: `Invoice #${invoice.number} created`,
description: `Amount: ¥${invoice.total.toLocaleString()}`,
2025-08-23 18:02:05 +09:00
date: invoice.issuedAt || new Date().toISOString(),
relatedId: Number(invoice.id),
});
}
});
// Add subscription activities
2025-08-22 17:02:49 +09:00
recentSubscriptions.forEach(subscription => {
activities.push({
id: `service-activated-${subscription.id}`,
2025-08-21 15:24:40 +09:00
type: "service_activated",
title: `${subscription.productName} activated`,
2025-08-21 15:24:40 +09:00
description: "Service successfully provisioned",
date: subscription.registrationDate,
2025-08-23 18:02:05 +09:00
relatedId: Number(subscription.id),
});
});
// Sort activities by date and take top 10
2025-08-22 17:02:49 +09:00
activities.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
const recentActivity = activities.slice(0, 10);
this.logger.log(`Generated dashboard summary for user ${userId}`, {
activeSubscriptions,
unpaidInvoices,
activitiesCount: recentActivity.length,
2025-08-21 15:24:40 +09:00
hasNextInvoice: !!nextInvoice,
});
return {
stats: {
activeSubscriptions,
unpaidInvoices,
openCases: 0, // TODO: Implement support cases when ready
2025-08-21 15:24:40 +09:00
currency: "JPY",
},
nextInvoice,
2025-08-21 15:24:40 +09:00
recentActivity,
};
} catch (error) {
2025-08-22 17:02:49 +09:00
this.logger.error(`Failed to get user summary for ${userId}`, {
error: getErrorMessage(error),
});
throw new Error(`Failed to retrieve dashboard data: ${getErrorMessage(error)}`);
}
}
2025-08-28 16:57:57 +09:00
/**
* Get billing information from WHMCS (authoritative source)
*/
async getBillingInfo(userId: string) {
try {
const mapping = await this.mappingsService.findByUserId(userId);
if (!mapping) {
throw new NotFoundException("User mapping not found");
}
// Get client details from WHMCS
const clientDetails = await this.whmcsService.getClientDetails(mapping.whmcsClientId);
2025-08-28 16:57:57 +09:00
return {
company: clientDetails.companyname || null,
email: clientDetails.email,
phone: clientDetails.phonenumber || null,
address: {
street: clientDetails.address1 || null,
streetLine2: clientDetails.address2 || null,
2025-08-28 16:57:57 +09:00
city: clientDetails.city || null,
state: clientDetails.state || null,
postalCode: clientDetails.postcode || null,
country: clientDetails.country || null,
},
isComplete: !!(
clientDetails.address1 &&
clientDetails.city &&
clientDetails.state &&
clientDetails.postcode &&
clientDetails.country
),
};
} catch (error) {
this.logger.error(`Failed to get billing info for ${userId}`, {
error: getErrorMessage(error),
});
throw new Error(`Failed to retrieve billing information: ${getErrorMessage(error)}`);
}
}
/**
* Update billing information in WHMCS (authoritative source)
*/
async updateBillingInfo(userId: string, billingData: UpdateBillingDto): Promise<void> {
try {
// Get user mapping
const mapping = await this.mappingsService.findByUserId(userId);
if (!mapping) {
throw new NotFoundException("User mapping not found");
}
// Prepare WHMCS update data
const whmcsUpdateData: Partial<WhmcsClientResponse["client"]> = {};
if (billingData.street !== undefined) {
whmcsUpdateData.address1 = billingData.street;
}
if (billingData.streetLine2 !== undefined) {
whmcsUpdateData.address2 = billingData.streetLine2;
}
if (billingData.city !== undefined) {
whmcsUpdateData.city = billingData.city;
}
if (billingData.state !== undefined) {
whmcsUpdateData.state = billingData.state;
}
if (billingData.postalCode !== undefined) {
whmcsUpdateData.postcode = billingData.postalCode;
}
if (billingData.country !== undefined) {
whmcsUpdateData.country = billingData.country;
}
if (billingData.phone !== undefined) {
whmcsUpdateData.phonenumber = billingData.phone;
}
if (billingData.company !== undefined) {
whmcsUpdateData.companyname = billingData.company;
}
// No-op if nothing to update
if (Object.keys(whmcsUpdateData).length === 0) {
this.logger.debug({ userId }, "No billing fields provided; skipping WHMCS update");
return;
}
// Update in WHMCS (authoritative source)
await this.whmcsService.updateClient(mapping.whmcsClientId, whmcsUpdateData);
this.logger.log({ userId }, "Successfully updated billing information in WHMCS");
} catch (error) {
const msg = getErrorMessage(error);
this.logger.error({ userId, error: msg }, "Failed to update billing information");
// Surface API error details when available as 400 instead of 500
if (msg.includes("WHMCS API Error")) {
throw new BadRequestException(msg.replace("WHMCS API Error: ", ""));
}
if (msg.includes("HTTP ")) {
throw new BadRequestException("Upstream billing system error. Please try again.");
}
if (msg.includes("Missing required WHMCS configuration")) {
throw new BadRequestException("Billing system not configured. Please contact support.");
}
throw new BadRequestException("Unable to update billing information.");
}
2025-08-28 16:57:57 +09:00
}
/**
* Update only address fields in WHMCS (alias used by checkout)
*/
async updateAddress(userId: string, address: UpdateAddressDto): Promise<void> {
// Reuse the billing updater since WHMCS stores address on the client record
return this.updateBillingInfo(userId, address as unknown as UpdateBillingDto);
}
}