forked from Shared/esg
Introduce plate, chassis, and third-party car endpoints, a wall-clock inquiry deadline with transport-only retries, and ownership-safe no-match errors so another person's identity never leaks. Co-authored-by: Cursor <cursoragent@cursor.com>
193 lines
5.3 KiB
TypeScript
193 lines
5.3 KiB
TypeScript
import { Injectable } from '@nestjs/common';
|
|
import axios, { AxiosError } from 'axios';
|
|
import { mergeOutboundAxiosConfig } from '../../common/helpers/http-client.helper';
|
|
import {
|
|
getCarPolicyProviderError,
|
|
parseFirstCarPolicy,
|
|
} from '../../common/helpers/soap-car-policy.helper';
|
|
import { InquiryType } from '../../common/enums/inquiry-type.enum';
|
|
import { ProviderEnvConfig, InquiryConfig } from '../../config/configuration';
|
|
|
|
export interface CarPlateSoapFields {
|
|
plk1: string;
|
|
plk2: string;
|
|
plk3: string;
|
|
plksrl: string;
|
|
}
|
|
|
|
@Injectable()
|
|
export class CentInsurCarPolicyClient {
|
|
async inquireByChassis(
|
|
config: ProviderEnvConfig,
|
|
inquiryType: InquiryType,
|
|
chassisNo: string,
|
|
): Promise<Record<string, string>> {
|
|
return this.callSoap(
|
|
config,
|
|
inquiryType,
|
|
InquiryType.POLICY_BY_CHASSIS,
|
|
'CIIWSPolicyChassis',
|
|
{ ChassisNo: chassisNo },
|
|
);
|
|
}
|
|
|
|
async inquireByPlate(
|
|
config: ProviderEnvConfig,
|
|
inquiryType: InquiryType,
|
|
plate: CarPlateSoapFields,
|
|
): Promise<Record<string, string>> {
|
|
return this.callSoap(
|
|
config,
|
|
inquiryType,
|
|
InquiryType.POLICY_BY_PLATE,
|
|
'CIIWSPolicyVehicleMeli',
|
|
{
|
|
Plk1: plate.plk1,
|
|
Plk2: plate.plk2,
|
|
Plk3: plate.plk3,
|
|
PlkSrl: plate.plksrl,
|
|
},
|
|
);
|
|
}
|
|
|
|
async inquireByNationalCode(
|
|
config: ProviderEnvConfig,
|
|
inquiryType: InquiryType,
|
|
nationalCode: string,
|
|
): Promise<Record<string, string>> {
|
|
return this.callSoap(
|
|
config,
|
|
inquiryType,
|
|
InquiryType.POLICY_BY_NATIONAL_CODE,
|
|
'CIIWSPolicyNationalId',
|
|
{ NationalId: nationalCode },
|
|
);
|
|
}
|
|
|
|
private async callSoap(
|
|
config: ProviderEnvConfig,
|
|
inquiryType: InquiryType,
|
|
fallbackInquiryType: InquiryType,
|
|
methodName: string,
|
|
fields: Record<string, string>,
|
|
): Promise<Record<string, string>> {
|
|
const inquiryConfig = this.resolveInquiryConfig(config, inquiryType, fallbackInquiryType);
|
|
|
|
if (!this.hasSoapCredentials(inquiryConfig)) {
|
|
throw new Error(`${inquiryType} is not configured for CentInsur car policy SOAP`);
|
|
}
|
|
|
|
try {
|
|
const response = await axios.post<string>(
|
|
inquiryConfig.url,
|
|
this.buildCarPolicyEnvelope(
|
|
methodName,
|
|
fields,
|
|
inquiryConfig.username,
|
|
inquiryConfig.password,
|
|
),
|
|
mergeOutboundAxiosConfig({
|
|
headers: {
|
|
'Content-Type': 'text/xml; charset=utf-8',
|
|
SOAPAction: `"http://tempuri.org/ICarAllPlcys/${methodName}"`,
|
|
},
|
|
timeout: config.timeout,
|
|
responseType: 'text',
|
|
}),
|
|
);
|
|
|
|
const policy = parseFirstCarPolicy(response.data);
|
|
const providerError = getCarPolicyProviderError(response.data, policy);
|
|
if (providerError) {
|
|
const error = new Error(providerError.message);
|
|
(error as Error & { normalizedError: { code: string; message: string } }).normalizedError = {
|
|
code: providerError.code,
|
|
message: providerError.message,
|
|
};
|
|
throw error;
|
|
}
|
|
|
|
return policy;
|
|
} catch (error) {
|
|
if (error instanceof AxiosError) {
|
|
const wrapped = new Error(error.message);
|
|
(wrapped as Error & { normalizedError: { code: string; message: string } }).normalizedError =
|
|
{
|
|
code: String(error.response?.status ?? 'NETWORK_ERROR'),
|
|
message: error.message,
|
|
};
|
|
throw wrapped;
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
resolveInquiryConfig(
|
|
config: ProviderEnvConfig,
|
|
inquiryType: InquiryType,
|
|
fallbackInquiryType: InquiryType,
|
|
): InquiryConfig {
|
|
const candidates = [
|
|
inquiryType,
|
|
fallbackInquiryType,
|
|
InquiryType.POLICY_BY_PLATE,
|
|
InquiryType.POLICY_BY_NATIONAL_CODE,
|
|
InquiryType.POLICY_BY_CHASSIS,
|
|
InquiryType.CAR_BY_PLATE,
|
|
InquiryType.THIRD_PARTY_CAR,
|
|
];
|
|
|
|
for (const candidate of candidates) {
|
|
const inquiryConfig = config.inquiries[candidate];
|
|
if (inquiryConfig?.url) {
|
|
return inquiryConfig;
|
|
}
|
|
}
|
|
|
|
return {
|
|
url: '',
|
|
username: '',
|
|
password: '',
|
|
apiKey: '',
|
|
authMethod: 'SOAP',
|
|
};
|
|
}
|
|
|
|
hasSoapCredentials(inquiryConfig: InquiryConfig | undefined): boolean {
|
|
return Boolean(inquiryConfig?.url && inquiryConfig.username && inquiryConfig.password);
|
|
}
|
|
|
|
private buildCarPolicyEnvelope(
|
|
methodName: string,
|
|
fields: Record<string, string>,
|
|
username: string,
|
|
password: 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>
|
|
<PassWrod>${this.escapeXml(password)}</PassWrod>
|
|
</${methodName}>
|
|
</soap:Body>
|
|
</soap:Envelope>`;
|
|
}
|
|
|
|
private escapeXml(value: string): string {
|
|
return value
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''');
|
|
}
|
|
}
|