forked from Yara724/api
Harden inquiry participant edge cases
This commit is contained in:
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user