forked from Yara724/api
Harden inquiry participant edge cases
This commit is contained in:
@@ -34,6 +34,7 @@
|
|||||||
"driver": {
|
"driver": {
|
||||||
"nationalCode": "0012345678",
|
"nationalCode": "0012345678",
|
||||||
"birthday": "1370/01/01",
|
"birthday": "1370/01/01",
|
||||||
|
"hasDrivingLicense": true,
|
||||||
"licenseNumber": "123456789",
|
"licenseNumber": "123456789",
|
||||||
"licenseType": "1"
|
"licenseType": "1"
|
||||||
},
|
},
|
||||||
@@ -48,6 +49,8 @@
|
|||||||
|
|
||||||
فیلد `carBodyPolicyholder` برای `THIRD_PARTY` مجاز نیست و برای `CAR_BODY` الزامی است. هر نقش باید فقط یکی از دو حالت «اطلاعات شخص» یا `sameAs` را داشته باشد. بکاند این ورودی را به فهرست اشخاص یکتا و اتصال نقشها به آنها تبدیل میکند.
|
فیلد `carBodyPolicyholder` برای `THIRD_PARTY` مجاز نیست و برای `CAR_BODY` الزامی است. هر نقش باید فقط یکی از دو حالت «اطلاعات شخص» یا `sameAs` را داشته باشد. بکاند این ورودی را به فهرست اشخاص یکتا و اتصال نقشها به آنها تبدیل میکند.
|
||||||
|
|
||||||
|
فقط برای طرف زیاندیده در پرونده `THIRD_PARTY` میتوان نامشخص بودن بیمهگذار را بهصورت صریح با `"thirdPartyPolicyholder": { "unknown": true }` ارسال کرد. این حالت برای طرف مقصر پذیرفته نمیشود. در مسیر پلاک، استعلام بیمه شخص ثالث ــ چون به هویت بیمهگذار نیاز دارد ــ بهشکل قابلممیزی «عمداً اجرا نشد» ثبت میشود؛ استعلام مبتنی بر VIN همچنان بدون حدسزدن هویت قابل اجرا است. اعتبارسنجی راننده و مالک و استعلامهای هویت، مالکیت و گواهینامه همچنان انجام میشوند.
|
||||||
|
|
||||||
برای راننده، `hasDrivingLicense` الزامی است. اگر مقدار آن `true` باشد، هر دو فیلد `licenseNumber` و `licenseType` نیز الزامی هستند؛ اگر مقدار آن `false` باشد، استعلام گواهینامه عمداً اجرا نمیشود.
|
برای راننده، `hasDrivingLicense` الزامی است. اگر مقدار آن `true` باشد، هر دو فیلد `licenseNumber` و `licenseType` نیز الزامی هستند؛ اگر مقدار آن `false` باشد، استعلام گواهینامه عمداً اجرا نمیشود.
|
||||||
|
|
||||||
## انتقال مالکیت اخیر و پلاک قبلی
|
## انتقال مالکیت اخیر و پلاک قبلی
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ Use explicit role references instead:
|
|||||||
"driver": {
|
"driver": {
|
||||||
"nationalCode": "0012345678",
|
"nationalCode": "0012345678",
|
||||||
"birthday": "1370/01/01",
|
"birthday": "1370/01/01",
|
||||||
|
"hasDrivingLicense": true,
|
||||||
"licenseNumber": "123456789",
|
"licenseNumber": "123456789",
|
||||||
"licenseType": "1"
|
"licenseType": "1"
|
||||||
},
|
},
|
||||||
@@ -48,6 +49,8 @@ Use explicit role references instead:
|
|||||||
|
|
||||||
`carBodyPolicyholder` is forbidden for `THIRD_PARTY` and required for `CAR_BODY`. A role is either a new person's identity or a `sameAs` reference, never both. The backend should normalize this input into unique participants plus role assignments.
|
`carBodyPolicyholder` is forbidden for `THIRD_PARTY` and required for `CAR_BODY`. A role is either a new person's identity or a `sameAs` reference, never both. The backend should normalize this input into unique participants plus role assignments.
|
||||||
|
|
||||||
|
For the damaged party of a `THIRD_PARTY` case only, the policyholder can be explicitly unresolved with `"thirdPartyPolicyholder": { "unknown": true }`. This is not accepted for the guilty party. Plate-based third-party-policy inquiry is recorded as intentionally skipped because it requires that identity; VIN-based lookup can still run without guessing a person. The driver, owner, personal, ownership, and licence rules remain enforced.
|
||||||
|
|
||||||
For Driver, `hasDrivingLicense` is required. When it is `true`, both `licenseNumber` and `licenseType` are required; when it is `false`, the licence inquiry is intentionally skipped.
|
For Driver, `hasDrivingLicense` is required. When it is `true`, both `licenseNumber` and `licenseType` are required; when it is `false`, the licence inquiry is intentionally skipped.
|
||||||
|
|
||||||
## Recent ownership transfer and previous plate
|
## Recent ownership transfer and previous plate
|
||||||
|
|||||||
@@ -50,6 +50,14 @@ export class InquiryParticipantInputDto {
|
|||||||
@IsEnum(InquiryParticipantRole)
|
@IsEnum(InquiryParticipantRole)
|
||||||
sameAs?: InquiryParticipantRole;
|
sameAs?: InquiryParticipantRole;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description:
|
||||||
|
"Only for the damaged party's third-party policyholder when the identity is genuinely unknown.",
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
unknown?: boolean;
|
||||||
|
|
||||||
@ApiPropertyOptional({ example: "0012345678" })
|
@ApiPropertyOptional({ example: "0012345678" })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
@@ -88,9 +96,13 @@ export class InquiryParticipantInputDto {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class InquiryVehicleInputDto {
|
export class InquiryVehicleInputDto {
|
||||||
@ApiProperty({ enum: VehicleRegistrationState })
|
@ApiPropertyOptional({
|
||||||
|
enum: VehicleRegistrationState,
|
||||||
|
default: VehicleRegistrationState.CURRENT,
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
@IsEnum(VehicleRegistrationState)
|
@IsEnum(VehicleRegistrationState)
|
||||||
registrationState: VehicleRegistrationState;
|
registrationState?: VehicleRegistrationState;
|
||||||
|
|
||||||
@ApiProperty({ type: InquiryPlateDto })
|
@ApiProperty({ type: InquiryPlateDto })
|
||||||
@ValidateNested()
|
@ValidateNested()
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||||
import { PlatesDto } from "src/plates/dto/plate.dto";
|
import { PlatesDto } from "src/plates/dto/plate.dto";
|
||||||
import { Plates } from "src/Types&Enums/plate.interface";
|
import { Plates } from "src/Types&Enums/plate.interface";
|
||||||
import { InquiryParticipantFieldsDto } from "src/request-management/dto/inquiry-participants.dto";
|
import { InquiryParticipantFieldsDto } from "src/common/dto/inquiry-participants.dto";
|
||||||
|
|
||||||
export class AddPlateDto extends InquiryParticipantFieldsDto {
|
export class AddPlateDto extends InquiryParticipantFieldsDto {
|
||||||
plateId: string;
|
plateId: string;
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
} from "src/Types&Enums/blame-request-management/accident-conditions.enum";
|
} from "src/Types&Enums/blame-request-management/accident-conditions.enum";
|
||||||
import { BlameRequestType } from "src/Types&Enums/blame-request-management/blameRequestType.enum";
|
import { BlameRequestType } from "src/Types&Enums/blame-request-management/blameRequestType.enum";
|
||||||
import { IsEnum } from "class-validator";
|
import { IsEnum } from "class-validator";
|
||||||
import { InquiryParticipantFieldsDto } from "./inquiry-participants.dto";
|
import { InquiryParticipantFieldsDto } from "src/common/dto/inquiry-participants.dto";
|
||||||
|
|
||||||
export class InitialFormDto {
|
export class InitialFormDto {
|
||||||
@ApiProperty({ required: false, default: false })
|
@ApiProperty({ required: false, default: false })
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
ValidateNested,
|
ValidateNested,
|
||||||
} from "class-validator";
|
} from "class-validator";
|
||||||
import { Type } from "class-transformer";
|
import { Type } from "class-transformer";
|
||||||
import { InquiryParticipantFieldsDto } from "./inquiry-participants.dto";
|
import { InquiryParticipantFieldsDto } from "src/common/dto/inquiry-participants.dto";
|
||||||
|
|
||||||
class PlateV6Dto {
|
class PlateV6Dto {
|
||||||
@ApiProperty({ example: "44", description: "Left two digits" })
|
@ApiProperty({ example: "44", description: "Left two digits" })
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
ValidateNested,
|
ValidateNested,
|
||||||
} from "class-validator";
|
} from "class-validator";
|
||||||
import { Type } from "class-transformer";
|
import { Type } from "class-transformer";
|
||||||
import { InquiryParticipantFieldsDto } from "./inquiry-participants.dto";
|
import { InquiryParticipantFieldsDto } from "src/common/dto/inquiry-participants.dto";
|
||||||
|
|
||||||
class PlateV3Dto {
|
class PlateV3Dto {
|
||||||
@ApiProperty({ example: "44", description: "Left two digits" })
|
@ApiProperty({ example: "44", description: "Left two digits" })
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ export class InquiryParticipant {
|
|||||||
@Prop({ type: Boolean }) hasDrivingLicense?: boolean;
|
@Prop({ type: Boolean }) hasDrivingLicense?: boolean;
|
||||||
@Prop() licenseNumber?: string;
|
@Prop() licenseNumber?: string;
|
||||||
@Prop() licenseType?: string;
|
@Prop() licenseType?: string;
|
||||||
|
@Prop({ type: Boolean }) unknown?: boolean;
|
||||||
}
|
}
|
||||||
export const InquiryParticipantSchema =
|
export const InquiryParticipantSchema =
|
||||||
SchemaFactory.createForClass(InquiryParticipant);
|
SchemaFactory.createForClass(InquiryParticipant);
|
||||||
|
|||||||
@@ -3,9 +3,10 @@ import { BlameRequestType } from "src/Types&Enums/blame-request-management/blame
|
|||||||
import {
|
import {
|
||||||
InquiryParticipantRole,
|
InquiryParticipantRole,
|
||||||
VehicleRegistrationState,
|
VehicleRegistrationState,
|
||||||
} from "./dto/inquiry-participants.dto";
|
} from "src/common/dto/inquiry-participants.dto";
|
||||||
import {
|
import {
|
||||||
assertPreviousPlateInquiryMatchesVin,
|
assertPreviousPlateInquiryMatchesVin,
|
||||||
|
isMappedPolicyCurrent,
|
||||||
normalizeInquirySubmission,
|
normalizeInquirySubmission,
|
||||||
participantForRole,
|
participantForRole,
|
||||||
resolveInquiryParticipants,
|
resolveInquiryParticipants,
|
||||||
@@ -52,6 +53,19 @@ describe("inquiry participant resolver", () => {
|
|||||||
).toThrow(BadRequestException);
|
).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", () => {
|
it("keeps legacy driver/policyholder payloads working during rollout", () => {
|
||||||
const resolved = resolveInquiryParticipants(BlameRequestType.THIRD_PARTY, {
|
const resolved = resolveInquiryParticipants(BlameRequestType.THIRD_PARTY, {
|
||||||
nationalCodeOfDriver: "0012345678",
|
nationalCodeOfDriver: "0012345678",
|
||||||
@@ -261,4 +275,177 @@ describe("inquiry participant resolver", () => {
|
|||||||
}),
|
}),
|
||||||
).toThrow(BadRequestException);
|
).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 { BadRequestException } from "@nestjs/common";
|
||||||
import { BlameRequestType } from "src/Types&Enums/blame-request-management/blameRequestType.enum";
|
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 {
|
import {
|
||||||
InquiryParticipantFieldsDto,
|
InquiryParticipantFieldsDto,
|
||||||
InquiryParticipantInputDto,
|
InquiryParticipantInputDto,
|
||||||
InquiryParticipantRole,
|
InquiryParticipantRole,
|
||||||
InquiryVehicleInputDto,
|
InquiryVehicleInputDto,
|
||||||
VehicleRegistrationState,
|
VehicleRegistrationState,
|
||||||
} from "./dto/inquiry-participants.dto";
|
} from "src/common/dto/inquiry-participants.dto";
|
||||||
|
|
||||||
export interface ResolvedInquiryParticipant {
|
export interface ResolvedInquiryParticipant {
|
||||||
participantId: string;
|
participantId: string;
|
||||||
@@ -17,6 +19,11 @@ export interface ResolvedInquiryParticipant {
|
|||||||
hasDrivingLicense?: boolean;
|
hasDrivingLicense?: boolean;
|
||||||
licenseNumber?: string;
|
licenseNumber?: string;
|
||||||
licenseType?: string;
|
licenseType?: string;
|
||||||
|
unknown?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InquiryParticipantResolutionOptions {
|
||||||
|
allowUnknownThirdPartyPolicyholder?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface InquiryParticipantRoleAssignments {
|
export interface InquiryParticipantRoleAssignments {
|
||||||
@@ -49,9 +56,13 @@ export interface NormalizedInquirySubmission<T extends Record<string, any>> {
|
|||||||
vehicleOwner?: ResolvedInquiryParticipant;
|
vehicleOwner?: ResolvedInquiryParticipant;
|
||||||
thirdPartyPolicyholder: ResolvedInquiryParticipant;
|
thirdPartyPolicyholder: ResolvedInquiryParticipant;
|
||||||
carBodyPolicyholder?: ResolvedInquiryParticipant;
|
carBodyPolicyholder?: ResolvedInquiryParticipant;
|
||||||
vehicle?: InquiryVehicleInputDto;
|
vehicle?: ResolvedInquiryVehicle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ResolvedInquiryVehicle = InquiryVehicleInputDto & {
|
||||||
|
registrationState: VehicleRegistrationState;
|
||||||
|
};
|
||||||
|
|
||||||
export function participantForRole(
|
export function participantForRole(
|
||||||
resolved: ResolvedInquiryParticipants,
|
resolved: ResolvedInquiryParticipants,
|
||||||
role: InquiryParticipantRole,
|
role: InquiryParticipantRole,
|
||||||
@@ -119,6 +130,7 @@ function requiredIdentity(
|
|||||||
export function resolveInquiryParticipants(
|
export function resolveInquiryParticipants(
|
||||||
caseType: BlameRequestType,
|
caseType: BlameRequestType,
|
||||||
input: InquiryParticipantFieldsDto & Record<string, any>,
|
input: InquiryParticipantFieldsDto & Record<string, any>,
|
||||||
|
options: InquiryParticipantResolutionOptions = {},
|
||||||
): ResolvedInquiryParticipants {
|
): ResolvedInquiryParticipants {
|
||||||
const hasRoleCompleteInput = Object.values(ROLE_FIELDS).some(
|
const hasRoleCompleteInput = Object.values(ROLE_FIELDS).some(
|
||||||
(field) => input[field] != null,
|
(field) => input[field] != null,
|
||||||
@@ -200,8 +212,40 @@ export function resolveInquiryParticipants(
|
|||||||
|
|
||||||
resolving.add(role);
|
resolving.add(role);
|
||||||
let participantId: string;
|
let participantId: string;
|
||||||
if (value.sameAs) {
|
if (
|
||||||
if (value.nationalCode != null || value.birthday != null) {
|
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(
|
throw new BadRequestException(
|
||||||
`${role} must contain either sameAs or identity fields, not both.`,
|
`${role} must contain either sameAs or identity fields, not both.`,
|
||||||
);
|
);
|
||||||
@@ -249,7 +293,7 @@ export function resolveInquiryParticipants(
|
|||||||
|
|
||||||
export function resolveInquiryVehicle(
|
export function resolveInquiryVehicle(
|
||||||
input: InquiryVehicleInputDto,
|
input: InquiryVehicleInputDto,
|
||||||
): InquiryVehicleInputDto {
|
): ResolvedInquiryVehicle {
|
||||||
const registrationState =
|
const registrationState =
|
||||||
input.registrationState ?? VehicleRegistrationState.CURRENT;
|
input.registrationState ?? VehicleRegistrationState.CURRENT;
|
||||||
if (!input.currentPlate) {
|
if (!input.currentPlate) {
|
||||||
@@ -274,7 +318,7 @@ export function resolveInquiryVehicle(
|
|||||||
return { ...input, registrationState };
|
return { ...input, registrationState };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function vehiclePlateCandidates(input?: InquiryVehicleInputDto): Array<{
|
export function vehiclePlateCandidates(input?: ResolvedInquiryVehicle): Array<{
|
||||||
kind: "CURRENT" | "PREVIOUS";
|
kind: "CURRENT" | "PREVIOUS";
|
||||||
plate: InquiryVehicleInputDto["currentPlate"];
|
plate: InquiryVehicleInputDto["currentPlate"];
|
||||||
}> {
|
}> {
|
||||||
@@ -294,6 +338,35 @@ function normalizeVehicleSerial(value: unknown): string {
|
|||||||
.replace(/[^A-Z0-9]/g, "");
|
.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(
|
export function assertPreviousPlateInquiryMatchesVin(
|
||||||
expectedVin: string,
|
expectedVin: string,
|
||||||
mapped: Record<string, any>,
|
mapped: Record<string, any>,
|
||||||
@@ -319,7 +392,7 @@ export function assertPreviousPlateInquiryMatchesVin(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function runPlateInquiryWithFallback<T>(options: {
|
export async function runPlateInquiryWithFallback<T>(options: {
|
||||||
vehicle?: InquiryVehicleInputDto;
|
vehicle?: ResolvedInquiryVehicle;
|
||||||
fallbackCurrentPlate: InquiryVehicleInputDto["currentPlate"];
|
fallbackCurrentPlate: InquiryVehicleInputDto["currentPlate"];
|
||||||
query: (plate: InquiryVehicleInputDto["currentPlate"]) => Promise<T>;
|
query: (plate: InquiryVehicleInputDto["currentPlate"]) => Promise<T>;
|
||||||
isUsable: (value: T) => boolean;
|
isUsable: (value: T) => boolean;
|
||||||
@@ -358,7 +431,11 @@ export async function runPlateInquiryWithFallback<T>(options: {
|
|||||||
usable: false,
|
usable: false,
|
||||||
});
|
});
|
||||||
if (!isLast) continue;
|
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) {
|
if (candidate.kind === "PREVIOUS" && usable) {
|
||||||
assertPreviousPlateInquiryMatchesVin(
|
assertPreviousPlateInquiryMatchesVin(
|
||||||
@@ -374,12 +451,23 @@ export async function runPlateInquiryWithFallback<T>(options: {
|
|||||||
return { value, plateKind: candidate.kind, attempts };
|
return { value, plateKind: candidate.kind, attempts };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
lastError = error;
|
lastError = error;
|
||||||
|
const alreadyRecorded =
|
||||||
|
typeof error === "object" &&
|
||||||
|
error != null &&
|
||||||
|
Array.isArray((error as { attempts?: unknown }).attempts);
|
||||||
|
if (!alreadyRecorded) {
|
||||||
attempts.push({
|
attempts.push({
|
||||||
plateKind: candidate.kind,
|
plateKind: candidate.kind,
|
||||||
succeeded: false,
|
succeeded: false,
|
||||||
error: error instanceof Error ? error.message : String(error),
|
error: error instanceof Error ? error.message : String(error),
|
||||||
});
|
});
|
||||||
if (isLast) throw 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>>(
|
export function normalizeInquirySubmission<T extends Record<string, any>>(
|
||||||
caseType: BlameRequestType,
|
caseType: BlameRequestType,
|
||||||
input: T,
|
input: T,
|
||||||
|
options: InquiryParticipantResolutionOptions = {},
|
||||||
): NormalizedInquirySubmission<T> {
|
): NormalizedInquirySubmission<T> {
|
||||||
const participants = resolveInquiryParticipants(caseType, input);
|
const participants = resolveInquiryParticipants(caseType, input, options);
|
||||||
const driver = participantForRole(
|
const driver = participantForRole(
|
||||||
participants,
|
participants,
|
||||||
InquiryParticipantRole.DRIVER,
|
InquiryParticipantRole.DRIVER,
|
||||||
@@ -415,6 +504,16 @@ export function normalizeInquirySubmission<T extends Record<string, any>>(
|
|||||||
const vehicle = input.vehicle
|
const vehicle = input.vehicle
|
||||||
? resolveInquiryVehicle(input.vehicle as InquiryVehicleInputDto)
|
? resolveInquiryVehicle(input.vehicle as InquiryVehicleInputDto)
|
||||||
: undefined;
|
: 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 =
|
const sameDriverAndPolicyholder =
|
||||||
participants.roles.driver === participants.roles.thirdPartyPolicyholder;
|
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 { normalizePlateText } from "src/utils/plate-normalizer/plate-normalizer.service";
|
||||||
import { fanavaranClaimReferences } from "src/claim-request-management/fanavaran-claim-references";
|
import { fanavaranClaimReferences } from "src/claim-request-management/fanavaran-claim-references";
|
||||||
import {
|
import {
|
||||||
|
isMappedPolicyCurrent,
|
||||||
NormalizedInquirySubmission,
|
NormalizedInquirySubmission,
|
||||||
normalizeInquirySubmission,
|
normalizeInquirySubmission,
|
||||||
runPlateInquiryWithFallback,
|
runPlateInquiryWithFallback,
|
||||||
@@ -308,8 +309,12 @@ export class RequestManagementService {
|
|||||||
private normalizeInquiryInput<T extends Record<string, any>>(
|
private normalizeInquiryInput<T extends Record<string, any>>(
|
||||||
type: BlameRequestType,
|
type: BlameRequestType,
|
||||||
input: T,
|
input: T,
|
||||||
|
partyRole?: PartyRole,
|
||||||
): NormalizedInquirySubmission<T> {
|
): NormalizedInquirySubmission<T> {
|
||||||
return normalizeInquirySubmission(type, input);
|
return normalizeInquirySubmission(type, input, {
|
||||||
|
allowUnknownThirdPartyPolicyholder:
|
||||||
|
type === BlameRequestType.THIRD_PARTY && partyRole === PartyRole.SECOND,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private applyInquiryParticipantsToParty(
|
private applyInquiryParticipantsToParty(
|
||||||
@@ -335,6 +340,15 @@ export class RequestManagementService {
|
|||||||
submission: NormalizedInquirySubmission<Record<string, any>>,
|
submission: NormalizedInquirySubmission<Record<string, any>>,
|
||||||
options?: Record<string, any>,
|
options?: Record<string, any>,
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
|
if (submission.thirdPartyPolicyholder.unknown) {
|
||||||
|
return {
|
||||||
|
raw: null,
|
||||||
|
mapped: {},
|
||||||
|
skipped: true,
|
||||||
|
plateKind: "CURRENT",
|
||||||
|
attempts: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
const result = await runPlateInquiryWithFallback({
|
const result = await runPlateInquiryWithFallback({
|
||||||
vehicle: submission.vehicle,
|
vehicle: submission.vehicle,
|
||||||
fallbackCurrentPlate: submission.dto.plate,
|
fallbackCurrentPlate: submission.dto.plate,
|
||||||
@@ -348,7 +362,9 @@ export class RequestManagementService {
|
|||||||
options,
|
options,
|
||||||
),
|
),
|
||||||
isUsable: (value) =>
|
isUsable: (value) =>
|
||||||
!value?.mapped?.Error && !!value?.mapped?.CompanyName,
|
!value?.mapped?.Error &&
|
||||||
|
!!value?.mapped?.CompanyName &&
|
||||||
|
isMappedPolicyCurrent(value.mapped),
|
||||||
mappedValue: (value) => value?.mapped ?? {},
|
mappedValue: (value) => value?.mapped ?? {},
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
@@ -373,6 +389,7 @@ export class RequestManagementService {
|
|||||||
}),
|
}),
|
||||||
isUsable: (value) =>
|
isUsable: (value) =>
|
||||||
!!value?.mapped &&
|
!!value?.mapped &&
|
||||||
|
isMappedPolicyCurrent(value.mapped) &&
|
||||||
!!(
|
!!(
|
||||||
value.mapped.policyNumber ||
|
value.mapped.policyNumber ||
|
||||||
value.mapped.CompanyName ||
|
value.mapped.CompanyName ||
|
||||||
@@ -395,7 +412,9 @@ export class RequestManagementService {
|
|||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const participants = submission.participants.legacy
|
const participants = submission.participants.legacy
|
||||||
? [submission.thirdPartyPolicyholder]
|
? [submission.thirdPartyPolicyholder]
|
||||||
: submission.participants.participants;
|
: submission.participants.participants.filter(
|
||||||
|
(participant) => !participant.unknown,
|
||||||
|
);
|
||||||
const results: Record<string, unknown> = {};
|
const results: Record<string, unknown> = {};
|
||||||
|
|
||||||
for (const participant of participants) {
|
for (const participant of participants) {
|
||||||
@@ -535,6 +554,7 @@ export class RequestManagementService {
|
|||||||
message: err?.message || String(err),
|
message: err?.message || String(err),
|
||||||
status: err?.response?.status,
|
status: err?.response?.status,
|
||||||
data: err?.response?.data,
|
data: err?.response?.data,
|
||||||
|
...(Array.isArray(err?.attempts) ? { attempts: err.attempts } : {}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1575,9 +1595,15 @@ export class RequestManagementService {
|
|||||||
: undefined;
|
: undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const inquirySubmission = this.normalizeInquiryInput(req.type, body);
|
const inquirySubmission = this.normalizeInquiryInput(
|
||||||
|
req.type,
|
||||||
|
body,
|
||||||
|
role,
|
||||||
|
);
|
||||||
body = inquirySubmission.dto as unknown as AddPlateDto;
|
body = inquirySubmission.dto as unknown as AddPlateDto;
|
||||||
this.applyInquiryParticipantsToParty(party, inquirySubmission);
|
this.applyInquiryParticipantsToParty(party, inquirySubmission);
|
||||||
|
const policyholderUnknown =
|
||||||
|
inquirySubmission.thirdPartyPolicyholder.unknown === true;
|
||||||
|
|
||||||
// Validation: driver/insurer sameness rules
|
// Validation: driver/insurer sameness rules
|
||||||
if (body.driverIsInsurer === false) {
|
if (body.driverIsInsurer === false) {
|
||||||
@@ -1637,13 +1663,25 @@ export class RequestManagementService {
|
|||||||
this.logger.log(
|
this.logger.log(
|
||||||
`[TEJARAT] block inquiry mapped for request=${req._id}: ${JSON.stringify(inquiryMapped)}`,
|
`[TEJARAT] block inquiry mapped for request=${req._id}: ${JSON.stringify(inquiryMapped)}`,
|
||||||
);
|
);
|
||||||
this.recordPartyCaseInquiryStatus(req, "thirdParty", role, true, {
|
this.recordPartyCaseInquiryStatus(
|
||||||
|
req,
|
||||||
|
"thirdParty",
|
||||||
|
role,
|
||||||
|
!inquiry.skipped,
|
||||||
|
{
|
||||||
source: inquirySource,
|
source: inquirySource,
|
||||||
plateKind: inquiry.plateKind,
|
plateKind: inquiry.plateKind,
|
||||||
attempts: inquiry.attempts,
|
attempts: inquiry.attempts,
|
||||||
raw: inquiryRaw,
|
raw: inquiryRaw,
|
||||||
mapped: inquiryMapped,
|
mapped: inquiryMapped,
|
||||||
});
|
...(inquiry.skipped
|
||||||
|
? {
|
||||||
|
skipped: true,
|
||||||
|
reason: "Third-party policyholder is unknown",
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
},
|
||||||
|
);
|
||||||
if (inquiry.offline?.fanavaranDriverId != null) {
|
if (inquiry.offline?.fanavaranDriverId != null) {
|
||||||
if (!party.person) party.person = {} as any;
|
if (!party.person) party.person = {} as any;
|
||||||
party.person.fanavaranDriverId = inquiry.offline.fanavaranDriverId;
|
party.person.fanavaranDriverId = inquiry.offline.fanavaranDriverId;
|
||||||
@@ -1714,17 +1752,19 @@ export class RequestManagementService {
|
|||||||
|
|
||||||
// Find client by company code
|
// Find client by company code
|
||||||
const clientName = inquiryMapped?.CompanyName;
|
const clientName = inquiryMapped?.CompanyName;
|
||||||
if (!clientName) {
|
if (!clientName && !policyholderUnknown) {
|
||||||
throw new BadRequestException(
|
throw new BadRequestException(
|
||||||
`CompanyName missing from inquiry response`,
|
`CompanyName missing from inquiry response`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const companyCode = inquiryMapped?.CompanyCode;
|
const companyCode = inquiryMapped?.CompanyCode;
|
||||||
const client = await this.clientService.findOrCreateClientByCompanyCode(
|
const client = clientName
|
||||||
|
? await this.clientService.findOrCreateClientByCompanyCode(
|
||||||
companyCode,
|
companyCode,
|
||||||
clientName,
|
clientName,
|
||||||
);
|
)
|
||||||
if (!client) {
|
: null;
|
||||||
|
if (clientName && !client) {
|
||||||
throw new BadRequestException(
|
throw new BadRequestException(
|
||||||
`CompanyCode missing or invalid in inquiry response`,
|
`CompanyCode missing or invalid in inquiry response`,
|
||||||
);
|
);
|
||||||
@@ -1734,7 +1774,7 @@ export class RequestManagementService {
|
|||||||
if (!party.person) party.person = {} as any;
|
if (!party.person) party.person = {} as any;
|
||||||
const resolvedClientId =
|
const resolvedClientId =
|
||||||
(client as any)?._id ?? (client as any)?._doc?._id;
|
(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.nationalCodeOfInsurer = body.nationalCodeOfInsurer;
|
||||||
party.person.nationalCodeOfDriver = body.nationalCodeOfDriver;
|
party.person.nationalCodeOfDriver = body.nationalCodeOfDriver;
|
||||||
party.person.insurerLicense = body.insurerLicense;
|
party.person.insurerLicense = body.insurerLicense;
|
||||||
@@ -1980,7 +2020,11 @@ export class RequestManagementService {
|
|||||||
: undefined;
|
: undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const inquirySubmission = this.normalizeInquiryInput(req.type, body);
|
const inquirySubmission = this.normalizeInquiryInput(
|
||||||
|
req.type,
|
||||||
|
body,
|
||||||
|
role,
|
||||||
|
);
|
||||||
body = inquirySubmission.dto as unknown as InitialFormVinDto;
|
body = inquirySubmission.dto as unknown as InitialFormVinDto;
|
||||||
this.applyInquiryParticipantsToParty(party, inquirySubmission);
|
this.applyInquiryParticipantsToParty(party, inquirySubmission);
|
||||||
|
|
||||||
@@ -2123,7 +2167,7 @@ export class RequestManagementService {
|
|||||||
// Persist party data
|
// Persist party data
|
||||||
const resolvedClientId =
|
const resolvedClientId =
|
||||||
(client as any)?._id ?? (client as any)?._doc?._id;
|
(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.nationalCodeOfInsurer = body.nationalCodeOfInsurer;
|
||||||
party.person.nationalCodeOfDriver = body.nationalCodeOfDriver;
|
party.person.nationalCodeOfDriver = body.nationalCodeOfDriver;
|
||||||
party.person.insurerLicense = body.insurerLicense;
|
party.person.insurerLicense = body.insurerLicense;
|
||||||
@@ -6578,6 +6622,7 @@ export class RequestManagementService {
|
|||||||
const inquirySubmission = this.normalizeInquiryInput(
|
const inquirySubmission = this.normalizeInquiryInput(
|
||||||
req.type,
|
req.type,
|
||||||
formData.firstPartyPlate,
|
formData.firstPartyPlate,
|
||||||
|
PartyRole.FIRST,
|
||||||
);
|
);
|
||||||
const firstPartyPlate = inquirySubmission.dto;
|
const firstPartyPlate = inquirySubmission.dto;
|
||||||
let sandHubReport: any;
|
let sandHubReport: any;
|
||||||
@@ -6586,8 +6631,7 @@ export class RequestManagementService {
|
|||||||
? await (async () => {
|
? await (async () => {
|
||||||
const response = await this.sandHubService.getSandHubResponse({
|
const response = await this.sandHubService.getSandHubResponse({
|
||||||
plate: firstPartyPlate.plate,
|
plate: firstPartyPlate.plate,
|
||||||
nationalCodeOfInsurer:
|
nationalCodeOfInsurer: firstPartyPlate.nationalCodeOfInsurer,
|
||||||
firstPartyPlate.nationalCodeOfInsurer,
|
|
||||||
});
|
});
|
||||||
const mapped = response["_doc"] || response;
|
const mapped = response["_doc"] || response;
|
||||||
return {
|
return {
|
||||||
@@ -6857,8 +6901,14 @@ export class RequestManagementService {
|
|||||||
desc: string,
|
desc: string,
|
||||||
role: PartyRole,
|
role: PartyRole,
|
||||||
) => {
|
) => {
|
||||||
const inquirySubmission = this.normalizeInquiryInput(req.type, plateDto);
|
const inquirySubmission = this.normalizeInquiryInput(
|
||||||
|
req.type,
|
||||||
|
plateDto,
|
||||||
|
role,
|
||||||
|
);
|
||||||
plateDto = inquirySubmission.dto;
|
plateDto = inquirySubmission.dto;
|
||||||
|
const policyholderUnknown =
|
||||||
|
inquirySubmission.thirdPartyPolicyholder.unknown === true;
|
||||||
let inquiry: any;
|
let inquiry: any;
|
||||||
try {
|
try {
|
||||||
if (inquirySubmission.participants.legacy) {
|
if (inquirySubmission.participants.legacy) {
|
||||||
@@ -6871,9 +6921,7 @@ export class RequestManagementService {
|
|||||||
raw: mapped,
|
raw: mapped,
|
||||||
mapped,
|
mapped,
|
||||||
plateKind: "CURRENT",
|
plateKind: "CURRENT",
|
||||||
attempts: [
|
attempts: [{ plateKind: "CURRENT", succeeded: true, usable: true }],
|
||||||
{ plateKind: "CURRENT", succeeded: true, usable: true },
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
inquiry = await this.getThirdPartyPlateInquiry(inquirySubmission);
|
inquiry = await this.getThirdPartyPlateInquiry(inquirySubmission);
|
||||||
@@ -6897,23 +6945,37 @@ export class RequestManagementService {
|
|||||||
"THIRD_PARTY",
|
"THIRD_PARTY",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
this.recordPartyCaseInquiryStatus(req, "thirdParty", role, true, {
|
this.recordPartyCaseInquiryStatus(
|
||||||
|
req,
|
||||||
|
"thirdParty",
|
||||||
|
role,
|
||||||
|
!inquiry.skipped,
|
||||||
|
{
|
||||||
source: inquiry.offline?.source ?? "TEJARAT_BLOCK_INQUIRY",
|
source: inquiry.offline?.source ?? "TEJARAT_BLOCK_INQUIRY",
|
||||||
plateKind: inquiry.plateKind,
|
plateKind: inquiry.plateKind,
|
||||||
attempts: inquiry.attempts,
|
attempts: inquiry.attempts,
|
||||||
raw: inquiry.raw,
|
raw: inquiry.raw,
|
||||||
mapped: inquiry.mapped,
|
mapped: inquiry.mapped,
|
||||||
});
|
...(inquiry.skipped
|
||||||
|
? {
|
||||||
|
skipped: true,
|
||||||
|
reason: "Third-party policyholder is unknown",
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
},
|
||||||
|
);
|
||||||
const clientName =
|
const clientName =
|
||||||
sandHubReport?.CompanyName || sandHubReport?.LastCompanyName;
|
sandHubReport?.CompanyName || sandHubReport?.LastCompanyName;
|
||||||
const companyCode = sandHubReport?.CompanyCode;
|
const companyCode = sandHubReport?.CompanyCode;
|
||||||
const client = companyCode
|
const client = clientName
|
||||||
|
? companyCode
|
||||||
? await this.clientService.findOrCreateClientByCompanyCode(
|
? await this.clientService.findOrCreateClientByCompanyCode(
|
||||||
companyCode,
|
companyCode,
|
||||||
clientName,
|
clientName,
|
||||||
)
|
)
|
||||||
: await this.clientService.findOne({ clientName });
|
: await this.clientService.findOne({ clientName })
|
||||||
if (!client) {
|
: null;
|
||||||
|
if (!client && !policyholderUnknown) {
|
||||||
throw new NotFoundException(
|
throw new NotFoundException(
|
||||||
`Client not found for company: ${clientName}`,
|
`Client not found for company: ${clientName}`,
|
||||||
);
|
);
|
||||||
@@ -9112,9 +9174,15 @@ export class RequestManagementService {
|
|||||||
partyRole: PartyRole,
|
partyRole: PartyRole,
|
||||||
party: any,
|
party: any,
|
||||||
): Promise<{ clientId?: string }> {
|
): Promise<{ clientId?: string }> {
|
||||||
const inquirySubmission = this.normalizeInquiryInput(req.type, partyData);
|
const inquirySubmission = this.normalizeInquiryInput(
|
||||||
|
req.type,
|
||||||
|
partyData,
|
||||||
|
partyRole,
|
||||||
|
);
|
||||||
partyData = inquirySubmission.dto as RunInquiriesV3Dto;
|
partyData = inquirySubmission.dto as RunInquiriesV3Dto;
|
||||||
this.applyInquiryParticipantsToParty(party, inquirySubmission);
|
this.applyInquiryParticipantsToParty(party, inquirySubmission);
|
||||||
|
const policyholderUnknown =
|
||||||
|
inquirySubmission.thirdPartyPolicyholder.unknown === true;
|
||||||
const roleLabel = partyRole === PartyRole.FIRST ? "FIRST" : "SECOND";
|
const roleLabel = partyRole === PartyRole.FIRST ? "FIRST" : "SECOND";
|
||||||
let clientId: string | undefined;
|
let clientId: string | undefined;
|
||||||
const inquiryOptions = (cid?: string) =>
|
const inquiryOptions = (cid?: string) =>
|
||||||
@@ -9137,13 +9205,25 @@ export class RequestManagementService {
|
|||||||
if (inquiry.offline?.source) {
|
if (inquiry.offline?.source) {
|
||||||
inquirySource = inquiry.offline.source;
|
inquirySource = inquiry.offline.source;
|
||||||
}
|
}
|
||||||
this.recordPartyCaseInquiryStatus(req, "thirdParty", partyRole, true, {
|
this.recordPartyCaseInquiryStatus(
|
||||||
|
req,
|
||||||
|
"thirdParty",
|
||||||
|
partyRole,
|
||||||
|
!inquiry.skipped,
|
||||||
|
{
|
||||||
source: inquirySource,
|
source: inquirySource,
|
||||||
plateKind: inquiry.plateKind,
|
plateKind: inquiry.plateKind,
|
||||||
attempts: inquiry.attempts,
|
attempts: inquiry.attempts,
|
||||||
raw: inquiryRaw,
|
raw: inquiryRaw,
|
||||||
mapped: inquiryMapped,
|
mapped: inquiryMapped,
|
||||||
});
|
...(inquiry.skipped
|
||||||
|
? {
|
||||||
|
skipped: true,
|
||||||
|
reason: "Third-party policyholder is unknown",
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
},
|
||||||
|
);
|
||||||
if (inquiry.offline?.fanavaranDriverId != null) {
|
if (inquiry.offline?.fanavaranDriverId != null) {
|
||||||
if (!party.person) party.person = {} as any;
|
if (!party.person) party.person = {} as any;
|
||||||
party.person.fanavaranDriverId = inquiry.offline.fanavaranDriverId;
|
party.person.fanavaranDriverId = inquiry.offline.fanavaranDriverId;
|
||||||
@@ -9174,16 +9254,18 @@ export class RequestManagementService {
|
|||||||
|
|
||||||
const clientName = inquiryMapped?.CompanyName;
|
const clientName = inquiryMapped?.CompanyName;
|
||||||
const companyCode = inquiryMapped?.CompanyCode;
|
const companyCode = inquiryMapped?.CompanyCode;
|
||||||
if (!clientName) {
|
if (!clientName && !policyholderUnknown) {
|
||||||
throw new BadRequestException(
|
throw new BadRequestException(
|
||||||
`CompanyName missing from ${roleLabel} party inquiry response`,
|
`CompanyName missing from ${roleLabel} party inquiry response`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const client = await this.clientService.findOrCreateClientByCompanyCode(
|
const client = clientName
|
||||||
|
? await this.clientService.findOrCreateClientByCompanyCode(
|
||||||
companyCode,
|
companyCode,
|
||||||
clientName,
|
clientName,
|
||||||
);
|
)
|
||||||
if (!client) {
|
: null;
|
||||||
|
if (clientName && !client) {
|
||||||
throw new BadRequestException(
|
throw new BadRequestException(
|
||||||
`CompanyCode missing or invalid in ${roleLabel} party inquiry response`,
|
`CompanyCode missing or invalid in ${roleLabel} party inquiry response`,
|
||||||
);
|
);
|
||||||
@@ -9192,7 +9274,7 @@ export class RequestManagementService {
|
|||||||
clientId = resolvedClientId ? String(resolvedClientId) : undefined;
|
clientId = resolvedClientId ? String(resolvedClientId) : undefined;
|
||||||
|
|
||||||
if (!party.person) party.person = {} as any;
|
if (!party.person) party.person = {} as any;
|
||||||
party.person.clientId = resolvedClientId;
|
if (resolvedClientId) party.person.clientId = resolvedClientId;
|
||||||
party.person.nationalCodeOfInsurer = partyData.nationalCodeOfInsurer;
|
party.person.nationalCodeOfInsurer = partyData.nationalCodeOfInsurer;
|
||||||
party.person.nationalCodeOfDriver = partyData.nationalCodeOfDriver;
|
party.person.nationalCodeOfDriver = partyData.nationalCodeOfDriver;
|
||||||
party.person.driverIsInsurer = partyData.driverIsInsurer;
|
party.person.driverIsInsurer = partyData.driverIsInsurer;
|
||||||
@@ -9337,7 +9419,22 @@ export class RequestManagementService {
|
|||||||
? partyData.insurerLicense
|
? partyData.insurerLicense
|
||||||
: partyData.driverLicense;
|
: 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(
|
this.recordPartyCaseInquiryStatus(
|
||||||
req,
|
req,
|
||||||
"drivingLicence",
|
"drivingLicence",
|
||||||
@@ -9683,12 +9780,12 @@ export class RequestManagementService {
|
|||||||
if (!req) throw new NotFoundException("Blame request not found");
|
if (!req) throw new NotFoundException("Blame request not found");
|
||||||
this.assertBlameV3ExpertInPerson(req);
|
this.assertBlameV3ExpertInPerson(req);
|
||||||
await this.verifyExpertAccessForBlameV2(req, actor);
|
await this.verifyExpertAccessForBlameV2(req, actor);
|
||||||
dto = this.normalizeInquiryInput(req.type, dto).dto as RunInquiriesV3Dto;
|
|
||||||
|
|
||||||
const partyRole = this.resolveV3InquiryPartyRole(req);
|
const partyRole = this.resolveV3InquiryPartyRole(req);
|
||||||
if (!partyRole) {
|
if (!partyRole) {
|
||||||
throw new ConflictException("All party inquiries are already complete.");
|
throw new ConflictException("All party inquiries are already complete.");
|
||||||
}
|
}
|
||||||
|
dto = this.normalizeInquiryInput(req.type, dto, partyRole)
|
||||||
|
.dto as RunInquiriesV3Dto;
|
||||||
|
|
||||||
if (partyRole === PartyRole.FIRST) {
|
if (partyRole === PartyRole.FIRST) {
|
||||||
const firstIdx = this.getPartyIndex(req, 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");
|
if (!req) throw new NotFoundException("Blame request not found");
|
||||||
this.assertBlameV3ExpertInPerson(req);
|
this.assertBlameV3ExpertInPerson(req);
|
||||||
await this.verifyExpertAccessForBlameV2(req, actor);
|
await this.verifyExpertAccessForBlameV2(req, actor);
|
||||||
dto = this.normalizeInquiryInput(req.type, dto).dto as RunInquiriesVinV3Dto;
|
|
||||||
|
|
||||||
const partyRole = this.resolveV3InquiryPartyRole(req);
|
const partyRole = this.resolveV3InquiryPartyRole(req);
|
||||||
if (!partyRole) {
|
if (!partyRole) {
|
||||||
throw new ConflictException("All party inquiries are already complete.");
|
throw new ConflictException("All party inquiries are already complete.");
|
||||||
}
|
}
|
||||||
|
dto = this.normalizeInquiryInput(req.type, dto, partyRole)
|
||||||
|
.dto as RunInquiriesVinV3Dto;
|
||||||
|
|
||||||
if (partyRole === PartyRole.FIRST) {
|
if (partyRole === PartyRole.FIRST) {
|
||||||
const firstIdx = this.getPartyIndex(req, PartyRole.FIRST);
|
const firstIdx = this.getPartyIndex(req, PartyRole.FIRST);
|
||||||
@@ -9952,7 +10049,11 @@ export class RequestManagementService {
|
|||||||
partyRole: PartyRole,
|
partyRole: PartyRole,
|
||||||
party: any,
|
party: any,
|
||||||
): Promise<{ clientId?: string }> {
|
): Promise<{ clientId?: string }> {
|
||||||
const inquirySubmission = this.normalizeInquiryInput(req.type, partyData);
|
const inquirySubmission = this.normalizeInquiryInput(
|
||||||
|
req.type,
|
||||||
|
partyData,
|
||||||
|
partyRole,
|
||||||
|
);
|
||||||
partyData = inquirySubmission.dto as RunInquiriesVinV3Dto;
|
partyData = inquirySubmission.dto as RunInquiriesVinV3Dto;
|
||||||
this.applyInquiryParticipantsToParty(party, inquirySubmission);
|
this.applyInquiryParticipantsToParty(party, inquirySubmission);
|
||||||
const roleLabel = partyRole === PartyRole.FIRST ? "FIRST" : "SECOND";
|
const roleLabel = partyRole === PartyRole.FIRST ? "FIRST" : "SECOND";
|
||||||
@@ -10018,7 +10119,7 @@ export class RequestManagementService {
|
|||||||
clientId = resolvedClientId ? String(resolvedClientId) : undefined;
|
clientId = resolvedClientId ? String(resolvedClientId) : undefined;
|
||||||
|
|
||||||
if (!party.person) party.person = {} as any;
|
if (!party.person) party.person = {} as any;
|
||||||
party.person.clientId = resolvedClientId;
|
if (resolvedClientId) party.person.clientId = resolvedClientId;
|
||||||
party.person.nationalCodeOfInsurer = partyData.nationalCodeOfInsurer;
|
party.person.nationalCodeOfInsurer = partyData.nationalCodeOfInsurer;
|
||||||
party.person.nationalCodeOfDriver = partyData.nationalCodeOfDriver;
|
party.person.nationalCodeOfDriver = partyData.nationalCodeOfDriver;
|
||||||
party.person.driverIsInsurer = partyData.driverIsInsurer;
|
party.person.driverIsInsurer = partyData.driverIsInsurer;
|
||||||
@@ -10174,7 +10275,22 @@ export class RequestManagementService {
|
|||||||
? partyData.insurerLicense
|
? partyData.insurerLicense
|
||||||
: partyData.driverLicense;
|
: 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(
|
this.recordPartyCaseInquiryStatus(
|
||||||
req,
|
req,
|
||||||
"drivingLicence",
|
"drivingLicence",
|
||||||
@@ -11403,7 +11519,7 @@ export class RequestManagementService {
|
|||||||
if (firstIdx === -1) throw new BadRequestException("First party not found");
|
if (firstIdx === -1) throw new BadRequestException("First party not found");
|
||||||
const firstParty = req.parties[firstIdx];
|
const firstParty = req.parties[firstIdx];
|
||||||
|
|
||||||
dto = this.normalizeInquiryInput(req.type, dto).dto;
|
dto = this.normalizeInquiryInput(req.type, dto, PartyRole.FIRST).dto;
|
||||||
|
|
||||||
await this.runPartyInquiriesV3Internal(
|
await this.runPartyInquiriesV3Internal(
|
||||||
req,
|
req,
|
||||||
@@ -11526,7 +11642,7 @@ export class RequestManagementService {
|
|||||||
if (firstIdx === -1) throw new BadRequestException("First party not found");
|
if (firstIdx === -1) throw new BadRequestException("First party not found");
|
||||||
const firstParty = req.parties[firstIdx];
|
const firstParty = req.parties[firstIdx];
|
||||||
|
|
||||||
dto = this.normalizeInquiryInput(req.type, dto).dto;
|
dto = this.normalizeInquiryInput(req.type, dto, PartyRole.FIRST).dto;
|
||||||
|
|
||||||
await this.runPartyInquiriesVinV3Internal(
|
await this.runPartyInquiriesVinV3Internal(
|
||||||
req,
|
req,
|
||||||
|
|||||||
Reference in New Issue
Block a user