2025-11-26 16:36:06 +09:00
|
|
|
|
/**
|
|
|
|
|
|
* 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",
|
2025-12-22 18:59:38 +09:00
|
|
|
|
LEGACY_ACCOUNT_EXISTS: "BIZ_007",
|
2025-12-29 14:09:33 +09:00
|
|
|
|
ACCOUNT_MAPPING_MISSING: "BIZ_008",
|
2025-11-26 16:36:06 +09:00
|
|
|
|
|
|
|
|
|
|
// 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",
|
|
|
|
|
|
|
2026-01-19 14:15:43 +09:00
|
|
|
|
// 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",
|
|
|
|
|
|
|
2025-11-26 16:36:06 +09:00
|
|
|
|
// 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
|
2025-12-22 18:59:38 +09:00
|
|
|
|
[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.",
|
2025-11-26 16:36:06 +09:00
|
|
|
|
[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.",
|
2025-12-15 13:32:42 +09:00
|
|
|
|
[ErrorCode.SERVICE_UNAVAILABLE]:
|
|
|
|
|
|
"This service is temporarily unavailable. Please try again later.",
|
2025-12-29 14:09:33 +09:00
|
|
|
|
[ErrorCode.ACCOUNT_MAPPING_MISSING]:
|
|
|
|
|
|
"Your account isn’t fully set up yet. Please contact support or try again later.",
|
2025-11-26 16:36:06 +09:00
|
|
|
|
|
|
|
|
|
|
// System
|
|
|
|
|
|
[ErrorCode.INTERNAL_ERROR]: "An unexpected error occurred. Please try again later.",
|
2025-12-15 13:32:42 +09:00
|
|
|
|
[ErrorCode.EXTERNAL_SERVICE_ERROR]:
|
|
|
|
|
|
"An external service is temporarily unavailable. Please try again later.",
|
2025-11-26 16:36:06 +09:00
|
|
|
|
[ErrorCode.DATABASE_ERROR]: "A system error occurred. Please try again later.",
|
|
|
|
|
|
[ErrorCode.CONFIGURATION_ERROR]: "A system configuration error occurred. Please contact support.",
|
|
|
|
|
|
|
|
|
|
|
|
// Network
|
2025-12-15 13:32:42 +09:00
|
|
|
|
[ErrorCode.NETWORK_ERROR]:
|
|
|
|
|
|
"Unable to connect to the server. Please check your internet connection.",
|
2025-11-26 16:36:06 +09:00
|
|
|
|
[ErrorCode.TIMEOUT]: "The request timed out. Please try again.",
|
|
|
|
|
|
[ErrorCode.RATE_LIMITED]: "Too many requests. Please wait a moment and try again.",
|
|
|
|
|
|
|
2026-01-19 14:15:43 +09:00
|
|
|
|
// 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.",
|
|
|
|
|
|
|
2025-11-26 16:36:06 +09:00
|
|
|
|
// 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,
|
2025-12-15 13:32:42 +09:00
|
|
|
|
// 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",
|
2025-11-26 16:36:06 +09:00
|
|
|
|
},
|
|
|
|
|
|
[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",
|
|
|
|
|
|
},
|
2025-12-22 18:59:38 +09:00
|
|
|
|
[ErrorCode.LEGACY_ACCOUNT_EXISTS]: {
|
|
|
|
|
|
category: "business",
|
|
|
|
|
|
severity: "low",
|
|
|
|
|
|
shouldLogout: false,
|
|
|
|
|
|
shouldRetry: false,
|
|
|
|
|
|
logLevel: "info",
|
|
|
|
|
|
},
|
2025-11-26 16:36:06 +09:00
|
|
|
|
[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",
|
|
|
|
|
|
},
|
2025-12-29 14:09:33 +09:00
|
|
|
|
[ErrorCode.ACCOUNT_MAPPING_MISSING]: {
|
|
|
|
|
|
category: "business",
|
|
|
|
|
|
severity: "medium",
|
|
|
|
|
|
shouldLogout: false,
|
|
|
|
|
|
shouldRetry: false,
|
|
|
|
|
|
logLevel: "warn",
|
|
|
|
|
|
},
|
2025-11-26 16:36:06 +09:00
|
|
|
|
|
|
|
|
|
|
// 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",
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-01-19 14:15:43 +09:00
|
|
|
|
// 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",
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2025-11-26 16:36:06 +09:00
|
|
|
|
// Generic
|
|
|
|
|
|
[ErrorCode.UNKNOWN]: {
|
|
|
|
|
|
category: "system",
|
|
|
|
|
|
severity: "medium",
|
|
|
|
|
|
shouldLogout: false,
|
|
|
|
|
|
shouldRetry: true,
|
|
|
|
|
|
logLevel: "error",
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
// Helper Functions
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
2025-12-29 15:44:01 +09:00
|
|
|
|
/**
|
|
|
|
|
|
* 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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-26 16:36:06 +09:00
|
|
|
|
/**
|
|
|
|
|
|
* Get user-friendly message for an error code
|
|
|
|
|
|
*/
|
2025-12-26 18:17:37 +09:00
|
|
|
|
export function getMessageForErrorCode(code: string): string {
|
2025-11-26 16:36:06 +09:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-15 13:32:42 +09:00
|
|
|
|
// 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).
|
2025-11-26 16:36:06 +09:00
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
// 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(),
|
2026-02-24 19:05:30 +09:00
|
|
|
|
// Intentionally z.unknown() values — error details vary by error type and may contain nested objects
|
2025-11-26 16:36:06 +09:00
|
|
|
|
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,
|
2025-12-26 18:17:37 +09:00
|
|
|
|
message: customMessage ?? getMessageForErrorCode(code),
|
2025-11-26 16:36:06 +09:00
|
|
|
|
details,
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|