/** * 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", LEGACY_ACCOUNT_EXISTS: "BIZ_007", // 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 = { // 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]: "We couldn't create a new account with these details. Please sign in or contact support.", [ErrorCode.ACCOUNT_ALREADY_LINKED]: "This billing account is already linked to a portal account. Please sign in.", [ErrorCode.LEGACY_ACCOUNT_EXISTS]: "We couldn't create a new account with these details. Please transfer your account or contact support.", [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 = { // 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, // Session expiry is an expected flow (browser tabs, refresh loops, etc.) and can be extremely noisy. // Keep it available for debugging but avoid spamming production logs at info level. logLevel: "debug", }, [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.LEGACY_ACCOUNT_EXISTS]: { 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; } // NOTE: We intentionally do NOT support matching error messages to codes. // Error codes must be explicit and stable (returned from the API or thrown by server code). // ============================================================================ // 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; /** * Create a standard error response object */ export function createErrorResponse( code: ErrorCodeType, customMessage?: string, details?: Record ): ApiError { return { success: false, error: { code, message: customMessage ?? getErrorMessage(code), details, }, }; }