forked from Shared/esg
final update on moallm client
This commit is contained in:
@@ -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;
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
72
src/common/helpers/centinsur-response.helper.ts
Normal file
72
src/common/helpers/centinsur-response.helper.ts
Normal 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(' | ');
|
||||||
|
}
|
||||||
32
src/common/helpers/inquiry-response.helper.ts
Normal file
32
src/common/helpers/inquiry-response.helper.ts
Normal 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),
|
||||||
|
};
|
||||||
|
}
|
||||||
124
src/common/helpers/shahkar-response.helper.ts
Normal file
124
src/common/helpers/shahkar-response.helper.ts
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
function decodeXml(value: string): string {
|
||||||
|
return value
|
||||||
|
.replace(/'/g, "'")
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/&/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(' | ');
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user