fix: restore lost error classifications and address code review findings
- Salesforce: add validation/duplicate/access/storage error classes and restore classification in error handler (400/409/403/503 vs generic 502) - Freebit: add auth/rate-limit/validation/network error classes and restore result-code-based classification (215, 381, 382) - Portal: replace unsafe string→enum casts with typed state variables - BaseRepository: narrow orderBy from unknown to Record<string, "asc"|"desc"> - WHMCS: narrow WhmcsNotFoundError.providerCode from string to union type - Remove unused UnitOfWork service from PrismaModule
This commit is contained in:
parent
98beed85c7
commit
49d6d21974
@ -55,7 +55,7 @@ export abstract class BaseRepository<TEntity, TCreateInput, TUpdateInput, TWhere
|
||||
|
||||
async findMany(
|
||||
where?: TWhere,
|
||||
options?: { skip?: number; take?: number; orderBy?: unknown }
|
||||
options?: { skip?: number; take?: number; orderBy?: Record<string, "asc" | "desc"> }
|
||||
): Promise<TEntity[]> {
|
||||
return this.d.findMany({
|
||||
...(where !== undefined && { where }),
|
||||
|
||||
@ -2,7 +2,6 @@ import { Global, Module } from "@nestjs/common";
|
||||
import { PrismaService } from "./prisma.service.js";
|
||||
import { TransactionService } from "./services/transaction.service.js";
|
||||
import { DistributedTransactionService } from "./services/distributed-transaction.service.js";
|
||||
import { UnitOfWork } from "./unit-of-work.service.js";
|
||||
import { IdMappingRepository } from "./repositories/id-mapping.repository.js";
|
||||
import { AuditLogRepository } from "./repositories/audit-log.repository.js";
|
||||
import { SimVoiceOptionsRepository } from "./repositories/sim-voice-options.repository.js";
|
||||
@ -13,7 +12,6 @@ import { SimVoiceOptionsRepository } from "./repositories/sim-voice-options.repo
|
||||
PrismaService,
|
||||
TransactionService,
|
||||
DistributedTransactionService,
|
||||
UnitOfWork,
|
||||
IdMappingRepository,
|
||||
AuditLogRepository,
|
||||
SimVoiceOptionsRepository,
|
||||
@ -22,7 +20,6 @@ import { SimVoiceOptionsRepository } from "./repositories/sim-voice-options.repo
|
||||
PrismaService,
|
||||
TransactionService,
|
||||
DistributedTransactionService,
|
||||
UnitOfWork,
|
||||
IdMappingRepository,
|
||||
AuditLogRepository,
|
||||
SimVoiceOptionsRepository,
|
||||
|
||||
@ -34,3 +34,47 @@ export class FreebitTimeoutError extends BaseProviderError {
|
||||
super("Freebit request timed out", providerMessage, cause);
|
||||
}
|
||||
}
|
||||
|
||||
export class FreebitAuthError extends BaseProviderError {
|
||||
readonly provider = "freebit" as const;
|
||||
readonly providerCode = FreebitProviderError.AUTH_FAILED;
|
||||
readonly domainErrorCode = ErrorCode.EXTERNAL_SERVICE_ERROR;
|
||||
readonly httpStatus = HttpStatus.SERVICE_UNAVAILABLE;
|
||||
|
||||
constructor(providerMessage: string, cause?: unknown) {
|
||||
super("Freebit authentication failed", providerMessage, cause);
|
||||
}
|
||||
}
|
||||
|
||||
export class FreebitRateLimitError extends BaseProviderError {
|
||||
readonly provider = "freebit" as const;
|
||||
readonly providerCode = FreebitProviderError.RATE_LIMITED;
|
||||
readonly domainErrorCode = ErrorCode.RATE_LIMITED;
|
||||
readonly httpStatus = HttpStatus.TOO_MANY_REQUESTS;
|
||||
|
||||
constructor(providerMessage: string, cause?: unknown) {
|
||||
super("Freebit rate limit exceeded", providerMessage, cause);
|
||||
}
|
||||
}
|
||||
|
||||
export class FreebitValidationError extends BaseProviderError {
|
||||
readonly provider = "freebit" as const;
|
||||
readonly providerCode = FreebitProviderError.VALIDATION_ERROR;
|
||||
readonly domainErrorCode = ErrorCode.VALIDATION_FAILED;
|
||||
readonly httpStatus = HttpStatus.BAD_REQUEST;
|
||||
|
||||
constructor(providerMessage: string, cause?: unknown) {
|
||||
super("Freebit validation failed", providerMessage, cause);
|
||||
}
|
||||
}
|
||||
|
||||
export class FreebitNetworkError extends BaseProviderError {
|
||||
readonly provider = "freebit" as const;
|
||||
readonly providerCode = FreebitProviderError.NETWORK_ERROR;
|
||||
readonly domainErrorCode = ErrorCode.NETWORK_ERROR;
|
||||
readonly httpStatus = HttpStatus.BAD_GATEWAY;
|
||||
|
||||
constructor(providerMessage: string, cause?: unknown) {
|
||||
super("Freebit network error", providerMessage, cause);
|
||||
}
|
||||
}
|
||||
|
||||
@ -67,3 +67,47 @@ export class SalesforceRateLimitError extends BaseProviderError {
|
||||
super("Salesforce rate limit exceeded", providerMessage, cause);
|
||||
}
|
||||
}
|
||||
|
||||
export class SalesforceValidationError extends BaseProviderError {
|
||||
readonly provider = "salesforce" as const;
|
||||
readonly providerCode = SalesforceProviderError.VALIDATION_ERROR;
|
||||
readonly domainErrorCode = ErrorCode.VALIDATION_FAILED;
|
||||
readonly httpStatus = HttpStatus.BAD_REQUEST;
|
||||
|
||||
constructor(providerMessage: string, cause?: unknown) {
|
||||
super("Salesforce validation failed", providerMessage, cause);
|
||||
}
|
||||
}
|
||||
|
||||
export class SalesforceDuplicateError extends BaseProviderError {
|
||||
readonly provider = "salesforce" as const;
|
||||
readonly providerCode = SalesforceProviderError.DUPLICATE_RECORD;
|
||||
readonly domainErrorCode = ErrorCode.ACCOUNT_EXISTS;
|
||||
readonly httpStatus = HttpStatus.CONFLICT;
|
||||
|
||||
constructor(providerMessage: string, cause?: unknown) {
|
||||
super("Salesforce duplicate record", providerMessage, cause);
|
||||
}
|
||||
}
|
||||
|
||||
export class SalesforceAccessError extends BaseProviderError {
|
||||
readonly provider = "salesforce" as const;
|
||||
readonly providerCode = SalesforceProviderError.ACCESS_DENIED;
|
||||
readonly domainErrorCode = ErrorCode.EXTERNAL_SERVICE_ERROR;
|
||||
readonly httpStatus = HttpStatus.FORBIDDEN;
|
||||
|
||||
constructor(providerMessage: string, cause?: unknown) {
|
||||
super("Salesforce insufficient access", providerMessage, cause);
|
||||
}
|
||||
}
|
||||
|
||||
export class SalesforceStorageLimitError extends BaseProviderError {
|
||||
readonly provider = "salesforce" as const;
|
||||
readonly providerCode = SalesforceProviderError.STORAGE_LIMIT;
|
||||
readonly domainErrorCode = ErrorCode.SERVICE_UNAVAILABLE;
|
||||
readonly httpStatus = HttpStatus.SERVICE_UNAVAILABLE;
|
||||
|
||||
constructor(providerMessage: string, cause?: unknown) {
|
||||
super("Salesforce storage limit exceeded", providerMessage, cause);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,10 @@
|
||||
import { HttpStatus } from "@nestjs/common";
|
||||
import { ErrorCode, type ErrorCodeType } from "@customer-portal/domain/common";
|
||||
import { WhmcsProviderError } from "@customer-portal/domain/common";
|
||||
import {
|
||||
ErrorCode,
|
||||
type ErrorCodeType,
|
||||
WhmcsProviderError,
|
||||
type WhmcsProviderErrorCode,
|
||||
} from "@customer-portal/domain/common";
|
||||
import { BaseProviderError } from "./base-provider.error.js";
|
||||
|
||||
export class WhmcsApiError extends BaseProviderError {
|
||||
@ -16,7 +20,7 @@ export class WhmcsApiError extends BaseProviderError {
|
||||
|
||||
export class WhmcsNotFoundError extends BaseProviderError {
|
||||
readonly provider = "whmcs" as const;
|
||||
readonly providerCode: string;
|
||||
readonly providerCode: WhmcsProviderErrorCode;
|
||||
readonly domainErrorCode = ErrorCode.NOT_FOUND;
|
||||
readonly httpStatus = HttpStatus.NOT_FOUND;
|
||||
|
||||
|
||||
@ -8,6 +8,10 @@ import {
|
||||
FreebitApiError,
|
||||
FreebitAccountNotFoundError,
|
||||
FreebitTimeoutError,
|
||||
FreebitAuthError,
|
||||
FreebitRateLimitError,
|
||||
FreebitValidationError,
|
||||
FreebitNetworkError,
|
||||
} from "@bff/integrations/common/errors/index.js";
|
||||
|
||||
/**
|
||||
@ -36,6 +40,22 @@ export class FreebitErrorHandlerService {
|
||||
handleRequestError(error: unknown, context: string): never {
|
||||
const message = extractErrorMessage(error);
|
||||
|
||||
// Check for auth errors (FreebitError-aware)
|
||||
if (error instanceof FreebitError && error.isAuthError?.()) {
|
||||
throw new FreebitAuthError(error.message, error);
|
||||
}
|
||||
if (this.isAuthError(message)) {
|
||||
throw new FreebitAuthError(message, error);
|
||||
}
|
||||
|
||||
// Check for rate limiting (FreebitError-aware)
|
||||
if (error instanceof FreebitError && error.isRateLimitError?.()) {
|
||||
throw new FreebitRateLimitError(error.message, error);
|
||||
}
|
||||
if (this.isRateLimitError(message)) {
|
||||
throw new FreebitRateLimitError(message, error);
|
||||
}
|
||||
|
||||
// Check for timeout
|
||||
if (this.isTimeoutError(message)) {
|
||||
throw new FreebitTimeoutError(message, error);
|
||||
@ -43,17 +63,7 @@ export class FreebitErrorHandlerService {
|
||||
|
||||
// Check for network errors
|
||||
if (this.isNetworkError(message)) {
|
||||
throw new FreebitApiError("Freebit network error", message, error);
|
||||
}
|
||||
|
||||
// Check for rate limiting
|
||||
if (this.isRateLimitError(message)) {
|
||||
throw new FreebitApiError("Freebit rate limit exceeded", message, error);
|
||||
}
|
||||
|
||||
// Check for auth errors
|
||||
if (this.isAuthError(message)) {
|
||||
throw new FreebitApiError("Freebit authentication failed", message, error);
|
||||
throw new FreebitNetworkError(message, error);
|
||||
}
|
||||
|
||||
// Re-throw if already a BaseProviderError or DomainHttpException
|
||||
@ -72,18 +82,47 @@ export class FreebitErrorHandlerService {
|
||||
* Map FreebitError to typed provider error and throw
|
||||
*/
|
||||
private throwTypedFreebitError(error: FreebitError): never {
|
||||
const resultCode = String(error.resultCode || "");
|
||||
const statusCode = String(error.statusCode || "");
|
||||
const message = error.message.toLowerCase();
|
||||
|
||||
// Authentication errors
|
||||
if (error.isAuthError?.()) {
|
||||
throw new FreebitAuthError(error.message, error);
|
||||
}
|
||||
|
||||
// Rate limit errors
|
||||
if (error.isRateLimitError?.()) {
|
||||
throw new FreebitRateLimitError(error.message, error);
|
||||
}
|
||||
|
||||
// Not found errors
|
||||
if (message.includes("account not found") || message.includes("no such account")) {
|
||||
throw new FreebitAccountNotFoundError(error.message, error);
|
||||
}
|
||||
|
||||
// Validation result codes (215 = plan change, 381/382 = network type change)
|
||||
if (
|
||||
resultCode === "215" ||
|
||||
statusCode === "215" ||
|
||||
resultCode === "381" ||
|
||||
statusCode === "381" ||
|
||||
resultCode === "382" ||
|
||||
statusCode === "382"
|
||||
) {
|
||||
throw new FreebitValidationError(error.message, error);
|
||||
}
|
||||
|
||||
// Timeout
|
||||
if (this.isTimeoutError(error.message)) {
|
||||
throw new FreebitTimeoutError(error.message, error);
|
||||
}
|
||||
|
||||
// Retryable server errors
|
||||
if (error.isRetryable?.()) {
|
||||
throw new FreebitApiError("Freebit server error", error.message, error);
|
||||
}
|
||||
|
||||
// Default: generic Freebit API error
|
||||
throw new FreebitApiError("Freebit API error", error.message, error);
|
||||
}
|
||||
|
||||
@ -9,6 +9,10 @@ import {
|
||||
SalesforceTimeoutError,
|
||||
SalesforceNetworkError,
|
||||
SalesforceRateLimitError,
|
||||
SalesforceValidationError,
|
||||
SalesforceDuplicateError,
|
||||
SalesforceAccessError,
|
||||
SalesforceStorageLimitError,
|
||||
} from "@bff/integrations/common/errors/index.js";
|
||||
import { DomainHttpException } from "@bff/core/http/domain-http.exception.js";
|
||||
|
||||
@ -61,11 +65,41 @@ export class SalesforceErrorHandlerService {
|
||||
throw new SalesforceQueryError(message);
|
||||
}
|
||||
|
||||
// Validation errors
|
||||
if (
|
||||
sfErrorCode === "REQUIRED_FIELD_MISSING" ||
|
||||
sfErrorCode === "INVALID_FIELD" ||
|
||||
sfErrorCode === "MALFORMED_ID" ||
|
||||
sfErrorCode === "FIELD_CUSTOM_VALIDATION_EXCEPTION" ||
|
||||
sfErrorCode === "FIELD_INTEGRITY_EXCEPTION"
|
||||
) {
|
||||
throw new SalesforceValidationError(message);
|
||||
}
|
||||
|
||||
// Duplicate detection
|
||||
if (sfErrorCode === "DUPLICATE_VALUE" || sfErrorCode === "DUPLICATE_EXTERNAL_ID") {
|
||||
throw new SalesforceDuplicateError(message);
|
||||
}
|
||||
|
||||
// Insufficient access
|
||||
if (
|
||||
sfErrorCode === "INSUFFICIENT_ACCESS" ||
|
||||
sfErrorCode === "INSUFFICIENT_ACCESS_OR_READONLY" ||
|
||||
sfErrorCode === "CANNOT_INSERT_UPDATE_ACTIVATE_ENTITY"
|
||||
) {
|
||||
throw new SalesforceAccessError(message);
|
||||
}
|
||||
|
||||
// Rate limiting
|
||||
if (sfErrorCode === "REQUEST_LIMIT_EXCEEDED" || sfErrorCode === "TOO_MANY_REQUESTS") {
|
||||
throw new SalesforceRateLimitError(message);
|
||||
}
|
||||
|
||||
// Storage limit
|
||||
if (sfErrorCode === "STORAGE_LIMIT_EXCEEDED") {
|
||||
throw new SalesforceStorageLimitError(message);
|
||||
}
|
||||
|
||||
// Default
|
||||
throw new SalesforceApiError(`Salesforce ${context} failed`, message);
|
||||
}
|
||||
|
||||
@ -38,17 +38,19 @@ import { formatIsoRelative } from "@/shared/utils";
|
||||
export function SupportCasesView() {
|
||||
const router = useRouter();
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [statusFilter, setStatusFilter] = useState<string>("all");
|
||||
const [priorityFilter, setPriorityFilter] = useState<string>("all");
|
||||
const [statusFilter, setStatusFilter] = useState<SupportCaseFilter["status"] | "all">("all");
|
||||
const [priorityFilter, setPriorityFilter] = useState<SupportCaseFilter["priority"] | "all">(
|
||||
"all"
|
||||
);
|
||||
const deferredSearchTerm = useDeferredValue(searchTerm);
|
||||
|
||||
const queryFilters = useMemo(() => {
|
||||
const nextFilters: SupportCaseFilter = {};
|
||||
if (statusFilter !== "all") {
|
||||
nextFilters.status = statusFilter as SupportCaseFilter["status"];
|
||||
nextFilters.status = statusFilter;
|
||||
}
|
||||
if (priorityFilter !== "all") {
|
||||
nextFilters.priority = priorityFilter as SupportCaseFilter["priority"];
|
||||
nextFilters.priority = priorityFilter;
|
||||
}
|
||||
if (deferredSearchTerm.trim()) {
|
||||
nextFilters.search = deferredSearchTerm.trim();
|
||||
@ -145,15 +147,15 @@ export function SupportCasesView() {
|
||||
searchValue={searchTerm}
|
||||
onSearchChange={setSearchTerm}
|
||||
searchPlaceholder="Search by case number or subject..."
|
||||
filterValue={statusFilter}
|
||||
onFilterChange={setStatusFilter}
|
||||
filterValue={statusFilter ?? "all"}
|
||||
onFilterChange={v => setStatusFilter(v as SupportCaseFilter["status"] | "all")}
|
||||
filterOptions={statusFilterOptions}
|
||||
filterLabel="Filter by status"
|
||||
>
|
||||
{/* Priority filter as additional child */}
|
||||
<FilterDropdown
|
||||
value={priorityFilter}
|
||||
onChange={setPriorityFilter}
|
||||
value={priorityFilter ?? "all"}
|
||||
onChange={v => setPriorityFilter(v as SupportCaseFilter["priority"] | "all")}
|
||||
options={priorityFilterOptions}
|
||||
label="Filter by priority"
|
||||
width="w-40"
|
||||
|
||||
@ -39,6 +39,12 @@ export const SalesforceProviderError = {
|
||||
QUERY_ERROR: "SF_QUERY_ERROR",
|
||||
RECORD_NOT_FOUND: "SF_RECORD_NOT_FOUND",
|
||||
|
||||
// Validation
|
||||
VALIDATION_ERROR: "SF_VALIDATION_ERROR",
|
||||
DUPLICATE_RECORD: "SF_DUPLICATE_RECORD",
|
||||
ACCESS_DENIED: "SF_ACCESS_DENIED",
|
||||
STORAGE_LIMIT: "SF_STORAGE_LIMIT",
|
||||
|
||||
// Network/Infrastructure
|
||||
TIMEOUT: "SF_TIMEOUT",
|
||||
NETWORK_ERROR: "SF_NETWORK_ERROR",
|
||||
@ -52,8 +58,21 @@ export type SalesforceProviderErrorCode =
|
||||
(typeof SalesforceProviderError)[keyof typeof SalesforceProviderError];
|
||||
|
||||
export const FreebitProviderError = {
|
||||
// Not found
|
||||
ACCOUNT_NOT_FOUND: "FREEBIT_ACCOUNT_NOT_FOUND",
|
||||
|
||||
// Authentication
|
||||
AUTH_FAILED: "FREEBIT_AUTH_FAILED",
|
||||
|
||||
// Validation
|
||||
VALIDATION_ERROR: "FREEBIT_VALIDATION_ERROR",
|
||||
|
||||
// Network/Infrastructure
|
||||
TIMEOUT: "FREEBIT_TIMEOUT",
|
||||
NETWORK_ERROR: "FREEBIT_NETWORK_ERROR",
|
||||
RATE_LIMITED: "FREEBIT_RATE_LIMITED",
|
||||
|
||||
// Generic
|
||||
API_ERROR: "FREEBIT_API_ERROR",
|
||||
} as const;
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user