refactor: consolidate error handling to safeOperation
- Enhance safeOperation with rethrow and fallbackMessage options for CRITICAL operations - Migrate all 19 withErrorHandling calls across 5 services to safeOperation - Remove safeAsync from error.util.ts - Delete error-handler.util.ts (withErrorHandling, withErrorSuppression, withErrorLogging) - Update barrel exports in core/utils/index.ts
This commit is contained in:
parent
a94e59fe06
commit
9736e96cb3
@ -1,122 +0,0 @@
|
||||
/**
|
||||
* Centralized error handling utilities for consistent error handling across services
|
||||
*/
|
||||
import {
|
||||
NotFoundException,
|
||||
BadRequestException,
|
||||
InternalServerErrorException,
|
||||
type Type,
|
||||
} from "@nestjs/common";
|
||||
import { Logger } from "nestjs-pino";
|
||||
import { extractErrorMessage } from "./error.util.js";
|
||||
|
||||
/**
|
||||
* Types of exceptions that can be rethrown without wrapping
|
||||
*/
|
||||
type RethrowableException = Type<Error>;
|
||||
|
||||
/**
|
||||
* Options for error handling
|
||||
*/
|
||||
interface ErrorHandlerOptions {
|
||||
/**
|
||||
* Context string for logging (e.g., "Get invoices for user xyz")
|
||||
*/
|
||||
context: string;
|
||||
|
||||
/**
|
||||
* Exception types that should be rethrown as-is without wrapping
|
||||
* @default [NotFoundException, BadRequestException]
|
||||
*/
|
||||
rethrow?: RethrowableException[];
|
||||
|
||||
/**
|
||||
* Custom fallback message for InternalServerErrorException
|
||||
* @default `${context} failed`
|
||||
*/
|
||||
fallbackMessage?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps an async operation with consistent error handling.
|
||||
*
|
||||
* - Logs errors with context
|
||||
* - Rethrows specified exception types as-is
|
||||
* - Wraps other errors in InternalServerErrorException
|
||||
*
|
||||
* @example
|
||||
* return withErrorHandling(
|
||||
* () => this.whmcsService.getInvoices(clientId, userId),
|
||||
* this.logger,
|
||||
* { context: `Get invoices for user ${userId}`, rethrow: [NotFoundException] }
|
||||
* );
|
||||
*/
|
||||
export async function withErrorHandling<T>(
|
||||
operation: () => Promise<T>,
|
||||
logger: Logger,
|
||||
options: ErrorHandlerOptions
|
||||
): Promise<T> {
|
||||
const { context, rethrow = [NotFoundException, BadRequestException], fallbackMessage } = options;
|
||||
|
||||
try {
|
||||
return await operation();
|
||||
} catch (error) {
|
||||
logger.error(context, { error: extractErrorMessage(error) });
|
||||
|
||||
for (const ExceptionType of rethrow) {
|
||||
if (error instanceof ExceptionType) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
throw new InternalServerErrorException(fallbackMessage ?? `${context} failed`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Safe wrapper that returns null instead of throwing.
|
||||
* Useful for optional operations where failure is acceptable.
|
||||
*
|
||||
* @example
|
||||
* const cached = await withErrorSuppression(
|
||||
* () => this.cache.get(key),
|
||||
* this.logger,
|
||||
* "Get cached value"
|
||||
* );
|
||||
*/
|
||||
export async function withErrorSuppression<T>(
|
||||
operation: () => Promise<T>,
|
||||
logger: Logger,
|
||||
context: string
|
||||
): Promise<T | null> {
|
||||
try {
|
||||
return await operation();
|
||||
} catch (error) {
|
||||
logger.warn(`${context} (suppressed)`, { error: extractErrorMessage(error) });
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps an async operation and logs errors, but always rethrows them.
|
||||
* Useful when you want consistent logging but don't want to change the error type.
|
||||
*
|
||||
* @example
|
||||
* return withErrorLogging(
|
||||
* () => this.externalService.call(),
|
||||
* this.logger,
|
||||
* "Call external service"
|
||||
* );
|
||||
*/
|
||||
export async function withErrorLogging<T>(
|
||||
operation: () => Promise<T>,
|
||||
logger: Logger,
|
||||
context: string
|
||||
): Promise<T> {
|
||||
try {
|
||||
return await operation();
|
||||
} catch (error) {
|
||||
logger.error(context, { error: extractErrorMessage(error) });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@ -185,19 +185,3 @@ export function createDeferredPromise<T>(): {
|
||||
|
||||
return { promise, resolve, reject };
|
||||
}
|
||||
|
||||
/**
|
||||
* Safe async operation wrapper with proper error handling
|
||||
*/
|
||||
export async function safeAsync<T>(
|
||||
operation: () => Promise<T>,
|
||||
fallback?: T
|
||||
): Promise<{ data: T | null; error: EnhancedError | null }> {
|
||||
try {
|
||||
const data = await operation();
|
||||
return { data, error: null };
|
||||
} catch (unknownError) {
|
||||
const error = toError(unknownError);
|
||||
return { data: fallback ?? null, error };
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,7 +9,7 @@ export { chunkArray, groupBy, uniqueBy } from "./array.util.js";
|
||||
|
||||
// Error utilities
|
||||
export { extractErrorMessage } from "./error.util.js";
|
||||
export { withErrorHandling, withErrorSuppression, withErrorLogging } from "./error-handler.util.js";
|
||||
export { safeOperation, OperationCriticality, type SafeOperationOptions, type SafeOperationResult } from "./safe-operation.util.js";
|
||||
|
||||
// Retry utilities
|
||||
export {
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { InternalServerErrorException } from "@nestjs/common";
|
||||
import type { Logger } from "nestjs-pino";
|
||||
import { extractErrorMessage } from "./error.util.js";
|
||||
|
||||
@ -36,6 +37,12 @@ export interface SafeOperationOptions<T> {
|
||||
|
||||
/** Additional metadata for logging */
|
||||
metadata?: Record<string, unknown>;
|
||||
|
||||
/** Exception types to rethrow as-is (only applicable when criticality is CRITICAL) */
|
||||
rethrow?: Array<new (...args: any[]) => Error>;
|
||||
|
||||
/** Custom message for InternalServerErrorException wrapper (only applicable when criticality is CRITICAL) */
|
||||
fallbackMessage?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -82,7 +89,7 @@ export async function safeOperation<T>(
|
||||
executor: () => Promise<T>,
|
||||
options: SafeOperationOptions<T>
|
||||
): Promise<T> {
|
||||
const { criticality, fallback, context, logger, metadata = {} } = options;
|
||||
const { criticality, fallback, context, logger, metadata = {}, rethrow, fallbackMessage } = options;
|
||||
|
||||
try {
|
||||
return await executor();
|
||||
@ -98,6 +105,14 @@ export async function safeOperation<T>(
|
||||
switch (criticality) {
|
||||
case OperationCriticality.CRITICAL:
|
||||
logger.error(logPayload, `Critical operation failed: ${context}`);
|
||||
if (rethrow) {
|
||||
for (const ExceptionType of rethrow) {
|
||||
if (error instanceof ExceptionType) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
throw new InternalServerErrorException(fallbackMessage ?? `${context} failed`);
|
||||
}
|
||||
throw error;
|
||||
|
||||
case OperationCriticality.DEGRADABLE:
|
||||
|
||||
@ -22,7 +22,7 @@ import {
|
||||
changePasswordRequestSchema,
|
||||
} from "@customer-portal/domain/auth";
|
||||
import { mapPrismaUserToDomain } from "@bff/infra/mappers/index.js";
|
||||
import { withErrorHandling } from "@bff/core/utils/error-handler.util.js";
|
||||
import { safeOperation, OperationCriticality } from "@bff/core/utils/safe-operation.util.js";
|
||||
import type { AuthResultInternal } from "@bff/modules/auth/auth.types.js";
|
||||
|
||||
@Injectable()
|
||||
@ -64,7 +64,7 @@ export class PasswordWorkflowService {
|
||||
|
||||
const passwordHash = await argon2.hash(password);
|
||||
|
||||
return withErrorHandling(
|
||||
return safeOperation(
|
||||
async () => {
|
||||
try {
|
||||
await this.usersService.update(user.id, { passwordHash });
|
||||
@ -102,9 +102,11 @@ export class PasswordWorkflowService {
|
||||
tokens,
|
||||
};
|
||||
},
|
||||
this.logger,
|
||||
{
|
||||
criticality: OperationCriticality.CRITICAL,
|
||||
context: `Set password for user ${user.id}`,
|
||||
logger: this.logger,
|
||||
rethrow: [NotFoundException, BadRequestException],
|
||||
fallbackMessage: "Failed to set password",
|
||||
}
|
||||
);
|
||||
@ -151,7 +153,7 @@ export class PasswordWorkflowService {
|
||||
// This will throw BadRequestException if token is invalid, expired, or already used
|
||||
const { userId } = await this.passwordResetTokenService.consume(token);
|
||||
|
||||
return withErrorHandling(
|
||||
return safeOperation(
|
||||
async () => {
|
||||
const prismaUser = await this.usersService.findByIdInternal(userId);
|
||||
if (!prismaUser) throw new BadRequestException("Invalid token");
|
||||
@ -168,11 +170,12 @@ export class PasswordWorkflowService {
|
||||
// Revoke all trusted devices - attacker could have set up a trusted device
|
||||
await this.trustedDeviceService.revokeAllUserDevices(freshUser.id);
|
||||
},
|
||||
this.logger,
|
||||
{
|
||||
criticality: OperationCriticality.CRITICAL,
|
||||
context: "Reset password",
|
||||
fallbackMessage: "Failed to reset password",
|
||||
logger: this.logger,
|
||||
rethrow: [BadRequestException],
|
||||
fallbackMessage: "Failed to reset password",
|
||||
}
|
||||
);
|
||||
}
|
||||
@ -213,7 +216,7 @@ export class PasswordWorkflowService {
|
||||
|
||||
const passwordHash = await argon2.hash(newPassword);
|
||||
|
||||
return withErrorHandling(
|
||||
return safeOperation(
|
||||
async () => {
|
||||
await this.usersService.update(user.id, { passwordHash });
|
||||
const prismaUser = await this.usersService.findByIdInternal(user.id);
|
||||
@ -244,9 +247,11 @@ export class PasswordWorkflowService {
|
||||
tokens,
|
||||
};
|
||||
},
|
||||
this.logger,
|
||||
{
|
||||
criticality: OperationCriticality.CRITICAL,
|
||||
context: `Change password for user ${user.id}`,
|
||||
logger: this.logger,
|
||||
rethrow: [NotFoundException, BadRequestException],
|
||||
fallbackMessage: "Failed to change password",
|
||||
}
|
||||
);
|
||||
|
||||
@ -14,7 +14,7 @@ import { SalesforceFacade } from "@bff/integrations/salesforce/facades/salesforc
|
||||
import { extractErrorMessage } from "@bff/core/utils/error.util.js";
|
||||
import { mapPrismaUserToDomain } from "@bff/infra/mappers/index.js";
|
||||
import { getCustomFieldValue } from "@customer-portal/domain/customer/providers";
|
||||
import { withErrorHandling } from "@bff/core/utils/error-handler.util.js";
|
||||
import { safeOperation, OperationCriticality } from "@bff/core/utils/safe-operation.util.js";
|
||||
import type { User } from "@customer-portal/domain/customer";
|
||||
import {
|
||||
PORTAL_SOURCE_MIGRATED,
|
||||
@ -50,7 +50,7 @@ export class WhmcsLinkWorkflowService {
|
||||
);
|
||||
}
|
||||
|
||||
return withErrorHandling(
|
||||
return safeOperation(
|
||||
async () => {
|
||||
const clientDetails = await this.discoveryService.findClientByEmail(email);
|
||||
if (!clientDetails) {
|
||||
@ -133,11 +133,12 @@ export class WhmcsLinkWorkflowService {
|
||||
needsPasswordSet: true,
|
||||
};
|
||||
},
|
||||
this.logger,
|
||||
{
|
||||
criticality: OperationCriticality.CRITICAL,
|
||||
context: "WHMCS account linking",
|
||||
fallbackMessage: "Failed to link WHMCS account",
|
||||
logger: this.logger,
|
||||
rethrow: [BadRequestException, ConflictException, UnauthorizedException],
|
||||
fallbackMessage: "Failed to link WHMCS account",
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { Injectable, Inject, BadRequestException } from "@nestjs/common";
|
||||
import { Injectable, Inject, BadRequestException, NotFoundException } from "@nestjs/common";
|
||||
import { Logger } from "nestjs-pino";
|
||||
import type {
|
||||
Invoice,
|
||||
@ -9,7 +9,7 @@ import type {
|
||||
import { INVOICE_PAGINATION, VALID_INVOICE_QUERY_STATUSES } from "@customer-portal/domain/billing";
|
||||
import { WhmcsInvoiceService } from "@bff/integrations/whmcs/services/whmcs-invoice.service.js";
|
||||
import { MappingsService } from "@bff/modules/id-mappings/mappings.service.js";
|
||||
import { withErrorHandling } from "@bff/core/utils/error-handler.util.js";
|
||||
import { safeOperation, OperationCriticality } from "@bff/core/utils/safe-operation.util.js";
|
||||
import { parseUuidOrThrow } from "@bff/core/utils/validation.util.js";
|
||||
|
||||
/**
|
||||
@ -40,7 +40,7 @@ export class InvoiceRetrievalService {
|
||||
parseUuidOrThrow(userId, "Invalid user ID format");
|
||||
const whmcsClientId = await this.mappingsService.getWhmcsClientIdOrThrow(userId);
|
||||
|
||||
return withErrorHandling(
|
||||
return safeOperation(
|
||||
async () => {
|
||||
const invoiceList = await this.whmcsInvoiceService.getInvoices(whmcsClientId, userId, {
|
||||
page,
|
||||
@ -57,8 +57,13 @@ export class InvoiceRetrievalService {
|
||||
|
||||
return invoiceList;
|
||||
},
|
||||
this.logger,
|
||||
{ context: `Get invoices for user ${userId}`, fallbackMessage: "Failed to retrieve invoices" }
|
||||
{
|
||||
criticality: OperationCriticality.CRITICAL,
|
||||
context: `Get invoices for user ${userId}`,
|
||||
logger: this.logger,
|
||||
rethrow: [NotFoundException, BadRequestException],
|
||||
fallbackMessage: "Failed to retrieve invoices",
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@ -69,7 +74,7 @@ export class InvoiceRetrievalService {
|
||||
parseUuidOrThrow(userId, "Invalid user ID format");
|
||||
const whmcsClientId = await this.mappingsService.getWhmcsClientIdOrThrow(userId);
|
||||
|
||||
return withErrorHandling(
|
||||
return safeOperation(
|
||||
async () => {
|
||||
const invoice = await this.whmcsInvoiceService.getInvoiceById(
|
||||
whmcsClientId,
|
||||
@ -79,9 +84,11 @@ export class InvoiceRetrievalService {
|
||||
this.logger.log(`Retrieved invoice ${invoiceId} for user ${userId}`);
|
||||
return invoice;
|
||||
},
|
||||
this.logger,
|
||||
{
|
||||
criticality: OperationCriticality.CRITICAL,
|
||||
context: `Get invoice ${invoiceId} for user ${userId}`,
|
||||
logger: this.logger,
|
||||
rethrow: [NotFoundException, BadRequestException],
|
||||
fallbackMessage: "Failed to retrieve invoice",
|
||||
}
|
||||
);
|
||||
@ -108,11 +115,13 @@ export class InvoiceRetrievalService {
|
||||
);
|
||||
}
|
||||
|
||||
return withErrorHandling(
|
||||
return safeOperation(
|
||||
async () => this.getInvoices(userId, { page, limit, status }),
|
||||
this.logger,
|
||||
{
|
||||
criticality: OperationCriticality.CRITICAL,
|
||||
context: `Get ${status} invoices for user ${userId}`,
|
||||
logger: this.logger,
|
||||
rethrow: [NotFoundException, BadRequestException],
|
||||
fallbackMessage: "Failed to retrieve invoices",
|
||||
}
|
||||
);
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { Injectable, Inject, BadRequestException } from "@nestjs/common";
|
||||
import { Injectable, Inject, BadRequestException, NotFoundException } from "@nestjs/common";
|
||||
import {
|
||||
subscriptionListSchema,
|
||||
subscriptionStatusSchema,
|
||||
@ -17,7 +17,7 @@ import { WhmcsInvoiceService } from "@bff/integrations/whmcs/services/whmcs-invo
|
||||
import { WhmcsSubscriptionService } from "@bff/integrations/whmcs/services/whmcs-subscription.service.js";
|
||||
import { MappingsService } from "@bff/modules/id-mappings/mappings.service.js";
|
||||
import { Logger } from "nestjs-pino";
|
||||
import { withErrorHandling } from "@bff/core/utils/error-handler.util.js";
|
||||
import { safeOperation, OperationCriticality } from "@bff/core/utils/safe-operation.util.js";
|
||||
import { extractErrorMessage } from "@bff/core/utils/error.util.js";
|
||||
|
||||
export interface GetSubscriptionsOptions {
|
||||
@ -53,7 +53,7 @@ export class SubscriptionsOrchestrator {
|
||||
const { status } = options;
|
||||
const whmcsClientId = await this.mappingsService.getWhmcsClientIdOrThrow(userId);
|
||||
|
||||
return withErrorHandling(
|
||||
return safeOperation(
|
||||
async () => {
|
||||
const subscriptionList = await this.whmcsSubscriptionService.getSubscriptions(
|
||||
whmcsClientId,
|
||||
@ -74,9 +74,11 @@ export class SubscriptionsOrchestrator {
|
||||
totalCount: subscriptions.length,
|
||||
});
|
||||
},
|
||||
this.logger,
|
||||
{
|
||||
criticality: OperationCriticality.CRITICAL,
|
||||
context: `Get subscriptions for user ${userId}`,
|
||||
logger: this.logger,
|
||||
rethrow: [NotFoundException, BadRequestException],
|
||||
fallbackMessage: "Failed to retrieve subscriptions",
|
||||
}
|
||||
);
|
||||
@ -94,7 +96,7 @@ export class SubscriptionsOrchestrator {
|
||||
// Get WHMCS client ID from user mapping
|
||||
const whmcsClientId = await this.mappingsService.getWhmcsClientIdOrThrow(userId);
|
||||
|
||||
return withErrorHandling(
|
||||
return safeOperation(
|
||||
async () => {
|
||||
const subscription = await this.whmcsSubscriptionService.getSubscriptionById(
|
||||
whmcsClientId,
|
||||
@ -111,9 +113,11 @@ export class SubscriptionsOrchestrator {
|
||||
|
||||
return subscription;
|
||||
},
|
||||
this.logger,
|
||||
{
|
||||
criticality: OperationCriticality.CRITICAL,
|
||||
context: `Get subscription ${subscriptionId} for user ${userId}`,
|
||||
logger: this.logger,
|
||||
rethrow: [NotFoundException, BadRequestException],
|
||||
fallbackMessage: "Failed to retrieve subscription",
|
||||
}
|
||||
);
|
||||
@ -164,7 +168,7 @@ export class SubscriptionsOrchestrator {
|
||||
completed: number;
|
||||
cancelled: number;
|
||||
}> {
|
||||
return withErrorHandling(
|
||||
return safeOperation(
|
||||
async () => {
|
||||
const subscriptionList = await this.getSubscriptions(userId);
|
||||
const subscriptions: Subscription[] = subscriptionList.subscriptions;
|
||||
@ -180,9 +184,11 @@ export class SubscriptionsOrchestrator {
|
||||
|
||||
return subscriptionStatsSchema.parse(stats);
|
||||
},
|
||||
this.logger,
|
||||
{
|
||||
criticality: OperationCriticality.CRITICAL,
|
||||
context: `Generate subscription stats for user ${userId}`,
|
||||
logger: this.logger,
|
||||
rethrow: [NotFoundException, BadRequestException],
|
||||
}
|
||||
);
|
||||
}
|
||||
@ -191,7 +197,7 @@ export class SubscriptionsOrchestrator {
|
||||
* Get subscriptions expiring soon (within next 30 days)
|
||||
*/
|
||||
async getExpiringSoon(userId: string, days: number = 30): Promise<Subscription[]> {
|
||||
return withErrorHandling(
|
||||
return safeOperation(
|
||||
async () => {
|
||||
const subscriptionList = await this.getSubscriptions(userId);
|
||||
const subscriptions: Subscription[] = subscriptionList.subscriptions;
|
||||
@ -213,9 +219,11 @@ export class SubscriptionsOrchestrator {
|
||||
);
|
||||
return expiringSoon;
|
||||
},
|
||||
this.logger,
|
||||
{
|
||||
criticality: OperationCriticality.CRITICAL,
|
||||
context: `Get expiring subscriptions for user ${userId}`,
|
||||
logger: this.logger,
|
||||
rethrow: [NotFoundException, BadRequestException],
|
||||
}
|
||||
);
|
||||
}
|
||||
@ -224,7 +232,7 @@ export class SubscriptionsOrchestrator {
|
||||
* Get recent subscription activity (newly created or status changed)
|
||||
*/
|
||||
async getRecentActivity(userId: string, days: number = 30): Promise<Subscription[]> {
|
||||
return withErrorHandling(
|
||||
return safeOperation(
|
||||
async () => {
|
||||
const subscriptionList = await this.getSubscriptions(userId);
|
||||
const subscriptions = subscriptionList.subscriptions;
|
||||
@ -242,9 +250,11 @@ export class SubscriptionsOrchestrator {
|
||||
);
|
||||
return recentActivity;
|
||||
},
|
||||
this.logger,
|
||||
{
|
||||
criticality: OperationCriticality.CRITICAL,
|
||||
context: `Get recent subscription activity for user ${userId}`,
|
||||
logger: this.logger,
|
||||
rethrow: [NotFoundException, BadRequestException],
|
||||
}
|
||||
);
|
||||
}
|
||||
@ -257,7 +267,7 @@ export class SubscriptionsOrchestrator {
|
||||
throw new BadRequestException("Search query must be at least 2 characters long");
|
||||
}
|
||||
|
||||
return withErrorHandling(
|
||||
return safeOperation(
|
||||
async () => {
|
||||
const subscriptionList = await this.getSubscriptions(userId);
|
||||
const subscriptions = subscriptionList.subscriptions;
|
||||
@ -275,9 +285,11 @@ export class SubscriptionsOrchestrator {
|
||||
);
|
||||
return matches;
|
||||
},
|
||||
this.logger,
|
||||
{
|
||||
criticality: OperationCriticality.CRITICAL,
|
||||
context: `Search subscriptions for user ${userId}`,
|
||||
logger: this.logger,
|
||||
rethrow: [NotFoundException, BadRequestException],
|
||||
}
|
||||
);
|
||||
}
|
||||
@ -292,7 +304,7 @@ export class SubscriptionsOrchestrator {
|
||||
): Promise<InvoiceList> {
|
||||
const { page = 1, limit = 10 } = options;
|
||||
|
||||
return withErrorHandling(
|
||||
return safeOperation(
|
||||
async () => {
|
||||
const cachedResult = await this.tryGetCachedInvoices(userId, subscriptionId, page, limit);
|
||||
if (cachedResult) return cachedResult;
|
||||
@ -326,9 +338,11 @@ export class SubscriptionsOrchestrator {
|
||||
|
||||
return result;
|
||||
},
|
||||
this.logger,
|
||||
{
|
||||
criticality: OperationCriticality.CRITICAL,
|
||||
context: `Get invoices for subscription ${subscriptionId}`,
|
||||
logger: this.logger,
|
||||
rethrow: [NotFoundException, BadRequestException],
|
||||
fallbackMessage: "Failed to retrieve subscription invoices",
|
||||
}
|
||||
);
|
||||
|
||||
@ -38,7 +38,7 @@ import { WhmcsClientService } from "@bff/integrations/whmcs/services/whmcs-clien
|
||||
import { WhmcsInvoiceService } from "@bff/integrations/whmcs/services/whmcs-invoice.service.js";
|
||||
import { WhmcsSubscriptionService } from "@bff/integrations/whmcs/services/whmcs-subscription.service.js";
|
||||
import { SalesforceFacade } from "@bff/integrations/salesforce/facades/salesforce.facade.js";
|
||||
import { withErrorHandling } from "@bff/core/utils/error-handler.util.js";
|
||||
import { safeOperation, OperationCriticality } from "@bff/core/utils/safe-operation.util.js";
|
||||
import { parseUuidOrThrow } from "@bff/core/utils/validation.util.js";
|
||||
import { UserAuthRepository } from "./user-auth.repository.js";
|
||||
import { AddressReconcileQueueService } from "../queue/address-reconcile.queue.js";
|
||||
@ -279,7 +279,7 @@ export class UserProfileAggregator {
|
||||
|
||||
const whmcsClientId = await this.mappingsService.getWhmcsClientIdOrThrow(validId);
|
||||
|
||||
return withErrorHandling(
|
||||
return safeOperation(
|
||||
async () => {
|
||||
await this.whmcsClientService.updateClientAddress(whmcsClientId, parsed);
|
||||
await this.whmcsClientService.invalidateUserCache(validId);
|
||||
@ -297,9 +297,11 @@ export class UserProfileAggregator {
|
||||
const refreshedAddress = await this.whmcsClientService.getClientAddress(whmcsClientId);
|
||||
return addressSchema.parse(refreshedAddress ?? {});
|
||||
},
|
||||
this.logger,
|
||||
{
|
||||
criticality: OperationCriticality.CRITICAL,
|
||||
context: `Update address for user ${validId}`,
|
||||
logger: this.logger,
|
||||
rethrow: [NotFoundException, BadRequestException],
|
||||
fallbackMessage: "Unable to update address",
|
||||
}
|
||||
);
|
||||
@ -320,7 +322,7 @@ export class UserProfileAggregator {
|
||||
): Promise<Address> {
|
||||
const validId = parseUuidOrThrow(userId, ERROR_INVALID_USER_ID);
|
||||
|
||||
return withErrorHandling(
|
||||
return safeOperation(
|
||||
async () => {
|
||||
const mapping = await this.mappingsService.findByUserId(validId);
|
||||
if (!mapping?.whmcsClientId) {
|
||||
@ -353,9 +355,11 @@ export class UserProfileAggregator {
|
||||
|
||||
return this.fetchRefreshedAddress(validId, mapping.whmcsClientId);
|
||||
},
|
||||
this.logger,
|
||||
{
|
||||
criticality: OperationCriticality.CRITICAL,
|
||||
context: `Update bilingual address for user ${validId}`,
|
||||
logger: this.logger,
|
||||
rethrow: [NotFoundException, BadRequestException],
|
||||
fallbackMessage: "Unable to update address",
|
||||
}
|
||||
);
|
||||
@ -438,7 +442,7 @@ export class UserProfileAggregator {
|
||||
const validId = parseUuidOrThrow(userId, ERROR_INVALID_USER_ID);
|
||||
const parsed = updateCustomerProfileRequestSchema.parse(update);
|
||||
|
||||
return withErrorHandling(
|
||||
return safeOperation(
|
||||
async () => {
|
||||
// Explicitly disallow name changes from portal
|
||||
if (parsed.firstname !== undefined || parsed.lastname !== undefined) {
|
||||
@ -482,17 +486,22 @@ export class UserProfileAggregator {
|
||||
|
||||
return this.getProfile(validId);
|
||||
},
|
||||
this.logger,
|
||||
{
|
||||
criticality: OperationCriticality.CRITICAL,
|
||||
context: `Update profile for user ${validId}`,
|
||||
logger: this.logger,
|
||||
rethrow: [NotFoundException, BadRequestException],
|
||||
fallbackMessage: "Unable to update profile",
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async getUserSummary(userId: string): Promise<DashboardSummary> {
|
||||
return withErrorHandling(async () => this.buildUserSummary(userId), this.logger, {
|
||||
return safeOperation(async () => this.buildUserSummary(userId), {
|
||||
criticality: OperationCriticality.CRITICAL,
|
||||
context: `Get user summary for ${userId}`,
|
||||
logger: this.logger,
|
||||
rethrow: [NotFoundException, BadRequestException],
|
||||
fallbackMessage: "Unable to retrieve dashboard summary",
|
||||
});
|
||||
}
|
||||
@ -641,7 +650,7 @@ export class UserProfileAggregator {
|
||||
private async getProfileForUser(user: PrismaUser): Promise<User> {
|
||||
const whmcsClientId = await this.mappingsService.getWhmcsClientIdOrThrow(user.id);
|
||||
|
||||
return withErrorHandling(
|
||||
return safeOperation(
|
||||
async () => {
|
||||
const whmcsClient = await this.whmcsClientService.getClientDetails(whmcsClientId);
|
||||
const userAuth = mapPrismaUserToUserAuth(user);
|
||||
@ -676,9 +685,11 @@ export class UserProfileAggregator {
|
||||
gender,
|
||||
};
|
||||
},
|
||||
this.logger,
|
||||
{
|
||||
criticality: OperationCriticality.CRITICAL,
|
||||
context: `Fetch client profile from WHMCS for user ${user.id}`,
|
||||
logger: this.logger,
|
||||
rethrow: [NotFoundException, BadRequestException],
|
||||
fallbackMessage: "Unable to retrieve customer profile from billing system",
|
||||
}
|
||||
);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user