- Replaced multiple global exception filters with a unified exception filter to streamline error handling across the application. - Removed deprecated AuthErrorFilter and GlobalExceptionFilter to reduce redundancy. - Enhanced SupportController to include new endpoints for listing, retrieving, and creating support cases, improving the support case management functionality. - Integrated SalesforceCaseService for better interaction with Salesforce data in support case operations. - Updated support case schemas to align with new requirements and ensure data consistency.
480 lines
14 KiB
TypeScript
480 lines
14 KiB
TypeScript
/**
|
|
* Unified Error Handling for Customer Portal
|
|
*
|
|
* Single source of truth for error codes and user-friendly messages.
|
|
* Used by both BFF (for generating responses) and Portal (for parsing responses).
|
|
*/
|
|
|
|
import { z } from "zod";
|
|
|
|
// ============================================================================
|
|
// Error Categories and Severity
|
|
// ============================================================================
|
|
|
|
export type ErrorCategory =
|
|
| "authentication"
|
|
| "authorization"
|
|
| "validation"
|
|
| "business"
|
|
| "system"
|
|
| "network";
|
|
|
|
export type ErrorSeverity = "low" | "medium" | "high" | "critical";
|
|
|
|
// ============================================================================
|
|
// Error Code Registry
|
|
// ============================================================================
|
|
|
|
/**
|
|
* All error codes used in the application.
|
|
* Format: CATEGORY_NUMBER (e.g., AUTH_001, VAL_001)
|
|
*/
|
|
export const ErrorCode = {
|
|
// Authentication Errors (AUTH_*)
|
|
INVALID_CREDENTIALS: "AUTH_001",
|
|
ACCOUNT_LOCKED: "AUTH_002",
|
|
SESSION_EXPIRED: "AUTH_003",
|
|
TOKEN_INVALID: "AUTH_004",
|
|
TOKEN_REVOKED: "AUTH_005",
|
|
REFRESH_TOKEN_INVALID: "AUTH_006",
|
|
|
|
// Authorization Errors (AUTHZ_*)
|
|
FORBIDDEN: "AUTHZ_001",
|
|
ADMIN_REQUIRED: "AUTHZ_002",
|
|
RESOURCE_ACCESS_DENIED: "AUTHZ_003",
|
|
|
|
// Validation Errors (VAL_*)
|
|
VALIDATION_FAILED: "VAL_001",
|
|
REQUIRED_FIELD_MISSING: "VAL_002",
|
|
INVALID_FORMAT: "VAL_003",
|
|
NOT_FOUND: "VAL_004",
|
|
|
|
// Business Logic Errors (BIZ_*)
|
|
ACCOUNT_EXISTS: "BIZ_001",
|
|
ACCOUNT_ALREADY_LINKED: "BIZ_002",
|
|
CUSTOMER_NOT_FOUND: "BIZ_003",
|
|
ORDER_ALREADY_PROCESSED: "BIZ_004",
|
|
INSUFFICIENT_BALANCE: "BIZ_005",
|
|
SERVICE_UNAVAILABLE: "BIZ_006",
|
|
|
|
// System Errors (SYS_*)
|
|
INTERNAL_ERROR: "SYS_001",
|
|
EXTERNAL_SERVICE_ERROR: "SYS_002",
|
|
DATABASE_ERROR: "SYS_003",
|
|
CONFIGURATION_ERROR: "SYS_004",
|
|
|
|
// Network/Rate Limiting (NET_*)
|
|
NETWORK_ERROR: "NET_001",
|
|
TIMEOUT: "NET_002",
|
|
RATE_LIMITED: "NET_003",
|
|
|
|
// Generic
|
|
UNKNOWN: "GEN_001",
|
|
} as const;
|
|
|
|
export type ErrorCodeType = (typeof ErrorCode)[keyof typeof ErrorCode];
|
|
|
|
// ============================================================================
|
|
// Error Messages (User-Friendly)
|
|
// ============================================================================
|
|
|
|
/**
|
|
* User-friendly error messages for each error code.
|
|
* These are safe to display to end users.
|
|
*/
|
|
export const ErrorMessages: Record<ErrorCodeType, string> = {
|
|
// Authentication
|
|
[ErrorCode.INVALID_CREDENTIALS]: "Invalid email or password. Please try again.",
|
|
[ErrorCode.ACCOUNT_LOCKED]:
|
|
"Your account has been temporarily locked due to multiple failed login attempts. Please try again later.",
|
|
[ErrorCode.SESSION_EXPIRED]: "Your session has expired. Please log in again.",
|
|
[ErrorCode.TOKEN_INVALID]: "Your session is invalid. Please log in again.",
|
|
[ErrorCode.TOKEN_REVOKED]: "Your session has been revoked. Please log in again.",
|
|
[ErrorCode.REFRESH_TOKEN_INVALID]: "Your session could not be refreshed. Please log in again.",
|
|
|
|
// Authorization
|
|
[ErrorCode.FORBIDDEN]: "You do not have permission to perform this action.",
|
|
[ErrorCode.ADMIN_REQUIRED]: "Administrator access is required for this action.",
|
|
[ErrorCode.RESOURCE_ACCESS_DENIED]: "You do not have access to this resource.",
|
|
|
|
// Validation
|
|
[ErrorCode.VALIDATION_FAILED]: "The provided data is invalid. Please check your input.",
|
|
[ErrorCode.REQUIRED_FIELD_MISSING]: "Required information is missing.",
|
|
[ErrorCode.INVALID_FORMAT]: "The data format is invalid.",
|
|
[ErrorCode.NOT_FOUND]: "The requested resource was not found.",
|
|
|
|
// Business Logic
|
|
[ErrorCode.ACCOUNT_EXISTS]: "An account with this email already exists. Please sign in.",
|
|
[ErrorCode.ACCOUNT_ALREADY_LINKED]: "This billing account is already linked. Please sign in.",
|
|
[ErrorCode.CUSTOMER_NOT_FOUND]: "Customer account not found. Please contact support.",
|
|
[ErrorCode.ORDER_ALREADY_PROCESSED]: "This order has already been processed.",
|
|
[ErrorCode.INSUFFICIENT_BALANCE]: "Insufficient account balance.",
|
|
[ErrorCode.SERVICE_UNAVAILABLE]: "This service is temporarily unavailable. Please try again later.",
|
|
|
|
// System
|
|
[ErrorCode.INTERNAL_ERROR]: "An unexpected error occurred. Please try again later.",
|
|
[ErrorCode.EXTERNAL_SERVICE_ERROR]: "An external service is temporarily unavailable. Please try again later.",
|
|
[ErrorCode.DATABASE_ERROR]: "A system error occurred. Please try again later.",
|
|
[ErrorCode.CONFIGURATION_ERROR]: "A system configuration error occurred. Please contact support.",
|
|
|
|
// Network
|
|
[ErrorCode.NETWORK_ERROR]: "Unable to connect to the server. Please check your internet connection.",
|
|
[ErrorCode.TIMEOUT]: "The request timed out. Please try again.",
|
|
[ErrorCode.RATE_LIMITED]: "Too many requests. Please wait a moment and try again.",
|
|
|
|
// Generic
|
|
[ErrorCode.UNKNOWN]: "An unexpected error occurred. Please try again.",
|
|
};
|
|
|
|
// ============================================================================
|
|
// Error Metadata
|
|
// ============================================================================
|
|
|
|
interface ErrorMetadata {
|
|
category: ErrorCategory;
|
|
severity: ErrorSeverity;
|
|
shouldLogout: boolean;
|
|
shouldRetry: boolean;
|
|
logLevel: "error" | "warn" | "info" | "debug";
|
|
}
|
|
|
|
/**
|
|
* Metadata for each error code defining behavior and classification.
|
|
*/
|
|
export const ErrorMetadata: Record<ErrorCodeType, ErrorMetadata> = {
|
|
// Authentication - mostly medium severity, some trigger logout
|
|
[ErrorCode.INVALID_CREDENTIALS]: {
|
|
category: "authentication",
|
|
severity: "medium",
|
|
shouldLogout: false,
|
|
shouldRetry: false,
|
|
logLevel: "warn",
|
|
},
|
|
[ErrorCode.ACCOUNT_LOCKED]: {
|
|
category: "authentication",
|
|
severity: "medium",
|
|
shouldLogout: false,
|
|
shouldRetry: false,
|
|
logLevel: "warn",
|
|
},
|
|
[ErrorCode.SESSION_EXPIRED]: {
|
|
category: "authentication",
|
|
severity: "low",
|
|
shouldLogout: true,
|
|
shouldRetry: false,
|
|
logLevel: "info",
|
|
},
|
|
[ErrorCode.TOKEN_INVALID]: {
|
|
category: "authentication",
|
|
severity: "medium",
|
|
shouldLogout: true,
|
|
shouldRetry: false,
|
|
logLevel: "warn",
|
|
},
|
|
[ErrorCode.TOKEN_REVOKED]: {
|
|
category: "authentication",
|
|
severity: "medium",
|
|
shouldLogout: true,
|
|
shouldRetry: false,
|
|
logLevel: "warn",
|
|
},
|
|
[ErrorCode.REFRESH_TOKEN_INVALID]: {
|
|
category: "authentication",
|
|
severity: "medium",
|
|
shouldLogout: true,
|
|
shouldRetry: false,
|
|
logLevel: "warn",
|
|
},
|
|
|
|
// Authorization
|
|
[ErrorCode.FORBIDDEN]: {
|
|
category: "authorization",
|
|
severity: "medium",
|
|
shouldLogout: false,
|
|
shouldRetry: false,
|
|
logLevel: "warn",
|
|
},
|
|
[ErrorCode.ADMIN_REQUIRED]: {
|
|
category: "authorization",
|
|
severity: "medium",
|
|
shouldLogout: false,
|
|
shouldRetry: false,
|
|
logLevel: "warn",
|
|
},
|
|
[ErrorCode.RESOURCE_ACCESS_DENIED]: {
|
|
category: "authorization",
|
|
severity: "medium",
|
|
shouldLogout: false,
|
|
shouldRetry: false,
|
|
logLevel: "warn",
|
|
},
|
|
|
|
// Validation - low severity, informational
|
|
[ErrorCode.VALIDATION_FAILED]: {
|
|
category: "validation",
|
|
severity: "low",
|
|
shouldLogout: false,
|
|
shouldRetry: false,
|
|
logLevel: "info",
|
|
},
|
|
[ErrorCode.REQUIRED_FIELD_MISSING]: {
|
|
category: "validation",
|
|
severity: "low",
|
|
shouldLogout: false,
|
|
shouldRetry: false,
|
|
logLevel: "info",
|
|
},
|
|
[ErrorCode.INVALID_FORMAT]: {
|
|
category: "validation",
|
|
severity: "low",
|
|
shouldLogout: false,
|
|
shouldRetry: false,
|
|
logLevel: "info",
|
|
},
|
|
[ErrorCode.NOT_FOUND]: {
|
|
category: "validation",
|
|
severity: "low",
|
|
shouldLogout: false,
|
|
shouldRetry: false,
|
|
logLevel: "info",
|
|
},
|
|
|
|
// Business Logic
|
|
[ErrorCode.ACCOUNT_EXISTS]: {
|
|
category: "business",
|
|
severity: "low",
|
|
shouldLogout: false,
|
|
shouldRetry: false,
|
|
logLevel: "info",
|
|
},
|
|
[ErrorCode.ACCOUNT_ALREADY_LINKED]: {
|
|
category: "business",
|
|
severity: "low",
|
|
shouldLogout: false,
|
|
shouldRetry: false,
|
|
logLevel: "info",
|
|
},
|
|
[ErrorCode.CUSTOMER_NOT_FOUND]: {
|
|
category: "business",
|
|
severity: "medium",
|
|
shouldLogout: false,
|
|
shouldRetry: false,
|
|
logLevel: "warn",
|
|
},
|
|
[ErrorCode.ORDER_ALREADY_PROCESSED]: {
|
|
category: "business",
|
|
severity: "low",
|
|
shouldLogout: false,
|
|
shouldRetry: false,
|
|
logLevel: "info",
|
|
},
|
|
[ErrorCode.INSUFFICIENT_BALANCE]: {
|
|
category: "business",
|
|
severity: "low",
|
|
shouldLogout: false,
|
|
shouldRetry: false,
|
|
logLevel: "info",
|
|
},
|
|
[ErrorCode.SERVICE_UNAVAILABLE]: {
|
|
category: "business",
|
|
severity: "medium",
|
|
shouldLogout: false,
|
|
shouldRetry: true,
|
|
logLevel: "warn",
|
|
},
|
|
|
|
// System - high severity
|
|
[ErrorCode.INTERNAL_ERROR]: {
|
|
category: "system",
|
|
severity: "high",
|
|
shouldLogout: false,
|
|
shouldRetry: true,
|
|
logLevel: "error",
|
|
},
|
|
[ErrorCode.EXTERNAL_SERVICE_ERROR]: {
|
|
category: "system",
|
|
severity: "medium",
|
|
shouldLogout: false,
|
|
shouldRetry: true,
|
|
logLevel: "error",
|
|
},
|
|
[ErrorCode.DATABASE_ERROR]: {
|
|
category: "system",
|
|
severity: "critical",
|
|
shouldLogout: false,
|
|
shouldRetry: true,
|
|
logLevel: "error",
|
|
},
|
|
[ErrorCode.CONFIGURATION_ERROR]: {
|
|
category: "system",
|
|
severity: "critical",
|
|
shouldLogout: false,
|
|
shouldRetry: false,
|
|
logLevel: "error",
|
|
},
|
|
|
|
// Network
|
|
[ErrorCode.NETWORK_ERROR]: {
|
|
category: "network",
|
|
severity: "medium",
|
|
shouldLogout: false,
|
|
shouldRetry: true,
|
|
logLevel: "warn",
|
|
},
|
|
[ErrorCode.TIMEOUT]: {
|
|
category: "network",
|
|
severity: "medium",
|
|
shouldLogout: false,
|
|
shouldRetry: true,
|
|
logLevel: "warn",
|
|
},
|
|
[ErrorCode.RATE_LIMITED]: {
|
|
category: "network",
|
|
severity: "low",
|
|
shouldLogout: false,
|
|
shouldRetry: true,
|
|
logLevel: "warn",
|
|
},
|
|
|
|
// Generic
|
|
[ErrorCode.UNKNOWN]: {
|
|
category: "system",
|
|
severity: "medium",
|
|
shouldLogout: false,
|
|
shouldRetry: true,
|
|
logLevel: "error",
|
|
},
|
|
};
|
|
|
|
// ============================================================================
|
|
// Helper Functions
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Get user-friendly message for an error code
|
|
*/
|
|
export function getErrorMessage(code: string): string {
|
|
return ErrorMessages[code as ErrorCodeType] ?? ErrorMessages[ErrorCode.UNKNOWN];
|
|
}
|
|
|
|
/**
|
|
* Get metadata for an error code
|
|
*/
|
|
export function getErrorMetadata(code: string): ErrorMetadata {
|
|
return ErrorMetadata[code as ErrorCodeType] ?? ErrorMetadata[ErrorCode.UNKNOWN];
|
|
}
|
|
|
|
/**
|
|
* Check if error code should trigger logout
|
|
*/
|
|
export function shouldLogoutForCode(code: string): boolean {
|
|
return getErrorMetadata(code).shouldLogout;
|
|
}
|
|
|
|
/**
|
|
* Check if error can be retried
|
|
*/
|
|
export function canRetryError(code: string): boolean {
|
|
return getErrorMetadata(code).shouldRetry;
|
|
}
|
|
|
|
// ============================================================================
|
|
// Pattern Matching for Error Classification
|
|
// ============================================================================
|
|
|
|
interface ErrorPattern {
|
|
pattern: RegExp;
|
|
code: ErrorCodeType;
|
|
}
|
|
|
|
/**
|
|
* Patterns to match error messages to error codes.
|
|
* Used when explicit error codes are not available.
|
|
*/
|
|
export const ErrorPatterns: ErrorPattern[] = [
|
|
// Authentication patterns
|
|
{ pattern: /invalid.*credentials?|wrong.*password|invalid.*password/i, code: ErrorCode.INVALID_CREDENTIALS },
|
|
{ pattern: /account.*locked|locked.*account|too.*many.*attempts/i, code: ErrorCode.ACCOUNT_LOCKED },
|
|
{ pattern: /session.*expired|expired.*session/i, code: ErrorCode.SESSION_EXPIRED },
|
|
{ pattern: /token.*expired|expired.*token/i, code: ErrorCode.SESSION_EXPIRED },
|
|
{ pattern: /token.*revoked|revoked.*token/i, code: ErrorCode.TOKEN_REVOKED },
|
|
{ pattern: /invalid.*token|token.*invalid/i, code: ErrorCode.TOKEN_INVALID },
|
|
{ pattern: /refresh.*token.*invalid|invalid.*refresh/i, code: ErrorCode.REFRESH_TOKEN_INVALID },
|
|
|
|
// Authorization patterns
|
|
{ pattern: /admin.*required|requires?.*admin/i, code: ErrorCode.ADMIN_REQUIRED },
|
|
{ pattern: /forbidden|not.*authorized|unauthorized/i, code: ErrorCode.FORBIDDEN },
|
|
{ pattern: /access.*denied|permission.*denied/i, code: ErrorCode.RESOURCE_ACCESS_DENIED },
|
|
|
|
// Business patterns
|
|
{ pattern: /already.*exists|email.*exists|account.*exists/i, code: ErrorCode.ACCOUNT_EXISTS },
|
|
{ pattern: /already.*linked/i, code: ErrorCode.ACCOUNT_ALREADY_LINKED },
|
|
{ pattern: /customer.*not.*found|account.*not.*found/i, code: ErrorCode.CUSTOMER_NOT_FOUND },
|
|
{ pattern: /already.*processed/i, code: ErrorCode.ORDER_ALREADY_PROCESSED },
|
|
{ pattern: /insufficient.*balance/i, code: ErrorCode.INSUFFICIENT_BALANCE },
|
|
|
|
// System patterns
|
|
{ pattern: /database|sql|postgres|prisma|connection.*refused/i, code: ErrorCode.DATABASE_ERROR },
|
|
{ pattern: /whmcs|salesforce|external.*service/i, code: ErrorCode.EXTERNAL_SERVICE_ERROR },
|
|
{ pattern: /configuration.*error|missing.*config/i, code: ErrorCode.CONFIGURATION_ERROR },
|
|
|
|
// Network patterns
|
|
{ pattern: /network.*error|fetch.*failed|econnrefused/i, code: ErrorCode.NETWORK_ERROR },
|
|
{ pattern: /timeout|timed?\s*out/i, code: ErrorCode.TIMEOUT },
|
|
{ pattern: /too.*many.*requests|rate.*limit/i, code: ErrorCode.RATE_LIMITED },
|
|
|
|
// Validation patterns (lower priority - checked last)
|
|
{ pattern: /not.*found/i, code: ErrorCode.NOT_FOUND },
|
|
{ pattern: /validation.*failed|invalid/i, code: ErrorCode.VALIDATION_FAILED },
|
|
{ pattern: /required|missing/i, code: ErrorCode.REQUIRED_FIELD_MISSING },
|
|
];
|
|
|
|
/**
|
|
* Match an error message to an error code using patterns
|
|
*/
|
|
export function matchErrorPattern(message: string): ErrorCodeType {
|
|
for (const { pattern, code } of ErrorPatterns) {
|
|
if (pattern.test(message)) {
|
|
return code;
|
|
}
|
|
}
|
|
return ErrorCode.UNKNOWN;
|
|
}
|
|
|
|
// ============================================================================
|
|
// Zod Schema for Error Response
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Schema for standard API error response
|
|
*/
|
|
export const apiErrorSchema = z.object({
|
|
success: z.literal(false),
|
|
error: z.object({
|
|
code: z.string(),
|
|
message: z.string(),
|
|
details: z.record(z.string(), z.unknown()).optional(),
|
|
}),
|
|
});
|
|
|
|
export type ApiError = z.infer<typeof apiErrorSchema>;
|
|
|
|
/**
|
|
* Create a standard error response object
|
|
*/
|
|
export function createErrorResponse(
|
|
code: ErrorCodeType,
|
|
customMessage?: string,
|
|
details?: Record<string, unknown>
|
|
): ApiError {
|
|
return {
|
|
success: false,
|
|
error: {
|
|
code,
|
|
message: customMessage ?? getErrorMessage(code),
|
|
details,
|
|
},
|
|
};
|
|
}
|
|
|