barsa b206de8dba refactor: enterprise-grade cleanup of BFF and domain packages
Comprehensive refactoring across 70 files (net -298 lines) improving
type safety, error handling, and code organization:

- Replace .passthrough()/.catchall(z.unknown()) with .strip() in all Zod schemas
- Tighten Record<string, unknown> to bounded union types where possible
- Replace throw new Error with domain-specific exceptions (OrderException,
  FulfillmentException, WhmcsOperationException, SalesforceOperationException, etc.)
- Split AuthTokenService (625 lines) into TokenGeneratorService and
  TokenRefreshService with thin orchestrator
- Deduplicate FreebitClientService with shared makeRequest() method
- Add typed interfaces to WHMCS facade, order service, and fulfillment mapper
- Externalize hardcoded config values to ConfigService with env fallbacks
- Consolidate duplicate billing cycle enums into shared billingCycleSchema
- Standardize logger usage (nestjs-pino @Inject(Logger) everywhere)
- Move shared WHMCS number coercion helpers to whmcs-utils/schema.ts
2026-02-24 19:05:30 +09:00

702 lines
20 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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",
ACCOUNT_MAPPING_MISSING: "BIZ_008",
// 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",
// SIM Errors (SIM_*)
SIM_ACTIVATION_FEE_REQUIRED: "SIM_001",
SIM_NOT_FOUND: "SIM_002",
SIM_PLAN_CHANGE_FAILED: "SIM_003",
SIM_TOPUP_FAILED: "SIM_004",
SIM_ACTIVATION_FAILED: "SIM_005",
SIM_CANCELLATION_FAILED: "SIM_006",
// Internet Errors (INT_*)
INTERNET_ELIGIBILITY_NOT_REQUESTED: "INT_001",
INTERNET_ELIGIBILITY_PENDING: "INT_002",
INTERNET_INELIGIBLE: "INT_003",
INTERNET_SERVICE_EXISTS: "INT_004",
INTERNET_CHECK_FAILED: "INT_005",
// Order Validation Errors (ORD_*)
USER_MAPPING_NOT_FOUND: "ORD_001",
WHMCS_CLIENT_NOT_LINKED: "ORD_002",
NO_PAYMENT_METHOD: "ORD_003",
INVALID_SKU: "ORD_004",
RESIDENCE_CARD_NOT_SUBMITTED: "ORD_005",
RESIDENCE_CARD_REJECTED: "ORD_006",
ORDER_VALIDATION_FAILED: "ORD_007",
FULFILLMENT_FAILED: "ORD_008",
// Subscription Errors (SUB_*)
SUBSCRIPTION_NOT_FOUND: "SUB_001",
SUBSCRIPTION_CANCELLATION_FAILED: "SUB_002",
SUBSCRIPTION_UPDATE_FAILED: "SUB_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]:
"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.",
[ErrorCode.ACCOUNT_MAPPING_MISSING]:
"Your account isnt fully set up yet. Please contact support or 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.",
// SIM
[ErrorCode.SIM_ACTIVATION_FEE_REQUIRED]: "SIM orders require an activation fee.",
[ErrorCode.SIM_NOT_FOUND]: "SIM subscription not found.",
[ErrorCode.SIM_PLAN_CHANGE_FAILED]: "Failed to change SIM plan. Please try again.",
[ErrorCode.SIM_TOPUP_FAILED]: "Failed to top up SIM data. Please try again.",
[ErrorCode.SIM_ACTIVATION_FAILED]: "SIM activation failed. Please contact support.",
[ErrorCode.SIM_CANCELLATION_FAILED]: "SIM cancellation failed. Please try again.",
// Internet
[ErrorCode.INTERNET_ELIGIBILITY_NOT_REQUESTED]:
"Internet eligibility review is required before ordering.",
[ErrorCode.INTERNET_ELIGIBILITY_PENDING]:
"Internet eligibility review is still in progress. Please wait for review to complete.",
[ErrorCode.INTERNET_INELIGIBLE]:
"Internet service is not available for your address. Please contact support.",
[ErrorCode.INTERNET_SERVICE_EXISTS]:
"An active Internet service already exists for this account.",
[ErrorCode.INTERNET_CHECK_FAILED]: "Unable to verify Internet eligibility. Please try again.",
// Order Validation
[ErrorCode.USER_MAPPING_NOT_FOUND]: "User account mapping is required before ordering.",
[ErrorCode.WHMCS_CLIENT_NOT_LINKED]: "Billing system integration is required before ordering.",
[ErrorCode.NO_PAYMENT_METHOD]: "A payment method is required before ordering.",
[ErrorCode.INVALID_SKU]: "One or more products in your order are invalid.",
[ErrorCode.RESIDENCE_CARD_NOT_SUBMITTED]: "Residence card submission is required for SIM orders.",
[ErrorCode.RESIDENCE_CARD_REJECTED]:
"Your residence card was rejected. Please resubmit and try again.",
[ErrorCode.ORDER_VALIDATION_FAILED]: "Order validation failed. Please check your order details.",
[ErrorCode.FULFILLMENT_FAILED]: "Order fulfillment failed. Please contact support.",
// Subscription
[ErrorCode.SUBSCRIPTION_NOT_FOUND]: "Subscription not found.",
[ErrorCode.SUBSCRIPTION_CANCELLATION_FAILED]:
"Subscription cancellation failed. Please try again.",
[ErrorCode.SUBSCRIPTION_UPDATE_FAILED]: "Subscription update failed. Please 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,
// 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",
},
[ErrorCode.ACCOUNT_MAPPING_MISSING]: {
category: "business",
severity: "medium",
shouldLogout: false,
shouldRetry: false,
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",
},
// SIM Errors
[ErrorCode.SIM_ACTIVATION_FEE_REQUIRED]: {
category: "validation",
severity: "low",
shouldLogout: false,
shouldRetry: false,
logLevel: "info",
},
[ErrorCode.SIM_NOT_FOUND]: {
category: "validation",
severity: "low",
shouldLogout: false,
shouldRetry: false,
logLevel: "warn",
},
[ErrorCode.SIM_PLAN_CHANGE_FAILED]: {
category: "business",
severity: "medium",
shouldLogout: false,
shouldRetry: true,
logLevel: "error",
},
[ErrorCode.SIM_TOPUP_FAILED]: {
category: "business",
severity: "medium",
shouldLogout: false,
shouldRetry: true,
logLevel: "error",
},
[ErrorCode.SIM_ACTIVATION_FAILED]: {
category: "business",
severity: "medium",
shouldLogout: false,
shouldRetry: true,
logLevel: "error",
},
[ErrorCode.SIM_CANCELLATION_FAILED]: {
category: "business",
severity: "medium",
shouldLogout: false,
shouldRetry: true,
logLevel: "error",
},
// Internet Errors
[ErrorCode.INTERNET_ELIGIBILITY_NOT_REQUESTED]: {
category: "validation",
severity: "low",
shouldLogout: false,
shouldRetry: false,
logLevel: "info",
},
[ErrorCode.INTERNET_ELIGIBILITY_PENDING]: {
category: "validation",
severity: "low",
shouldLogout: false,
shouldRetry: false,
logLevel: "info",
},
[ErrorCode.INTERNET_INELIGIBLE]: {
category: "validation",
severity: "low",
shouldLogout: false,
shouldRetry: false,
logLevel: "info",
},
[ErrorCode.INTERNET_SERVICE_EXISTS]: {
category: "validation",
severity: "low",
shouldLogout: false,
shouldRetry: false,
logLevel: "warn",
},
[ErrorCode.INTERNET_CHECK_FAILED]: {
category: "system",
severity: "medium",
shouldLogout: false,
shouldRetry: true,
logLevel: "error",
},
// Order Validation Errors
[ErrorCode.USER_MAPPING_NOT_FOUND]: {
category: "validation",
severity: "low",
shouldLogout: false,
shouldRetry: false,
logLevel: "warn",
},
[ErrorCode.WHMCS_CLIENT_NOT_LINKED]: {
category: "validation",
severity: "low",
shouldLogout: false,
shouldRetry: false,
logLevel: "warn",
},
[ErrorCode.NO_PAYMENT_METHOD]: {
category: "validation",
severity: "low",
shouldLogout: false,
shouldRetry: false,
logLevel: "info",
},
[ErrorCode.INVALID_SKU]: {
category: "validation",
severity: "low",
shouldLogout: false,
shouldRetry: false,
logLevel: "warn",
},
[ErrorCode.RESIDENCE_CARD_NOT_SUBMITTED]: {
category: "validation",
severity: "low",
shouldLogout: false,
shouldRetry: false,
logLevel: "info",
},
[ErrorCode.RESIDENCE_CARD_REJECTED]: {
category: "validation",
severity: "low",
shouldLogout: false,
shouldRetry: false,
logLevel: "warn",
},
[ErrorCode.ORDER_VALIDATION_FAILED]: {
category: "validation",
severity: "low",
shouldLogout: false,
shouldRetry: false,
logLevel: "warn",
},
[ErrorCode.FULFILLMENT_FAILED]: {
category: "system",
severity: "high",
shouldLogout: false,
shouldRetry: true,
logLevel: "error",
},
// Subscription Errors
[ErrorCode.SUBSCRIPTION_NOT_FOUND]: {
category: "validation",
severity: "low",
shouldLogout: false,
shouldRetry: false,
logLevel: "warn",
},
[ErrorCode.SUBSCRIPTION_CANCELLATION_FAILED]: {
category: "business",
severity: "medium",
shouldLogout: false,
shouldRetry: true,
logLevel: "error",
},
[ErrorCode.SUBSCRIPTION_UPDATE_FAILED]: {
category: "business",
severity: "medium",
shouldLogout: false,
shouldRetry: true,
logLevel: "error",
},
// Generic
[ErrorCode.UNKNOWN]: {
category: "system",
severity: "medium",
shouldLogout: false,
shouldRetry: true,
logLevel: "error",
},
};
// ============================================================================
// Helper Functions
// ============================================================================
/**
* Map HTTP status codes to domain error codes.
* Shared between BFF (for responses) and Portal (for parsing).
*/
export function mapHttpStatusToErrorCode(status?: number): ErrorCodeType {
if (!status) return ErrorCode.UNKNOWN;
switch (status) {
case 401:
return ErrorCode.SESSION_EXPIRED;
case 403:
return ErrorCode.FORBIDDEN;
case 404:
return ErrorCode.NOT_FOUND;
case 409:
return ErrorCode.ACCOUNT_EXISTS;
case 400:
return ErrorCode.VALIDATION_FAILED;
case 429:
return ErrorCode.RATE_LIMITED;
case 503:
return ErrorCode.SERVICE_UNAVAILABLE;
default:
return status >= 500 ? ErrorCode.INTERNAL_ERROR : ErrorCode.UNKNOWN;
}
}
/**
* Get user-friendly message for an error code
*/
export function getMessageForErrorCode(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(),
// Intentionally z.unknown() values — error details vary by error type and may contain nested objects
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 ?? getMessageForErrorCode(code),
details,
},
};
}