Implement role-complete inquiry participants

This commit is contained in:
SepehrYahyaee
2026-09-13 10:59:00 +03:30
parent 401ad6a143
commit c64f23091a
12 changed files with 2179 additions and 690 deletions

View File

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

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

View File

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

View File

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

View File

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

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

View 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