forked from Yara724/api
Implement role-complete inquiry participants
This commit is contained in:
@@ -1,5 +1,11 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { IsNotEmpty, IsOptional, IsString, MaxLength } from "class-validator";
|
||||
import {
|
||||
IsNotEmpty,
|
||||
IsOptional,
|
||||
IsString,
|
||||
MaxLength,
|
||||
ValidateIf,
|
||||
} from "class-validator";
|
||||
import { Types } from "mongoose";
|
||||
import { AddPlateDto } from "src/profile/dto/user/AddPlateDto";
|
||||
import { StepsEnum } from "src/Types&Enums/blame-request-management/steps.enum";
|
||||
@@ -10,6 +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";
|
||||
|
||||
export class InitialFormDto {
|
||||
@ApiProperty({ required: false, default: false })
|
||||
@@ -227,10 +234,10 @@ export class BlameConfessionDtoV2 {
|
||||
* All identity and license fields from {@link AddPlateDto} are preserved; only
|
||||
* `plate` is replaced by `vin` (the 17-character chassis / VIN string).
|
||||
*/
|
||||
export class InitialFormVinDto {
|
||||
@ApiProperty({
|
||||
export class InitialFormVinDto extends InquiryParticipantFieldsDto {
|
||||
@ApiPropertyOptional({
|
||||
type: String,
|
||||
required: true,
|
||||
required: false,
|
||||
description: "17-character VIN / chassis number (شماره شاسی)",
|
||||
example: "NAAM01E15HK123456",
|
||||
maxLength: 17,
|
||||
@@ -238,30 +245,49 @@ export class InitialFormVinDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@MaxLength(17)
|
||||
@ValidateIf((dto) => !dto.vehicle?.vin)
|
||||
vin: string;
|
||||
|
||||
@ApiProperty({ type: String, required: true })
|
||||
@ApiPropertyOptional({
|
||||
type: String,
|
||||
description: "Legacy third-party policyholder national code",
|
||||
})
|
||||
@ValidateIf((dto) => !dto.thirdPartyPolicyholder)
|
||||
nationalCodeOfInsurer: string;
|
||||
|
||||
@ApiProperty({ type: String, required: true })
|
||||
@ApiPropertyOptional({
|
||||
type: String,
|
||||
description: "Legacy driver national code",
|
||||
})
|
||||
@ValidateIf((dto) => !dto.driver)
|
||||
nationalCodeOfDriver: string;
|
||||
|
||||
@ApiProperty({ type: String, required: true })
|
||||
@ApiPropertyOptional({ type: String })
|
||||
insurerLicense: string;
|
||||
|
||||
@ApiProperty({ type: String, required: true })
|
||||
@ApiPropertyOptional({ type: String })
|
||||
driverLicense: string;
|
||||
|
||||
@ApiProperty({ type: Boolean, required: true })
|
||||
@ApiPropertyOptional({
|
||||
type: Boolean,
|
||||
description: "Legacy role relationship",
|
||||
})
|
||||
@ValidateIf((dto) => !dto.driver)
|
||||
driverIsInsurer: boolean;
|
||||
|
||||
@ApiProperty({ type: Boolean, required: true, default: false })
|
||||
@ApiPropertyOptional({ type: Boolean, default: false })
|
||||
isNewCar: boolean;
|
||||
|
||||
@ApiProperty({ type: Boolean, required: true })
|
||||
@ApiPropertyOptional({
|
||||
type: Boolean,
|
||||
description: "Legacy; use driver.hasDrivingLicense",
|
||||
})
|
||||
userNoCertificate: boolean;
|
||||
|
||||
@ApiProperty({ type: Number, required: true })
|
||||
@ApiPropertyOptional({
|
||||
type: Number,
|
||||
description: "Legacy policyholder birthday",
|
||||
})
|
||||
insurerBirthday: number;
|
||||
|
||||
@ApiPropertyOptional({ type: String, required: false })
|
||||
|
||||
144
src/request-management/dto/inquiry-participants.dto.ts
Normal file
144
src/request-management/dto/inquiry-participants.dto.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
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;
|
||||
}
|
||||
@@ -5,9 +5,11 @@ import {
|
||||
IsOptional,
|
||||
IsString,
|
||||
MaxLength,
|
||||
ValidateIf,
|
||||
ValidateNested,
|
||||
} from "class-validator";
|
||||
import { Type } from "class-transformer";
|
||||
import { InquiryParticipantFieldsDto } from "./inquiry-participants.dto";
|
||||
|
||||
class PlateV6Dto {
|
||||
@ApiProperty({ example: "44", description: "Left two digits" })
|
||||
@@ -25,7 +27,10 @@ class PlateV6Dto {
|
||||
@IsNotEmpty()
|
||||
centerDigits: string;
|
||||
|
||||
@ApiProperty({ example: "22", description: "Right two digits (Iran region code)" })
|
||||
@ApiProperty({
|
||||
example: "22",
|
||||
description: "Right two digits (Iran region code)",
|
||||
})
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
ir: string;
|
||||
@@ -35,35 +40,52 @@ class PlateV6Dto {
|
||||
* Inquiry body for the V6 call-center flow.
|
||||
* Same as V3 but without `sheba` — the user adds their own IBAN later via the link.
|
||||
*/
|
||||
export class RunCallCenterInquiryV6Dto {
|
||||
@ApiProperty({
|
||||
export class RunCallCenterInquiryV6Dto extends InquiryParticipantFieldsDto {
|
||||
@ApiPropertyOptional({
|
||||
type: PlateV6Dto,
|
||||
description: "Plate segments — Tejarat block / third-party inquiry.",
|
||||
})
|
||||
@ValidateNested()
|
||||
@Type(() => PlateV6Dto)
|
||||
@ValidateIf((dto) => !dto.vehicle?.currentPlate)
|
||||
plate: PlateV6Dto;
|
||||
|
||||
@ApiProperty({ example: "1234567890", description: "National code of the policyholder (insurer)" })
|
||||
@ApiPropertyOptional({
|
||||
example: "1234567890",
|
||||
description: "Legacy third-party policyholder national code",
|
||||
})
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@ValidateIf((dto) => !dto.thirdPartyPolicyholder)
|
||||
nationalCodeOfInsurer: string;
|
||||
|
||||
@ApiProperty({ example: "1234567890", description: "National code of the driver" })
|
||||
@ApiPropertyOptional({
|
||||
example: "1234567890",
|
||||
description: "Legacy driver national code",
|
||||
})
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@ValidateIf((dto) => !dto.driver)
|
||||
nationalCodeOfDriver: string;
|
||||
|
||||
@ApiProperty({ example: true, description: "Whether the driver is the same person as the insurer" })
|
||||
@ApiPropertyOptional({
|
||||
example: true,
|
||||
description: "Legacy role relationship",
|
||||
})
|
||||
@IsBoolean()
|
||||
@ValidateIf((dto) => !dto.driver)
|
||||
driverIsInsurer: boolean;
|
||||
|
||||
@ApiProperty({ example: 13780624, description: "Insurer birth date (Jalali)" })
|
||||
@ApiPropertyOptional({
|
||||
example: 13780624,
|
||||
description: "Legacy policyholder birth date (Jalali)",
|
||||
})
|
||||
insurerBirthday: number | string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
example: 13780624,
|
||||
description: "Driver birth date (Jalali). Required when driverIsInsurer is false.",
|
||||
description:
|
||||
"Driver birth date (Jalali). Required when driverIsInsurer is false.",
|
||||
})
|
||||
@IsOptional()
|
||||
driverBirthday?: number | string | null;
|
||||
@@ -90,8 +112,8 @@ export class RunCallCenterInquiryV6Dto {
|
||||
* Identical to `RunCallCenterInquiryV6Dto` but replaces `plate` with `vin`.
|
||||
* Sheba (IBAN) is intentionally absent — the user provides it themselves via the link.
|
||||
*/
|
||||
export class RunCallCenterInquiryVinV6Dto {
|
||||
@ApiProperty({
|
||||
export class RunCallCenterInquiryVinV6Dto extends InquiryParticipantFieldsDto {
|
||||
@ApiPropertyOptional({
|
||||
example: "NAAM01E15HK123456",
|
||||
description: "17-character VIN / chassis number (شماره شاسی)",
|
||||
maxLength: 17,
|
||||
@@ -99,28 +121,45 @@ export class RunCallCenterInquiryVinV6Dto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@MaxLength(17)
|
||||
@ValidateIf((dto) => !dto.vehicle?.vin)
|
||||
vin: string;
|
||||
|
||||
@ApiProperty({ example: "1234567890", description: "National code of the policyholder (insurer)" })
|
||||
@ApiPropertyOptional({
|
||||
example: "1234567890",
|
||||
description: "Legacy third-party policyholder national code",
|
||||
})
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@ValidateIf((dto) => !dto.thirdPartyPolicyholder)
|
||||
nationalCodeOfInsurer: string;
|
||||
|
||||
@ApiProperty({ example: "1234567890", description: "National code of the driver" })
|
||||
@ApiPropertyOptional({
|
||||
example: "1234567890",
|
||||
description: "Legacy driver national code",
|
||||
})
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@ValidateIf((dto) => !dto.driver)
|
||||
nationalCodeOfDriver: string;
|
||||
|
||||
@ApiProperty({ example: true, description: "Whether the driver is the same person as the insurer" })
|
||||
@ApiPropertyOptional({
|
||||
example: true,
|
||||
description: "Legacy role relationship",
|
||||
})
|
||||
@IsBoolean()
|
||||
@ValidateIf((dto) => !dto.driver)
|
||||
driverIsInsurer: boolean;
|
||||
|
||||
@ApiProperty({ example: 13780624, description: "Insurer birth date (Jalali)" })
|
||||
@ApiPropertyOptional({
|
||||
example: 13780624,
|
||||
description: "Legacy policyholder birth date (Jalali)",
|
||||
})
|
||||
insurerBirthday: number | string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
example: 13780624,
|
||||
description: "Driver birth date (Jalali). Required when driverIsInsurer is false.",
|
||||
description:
|
||||
"Driver birth date (Jalali). Required when driverIsInsurer is false.",
|
||||
})
|
||||
@IsOptional()
|
||||
driverBirthday?: number | string | null;
|
||||
|
||||
@@ -5,9 +5,11 @@ import {
|
||||
IsOptional,
|
||||
IsString,
|
||||
MaxLength,
|
||||
ValidateIf,
|
||||
ValidateNested,
|
||||
} from "class-validator";
|
||||
import { Type } from "class-transformer";
|
||||
import { InquiryParticipantFieldsDto } from "./inquiry-participants.dto";
|
||||
|
||||
class PlateV3Dto {
|
||||
@ApiProperty({ example: "44", description: "Left two digits" })
|
||||
@@ -25,7 +27,10 @@ class PlateV3Dto {
|
||||
@IsNotEmpty()
|
||||
centerDigits: string;
|
||||
|
||||
@ApiProperty({ example: "22", description: "Right two digits (Iran region code)" })
|
||||
@ApiProperty({
|
||||
example: "22",
|
||||
description: "Right two digits (Iran region code)",
|
||||
})
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
ir: string;
|
||||
@@ -35,33 +40,53 @@ class PlateV3Dto {
|
||||
* Body for `POST run-inquiries/:requestId`.
|
||||
* First call = guilty party (+ auto claim). Second call = damaged party (THIRD_PARTY only).
|
||||
*/
|
||||
export class RunInquiriesV3Dto {
|
||||
@ApiProperty({
|
||||
export class RunInquiriesV3Dto extends InquiryParticipantFieldsDto {
|
||||
@ApiPropertyOptional({
|
||||
type: PlateV3Dto,
|
||||
description: "Plate segments — Tejarat block / third-party inquiry.",
|
||||
})
|
||||
@ValidateNested()
|
||||
@Type(() => PlateV3Dto)
|
||||
@ValidateIf((dto) => !dto.vehicle?.currentPlate)
|
||||
plate: PlateV3Dto;
|
||||
|
||||
@ApiProperty({ example: "1234567890", description: "National code of the policyholder (insurer)" })
|
||||
@ApiPropertyOptional({
|
||||
example: "1234567890",
|
||||
description: "Legacy third-party policyholder national code",
|
||||
})
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@ValidateIf((dto) => !dto.thirdPartyPolicyholder)
|
||||
nationalCodeOfInsurer: string;
|
||||
|
||||
@ApiProperty({ example: "1234567890", description: "National code of the driver" })
|
||||
@ApiPropertyOptional({
|
||||
example: "1234567890",
|
||||
description: "Legacy driver national code",
|
||||
})
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@ValidateIf((dto) => !dto.driver)
|
||||
nationalCodeOfDriver: string;
|
||||
|
||||
@ApiProperty({ example: true, description: "Whether the driver is the same person as the insurer" })
|
||||
@ApiPropertyOptional({
|
||||
example: true,
|
||||
description: "Legacy role relationship",
|
||||
})
|
||||
@IsBoolean()
|
||||
@ValidateIf((dto) => !dto.driver)
|
||||
driverIsInsurer: boolean;
|
||||
|
||||
@ApiProperty({ example: 13780624, description: "Insurer birth date (Jalali)" })
|
||||
@ApiPropertyOptional({
|
||||
example: 13780624,
|
||||
description: "Legacy policyholder birth date (Jalali)",
|
||||
})
|
||||
insurerBirthday: number | string;
|
||||
|
||||
@ApiPropertyOptional({ example: 13780624, description: "Driver birth date (Jalali). Required when driverIsInsurer is false." })
|
||||
@ApiPropertyOptional({
|
||||
example: 13780624,
|
||||
description:
|
||||
"Driver birth date (Jalali). Required when driverIsInsurer is false.",
|
||||
})
|
||||
@IsOptional()
|
||||
driverBirthday?: number | string | null;
|
||||
|
||||
@@ -106,8 +131,8 @@ export class RunInquiriesV3Dto {
|
||||
* Identical to RunInquiriesV3Dto but uses `vin` (17-char chassis number) instead of `plate`.
|
||||
* First call = guilty party (+ auto claim). Second call = damaged party (THIRD_PARTY only).
|
||||
*/
|
||||
export class RunInquiriesVinV3Dto {
|
||||
@ApiProperty({
|
||||
export class RunInquiriesVinV3Dto extends InquiryParticipantFieldsDto {
|
||||
@ApiPropertyOptional({
|
||||
example: "NAAM01E15HK123456",
|
||||
description: "17-character VIN / chassis number (شماره شاسی)",
|
||||
maxLength: 17,
|
||||
@@ -115,26 +140,46 @@ export class RunInquiriesVinV3Dto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@MaxLength(17)
|
||||
@ValidateIf((dto) => !dto.vehicle?.vin)
|
||||
vin: string;
|
||||
|
||||
@ApiProperty({ example: "1234567890", description: "National code of the policyholder (insurer)" })
|
||||
@ApiPropertyOptional({
|
||||
example: "1234567890",
|
||||
description: "Legacy third-party policyholder national code",
|
||||
})
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@ValidateIf((dto) => !dto.thirdPartyPolicyholder)
|
||||
nationalCodeOfInsurer: string;
|
||||
|
||||
@ApiProperty({ example: "1234567890", description: "National code of the driver" })
|
||||
@ApiPropertyOptional({
|
||||
example: "1234567890",
|
||||
description: "Legacy driver national code",
|
||||
})
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@ValidateIf((dto) => !dto.driver)
|
||||
nationalCodeOfDriver: string;
|
||||
|
||||
@ApiProperty({ example: true, description: "Whether the driver is the same person as the insurer" })
|
||||
@ApiPropertyOptional({
|
||||
example: true,
|
||||
description: "Legacy role relationship",
|
||||
})
|
||||
@IsBoolean()
|
||||
@ValidateIf((dto) => !dto.driver)
|
||||
driverIsInsurer: boolean;
|
||||
|
||||
@ApiProperty({ example: 13780624, description: "Insurer birth date (Jalali)" })
|
||||
@ApiPropertyOptional({
|
||||
example: 13780624,
|
||||
description: "Legacy policyholder birth date (Jalali)",
|
||||
})
|
||||
insurerBirthday: number | string;
|
||||
|
||||
@ApiPropertyOptional({ example: 13780624, description: "Driver birth date (Jalali). Required when driverIsInsurer is false." })
|
||||
@ApiPropertyOptional({
|
||||
example: 13780624,
|
||||
description:
|
||||
"Driver birth date (Jalali). Required when driverIsInsurer is false.",
|
||||
})
|
||||
@IsOptional()
|
||||
driverBirthday?: number | string | null;
|
||||
|
||||
|
||||
@@ -4,213 +4,253 @@ import { Schema as MongooseSchema, Types } from "mongoose";
|
||||
import { Location, LocationSchema } from "./accidentInformation.type";
|
||||
|
||||
export enum PartyRole {
|
||||
FIRST = "FIRST",
|
||||
SECOND = "SECOND",
|
||||
}
|
||||
FIRST = "FIRST",
|
||||
SECOND = "SECOND",
|
||||
}
|
||||
|
||||
@Schema({ _id: false })
|
||||
export class Person {
|
||||
@Prop({ type: Types.ObjectId })
|
||||
userId?: Types.ObjectId;
|
||||
@Prop({ type: Types.ObjectId })
|
||||
userId?: Types.ObjectId;
|
||||
|
||||
@Prop() fullName?: string;
|
||||
@Prop() phoneNumber?: string;
|
||||
@Prop({ type: Types.ObjectId }) clientId?: Types.ObjectId;
|
||||
@Prop() birthday?: string;
|
||||
@Prop() fullName?: string;
|
||||
@Prop() phoneNumber?: string;
|
||||
@Prop({ type: Types.ObjectId }) clientId?: Types.ObjectId;
|
||||
@Prop() birthday?: string;
|
||||
|
||||
// ---- FIRST_INITIAL_FORM (step-manager driven) ----
|
||||
@Prop() nationalCodeOfInsurer?: string;
|
||||
@Prop() nationalCodeOfDriver?: string;
|
||||
@Prop() insurerLicense?: string;
|
||||
@Prop() driverLicense?: string;
|
||||
/** Driving licence type code from the Fanavaran lookup (GET /lookups/driving-licence-types). */
|
||||
@Prop() licenseType?: string;
|
||||
// ---- FIRST_INITIAL_FORM (step-manager driven) ----
|
||||
@Prop() nationalCodeOfInsurer?: string;
|
||||
@Prop() nationalCodeOfDriver?: string;
|
||||
@Prop() insurerLicense?: string;
|
||||
@Prop() driverLicense?: string;
|
||||
/** Driving licence type code from the Fanavaran lookup (GET /lookups/driving-licence-types). */
|
||||
@Prop() licenseType?: string;
|
||||
|
||||
@Prop({ type: Boolean })
|
||||
driverIsInsurer?: boolean;
|
||||
@Prop({ type: Boolean })
|
||||
driverIsInsurer?: boolean;
|
||||
|
||||
@Prop({ type: Boolean })
|
||||
isNewCar?: boolean;
|
||||
@Prop({ type: Boolean })
|
||||
isNewCar?: boolean;
|
||||
|
||||
/**
|
||||
* Mirrors existing DTO `userNoCertificate` (true means user has NO certificate).
|
||||
*/
|
||||
@Prop({ type: Boolean })
|
||||
userNoCertificate?: boolean;
|
||||
/**
|
||||
* Mirrors existing DTO `userNoCertificate` (true means user has NO certificate).
|
||||
*/
|
||||
@Prop({ type: Boolean })
|
||||
userNoCertificate?: boolean;
|
||||
|
||||
@Prop({ type: Number })
|
||||
insurerBirthday?: number;
|
||||
@Prop({ type: Number })
|
||||
insurerBirthday?: number;
|
||||
|
||||
@Prop({ type: String })
|
||||
driverBirthday?: string | null;
|
||||
@Prop({ type: String })
|
||||
driverBirthday?: string | null;
|
||||
|
||||
/** Cached Fanavaran party ID resolved from nationalCodeOfDriver + driverBirthday + driverIsInsurer */
|
||||
@Prop({ type: Number })
|
||||
fanavaranDriverId?: number;
|
||||
/** Cached Fanavaran party ID resolved from nationalCodeOfDriver + driverBirthday + driverIsInsurer */
|
||||
@Prop({ type: Number })
|
||||
fanavaranDriverId?: number;
|
||||
}
|
||||
export const PersonSchema = SchemaFactory.createForClass(Person);
|
||||
|
||||
@Schema({ _id: false })
|
||||
export class Vehicle {
|
||||
@Prop() plateId?: string;
|
||||
/** VIN / chassis number — populated only when the inquiry was performed via VIN lookup. */
|
||||
@Prop() vin?: string;
|
||||
@Prop() name?: string;
|
||||
@Prop() model?: string;
|
||||
@Prop() type?: string;
|
||||
@Prop() isNew?: boolean;
|
||||
export class InquiryParticipant {
|
||||
@Prop({ required: true })
|
||||
participantId: string;
|
||||
|
||||
/**
|
||||
* Full external inquiry payload (Tejarat/SandHub) stored as-is
|
||||
* so we never lose fields that are not mapped yet.
|
||||
*/
|
||||
@Prop({ type: MongooseSchema.Types.Mixed })
|
||||
inquiry?: any;
|
||||
@Prop()
|
||||
nationalCode: string;
|
||||
|
||||
@Prop()
|
||||
birthday: string;
|
||||
|
||||
@Prop() fullName?: string;
|
||||
@Prop() phoneNumber?: string;
|
||||
@Prop({ type: Boolean }) hasDrivingLicense?: boolean;
|
||||
@Prop() licenseNumber?: string;
|
||||
@Prop() licenseType?: string;
|
||||
}
|
||||
export const InquiryParticipantSchema =
|
||||
SchemaFactory.createForClass(InquiryParticipant);
|
||||
|
||||
@Schema({ _id: false })
|
||||
export class InquiryParticipantRoles {
|
||||
@Prop() driver?: string;
|
||||
@Prop() vehicleOwner?: string;
|
||||
@Prop() thirdPartyPolicyholder?: string;
|
||||
@Prop() carBodyPolicyholder?: string;
|
||||
}
|
||||
export const InquiryParticipantRolesSchema = SchemaFactory.createForClass(
|
||||
InquiryParticipantRoles,
|
||||
);
|
||||
|
||||
@Schema({ _id: false })
|
||||
export class Vehicle {
|
||||
@Prop() plateId?: string;
|
||||
/** VIN / chassis number — populated only when the inquiry was performed via VIN lookup. */
|
||||
@Prop() vin?: string;
|
||||
@Prop() name?: string;
|
||||
@Prop() model?: string;
|
||||
@Prop() type?: string;
|
||||
@Prop() isNew?: boolean;
|
||||
@Prop({ enum: ["CURRENT", "RECENTLY_TRANSFERRED"] })
|
||||
registrationState?: "CURRENT" | "RECENTLY_TRANSFERRED";
|
||||
@Prop() previousPlateId?: string;
|
||||
|
||||
/**
|
||||
* Full external inquiry payload (Tejarat/SandHub) stored as-is
|
||||
* so we never lose fields that are not mapped yet.
|
||||
*/
|
||||
@Prop({ type: MongooseSchema.Types.Mixed })
|
||||
inquiry?: any;
|
||||
}
|
||||
export const VehicleSchema = SchemaFactory.createForClass(Vehicle);
|
||||
|
||||
@Schema({ _id: false })
|
||||
export class Insurance {
|
||||
@Prop() policyNumber?: string;
|
||||
@Prop() company?: string;
|
||||
@Prop() startDate?: string;
|
||||
@Prop() endDate?: string;
|
||||
@Prop() financialCeiling?: string;
|
||||
@Prop({ type: [String] })
|
||||
coverages?: string[];
|
||||
@Prop() policyNumber?: string;
|
||||
@Prop() company?: string;
|
||||
@Prop() startDate?: string;
|
||||
@Prop() endDate?: string;
|
||||
@Prop() financialCeiling?: string;
|
||||
@Prop({ type: [String] })
|
||||
coverages?: string[];
|
||||
|
||||
/** CAR_BODY inquiry result; raw provider data is retained in vehicle.inquiry.carBody.raw. */
|
||||
@Prop({ type: MongooseSchema.Types.Mixed })
|
||||
carBodyInsurance?: {
|
||||
policyNumber?: string;
|
||||
policyId?: number;
|
||||
contractId?: number;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
insurerCompany?: string;
|
||||
coverages?: string[];
|
||||
companyId?: number | string;
|
||||
companyName?: string;
|
||||
insurerName?: string;
|
||||
insurerNationalCode?: string;
|
||||
ownerNationalCode?: string;
|
||||
ownerName?: string;
|
||||
customerName?: string;
|
||||
customerLastName?: string;
|
||||
customerFatherName?: string;
|
||||
customerMobile?: string;
|
||||
customerAddress?: string;
|
||||
customerPostalCode?: string;
|
||||
chassisNumber?: string;
|
||||
vin?: string;
|
||||
motorNumber?: string;
|
||||
vehicleGroup?: string;
|
||||
vehicleSystem?: string;
|
||||
vehicleKind?: string;
|
||||
builtYear?: number;
|
||||
cylinderCount?: number;
|
||||
passengerCount?: number;
|
||||
usage?: string;
|
||||
issueDate?: string;
|
||||
vehicleValue?: number;
|
||||
totalPremium?: number;
|
||||
noLossYearsCount?: number;
|
||||
lossDocuments?: unknown[];
|
||||
hasEndorsement?: boolean;
|
||||
};
|
||||
/** CAR_BODY inquiry result; raw provider data is retained in vehicle.inquiry.carBody.raw. */
|
||||
@Prop({ type: MongooseSchema.Types.Mixed })
|
||||
carBodyInsurance?: {
|
||||
policyNumber?: string;
|
||||
policyId?: number;
|
||||
contractId?: number;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
insurerCompany?: string;
|
||||
coverages?: string[];
|
||||
companyId?: number | string;
|
||||
companyName?: string;
|
||||
insurerName?: string;
|
||||
insurerNationalCode?: string;
|
||||
ownerNationalCode?: string;
|
||||
ownerName?: string;
|
||||
customerName?: string;
|
||||
customerLastName?: string;
|
||||
customerFatherName?: string;
|
||||
customerMobile?: string;
|
||||
customerAddress?: string;
|
||||
customerPostalCode?: string;
|
||||
chassisNumber?: string;
|
||||
vin?: string;
|
||||
motorNumber?: string;
|
||||
vehicleGroup?: string;
|
||||
vehicleSystem?: string;
|
||||
vehicleKind?: string;
|
||||
builtYear?: number;
|
||||
cylinderCount?: number;
|
||||
passengerCount?: number;
|
||||
usage?: string;
|
||||
issueDate?: string;
|
||||
vehicleValue?: number;
|
||||
totalPremium?: number;
|
||||
noLossYearsCount?: number;
|
||||
lossDocuments?: unknown[];
|
||||
hasEndorsement?: boolean;
|
||||
};
|
||||
}
|
||||
export const InsuranceSchema = SchemaFactory.createForClass(Insurance);
|
||||
|
||||
@Schema({ _id: false })
|
||||
export class PartyStatement {
|
||||
@Prop({ default: false })
|
||||
acceptsExpertOpinion?: boolean;
|
||||
|
||||
@Prop({ default: false })
|
||||
acceptsExpertOpinion?: boolean;
|
||||
|
||||
@Prop({ default: false })
|
||||
claimsDamage?: boolean;
|
||||
|
||||
@Prop({ default: false })
|
||||
admitsGuilt?: boolean;
|
||||
|
||||
@Prop({ type: String })
|
||||
description?: string;
|
||||
@Prop({ default: false })
|
||||
claimsDamage?: boolean;
|
||||
|
||||
/** CAR_BODY: accident conditions from description step */
|
||||
@Prop() accidentDate?: Date;
|
||||
@Prop() accidentTime?: string;
|
||||
@Prop() weatherCondition?: string;
|
||||
@Prop() roadCondition?: string;
|
||||
@Prop() lightCondition?: string;
|
||||
@Prop({ default: false })
|
||||
admitsGuilt?: boolean;
|
||||
|
||||
@Prop({ type: String })
|
||||
description?: string;
|
||||
|
||||
/** CAR_BODY: accident conditions from description step */
|
||||
@Prop() accidentDate?: Date;
|
||||
@Prop() accidentTime?: string;
|
||||
@Prop() weatherCondition?: string;
|
||||
@Prop() roadCondition?: string;
|
||||
@Prop() lightCondition?: string;
|
||||
}
|
||||
export const PartyStatementSchema = SchemaFactory.createForClass(PartyStatement);
|
||||
export const PartyStatementSchema =
|
||||
SchemaFactory.createForClass(PartyStatement);
|
||||
|
||||
@Schema({ _id: false })
|
||||
export class EvidenceBundle {
|
||||
@Prop({ type: [String] })
|
||||
images?: string[];
|
||||
|
||||
@Prop({ type: [String] })
|
||||
images?: string[];
|
||||
|
||||
@Prop({ type: [String] })
|
||||
voices?: string[];
|
||||
|
||||
@Prop() videoId?: string;
|
||||
@Prop({ type: [String] })
|
||||
voices?: string[];
|
||||
|
||||
@Prop() videoId?: string;
|
||||
}
|
||||
export const EvidenceBundleSchema = SchemaFactory.createForClass(EvidenceBundle);
|
||||
export const EvidenceBundleSchema =
|
||||
SchemaFactory.createForClass(EvidenceBundle);
|
||||
|
||||
@Schema({ _id: false })
|
||||
export class Signature {
|
||||
@Prop() fileId: string;
|
||||
@Prop() fileName: string;
|
||||
@Prop() fileUrl: string;
|
||||
@Prop() fileId: string;
|
||||
@Prop() fileName: string;
|
||||
@Prop() fileUrl: string;
|
||||
}
|
||||
export const SignatureSchema = SchemaFactory.createForClass(Signature);
|
||||
|
||||
@Schema({ _id: false })
|
||||
export class PartyConfirmation {
|
||||
@Prop() partyRole: PartyRole;
|
||||
@Prop() accepted: boolean;
|
||||
@Prop({ type: SignatureSchema })
|
||||
signature?: Signature;
|
||||
@Prop() partyRole: PartyRole;
|
||||
@Prop() accepted: boolean;
|
||||
@Prop({ type: SignatureSchema })
|
||||
signature?: Signature;
|
||||
}
|
||||
export const PartyConfirmationSchema =
|
||||
SchemaFactory.createForClass(PartyConfirmation);
|
||||
|
||||
@Schema({ _id: false })
|
||||
export class Party {
|
||||
@Prop({ enum: PartyRole })
|
||||
role: PartyRole;
|
||||
|
||||
@Prop({ enum: PartyRole })
|
||||
role: PartyRole;
|
||||
|
||||
@Prop({ type: PersonSchema })
|
||||
person: Person;
|
||||
@Prop({ type: PersonSchema })
|
||||
person: Person;
|
||||
|
||||
/**
|
||||
* CAR_BODY only: first form – accident with car vs object.
|
||||
* Second form (guilty/damaged) may be added later as carBodySecondForm.
|
||||
*/
|
||||
@Prop({ type: MongooseSchema.Types.Mixed })
|
||||
carBodyFirstForm?: { car?: boolean; object?: boolean };
|
||||
/** Distinct people involved with this vehicle; roles below reference participantId. */
|
||||
@Prop({ type: [InquiryParticipantSchema], default: undefined })
|
||||
participants?: InquiryParticipant[];
|
||||
|
||||
/**
|
||||
* Party-submitted location (step-driven: FIRST_LOCATION / SECOND_LOCATION).
|
||||
*/
|
||||
@Prop({ type: LocationSchema })
|
||||
location?: Location;
|
||||
|
||||
@Prop({ type: VehicleSchema })
|
||||
vehicle?: Vehicle;
|
||||
|
||||
@Prop({ type: InsuranceSchema })
|
||||
insurance?: Insurance;
|
||||
|
||||
@Prop({ type: PartyStatementSchema })
|
||||
statement?: PartyStatement;
|
||||
|
||||
@Prop({ type: EvidenceBundleSchema })
|
||||
evidence?: EvidenceBundle;
|
||||
|
||||
@Prop({ type: PartyConfirmationSchema })
|
||||
confirmation?: PartyConfirmation;
|
||||
@Prop({ type: InquiryParticipantRolesSchema })
|
||||
participantRoles?: InquiryParticipantRoles;
|
||||
|
||||
/**
|
||||
* CAR_BODY only: first form – accident with car vs object.
|
||||
* Second form (guilty/damaged) may be added later as carBodySecondForm.
|
||||
*/
|
||||
@Prop({ type: MongooseSchema.Types.Mixed })
|
||||
carBodyFirstForm?: { car?: boolean; object?: boolean };
|
||||
|
||||
/**
|
||||
* Party-submitted location (step-driven: FIRST_LOCATION / SECOND_LOCATION).
|
||||
*/
|
||||
@Prop({ type: LocationSchema })
|
||||
location?: Location;
|
||||
|
||||
@Prop({ type: VehicleSchema })
|
||||
vehicle?: Vehicle;
|
||||
|
||||
@Prop({ type: InsuranceSchema })
|
||||
insurance?: Insurance;
|
||||
|
||||
@Prop({ type: PartyStatementSchema })
|
||||
statement?: PartyStatement;
|
||||
|
||||
@Prop({ type: EvidenceBundleSchema })
|
||||
evidence?: EvidenceBundle;
|
||||
|
||||
@Prop({ type: PartyConfirmationSchema })
|
||||
confirmation?: PartyConfirmation;
|
||||
}
|
||||
export const PartySchema = SchemaFactory.createForClass(Party);
|
||||
|
||||
264
src/request-management/inquiry-participant-resolver.spec.ts
Normal file
264
src/request-management/inquiry-participant-resolver.spec.ts
Normal file
@@ -0,0 +1,264 @@
|
||||
import { BadRequestException } from "@nestjs/common";
|
||||
import { BlameRequestType } from "src/Types&Enums/blame-request-management/blameRequestType.enum";
|
||||
import {
|
||||
InquiryParticipantRole,
|
||||
VehicleRegistrationState,
|
||||
} from "./dto/inquiry-participants.dto";
|
||||
import {
|
||||
assertPreviousPlateInquiryMatchesVin,
|
||||
normalizeInquirySubmission,
|
||||
participantForRole,
|
||||
resolveInquiryParticipants,
|
||||
resolveInquiryVehicle,
|
||||
runPlateInquiryWithFallback,
|
||||
vehiclePlateCandidates,
|
||||
} from "./inquiry-participant-resolver";
|
||||
|
||||
describe("inquiry participant resolver", () => {
|
||||
it("links several roles to one explicitly shared person", () => {
|
||||
const resolved = resolveInquiryParticipants(BlameRequestType.THIRD_PARTY, {
|
||||
driver: {
|
||||
nationalCode: "0012345678",
|
||||
birthday: "1370/01/01",
|
||||
hasDrivingLicense: true,
|
||||
licenseNumber: "123456789",
|
||||
licenseType: "1",
|
||||
},
|
||||
vehicleOwner: { sameAs: InquiryParticipantRole.DRIVER },
|
||||
thirdPartyPolicyholder: {
|
||||
sameAs: InquiryParticipantRole.VEHICLE_OWNER,
|
||||
},
|
||||
});
|
||||
|
||||
expect(resolved.participants).toHaveLength(1);
|
||||
expect(resolved.roles).toEqual({
|
||||
driver: "DRIVER",
|
||||
vehicleOwner: "DRIVER",
|
||||
thirdPartyPolicyholder: "DRIVER",
|
||||
});
|
||||
});
|
||||
|
||||
it("requires both previous plate and VIN for a recent transfer", () => {
|
||||
expect(() =>
|
||||
resolveInquiryVehicle({
|
||||
registrationState: VehicleRegistrationState.RECENTLY_TRANSFERRED,
|
||||
currentPlate: {
|
||||
leftDigits: "44",
|
||||
centerAlphabet: "ب",
|
||||
centerDigits: "111",
|
||||
ir: "22",
|
||||
},
|
||||
}),
|
||||
).toThrow(BadRequestException);
|
||||
});
|
||||
|
||||
it("keeps legacy driver/policyholder payloads working during rollout", () => {
|
||||
const resolved = resolveInquiryParticipants(BlameRequestType.THIRD_PARTY, {
|
||||
nationalCodeOfDriver: "0012345678",
|
||||
driverBirthday: "1370/01/01",
|
||||
driverLicense: "123456789",
|
||||
nationalCodeOfInsurer: "0098765432",
|
||||
insurerBirthday: "1360/02/02",
|
||||
driverIsInsurer: false,
|
||||
} as any);
|
||||
|
||||
expect(resolved.legacy).toBe(true);
|
||||
expect(resolved.roles.driver).toBe("DRIVER");
|
||||
expect(resolved.roles.thirdPartyPolicyholder).toBe(
|
||||
"THIRD_PARTY_POLICYHOLDER",
|
||||
);
|
||||
expect(resolved.roles.vehicleOwner).toBeUndefined();
|
||||
});
|
||||
|
||||
it("keeps third-party and car-body policyholders distinct", () => {
|
||||
const resolved = resolveInquiryParticipants(BlameRequestType.CAR_BODY, {
|
||||
driver: {
|
||||
nationalCode: "0012345678",
|
||||
birthday: "1370/01/01",
|
||||
hasDrivingLicense: false,
|
||||
},
|
||||
vehicleOwner: { sameAs: InquiryParticipantRole.DRIVER },
|
||||
thirdPartyPolicyholder: {
|
||||
nationalCode: "0023456789",
|
||||
birthday: "1360/02/02",
|
||||
},
|
||||
carBodyPolicyholder: {
|
||||
nationalCode: "0034567890",
|
||||
birthday: "1350/03/03",
|
||||
},
|
||||
});
|
||||
|
||||
expect(
|
||||
participantForRole(
|
||||
resolved,
|
||||
InquiryParticipantRole.THIRD_PARTY_POLICYHOLDER,
|
||||
)?.nationalCode,
|
||||
).toBe("0023456789");
|
||||
expect(
|
||||
participantForRole(resolved, InquiryParticipantRole.CAR_BODY_POLICYHOLDER)
|
||||
?.nationalCode,
|
||||
).toBe("0034567890");
|
||||
});
|
||||
|
||||
it("projects role-complete input onto the existing inquiry fields", () => {
|
||||
const normalized = normalizeInquirySubmission(BlameRequestType.CAR_BODY, {
|
||||
plate: {
|
||||
leftDigits: "44",
|
||||
centerAlphabet: "ب",
|
||||
centerDigits: "111",
|
||||
ir: "22",
|
||||
},
|
||||
driver: {
|
||||
nationalCode: "0012345678",
|
||||
birthday: "1370/01/01",
|
||||
hasDrivingLicense: true,
|
||||
licenseNumber: "D-1",
|
||||
licenseType: "1",
|
||||
},
|
||||
vehicleOwner: { sameAs: InquiryParticipantRole.DRIVER },
|
||||
thirdPartyPolicyholder: {
|
||||
nationalCode: "0023456789",
|
||||
birthday: "1360/02/02",
|
||||
},
|
||||
carBodyPolicyholder: {
|
||||
nationalCode: "0034567890",
|
||||
birthday: "1350/03/03",
|
||||
},
|
||||
});
|
||||
|
||||
expect(normalized.dto.nationalCodeOfInsurer).toBe("0023456789");
|
||||
expect(normalized.dto.nationalCodeOfDriver).toBe("0012345678");
|
||||
expect(normalized.carBodyPolicyholder?.nationalCode).toBe("0034567890");
|
||||
expect(normalized.vehicleOwner?.nationalCode).toBe("0012345678");
|
||||
});
|
||||
|
||||
it("orders the current plate before the previous-plate fallback", () => {
|
||||
const currentPlate = {
|
||||
leftDigits: "44",
|
||||
centerAlphabet: "ب",
|
||||
centerDigits: "111",
|
||||
ir: "22",
|
||||
};
|
||||
const previousPlate = {
|
||||
leftDigits: "55",
|
||||
centerAlphabet: "ج",
|
||||
centerDigits: "222",
|
||||
ir: "33",
|
||||
};
|
||||
|
||||
expect(
|
||||
vehiclePlateCandidates({
|
||||
registrationState: VehicleRegistrationState.RECENTLY_TRANSFERRED,
|
||||
currentPlate,
|
||||
previousPlate,
|
||||
vin: "NAAM01E15HK123456",
|
||||
}),
|
||||
).toEqual([
|
||||
{ kind: "CURRENT", plate: currentPlate },
|
||||
{ kind: "PREVIOUS", plate: previousPlate },
|
||||
]);
|
||||
});
|
||||
|
||||
it("rejects a previous-plate result for another chassis", () => {
|
||||
expect(() =>
|
||||
assertPreviousPlateInquiryMatchesVin("NAAM01E15HK123456", {
|
||||
VinNumberField: "DIFFERENTVIN00001",
|
||||
}),
|
||||
).toThrow(BadRequestException);
|
||||
});
|
||||
|
||||
it("requires the driver's licence status in the new contract", () => {
|
||||
expect(() =>
|
||||
resolveInquiryParticipants(BlameRequestType.THIRD_PARTY, {
|
||||
driver: { nationalCode: "0012345678", birthday: "1370/01/01" },
|
||||
vehicleOwner: { sameAs: InquiryParticipantRole.DRIVER },
|
||||
thirdPartyPolicyholder: {
|
||||
sameAs: InquiryParticipantRole.VEHICLE_OWNER,
|
||||
},
|
||||
}),
|
||||
).toThrow(BadRequestException);
|
||||
});
|
||||
|
||||
it("falls back to the previous plate and accepts only a matching VIN", async () => {
|
||||
const currentPlate = {
|
||||
leftDigits: "44",
|
||||
centerAlphabet: "ب",
|
||||
centerDigits: "111",
|
||||
ir: "22",
|
||||
};
|
||||
const previousPlate = {
|
||||
leftDigits: "55",
|
||||
centerAlphabet: "ج",
|
||||
centerDigits: "222",
|
||||
ir: "33",
|
||||
};
|
||||
const query = jest
|
||||
.fn()
|
||||
.mockRejectedValueOnce(new Error("not found"))
|
||||
.mockResolvedValueOnce({
|
||||
mapped: { VinNumberField: "NAAM01E15HK123456", CompanyName: "پارسیان" },
|
||||
});
|
||||
|
||||
const result = await runPlateInquiryWithFallback<{
|
||||
mapped: { VinNumberField?: string; CompanyName?: string };
|
||||
}>({
|
||||
vehicle: {
|
||||
registrationState: VehicleRegistrationState.RECENTLY_TRANSFERRED,
|
||||
currentPlate,
|
||||
previousPlate,
|
||||
vin: "NAAM01E15HK123456",
|
||||
},
|
||||
fallbackCurrentPlate: currentPlate,
|
||||
query,
|
||||
isUsable: (value) => !!value.mapped.CompanyName,
|
||||
mappedValue: (value) => value.mapped,
|
||||
});
|
||||
|
||||
expect(query).toHaveBeenCalledTimes(2);
|
||||
expect(result.plateKind).toBe("PREVIOUS");
|
||||
expect(result.attempts).toEqual([
|
||||
{ plateKind: "CURRENT", succeeded: false, error: "not found" },
|
||||
{ plateKind: "PREVIOUS", succeeded: true, usable: true },
|
||||
]);
|
||||
});
|
||||
|
||||
it("rejects a car-body policyholder on a third-party case", () => {
|
||||
expect(() =>
|
||||
resolveInquiryParticipants(BlameRequestType.THIRD_PARTY, {
|
||||
driver: {
|
||||
nationalCode: "0012345678",
|
||||
birthday: "1370/01/01",
|
||||
hasDrivingLicense: false,
|
||||
},
|
||||
vehicleOwner: { sameAs: InquiryParticipantRole.DRIVER },
|
||||
thirdPartyPolicyholder: {
|
||||
sameAs: InquiryParticipantRole.VEHICLE_OWNER,
|
||||
},
|
||||
carBodyPolicyholder: {
|
||||
nationalCode: "0034567890",
|
||||
birthday: "1350/03/03",
|
||||
},
|
||||
}),
|
||||
).toThrow(BadRequestException);
|
||||
});
|
||||
|
||||
it("does not accept a previous plate unless recent transfer is declared", () => {
|
||||
expect(() =>
|
||||
resolveInquiryVehicle({
|
||||
registrationState: VehicleRegistrationState.CURRENT,
|
||||
currentPlate: {
|
||||
leftDigits: "44",
|
||||
centerAlphabet: "ب",
|
||||
centerDigits: "111",
|
||||
ir: "22",
|
||||
},
|
||||
previousPlate: {
|
||||
leftDigits: "55",
|
||||
centerAlphabet: "ج",
|
||||
centerDigits: "222",
|
||||
ir: "33",
|
||||
},
|
||||
}),
|
||||
).toThrow(BadRequestException);
|
||||
});
|
||||
});
|
||||
450
src/request-management/inquiry-participant-resolver.ts
Normal file
450
src/request-management/inquiry-participant-resolver.ts
Normal file
@@ -0,0 +1,450 @@
|
||||
import { BadRequestException } from "@nestjs/common";
|
||||
import { BlameRequestType } from "src/Types&Enums/blame-request-management/blameRequestType.enum";
|
||||
import {
|
||||
InquiryParticipantFieldsDto,
|
||||
InquiryParticipantInputDto,
|
||||
InquiryParticipantRole,
|
||||
InquiryVehicleInputDto,
|
||||
VehicleRegistrationState,
|
||||
} from "./dto/inquiry-participants.dto";
|
||||
|
||||
export interface ResolvedInquiryParticipant {
|
||||
participantId: string;
|
||||
nationalCode: string;
|
||||
birthday: string;
|
||||
fullName?: string;
|
||||
phoneNumber?: string;
|
||||
hasDrivingLicense?: boolean;
|
||||
licenseNumber?: string;
|
||||
licenseType?: string;
|
||||
}
|
||||
|
||||
export interface InquiryParticipantRoleAssignments {
|
||||
driver: string;
|
||||
vehicleOwner?: string;
|
||||
thirdPartyPolicyholder: string;
|
||||
carBodyPolicyholder?: string;
|
||||
}
|
||||
|
||||
export interface ResolvedInquiryParticipants {
|
||||
participants: ResolvedInquiryParticipant[];
|
||||
roles: InquiryParticipantRoleAssignments;
|
||||
legacy: boolean;
|
||||
}
|
||||
|
||||
export interface NormalizedInquirySubmission<T extends Record<string, any>> {
|
||||
dto: T & {
|
||||
nationalCodeOfDriver: string;
|
||||
driverBirthday: string;
|
||||
driverLicense?: string;
|
||||
licenseType?: string;
|
||||
userNoCertificate?: boolean;
|
||||
nationalCodeOfInsurer: string;
|
||||
insurerBirthday: string;
|
||||
driverIsInsurer: boolean;
|
||||
insurerLicense?: string;
|
||||
};
|
||||
participants: ResolvedInquiryParticipants;
|
||||
driver: ResolvedInquiryParticipant;
|
||||
vehicleOwner?: ResolvedInquiryParticipant;
|
||||
thirdPartyPolicyholder: ResolvedInquiryParticipant;
|
||||
carBodyPolicyholder?: ResolvedInquiryParticipant;
|
||||
vehicle?: InquiryVehicleInputDto;
|
||||
}
|
||||
|
||||
export function participantForRole(
|
||||
resolved: ResolvedInquiryParticipants,
|
||||
role: InquiryParticipantRole,
|
||||
): ResolvedInquiryParticipant | undefined {
|
||||
const roleField = ROLE_FIELDS[
|
||||
role
|
||||
] as keyof InquiryParticipantRoleAssignments;
|
||||
const participantId = resolved.roles[roleField];
|
||||
return resolved.participants.find(
|
||||
(participant) => participant.participantId === participantId,
|
||||
);
|
||||
}
|
||||
|
||||
const ROLE_FIELDS: Record<
|
||||
InquiryParticipantRole,
|
||||
keyof InquiryParticipantFieldsDto
|
||||
> = {
|
||||
[InquiryParticipantRole.DRIVER]: "driver",
|
||||
[InquiryParticipantRole.VEHICLE_OWNER]: "vehicleOwner",
|
||||
[InquiryParticipantRole.THIRD_PARTY_POLICYHOLDER]: "thirdPartyPolicyholder",
|
||||
[InquiryParticipantRole.CAR_BODY_POLICYHOLDER]: "carBodyPolicyholder",
|
||||
};
|
||||
|
||||
function requiredIdentity(
|
||||
role: InquiryParticipantRole,
|
||||
input: InquiryParticipantInputDto,
|
||||
): ResolvedInquiryParticipant {
|
||||
const nationalCode = String(input.nationalCode ?? "").trim();
|
||||
const birthday = String(input.birthday ?? "").trim();
|
||||
if (!nationalCode || !birthday) {
|
||||
throw new BadRequestException(
|
||||
`${role} requires nationalCode and birthday.`,
|
||||
);
|
||||
}
|
||||
if (
|
||||
role === InquiryParticipantRole.DRIVER &&
|
||||
typeof input.hasDrivingLicense !== "boolean"
|
||||
) {
|
||||
throw new BadRequestException("DRIVER requires hasDrivingLicense.");
|
||||
}
|
||||
if (
|
||||
role === InquiryParticipantRole.DRIVER &&
|
||||
input.hasDrivingLicense === true &&
|
||||
(!String(input.licenseNumber ?? "").trim() ||
|
||||
!String(input.licenseType ?? "").trim())
|
||||
) {
|
||||
throw new BadRequestException(
|
||||
"DRIVER requires licenseNumber and licenseType when hasDrivingLicense is true.",
|
||||
);
|
||||
}
|
||||
return {
|
||||
participantId: role,
|
||||
nationalCode,
|
||||
birthday,
|
||||
...(input.fullName ? { fullName: input.fullName } : {}),
|
||||
...(input.phoneNumber ? { phoneNumber: input.phoneNumber } : {}),
|
||||
...(input.hasDrivingLicense != null
|
||||
? { hasDrivingLicense: input.hasDrivingLicense }
|
||||
: {}),
|
||||
...(input.licenseNumber ? { licenseNumber: input.licenseNumber } : {}),
|
||||
...(input.licenseType ? { licenseType: input.licenseType } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveInquiryParticipants(
|
||||
caseType: BlameRequestType,
|
||||
input: InquiryParticipantFieldsDto & Record<string, any>,
|
||||
): ResolvedInquiryParticipants {
|
||||
const hasRoleCompleteInput = Object.values(ROLE_FIELDS).some(
|
||||
(field) => input[field] != null,
|
||||
);
|
||||
if (!hasRoleCompleteInput) {
|
||||
const driverId = InquiryParticipantRole.DRIVER;
|
||||
const policyholderId = input.driverIsInsurer
|
||||
? driverId
|
||||
: InquiryParticipantRole.THIRD_PARTY_POLICYHOLDER;
|
||||
const participants: ResolvedInquiryParticipant[] = [
|
||||
{
|
||||
participantId: driverId,
|
||||
nationalCode: String(input.nationalCodeOfDriver ?? ""),
|
||||
birthday: String(
|
||||
input.driverBirthday ??
|
||||
(input.driverIsInsurer ? input.insurerBirthday : "") ??
|
||||
"",
|
||||
),
|
||||
...(input.driverLicense
|
||||
? { licenseNumber: String(input.driverLicense) }
|
||||
: {}),
|
||||
...(input.licenseType
|
||||
? { licenseType: String(input.licenseType) }
|
||||
: {}),
|
||||
...(input.userNoCertificate != null
|
||||
? { hasDrivingLicense: !input.userNoCertificate }
|
||||
: {}),
|
||||
},
|
||||
];
|
||||
if (policyholderId !== driverId) {
|
||||
participants.push({
|
||||
participantId: policyholderId,
|
||||
nationalCode: String(input.nationalCodeOfInsurer ?? ""),
|
||||
birthday: String(input.insurerBirthday ?? ""),
|
||||
});
|
||||
}
|
||||
return {
|
||||
participants,
|
||||
roles: {
|
||||
driver: driverId,
|
||||
thirdPartyPolicyholder: policyholderId,
|
||||
},
|
||||
legacy: true,
|
||||
};
|
||||
}
|
||||
if (
|
||||
caseType === BlameRequestType.THIRD_PARTY &&
|
||||
input.carBodyPolicyholder != null
|
||||
) {
|
||||
throw new BadRequestException(
|
||||
"CAR_BODY_POLICYHOLDER is not allowed for a THIRD_PARTY case.",
|
||||
);
|
||||
}
|
||||
|
||||
const requiredRoles = [
|
||||
InquiryParticipantRole.DRIVER,
|
||||
InquiryParticipantRole.VEHICLE_OWNER,
|
||||
InquiryParticipantRole.THIRD_PARTY_POLICYHOLDER,
|
||||
...(caseType === BlameRequestType.CAR_BODY
|
||||
? [InquiryParticipantRole.CAR_BODY_POLICYHOLDER]
|
||||
: []),
|
||||
];
|
||||
const participants = new Map<string, ResolvedInquiryParticipant>();
|
||||
const resolvedRoleIds = new Map<InquiryParticipantRole, string>();
|
||||
const resolving = new Set<InquiryParticipantRole>();
|
||||
|
||||
const resolveRole = (role: InquiryParticipantRole): string => {
|
||||
const existing = resolvedRoleIds.get(role);
|
||||
if (existing) return existing;
|
||||
if (resolving.has(role)) {
|
||||
throw new BadRequestException(
|
||||
"Participant sameAs references cannot be circular.",
|
||||
);
|
||||
}
|
||||
const value = input[ROLE_FIELDS[role]] as
|
||||
| InquiryParticipantInputDto
|
||||
| undefined;
|
||||
if (!value) throw new BadRequestException(`${role} is required.`);
|
||||
|
||||
resolving.add(role);
|
||||
let participantId: string;
|
||||
if (value.sameAs) {
|
||||
if (value.nationalCode != null || value.birthday != null) {
|
||||
throw new BadRequestException(
|
||||
`${role} must contain either sameAs or identity fields, not both.`,
|
||||
);
|
||||
}
|
||||
participantId = resolveRole(value.sameAs);
|
||||
} else {
|
||||
const participant = requiredIdentity(role, value);
|
||||
const duplicate = [...participants.values()].find(
|
||||
(item) => item.nationalCode === participant.nationalCode,
|
||||
);
|
||||
if (duplicate) {
|
||||
throw new BadRequestException(
|
||||
`${role} duplicates an existing nationalCode; use sameAs instead.`,
|
||||
);
|
||||
}
|
||||
participants.set(participant.participantId, participant);
|
||||
participantId = participant.participantId;
|
||||
}
|
||||
resolving.delete(role);
|
||||
resolvedRoleIds.set(role, participantId);
|
||||
return participantId;
|
||||
};
|
||||
|
||||
for (const role of requiredRoles) resolveRole(role);
|
||||
|
||||
return {
|
||||
participants: [...participants.values()],
|
||||
roles: {
|
||||
driver: resolvedRoleIds.get(InquiryParticipantRole.DRIVER)!,
|
||||
vehicleOwner: resolvedRoleIds.get(InquiryParticipantRole.VEHICLE_OWNER)!,
|
||||
thirdPartyPolicyholder: resolvedRoleIds.get(
|
||||
InquiryParticipantRole.THIRD_PARTY_POLICYHOLDER,
|
||||
)!,
|
||||
...(caseType === BlameRequestType.CAR_BODY
|
||||
? {
|
||||
carBodyPolicyholder: resolvedRoleIds.get(
|
||||
InquiryParticipantRole.CAR_BODY_POLICYHOLDER,
|
||||
)!,
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
legacy: false,
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveInquiryVehicle(
|
||||
input: InquiryVehicleInputDto,
|
||||
): InquiryVehicleInputDto {
|
||||
const registrationState =
|
||||
input.registrationState ?? VehicleRegistrationState.CURRENT;
|
||||
if (!input.currentPlate) {
|
||||
throw new BadRequestException("vehicle.currentPlate is required.");
|
||||
}
|
||||
if (
|
||||
registrationState === VehicleRegistrationState.RECENTLY_TRANSFERRED &&
|
||||
(!input.previousPlate || !String(input.vin ?? "").trim())
|
||||
) {
|
||||
throw new BadRequestException(
|
||||
"RECENTLY_TRANSFERRED requires previousPlate and vin.",
|
||||
);
|
||||
}
|
||||
if (
|
||||
registrationState !== VehicleRegistrationState.RECENTLY_TRANSFERRED &&
|
||||
input.previousPlate
|
||||
) {
|
||||
throw new BadRequestException(
|
||||
"previousPlate is only allowed for RECENTLY_TRANSFERRED vehicles.",
|
||||
);
|
||||
}
|
||||
return { ...input, registrationState };
|
||||
}
|
||||
|
||||
export function vehiclePlateCandidates(input?: InquiryVehicleInputDto): Array<{
|
||||
kind: "CURRENT" | "PREVIOUS";
|
||||
plate: InquiryVehicleInputDto["currentPlate"];
|
||||
}> {
|
||||
if (!input) return [];
|
||||
return [
|
||||
{ kind: "CURRENT" as const, plate: input.currentPlate },
|
||||
...(input.registrationState ===
|
||||
VehicleRegistrationState.RECENTLY_TRANSFERRED && input.previousPlate
|
||||
? [{ kind: "PREVIOUS" as const, plate: input.previousPlate }]
|
||||
: []),
|
||||
];
|
||||
}
|
||||
|
||||
function normalizeVehicleSerial(value: unknown): string {
|
||||
return String(value ?? "")
|
||||
.toUpperCase()
|
||||
.replace(/[^A-Z0-9]/g, "");
|
||||
}
|
||||
|
||||
export function assertPreviousPlateInquiryMatchesVin(
|
||||
expectedVin: string,
|
||||
mapped: Record<string, any>,
|
||||
): void {
|
||||
const expected = normalizeVehicleSerial(expectedVin);
|
||||
const candidates = [
|
||||
mapped?.VinNumberField,
|
||||
mapped?.vin,
|
||||
mapped?.VIN,
|
||||
mapped?.ChassisNumberField,
|
||||
mapped?.chassisNumber,
|
||||
mapped?.ChassisNo,
|
||||
mapped?.vehicle?.VIN,
|
||||
mapped?.vehicle?.ChassisNo,
|
||||
]
|
||||
.map(normalizeVehicleSerial)
|
||||
.filter(Boolean);
|
||||
if (!expected || !candidates.includes(expected)) {
|
||||
throw new BadRequestException(
|
||||
"Previous-plate inquiry does not match the submitted VIN/chassis; manual review is required.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function runPlateInquiryWithFallback<T>(options: {
|
||||
vehicle?: InquiryVehicleInputDto;
|
||||
fallbackCurrentPlate: InquiryVehicleInputDto["currentPlate"];
|
||||
query: (plate: InquiryVehicleInputDto["currentPlate"]) => Promise<T>;
|
||||
isUsable: (value: T) => boolean;
|
||||
mappedValue: (value: T) => Record<string, any>;
|
||||
}): Promise<{
|
||||
value: T;
|
||||
plateKind: "CURRENT" | "PREVIOUS";
|
||||
attempts: Array<{
|
||||
plateKind: "CURRENT" | "PREVIOUS";
|
||||
succeeded: boolean;
|
||||
usable?: boolean;
|
||||
error?: string;
|
||||
}>;
|
||||
}> {
|
||||
const candidates = options.vehicle
|
||||
? vehiclePlateCandidates(options.vehicle)
|
||||
: [{ kind: "CURRENT" as const, plate: options.fallbackCurrentPlate }];
|
||||
let lastError: unknown;
|
||||
const attempts: Array<{
|
||||
plateKind: "CURRENT" | "PREVIOUS";
|
||||
succeeded: boolean;
|
||||
usable?: boolean;
|
||||
error?: string;
|
||||
}> = [];
|
||||
|
||||
for (let index = 0; index < candidates.length; index += 1) {
|
||||
const candidate = candidates[index];
|
||||
const isLast = index === candidates.length - 1;
|
||||
try {
|
||||
const value = await options.query(candidate.plate);
|
||||
const usable = options.isUsable(value);
|
||||
if (!usable) {
|
||||
attempts.push({
|
||||
plateKind: candidate.kind,
|
||||
succeeded: true,
|
||||
usable: false,
|
||||
});
|
||||
if (!isLast) continue;
|
||||
return { value, plateKind: candidate.kind, attempts };
|
||||
}
|
||||
if (candidate.kind === "PREVIOUS" && usable) {
|
||||
assertPreviousPlateInquiryMatchesVin(
|
||||
options.vehicle!.vin!,
|
||||
options.mappedValue(value),
|
||||
);
|
||||
}
|
||||
attempts.push({
|
||||
plateKind: candidate.kind,
|
||||
succeeded: true,
|
||||
usable: true,
|
||||
});
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError ?? new BadRequestException("Inquiry failed for all plates.");
|
||||
}
|
||||
|
||||
export function normalizeInquirySubmission<T extends Record<string, any>>(
|
||||
caseType: BlameRequestType,
|
||||
input: T,
|
||||
): NormalizedInquirySubmission<T> {
|
||||
const participants = resolveInquiryParticipants(caseType, input);
|
||||
const driver = participantForRole(
|
||||
participants,
|
||||
InquiryParticipantRole.DRIVER,
|
||||
);
|
||||
const thirdPartyPolicyholder = participantForRole(
|
||||
participants,
|
||||
InquiryParticipantRole.THIRD_PARTY_POLICYHOLDER,
|
||||
);
|
||||
if (!driver || !thirdPartyPolicyholder) {
|
||||
throw new BadRequestException(
|
||||
"Driver and third-party policyholder identities are required.",
|
||||
);
|
||||
}
|
||||
const vehicleOwner = participantForRole(
|
||||
participants,
|
||||
InquiryParticipantRole.VEHICLE_OWNER,
|
||||
);
|
||||
const carBodyPolicyholder = participantForRole(
|
||||
participants,
|
||||
InquiryParticipantRole.CAR_BODY_POLICYHOLDER,
|
||||
);
|
||||
const vehicle = input.vehicle
|
||||
? resolveInquiryVehicle(input.vehicle as InquiryVehicleInputDto)
|
||||
: undefined;
|
||||
const sameDriverAndPolicyholder =
|
||||
participants.roles.driver === participants.roles.thirdPartyPolicyholder;
|
||||
|
||||
return {
|
||||
dto: {
|
||||
...input,
|
||||
...(vehicle?.currentPlate && !input.plate
|
||||
? { plate: vehicle.currentPlate }
|
||||
: {}),
|
||||
...(vehicle?.vin && !input.vin ? { vin: vehicle.vin } : {}),
|
||||
nationalCodeOfDriver: driver.nationalCode,
|
||||
driverBirthday: driver.birthday,
|
||||
driverLicense: driver.licenseNumber,
|
||||
licenseType: driver.licenseType,
|
||||
userNoCertificate:
|
||||
driver.hasDrivingLicense == null
|
||||
? input.userNoCertificate
|
||||
: !driver.hasDrivingLicense,
|
||||
nationalCodeOfInsurer: thirdPartyPolicyholder.nationalCode,
|
||||
insurerBirthday: thirdPartyPolicyholder.birthday,
|
||||
driverIsInsurer: sameDriverAndPolicyholder,
|
||||
insurerLicense: sameDriverAndPolicyholder
|
||||
? driver.licenseNumber
|
||||
: input.insurerLicense,
|
||||
} as NormalizedInquirySubmission<T>["dto"],
|
||||
participants,
|
||||
driver,
|
||||
vehicleOwner,
|
||||
thirdPartyPolicyholder,
|
||||
carBodyPolicyholder,
|
||||
vehicle,
|
||||
};
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user