parsian inquiries implemented

This commit is contained in:
2026-06-13 12:59:15 +03:30
parent ead2e301e5
commit 0aad5a34d2
19 changed files with 771 additions and 50 deletions

View File

@@ -0,0 +1,449 @@
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,
} from '../shared/legacy-api.provider.abstract';
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<string, string> | Array<{ Code?: string; Message?: string }> | null;
errors?: Record<string, string> | 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<LegacyInquiryPayload, LegacyInquiryResult> {
readonly name = ProviderName.PARSIAN;
readonly supportedInquiryTypes = [
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<ProviderEnvConfig>('parsian')!);
}
isEnabled(): boolean {
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 &&
((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<LegacyInquiryResult> {
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<ParsianShahkarResponse>(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 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<string, string>,
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<string>(
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<ParsianSayahResponse>(
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<ProviderEnvConfig['inquiries'][InquiryType]> {
return Boolean(inquiryConfig?.url && inquiryConfig.username && inquiryConfig.password);
}
private buildCarPolicyEnvelope(
methodName: string,
fields: Record<string, string>,
username: string,
password: string,
passwordFieldName: string,
): string {
const fieldXml = Object.entries(fields)
.map(([name, value]) => ` <${name}>${this.escapeXml(value)}</${name}>`)
.join('\n');
return `<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<${methodName} xmlns="http://tempuri.org/">
${fieldXml}
<UserName>${this.escapeXml(username)}</UserName>
<${passwordFieldName}>${this.escapeXml(password)}</${passwordFieldName}>
</${methodName}>
</soap:Body>
</soap:Envelope>`;
}
private extractSoapValue(xml: string, tagName: string): string | null {
const match = xml.match(
new RegExp(`<(?:\\w+:)?${tagName}[^>]*>([\\s\\S]*?)</(?:\\w+:)?${tagName}>`),
);
return match ? this.decodeXml(match[1].trim()) : null;
}
private escapeXml(value: string): string {
return value
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;');
}
private decodeXml(value: string): string {
return value
.replace(/&apos;/g, "'")
.replace(/&quot;/g, '"')
.replace(/&gt;/g, '>')
.replace(/&lt;/g, '<')
.replace(/&amp;/g, '&');
}
private hasErrors(
errors?: Record<string, string> | 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<string, string> | 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(', ');
}
}