feat: add car inquiries with resilience, ownership privacy, and retry visibility

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>
This commit is contained in:
2026-09-05 17:21:16 +03:30
parent b4be98b156
commit 61098f1bf4
40 changed files with 2595 additions and 374 deletions

View File

@@ -0,0 +1,192 @@
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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;');
}
}

View File

@@ -0,0 +1,149 @@
import { Injectable } from '@nestjs/common';
import { assertCarPolicyOwnedBy } from '../../common/helpers/car-inquiry-safety.helper';
import { resolveThirdPartyCarPolicy } from '../../common/helpers/third-party-car-rules.helper';
import { InquiryType } from '../../common/enums/inquiry-type.enum';
import { ProviderEnvConfig } from '../../config/configuration';
import { LegacyInquiryPayload } from './legacy-api.provider.abstract';
import { CentInsurCarPolicyClient } from './centinsur-car-policy.client';
export interface CarPlatePayload extends LegacyInquiryPayload {
nationalCode?: string;
plk1?: string;
plk2?: string;
plk3?: string;
plksrl?: string;
}
export interface CarChassisPayload extends LegacyInquiryPayload {
nationalCode?: string;
chassisNo?: string;
}
type ErrorFormatter = (
providerMessage?: string,
providerCode?: string,
fallbackMessage?: string,
extras?: Record<string, unknown>,
) => Error;
@Injectable()
export class CentInsurCarProviderSupport {
constructor(private readonly carPolicyClient: CentInsurCarPolicyClient) {}
async inquireCarByPlate(
config: ProviderEnvConfig,
payload: CarPlatePayload,
formatError: ErrorFormatter,
): Promise<{ raw: unknown }> {
const nationalCode = this.getRequiredString(payload.nationalCode, 'nationalCode', formatError);
const plate = {
plk1: this.getRequiredString(payload.plk1, 'plk1', formatError),
plk2: this.getRequiredString(payload.plk2, 'plk2', formatError),
plk3: this.getRequiredString(payload.plk3, 'plk3', formatError),
plksrl: this.getRequiredString(payload.plksrl, 'plksrl', formatError),
};
const policy = await this.carPolicyClient.inquireByPlate(
config,
InquiryType.CAR_BY_PLATE,
plate,
);
assertCarPolicyOwnedBy(nationalCode, policy, formatError);
return {
raw: {
nationalCode,
...plate,
...policy,
},
};
}
async inquireCarByChassis(
config: ProviderEnvConfig,
payload: CarChassisPayload,
formatError: ErrorFormatter,
): Promise<{ raw: unknown }> {
const nationalCode = this.getRequiredString(payload.nationalCode, 'nationalCode', formatError);
const chassisNo = this.getRequiredString(payload.chassisNo, 'chassisNo', formatError);
const policy = await this.carPolicyClient.inquireByChassis(
config,
InquiryType.CAR_BY_CHASSIS,
chassisNo,
);
assertCarPolicyOwnedBy(nationalCode, policy, formatError);
return {
raw: {
nationalCode,
chassisNo,
...policy,
},
};
}
async inquireThirdPartyCar(
config: ProviderEnvConfig,
payload: CarPlatePayload,
formatError: ErrorFormatter,
): Promise<{ raw: unknown }> {
const input = {
nationalCode: this.getRequiredString(payload.nationalCode, 'nationalCode', formatError),
plk1: this.getRequiredString(payload.plk1, 'plk1', formatError),
plk2: this.getRequiredString(payload.plk2, 'plk2', formatError),
plk3: this.getRequiredString(payload.plk3, 'plk3', formatError),
plksrl: this.getRequiredString(payload.plksrl, 'plksrl', formatError),
};
const resolved = await resolveThirdPartyCarPolicy(input, {
byPlate: async () =>
this.carPolicyClient.inquireByPlate(config, InquiryType.THIRD_PARTY_CAR, input),
byNationalCode: async () =>
this.carPolicyClient.inquireByNationalCode(
config,
InquiryType.THIRD_PARTY_CAR,
input.nationalCode,
),
});
if (!resolved) {
throw formatError(
undefined,
'INQUIRY_NO_MATCH',
'Inquiry returned no matching result',
);
}
return {
raw: {
...input,
...resolved.policy.raw,
selection: {
sources: resolved.sources,
vehicleGroup: resolved.policy.vehicleGroup,
isActive: resolved.policy.isActive,
isZeroKm: resolved.policy.isZeroKm,
},
},
};
}
hasCarPolicyConfig(config: ProviderEnvConfig, inquiryType: InquiryType): boolean {
return this.carPolicyClient.hasSoapCredentials(
this.carPolicyClient.resolveInquiryConfig(config, inquiryType, InquiryType.POLICY_BY_PLATE),
);
}
private getRequiredString(
value: unknown,
fieldName: string,
formatError: ErrorFormatter,
): string {
if (typeof value !== 'string' || !value.trim()) {
throw formatError(undefined, undefined, `${fieldName} is required`);
}
return value.trim();
}
}