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 { ProviderExecutionContext } from '../../common/interfaces/inquiry-provider.interface'; import { ProviderEnvConfig } from '../../config/configuration'; import { BaseProvider } from '../base/base-provider.abstract'; import { AmitisProvider } from './amitis.provider'; import { LegacyInquiryPayload, LegacyInquiryResult, PersonInquiryResult, } from '../shared/legacy-api.provider.abstract'; interface ParsianCivilRegistrationPayload extends LegacyInquiryPayload { nationalCode?: string; birthDate?: string; NIN?: string; BirthDate?: string; } interface ParsianShahkarPayload extends LegacyInquiryPayload { nationalCode?: string; nationalCod?: string; NationalCod?: string; mobileNo?: string; MobileNo?: string; mobileNumber?: string; MobileNumber?: string; } interface ParsianShahkarResponse { errorNams?: string | null; ErrorNams?: string | null; comment?: string | null; id?: string | null; requestId?: string | null; response?: number; result?: string | null; } interface ParsianSayahPayload extends LegacyInquiryPayload { accountOwnerType?: string; nationalCode?: string; legalId?: string | null; sheba?: string; } interface ParsianSayahResponse { ReturnValue?: boolean; returnValue?: boolean; HasError?: boolean; hasError?: boolean; Errors?: Record | Array<{ Code?: string; Message?: string }> | null; errors?: Record | Array<{ Code?: string; Message?: string }> | null; IsSucceed?: boolean; isSucceed?: boolean; Result?: unknown; result?: unknown; } interface ParsianPolicyByChassisPayload extends LegacyInquiryPayload { chassisNo?: string; } interface ParsianPolicyByNationalCodePayload extends LegacyInquiryPayload { nationalCode?: string; } interface ParsianPolicyByPlatePayload extends LegacyInquiryPayload { plk1?: string; plk2?: string; plk3?: string; plksrl?: string; } @Injectable() export class ParsianProvider extends BaseProvider { readonly name = ProviderName.PARSIAN; readonly supportedInquiryTypes = [ InquiryType.PERSON, InquiryType.SHAHKAR, InquiryType.SHEBA, InquiryType.POLICY_BY_CHASSIS, InquiryType.POLICY_BY_PLATE, InquiryType.POLICY_BY_NATIONAL_CODE, ]; constructor( configService: ConfigService, private readonly amitisProvider: AmitisProvider, ) { super(configService.get('parsian')!); } isEnabled(): boolean { const personConfig = this.config.inquiries[InquiryType.PERSON]; const shahkarConfig = this.config.inquiries[InquiryType.SHAHKAR]; const shebaConfig = this.config.inquiries[InquiryType.SHEBA]; const policyByChassisConfig = this.config.inquiries[InquiryType.POLICY_BY_CHASSIS]; const policyByPlateConfig = this.config.inquiries[InquiryType.POLICY_BY_PLATE]; const policyByNationalCodeConfig = this.config.inquiries[InquiryType.POLICY_BY_NATIONAL_CODE]; return ( this.config.enabled && (this.hasSoapCredentials(personConfig) || (Boolean(shahkarConfig?.url) && Boolean(shahkarConfig?.apiKey)) || (Boolean(shebaConfig?.url) && Boolean(shebaConfig?.username) && Boolean(shebaConfig?.password)) || this.hasSoapCredentials(policyByChassisConfig) || this.hasSoapCredentials(policyByPlateConfig) || this.hasSoapCredentials(policyByNationalCodeConfig)) ); } protected async callProvider( inquiryType: InquiryType, payload: LegacyInquiryPayload, _context: ProviderExecutionContext, ): Promise { if (inquiryType === InquiryType.PERSON) { return this.inquireCivilRegistration(payload as ParsianCivilRegistrationPayload); } if (inquiryType === InquiryType.SHAHKAR) { return this.inquireShahkar(payload as ParsianShahkarPayload); } if (inquiryType === InquiryType.SHEBA) { return this.inquireSayah(payload as ParsianSayahPayload); } if (inquiryType === InquiryType.POLICY_BY_CHASSIS) { return this.inquirePolicyByChassis(payload as ParsianPolicyByChassisPayload); } if (inquiryType === InquiryType.POLICY_BY_PLATE) { return this.inquirePolicyByPlate(payload as ParsianPolicyByPlatePayload); } if (inquiryType === InquiryType.POLICY_BY_NATIONAL_CODE) { return this.inquirePolicyByNationalCode(payload as ParsianPolicyByNationalCodePayload); } throw this.formatProviderError( undefined, 'UNSUPPORTED_INQUIRY', `Parsian does not support ${inquiryType}`, ); } private async inquireShahkar(payload: ParsianShahkarPayload): Promise<{ raw: unknown }> { const nationalCode = this.getRequiredString( payload.nationalCode ?? payload.nationalCod ?? payload.NationalCod, 'nationalCode', ); const mobileNumber = this.getRequiredString( payload.mobileNumber ?? payload.MobileNumber ?? payload.mobileNo ?? payload.MobileNo, 'mobileNo', ); const inquiryConfig = this.config.inquiries[InquiryType.SHAHKAR]; if (!inquiryConfig?.url || !inquiryConfig.apiKey) { throw this.formatProviderError( undefined, undefined, 'Parsian Shahkar inquiry is not configured', ); } try { const response = await axios.get(inquiryConfig.url, { params: { nationalCode, mobileNumber, }, headers: { 'X-PACKAGE-API-KEY': inquiryConfig.apiKey, }, timeout: this.config.timeout, }); const body = response.data; return { raw: { nationalCode, mobileNumber, ...body, }, }; } catch (error) { if (error instanceof AxiosError) { const data = error.response?.data as ParsianShahkarResponse | undefined; throw this.formatProviderError( data?.comment ?? data?.errorNams ?? data?.ErrorNams ?? error.message, String(data?.response ?? error.response?.status ?? 'NETWORK_ERROR'), ); } throw error; } } private async inquireCivilRegistration( payload: ParsianCivilRegistrationPayload, ): Promise { const nationalCode = this.getRequiredString( payload.nationalCode ?? payload.NIN, 'nationalCode', ); const birthDate = this.getRequiredString(payload.birthDate ?? payload.BirthDate, 'birthDate'); const inquiryConfig = this.config.inquiries[InquiryType.PERSON]; if (!this.hasSoapCredentials(inquiryConfig)) { throw this.formatProviderError( undefined, undefined, 'Parsian person inquiry is not configured', ); } 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/ISabtInq/SubmitInqDteStsWithPstCod"', }, timeout: this.config.timeout, responseType: 'text', }, ); return { nationalCode, birthDate, raw: { nationalCode, birthDate, result: this.extractSoapValue(response.data, 'SubmitInqDteStsWithPstCodResult'), soap: response.data, }, }; } catch (error) { if (error instanceof AxiosError) { throw this.formatProviderError( error.message, String(error.response?.status ?? 'NETWORK_ERROR'), ); } throw error; } } private async inquirePolicyByChassis( payload: ParsianPolicyByChassisPayload, ): Promise<{ raw: unknown }> { const chassisNo = this.getRequiredString(payload.chassisNo, 'chassisNo'); const response = await this.callCarPolicySoap( InquiryType.POLICY_BY_CHASSIS, 'CIIWSPolicyChassis', { Chassisno: chassisNo }, ); return { raw: { chassisNo, ...response, }, }; } private async inquirePolicyByNationalCode( payload: ParsianPolicyByNationalCodePayload, ): Promise<{ raw: unknown }> { const nationalCode = this.getRequiredString(payload.nationalCode, 'nationalCode'); const response = await this.callCarPolicySoap( InquiryType.POLICY_BY_NATIONAL_CODE, 'CIIWSPolicyNationalId', { nationalId: nationalCode }, ); return { raw: { nationalCode, ...response, }, }; } private async inquirePolicyByPlate( payload: ParsianPolicyByPlatePayload, ): Promise<{ raw: unknown }> { const plk1 = this.getRequiredString(payload.plk1, 'plk1'); const plk2 = this.getRequiredString(payload.plk2, 'plk2'); const plk3 = this.getRequiredString(payload.plk3, 'plk3'); const plksrl = this.getRequiredString(payload.plksrl, 'plksrl'); const response = await this.callCarPolicySoap( InquiryType.POLICY_BY_PLATE, 'CIIWSPolicyVehicleMeli', { Plk1: plk1, Plk2: plk2, Plk3: plk3, Plksrl: plksrl }, 'PassWord', ); return { raw: { plk1, plk2, plk3, plksrl, ...response, }, }; } private async callCarPolicySoap( inquiryType: InquiryType, methodName: string, fields: Record, passwordFieldName = 'Password', ): Promise<{ result: string | null; soap: string }> { const inquiryConfig = this.config.inquiries[inquiryType]; if (!this.hasSoapCredentials(inquiryConfig)) { throw this.formatProviderError( undefined, undefined, `Parsian ${inquiryType} is not configured`, ); } try { const response = await axios.post( inquiryConfig.url, this.buildCarPolicyEnvelope( methodName, fields, inquiryConfig.username, inquiryConfig.password, passwordFieldName, ), { headers: { 'Content-Type': 'text/xml; charset=utf-8', SOAPAction: `"http://tempuri.org/ICarAllPlcysV4/${methodName}"`, }, timeout: this.config.timeout, responseType: 'text', }, ); return { result: this.extractSoapValue(response.data, `${methodName}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: ParsianSayahPayload): Promise<{ raw: unknown }> { const accountOwnerType = payload.accountOwnerType ?? '1'; const nationalId = payload.nationalCode ?? ''; const legalId = payload.legalId ?? null; const shebaId = this.getRequiredString(payload.sheba, 'sheba'); const inquiryConfig = this.config.inquiries[InquiryType.SHEBA]; if (!inquiryConfig?.url || !inquiryConfig.username || !inquiryConfig.password) { throw this.formatProviderError( undefined, undefined, 'Parsian Sayah inquiry is not configured', ); } const token = await this.amitisProvider.getAccessToken( ProviderName.PARSIAN, InquiryType.SHEBA, inquiryConfig.username, inquiryConfig.password, ); try { const response = await axios.post( inquiryConfig.url, { AccountOwnerType: accountOwnerType, NationalId: nationalId, LegalId: legalId, ShebaId: shebaId, }, { headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', }, timeout: this.config.timeout, }, ); if ( (response.data.IsSucceed === false || response.data.isSucceed === false) && this.hasErrors(response.data.Errors ?? response.data.errors) ) { throw this.formatProviderError( this.formatErrors(response.data.Errors ?? response.data.errors), 'SAYAH_HAS_ERROR', ); } return { raw: response.data }; } catch (error) { if (error instanceof AxiosError) { const data = error.response?.data as ParsianSayahResponse | undefined; throw this.formatProviderError( this.formatErrors(data?.Errors ?? data?.errors) ?? error.message, String(error.response?.status ?? 'NETWORK_ERROR'), ); } throw error; } } 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 hasSoapCredentials( inquiryConfig: ProviderEnvConfig['inquiries'][InquiryType], ): inquiryConfig is NonNullable { return Boolean(inquiryConfig?.url && inquiryConfig.username && inquiryConfig.password); } private buildCarPolicyEnvelope( methodName: string, fields: Record, username: string, password: string, passwordFieldName: string, ): string { const fieldXml = Object.entries(fields) .map(([name, value]) => ` <${name}>${this.escapeXml(value)}`) .join('\n'); return ` <${methodName} xmlns="http://tempuri.org/"> ${fieldXml} ${this.escapeXml(username)} <${passwordFieldName}>${this.escapeXml(password)} `; } 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 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 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(/</g, '<') .replace(/&/g, '&'); } private hasErrors( errors?: Record | Array<{ Code?: string; Message?: string }> | null, ): boolean { if (!errors) return false; return Array.isArray(errors) ? errors.length > 0 : Object.keys(errors).length > 0; } private formatErrors( errors?: Record | Array<{ Code?: string; Message?: string }> | null, ): string | undefined { if (!this.hasErrors(errors)) return undefined; if (Array.isArray(errors)) { return errors.map((error) => `${error.Code}: ${error.Message}`).join(', '); } return Object.entries(errors ?? {}) .map(([code, field]) => `${field} (code: ${code})`) .join(', '); } }