import axios, { AxiosError, Method } from "axios"; export interface SapB1ErrorDetails { status?: number; code?: string; message?: string; method?: string; url?: string; retryable?: boolean; cause?: unknown; } export class SapB1Error extends Error { public readonly status?: number; public readonly code?: string; public readonly method?: string; public readonly url?: string; public readonly retryable: boolean; public override readonly cause?: unknown; public constructor(details: SapB1ErrorDetails) { super(details.message || "SAP Business One Service Layer request failed"); this.name = "SapB1Error"; this.status = details.status; this.code = details.code; this.method = details.method; this.url = details.url; this.retryable = details.retryable ?? false; this.cause = details.cause; } } // Shared normalizer so every SAP call (including login) surfaces the SAP status/code // and message when the Service Layer actively rejected the request, and falls back to // the transport-level message (ECONNREFUSED, timeout, TLS, ...) otherwise. This is what // lets a caller tell "SAP rejected it" apart from "we never reached SAP". export function toSapB1Error( error: unknown, method?: Method, path?: string, fallbackMessage = "SAP B1 request failed" ): SapB1Error { if (error instanceof SapB1Error) { return error; } if (axios.isAxiosError(error)) { const axiosError = error as AxiosError<{ error?: { code?: string; message?: { value?: string } | string } }>; const status = axiosError.response?.status; const sapError = axiosError.response?.data?.error; const message = typeof sapError?.message === "string" ? sapError.message : sapError?.message?.value || axiosError.message || fallbackMessage; return new SapB1Error({ status, code: sapError?.code, message, method, url: path, retryable: status === 408 || status === 429 || (status !== undefined && status >= 500), cause: error }); } return new SapB1Error({ message: fallbackMessage, method, url: path, cause: error }); }