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; export type LegacyInquiryResult = PersonInquiryResult | { raw: unknown }; export type LegacyAuthTokenResolver = ( providerName: ProviderName, inquiryType: InquiryType, username: string, password: string, ) => Promise; /** * 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 { 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 { const inquiryConfig = this.getInquiryConfig(inquiryType); try { const response = await this.httpClient.post( 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( 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; }> { // 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