forked from Yara724/api
Harden inquiry participant edge cases
This commit is contained in:
@@ -16,7 +16,7 @@ import {
|
||||
} from "src/Types&Enums/blame-request-management/accident-conditions.enum";
|
||||
import { BlameRequestType } from "src/Types&Enums/blame-request-management/blameRequestType.enum";
|
||||
import { IsEnum } from "class-validator";
|
||||
import { InquiryParticipantFieldsDto } from "./inquiry-participants.dto";
|
||||
import { InquiryParticipantFieldsDto } from "src/common/dto/inquiry-participants.dto";
|
||||
|
||||
export class InitialFormDto {
|
||||
@ApiProperty({ required: false, default: false })
|
||||
|
||||
@@ -1,144 +0,0 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import {
|
||||
IsBoolean,
|
||||
IsEnum,
|
||||
IsNotEmpty,
|
||||
IsOptional,
|
||||
IsString,
|
||||
MaxLength,
|
||||
ValidateNested,
|
||||
} from "class-validator";
|
||||
|
||||
export enum InquiryParticipantRole {
|
||||
DRIVER = "DRIVER",
|
||||
VEHICLE_OWNER = "VEHICLE_OWNER",
|
||||
THIRD_PARTY_POLICYHOLDER = "THIRD_PARTY_POLICYHOLDER",
|
||||
CAR_BODY_POLICYHOLDER = "CAR_BODY_POLICYHOLDER",
|
||||
}
|
||||
|
||||
export enum VehicleRegistrationState {
|
||||
CURRENT = "CURRENT",
|
||||
RECENTLY_TRANSFERRED = "RECENTLY_TRANSFERRED",
|
||||
}
|
||||
|
||||
export class InquiryPlateDto {
|
||||
@ApiProperty({ example: "44" })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
leftDigits: string;
|
||||
|
||||
@ApiProperty({ example: "ب" })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
centerAlphabet: string;
|
||||
|
||||
@ApiProperty({ example: "111" })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
centerDigits: string;
|
||||
|
||||
@ApiProperty({ example: "22" })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
ir: string;
|
||||
}
|
||||
|
||||
export class InquiryParticipantInputDto {
|
||||
@ApiPropertyOptional({ enum: InquiryParticipantRole })
|
||||
@IsOptional()
|
||||
@IsEnum(InquiryParticipantRole)
|
||||
sameAs?: InquiryParticipantRole;
|
||||
|
||||
@ApiPropertyOptional({ example: "0012345678" })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
nationalCode?: string;
|
||||
|
||||
@ApiPropertyOptional({ example: "1370/01/01" })
|
||||
@IsOptional()
|
||||
birthday?: string | number;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
fullName?: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
phoneNumber?: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: "Required for a driver who has a licence.",
|
||||
})
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
hasDrivingLicense?: boolean;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
licenseNumber?: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
licenseType?: string;
|
||||
}
|
||||
|
||||
export class InquiryVehicleInputDto {
|
||||
@ApiProperty({ enum: VehicleRegistrationState })
|
||||
@IsEnum(VehicleRegistrationState)
|
||||
registrationState: VehicleRegistrationState;
|
||||
|
||||
@ApiProperty({ type: InquiryPlateDto })
|
||||
@ValidateNested()
|
||||
@Type(() => InquiryPlateDto)
|
||||
currentPlate: InquiryPlateDto;
|
||||
|
||||
@ApiPropertyOptional({ type: InquiryPlateDto })
|
||||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@Type(() => InquiryPlateDto)
|
||||
previousPlate?: InquiryPlateDto;
|
||||
|
||||
@ApiPropertyOptional({ maxLength: 17, example: "NAAM01E15HK123456" })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(17)
|
||||
vin?: string;
|
||||
}
|
||||
|
||||
/** New role-complete contract mixed into every inquiry DTO. Legacy fields remain during rollout. */
|
||||
export class InquiryParticipantFieldsDto {
|
||||
@ApiPropertyOptional({ type: InquiryParticipantInputDto })
|
||||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@Type(() => InquiryParticipantInputDto)
|
||||
driver?: InquiryParticipantInputDto;
|
||||
|
||||
@ApiPropertyOptional({ type: InquiryParticipantInputDto })
|
||||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@Type(() => InquiryParticipantInputDto)
|
||||
vehicleOwner?: InquiryParticipantInputDto;
|
||||
|
||||
@ApiPropertyOptional({ type: InquiryParticipantInputDto })
|
||||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@Type(() => InquiryParticipantInputDto)
|
||||
thirdPartyPolicyholder?: InquiryParticipantInputDto;
|
||||
|
||||
@ApiPropertyOptional({ type: InquiryParticipantInputDto })
|
||||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@Type(() => InquiryParticipantInputDto)
|
||||
carBodyPolicyholder?: InquiryParticipantInputDto;
|
||||
|
||||
@ApiPropertyOptional({ type: InquiryVehicleInputDto })
|
||||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@Type(() => InquiryVehicleInputDto)
|
||||
vehicle?: InquiryVehicleInputDto;
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
ValidateNested,
|
||||
} from "class-validator";
|
||||
import { Type } from "class-transformer";
|
||||
import { InquiryParticipantFieldsDto } from "./inquiry-participants.dto";
|
||||
import { InquiryParticipantFieldsDto } from "src/common/dto/inquiry-participants.dto";
|
||||
|
||||
class PlateV6Dto {
|
||||
@ApiProperty({ example: "44", description: "Left two digits" })
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
ValidateNested,
|
||||
} from "class-validator";
|
||||
import { Type } from "class-transformer";
|
||||
import { InquiryParticipantFieldsDto } from "./inquiry-participants.dto";
|
||||
import { InquiryParticipantFieldsDto } from "src/common/dto/inquiry-participants.dto";
|
||||
|
||||
class PlateV3Dto {
|
||||
@ApiProperty({ example: "44", description: "Left two digits" })
|
||||
|
||||
@@ -66,6 +66,7 @@ export class InquiryParticipant {
|
||||
@Prop({ type: Boolean }) hasDrivingLicense?: boolean;
|
||||
@Prop() licenseNumber?: string;
|
||||
@Prop() licenseType?: string;
|
||||
@Prop({ type: Boolean }) unknown?: boolean;
|
||||
}
|
||||
export const InquiryParticipantSchema =
|
||||
SchemaFactory.createForClass(InquiryParticipant);
|
||||
|
||||
@@ -3,9 +3,10 @@ import { BlameRequestType } from "src/Types&Enums/blame-request-management/blame
|
||||
import {
|
||||
InquiryParticipantRole,
|
||||
VehicleRegistrationState,
|
||||
} from "./dto/inquiry-participants.dto";
|
||||
} from "src/common/dto/inquiry-participants.dto";
|
||||
import {
|
||||
assertPreviousPlateInquiryMatchesVin,
|
||||
isMappedPolicyCurrent,
|
||||
normalizeInquirySubmission,
|
||||
participantForRole,
|
||||
resolveInquiryParticipants,
|
||||
@@ -52,6 +53,19 @@ describe("inquiry participant resolver", () => {
|
||||
).toThrow(BadRequestException);
|
||||
});
|
||||
|
||||
it("defaults an omitted registration state to CURRENT", () => {
|
||||
expect(
|
||||
resolveInquiryVehicle({
|
||||
currentPlate: {
|
||||
leftDigits: "44",
|
||||
centerAlphabet: "ب",
|
||||
centerDigits: "111",
|
||||
ir: "22",
|
||||
},
|
||||
}).registrationState,
|
||||
).toBe(VehicleRegistrationState.CURRENT);
|
||||
});
|
||||
|
||||
it("keeps legacy driver/policyholder payloads working during rollout", () => {
|
||||
const resolved = resolveInquiryParticipants(BlameRequestType.THIRD_PARTY, {
|
||||
nationalCodeOfDriver: "0012345678",
|
||||
@@ -261,4 +275,177 @@ describe("inquiry participant resolver", () => {
|
||||
}),
|
||||
).toThrow(BadRequestException);
|
||||
});
|
||||
|
||||
it("rejects sameAs together with any person-specific fields", () => {
|
||||
expect(() =>
|
||||
resolveInquiryParticipants(BlameRequestType.THIRD_PARTY, {
|
||||
driver: {
|
||||
nationalCode: "0012345678",
|
||||
birthday: "1370/01/01",
|
||||
hasDrivingLicense: false,
|
||||
},
|
||||
vehicleOwner: {
|
||||
sameAs: InquiryParticipantRole.DRIVER,
|
||||
fullName: "Duplicate details must not be silently discarded",
|
||||
},
|
||||
thirdPartyPolicyholder: {
|
||||
sameAs: InquiryParticipantRole.DRIVER,
|
||||
},
|
||||
}),
|
||||
).toThrow(BadRequestException);
|
||||
});
|
||||
|
||||
it("rejects conflicting legacy and structured current plates", () => {
|
||||
expect(() =>
|
||||
normalizeInquirySubmission(BlameRequestType.THIRD_PARTY, {
|
||||
plate: {
|
||||
leftDigits: "44",
|
||||
centerAlphabet: "ب",
|
||||
centerDigits: "111",
|
||||
ir: "22",
|
||||
},
|
||||
vehicle: {
|
||||
registrationState: VehicleRegistrationState.CURRENT,
|
||||
currentPlate: {
|
||||
leftDigits: "55",
|
||||
centerAlphabet: "ج",
|
||||
centerDigits: "222",
|
||||
ir: "33",
|
||||
},
|
||||
},
|
||||
driver: {
|
||||
nationalCode: "0012345678",
|
||||
birthday: "1370/01/01",
|
||||
hasDrivingLicense: false,
|
||||
},
|
||||
vehicleOwner: { sameAs: InquiryParticipantRole.DRIVER },
|
||||
thirdPartyPolicyholder: {
|
||||
sameAs: InquiryParticipantRole.DRIVER,
|
||||
},
|
||||
}),
|
||||
).toThrow(BadRequestException);
|
||||
});
|
||||
|
||||
it("treats an expired policy result as stale", () => {
|
||||
expect(isMappedPolicyCurrent({ EndDate: "1404/01/01" }, "2026-09-13")).toBe(
|
||||
false,
|
||||
);
|
||||
expect(isMappedPolicyCurrent({ EndDate: "1406/01/01" }, "2026-09-13")).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it("falls back from a stale current policy to a current previous-plate policy", async () => {
|
||||
const currentPlate = {
|
||||
leftDigits: "44",
|
||||
centerAlphabet: "ب",
|
||||
centerDigits: "111",
|
||||
ir: "22",
|
||||
};
|
||||
const previousPlate = {
|
||||
leftDigits: "55",
|
||||
centerAlphabet: "ج",
|
||||
centerDigits: "222",
|
||||
ir: "33",
|
||||
};
|
||||
const query = jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce({
|
||||
mapped: { CompanyName: "پارسیان", EndDate: "1404/01/01" },
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
mapped: {
|
||||
CompanyName: "پارسیان",
|
||||
EndDate: "1406/01/01",
|
||||
VinNumberField: "NAAM01E15HK123456",
|
||||
},
|
||||
});
|
||||
|
||||
const result = await runPlateInquiryWithFallback<{
|
||||
mapped: Record<string, any>;
|
||||
}>({
|
||||
vehicle: resolveInquiryVehicle({
|
||||
registrationState: VehicleRegistrationState.RECENTLY_TRANSFERRED,
|
||||
currentPlate,
|
||||
previousPlate,
|
||||
vin: "NAAM01E15HK123456",
|
||||
}),
|
||||
fallbackCurrentPlate: currentPlate,
|
||||
query,
|
||||
isUsable: (value) =>
|
||||
!!value.mapped.CompanyName &&
|
||||
isMappedPolicyCurrent(value.mapped, "2026-09-13"),
|
||||
mappedValue: (value) => value.mapped,
|
||||
});
|
||||
|
||||
expect(result.plateKind).toBe("PREVIOUS");
|
||||
expect(result.attempts[0]).toMatchObject({
|
||||
plateKind: "CURRENT",
|
||||
succeeded: true,
|
||||
usable: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects the result and retains audit attempts when every plate is unusable", async () => {
|
||||
const currentPlate = {
|
||||
leftDigits: "44",
|
||||
centerAlphabet: "ب",
|
||||
centerDigits: "111",
|
||||
ir: "22",
|
||||
};
|
||||
const previousPlate = {
|
||||
leftDigits: "55",
|
||||
centerAlphabet: "ج",
|
||||
centerDigits: "222",
|
||||
ir: "33",
|
||||
};
|
||||
|
||||
await expect(
|
||||
runPlateInquiryWithFallback({
|
||||
vehicle: resolveInquiryVehicle({
|
||||
registrationState: VehicleRegistrationState.RECENTLY_TRANSFERRED,
|
||||
currentPlate,
|
||||
previousPlate,
|
||||
vin: "NAAM01E15HK123456",
|
||||
}),
|
||||
fallbackCurrentPlate: currentPlate,
|
||||
query: async () => ({ mapped: { CompanyName: "پارسیان" } }),
|
||||
isUsable: () => false,
|
||||
mappedValue: (value) => value.mapped,
|
||||
}),
|
||||
).rejects.toMatchObject({
|
||||
attempts: [
|
||||
{ plateKind: "CURRENT", succeeded: true, usable: false },
|
||||
{ plateKind: "PREVIOUS", succeeded: true, usable: false },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("allows an explicitly unknown policyholder only for a damaged-party submission", () => {
|
||||
const input = {
|
||||
driver: {
|
||||
nationalCode: "0012345678",
|
||||
birthday: "1370/01/01",
|
||||
hasDrivingLicense: false,
|
||||
},
|
||||
vehicleOwner: { sameAs: InquiryParticipantRole.DRIVER },
|
||||
thirdPartyPolicyholder: { unknown: true },
|
||||
};
|
||||
|
||||
expect(() =>
|
||||
resolveInquiryParticipants(BlameRequestType.THIRD_PARTY, input),
|
||||
).toThrow(BadRequestException);
|
||||
|
||||
const resolved = resolveInquiryParticipants(
|
||||
BlameRequestType.THIRD_PARTY,
|
||||
input,
|
||||
{ allowUnknownThirdPartyPolicyholder: true },
|
||||
);
|
||||
expect(
|
||||
participantForRole(
|
||||
resolved,
|
||||
InquiryParticipantRole.THIRD_PARTY_POLICYHOLDER,
|
||||
),
|
||||
).toMatchObject({ unknown: true });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { BadRequestException } from "@nestjs/common";
|
||||
import { BlameRequestType } from "src/Types&Enums/blame-request-management/blameRequestType.enum";
|
||||
import { jalaliToGregorianDate } from "src/helpers/date-jalali";
|
||||
import { gregorianDateInIran } from "src/helpers/iran-datetime";
|
||||
import {
|
||||
InquiryParticipantFieldsDto,
|
||||
InquiryParticipantInputDto,
|
||||
InquiryParticipantRole,
|
||||
InquiryVehicleInputDto,
|
||||
VehicleRegistrationState,
|
||||
} from "./dto/inquiry-participants.dto";
|
||||
} from "src/common/dto/inquiry-participants.dto";
|
||||
|
||||
export interface ResolvedInquiryParticipant {
|
||||
participantId: string;
|
||||
@@ -17,6 +19,11 @@ export interface ResolvedInquiryParticipant {
|
||||
hasDrivingLicense?: boolean;
|
||||
licenseNumber?: string;
|
||||
licenseType?: string;
|
||||
unknown?: boolean;
|
||||
}
|
||||
|
||||
export interface InquiryParticipantResolutionOptions {
|
||||
allowUnknownThirdPartyPolicyholder?: boolean;
|
||||
}
|
||||
|
||||
export interface InquiryParticipantRoleAssignments {
|
||||
@@ -49,9 +56,13 @@ export interface NormalizedInquirySubmission<T extends Record<string, any>> {
|
||||
vehicleOwner?: ResolvedInquiryParticipant;
|
||||
thirdPartyPolicyholder: ResolvedInquiryParticipant;
|
||||
carBodyPolicyholder?: ResolvedInquiryParticipant;
|
||||
vehicle?: InquiryVehicleInputDto;
|
||||
vehicle?: ResolvedInquiryVehicle;
|
||||
}
|
||||
|
||||
export type ResolvedInquiryVehicle = InquiryVehicleInputDto & {
|
||||
registrationState: VehicleRegistrationState;
|
||||
};
|
||||
|
||||
export function participantForRole(
|
||||
resolved: ResolvedInquiryParticipants,
|
||||
role: InquiryParticipantRole,
|
||||
@@ -119,6 +130,7 @@ function requiredIdentity(
|
||||
export function resolveInquiryParticipants(
|
||||
caseType: BlameRequestType,
|
||||
input: InquiryParticipantFieldsDto & Record<string, any>,
|
||||
options: InquiryParticipantResolutionOptions = {},
|
||||
): ResolvedInquiryParticipants {
|
||||
const hasRoleCompleteInput = Object.values(ROLE_FIELDS).some(
|
||||
(field) => input[field] != null,
|
||||
@@ -200,8 +212,40 @@ export function resolveInquiryParticipants(
|
||||
|
||||
resolving.add(role);
|
||||
let participantId: string;
|
||||
if (value.sameAs) {
|
||||
if (value.nationalCode != null || value.birthday != null) {
|
||||
if (
|
||||
role === InquiryParticipantRole.THIRD_PARTY_POLICYHOLDER &&
|
||||
value.unknown === true
|
||||
) {
|
||||
if (!options.allowUnknownThirdPartyPolicyholder) {
|
||||
throw new BadRequestException(
|
||||
"An unknown third-party policyholder is allowed only for the damaged party.",
|
||||
);
|
||||
}
|
||||
const hasIdentityFields = Object.entries(value).some(
|
||||
([field, fieldValue]) => field !== "unknown" && fieldValue != null,
|
||||
);
|
||||
if (hasIdentityFields) {
|
||||
throw new BadRequestException(
|
||||
`${role} must contain either unknown=true or person fields, not both.`,
|
||||
);
|
||||
}
|
||||
const participant: ResolvedInquiryParticipant = {
|
||||
participantId: role,
|
||||
nationalCode: "",
|
||||
birthday: "",
|
||||
unknown: true,
|
||||
};
|
||||
participants.set(participant.participantId, participant);
|
||||
participantId = participant.participantId;
|
||||
} else if (value.unknown != null) {
|
||||
throw new BadRequestException(
|
||||
`${role} does not support the unknown option.`,
|
||||
);
|
||||
} else if (value.sameAs) {
|
||||
const hasPersonSpecificFields = Object.entries(value).some(
|
||||
([field, fieldValue]) => field !== "sameAs" && fieldValue != null,
|
||||
);
|
||||
if (hasPersonSpecificFields) {
|
||||
throw new BadRequestException(
|
||||
`${role} must contain either sameAs or identity fields, not both.`,
|
||||
);
|
||||
@@ -249,7 +293,7 @@ export function resolveInquiryParticipants(
|
||||
|
||||
export function resolveInquiryVehicle(
|
||||
input: InquiryVehicleInputDto,
|
||||
): InquiryVehicleInputDto {
|
||||
): ResolvedInquiryVehicle {
|
||||
const registrationState =
|
||||
input.registrationState ?? VehicleRegistrationState.CURRENT;
|
||||
if (!input.currentPlate) {
|
||||
@@ -274,7 +318,7 @@ export function resolveInquiryVehicle(
|
||||
return { ...input, registrationState };
|
||||
}
|
||||
|
||||
export function vehiclePlateCandidates(input?: InquiryVehicleInputDto): Array<{
|
||||
export function vehiclePlateCandidates(input?: ResolvedInquiryVehicle): Array<{
|
||||
kind: "CURRENT" | "PREVIOUS";
|
||||
plate: InquiryVehicleInputDto["currentPlate"];
|
||||
}> {
|
||||
@@ -294,6 +338,35 @@ function normalizeVehicleSerial(value: unknown): string {
|
||||
.replace(/[^A-Z0-9]/g, "");
|
||||
}
|
||||
|
||||
/** A dated result is usable only while the returned policy has not expired. */
|
||||
export function isMappedPolicyCurrent(
|
||||
mapped: Record<string, any>,
|
||||
todayGregorian: string = gregorianDateInIran(new Date()),
|
||||
): boolean {
|
||||
const rawEndDate =
|
||||
mapped?.EndDate ??
|
||||
mapped?.HEndDte ??
|
||||
mapped?.persianEndDate ??
|
||||
mapped?.PolicyEndDate ??
|
||||
mapped?.endDate;
|
||||
if (rawEndDate == null || String(rawEndDate).trim() === "") return true;
|
||||
const endDate = jalaliToGregorianDate(rawEndDate);
|
||||
return endDate != null && endDate >= todayGregorian;
|
||||
}
|
||||
|
||||
function normalizePlateForComparison(
|
||||
plate: InquiryVehicleInputDto["currentPlate"],
|
||||
): string {
|
||||
return [
|
||||
plate?.ir,
|
||||
plate?.leftDigits,
|
||||
plate?.centerAlphabet,
|
||||
plate?.centerDigits,
|
||||
]
|
||||
.map((part) => String(part ?? "").trim())
|
||||
.join("|");
|
||||
}
|
||||
|
||||
export function assertPreviousPlateInquiryMatchesVin(
|
||||
expectedVin: string,
|
||||
mapped: Record<string, any>,
|
||||
@@ -319,7 +392,7 @@ export function assertPreviousPlateInquiryMatchesVin(
|
||||
}
|
||||
|
||||
export async function runPlateInquiryWithFallback<T>(options: {
|
||||
vehicle?: InquiryVehicleInputDto;
|
||||
vehicle?: ResolvedInquiryVehicle;
|
||||
fallbackCurrentPlate: InquiryVehicleInputDto["currentPlate"];
|
||||
query: (plate: InquiryVehicleInputDto["currentPlate"]) => Promise<T>;
|
||||
isUsable: (value: T) => boolean;
|
||||
@@ -358,7 +431,11 @@ export async function runPlateInquiryWithFallback<T>(options: {
|
||||
usable: false,
|
||||
});
|
||||
if (!isLast) continue;
|
||||
return { value, plateKind: candidate.kind, attempts };
|
||||
const error = new BadRequestException(
|
||||
"No current usable policy was found for the submitted vehicle identifiers.",
|
||||
) as BadRequestException & { attempts?: typeof attempts };
|
||||
error.attempts = attempts;
|
||||
throw error;
|
||||
}
|
||||
if (candidate.kind === "PREVIOUS" && usable) {
|
||||
assertPreviousPlateInquiryMatchesVin(
|
||||
@@ -374,12 +451,23 @@ export async function runPlateInquiryWithFallback<T>(options: {
|
||||
return { value, plateKind: candidate.kind, attempts };
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
attempts.push({
|
||||
plateKind: candidate.kind,
|
||||
succeeded: false,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
if (isLast) throw error;
|
||||
const alreadyRecorded =
|
||||
typeof error === "object" &&
|
||||
error != null &&
|
||||
Array.isArray((error as { attempts?: unknown }).attempts);
|
||||
if (!alreadyRecorded) {
|
||||
attempts.push({
|
||||
plateKind: candidate.kind,
|
||||
succeeded: false,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
if (isLast) {
|
||||
if (typeof error === "object" && error != null) {
|
||||
(error as { attempts?: typeof attempts }).attempts = attempts;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -389,8 +477,9 @@ export async function runPlateInquiryWithFallback<T>(options: {
|
||||
export function normalizeInquirySubmission<T extends Record<string, any>>(
|
||||
caseType: BlameRequestType,
|
||||
input: T,
|
||||
options: InquiryParticipantResolutionOptions = {},
|
||||
): NormalizedInquirySubmission<T> {
|
||||
const participants = resolveInquiryParticipants(caseType, input);
|
||||
const participants = resolveInquiryParticipants(caseType, input, options);
|
||||
const driver = participantForRole(
|
||||
participants,
|
||||
InquiryParticipantRole.DRIVER,
|
||||
@@ -415,6 +504,16 @@ export function normalizeInquirySubmission<T extends Record<string, any>>(
|
||||
const vehicle = input.vehicle
|
||||
? resolveInquiryVehicle(input.vehicle as InquiryVehicleInputDto)
|
||||
: undefined;
|
||||
if (
|
||||
vehicle &&
|
||||
input.plate &&
|
||||
normalizePlateForComparison(vehicle.currentPlate) !==
|
||||
normalizePlateForComparison(input.plate)
|
||||
) {
|
||||
throw new BadRequestException(
|
||||
"plate and vehicle.currentPlate must identify the same current plate.",
|
||||
);
|
||||
}
|
||||
const sameDriverAndPolicyholder =
|
||||
participants.roles.driver === participants.roles.thirdPartyPolicyholder;
|
||||
|
||||
|
||||
@@ -124,6 +124,7 @@ import { resolveLinkedUserIdStrings } from "src/helpers/user-access-resolver";
|
||||
import { normalizePlateText } from "src/utils/plate-normalizer/plate-normalizer.service";
|
||||
import { fanavaranClaimReferences } from "src/claim-request-management/fanavaran-claim-references";
|
||||
import {
|
||||
isMappedPolicyCurrent,
|
||||
NormalizedInquirySubmission,
|
||||
normalizeInquirySubmission,
|
||||
runPlateInquiryWithFallback,
|
||||
@@ -308,8 +309,12 @@ export class RequestManagementService {
|
||||
private normalizeInquiryInput<T extends Record<string, any>>(
|
||||
type: BlameRequestType,
|
||||
input: T,
|
||||
partyRole?: PartyRole,
|
||||
): NormalizedInquirySubmission<T> {
|
||||
return normalizeInquirySubmission(type, input);
|
||||
return normalizeInquirySubmission(type, input, {
|
||||
allowUnknownThirdPartyPolicyholder:
|
||||
type === BlameRequestType.THIRD_PARTY && partyRole === PartyRole.SECOND,
|
||||
});
|
||||
}
|
||||
|
||||
private applyInquiryParticipantsToParty(
|
||||
@@ -335,6 +340,15 @@ export class RequestManagementService {
|
||||
submission: NormalizedInquirySubmission<Record<string, any>>,
|
||||
options?: Record<string, any>,
|
||||
): Promise<any> {
|
||||
if (submission.thirdPartyPolicyholder.unknown) {
|
||||
return {
|
||||
raw: null,
|
||||
mapped: {},
|
||||
skipped: true,
|
||||
plateKind: "CURRENT",
|
||||
attempts: [],
|
||||
};
|
||||
}
|
||||
const result = await runPlateInquiryWithFallback({
|
||||
vehicle: submission.vehicle,
|
||||
fallbackCurrentPlate: submission.dto.plate,
|
||||
@@ -348,7 +362,9 @@ export class RequestManagementService {
|
||||
options,
|
||||
),
|
||||
isUsable: (value) =>
|
||||
!value?.mapped?.Error && !!value?.mapped?.CompanyName,
|
||||
!value?.mapped?.Error &&
|
||||
!!value?.mapped?.CompanyName &&
|
||||
isMappedPolicyCurrent(value.mapped),
|
||||
mappedValue: (value) => value?.mapped ?? {},
|
||||
});
|
||||
return {
|
||||
@@ -373,6 +389,7 @@ export class RequestManagementService {
|
||||
}),
|
||||
isUsable: (value) =>
|
||||
!!value?.mapped &&
|
||||
isMappedPolicyCurrent(value.mapped) &&
|
||||
!!(
|
||||
value.mapped.policyNumber ||
|
||||
value.mapped.CompanyName ||
|
||||
@@ -395,7 +412,9 @@ export class RequestManagementService {
|
||||
): Promise<void> {
|
||||
const participants = submission.participants.legacy
|
||||
? [submission.thirdPartyPolicyholder]
|
||||
: submission.participants.participants;
|
||||
: submission.participants.participants.filter(
|
||||
(participant) => !participant.unknown,
|
||||
);
|
||||
const results: Record<string, unknown> = {};
|
||||
|
||||
for (const participant of participants) {
|
||||
@@ -535,6 +554,7 @@ export class RequestManagementService {
|
||||
message: err?.message || String(err),
|
||||
status: err?.response?.status,
|
||||
data: err?.response?.data,
|
||||
...(Array.isArray(err?.attempts) ? { attempts: err.attempts } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1575,9 +1595,15 @@ export class RequestManagementService {
|
||||
: undefined;
|
||||
}
|
||||
|
||||
const inquirySubmission = this.normalizeInquiryInput(req.type, body);
|
||||
const inquirySubmission = this.normalizeInquiryInput(
|
||||
req.type,
|
||||
body,
|
||||
role,
|
||||
);
|
||||
body = inquirySubmission.dto as unknown as AddPlateDto;
|
||||
this.applyInquiryParticipantsToParty(party, inquirySubmission);
|
||||
const policyholderUnknown =
|
||||
inquirySubmission.thirdPartyPolicyholder.unknown === true;
|
||||
|
||||
// Validation: driver/insurer sameness rules
|
||||
if (body.driverIsInsurer === false) {
|
||||
@@ -1637,13 +1663,25 @@ export class RequestManagementService {
|
||||
this.logger.log(
|
||||
`[TEJARAT] block inquiry mapped for request=${req._id}: ${JSON.stringify(inquiryMapped)}`,
|
||||
);
|
||||
this.recordPartyCaseInquiryStatus(req, "thirdParty", role, true, {
|
||||
source: inquirySource,
|
||||
plateKind: inquiry.plateKind,
|
||||
attempts: inquiry.attempts,
|
||||
raw: inquiryRaw,
|
||||
mapped: inquiryMapped,
|
||||
});
|
||||
this.recordPartyCaseInquiryStatus(
|
||||
req,
|
||||
"thirdParty",
|
||||
role,
|
||||
!inquiry.skipped,
|
||||
{
|
||||
source: inquirySource,
|
||||
plateKind: inquiry.plateKind,
|
||||
attempts: inquiry.attempts,
|
||||
raw: inquiryRaw,
|
||||
mapped: inquiryMapped,
|
||||
...(inquiry.skipped
|
||||
? {
|
||||
skipped: true,
|
||||
reason: "Third-party policyholder is unknown",
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
);
|
||||
if (inquiry.offline?.fanavaranDriverId != null) {
|
||||
if (!party.person) party.person = {} as any;
|
||||
party.person.fanavaranDriverId = inquiry.offline.fanavaranDriverId;
|
||||
@@ -1714,17 +1752,19 @@ export class RequestManagementService {
|
||||
|
||||
// Find client by company code
|
||||
const clientName = inquiryMapped?.CompanyName;
|
||||
if (!clientName) {
|
||||
if (!clientName && !policyholderUnknown) {
|
||||
throw new BadRequestException(
|
||||
`CompanyName missing from inquiry response`,
|
||||
);
|
||||
}
|
||||
const companyCode = inquiryMapped?.CompanyCode;
|
||||
const client = await this.clientService.findOrCreateClientByCompanyCode(
|
||||
companyCode,
|
||||
clientName,
|
||||
);
|
||||
if (!client) {
|
||||
const client = clientName
|
||||
? await this.clientService.findOrCreateClientByCompanyCode(
|
||||
companyCode,
|
||||
clientName,
|
||||
)
|
||||
: null;
|
||||
if (clientName && !client) {
|
||||
throw new BadRequestException(
|
||||
`CompanyCode missing or invalid in inquiry response`,
|
||||
);
|
||||
@@ -1734,7 +1774,7 @@ export class RequestManagementService {
|
||||
if (!party.person) party.person = {} as any;
|
||||
const resolvedClientId =
|
||||
(client as any)?._id ?? (client as any)?._doc?._id;
|
||||
party.person.clientId = resolvedClientId;
|
||||
if (resolvedClientId) party.person.clientId = resolvedClientId;
|
||||
party.person.nationalCodeOfInsurer = body.nationalCodeOfInsurer;
|
||||
party.person.nationalCodeOfDriver = body.nationalCodeOfDriver;
|
||||
party.person.insurerLicense = body.insurerLicense;
|
||||
@@ -1980,7 +2020,11 @@ export class RequestManagementService {
|
||||
: undefined;
|
||||
}
|
||||
|
||||
const inquirySubmission = this.normalizeInquiryInput(req.type, body);
|
||||
const inquirySubmission = this.normalizeInquiryInput(
|
||||
req.type,
|
||||
body,
|
||||
role,
|
||||
);
|
||||
body = inquirySubmission.dto as unknown as InitialFormVinDto;
|
||||
this.applyInquiryParticipantsToParty(party, inquirySubmission);
|
||||
|
||||
@@ -2123,7 +2167,7 @@ export class RequestManagementService {
|
||||
// Persist party data
|
||||
const resolvedClientId =
|
||||
(client as any)?._id ?? (client as any)?._doc?._id;
|
||||
party.person.clientId = resolvedClientId;
|
||||
if (resolvedClientId) party.person.clientId = resolvedClientId;
|
||||
party.person.nationalCodeOfInsurer = body.nationalCodeOfInsurer;
|
||||
party.person.nationalCodeOfDriver = body.nationalCodeOfDriver;
|
||||
party.person.insurerLicense = body.insurerLicense;
|
||||
@@ -6578,6 +6622,7 @@ export class RequestManagementService {
|
||||
const inquirySubmission = this.normalizeInquiryInput(
|
||||
req.type,
|
||||
formData.firstPartyPlate,
|
||||
PartyRole.FIRST,
|
||||
);
|
||||
const firstPartyPlate = inquirySubmission.dto;
|
||||
let sandHubReport: any;
|
||||
@@ -6586,8 +6631,7 @@ export class RequestManagementService {
|
||||
? await (async () => {
|
||||
const response = await this.sandHubService.getSandHubResponse({
|
||||
plate: firstPartyPlate.plate,
|
||||
nationalCodeOfInsurer:
|
||||
firstPartyPlate.nationalCodeOfInsurer,
|
||||
nationalCodeOfInsurer: firstPartyPlate.nationalCodeOfInsurer,
|
||||
});
|
||||
const mapped = response["_doc"] || response;
|
||||
return {
|
||||
@@ -6857,8 +6901,14 @@ export class RequestManagementService {
|
||||
desc: string,
|
||||
role: PartyRole,
|
||||
) => {
|
||||
const inquirySubmission = this.normalizeInquiryInput(req.type, plateDto);
|
||||
const inquirySubmission = this.normalizeInquiryInput(
|
||||
req.type,
|
||||
plateDto,
|
||||
role,
|
||||
);
|
||||
plateDto = inquirySubmission.dto;
|
||||
const policyholderUnknown =
|
||||
inquirySubmission.thirdPartyPolicyholder.unknown === true;
|
||||
let inquiry: any;
|
||||
try {
|
||||
if (inquirySubmission.participants.legacy) {
|
||||
@@ -6871,9 +6921,7 @@ export class RequestManagementService {
|
||||
raw: mapped,
|
||||
mapped,
|
||||
plateKind: "CURRENT",
|
||||
attempts: [
|
||||
{ plateKind: "CURRENT", succeeded: true, usable: true },
|
||||
],
|
||||
attempts: [{ plateKind: "CURRENT", succeeded: true, usable: true }],
|
||||
};
|
||||
} else {
|
||||
inquiry = await this.getThirdPartyPlateInquiry(inquirySubmission);
|
||||
@@ -6897,23 +6945,37 @@ export class RequestManagementService {
|
||||
"THIRD_PARTY",
|
||||
);
|
||||
}
|
||||
this.recordPartyCaseInquiryStatus(req, "thirdParty", role, true, {
|
||||
this.recordPartyCaseInquiryStatus(
|
||||
req,
|
||||
"thirdParty",
|
||||
role,
|
||||
!inquiry.skipped,
|
||||
{
|
||||
source: inquiry.offline?.source ?? "TEJARAT_BLOCK_INQUIRY",
|
||||
plateKind: inquiry.plateKind,
|
||||
attempts: inquiry.attempts,
|
||||
raw: inquiry.raw,
|
||||
mapped: inquiry.mapped,
|
||||
});
|
||||
raw: inquiry.raw,
|
||||
mapped: inquiry.mapped,
|
||||
...(inquiry.skipped
|
||||
? {
|
||||
skipped: true,
|
||||
reason: "Third-party policyholder is unknown",
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
);
|
||||
const clientName =
|
||||
sandHubReport?.CompanyName || sandHubReport?.LastCompanyName;
|
||||
const companyCode = sandHubReport?.CompanyCode;
|
||||
const client = companyCode
|
||||
? await this.clientService.findOrCreateClientByCompanyCode(
|
||||
companyCode,
|
||||
clientName,
|
||||
)
|
||||
: await this.clientService.findOne({ clientName });
|
||||
if (!client) {
|
||||
const client = clientName
|
||||
? companyCode
|
||||
? await this.clientService.findOrCreateClientByCompanyCode(
|
||||
companyCode,
|
||||
clientName,
|
||||
)
|
||||
: await this.clientService.findOne({ clientName })
|
||||
: null;
|
||||
if (!client && !policyholderUnknown) {
|
||||
throw new NotFoundException(
|
||||
`Client not found for company: ${clientName}`,
|
||||
);
|
||||
@@ -9112,9 +9174,15 @@ export class RequestManagementService {
|
||||
partyRole: PartyRole,
|
||||
party: any,
|
||||
): Promise<{ clientId?: string }> {
|
||||
const inquirySubmission = this.normalizeInquiryInput(req.type, partyData);
|
||||
const inquirySubmission = this.normalizeInquiryInput(
|
||||
req.type,
|
||||
partyData,
|
||||
partyRole,
|
||||
);
|
||||
partyData = inquirySubmission.dto as RunInquiriesV3Dto;
|
||||
this.applyInquiryParticipantsToParty(party, inquirySubmission);
|
||||
const policyholderUnknown =
|
||||
inquirySubmission.thirdPartyPolicyholder.unknown === true;
|
||||
const roleLabel = partyRole === PartyRole.FIRST ? "FIRST" : "SECOND";
|
||||
let clientId: string | undefined;
|
||||
const inquiryOptions = (cid?: string) =>
|
||||
@@ -9137,13 +9205,25 @@ export class RequestManagementService {
|
||||
if (inquiry.offline?.source) {
|
||||
inquirySource = inquiry.offline.source;
|
||||
}
|
||||
this.recordPartyCaseInquiryStatus(req, "thirdParty", partyRole, true, {
|
||||
source: inquirySource,
|
||||
plateKind: inquiry.plateKind,
|
||||
attempts: inquiry.attempts,
|
||||
raw: inquiryRaw,
|
||||
mapped: inquiryMapped,
|
||||
});
|
||||
this.recordPartyCaseInquiryStatus(
|
||||
req,
|
||||
"thirdParty",
|
||||
partyRole,
|
||||
!inquiry.skipped,
|
||||
{
|
||||
source: inquirySource,
|
||||
plateKind: inquiry.plateKind,
|
||||
attempts: inquiry.attempts,
|
||||
raw: inquiryRaw,
|
||||
mapped: inquiryMapped,
|
||||
...(inquiry.skipped
|
||||
? {
|
||||
skipped: true,
|
||||
reason: "Third-party policyholder is unknown",
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
);
|
||||
if (inquiry.offline?.fanavaranDriverId != null) {
|
||||
if (!party.person) party.person = {} as any;
|
||||
party.person.fanavaranDriverId = inquiry.offline.fanavaranDriverId;
|
||||
@@ -9174,16 +9254,18 @@ export class RequestManagementService {
|
||||
|
||||
const clientName = inquiryMapped?.CompanyName;
|
||||
const companyCode = inquiryMapped?.CompanyCode;
|
||||
if (!clientName) {
|
||||
if (!clientName && !policyholderUnknown) {
|
||||
throw new BadRequestException(
|
||||
`CompanyName missing from ${roleLabel} party inquiry response`,
|
||||
);
|
||||
}
|
||||
const client = await this.clientService.findOrCreateClientByCompanyCode(
|
||||
companyCode,
|
||||
clientName,
|
||||
);
|
||||
if (!client) {
|
||||
const client = clientName
|
||||
? await this.clientService.findOrCreateClientByCompanyCode(
|
||||
companyCode,
|
||||
clientName,
|
||||
)
|
||||
: null;
|
||||
if (clientName && !client) {
|
||||
throw new BadRequestException(
|
||||
`CompanyCode missing or invalid in ${roleLabel} party inquiry response`,
|
||||
);
|
||||
@@ -9192,7 +9274,7 @@ export class RequestManagementService {
|
||||
clientId = resolvedClientId ? String(resolvedClientId) : undefined;
|
||||
|
||||
if (!party.person) party.person = {} as any;
|
||||
party.person.clientId = resolvedClientId;
|
||||
if (resolvedClientId) party.person.clientId = resolvedClientId;
|
||||
party.person.nationalCodeOfInsurer = partyData.nationalCodeOfInsurer;
|
||||
party.person.nationalCodeOfDriver = partyData.nationalCodeOfDriver;
|
||||
party.person.driverIsInsurer = partyData.driverIsInsurer;
|
||||
@@ -9337,7 +9419,22 @@ export class RequestManagementService {
|
||||
? partyData.insurerLicense
|
||||
: partyData.driverLicense;
|
||||
|
||||
if (!licenseNumber) {
|
||||
if (
|
||||
!inquirySubmission.participants.legacy &&
|
||||
inquirySubmission.driver.hasDrivingLicense === false
|
||||
) {
|
||||
this.recordPartyCaseInquiryStatus(
|
||||
req,
|
||||
"drivingLicence",
|
||||
partyRole,
|
||||
true,
|
||||
{
|
||||
participantId: inquirySubmission.driver.participantId,
|
||||
skipped: true,
|
||||
reason: "Driver declared no driving licence",
|
||||
},
|
||||
);
|
||||
} else if (!licenseNumber) {
|
||||
this.recordPartyCaseInquiryStatus(
|
||||
req,
|
||||
"drivingLicence",
|
||||
@@ -9683,12 +9780,12 @@ export class RequestManagementService {
|
||||
if (!req) throw new NotFoundException("Blame request not found");
|
||||
this.assertBlameV3ExpertInPerson(req);
|
||||
await this.verifyExpertAccessForBlameV2(req, actor);
|
||||
dto = this.normalizeInquiryInput(req.type, dto).dto as RunInquiriesV3Dto;
|
||||
|
||||
const partyRole = this.resolveV3InquiryPartyRole(req);
|
||||
if (!partyRole) {
|
||||
throw new ConflictException("All party inquiries are already complete.");
|
||||
}
|
||||
dto = this.normalizeInquiryInput(req.type, dto, partyRole)
|
||||
.dto as RunInquiriesV3Dto;
|
||||
|
||||
if (partyRole === PartyRole.FIRST) {
|
||||
const firstIdx = this.getPartyIndex(req, PartyRole.FIRST);
|
||||
@@ -9820,12 +9917,12 @@ export class RequestManagementService {
|
||||
if (!req) throw new NotFoundException("Blame request not found");
|
||||
this.assertBlameV3ExpertInPerson(req);
|
||||
await this.verifyExpertAccessForBlameV2(req, actor);
|
||||
dto = this.normalizeInquiryInput(req.type, dto).dto as RunInquiriesVinV3Dto;
|
||||
|
||||
const partyRole = this.resolveV3InquiryPartyRole(req);
|
||||
if (!partyRole) {
|
||||
throw new ConflictException("All party inquiries are already complete.");
|
||||
}
|
||||
dto = this.normalizeInquiryInput(req.type, dto, partyRole)
|
||||
.dto as RunInquiriesVinV3Dto;
|
||||
|
||||
if (partyRole === PartyRole.FIRST) {
|
||||
const firstIdx = this.getPartyIndex(req, PartyRole.FIRST);
|
||||
@@ -9952,7 +10049,11 @@ export class RequestManagementService {
|
||||
partyRole: PartyRole,
|
||||
party: any,
|
||||
): Promise<{ clientId?: string }> {
|
||||
const inquirySubmission = this.normalizeInquiryInput(req.type, partyData);
|
||||
const inquirySubmission = this.normalizeInquiryInput(
|
||||
req.type,
|
||||
partyData,
|
||||
partyRole,
|
||||
);
|
||||
partyData = inquirySubmission.dto as RunInquiriesVinV3Dto;
|
||||
this.applyInquiryParticipantsToParty(party, inquirySubmission);
|
||||
const roleLabel = partyRole === PartyRole.FIRST ? "FIRST" : "SECOND";
|
||||
@@ -10018,7 +10119,7 @@ export class RequestManagementService {
|
||||
clientId = resolvedClientId ? String(resolvedClientId) : undefined;
|
||||
|
||||
if (!party.person) party.person = {} as any;
|
||||
party.person.clientId = resolvedClientId;
|
||||
if (resolvedClientId) party.person.clientId = resolvedClientId;
|
||||
party.person.nationalCodeOfInsurer = partyData.nationalCodeOfInsurer;
|
||||
party.person.nationalCodeOfDriver = partyData.nationalCodeOfDriver;
|
||||
party.person.driverIsInsurer = partyData.driverIsInsurer;
|
||||
@@ -10174,7 +10275,22 @@ export class RequestManagementService {
|
||||
? partyData.insurerLicense
|
||||
: partyData.driverLicense;
|
||||
|
||||
if (!licenseNumber) {
|
||||
if (
|
||||
!inquirySubmission.participants.legacy &&
|
||||
inquirySubmission.driver.hasDrivingLicense === false
|
||||
) {
|
||||
this.recordPartyCaseInquiryStatus(
|
||||
req,
|
||||
"drivingLicence",
|
||||
partyRole,
|
||||
true,
|
||||
{
|
||||
participantId: inquirySubmission.driver.participantId,
|
||||
skipped: true,
|
||||
reason: "Driver declared no driving licence",
|
||||
},
|
||||
);
|
||||
} else if (!licenseNumber) {
|
||||
this.recordPartyCaseInquiryStatus(
|
||||
req,
|
||||
"drivingLicence",
|
||||
@@ -11403,7 +11519,7 @@ export class RequestManagementService {
|
||||
if (firstIdx === -1) throw new BadRequestException("First party not found");
|
||||
const firstParty = req.parties[firstIdx];
|
||||
|
||||
dto = this.normalizeInquiryInput(req.type, dto).dto;
|
||||
dto = this.normalizeInquiryInput(req.type, dto, PartyRole.FIRST).dto;
|
||||
|
||||
await this.runPartyInquiriesV3Internal(
|
||||
req,
|
||||
@@ -11526,7 +11642,7 @@ export class RequestManagementService {
|
||||
if (firstIdx === -1) throw new BadRequestException("First party not found");
|
||||
const firstParty = req.parties[firstIdx];
|
||||
|
||||
dto = this.normalizeInquiryInput(req.type, dto).dto;
|
||||
dto = this.normalizeInquiryInput(req.type, dto, PartyRole.FIRST).dto;
|
||||
|
||||
await this.runPartyInquiriesVinV3Internal(
|
||||
req,
|
||||
|
||||
Reference in New Issue
Block a user