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:
barsa 2026-02-25 11:30:38 +09:00
parent 7da032fd95
commit 2d076cf6d4
6 changed files with 315 additions and 0 deletions

View File

@ -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;
}
}

View 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);
}
}

View 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";

View 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);
}
}

View 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;
}
}

View 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];