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>
622 lines
19 KiB
TypeScript
622 lines
19 KiB
TypeScript
import { Injectable } from '@nestjs/common';
|
|
import { ConfigService } from '@nestjs/config';
|
|
import axios, { AxiosError } from 'axios';
|
|
import { mergeOutboundAxiosConfig } from '../../common/helpers/http-client.helper';
|
|
import {
|
|
buildCivilRegistrationFullName,
|
|
getCivilRegistrationProviderError,
|
|
parseCiiEstelamResult,
|
|
} from '../../common/helpers/soap-civil-registration.helper';
|
|
import {
|
|
getCarPolicyProviderError,
|
|
parseFirstCarPolicy,
|
|
} from '../../common/helpers/soap-car-policy.helper';
|
|
import { getSayahProviderError, SayahApiResponse } from '../../common/helpers/sayah-response.helper';
|
|
import { assertCarPolicyOwnedBy } from '../../common/helpers/car-inquiry-safety.helper';
|
|
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 {
|
|
CentInsurCarProviderSupport,
|
|
CarChassisPayload,
|
|
CarPlatePayload,
|
|
} from '../shared/centinsur-car-provider.support';
|
|
import {
|
|
LegacyInquiryPayload,
|
|
LegacyInquiryResult,
|
|
PersonInquiryResult,
|
|
} from '../shared/legacy-api.provider.abstract';
|
|
|
|
interface ParsianCivilRegistrationPayload extends LegacyInquiryPayload {
|
|
nationalCode?: string;
|
|
birthDate?: string;
|
|
NIN?: string;
|
|
BirthDate?: string;
|
|
dateHasPostfix?: number;
|
|
}
|
|
|
|
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 | string;
|
|
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 {
|
|
nationalCode?: string;
|
|
plk1?: string;
|
|
plk2?: string;
|
|
plk3?: string;
|
|
plksrl?: string;
|
|
}
|
|
|
|
@Injectable()
|
|
export class ParsianProvider extends BaseProvider<LegacyInquiryPayload, LegacyInquiryResult> {
|
|
readonly name = ProviderName.PARSIAN;
|
|
readonly supportedInquiryTypes = [
|
|
InquiryType.PERSON,
|
|
InquiryType.SHAHKAR,
|
|
InquiryType.SHEBA,
|
|
InquiryType.CAR_BY_PLATE,
|
|
InquiryType.CAR_BY_CHASSIS,
|
|
InquiryType.THIRD_PARTY_CAR,
|
|
InquiryType.POLICY_BY_CHASSIS,
|
|
InquiryType.POLICY_BY_PLATE,
|
|
InquiryType.POLICY_BY_NATIONAL_CODE,
|
|
];
|
|
|
|
constructor(
|
|
configService: ConfigService,
|
|
private readonly amitisProvider: AmitisProvider,
|
|
private readonly centInsurCarSupport: CentInsurCarProviderSupport,
|
|
) {
|
|
super(configService.get<ProviderEnvConfig>('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) ||
|
|
this.centInsurCarSupport.hasCarPolicyConfig(this.config, InquiryType.CAR_BY_PLATE) ||
|
|
this.centInsurCarSupport.hasCarPolicyConfig(this.config, InquiryType.THIRD_PARTY_CAR))
|
|
);
|
|
}
|
|
|
|
protected async callProvider(
|
|
inquiryType: InquiryType,
|
|
payload: LegacyInquiryPayload,
|
|
_context: ProviderExecutionContext,
|
|
): Promise<LegacyInquiryResult> {
|
|
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.CAR_BY_PLATE) {
|
|
return this.centInsurCarSupport.inquireCarByPlate(
|
|
this.config,
|
|
payload as CarPlatePayload,
|
|
(message, code, fallback, extras) =>
|
|
this.formatProviderError(message, code, fallback, extras as never),
|
|
);
|
|
}
|
|
|
|
if (inquiryType === InquiryType.CAR_BY_CHASSIS) {
|
|
return this.centInsurCarSupport.inquireCarByChassis(
|
|
this.config,
|
|
payload as CarChassisPayload,
|
|
(message, code, fallback, extras) =>
|
|
this.formatProviderError(message, code, fallback, extras as never),
|
|
);
|
|
}
|
|
|
|
if (inquiryType === InquiryType.THIRD_PARTY_CAR) {
|
|
return this.centInsurCarSupport.inquireThirdPartyCar(
|
|
this.config,
|
|
payload as CarPlatePayload,
|
|
(message, code, fallback, extras) =>
|
|
this.formatProviderError(message, code, fallback, extras as never),
|
|
);
|
|
}
|
|
|
|
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,
|
|
mergeOutboundAxiosConfig({
|
|
params: {
|
|
nationalCode,
|
|
mobileNumber,
|
|
},
|
|
headers: {
|
|
Accept: 'application/json',
|
|
'X-PACKAGE-API-KEY': inquiryConfig.apiKey,
|
|
},
|
|
timeout: this.config.timeout,
|
|
}),
|
|
);
|
|
|
|
const body = response.data;
|
|
const responseCode = Number(body.response);
|
|
|
|
if (!Number.isFinite(responseCode) || responseCode !== 200) {
|
|
throw this.formatProviderError(
|
|
body.comment ?? body.errorNams ?? body.ErrorNams ?? 'Shahkar inquiry failed',
|
|
String(body.response ?? 'PROVIDER_ERROR'),
|
|
undefined,
|
|
{
|
|
providerTrackingCode: body.requestId ?? body.id ?? undefined,
|
|
},
|
|
);
|
|
}
|
|
|
|
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<PersonInquiryResult> {
|
|
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<string>(
|
|
inquiryConfig.url,
|
|
this.buildCivilRegistrationEnvelope(
|
|
payload,
|
|
nationalCode,
|
|
birthDate,
|
|
inquiryConfig.username,
|
|
inquiryConfig.password,
|
|
),
|
|
mergeOutboundAxiosConfig({
|
|
headers: {
|
|
'Content-Type': 'text/xml; charset=utf-8',
|
|
SOAPAction: '"http://tempuri.org/ISabtInq/SubmitInqDteStsWithPstCod"',
|
|
},
|
|
timeout: this.config.timeout,
|
|
responseType: 'text',
|
|
}),
|
|
);
|
|
|
|
const civilRegistration = parseCiiEstelamResult(response.data);
|
|
const providerError = getCivilRegistrationProviderError(civilRegistration);
|
|
if (providerError) {
|
|
throw this.formatProviderError(providerError.message, providerError.code);
|
|
}
|
|
|
|
const fullName = buildCivilRegistrationFullName(civilRegistration);
|
|
|
|
return {
|
|
nationalCode,
|
|
birthDate,
|
|
fullName: fullName || undefined,
|
|
raw: civilRegistration,
|
|
};
|
|
} 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 policy = await this.callCarPolicySoap(
|
|
InquiryType.POLICY_BY_CHASSIS,
|
|
'CIIWSPolicyChassis',
|
|
{ ChassisNo: chassisNo },
|
|
);
|
|
|
|
return {
|
|
raw: {
|
|
chassisNo,
|
|
...policy,
|
|
},
|
|
};
|
|
}
|
|
|
|
private async inquirePolicyByNationalCode(
|
|
payload: ParsianPolicyByNationalCodePayload,
|
|
): Promise<{ raw: unknown }> {
|
|
const nationalCode = this.getRequiredString(payload.nationalCode, 'nationalCode');
|
|
const policy = await this.callCarPolicySoap(
|
|
InquiryType.POLICY_BY_NATIONAL_CODE,
|
|
'CIIWSPolicyNationalId',
|
|
{ NationalId: nationalCode },
|
|
);
|
|
|
|
return {
|
|
raw: {
|
|
nationalCode,
|
|
...policy,
|
|
},
|
|
};
|
|
}
|
|
|
|
private async inquirePolicyByPlate(
|
|
payload: ParsianPolicyByPlatePayload,
|
|
): Promise<{ raw: unknown }> {
|
|
const nationalCode = this.getRequiredString(payload.nationalCode, 'nationalCode');
|
|
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 policy = await this.callCarPolicySoap(
|
|
InquiryType.POLICY_BY_PLATE,
|
|
'CIIWSPolicyVehicleMeli',
|
|
{ Plk1: plk1, Plk2: plk2, Plk3: plk3, PlkSrl: plksrl },
|
|
);
|
|
|
|
assertCarPolicyOwnedBy(nationalCode, policy, (message, code, fallback, extras) =>
|
|
this.formatProviderError(message, code, fallback, extras as never),
|
|
);
|
|
|
|
return {
|
|
raw: {
|
|
nationalCode,
|
|
plk1,
|
|
plk2,
|
|
plk3,
|
|
plksrl,
|
|
...policy,
|
|
},
|
|
};
|
|
}
|
|
|
|
private async callCarPolicySoap(
|
|
inquiryType: InquiryType,
|
|
methodName: string,
|
|
fields: Record<string, string>,
|
|
): Promise<Record<string, 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,
|
|
),
|
|
mergeOutboundAxiosConfig({
|
|
headers: {
|
|
'Content-Type': 'text/xml; charset=utf-8',
|
|
SOAPAction: `"http://tempuri.org/ICarAllPlcys/${methodName}"`,
|
|
},
|
|
timeout: this.config.timeout,
|
|
responseType: 'text',
|
|
}),
|
|
);
|
|
|
|
const policy = parseFirstCarPolicy(response.data);
|
|
const providerError = getCarPolicyProviderError(response.data, policy);
|
|
if (providerError) {
|
|
throw this.formatProviderError(providerError.message, providerError.code);
|
|
}
|
|
|
|
return policy;
|
|
} 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<SayahApiResponse>(
|
|
inquiryConfig.url,
|
|
{
|
|
AccountOwnerType: accountOwnerType,
|
|
NationalId: nationalId,
|
|
LegalId: legalId,
|
|
ShebaId: shebaId,
|
|
},
|
|
mergeOutboundAxiosConfig({
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
'Content-Type': 'application/json',
|
|
},
|
|
timeout: this.config.timeout,
|
|
}),
|
|
);
|
|
|
|
const providerError = getSayahProviderError(response.data);
|
|
if (providerError) {
|
|
throw this.formatProviderError(providerError.message, providerError.code);
|
|
}
|
|
|
|
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,
|
|
): 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 buildCivilRegistrationEnvelope(
|
|
payload: ParsianCivilRegistrationPayload,
|
|
nationalCode: string,
|
|
birthDate: string,
|
|
username: string,
|
|
password: string,
|
|
): string {
|
|
const birthDateCompact = birthDate.replace(/-/g, '');
|
|
const dateHasPostfix = payload.dateHasPostfix ?? 0;
|
|
|
|
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>
|
|
<SubmitInqDteStsWithPstCod xmlns="http://tempuri.org/">
|
|
<req>
|
|
<Nin>${this.escapeXml(nationalCode)}</Nin>
|
|
<Name xsi:nil="true"/>
|
|
<Family xsi:nil="true"/>
|
|
<Fathername xsi:nil="true"/>
|
|
<Shenasnameseri xsi:nil="true"/>
|
|
<Shenasnameserial>0</Shenasnameserial>
|
|
<ShenasnameNo>0</ShenasnameNo>
|
|
<BirthDate>${this.escapeXml(birthDateCompact)}</BirthDate>
|
|
<DateHasPostfix>${dateHasPostfix}</DateHasPostfix>
|
|
<Gender>0</Gender>
|
|
<OfficeCode>0</OfficeCode>
|
|
<BookNo>0</BookNo>
|
|
<NameHasPrefix>0</NameHasPrefix>
|
|
<NameHasPostFix>0</NameHasPostFix>
|
|
<FamilyHasPrefix>0</FamilyHasPrefix>
|
|
<FamilyHasPostFix>0</FamilyHasPostFix>
|
|
</req>
|
|
<Username>${this.escapeXml(username)}</Username>
|
|
<Password>${this.escapeXml(password)}</Password>
|
|
</SubmitInqDteStsWithPstCod>
|
|
</soap:Body>
|
|
</soap:Envelope>`;
|
|
}
|
|
|
|
private escapeXml(value: string): string {
|
|
return value
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/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(', ');
|
|
}
|
|
}
|