refactor: add typed provider error classes replacing string matching
Add structured error code enums to domain package for WHMCS, Salesforce, and Freebit providers. Create BaseProviderError and typed error classes for each provider. Update UnifiedExceptionFilter to handle provider errors. Migrate all three error handler services from DomainHttpException with brittle string matching to typed error classes with instanceof checks.
This commit is contained in:
parent
7da032fd95
commit
2d076cf6d4
@ -0,0 +1,24 @@
|
||||
import type { HttpStatus } from "@nestjs/common";
|
||||
import type { ErrorCodeType } from "@customer-portal/domain/common";
|
||||
|
||||
/**
|
||||
* Base class for all provider-specific errors.
|
||||
*
|
||||
* Carries structured metadata for error classification without string matching.
|
||||
* The UnifiedExceptionFilter detects subclasses and maps them to API responses.
|
||||
*/
|
||||
export abstract class BaseProviderError extends Error {
|
||||
abstract readonly provider: "whmcs" | "salesforce" | "freebit";
|
||||
abstract readonly providerCode: string;
|
||||
abstract readonly domainErrorCode: ErrorCodeType;
|
||||
abstract readonly httpStatus: HttpStatus;
|
||||
|
||||
constructor(
|
||||
message: string,
|
||||
public readonly providerMessage: string,
|
||||
public override readonly cause?: unknown
|
||||
) {
|
||||
super(message);
|
||||
this.name = this.constructor.name;
|
||||
}
|
||||
}
|
||||
36
apps/bff/src/integrations/common/errors/freebit.errors.ts
Normal file
36
apps/bff/src/integrations/common/errors/freebit.errors.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import { HttpStatus } from "@nestjs/common";
|
||||
import { ErrorCode, FreebitProviderError } from "@customer-portal/domain/common";
|
||||
import { BaseProviderError } from "./base-provider.error.js";
|
||||
|
||||
export class FreebitApiError extends BaseProviderError {
|
||||
readonly provider = "freebit" as const;
|
||||
readonly providerCode = FreebitProviderError.API_ERROR;
|
||||
readonly domainErrorCode = ErrorCode.EXTERNAL_SERVICE_ERROR;
|
||||
readonly httpStatus = HttpStatus.BAD_GATEWAY;
|
||||
|
||||
constructor(message: string, providerMessage: string, cause?: unknown) {
|
||||
super(message, providerMessage, cause);
|
||||
}
|
||||
}
|
||||
|
||||
export class FreebitAccountNotFoundError extends BaseProviderError {
|
||||
readonly provider = "freebit" as const;
|
||||
readonly providerCode = FreebitProviderError.ACCOUNT_NOT_FOUND;
|
||||
readonly domainErrorCode = ErrorCode.NOT_FOUND;
|
||||
readonly httpStatus = HttpStatus.NOT_FOUND;
|
||||
|
||||
constructor(providerMessage: string, cause?: unknown) {
|
||||
super("Freebit account not found", providerMessage, cause);
|
||||
}
|
||||
}
|
||||
|
||||
export class FreebitTimeoutError extends BaseProviderError {
|
||||
readonly provider = "freebit" as const;
|
||||
readonly providerCode = FreebitProviderError.TIMEOUT;
|
||||
readonly domainErrorCode = ErrorCode.TIMEOUT;
|
||||
readonly httpStatus = HttpStatus.GATEWAY_TIMEOUT;
|
||||
|
||||
constructor(providerMessage: string, cause?: unknown) {
|
||||
super("Freebit request timed out", providerMessage, cause);
|
||||
}
|
||||
}
|
||||
4
apps/bff/src/integrations/common/errors/index.ts
Normal file
4
apps/bff/src/integrations/common/errors/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export { BaseProviderError } from "./base-provider.error.js";
|
||||
export * from "./whmcs.errors.js";
|
||||
export * from "./salesforce.errors.js";
|
||||
export * from "./freebit.errors.js";
|
||||
69
apps/bff/src/integrations/common/errors/salesforce.errors.ts
Normal file
69
apps/bff/src/integrations/common/errors/salesforce.errors.ts
Normal file
@ -0,0 +1,69 @@
|
||||
import { HttpStatus } from "@nestjs/common";
|
||||
import { ErrorCode, SalesforceProviderError } from "@customer-portal/domain/common";
|
||||
import { BaseProviderError } from "./base-provider.error.js";
|
||||
|
||||
export class SalesforceApiError extends BaseProviderError {
|
||||
readonly provider = "salesforce" as const;
|
||||
readonly providerCode = SalesforceProviderError.API_ERROR;
|
||||
readonly domainErrorCode = ErrorCode.EXTERNAL_SERVICE_ERROR;
|
||||
readonly httpStatus = HttpStatus.BAD_GATEWAY;
|
||||
|
||||
constructor(message: string, providerMessage: string, cause?: unknown) {
|
||||
super(message, providerMessage, cause);
|
||||
}
|
||||
}
|
||||
|
||||
export class SalesforceSessionExpiredError extends BaseProviderError {
|
||||
readonly provider = "salesforce" as const;
|
||||
readonly providerCode = SalesforceProviderError.SESSION_EXPIRED;
|
||||
readonly domainErrorCode = ErrorCode.EXTERNAL_SERVICE_ERROR;
|
||||
readonly httpStatus = HttpStatus.SERVICE_UNAVAILABLE;
|
||||
|
||||
constructor(providerMessage: string, cause?: unknown) {
|
||||
super("Salesforce session expired", providerMessage, cause);
|
||||
}
|
||||
}
|
||||
|
||||
export class SalesforceQueryError extends BaseProviderError {
|
||||
readonly provider = "salesforce" as const;
|
||||
readonly providerCode = SalesforceProviderError.QUERY_ERROR;
|
||||
readonly domainErrorCode = ErrorCode.EXTERNAL_SERVICE_ERROR;
|
||||
readonly httpStatus = HttpStatus.BAD_GATEWAY;
|
||||
|
||||
constructor(providerMessage: string, cause?: unknown) {
|
||||
super("Salesforce query failed", providerMessage, cause);
|
||||
}
|
||||
}
|
||||
|
||||
export class SalesforceTimeoutError extends BaseProviderError {
|
||||
readonly provider = "salesforce" as const;
|
||||
readonly providerCode = SalesforceProviderError.TIMEOUT;
|
||||
readonly domainErrorCode = ErrorCode.TIMEOUT;
|
||||
readonly httpStatus = HttpStatus.GATEWAY_TIMEOUT;
|
||||
|
||||
constructor(providerMessage: string, cause?: unknown) {
|
||||
super("Salesforce request timed out", providerMessage, cause);
|
||||
}
|
||||
}
|
||||
|
||||
export class SalesforceNetworkError extends BaseProviderError {
|
||||
readonly provider = "salesforce" as const;
|
||||
readonly providerCode = SalesforceProviderError.NETWORK_ERROR;
|
||||
readonly domainErrorCode = ErrorCode.NETWORK_ERROR;
|
||||
readonly httpStatus = HttpStatus.BAD_GATEWAY;
|
||||
|
||||
constructor(providerMessage: string, cause?: unknown) {
|
||||
super("Salesforce network error", providerMessage, cause);
|
||||
}
|
||||
}
|
||||
|
||||
export class SalesforceRateLimitError extends BaseProviderError {
|
||||
readonly provider = "salesforce" as const;
|
||||
readonly providerCode = SalesforceProviderError.RATE_LIMITED;
|
||||
readonly domainErrorCode = ErrorCode.RATE_LIMITED;
|
||||
readonly httpStatus = HttpStatus.TOO_MANY_REQUESTS;
|
||||
|
||||
constructor(providerMessage: string, cause?: unknown) {
|
||||
super("Salesforce rate limit exceeded", providerMessage, cause);
|
||||
}
|
||||
}
|
||||
121
apps/bff/src/integrations/common/errors/whmcs.errors.ts
Normal file
121
apps/bff/src/integrations/common/errors/whmcs.errors.ts
Normal file
@ -0,0 +1,121 @@
|
||||
import { HttpStatus } from "@nestjs/common";
|
||||
import { ErrorCode, type ErrorCodeType } from "@customer-portal/domain/common";
|
||||
import { WhmcsProviderError } from "@customer-portal/domain/common";
|
||||
import { BaseProviderError } from "./base-provider.error.js";
|
||||
|
||||
export class WhmcsApiError extends BaseProviderError {
|
||||
readonly provider = "whmcs" as const;
|
||||
readonly providerCode = WhmcsProviderError.API_ERROR;
|
||||
readonly domainErrorCode = ErrorCode.EXTERNAL_SERVICE_ERROR;
|
||||
readonly httpStatus = HttpStatus.BAD_GATEWAY;
|
||||
|
||||
constructor(message: string, providerMessage: string, cause?: unknown) {
|
||||
super(message, providerMessage, cause);
|
||||
}
|
||||
}
|
||||
|
||||
export class WhmcsNotFoundError extends BaseProviderError {
|
||||
readonly provider = "whmcs" as const;
|
||||
readonly providerCode: string;
|
||||
readonly domainErrorCode = ErrorCode.NOT_FOUND;
|
||||
readonly httpStatus = HttpStatus.NOT_FOUND;
|
||||
|
||||
constructor(
|
||||
resource: "client" | "invoice" | "product",
|
||||
providerMessage: string,
|
||||
cause?: unknown
|
||||
) {
|
||||
super(`WHMCS ${resource} not found`, providerMessage, cause);
|
||||
const codeMap = {
|
||||
client: WhmcsProviderError.CLIENT_NOT_FOUND,
|
||||
invoice: WhmcsProviderError.INVOICE_NOT_FOUND,
|
||||
product: WhmcsProviderError.PRODUCT_NOT_FOUND,
|
||||
} as const;
|
||||
this.providerCode = codeMap[resource];
|
||||
}
|
||||
}
|
||||
|
||||
export class WhmcsAuthError extends BaseProviderError {
|
||||
readonly provider = "whmcs" as const;
|
||||
readonly providerCode = WhmcsProviderError.AUTH_FAILED;
|
||||
readonly domainErrorCode = ErrorCode.EXTERNAL_SERVICE_ERROR;
|
||||
readonly httpStatus = HttpStatus.SERVICE_UNAVAILABLE;
|
||||
|
||||
constructor(providerMessage: string, cause?: unknown) {
|
||||
super("WHMCS authentication failed", providerMessage, cause);
|
||||
}
|
||||
}
|
||||
|
||||
export class WhmcsInvalidCredentialsError extends BaseProviderError {
|
||||
readonly provider = "whmcs" as const;
|
||||
readonly providerCode = WhmcsProviderError.INVALID_CREDENTIALS;
|
||||
readonly domainErrorCode = ErrorCode.INVALID_CREDENTIALS;
|
||||
readonly httpStatus = HttpStatus.UNAUTHORIZED;
|
||||
|
||||
constructor(providerMessage: string, cause?: unknown) {
|
||||
super("Invalid login credentials", providerMessage, cause);
|
||||
}
|
||||
}
|
||||
|
||||
export class WhmcsValidationError extends BaseProviderError {
|
||||
readonly provider = "whmcs" as const;
|
||||
readonly providerCode = WhmcsProviderError.VALIDATION_ERROR;
|
||||
readonly domainErrorCode = ErrorCode.VALIDATION_FAILED;
|
||||
readonly httpStatus = HttpStatus.BAD_REQUEST;
|
||||
|
||||
constructor(providerMessage: string, cause?: unknown) {
|
||||
super("WHMCS validation failed", providerMessage, cause);
|
||||
}
|
||||
}
|
||||
|
||||
export class WhmcsTimeoutError extends BaseProviderError {
|
||||
readonly provider = "whmcs" as const;
|
||||
readonly providerCode = WhmcsProviderError.TIMEOUT;
|
||||
readonly domainErrorCode = ErrorCode.TIMEOUT;
|
||||
readonly httpStatus = HttpStatus.GATEWAY_TIMEOUT;
|
||||
|
||||
constructor(providerMessage: string, cause?: unknown) {
|
||||
super("WHMCS request timed out", providerMessage, cause);
|
||||
}
|
||||
}
|
||||
|
||||
export class WhmcsNetworkError extends BaseProviderError {
|
||||
readonly provider = "whmcs" as const;
|
||||
readonly providerCode = WhmcsProviderError.NETWORK_ERROR;
|
||||
readonly domainErrorCode = ErrorCode.NETWORK_ERROR;
|
||||
readonly httpStatus = HttpStatus.BAD_GATEWAY;
|
||||
|
||||
constructor(providerMessage: string, cause?: unknown) {
|
||||
super("WHMCS network error", providerMessage, cause);
|
||||
}
|
||||
}
|
||||
|
||||
export class WhmcsRateLimitError extends BaseProviderError {
|
||||
readonly provider = "whmcs" as const;
|
||||
readonly providerCode = WhmcsProviderError.RATE_LIMITED;
|
||||
readonly domainErrorCode = ErrorCode.RATE_LIMITED;
|
||||
readonly httpStatus = HttpStatus.TOO_MANY_REQUESTS;
|
||||
|
||||
constructor(providerMessage: string, cause?: unknown) {
|
||||
super("WHMCS rate limit exceeded", providerMessage, cause);
|
||||
}
|
||||
}
|
||||
|
||||
export class WhmcsHttpError extends BaseProviderError {
|
||||
readonly provider = "whmcs" as const;
|
||||
readonly providerCode = WhmcsProviderError.HTTP_ERROR;
|
||||
readonly domainErrorCode: ErrorCodeType;
|
||||
readonly httpStatus: HttpStatus;
|
||||
|
||||
constructor(
|
||||
upstreamStatus: number,
|
||||
providerMessage: string,
|
||||
domainErrorCode: ErrorCodeType,
|
||||
httpStatus: HttpStatus,
|
||||
cause?: unknown
|
||||
) {
|
||||
super(`WHMCS HTTP ${upstreamStatus} error`, providerMessage, cause);
|
||||
this.domainErrorCode = domainErrorCode;
|
||||
this.httpStatus = httpStatus;
|
||||
}
|
||||
}
|
||||
61
packages/domain/common/provider-errors.ts
Normal file
61
packages/domain/common/provider-errors.ts
Normal file
@ -0,0 +1,61 @@
|
||||
/**
|
||||
* Provider-specific error codes for structured error detection.
|
||||
*
|
||||
* These codes identify known error conditions from external providers.
|
||||
* Used by provider error classes in the BFF to replace brittle string matching.
|
||||
*/
|
||||
|
||||
export const WhmcsProviderError = {
|
||||
// Authentication
|
||||
AUTH_FAILED: "WHMCS_AUTH_FAILED",
|
||||
INVALID_CREDENTIALS: "WHMCS_INVALID_CREDENTIALS",
|
||||
|
||||
// Not found
|
||||
CLIENT_NOT_FOUND: "WHMCS_CLIENT_NOT_FOUND",
|
||||
INVOICE_NOT_FOUND: "WHMCS_INVOICE_NOT_FOUND",
|
||||
PRODUCT_NOT_FOUND: "WHMCS_PRODUCT_NOT_FOUND",
|
||||
|
||||
// Validation
|
||||
VALIDATION_ERROR: "WHMCS_VALIDATION_ERROR",
|
||||
|
||||
// Network/Infrastructure
|
||||
TIMEOUT: "WHMCS_TIMEOUT",
|
||||
NETWORK_ERROR: "WHMCS_NETWORK_ERROR",
|
||||
RATE_LIMITED: "WHMCS_RATE_LIMITED",
|
||||
HTTP_ERROR: "WHMCS_HTTP_ERROR",
|
||||
|
||||
// Generic
|
||||
API_ERROR: "WHMCS_API_ERROR",
|
||||
} as const;
|
||||
|
||||
export type WhmcsProviderErrorCode = (typeof WhmcsProviderError)[keyof typeof WhmcsProviderError];
|
||||
|
||||
export const SalesforceProviderError = {
|
||||
// Authentication
|
||||
SESSION_EXPIRED: "SF_SESSION_EXPIRED",
|
||||
AUTH_FAILED: "SF_AUTH_FAILED",
|
||||
|
||||
// Query
|
||||
QUERY_ERROR: "SF_QUERY_ERROR",
|
||||
RECORD_NOT_FOUND: "SF_RECORD_NOT_FOUND",
|
||||
|
||||
// Network/Infrastructure
|
||||
TIMEOUT: "SF_TIMEOUT",
|
||||
NETWORK_ERROR: "SF_NETWORK_ERROR",
|
||||
RATE_LIMITED: "SF_RATE_LIMITED",
|
||||
|
||||
// Generic
|
||||
API_ERROR: "SF_API_ERROR",
|
||||
} as const;
|
||||
|
||||
export type SalesforceProviderErrorCode =
|
||||
(typeof SalesforceProviderError)[keyof typeof SalesforceProviderError];
|
||||
|
||||
export const FreebitProviderError = {
|
||||
ACCOUNT_NOT_FOUND: "FREEBIT_ACCOUNT_NOT_FOUND",
|
||||
TIMEOUT: "FREEBIT_TIMEOUT",
|
||||
API_ERROR: "FREEBIT_API_ERROR",
|
||||
} as const;
|
||||
|
||||
export type FreebitProviderErrorCode =
|
||||
(typeof FreebitProviderError)[keyof typeof FreebitProviderError];
|
||||
Loading…
x
Reference in New Issue
Block a user