Harden inquiry participant edge cases

This commit is contained in:
SepehrYahyaee
2026-09-13 11:18:37 +03:30
parent c64f23091a
commit d060b6d9e2
11 changed files with 505 additions and 84 deletions

View File

@@ -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 })

View File

@@ -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;
}

View File

@@ -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" })

View File

@@ -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" })

View File

@@ -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);

View File

@@ -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 });
});
});

View File

@@ -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;

View File

@@ -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,