Files
esg/src/providers/shared/legacy-api.provider.abstract.ts
2026-06-15 17:13:21 +03:30

296 lines
8.9 KiB
TypeScript

import { AxiosError, AxiosInstance } from 'axios';
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 { InquiryType } from '../../common/enums/inquiry-type.enum';
import { ProviderName } from '../../common/enums/provider-name.enum';
import { ProviderExecutionContext } from '../../common/interfaces/inquiry-provider.interface';
import { BaseProvider } from '../base/base-provider.abstract';
import {
LegacyProviderApiResponse,
} from '../interfaces/provider-config.interface';
import { ProviderEnvConfig, InquiryConfig } from '../../config/configuration';
export interface PersonInquiryPayload {
nationalCode: string;
birthDate: string;
dateHasPostfix?: number;
}
export interface PersonInquiryResult {
fullName?: string;
nationalCode: string;
birthDate: string;
raw: unknown;
}
export type LegacyInquiryPayload = Record<string, unknown>;
export type LegacyInquiryResult = PersonInquiryResult | { raw: unknown };
export type LegacyAuthTokenResolver = (
providerName: ProviderName,
inquiryType: InquiryType,
username: string,
password: string,
) => Promise<string>;
/**
* Shared implementation for providers with REST API contracts (Hamta, Moallem).
*
* Supports multiple authentication methods:
* - AMITIS: Token-based authentication via AMITIS service
* - SOAP: Direct SOAP authentication (handled by subclasses)
* - NONE: No authentication required
*/
export abstract class LegacyApiProvider extends BaseProvider<
LegacyInquiryPayload,
LegacyInquiryResult
> {
protected readonly httpClient: AxiosInstance;
protected readonly providerConfig: ProviderEnvConfig;
constructor(
config: ProviderEnvConfig,
private readonly authTokenResolver?: LegacyAuthTokenResolver,
) {
super(config);
this.providerConfig = config;
this.httpClient = createOutboundAxiosInstance({
timeout: config.timeout,
headers: {
'Content-Type': 'application/json',
},
});
}
protected async callProvider(
inquiryType: InquiryType,
payload: LegacyInquiryPayload,
_context: ProviderExecutionContext,
): Promise<LegacyInquiryResult> {
if (inquiryType === InquiryType.PERSON) {
return this.inquirePerson(payload as unknown as PersonInquiryPayload, inquiryType);
}
return this.inquireGeneric(inquiryType, payload);
}
protected async inquirePerson(
payload: PersonInquiryPayload,
inquiryType: InquiryType,
): Promise<PersonInquiryResult> {
const inquiryConfig = this.getInquiryConfig(inquiryType);
try {
const response = await this.httpClient.post<LegacyProviderApiResponse>(
inquiryConfig.url,
{
nationalCode: payload.nationalCode,
birthDate: payload.birthDate,
},
await this.buildRequestConfig(inquiryType, inquiryConfig),
);
const body = response.data;
if (!body.success) {
throw this.formatProviderError(body.message, body.code);
}
return {
nationalCode: payload.nationalCode,
birthDate: payload.birthDate,
fullName: body.data?.fullName as string | undefined,
raw: body,
};
} catch (error) {
if (error instanceof AxiosError) {
const data = error.response?.data as LegacyProviderApiResponse | undefined;
throw this.formatProviderError(
data?.message ?? error.message,
data?.code ?? String(error.response?.status ?? 'NETWORK_ERROR'),
);
}
throw error;
}
}
protected async inquireGeneric(
inquiryType: InquiryType,
payload: LegacyInquiryPayload,
): Promise<{ raw: unknown }> {
const inquiryConfig = this.getInquiryConfig(inquiryType);
// Transform payload for Real Estate inquiry to match CentInsur API format
const requestPayload = this.transformPayloadForInquiryType(inquiryType, payload);
try {
const response = await this.httpClient.post<LegacyProviderApiResponse>(
inquiryConfig.url,
requestPayload,
await this.buildRequestConfig(inquiryType, inquiryConfig),
);
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)
if ('ReturnValue' in body || 'HasError' in body) {
const providerError = getSayahProviderError(body);
if (providerError) {
throw this.formatProviderError(providerError.message, providerError.code);
}
return { raw: body };
}
// Handle CentInsur API format (IsSucceed/Result)
if ('IsSucceed' in body) {
const providerError = getCentInsurProviderError(body, inquiryType);
if (providerError) {
this.nestLogger.warn(
`${inquiryType} provider business failure | ${describeCentInsurResponse(body)}`,
);
throw this.formatProviderError(providerError.message, providerError.code, undefined, {
providerTrackingCode: providerError.providerTrackingCode,
});
}
return { raw: body };
}
// Handle legacy format (success)
if (!body.success) {
throw this.formatProviderError(body.message, body.code);
}
return {
raw: body,
};
} catch (error) {
if (error instanceof AxiosError) {
const data = error.response?.data as LegacyProviderApiResponse | undefined;
// Check for Sayah/Sheba format 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
if (data && 'IsSucceed' in data) {
const providerError = getCentInsurProviderError(data, inquiryType);
if (providerError) {
throw this.formatProviderError(providerError.message, providerError.code, undefined, {
providerTrackingCode: providerError.providerTrackingCode,
});
}
}
throw this.formatProviderError(
data?.message ?? error.message,
data?.code ?? String(error.response?.status ?? 'NETWORK_ERROR'),
);
}
throw error;
}
}
/**
* Transform payload to match provider-specific API format
* Real Estate inquiry uses NationalId/PostalCode instead of nationalCode/postalCode
*/
protected transformPayloadForInquiryType(
inquiryType: InquiryType,
payload: LegacyInquiryPayload,
): LegacyInquiryPayload {
if (inquiryType === InquiryType.REAL_ESTATE) {
return {
NationalId: payload.nationalCode ?? payload.nationalId ?? payload.NationalId,
PostalCode: payload.postalCode ?? payload.PostalCode,
};
}
return payload;
}
protected getInquiryConfig(inquiryType: InquiryType): InquiryConfig {
const inquiryConfig = this.providerConfig.inquiries[inquiryType];
if (!inquiryConfig || !inquiryConfig.url) {
throw this.formatProviderError(
undefined,
undefined,
`Inquiry type ${inquiryType} is not configured for ${this.name}`,
);
}
return inquiryConfig;
}
private async buildRequestConfig(
inquiryType: InquiryType,
inquiryConfig: InquiryConfig,
): Promise<{
headers?: Record<string, string>;
}> {
// No authentication required
if (inquiryConfig.authMethod === 'NONE') {
return {};
}
// SOAP authentication is handled by subclasses (e.g., Shahkar)
if (inquiryConfig.authMethod === 'SOAP') {
return {};
}
// AMITIS token-based authentication
if (inquiryConfig.authMethod === 'AMITIS') {
if (!this.authTokenResolver) {
throw this.formatProviderError(
undefined,
undefined,
`No auth token resolver configured for ${this.name}`,
);
}
if (!inquiryConfig.username || !inquiryConfig.password) {
throw this.formatProviderError(
undefined,
undefined,
`Credentials not configured for ${this.name}/${inquiryType}`,
);
}
const token = await this.authTokenResolver(
this.name,
inquiryType,
inquiryConfig.username,
inquiryConfig.password,
);
return {
headers: {
Authorization: `Bearer ${token}`,
},
};
}
throw this.formatProviderError(
undefined,
undefined,
`Unsupported auth method ${inquiryConfig.authMethod} for ${this.name}/${inquiryType}`,
);
}
}
// Made with Bob