moallem all endpoints working

This commit is contained in:
2026-06-14 16:42:52 +03:30
parent 7a6e482586
commit 2212f6da41
18 changed files with 833 additions and 234 deletions

View File

@@ -1,4 +1,5 @@
import { Logger } from '@nestjs/common';
import { AxiosError } from 'axios';
import { InquiryType } from '../../common/enums/inquiry-type.enum';
import { ProviderName } from '../../common/enums/provider-name.enum';
import { NormalizedErrorDto } from '../../common/dto/normalized-error.dto';
@@ -131,19 +132,41 @@ export abstract class BaseProvider<TRequest = unknown, TResponse = unknown>
protected formatProviderError(
providerMessage?: string,
providerCode?: string,
fallbackMessage = 'Provider returned an error',
fallbackMessage = 'Provider request failed',
): Error {
const message = providerMessage?.trim() || fallbackMessage;
const code = providerCode?.trim() || 'PROVIDER_ERROR';
const normalized = this.normalizeError({
code: 'PROVIDER_ERROR',
message: fallbackMessage,
providerMessage,
providerCode,
code,
message,
providerMessage: message,
providerCode: code,
});
const err = new Error(normalized.message);
const err = new Error(message);
(err as Error & { normalizedError: NormalizedErrorDto }).normalizedError = normalized;
return err;
}
protected formatAxiosError(error: AxiosError): Error {
const data = error.response?.data;
if (typeof data === 'string') {
const faultMatch = data.match(
/<(?:\w+:)?faultstring[^>]*>([\s\S]*?)<\/(?:\w+:)?faultstring>/i,
);
if (faultMatch?.[1]) {
return this.formatProviderError(
faultMatch[1].trim().replace(/&amp;/g, '&').replace(/&lt;/g, '<').replace(/&gt;/g, '>'),
String(error.response?.status ?? 'NETWORK_ERROR'),
);
}
}
return this.formatProviderError(
error.message,
String(error.response?.status ?? 'NETWORK_ERROR'),
);
}
protected abstract callProvider(
inquiryType: InquiryType,
payload: TRequest,

View File

@@ -1,6 +1,10 @@
import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import axios, { AxiosError, AxiosInstance } from 'axios';
import { AxiosError, AxiosInstance } from 'axios';
import {
createOutboundAxiosInstance,
isOutboundHttpDebugEnabled,
} from '../../common/helpers/http-client.helper';
import { InquiryType } from '../../common/enums/inquiry-type.enum';
import { ProviderName } from '../../common/enums/provider-name.enum';
import { AmitisAuthServiceConfig } from '../../config/configuration';
@@ -22,6 +26,8 @@ interface CentInsurTokenResponse {
ExpiresIn?: number | string;
expiresIn?: number | string;
expires_in?: number | string;
IsSucceed?: boolean;
LoginStatus?: number;
data?: CentInsurTokenResponse;
}
@@ -49,7 +55,7 @@ export class AmitisProvider {
private readonly generalTokenService: GeneralTokenService,
) {
this.config = configService.get<AmitisAuthServiceConfig>('amitis')!;
this.httpClient = axios.create({
this.httpClient = createOutboundAxiosInstance({
baseURL: this.config.baseUrl,
timeout: this.config.timeout,
});
@@ -102,25 +108,36 @@ export class AmitisProvider {
username: string,
password: string,
): Promise<string> {
// Use FormData for multipart/form-data format
const FormData = require('form-data');
const formData = new FormData();
formData.append('username', username);
formData.append('password', password);
const loginBody = new URLSearchParams({
username: username.toLowerCase(),
password,
});
try {
const loginUrl = `${this.config.baseUrl}${this.config.loginPath}`;
this.logger.log(
`AMITIS login → POST ${loginUrl} | provider=${providerName} | inquiry=${inquiryType} | username=${username.toLowerCase()}`,
);
const response = await this.httpClient.post<CentInsurTokenResponse>(
this.config.loginPath,
formData,
loginBody.toString(),
{
headers: {
...formData.getHeaders(),
'Content-Type': 'application/x-www-form-urlencoded',
},
},
);
if (isOutboundHttpDebugEnabled()) {
this.logger.log(
`AMITIS login ← ${response.status} | provider=${providerName} | inquiry=${inquiryType} | bodyKeys=${Object.keys(response.data ?? {}).join(',') || 'none'}`,
);
}
const token = this.extractToken(response.data);
await this.saveToken(token, providerName, inquiryType, username, 'login');
await this.saveToken(token, providerName, inquiryType, username, password, 'login');
return token.accessToken;
} catch (error) {
throw this.toAuthError(`AMITIS login failed for ${providerName}/${inquiryType}`, error);
@@ -144,6 +161,7 @@ export class AmitisProvider {
providerName,
inquiryType,
latestToken.username,
latestToken.clientSecret,
'refresh',
);
return token.accessToken;
@@ -160,15 +178,16 @@ export class AmitisProvider {
providerName: ProviderName,
inquiryType: InquiryType,
username: string,
password: string,
scope: string,
): Promise<void> {
await this.generalTokenService.create({
serviceProvider: this.getTokenKey(providerName, inquiryType),
tokenType: token.tokenType,
url: this.config.baseUrl,
clientId: '',
clientSecret: '',
username,
clientId: username.toLowerCase(),
clientSecret: password,
username: username.toLowerCase(),
scope,
accessToken: token.accessToken,
refreshToken: token.refreshToken,
@@ -182,11 +201,21 @@ export class AmitisProvider {
fallbackRefreshToken?: string,
): AmitisAuthToken {
const body = responseBody.data ?? responseBody;
if (body.IsSucceed === false) {
throw new Error(
`AMITIS login rejected (LoginStatus=${body.LoginStatus ?? 'unknown'})`,
);
}
const accessToken =
body.Token ?? body.token ?? body.AccessToken ?? body.accessToken ?? body.access_token;
if (!accessToken) {
throw new Error('AMITIS auth response did not include an access token');
if (!accessToken || !String(accessToken).trim()) {
const bodyPreview = JSON.stringify(responseBody).slice(0, 500);
throw new Error(
`AMITIS auth response did not include an access token | body=${bodyPreview}`,
);
}
const expiresIn = Number(body.ExpiresIn ?? body.expiresIn ?? body.expires_in ?? 20 * 60);
@@ -229,7 +258,13 @@ export class AmitisProvider {
private toAuthError(message: string, error: unknown): Error {
if (error instanceof AxiosError) {
const status = error.response?.status ?? 'NETWORK_ERROR';
return new Error(`${message}: ${status} ${error.message}`);
const body =
error.response?.data !== undefined
? JSON.stringify(error.response.data).slice(0, 500)
: undefined;
return new Error(
body ? `${message}: ${status} ${error.message} | body=${body}` : `${message}: ${status} ${error.message}`,
);
}
if (error instanceof Error) {

View File

@@ -1,6 +1,11 @@
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import axios, { AxiosError } from 'axios';
import { mergeOutboundAxiosConfig } from '../../common/helpers/http-client.helper';
import {
getSayahProviderError,
SayahApiResponse,
} from '../../common/helpers/sayah-response.helper';
import { InquiryType } from '../../common/enums/inquiry-type.enum';
import { ProviderName } from '../../common/enums/provider-name.enum';
import { ProviderEnvConfig } from '../../config/configuration';
@@ -107,11 +112,11 @@ export class HamtaProvider extends LegacyApiProvider {
try {
const response = await axios.get<Record<string, unknown>>(
`${inquiryConfig.url}/AddressByPostcode`,
{
mergeOutboundAxiosConfig({
params: { PostalCode: postalCode },
headers: { Authorization: `Bearer ${token}` },
timeout: this.hamtaConfig.timeout,
},
}),
);
return { raw: response.data };
@@ -146,14 +151,14 @@ export class HamtaProvider extends LegacyApiProvider {
inquiryConfig.username,
inquiryConfig.password,
),
{
mergeOutboundAxiosConfig({
headers: {
'Content-Type': 'text/xml; charset=utf-8',
SOAPAction: '"http://tempuri.org/ISabtInq/SubmitInqDteStsWithPstCod"',
},
timeout: this.hamtaConfig.timeout,
responseType: 'text',
},
}),
);
const result = this.extractSoapValue(response.data, 'SubmitInqDteStsWithPstCodResult');
@@ -195,14 +200,14 @@ export class HamtaProvider extends LegacyApiProvider {
inquiryConfig.username,
inquiryConfig.password,
),
{
mergeOutboundAxiosConfig({
headers: {
'Content-Type': 'text/xml; charset=utf-8',
SOAPAction: '"http://tempuri.org/IShahkarInq/ShahkarInquery"',
},
timeout: this.hamtaConfig.timeout,
responseType: 'text',
},
}),
);
const result = this.extractSoapValue(response.data, 'ShahkarInqueryResult');
@@ -241,11 +246,7 @@ export class HamtaProvider extends LegacyApiProvider {
);
try {
const response = await axios.post<{
IsSucceed?: boolean;
Errors?: Array<{ Code?: string; Message?: string }>;
Result?: unknown;
}>(
const response = await axios.post<SayahApiResponse>(
inquiryConfig.url,
{
AccountOwnerType: accountOwnerType,
@@ -253,20 +254,18 @@ export class HamtaProvider extends LegacyApiProvider {
LegalId: legalId,
ShebaId: shebaId,
},
{
mergeOutboundAxiosConfig({
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
timeout: this.hamtaConfig.timeout,
},
}),
);
if (!response.data.IsSucceed && response.data.Errors) {
throw this.formatProviderError(
this.formatErrors(response.data.Errors),
'SAYAH_HAS_ERROR',
);
const providerError = getSayahProviderError(response.data);
if (providerError) {
throw this.formatProviderError(providerError.message, providerError.code);
}
return { raw: response.data };

View File

@@ -1,6 +1,16 @@
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import axios, { AxiosError } from 'axios';
import { mergeOutboundAxiosConfig } from '../../common/helpers/http-client.helper';
import {
buildCivilRegistrationFullName,
getCivilRegistrationProviderError,
parseCiiEstelamResult,
} from '../../common/helpers/soap-civil-registration.helper';
import {
getSayahProviderError,
SayahApiResponse,
} from '../../common/helpers/sayah-response.helper';
import { InquiryType } from '../../common/enums/inquiry-type.enum';
import { ProviderName } from '../../common/enums/provider-name.enum';
import { ProviderEnvConfig } from '../../config/configuration';
@@ -10,6 +20,7 @@ import {
LegacyApiProvider,
LegacyInquiryPayload,
LegacyInquiryResult,
PersonInquiryResult,
} from '../shared/legacy-api.provider.abstract';
interface CivilRegistrationPayload extends LegacyInquiryPayload {
@@ -107,11 +118,11 @@ export class MoallemProvider extends LegacyApiProvider {
try {
const response = await axios.get<Record<string, unknown>>(
`${inquiryConfig.url}/AddressByPostcode`,
{
mergeOutboundAxiosConfig({
params: { PostalCode: postalCode },
headers: { Authorization: `Bearer ${token}` },
timeout: this.moallemConfig.timeout,
},
}),
);
return { raw: response.data };
@@ -128,7 +139,7 @@ export class MoallemProvider extends LegacyApiProvider {
private async inquireCivilRegistration(
payload: CivilRegistrationPayload,
): Promise<{ raw: unknown }> {
): Promise<PersonInquiryResult> {
const nationalCode = this.getRequiredString(
payload.nationalCode ?? payload.NIN,
'nationalCode',
@@ -141,37 +152,39 @@ export class MoallemProvider extends LegacyApiProvider {
const response = await axios.post<string>(
inquiryConfig.url,
this.buildCivilRegistrationEnvelope(
payload,
nationalCode,
birthDate,
inquiryConfig.username,
inquiryConfig.password,
),
{
mergeOutboundAxiosConfig({
headers: {
'Content-Type': 'text/xml; charset=utf-8',
SOAPAction: '"http://tempuri.org/ISabtV3/SabtInquery"',
SOAPAction: '"http://tempuri.org/ISabtInq/SubmitInqDteStsWithPstCod"',
},
timeout: this.moallemConfig.timeout,
responseType: 'text',
},
}),
);
const result = this.extractSoapValue(response.data, 'SabtInqueryResult');
const civilRegistration = parseCiiEstelamResult(response.data);
const providerError = getCivilRegistrationProviderError(civilRegistration);
if (providerError) {
throw this.formatProviderError(providerError.message, providerError.code);
}
const fullName = buildCivilRegistrationFullName(civilRegistration);
return {
raw: {
nationalCode,
birthDate,
result,
soap: response.data,
},
nationalCode,
birthDate,
fullName: fullName || undefined,
raw: civilRegistration,
};
} catch (error) {
if (error instanceof AxiosError) {
throw this.formatProviderError(
error.message,
String(error.response?.status ?? 'NETWORK_ERROR'),
);
throw this.formatAxiosError(error);
}
throw error;
}
@@ -195,14 +208,14 @@ export class MoallemProvider extends LegacyApiProvider {
inquiryConfig.username,
inquiryConfig.password,
),
{
mergeOutboundAxiosConfig({
headers: {
'Content-Type': 'text/xml; charset=utf-8',
SOAPAction: '"http://tempuri.org/IShahkarInq/ShahkarInquery"',
},
timeout: this.moallemConfig.timeout,
responseType: 'text',
},
}),
);
const result = this.extractSoapValue(response.data, 'ShahkarInqueryResult');
@@ -241,11 +254,7 @@ export class MoallemProvider extends LegacyApiProvider {
);
try {
const response = await axios.post<{
IsSucceed?: boolean;
Errors?: Array<{ Code?: string; Message?: string }>;
Result?: unknown;
}>(
const response = await axios.post<SayahApiResponse>(
inquiryConfig.url,
{
AccountOwnerType: accountOwnerType,
@@ -253,20 +262,18 @@ export class MoallemProvider extends LegacyApiProvider {
LegalId: legalId,
ShebaId: shebaId,
},
{
mergeOutboundAxiosConfig({
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
timeout: this.moallemConfig.timeout,
},
}),
);
if (!response.data.IsSucceed && response.data.Errors) {
throw this.formatProviderError(
this.formatErrors(response.data.Errors),
'SAYAH_HAS_ERROR',
);
const providerError = getSayahProviderError(response.data);
if (providerError) {
throw this.formatProviderError(providerError.message, providerError.code);
}
return { raw: response.data };
@@ -282,22 +289,38 @@ export class MoallemProvider extends LegacyApiProvider {
}
private buildCivilRegistrationEnvelope(
payload: CivilRegistrationPayload,
nationalCode: string,
birthDate: string,
username: string,
password: string,
): string {
const birthDateCompact = birthDate.replace(/-/g, '');
const dateHasPostfix = payload.dateHasPostfix ?? 0;
return `<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<SabtInquery xmlns="http://tempuri.org/">
<NIN>${this.escapeXml(nationalCode)}</NIN>
<BirthDate>${this.escapeXml(birthDate)}</BirthDate>
<SubmitInqDteStsWithPstCod xmlns="http://tempuri.org/">
<req>
<Nin>${this.escapeXml(nationalCode)}</Nin>
<Shenasnameserial>0</Shenasnameserial>
<ShenasnameNo>0</ShenasnameNo>
<BirthDate>${this.escapeXml(birthDateCompact)}</BirthDate>
<DateHasPostfix>${dateHasPostfix}</DateHasPostfix>
<Gender>0</Gender>
<OfficeCode>0</OfficeCode>
<BookNo>0</BookNo>
<NameHasPrefix>0</NameHasPrefix>
<NameHasPostFix>0</NameHasPostFix>
<FamilyHasPrefix>0</FamilyHasPrefix>
<FamilyHasPostFix>0</FamilyHasPostFix>
</req>
<Username>${this.escapeXml(username)}</Username>
<Password>${this.escapeXml(password)}</Password>
</SabtInquery>
</SubmitInqDteStsWithPstCod>
</soap:Body>
</soap:Envelope>`;
}
@@ -341,10 +364,10 @@ export class MoallemProvider extends LegacyApiProvider {
private escapeXml(value: string): string {
return value
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;');
}

View File

@@ -1,6 +1,11 @@
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import axios, { AxiosError } from 'axios';
import { mergeOutboundAxiosConfig } from '../../common/helpers/http-client.helper';
import {
getSayahProviderError,
SayahApiResponse,
} from '../../common/helpers/sayah-response.helper';
import { InquiryType } from '../../common/enums/inquiry-type.enum';
import { ProviderName } from '../../common/enums/provider-name.enum';
import { ProviderExecutionContext } from '../../common/interfaces/inquiry-provider.interface';
@@ -172,16 +177,19 @@ export class ParsianProvider extends BaseProvider<LegacyInquiryPayload, LegacyIn
}
try {
const response = await axios.get<ParsianShahkarResponse>(inquiryConfig.url, {
params: {
nationalCode,
mobileNumber,
},
headers: {
'X-PACKAGE-API-KEY': inquiryConfig.apiKey,
},
timeout: this.config.timeout,
});
const response = await axios.get<ParsianShahkarResponse>(
inquiryConfig.url,
mergeOutboundAxiosConfig({
params: {
nationalCode,
mobileNumber,
},
headers: {
'X-PACKAGE-API-KEY': inquiryConfig.apiKey,
},
timeout: this.config.timeout,
}),
);
const body = response.data;
return {
@@ -230,14 +238,14 @@ export class ParsianProvider extends BaseProvider<LegacyInquiryPayload, LegacyIn
inquiryConfig.username,
inquiryConfig.password,
),
{
mergeOutboundAxiosConfig({
headers: {
'Content-Type': 'text/xml; charset=utf-8',
SOAPAction: '"http://tempuri.org/ISabtInq/SubmitInqDteStsWithPstCod"',
},
timeout: this.config.timeout,
responseType: 'text',
},
}),
);
return {
@@ -348,14 +356,14 @@ export class ParsianProvider extends BaseProvider<LegacyInquiryPayload, LegacyIn
inquiryConfig.password,
passwordFieldName,
),
{
mergeOutboundAxiosConfig({
headers: {
'Content-Type': 'text/xml; charset=utf-8',
SOAPAction: `"http://tempuri.org/ICarAllPlcysV4/${methodName}"`,
},
timeout: this.config.timeout,
responseType: 'text',
},
}),
);
return {
@@ -396,7 +404,7 @@ export class ParsianProvider extends BaseProvider<LegacyInquiryPayload, LegacyIn
);
try {
const response = await axios.post<ParsianSayahResponse>(
const response = await axios.post<SayahApiResponse>(
inquiryConfig.url,
{
AccountOwnerType: accountOwnerType,
@@ -404,23 +412,18 @@ export class ParsianProvider extends BaseProvider<LegacyInquiryPayload, LegacyIn
LegalId: legalId,
ShebaId: shebaId,
},
{
mergeOutboundAxiosConfig({
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
timeout: this.config.timeout,
},
}),
);
if (
(response.data.IsSucceed === false || response.data.isSucceed === false) &&
this.hasErrors(response.data.Errors ?? response.data.errors)
) {
throw this.formatProviderError(
this.formatErrors(response.data.Errors ?? response.data.errors),
'SAYAH_HAS_ERROR',
);
const providerError = getSayahProviderError(response.data);
if (providerError) {
throw this.formatProviderError(providerError.message, providerError.code);
}
return { raw: response.data };

View File

@@ -1,6 +1,7 @@
import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import axios, { AxiosError, AxiosInstance } from 'axios';
import { createOutboundAxiosInstance, mergeOutboundAxiosConfig } from '../../common/helpers/http-client.helper';
import { InquiryType } from '../../common/enums/inquiry-type.enum';
import { ProviderName } from '../../common/enums/provider-name.enum';
import { jalaliDateToGregorianDate } from '../../common/helpers/jalali-date.helper';
@@ -51,7 +52,7 @@ export class TejaratNouProvider implements InquiryProvider<PersonInquiryPayload,
private readonly cachedInquiryResultService: CachedInquiryResultService,
) {
this.config = this.configService.get<TejaratNouConfig>('tejaratnou')!;
this.httpClient = axios.create({
this.httpClient = createOutboundAxiosInstance({
baseURL: this.config.inquiryBaseUrl,
timeout: this.config.timeout,
headers: {
@@ -227,13 +228,13 @@ export class TejaratNouProvider implements InquiryProvider<PersonInquiryPayload,
password: this.config.password,
scope: 'api-gateway access-management',
}),
{
mergeOutboundAxiosConfig({
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Cookie: 'cookiesession1=678A8C5D2222753B93723D81AA53FF8C',
},
timeout: this.config.timeout,
},
}),
);
const jsonData = response.data;

View File

@@ -1,4 +1,6 @@
import axios, { AxiosError, AxiosInstance } from 'axios';
import { AxiosError, AxiosInstance } from 'axios';
import { createOutboundAxiosInstance } from '../../common/helpers/http-client.helper';
import { getSayahProviderError } from '../../common/helpers/sayah-response.helper';
import { InquiryType } from '../../common/enums/inquiry-type.enum';
import { ProviderName } from '../../common/enums/provider-name.enum';
import { ProviderExecutionContext } from '../../common/interfaces/inquiry-provider.interface';
@@ -51,7 +53,7 @@ export abstract class LegacyApiProvider extends BaseProvider<
) {
super(config);
this.providerConfig = config;
this.httpClient = axios.create({
this.httpClient = createOutboundAxiosInstance({
timeout: config.timeout,
headers: {
'Content-Type': 'application/json',
@@ -131,15 +133,9 @@ export abstract class LegacyApiProvider extends BaseProvider<
// Handle Sayah/Sheba API format (ReturnValue/HasError)
if ('ReturnValue' in body || 'HasError' in body) {
if (body.HasError) {
// Format error message from Errors object
const errorMessages = body.Errors
? Object.entries(body.Errors).map(([code, field]) => `${field} (code: ${code})`).join(', ')
: 'Request failed';
throw this.formatProviderError(
errorMessages,
'SAYAH_ERROR',
);
const providerError = getSayahProviderError(body);
if (providerError) {
throw this.formatProviderError(providerError.message, providerError.code);
}
return { raw: body };
}
@@ -168,14 +164,11 @@ export abstract class LegacyApiProvider extends BaseProvider<
const data = error.response?.data as LegacyProviderApiResponse | undefined;
// Check for Sayah/Sheba format error
if (data && ('ReturnValue' in data || 'HasError' in data) && data.HasError) {
const errorMessages = data.Errors
? Object.entries(data.Errors).map(([code, field]) => `${field} (code: ${code})`).join(', ')
: error.message;
throw this.formatProviderError(
errorMessages,
String(error.response?.status ?? 'SAYAH_ERROR'),
);
if (data && ('ReturnValue' in data || 'HasError' in data)) {
const providerError = getSayahProviderError(data);
if (providerError) {
throw this.formatProviderError(providerError.message, providerError.code);
}
}
// Check for CentInsur format error

View File

@@ -55,27 +55,34 @@ export class ProviderOrchestratorService {
} catch (error) {
const normalized = this.extractError(error);
errors.push(normalized);
const errorSummary = normalized.providerMessage ?? normalized.message;
const hasNextProvider = providers.indexOf(provider) < providers.length - 1;
this.logger.warn(
hasNextProvider
? `Provider ${provider.name} failed for ${inquiryType}, trying next fallback`
: `Provider ${provider.name} failed for ${inquiryType}, no fallback available`,
? `Provider ${provider.name} failed for ${inquiryType}, trying next fallback | ${errorSummary}`
: `Provider ${provider.name} failed for ${inquiryType}, no fallback available | ${errorSummary}`,
);
this.logger.debug(
`Attempt duration: ${Date.now() - attemptStart}ms | error: ${normalized.message}`,
this.logger.warn(
`Attempt duration: ${Date.now() - attemptStart}ms | providerCode=${normalized.providerCode ?? normalized.code} | providerMessage=${normalized.providerMessage ?? normalized.message}`,
);
}
}
throw new InquiryException('All providers failed', {
code: providers.length === 1 ? 'PROVIDER_SERVICE_NOT_AVAILABLE' : 'ALL_PROVIDERS_FAILED',
message:
providers.length === 1
? `${providers[0].name} service is not available`
: errors.map((e) => e.message).join('; '),
providerMessage: errors[0]?.providerMessage,
providerCode: errors[0]?.providerCode,
});
const lastError = errors[errors.length - 1]!;
if (providers.length === 1) {
throw new InquiryException(lastError.message, lastError);
}
throw new InquiryException(
errors.map((error) => error.message).join('; '),
{
code: 'ALL_PROVIDERS_FAILED',
message: errors.map((error) => error.message).join('; '),
providerMessage: lastError.providerMessage ?? lastError.message,
providerCode: lastError.providerCode ?? lastError.code,
},
);
}
private extractError(error: unknown): NormalizedErrorDto {