import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import axios, { AxiosError } from 'axios'; import { InquiryType } from '../../common/enums/inquiry-type.enum'; import { ProviderName } from '../../common/enums/provider-name.enum'; import { ProviderEnvConfig } from '../../config/configuration'; import { ProviderExecutionContext } from '../../common/interfaces/inquiry-provider.interface'; import { AmitisProvider } from './amitis.provider'; import { LegacyApiProvider, LegacyInquiryPayload, LegacyInquiryResult, } from '../shared/legacy-api.provider.abstract'; interface CivilRegistrationPayload extends LegacyInquiryPayload { nationalCode?: string; NIN?: string; birthDate?: string; BirthDate?: string; dateHasPostfix?: number; } interface PostalCodePayload extends LegacyInquiryPayload { postalCode?: string; PostalCode?: string; } interface ShahkarPayload extends LegacyInquiryPayload { nationalCode?: string; nationalCod?: string; NationalCod?: string; mobileNo?: string; MobileNo?: string; } interface SayahPayload extends LegacyInquiryPayload { accountOwnerType?: string; nationalId?: string; legalId?: string | null; shebaId?: string; } /** * Moallem provider — implements Moallem/CentInsur REST and SOAP inquiry services. */ @Injectable() export class MoallemProvider extends LegacyApiProvider { readonly name = ProviderName.MOALLEM; readonly supportedInquiryTypes = [ InquiryType.PERSON, InquiryType.SHEBA, InquiryType.SHAHKAR, InquiryType.POSTAL_CODE, ]; private readonly moallemConfig: ProviderEnvConfig; constructor(configService: ConfigService, private readonly amitisProvider: AmitisProvider) { const config = configService.get('moallem')!; super( config, (providerName, inquiryType, username, password) => amitisProvider.getAccessToken(providerName, inquiryType, username, password), ); this.moallemConfig = config; } protected async callProvider( inquiryType: InquiryType, payload: LegacyInquiryPayload, context: ProviderExecutionContext, ): Promise { if (inquiryType === InquiryType.PERSON) { return this.inquireCivilRegistration(payload as CivilRegistrationPayload); } if (inquiryType === InquiryType.POSTAL_CODE) { return this.inquirePostalCode(payload as PostalCodePayload); } if (inquiryType === InquiryType.SHAHKAR) { return this.inquireShahkar(payload as ShahkarPayload); } if (inquiryType === InquiryType.SHEBA) { return this.inquireSayah(payload as SayahPayload); } return super.callProvider(inquiryType, payload, context); } private async inquirePostalCode(payload: PostalCodePayload): Promise<{ raw: unknown }> { const postalCode = this.getRequiredString( payload.postalCode ?? payload.PostalCode, 'postalCode', ); const inquiryConfig = this.getInquiryConfig(InquiryType.POSTAL_CODE); const token = await this.amitisProvider.getAccessToken( ProviderName.MOALLEM, InquiryType.POSTAL_CODE, inquiryConfig.username, inquiryConfig.password, ); try { const response = await axios.get>( `${inquiryConfig.url}/AddressByPostcode`, { params: { PostalCode: postalCode }, headers: { Authorization: `Bearer ${token}` }, timeout: this.moallemConfig.timeout, }, ); return { raw: response.data }; } catch (error) { if (error instanceof AxiosError) { throw this.formatProviderError( error.message, String(error.response?.status ?? 'NETWORK_ERROR'), ); } throw error; } } private async inquireCivilRegistration( payload: CivilRegistrationPayload, ): Promise<{ raw: unknown }> { const nationalCode = this.getRequiredString( payload.nationalCode ?? payload.NIN, 'nationalCode', ); const birthDate = this.getRequiredString(payload.birthDate ?? payload.BirthDate, 'birthDate'); const inquiryConfig = this.getInquiryConfig(InquiryType.PERSON); try { const response = await axios.post( inquiryConfig.url, this.buildCivilRegistrationEnvelope( nationalCode, birthDate, inquiryConfig.username, inquiryConfig.password, ), { headers: { 'Content-Type': 'text/xml; charset=utf-8', SOAPAction: '"http://tempuri.org/ISabtV3/SabtInquery"', }, timeout: this.moallemConfig.timeout, responseType: 'text', }, ); const result = this.extractSoapValue(response.data, 'SabtInqueryResult'); return { raw: { nationalCode, birthDate, result, soap: response.data, }, }; } catch (error) { if (error instanceof AxiosError) { throw this.formatProviderError( error.message, String(error.response?.status ?? 'NETWORK_ERROR'), ); } throw error; } } private async inquireShahkar(payload: ShahkarPayload): Promise<{ raw: unknown }> { const nationalCode = this.getRequiredString( payload.nationalCode ?? payload.nationalCod ?? payload.NationalCod, 'nationalCode', ); const mobileNo = this.getRequiredString(payload.mobileNo ?? payload.MobileNo, 'mobileNo'); const inquiryConfig = this.getInquiryConfig(InquiryType.SHAHKAR); try { const response = await axios.post( inquiryConfig.url, this.buildShahkarEnvelope( nationalCode, mobileNo, inquiryConfig.username, inquiryConfig.password, ), { 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'); return { raw: { nationalCode, mobileNo, result, soap: response.data, }, }; } catch (error) { if (error instanceof AxiosError) { throw this.formatProviderError( error.message, String(error.response?.status ?? 'NETWORK_ERROR'), ); } throw error; } } private async inquireSayah(payload: SayahPayload): Promise<{ raw: unknown }> { const accountOwnerType = this.getRequiredString(payload.accountOwnerType, 'accountOwnerType'); const nationalId = payload.nationalId ?? ''; const legalId = payload.legalId ?? null; const shebaId = this.getRequiredString(payload.shebaId, 'shebaId'); const inquiryConfig = this.getInquiryConfig(InquiryType.SHEBA); const token = await this.amitisProvider.getAccessToken( ProviderName.MOALLEM, InquiryType.SHEBA, inquiryConfig.username, inquiryConfig.password, ); try { const response = await axios.post<{ IsSucceed?: boolean; Errors?: Array<{ Code?: string; Message?: string }>; Result?: unknown; }>( inquiryConfig.url, { AccountOwnerType: accountOwnerType, NationalId: nationalId, LegalId: legalId, ShebaId: shebaId, }, { 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', ); } return { raw: response.data }; } catch (error) { if (error instanceof AxiosError) { throw this.formatProviderError( error.message, String(error.response?.status ?? 'NETWORK_ERROR'), ); } throw error; } } private buildCivilRegistrationEnvelope( nationalCode: string, birthDate: string, username: string, password: string, ): string { return ` ${this.escapeXml(nationalCode)} ${this.escapeXml(birthDate)} ${this.escapeXml(username)} ${this.escapeXml(password)} `; } private buildShahkarEnvelope( nationalCode: string, mobileNo: string, username: string, password: string, ): string { return ` ${this.escapeXml(nationalCode)} ${this.escapeXml(mobileNo)} ${this.escapeXml(username)} ${this.escapeXml(password)} `; } private extractSoapValue(xml: string, tagName: string): string | null { const match = xml.match(new RegExp(`<(?:\\w+:)?${tagName}[^>]*>([\\s\\S]*?)`)); return match ? this.decodeXml(match[1].trim()) : null; } private getRequiredString(value: unknown, fieldName: string): string { if (typeof value !== 'string' || !value.trim()) { throw this.formatProviderError(undefined, undefined, `${fieldName} is required`); } return value.trim(); } private formatErrors(errors: Array<{ Code?: string; Message?: string }>): string { return errors.map((e) => `${e.Code}: ${e.Message}`).join(', '); } private escapeXml(value: string): string { return value .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } private decodeXml(value: string): string { return value .replace(/'/g, "'") .replace(/"/g, '"') .replace(/>/g, '>') .replace(/