final update on moallm client

This commit is contained in:
2026-06-15 10:23:00 +03:30
parent fac6142483
commit 1b7f678538
17 changed files with 394 additions and 70 deletions

View File

@@ -18,11 +18,11 @@ export class BaseInquiryResponseDto<T = Record<string, unknown>> {
@ApiPropertyOptional({ example: 'Inquiry completed successfully' }) @ApiPropertyOptional({ example: 'Inquiry completed successfully' })
message?: string; message?: string;
@ApiPropertyOptional() @ApiPropertyOptional({ nullable: true })
data?: T; data?: T | null;
@ApiPropertyOptional({ type: NormalizedErrorDto }) @ApiPropertyOptional({ type: NormalizedErrorDto, nullable: true })
error?: NormalizedErrorDto; error?: NormalizedErrorDto | null;
@ApiProperty({ example: 342, description: 'Duration in milliseconds' }) @ApiProperty({ example: 342, description: 'Duration in milliseconds' })
duration!: number; duration!: number;

View File

@@ -17,6 +17,12 @@ export class NormalizedErrorDto {
@ApiPropertyOptional({ example: '503' }) @ApiPropertyOptional({ example: '503' })
providerCode?: string; providerCode?: string;
@ApiPropertyOptional({
example: 'MjFiNDQwYjAtOTdkMy00YjFkLWEzN2UtMWEyOTk2NDE0MjRm',
description: 'Upstream provider reference code when available',
})
providerTrackingCode?: string;
@ApiPropertyOptional({ @ApiPropertyOptional({
example: [{ field: 'sheba', constraints: ['sheba must start with IR and contain 24 digits'] }], example: [{ field: 'sheba', constraints: ['sheba must start with IR and contain 24 digits'] }],
description: 'Field-level validation errors when code is VALIDATION_ERROR', description: 'Field-level validation errors when code is VALIDATION_ERROR',

View File

@@ -6,6 +6,7 @@ export class InquiryException extends HttpException {
message: string, message: string,
public readonly normalizedError: NormalizedErrorDto, public readonly normalizedError: NormalizedErrorDto,
status: HttpStatus = HttpStatus.UNPROCESSABLE_ENTITY, status: HttpStatus = HttpStatus.UNPROCESSABLE_ENTITY,
public readonly provider?: string,
) { ) {
super({ message, error: normalizedError }, status); super({ message, error: normalizedError }, status);
} }

View File

@@ -7,8 +7,8 @@ import {
Logger, Logger,
} from '@nestjs/common'; } from '@nestjs/common';
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import { BaseInquiryResponseDto } from '../dto/base-inquiry-response.dto';
import { NormalizedErrorDto } from '../dto/normalized-error.dto'; import { NormalizedErrorDto } from '../dto/normalized-error.dto';
import { buildInquiryResponse } from '../helpers/inquiry-response.helper';
import { ProviderException } from '../exceptions/provider.exception'; import { ProviderException } from '../exceptions/provider.exception';
import { isNormalizedError } from '../helpers/validation-error.helper'; import { isNormalizedError } from '../helpers/validation-error.helper';
@@ -39,14 +39,14 @@ export class AllExceptionsFilter implements ExceptionFilter {
exception instanceof Error ? exception.stack : undefined, exception instanceof Error ? exception.stack : undefined,
); );
const body: BaseInquiryResponseDto = { const body = buildInquiryResponse({
success: false, success: false,
provider: 'GATEWAY', provider: 'GATEWAY',
trackingCode, trackingCode,
message: normalizedError.message, message: normalizedError.message,
error: normalizedError,
duration: 0, duration: 0,
}; error: normalizedError,
});
response.status(status).json(body); response.status(status).json(body);
} }

View File

@@ -0,0 +1,72 @@
import { InquiryType } from '../enums/inquiry-type.enum';
export interface CentInsurApiResponse {
IsSucceed?: boolean;
Result?: {
Result?: boolean;
ErrorMessage?: string | null;
[key: string]: unknown;
};
TrackingCode?: string;
message?: string;
code?: string;
success?: boolean;
}
export interface CentInsurProviderError {
message: string;
code: string;
providerTrackingCode?: string;
}
function getNoMatchMessage(inquiryType?: InquiryType): string {
switch (inquiryType) {
case InquiryType.REAL_ESTATE:
return 'No property ownership found for the provided national code and postal code';
default:
return 'Inquiry returned no matching result';
}
}
export function getCentInsurProviderError(
body: CentInsurApiResponse,
inquiryType?: InquiryType,
): CentInsurProviderError | null {
if (!('IsSucceed' in body)) {
return null;
}
const providerTrackingCode = body.TrackingCode?.trim() || undefined;
if (body.IsSucceed === false) {
return {
message: body.Result?.ErrorMessage?.trim() || 'Request failed',
code: 'API_ERROR',
providerTrackingCode,
};
}
if (body.Result?.Result === false) {
return {
message: body.Result?.ErrorMessage?.trim() || getNoMatchMessage(inquiryType),
code: 'INQUIRY_NO_MATCH',
providerTrackingCode,
};
}
return null;
}
export function describeCentInsurResponse(body: CentInsurApiResponse): string {
const innerResult =
body.Result && 'Result' in body.Result ? String(body.Result.Result) : 'undefined';
return [
`IsSucceed=${String(body.IsSucceed)}`,
`Result.Result=${innerResult}`,
`ErrorMessage=${body.Result?.ErrorMessage ?? 'none'}`,
body.TrackingCode ? `TrackingCode=${body.TrackingCode}` : null,
]
.filter(Boolean)
.join(' | ');
}

View File

@@ -0,0 +1,32 @@
import { BaseInquiryResponseDto } from '../dto/base-inquiry-response.dto';
import { NormalizedErrorDto } from '../dto/normalized-error.dto';
export function buildInquiryResponse<T = Record<string, unknown>>(params: {
success: boolean;
provider: string;
trackingCode: string;
message: string;
duration: number;
data?: T | null;
error?: NormalizedErrorDto | null;
}): BaseInquiryResponseDto<T> {
return {
success: params.success,
provider: params.provider,
trackingCode: params.trackingCode,
message: params.message,
duration: params.duration,
data: params.success ? (params.data ?? null) : null,
error: params.success ? null : (params.error ?? null),
};
}
export function normalizeInquiryResponse<T = Record<string, unknown>>(
response: BaseInquiryResponseDto<T>,
): BaseInquiryResponseDto<T> {
return {
...response,
data: response.success ? (response.data ?? null) : null,
error: response.success ? null : (response.error ?? null),
};
}

View File

@@ -0,0 +1,124 @@
function decodeXml(value: string): string {
return value
.replace(/&apos;/g, "'")
.replace(/&quot;/g, '"')
.replace(/&gt;/g, '>')
.replace(/&lt;/g, '<')
.replace(/&amp;/g, '&');
}
function normalizeSoapTextValue(value: string): string {
if (/i:nil\s*=\s*["']true["']/i.test(value)) {
return '';
}
const nestedStrings = [
...value.matchAll(/<(?:[\w]+:)?string[^>]*>([\s\S]*?)<\/(?:[\w]+:)?string>/gi),
]
.map((match) => decodeXml(match[1].trim()))
.filter(Boolean);
if (nestedStrings.length > 0) {
return nestedStrings.join(', ');
}
return decodeXml(value.trim());
}
export interface ShahkarInquiryFields {
response?: string;
result?: string;
comment?: string;
requestId?: string;
id?: string;
ErrorNams?: string;
}
export function parseShahkarInqueryResult(soapXml: string): ShahkarInquiryFields {
const blockMatch = soapXml.match(
/<(?:[\w]+:)?ShahkarInqueryResult[^>]*>([\s\S]*?)<\/(?:[\w]+:)?ShahkarInqueryResult>/i,
);
if (!blockMatch) {
return {};
}
const fields: Record<string, string> = {};
const tagRegex = /<(?:[\w]+:)?(\w+)(?:[^>]*)>([\s\S]*?)<\/(?:[\w]+:)?\1>/gi;
let match: RegExpExecArray | null;
while ((match = tagRegex.exec(blockMatch[1])) !== null) {
fields[match[1]] = normalizeSoapTextValue(match[2]);
}
return fields;
}
export function normalizeShahkarFields(raw: Record<string, unknown>): ShahkarInquiryFields {
const readString = (value: unknown): string | undefined => {
if (value === null || value === undefined) {
return undefined;
}
const text = String(value).trim();
return text.length > 0 ? text : undefined;
};
return {
response: readString(raw.response ?? raw.Response),
result: readString(raw.result ?? raw.Result),
comment: readString(raw.comment ?? raw.Comment),
requestId: readString(raw.requestId ?? raw.RequestId),
id: readString(raw.id ?? raw.Id),
ErrorNams: readString(raw.ErrorNams ?? raw.errorNams),
};
}
export function isShahkarSuccess(fields: ShahkarInquiryFields): boolean {
const responseCode = fields.response?.trim();
const resultText = fields.result?.trim().toUpperCase() ?? '';
return responseCode === '200' && resultText.startsWith('OK');
}
export function getShahkarProviderError(
fields: ShahkarInquiryFields,
): { message: string; code: string; providerCode?: string; providerTrackingCode?: string } | null {
if (isShahkarSuccess(fields)) {
return null;
}
const comment = fields.comment?.trim();
const errorNams = fields.ErrorNams?.trim();
const resultText = fields.result?.trim();
const responseCode = fields.response?.trim();
const providerTrackingCode = fields.requestId?.trim() || undefined;
if (resultText === 'NotIdentifiedException' || responseCode === '600') {
return {
message:
comment ||
'No match found between the provided national code and mobile number',
code: 'INQUIRY_NO_MATCH',
providerCode: resultText ?? responseCode,
providerTrackingCode,
};
}
return {
message: comment || errorNams || resultText || 'Shahkar inquiry failed',
code: 'PROVIDER_ERROR',
providerCode: resultText ?? responseCode,
providerTrackingCode,
};
}
export function describeShahkarResponse(fields: ShahkarInquiryFields): string {
return [
fields.response ? `response=${fields.response}` : null,
fields.result ? `result=${fields.result}` : null,
fields.comment ? `comment=${fields.comment}` : null,
fields.requestId ? `requestId=${fields.requestId}` : null,
]
.filter(Boolean)
.join(' | ');
}

View File

@@ -5,6 +5,7 @@ import {
NestInterceptor, NestInterceptor,
} from '@nestjs/common'; } from '@nestjs/common';
import { Observable, tap } from 'rxjs'; import { Observable, tap } from 'rxjs';
import { BaseInquiryResponseDto } from '../dto/base-inquiry-response.dto';
import { RequestLogger } from '../helpers/request-logger.helper'; import { RequestLogger } from '../helpers/request-logger.helper';
import { formatHttpExceptionMessage } from '../helpers/validation-error.helper'; import { formatHttpExceptionMessage } from '../helpers/validation-error.helper';
@@ -25,11 +26,21 @@ export class LoggingInterceptor implements NestInterceptor {
return next.handle().pipe( return next.handle().pipe(
tap({ tap({
next: () => { next: (body: unknown) => {
this.requestLogger.logSuccess( const durationMs = Date.now() - start;
{ requestId: req.requestId ?? 'unknown' }, const requestId = req.requestId ?? 'unknown';
`${req.method} ${req.url} completed in ${Date.now() - start}ms`, const summary = `${req.method} ${req.url} completed in ${durationMs}ms`;
);
if (this.isBusinessFailure(body)) {
const response = body as BaseInquiryResponseDto;
this.requestLogger.logFailure(
{ requestId, durationMs },
`${summary} | businessSuccess=false | ${response.message ?? response.error?.code ?? 'failed'}`,
);
return;
}
this.requestLogger.logSuccess({ requestId }, summary);
}, },
error: (err: unknown) => { error: (err: unknown) => {
const detail = formatHttpExceptionMessage(err); const detail = formatHttpExceptionMessage(err);
@@ -42,4 +53,13 @@ export class LoggingInterceptor implements NestInterceptor {
}), }),
); );
} }
private isBusinessFailure(body: unknown): body is BaseInquiryResponseDto {
return Boolean(
body &&
typeof body === 'object' &&
'success' in body &&
(body as BaseInquiryResponseDto).success === false,
);
}
} }

View File

@@ -7,6 +7,7 @@ import {
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { BaseInquiryResponseDto } from '../dto/base-inquiry-response.dto'; import { BaseInquiryResponseDto } from '../dto/base-inquiry-response.dto';
import { normalizeInquiryResponse } from '../helpers/inquiry-response.helper';
/** /**
* Ensures inquiry endpoints always emit BaseInquiryResponseDto shape. * Ensures inquiry endpoints always emit BaseInquiryResponseDto shape.
@@ -15,7 +16,7 @@ import { BaseInquiryResponseDto } from '../dto/base-inquiry-response.dto';
export class ResponseTransformInterceptor implements NestInterceptor { export class ResponseTransformInterceptor implements NestInterceptor {
intercept(_context: ExecutionContext, next: CallHandler): Observable<unknown> { intercept(_context: ExecutionContext, next: CallHandler): Observable<unknown> {
return next.handle().pipe( return next.handle().pipe(
map((data: BaseInquiryResponseDto) => data), map((data: BaseInquiryResponseDto) => normalizeInquiryResponse(data)),
); );
} }
} }

View File

@@ -16,9 +16,23 @@ export class SayahRequestDto {
@Matches(/^\d{10}$/, { message: 'nationalCode must be exactly 10 digits' }) @Matches(/^\d{10}$/, { message: 'nationalCode must be exactly 10 digits' })
nationalCode!: string; nationalCode!: string;
@ApiPropertyOptional({ nullable: true }) @ApiPropertyOptional({
type: String,
example: '',
nullable: true,
description: 'Legal entity ID; use empty string for natural persons',
})
@IsOptional() @IsOptional()
@IsString() @IsString()
@Transform(({ value }: { value: unknown }) => {
if (value === null || value === undefined) {
return value;
}
if (typeof value === 'object') {
return '';
}
return String(value);
})
legalId?: string | null; legalId?: string | null;
@ApiProperty({ @ApiProperty({

View File

@@ -5,6 +5,7 @@ import { ProviderName } from '../common/enums/provider-name.enum';
import { BaseInquiryResponseDto } from '../common/dto/base-inquiry-response.dto'; import { BaseInquiryResponseDto } from '../common/dto/base-inquiry-response.dto';
import { NormalizedErrorDto } from '../common/dto/normalized-error.dto'; import { NormalizedErrorDto } from '../common/dto/normalized-error.dto';
import { generateTrackingCode } from '../common/helpers/tracking-code.helper'; import { generateTrackingCode } from '../common/helpers/tracking-code.helper';
import { buildInquiryResponse } from '../common/helpers/inquiry-response.helper';
import { InquiryException } from '../common/exceptions/inquiry.exception'; import { InquiryException } from '../common/exceptions/inquiry.exception';
import { InquiryLogService } from '../logging/inquiry-log.service'; import { InquiryLogService } from '../logging/inquiry-log.service';
import { import {
@@ -54,19 +55,20 @@ export class InquiryService {
trackingCode, trackingCode,
); );
const response = this.buildSuccessResponse( const response = buildInquiryResponse({
result.data, success: true,
result.provider, provider: result.provider,
trackingCode, trackingCode,
result.duration, message: `${this.getInquiryLabel(inquiryType)} completed successfully`,
`${this.getInquiryLabel(inquiryType)} completed successfully`, duration: result.duration,
); data: this.toResponseData(result.data),
});
await this.persistLog({ await this.persistLog({
inquiryType, inquiryType,
provider: result.provider, provider: result.provider,
requestPayload: payload, requestPayload: payload,
responsePayload: response.data, responsePayload: response.data ?? undefined,
status: InquiryStatus.SUCCESS, status: InquiryStatus.SUCCESS,
duration: result.duration, duration: result.duration,
requestId, requestId,
@@ -76,20 +78,20 @@ export class InquiryService {
return response; return response;
} catch (error) { } catch (error) {
const duration = Date.now() - start; const duration = Date.now() - start;
const normalized = this.extractError(error); const { normalized, provider } = this.extractFailure(error);
const response: BaseInquiryResponseDto<Record<string, unknown>> = { const response = buildInquiryResponse({
success: false, success: false,
provider: 'GATEWAY', provider: provider ?? 'GATEWAY',
trackingCode, trackingCode,
message: normalized.message, message: normalized.message,
error: normalized,
duration, duration,
}; error: normalized,
});
await this.persistLog({ await this.persistLog({
inquiryType, inquiryType,
provider: ProviderName.HAMTA, provider: (provider as ProviderName | undefined) ?? ProviderName.HAMTA,
requestPayload: payload, requestPayload: payload,
status: InquiryStatus.FAILURE, status: InquiryStatus.FAILURE,
duration, duration,
@@ -114,23 +116,6 @@ export class InquiryService {
}); });
} }
private buildSuccessResponse(
result: unknown,
provider: string,
trackingCode: string,
duration: number,
message: string,
): BaseInquiryResponseDto<Record<string, unknown>> {
return {
success: true,
provider,
trackingCode,
message,
duration,
data: this.toResponseData(result),
};
}
private toResponseData(result: unknown): Record<string, unknown> { private toResponseData(result: unknown): Record<string, unknown> {
if (this.isPersonInquiryResult(result)) { if (this.isPersonInquiryResult(result)) {
const raw = const raw =
@@ -169,16 +154,23 @@ export class InquiryService {
return inquiryType.replace(/_INQUIRY$/, '').toLowerCase().replace(/_/g, ' '); return inquiryType.replace(/_INQUIRY$/, '').toLowerCase().replace(/_/g, ' ');
} }
private extractError(error: unknown): NormalizedErrorDto { private extractFailure(error: unknown): {
normalized: NormalizedErrorDto;
provider?: string;
} {
if (error instanceof InquiryException) { if (error instanceof InquiryException) {
return error.normalizedError; return { normalized: error.normalizedError, provider: error.provider };
} }
if (error && typeof error === 'object' && 'normalizedError' in error) { if (error && typeof error === 'object' && 'normalizedError' in error) {
return (error as { normalizedError: NormalizedErrorDto }).normalizedError; return {
normalized: (error as { normalizedError: NormalizedErrorDto }).normalizedError,
};
} }
return { return {
code: 'INQUIRY_FAILED', normalized: {
message: error instanceof Error ? error.message : 'Inquiry failed', code: 'INQUIRY_FAILED',
message: error instanceof Error ? error.message : 'Inquiry failed',
},
}; };
} }

View File

@@ -126,6 +126,8 @@ export abstract class BaseProvider<TRequest = unknown, TResponse = unknown>
message: partial.message ?? 'Provider request failed', message: partial.message ?? 'Provider request failed',
providerMessage: partial.providerMessage, providerMessage: partial.providerMessage,
providerCode: partial.providerCode, providerCode: partial.providerCode,
providerTrackingCode: partial.providerTrackingCode,
details: partial.details,
}; };
} }
@@ -133,6 +135,7 @@ export abstract class BaseProvider<TRequest = unknown, TResponse = unknown>
providerMessage?: string, providerMessage?: string,
providerCode?: string, providerCode?: string,
fallbackMessage = 'Provider request failed', fallbackMessage = 'Provider request failed',
extras?: Pick<NormalizedErrorDto, 'providerTrackingCode' | 'details'>,
): Error { ): Error {
const message = providerMessage?.trim() || fallbackMessage; const message = providerMessage?.trim() || fallbackMessage;
const code = providerCode?.trim() || 'PROVIDER_ERROR'; const code = providerCode?.trim() || 'PROVIDER_ERROR';
@@ -141,6 +144,7 @@ export abstract class BaseProvider<TRequest = unknown, TResponse = unknown>
message, message,
providerMessage: message, providerMessage: message,
providerCode: code, providerCode: code,
...extras,
}); });
const err = new Error(message); const err = new Error(message);
(err as Error & { normalizedError: NormalizedErrorDto }).normalizedError = normalized; (err as Error & { normalizedError: NormalizedErrorDto }).normalizedError = normalized;

View File

@@ -6,6 +6,11 @@ import {
getSayahProviderError, getSayahProviderError,
SayahApiResponse, SayahApiResponse,
} from '../../common/helpers/sayah-response.helper'; } from '../../common/helpers/sayah-response.helper';
import {
describeShahkarResponse,
getShahkarProviderError,
parseShahkarInqueryResult,
} from '../../common/helpers/shahkar-response.helper';
import { InquiryType } from '../../common/enums/inquiry-type.enum'; import { InquiryType } from '../../common/enums/inquiry-type.enum';
import { ProviderName } from '../../common/enums/provider-name.enum'; import { ProviderName } from '../../common/enums/provider-name.enum';
import { ProviderEnvConfig } from '../../config/configuration'; import { ProviderEnvConfig } from '../../config/configuration';
@@ -210,14 +215,22 @@ export class HamtaProvider extends LegacyApiProvider {
}), }),
); );
const result = this.extractSoapValue(response.data, 'ShahkarInqueryResult'); const shahkarFields = parseShahkarInqueryResult(response.data);
const providerError = getShahkarProviderError(shahkarFields);
if (providerError) {
this.nestLogger.warn(
`SHAHKAR provider business failure | ${describeShahkarResponse(shahkarFields)}`,
);
throw this.formatProviderError(providerError.message, providerError.code, undefined, {
providerTrackingCode: providerError.providerTrackingCode,
});
}
return { return {
raw: { raw: {
nationalCode, nationalCode,
mobileNo, mobileNo,
result, ...shahkarFields,
soap: response.data,
}, },
}; };
} catch (error) { } catch (error) {

View File

@@ -7,6 +7,11 @@ import {
getCivilRegistrationProviderError, getCivilRegistrationProviderError,
parseCiiEstelamResult, parseCiiEstelamResult,
} from '../../common/helpers/soap-civil-registration.helper'; } from '../../common/helpers/soap-civil-registration.helper';
import {
getShahkarProviderError,
parseShahkarInqueryResult,
describeShahkarResponse,
} from '../../common/helpers/shahkar-response.helper';
import { import {
getSayahProviderError, getSayahProviderError,
SayahApiResponse, SayahApiResponse,
@@ -218,14 +223,22 @@ export class MoallemProvider extends LegacyApiProvider {
}), }),
); );
const result = this.extractSoapValue(response.data, 'ShahkarInqueryResult'); const shahkarFields = parseShahkarInqueryResult(response.data);
const providerError = getShahkarProviderError(shahkarFields);
if (providerError) {
this.nestLogger.warn(
`SHAHKAR provider business failure | ${describeShahkarResponse(shahkarFields)}`,
);
throw this.formatProviderError(providerError.message, providerError.code, undefined, {
providerTrackingCode: providerError.providerTrackingCode,
});
}
return { return {
raw: { raw: {
nationalCode, nationalCode,
mobileNo, mobileNo,
result, ...shahkarFields,
soap: response.data,
}, },
}; };
} catch (error) { } catch (error) {

View File

@@ -2,10 +2,12 @@ import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
import axios, { AxiosError } from 'axios'; import axios, { AxiosError } from 'axios';
import { mergeOutboundAxiosConfig } from '../../common/helpers/http-client.helper'; import { mergeOutboundAxiosConfig } from '../../common/helpers/http-client.helper';
import { getSayahProviderError, SayahApiResponse } from '../../common/helpers/sayah-response.helper';
import { import {
getSayahProviderError, describeShahkarResponse,
SayahApiResponse, getShahkarProviderError,
} from '../../common/helpers/sayah-response.helper'; normalizeShahkarFields,
} from '../../common/helpers/shahkar-response.helper';
import { InquiryType } from '../../common/enums/inquiry-type.enum'; import { InquiryType } from '../../common/enums/inquiry-type.enum';
import { ProviderName } from '../../common/enums/provider-name.enum'; import { ProviderName } from '../../common/enums/provider-name.enum';
import { ProviderExecutionContext } from '../../common/interfaces/inquiry-provider.interface'; import { ProviderExecutionContext } from '../../common/interfaces/inquiry-provider.interface';
@@ -192,11 +194,22 @@ export class ParsianProvider extends BaseProvider<LegacyInquiryPayload, LegacyIn
); );
const body = response.data; const body = response.data;
const shahkarFields = normalizeShahkarFields(body as unknown as Record<string, unknown>);
const providerError = getShahkarProviderError(shahkarFields);
if (providerError) {
this.nestLogger.warn(
`SHAHKAR provider business failure | ${describeShahkarResponse(shahkarFields)}`,
);
throw this.formatProviderError(providerError.message, providerError.code, undefined, {
providerTrackingCode: providerError.providerTrackingCode,
});
}
return { return {
raw: { raw: {
nationalCode, nationalCode,
mobileNumber, mobileNumber,
...body, ...shahkarFields,
}, },
}; };
} catch (error) { } catch (error) {

View File

@@ -1,5 +1,10 @@
import { AxiosError, AxiosInstance } from 'axios'; import { AxiosError, AxiosInstance } from 'axios';
import { createOutboundAxiosInstance } from '../../common/helpers/http-client.helper'; import { createOutboundAxiosInstance } from '../../common/helpers/http-client.helper';
import {
CentInsurApiResponse,
describeCentInsurResponse,
getCentInsurProviderError,
} from '../../common/helpers/centinsur-response.helper';
import { getSayahProviderError } from '../../common/helpers/sayah-response.helper'; import { getSayahProviderError } from '../../common/helpers/sayah-response.helper';
import { InquiryType } from '../../common/enums/inquiry-type.enum'; import { InquiryType } from '../../common/enums/inquiry-type.enum';
import { ProviderName } from '../../common/enums/provider-name.enum'; import { ProviderName } from '../../common/enums/provider-name.enum';
@@ -131,6 +136,12 @@ export abstract class LegacyApiProvider extends BaseProvider<
const body = response.data; const body = response.data;
if ('IsSucceed' in body) {
this.nestLogger.log(
`${inquiryType} provider response | http=${response.status} | ${describeCentInsurResponse(body)}`,
);
}
// Handle Sayah/Sheba API format (ReturnValue/HasError) // Handle Sayah/Sheba API format (ReturnValue/HasError)
if ('ReturnValue' in body || 'HasError' in body) { if ('ReturnValue' in body || 'HasError' in body) {
const providerError = getSayahProviderError(body); const providerError = getSayahProviderError(body);
@@ -142,11 +153,14 @@ export abstract class LegacyApiProvider extends BaseProvider<
// Handle CentInsur API format (IsSucceed/Result) // Handle CentInsur API format (IsSucceed/Result)
if ('IsSucceed' in body) { if ('IsSucceed' in body) {
if (!body.IsSucceed) { const providerError = getCentInsurProviderError(body, inquiryType);
throw this.formatProviderError( if (providerError) {
body.Result?.ErrorMessage ?? 'Request failed', this.nestLogger.warn(
'API_ERROR', `${inquiryType} provider business failure | ${describeCentInsurResponse(body)}`,
); );
throw this.formatProviderError(providerError.message, providerError.code, undefined, {
providerTrackingCode: providerError.providerTrackingCode,
});
} }
return { raw: body }; return { raw: body };
} }
@@ -172,11 +186,13 @@ export abstract class LegacyApiProvider extends BaseProvider<
} }
// Check for CentInsur format error // Check for CentInsur format error
if (data && 'IsSucceed' in data && !data.IsSucceed) { if (data && 'IsSucceed' in data) {
throw this.formatProviderError( const providerError = getCentInsurProviderError(data, inquiryType);
data.Result?.ErrorMessage ?? error.message, if (providerError) {
String(error.response?.status ?? 'API_ERROR'), throw this.formatProviderError(providerError.message, providerError.code, undefined, {
); providerTrackingCode: providerError.providerTrackingCode,
});
}
} }
throw this.formatProviderError( throw this.formatProviderError(

View File

@@ -71,9 +71,10 @@ export class ProviderOrchestratorService {
const lastError = errors[errors.length - 1]!; const lastError = errors[errors.length - 1]!;
if (providers.length === 1) { if (providers.length === 1) {
throw new InquiryException(lastError.message, lastError); throw new InquiryException(lastError.message, lastError, undefined, providers[0]!.name);
} }
const lastProvider = providers[providers.length - 1]!.name;
throw new InquiryException( throw new InquiryException(
errors.map((error) => error.message).join('; '), errors.map((error) => error.message).join('; '),
{ {
@@ -82,6 +83,8 @@ export class ProviderOrchestratorService {
providerMessage: lastError.providerMessage ?? lastError.message, providerMessage: lastError.providerMessage ?? lastError.message,
providerCode: lastError.providerCode ?? lastError.code, providerCode: lastError.providerCode ?? lastError.code,
}, },
undefined,
lastProvider,
); );
} }