forked from Yara724/api
Implement role-complete inquiry participants
This commit is contained in:
@@ -1,9 +1,15 @@
|
|||||||
# پیشنهاد مدل اشخاص در مرحله استعلام
|
# پیشنهاد مدل اشخاص در مرحله استعلام
|
||||||
|
|
||||||
وضعیت: پیشنهادی
|
وضعیت: پیادهسازیشده در ۱۴۰۵/۰۶/۲۲
|
||||||
دامنه: تمام جریانهای استعلام کاربر، کارشناس، پروندهساز و مرکز تماس
|
دامنه: تمام جریانهای استعلام کاربر، کارشناس، پروندهساز و مرکز تماس
|
||||||
نسخه انگلیسی: [inquiry-participants-proposal.md](./inquiry-participants-proposal.md)
|
نسخه انگلیسی: [inquiry-participants-proposal.md](./inquiry-participants-proposal.md)
|
||||||
|
|
||||||
|
## interface پیادهسازیشده
|
||||||
|
|
||||||
|
فیلدهای نقشها و آبجکت اختیاری `vehicle` که در ادامه آمدهاند، مستقیماً در body تمام درخواستهای استعلام فعلی پذیرفته میشوند. این تغییر شامل فرم اولیه کاربر و mirror کارشناس/ثبتکننده در V2، جریان کارشناس V3، جریانهای پروندهساز V4/V5، مرکز تماس V6 و مسیرهای تکدرخواستی حضوری است. routeهای پلاک و VIN از قوانین مشترک اشخاص استفاده میکنند.
|
||||||
|
|
||||||
|
در دوره مهاجرت، فیلدهای قدیمی و تخت همچنان پذیرفته میشوند. پاسخها و جزئیات پرونده برای نقشهای عملیاتی، در صورت وجود داده، فیلدهای نرمالشده `participants`، `participantRoles`، `vehicle.registrationState` و `vehicle.previousPlateId` را نمایش میدهند.
|
||||||
|
|
||||||
## مسئله
|
## مسئله
|
||||||
|
|
||||||
قرارداد فعلی استعلام عمدتاً فقط راننده و شخصی با عنوان `insurer` را نگه میدارد. این مدل کامل نیست و نامگذاری نیز دقیق نیست: شخص، **بیمهگذار** است و **بیمهگر** شرکت بیمه است.
|
قرارداد فعلی استعلام عمدتاً فقط راننده و شخصی با عنوان `insurer` را نگه میدارد. این مدل کامل نیست و نامگذاری نیز دقیق نیست: شخص، **بیمهگذار** است و **بیمهگر** شرکت بیمه است.
|
||||||
@@ -11,7 +17,7 @@
|
|||||||
برای هر وسیله نقلیه در یک `Party` نقشهای هویتی زیر وجود دارد:
|
برای هر وسیله نقلیه در یک `Party` نقشهای هویتی زیر وجود دارد:
|
||||||
|
|
||||||
| نوع پرونده | نقشهای الزامی |
|
| نوع پرونده | نقشهای الزامی |
|
||||||
| --- | --- |
|
| ------------- | ------------------------------------------------------------ |
|
||||||
| `THIRD_PARTY` | راننده، مالک وسیله نقلیه، بیمهگذار شخص ثالث |
|
| `THIRD_PARTY` | راننده، مالک وسیله نقلیه، بیمهگذار شخص ثالث |
|
||||||
| `CAR_BODY` | راننده، مالک وسیله نقلیه، بیمهگذار شخص ثالث، بیمهگذار بدنه |
|
| `CAR_BODY` | راننده، مالک وسیله نقلیه، بیمهگذار شخص ثالث، بیمهگذار بدنه |
|
||||||
|
|
||||||
@@ -42,6 +48,8 @@
|
|||||||
|
|
||||||
فیلد `carBodyPolicyholder` برای `THIRD_PARTY` مجاز نیست و برای `CAR_BODY` الزامی است. هر نقش باید فقط یکی از دو حالت «اطلاعات شخص» یا `sameAs` را داشته باشد. بکاند این ورودی را به فهرست اشخاص یکتا و اتصال نقشها به آنها تبدیل میکند.
|
فیلد `carBodyPolicyholder` برای `THIRD_PARTY` مجاز نیست و برای `CAR_BODY` الزامی است. هر نقش باید فقط یکی از دو حالت «اطلاعات شخص» یا `sameAs` را داشته باشد. بکاند این ورودی را به فهرست اشخاص یکتا و اتصال نقشها به آنها تبدیل میکند.
|
||||||
|
|
||||||
|
برای راننده، `hasDrivingLicense` الزامی است. اگر مقدار آن `true` باشد، هر دو فیلد `licenseNumber` و `licenseType` نیز الزامی هستند؛ اگر مقدار آن `false` باشد، استعلام گواهینامه عمداً اجرا نمیشود.
|
||||||
|
|
||||||
## انتقال مالکیت اخیر و پلاک قبلی
|
## انتقال مالکیت اخیر و پلاک قبلی
|
||||||
|
|
||||||
نقش اشخاص و شناسههای خودرو دو موضوع جدا هستند. اگر خودرو بهتازگی فروخته یا خریداری شده باشد، ممکن است اطلاعات رسمی یا بیمهنامه هنوز به پلاک قبلی متصل باشد. این وضعیت باید صریح ثبت شود و پلاک قبلی نباید جایگزین پلاک فعلی شود:
|
نقش اشخاص و شناسههای خودرو دو موضوع جدا هستند. اگر خودرو بهتازگی فروخته یا خریداری شده باشد، ممکن است اطلاعات رسمی یا بیمهنامه هنوز به پلاک قبلی متصل باشد. این وضعیت باید صریح ثبت شود و پلاک قبلی نباید جایگزین پلاک فعلی شود:
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
# Inquiry participant identity proposal
|
# Inquiry participant identity proposal
|
||||||
|
|
||||||
Status: proposed
|
Status: implemented on 2026-09-13
|
||||||
Scope: every user, expert, FileMaker, and call-center inquiry flow
|
Scope: every user, expert, FileMaker, and call-center inquiry flow
|
||||||
Persian version: [inquiry-participants-proposal.fa.md](./inquiry-participants-proposal.fa.md)
|
Persian version: [inquiry-participants-proposal.fa.md](./inquiry-participants-proposal.fa.md)
|
||||||
|
|
||||||
|
## Implemented interface
|
||||||
|
|
||||||
|
The role fields and optional `vehicle` object shown below are accepted directly in every existing inquiry request body. This covers V2 user and expert/registrar mirror initial forms, V3 expert flow, V4/V5 FileMaker flows, V6 call-center flow, and the one-shot in-person completion paths. Plate and VIN routes share the same participant rules.
|
||||||
|
|
||||||
|
Existing flat fields remain accepted during migration. Responses and file-detail views for operational actors expose normalized `participants`, `participantRoles`, `vehicle.registrationState`, and `vehicle.previousPlateId` where available.
|
||||||
|
|
||||||
## Problem
|
## Problem
|
||||||
|
|
||||||
The current inquiry contract mainly models a driver and a value named `insurer`. That is incomplete and the name is misleading: a person is the **policyholder**; the **insurer** is the insurance company.
|
The current inquiry contract mainly models a driver and a value named `insurer`. That is incomplete and the name is misleading: a person is the **policyholder**; the **insurer** is the insurance company.
|
||||||
@@ -11,7 +17,7 @@ The current inquiry contract mainly models a driver and a value named `insurer`.
|
|||||||
Each vehicle-side `Party` can have these identity roles:
|
Each vehicle-side `Party` can have these identity roles:
|
||||||
|
|
||||||
| Case type | Required roles |
|
| Case type | Required roles |
|
||||||
| --- | --- |
|
| ------------- | ---------------------------------------------------------------------- |
|
||||||
| `THIRD_PARTY` | Driver, vehicle owner, third-party policyholder |
|
| `THIRD_PARTY` | Driver, vehicle owner, third-party policyholder |
|
||||||
| `CAR_BODY` | Driver, vehicle owner, third-party policyholder, car-body policyholder |
|
| `CAR_BODY` | Driver, vehicle owner, third-party policyholder, car-body policyholder |
|
||||||
|
|
||||||
@@ -42,6 +48,8 @@ Use explicit role references instead:
|
|||||||
|
|
||||||
`carBodyPolicyholder` is forbidden for `THIRD_PARTY` and required for `CAR_BODY`. A role is either a new person's identity or a `sameAs` reference, never both. The backend should normalize this input into unique participants plus role assignments.
|
`carBodyPolicyholder` is forbidden for `THIRD_PARTY` and required for `CAR_BODY`. A role is either a new person's identity or a `sameAs` reference, never both. The backend should normalize this input into unique participants plus role assignments.
|
||||||
|
|
||||||
|
For Driver, `hasDrivingLicense` is required. When it is `true`, both `licenseNumber` and `licenseType` are required; when it is `false`, the licence inquiry is intentionally skipped.
|
||||||
|
|
||||||
## Recent ownership transfer and previous plate
|
## Recent ownership transfer and previous plate
|
||||||
|
|
||||||
Participant roles and vehicle identifiers are separate concerns. When a vehicle has recently been sold or purchased, the current official record or policy may still be connected to its previous plate. Model this explicitly instead of replacing the current plate:
|
Participant roles and vehicle identifiers are separate concerns. When a vehicle has recently been sold or purchased, the current official record or policy may still be connected to its previous plate. Model this explicitly instead of replacing the current plate:
|
||||||
|
|||||||
@@ -30,5 +30,8 @@ export class CaseInquiries {
|
|||||||
|
|
||||||
@Prop({ type: InquiryStatusSchema })
|
@Prop({ type: InquiryStatusSchema })
|
||||||
drivingLicence?: InquiryStatus;
|
drivingLicence?: InquiryStatus;
|
||||||
|
|
||||||
|
@Prop({ type: InquiryStatusSchema })
|
||||||
|
ownership?: InquiryStatus;
|
||||||
}
|
}
|
||||||
export const CaseInquiriesSchema = SchemaFactory.createForClass(CaseInquiries);
|
export const CaseInquiriesSchema = SchemaFactory.createForClass(CaseInquiries);
|
||||||
|
|||||||
@@ -1,36 +1,53 @@
|
|||||||
import { ApiProperty } from "@nestjs/swagger";
|
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||||
import { PlatesDto } from "src/plates/dto/plate.dto";
|
import { PlatesDto } from "src/plates/dto/plate.dto";
|
||||||
import { Plates } from "src/Types&Enums/plate.interface";
|
import { Plates } from "src/Types&Enums/plate.interface";
|
||||||
|
import { InquiryParticipantFieldsDto } from "src/request-management/dto/inquiry-participants.dto";
|
||||||
|
|
||||||
export class AddPlateDto {
|
export class AddPlateDto extends InquiryParticipantFieldsDto {
|
||||||
plateId: string;
|
plateId: string;
|
||||||
@ApiProperty({ type: String, required: true })
|
@ApiPropertyOptional({
|
||||||
|
type: String,
|
||||||
|
description: "Legacy third-party policyholder national code",
|
||||||
|
})
|
||||||
nationalCodeOfInsurer: string;
|
nationalCodeOfInsurer: string;
|
||||||
|
|
||||||
@ApiProperty({ type: String, required: true })
|
@ApiPropertyOptional({
|
||||||
|
type: String,
|
||||||
|
description: "Legacy driver national code",
|
||||||
|
})
|
||||||
nationalCodeOfDriver: string;
|
nationalCodeOfDriver: string;
|
||||||
|
|
||||||
@ApiProperty({ type: String, required: true })
|
@ApiPropertyOptional({ type: String })
|
||||||
insurerLicense: string;
|
insurerLicense: string;
|
||||||
|
|
||||||
@ApiProperty({ type: String, required: true })
|
@ApiPropertyOptional({ type: String })
|
||||||
driverLicense: string;
|
driverLicense: string;
|
||||||
|
|
||||||
@ApiProperty({ type: PlatesDto, required: true })
|
@ApiPropertyOptional({
|
||||||
|
type: PlatesDto,
|
||||||
|
description:
|
||||||
|
"Legacy/current plate; may be supplied as vehicle.currentPlate",
|
||||||
|
})
|
||||||
plate: Plates;
|
plate: Plates;
|
||||||
|
|
||||||
@ApiProperty({ type: Boolean, required: true })
|
@ApiPropertyOptional({
|
||||||
|
type: Boolean,
|
||||||
|
description: "Legacy role relationship",
|
||||||
|
})
|
||||||
driverIsInsurer: boolean;
|
driverIsInsurer: boolean;
|
||||||
|
|
||||||
@ApiProperty({ type: Boolean, required: true, default: false })
|
@ApiProperty({ type: Boolean, required: true, default: false })
|
||||||
isNewCar: boolean;
|
isNewCar: boolean;
|
||||||
|
|
||||||
@ApiProperty({ type: Boolean, required: true })
|
@ApiPropertyOptional({
|
||||||
|
type: Boolean,
|
||||||
|
description: "Legacy; use driver.hasDrivingLicense",
|
||||||
|
})
|
||||||
userNoCertificate: boolean;
|
userNoCertificate: boolean;
|
||||||
|
|
||||||
@ApiProperty({
|
@ApiPropertyOptional({
|
||||||
type: Number,
|
type: Number,
|
||||||
required: true,
|
description: "Legacy third-party policyholder birthday",
|
||||||
})
|
})
|
||||||
insurerBirthday: number;
|
insurerBirthday: number;
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
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 { Types } from "mongoose";
|
||||||
import { AddPlateDto } from "src/profile/dto/user/AddPlateDto";
|
import { AddPlateDto } from "src/profile/dto/user/AddPlateDto";
|
||||||
import { StepsEnum } from "src/Types&Enums/blame-request-management/steps.enum";
|
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";
|
} from "src/Types&Enums/blame-request-management/accident-conditions.enum";
|
||||||
import { BlameRequestType } from "src/Types&Enums/blame-request-management/blameRequestType.enum";
|
import { BlameRequestType } from "src/Types&Enums/blame-request-management/blameRequestType.enum";
|
||||||
import { IsEnum } from "class-validator";
|
import { IsEnum } from "class-validator";
|
||||||
|
import { InquiryParticipantFieldsDto } from "./inquiry-participants.dto";
|
||||||
|
|
||||||
export class InitialFormDto {
|
export class InitialFormDto {
|
||||||
@ApiProperty({ required: false, default: false })
|
@ApiProperty({ required: false, default: false })
|
||||||
@@ -227,10 +234,10 @@ export class BlameConfessionDtoV2 {
|
|||||||
* All identity and license fields from {@link AddPlateDto} are preserved; only
|
* All identity and license fields from {@link AddPlateDto} are preserved; only
|
||||||
* `plate` is replaced by `vin` (the 17-character chassis / VIN string).
|
* `plate` is replaced by `vin` (the 17-character chassis / VIN string).
|
||||||
*/
|
*/
|
||||||
export class InitialFormVinDto {
|
export class InitialFormVinDto extends InquiryParticipantFieldsDto {
|
||||||
@ApiProperty({
|
@ApiPropertyOptional({
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: false,
|
||||||
description: "17-character VIN / chassis number (شماره شاسی)",
|
description: "17-character VIN / chassis number (شماره شاسی)",
|
||||||
example: "NAAM01E15HK123456",
|
example: "NAAM01E15HK123456",
|
||||||
maxLength: 17,
|
maxLength: 17,
|
||||||
@@ -238,30 +245,49 @@ export class InitialFormVinDto {
|
|||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@MaxLength(17)
|
@MaxLength(17)
|
||||||
|
@ValidateIf((dto) => !dto.vehicle?.vin)
|
||||||
vin: string;
|
vin: string;
|
||||||
|
|
||||||
@ApiProperty({ type: String, required: true })
|
@ApiPropertyOptional({
|
||||||
|
type: String,
|
||||||
|
description: "Legacy third-party policyholder national code",
|
||||||
|
})
|
||||||
|
@ValidateIf((dto) => !dto.thirdPartyPolicyholder)
|
||||||
nationalCodeOfInsurer: string;
|
nationalCodeOfInsurer: string;
|
||||||
|
|
||||||
@ApiProperty({ type: String, required: true })
|
@ApiPropertyOptional({
|
||||||
|
type: String,
|
||||||
|
description: "Legacy driver national code",
|
||||||
|
})
|
||||||
|
@ValidateIf((dto) => !dto.driver)
|
||||||
nationalCodeOfDriver: string;
|
nationalCodeOfDriver: string;
|
||||||
|
|
||||||
@ApiProperty({ type: String, required: true })
|
@ApiPropertyOptional({ type: String })
|
||||||
insurerLicense: string;
|
insurerLicense: string;
|
||||||
|
|
||||||
@ApiProperty({ type: String, required: true })
|
@ApiPropertyOptional({ type: String })
|
||||||
driverLicense: string;
|
driverLicense: string;
|
||||||
|
|
||||||
@ApiProperty({ type: Boolean, required: true })
|
@ApiPropertyOptional({
|
||||||
|
type: Boolean,
|
||||||
|
description: "Legacy role relationship",
|
||||||
|
})
|
||||||
|
@ValidateIf((dto) => !dto.driver)
|
||||||
driverIsInsurer: boolean;
|
driverIsInsurer: boolean;
|
||||||
|
|
||||||
@ApiProperty({ type: Boolean, required: true, default: false })
|
@ApiPropertyOptional({ type: Boolean, default: false })
|
||||||
isNewCar: boolean;
|
isNewCar: boolean;
|
||||||
|
|
||||||
@ApiProperty({ type: Boolean, required: true })
|
@ApiPropertyOptional({
|
||||||
|
type: Boolean,
|
||||||
|
description: "Legacy; use driver.hasDrivingLicense",
|
||||||
|
})
|
||||||
userNoCertificate: boolean;
|
userNoCertificate: boolean;
|
||||||
|
|
||||||
@ApiProperty({ type: Number, required: true })
|
@ApiPropertyOptional({
|
||||||
|
type: Number,
|
||||||
|
description: "Legacy policyholder birthday",
|
||||||
|
})
|
||||||
insurerBirthday: number;
|
insurerBirthday: number;
|
||||||
|
|
||||||
@ApiPropertyOptional({ type: String, required: false })
|
@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,
|
IsOptional,
|
||||||
IsString,
|
IsString,
|
||||||
MaxLength,
|
MaxLength,
|
||||||
|
ValidateIf,
|
||||||
ValidateNested,
|
ValidateNested,
|
||||||
} from "class-validator";
|
} from "class-validator";
|
||||||
import { Type } from "class-transformer";
|
import { Type } from "class-transformer";
|
||||||
|
import { InquiryParticipantFieldsDto } from "./inquiry-participants.dto";
|
||||||
|
|
||||||
class PlateV6Dto {
|
class PlateV6Dto {
|
||||||
@ApiProperty({ example: "44", description: "Left two digits" })
|
@ApiProperty({ example: "44", description: "Left two digits" })
|
||||||
@@ -25,7 +27,10 @@ class PlateV6Dto {
|
|||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
centerDigits: string;
|
centerDigits: string;
|
||||||
|
|
||||||
@ApiProperty({ example: "22", description: "Right two digits (Iran region code)" })
|
@ApiProperty({
|
||||||
|
example: "22",
|
||||||
|
description: "Right two digits (Iran region code)",
|
||||||
|
})
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
ir: string;
|
ir: string;
|
||||||
@@ -35,35 +40,52 @@ class PlateV6Dto {
|
|||||||
* Inquiry body for the V6 call-center flow.
|
* Inquiry body for the V6 call-center flow.
|
||||||
* Same as V3 but without `sheba` — the user adds their own IBAN later via the link.
|
* Same as V3 but without `sheba` — the user adds their own IBAN later via the link.
|
||||||
*/
|
*/
|
||||||
export class RunCallCenterInquiryV6Dto {
|
export class RunCallCenterInquiryV6Dto extends InquiryParticipantFieldsDto {
|
||||||
@ApiProperty({
|
@ApiPropertyOptional({
|
||||||
type: PlateV6Dto,
|
type: PlateV6Dto,
|
||||||
description: "Plate segments — Tejarat block / third-party inquiry.",
|
description: "Plate segments — Tejarat block / third-party inquiry.",
|
||||||
})
|
})
|
||||||
@ValidateNested()
|
@ValidateNested()
|
||||||
@Type(() => PlateV6Dto)
|
@Type(() => PlateV6Dto)
|
||||||
|
@ValidateIf((dto) => !dto.vehicle?.currentPlate)
|
||||||
plate: PlateV6Dto;
|
plate: PlateV6Dto;
|
||||||
|
|
||||||
@ApiProperty({ example: "1234567890", description: "National code of the policyholder (insurer)" })
|
@ApiPropertyOptional({
|
||||||
|
example: "1234567890",
|
||||||
|
description: "Legacy third-party policyholder national code",
|
||||||
|
})
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
|
@ValidateIf((dto) => !dto.thirdPartyPolicyholder)
|
||||||
nationalCodeOfInsurer: string;
|
nationalCodeOfInsurer: string;
|
||||||
|
|
||||||
@ApiProperty({ example: "1234567890", description: "National code of the driver" })
|
@ApiPropertyOptional({
|
||||||
|
example: "1234567890",
|
||||||
|
description: "Legacy driver national code",
|
||||||
|
})
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
|
@ValidateIf((dto) => !dto.driver)
|
||||||
nationalCodeOfDriver: string;
|
nationalCodeOfDriver: string;
|
||||||
|
|
||||||
@ApiProperty({ example: true, description: "Whether the driver is the same person as the insurer" })
|
@ApiPropertyOptional({
|
||||||
|
example: true,
|
||||||
|
description: "Legacy role relationship",
|
||||||
|
})
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
|
@ValidateIf((dto) => !dto.driver)
|
||||||
driverIsInsurer: boolean;
|
driverIsInsurer: boolean;
|
||||||
|
|
||||||
@ApiProperty({ example: 13780624, description: "Insurer birth date (Jalali)" })
|
@ApiPropertyOptional({
|
||||||
|
example: 13780624,
|
||||||
|
description: "Legacy policyholder birth date (Jalali)",
|
||||||
|
})
|
||||||
insurerBirthday: number | string;
|
insurerBirthday: number | string;
|
||||||
|
|
||||||
@ApiPropertyOptional({
|
@ApiPropertyOptional({
|
||||||
example: 13780624,
|
example: 13780624,
|
||||||
description: "Driver birth date (Jalali). Required when driverIsInsurer is false.",
|
description:
|
||||||
|
"Driver birth date (Jalali). Required when driverIsInsurer is false.",
|
||||||
})
|
})
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
driverBirthday?: number | string | null;
|
driverBirthday?: number | string | null;
|
||||||
@@ -90,8 +112,8 @@ export class RunCallCenterInquiryV6Dto {
|
|||||||
* Identical to `RunCallCenterInquiryV6Dto` but replaces `plate` with `vin`.
|
* Identical to `RunCallCenterInquiryV6Dto` but replaces `plate` with `vin`.
|
||||||
* Sheba (IBAN) is intentionally absent — the user provides it themselves via the link.
|
* Sheba (IBAN) is intentionally absent — the user provides it themselves via the link.
|
||||||
*/
|
*/
|
||||||
export class RunCallCenterInquiryVinV6Dto {
|
export class RunCallCenterInquiryVinV6Dto extends InquiryParticipantFieldsDto {
|
||||||
@ApiProperty({
|
@ApiPropertyOptional({
|
||||||
example: "NAAM01E15HK123456",
|
example: "NAAM01E15HK123456",
|
||||||
description: "17-character VIN / chassis number (شماره شاسی)",
|
description: "17-character VIN / chassis number (شماره شاسی)",
|
||||||
maxLength: 17,
|
maxLength: 17,
|
||||||
@@ -99,28 +121,45 @@ export class RunCallCenterInquiryVinV6Dto {
|
|||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@MaxLength(17)
|
@MaxLength(17)
|
||||||
|
@ValidateIf((dto) => !dto.vehicle?.vin)
|
||||||
vin: string;
|
vin: string;
|
||||||
|
|
||||||
@ApiProperty({ example: "1234567890", description: "National code of the policyholder (insurer)" })
|
@ApiPropertyOptional({
|
||||||
|
example: "1234567890",
|
||||||
|
description: "Legacy third-party policyholder national code",
|
||||||
|
})
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
|
@ValidateIf((dto) => !dto.thirdPartyPolicyholder)
|
||||||
nationalCodeOfInsurer: string;
|
nationalCodeOfInsurer: string;
|
||||||
|
|
||||||
@ApiProperty({ example: "1234567890", description: "National code of the driver" })
|
@ApiPropertyOptional({
|
||||||
|
example: "1234567890",
|
||||||
|
description: "Legacy driver national code",
|
||||||
|
})
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
|
@ValidateIf((dto) => !dto.driver)
|
||||||
nationalCodeOfDriver: string;
|
nationalCodeOfDriver: string;
|
||||||
|
|
||||||
@ApiProperty({ example: true, description: "Whether the driver is the same person as the insurer" })
|
@ApiPropertyOptional({
|
||||||
|
example: true,
|
||||||
|
description: "Legacy role relationship",
|
||||||
|
})
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
|
@ValidateIf((dto) => !dto.driver)
|
||||||
driverIsInsurer: boolean;
|
driverIsInsurer: boolean;
|
||||||
|
|
||||||
@ApiProperty({ example: 13780624, description: "Insurer birth date (Jalali)" })
|
@ApiPropertyOptional({
|
||||||
|
example: 13780624,
|
||||||
|
description: "Legacy policyholder birth date (Jalali)",
|
||||||
|
})
|
||||||
insurerBirthday: number | string;
|
insurerBirthday: number | string;
|
||||||
|
|
||||||
@ApiPropertyOptional({
|
@ApiPropertyOptional({
|
||||||
example: 13780624,
|
example: 13780624,
|
||||||
description: "Driver birth date (Jalali). Required when driverIsInsurer is false.",
|
description:
|
||||||
|
"Driver birth date (Jalali). Required when driverIsInsurer is false.",
|
||||||
})
|
})
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
driverBirthday?: number | string | null;
|
driverBirthday?: number | string | null;
|
||||||
|
|||||||
@@ -5,9 +5,11 @@ import {
|
|||||||
IsOptional,
|
IsOptional,
|
||||||
IsString,
|
IsString,
|
||||||
MaxLength,
|
MaxLength,
|
||||||
|
ValidateIf,
|
||||||
ValidateNested,
|
ValidateNested,
|
||||||
} from "class-validator";
|
} from "class-validator";
|
||||||
import { Type } from "class-transformer";
|
import { Type } from "class-transformer";
|
||||||
|
import { InquiryParticipantFieldsDto } from "./inquiry-participants.dto";
|
||||||
|
|
||||||
class PlateV3Dto {
|
class PlateV3Dto {
|
||||||
@ApiProperty({ example: "44", description: "Left two digits" })
|
@ApiProperty({ example: "44", description: "Left two digits" })
|
||||||
@@ -25,7 +27,10 @@ class PlateV3Dto {
|
|||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
centerDigits: string;
|
centerDigits: string;
|
||||||
|
|
||||||
@ApiProperty({ example: "22", description: "Right two digits (Iran region code)" })
|
@ApiProperty({
|
||||||
|
example: "22",
|
||||||
|
description: "Right two digits (Iran region code)",
|
||||||
|
})
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
ir: string;
|
ir: string;
|
||||||
@@ -35,33 +40,53 @@ class PlateV3Dto {
|
|||||||
* Body for `POST run-inquiries/:requestId`.
|
* Body for `POST run-inquiries/:requestId`.
|
||||||
* First call = guilty party (+ auto claim). Second call = damaged party (THIRD_PARTY only).
|
* First call = guilty party (+ auto claim). Second call = damaged party (THIRD_PARTY only).
|
||||||
*/
|
*/
|
||||||
export class RunInquiriesV3Dto {
|
export class RunInquiriesV3Dto extends InquiryParticipantFieldsDto {
|
||||||
@ApiProperty({
|
@ApiPropertyOptional({
|
||||||
type: PlateV3Dto,
|
type: PlateV3Dto,
|
||||||
description: "Plate segments — Tejarat block / third-party inquiry.",
|
description: "Plate segments — Tejarat block / third-party inquiry.",
|
||||||
})
|
})
|
||||||
@ValidateNested()
|
@ValidateNested()
|
||||||
@Type(() => PlateV3Dto)
|
@Type(() => PlateV3Dto)
|
||||||
|
@ValidateIf((dto) => !dto.vehicle?.currentPlate)
|
||||||
plate: PlateV3Dto;
|
plate: PlateV3Dto;
|
||||||
|
|
||||||
@ApiProperty({ example: "1234567890", description: "National code of the policyholder (insurer)" })
|
@ApiPropertyOptional({
|
||||||
|
example: "1234567890",
|
||||||
|
description: "Legacy third-party policyholder national code",
|
||||||
|
})
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
|
@ValidateIf((dto) => !dto.thirdPartyPolicyholder)
|
||||||
nationalCodeOfInsurer: string;
|
nationalCodeOfInsurer: string;
|
||||||
|
|
||||||
@ApiProperty({ example: "1234567890", description: "National code of the driver" })
|
@ApiPropertyOptional({
|
||||||
|
example: "1234567890",
|
||||||
|
description: "Legacy driver national code",
|
||||||
|
})
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
|
@ValidateIf((dto) => !dto.driver)
|
||||||
nationalCodeOfDriver: string;
|
nationalCodeOfDriver: string;
|
||||||
|
|
||||||
@ApiProperty({ example: true, description: "Whether the driver is the same person as the insurer" })
|
@ApiPropertyOptional({
|
||||||
|
example: true,
|
||||||
|
description: "Legacy role relationship",
|
||||||
|
})
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
|
@ValidateIf((dto) => !dto.driver)
|
||||||
driverIsInsurer: boolean;
|
driverIsInsurer: boolean;
|
||||||
|
|
||||||
@ApiProperty({ example: 13780624, description: "Insurer birth date (Jalali)" })
|
@ApiPropertyOptional({
|
||||||
|
example: 13780624,
|
||||||
|
description: "Legacy policyholder birth date (Jalali)",
|
||||||
|
})
|
||||||
insurerBirthday: number | string;
|
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()
|
@IsOptional()
|
||||||
driverBirthday?: number | string | null;
|
driverBirthday?: number | string | null;
|
||||||
|
|
||||||
@@ -106,8 +131,8 @@ export class RunInquiriesV3Dto {
|
|||||||
* Identical to RunInquiriesV3Dto but uses `vin` (17-char chassis number) instead of `plate`.
|
* 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).
|
* First call = guilty party (+ auto claim). Second call = damaged party (THIRD_PARTY only).
|
||||||
*/
|
*/
|
||||||
export class RunInquiriesVinV3Dto {
|
export class RunInquiriesVinV3Dto extends InquiryParticipantFieldsDto {
|
||||||
@ApiProperty({
|
@ApiPropertyOptional({
|
||||||
example: "NAAM01E15HK123456",
|
example: "NAAM01E15HK123456",
|
||||||
description: "17-character VIN / chassis number (شماره شاسی)",
|
description: "17-character VIN / chassis number (شماره شاسی)",
|
||||||
maxLength: 17,
|
maxLength: 17,
|
||||||
@@ -115,26 +140,46 @@ export class RunInquiriesVinV3Dto {
|
|||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@MaxLength(17)
|
@MaxLength(17)
|
||||||
|
@ValidateIf((dto) => !dto.vehicle?.vin)
|
||||||
vin: string;
|
vin: string;
|
||||||
|
|
||||||
@ApiProperty({ example: "1234567890", description: "National code of the policyholder (insurer)" })
|
@ApiPropertyOptional({
|
||||||
|
example: "1234567890",
|
||||||
|
description: "Legacy third-party policyholder national code",
|
||||||
|
})
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
|
@ValidateIf((dto) => !dto.thirdPartyPolicyholder)
|
||||||
nationalCodeOfInsurer: string;
|
nationalCodeOfInsurer: string;
|
||||||
|
|
||||||
@ApiProperty({ example: "1234567890", description: "National code of the driver" })
|
@ApiPropertyOptional({
|
||||||
|
example: "1234567890",
|
||||||
|
description: "Legacy driver national code",
|
||||||
|
})
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
|
@ValidateIf((dto) => !dto.driver)
|
||||||
nationalCodeOfDriver: string;
|
nationalCodeOfDriver: string;
|
||||||
|
|
||||||
@ApiProperty({ example: true, description: "Whether the driver is the same person as the insurer" })
|
@ApiPropertyOptional({
|
||||||
|
example: true,
|
||||||
|
description: "Legacy role relationship",
|
||||||
|
})
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
|
@ValidateIf((dto) => !dto.driver)
|
||||||
driverIsInsurer: boolean;
|
driverIsInsurer: boolean;
|
||||||
|
|
||||||
@ApiProperty({ example: 13780624, description: "Insurer birth date (Jalali)" })
|
@ApiPropertyOptional({
|
||||||
|
example: 13780624,
|
||||||
|
description: "Legacy policyholder birth date (Jalali)",
|
||||||
|
})
|
||||||
insurerBirthday: number | string;
|
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()
|
@IsOptional()
|
||||||
driverBirthday?: number | string | null;
|
driverBirthday?: number | string | null;
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { Location, LocationSchema } from "./accidentInformation.type";
|
|||||||
export enum PartyRole {
|
export enum PartyRole {
|
||||||
FIRST = "FIRST",
|
FIRST = "FIRST",
|
||||||
SECOND = "SECOND",
|
SECOND = "SECOND",
|
||||||
}
|
}
|
||||||
|
|
||||||
@Schema({ _id: false })
|
@Schema({ _id: false })
|
||||||
export class Person {
|
export class Person {
|
||||||
@@ -50,6 +50,37 @@ export class Person {
|
|||||||
}
|
}
|
||||||
export const PersonSchema = SchemaFactory.createForClass(Person);
|
export const PersonSchema = SchemaFactory.createForClass(Person);
|
||||||
|
|
||||||
|
@Schema({ _id: false })
|
||||||
|
export class InquiryParticipant {
|
||||||
|
@Prop({ required: true })
|
||||||
|
participantId: string;
|
||||||
|
|
||||||
|
@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 })
|
@Schema({ _id: false })
|
||||||
export class Vehicle {
|
export class Vehicle {
|
||||||
@Prop() plateId?: string;
|
@Prop() plateId?: string;
|
||||||
@@ -59,6 +90,9 @@ export class Vehicle {
|
|||||||
@Prop() model?: string;
|
@Prop() model?: string;
|
||||||
@Prop() type?: string;
|
@Prop() type?: string;
|
||||||
@Prop() isNew?: boolean;
|
@Prop() isNew?: boolean;
|
||||||
|
@Prop({ enum: ["CURRENT", "RECENTLY_TRANSFERRED"] })
|
||||||
|
registrationState?: "CURRENT" | "RECENTLY_TRANSFERRED";
|
||||||
|
@Prop() previousPlateId?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Full external inquiry payload (Tejarat/SandHub) stored as-is
|
* Full external inquiry payload (Tejarat/SandHub) stored as-is
|
||||||
@@ -123,7 +157,6 @@ export const InsuranceSchema = SchemaFactory.createForClass(Insurance);
|
|||||||
|
|
||||||
@Schema({ _id: false })
|
@Schema({ _id: false })
|
||||||
export class PartyStatement {
|
export class PartyStatement {
|
||||||
|
|
||||||
@Prop({ default: false })
|
@Prop({ default: false })
|
||||||
acceptsExpertOpinion?: boolean;
|
acceptsExpertOpinion?: boolean;
|
||||||
|
|
||||||
@@ -143,11 +176,11 @@ export class PartyStatement {
|
|||||||
@Prop() roadCondition?: string;
|
@Prop() roadCondition?: string;
|
||||||
@Prop() lightCondition?: string;
|
@Prop() lightCondition?: string;
|
||||||
}
|
}
|
||||||
export const PartyStatementSchema = SchemaFactory.createForClass(PartyStatement);
|
export const PartyStatementSchema =
|
||||||
|
SchemaFactory.createForClass(PartyStatement);
|
||||||
|
|
||||||
@Schema({ _id: false })
|
@Schema({ _id: false })
|
||||||
export class EvidenceBundle {
|
export class EvidenceBundle {
|
||||||
|
|
||||||
@Prop({ type: [String] })
|
@Prop({ type: [String] })
|
||||||
images?: string[];
|
images?: string[];
|
||||||
|
|
||||||
@@ -156,7 +189,8 @@ export class EvidenceBundle {
|
|||||||
|
|
||||||
@Prop() videoId?: string;
|
@Prop() videoId?: string;
|
||||||
}
|
}
|
||||||
export const EvidenceBundleSchema = SchemaFactory.createForClass(EvidenceBundle);
|
export const EvidenceBundleSchema =
|
||||||
|
SchemaFactory.createForClass(EvidenceBundle);
|
||||||
|
|
||||||
@Schema({ _id: false })
|
@Schema({ _id: false })
|
||||||
export class Signature {
|
export class Signature {
|
||||||
@@ -178,13 +212,19 @@ export const PartyConfirmationSchema =
|
|||||||
|
|
||||||
@Schema({ _id: false })
|
@Schema({ _id: false })
|
||||||
export class Party {
|
export class Party {
|
||||||
|
|
||||||
@Prop({ enum: PartyRole })
|
@Prop({ enum: PartyRole })
|
||||||
role: PartyRole;
|
role: PartyRole;
|
||||||
|
|
||||||
@Prop({ type: PersonSchema })
|
@Prop({ type: PersonSchema })
|
||||||
person: Person;
|
person: Person;
|
||||||
|
|
||||||
|
/** Distinct people involved with this vehicle; roles below reference participantId. */
|
||||||
|
@Prop({ type: [InquiryParticipantSchema], default: undefined })
|
||||||
|
participants?: InquiryParticipant[];
|
||||||
|
|
||||||
|
@Prop({ type: InquiryParticipantRolesSchema })
|
||||||
|
participantRoles?: InquiryParticipantRoles;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CAR_BODY only: first form – accident with car vs object.
|
* CAR_BODY only: first form – accident with car vs object.
|
||||||
* Second form (guilty/damaged) may be added later as carBodySecondForm.
|
* Second form (guilty/damaged) may be added later as carBodySecondForm.
|
||||||
|
|||||||
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