Require structured inquiry participant inputs

This commit is contained in:
SepehrYahyaee
2026-09-13 14:13:05 +03:30
parent cc8a96c875
commit 5b77241f28
9 changed files with 162 additions and 602 deletions

View File

@@ -6,9 +6,9 @@
## interface پیاده‌سازی‌شده
فیلدهای نقش‌ها و آبجکت اختیاری `vehicle` که در ادامه آمده‌اند، مستقیماً در body تمام درخواست‌های استعلام فعلی پذیرفته می‌شوند. این تغییر شامل فرم اولیه کاربر و mirror کارشناس/ثبت‌کننده در V2، جریان کارشناس V3، جریان‌های پرونده‌ساز V4/V5، مرکز تماس V6 و مسیرهای تک‌درخواستی حضوری است. routeهای پلاک و VIN از قوانین مشترک اشخاص استفاده می‌کنند.
فیلدهای نقش‌ها و آبجکت الزامی `vehicle` که در ادامه آمده‌اند، مستقیماً در body تمام درخواست‌های استعلام فعلی پذیرفته می‌شوند. این تغییر شامل فرم اولیه کاربر و mirror کارشناس/ثبت‌کننده در V2، جریان کارشناس V3، جریان‌های پرونده‌ساز V4/V5، مرکز تماس V6 و مسیرهای تک‌درخواستی حضوری است. routeهای پلاک و VIN از قوانین مشترک اشخاص استفاده می‌کنند. فیلدهای تخت راننده/بیمه‌گذار و شماره تلفن، ورودی استعلام نیستند.
در دوره مهاجرت، فیلدهای قدیمی و تخت همچنان پذیرفته می‌شوند. پاسخ‌ها و جزئیات پرونده برای نقش‌های عملیاتی، در صورت وجود داده، فیلدهای نرمال‌شده `participants`، `participantRoles`، `vehicle.registrationState` و `vehicle.previousPlateId` را نمایش می‌دهند.
پاسخ‌ها و جزئیات پرونده برای نقش‌های عملیاتی، در صورت وجود داده، فیلدهای نرمال‌شده `participants`، `participantRoles`، `vehicle.registrationState` و `vehicle.previousPlateId` را نمایش می‌دهند.
## مسئله
@@ -106,14 +106,9 @@
جریان‌های V2 کاربر/کارشناس، V3، V4، V5 و V6 باید adapter همین قوانین مشترک باشند و منطق نسبت اشخاص را جداگانه پیاده‌سازی نکنند.
## سازگاری و انتشار تدریجی
## مرز قرارداد
1. در دوره گذار، ساختار جدید در کنار فیلدهای قدیمی پذیرفته شود.
2. `nationalCodeOfDriver` به راننده و `nationalCodeOfInsurer` به بیمه‌گذار شخص ثالث نگاشت شود. اگر `driverIsInsurer=true` است، هر دو نقش به یک شخص متصل شوند.
3. `plate` یا `plateId` قدیمی به پلاک فعلی نگاشت شود؛ پلاک قبلی فقط وقتی ثبت شود که صریحاً از کاربر دریافت شده باشد.
4. برای داده‌های قدیمی، مالک خودرو یا بیمه‌گذار بدنه حدس زده نشود؛ مگر اینکه نتیجه استعلام ذخیره‌شده آن را قطعی کند، نقش «نامشخص» باقی بماند.
5. پاسخ جزئیات و گزارش، اشخاص را بر اساس نقش نمایش دهد و در زمان مهاجرت فیلدهای قدیمی را نیز حفظ کند.
6. پس از مهاجرت همه فرانت‌اندها، قرارداد جدید برای پرونده‌های تازه الزامی و فیلدهای قدیمی deprecated شوند.
ورودی استعلام فقط شامل آبجکت‌های ساختاریافته اشخاص و خودرو است. بک‌اند فیلدهای تختی مانند `nationalCodeOfDriver`، `nationalCodeOfInsurer`، `driverIsInsurer`، `plate`/`vin` سطح بالا و `phoneNumber` را با خطای اعتبارسنجی رد می‌کند. احراز هویت تلفنی و جریان‌های تماس با طرفین از جمع‌آوری هویت برای استعلام جدا هستند.
## تصمیم پیشنهادی

View File

@@ -6,9 +6,9 @@ Persian version: [inquiry-participants-proposal.fa.md](./inquiry-participants-pr
## 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.
The role fields and required `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. Flat driver/insurer fields and phone numbers are not inquiry inputs.
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.
Responses and file-detail views for operational actors expose normalized `participants`, `participantRoles`, `vehicle.registrationState`, and `vehicle.previousPlateId` where available.
## Problem
@@ -106,14 +106,9 @@ Create one shared participant resolver used by every inquiry route. Its interfac
V2 user/expert routes, V3, V4, V5, and V6 should be adapters over this shared rule set rather than implementing their own relationship logic.
## Compatibility and rollout
## Contract boundary
1. Accept the new participant object alongside the legacy fields temporarily.
2. Map legacy `nationalCodeOfDriver` to Driver and `nationalCodeOfInsurer` to Third-party Policyholder. When `driverIsInsurer=true`, bind both roles to the same participant.
3. Map the legacy `plate`/`plateId` to Current Plate; leave Previous Plate absent unless it was explicitly collected.
4. Do not guess Vehicle Owner or Car-body Policyholder for old records. Mark unresolved roles as unknown unless stored inquiry evidence identifies them.
5. Update report/detail responses to expose people by role while retaining legacy fields during migration.
6. After all frontends use the new step, require the new contract for newly created cases and deprecate the legacy fields.
The structured participant and vehicle objects are the only accepted inquiry input. The backend rejects flat fields such as `nationalCodeOfDriver`, `nationalCodeOfInsurer`, `driverIsInsurer`, top-level `plate`/`vin`, and `phoneNumber` with a validation error. Phone-based authentication and party contact flows remain separate from inquiry identity collection.
## Decision

View File

@@ -1,4 +1,8 @@
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import {
ApiHideProperty,
ApiProperty,
ApiPropertyOptional,
} from "@nestjs/swagger";
import { Type } from "class-transformer";
import {
IsBoolean,
@@ -69,11 +73,6 @@ export class InquiryParticipantInputDto {
@IsString()
fullName?: string;
@ApiPropertyOptional()
@IsOptional()
@IsString()
phoneNumber?: string;
@ApiPropertyOptional({
description: "Required for a driver who has a licence.",
})
@@ -117,27 +116,31 @@ export class InquiryVehicleInputDto {
@IsString()
@MaxLength(17)
vin?: string;
@ApiPropertyOptional({
description: "Whether the vehicle is newly purchased/registered.",
})
@IsOptional()
@IsBoolean()
isNewCar?: boolean;
}
/** New role-complete contract mixed into every inquiry DTO. Legacy fields remain during rollout. */
/** Structured role-complete contract mixed into every inquiry DTO. */
export class InquiryParticipantFieldsDto {
@ApiPropertyOptional({ type: InquiryParticipantInputDto })
@IsOptional()
@ApiProperty({ type: InquiryParticipantInputDto })
@ValidateNested()
@Type(() => InquiryParticipantInputDto)
driver?: InquiryParticipantInputDto;
driver: InquiryParticipantInputDto;
@ApiPropertyOptional({ type: InquiryParticipantInputDto })
@IsOptional()
@ApiProperty({ type: InquiryParticipantInputDto })
@ValidateNested()
@Type(() => InquiryParticipantInputDto)
vehicleOwner?: InquiryParticipantInputDto;
vehicleOwner: InquiryParticipantInputDto;
@ApiPropertyOptional({ type: InquiryParticipantInputDto })
@IsOptional()
@ApiProperty({ type: InquiryParticipantInputDto })
@ValidateNested()
@Type(() => InquiryParticipantInputDto)
thirdPartyPolicyholder?: InquiryParticipantInputDto;
thirdPartyPolicyholder: InquiryParticipantInputDto;
@ApiPropertyOptional({ type: InquiryParticipantInputDto })
@IsOptional()
@@ -145,9 +148,48 @@ export class InquiryParticipantFieldsDto {
@Type(() => InquiryParticipantInputDto)
carBodyPolicyholder?: InquiryParticipantInputDto;
@ApiPropertyOptional({ type: InquiryVehicleInputDto })
@IsOptional()
@ApiProperty({ type: InquiryVehicleInputDto })
@ValidateNested()
@Type(() => InquiryVehicleInputDto)
vehicle?: InquiryVehicleInputDto;
vehicle: InquiryVehicleInputDto;
/** Internal normalized projections; rejected as request input by the resolver. */
@ApiHideProperty()
nationalCodeOfDriver?: string;
@ApiHideProperty()
driverBirthday?: any;
@ApiHideProperty()
driverLicense?: string;
@ApiHideProperty()
licenseType?: string;
@ApiHideProperty()
nationalCodeOfInsurer?: string;
@ApiHideProperty()
insurerBirthday?: any;
@ApiHideProperty()
insurerLicense?: string;
@ApiHideProperty()
driverIsInsurer?: boolean;
@ApiHideProperty()
userNoCertificate?: boolean;
@ApiHideProperty()
plate?: any;
@ApiHideProperty()
plateId?: string;
@ApiHideProperty()
vin?: string;
@ApiHideProperty()
isNewCar?: boolean;
}

View File

@@ -1,61 +1,9 @@
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { ApiProperty } from "@nestjs/swagger";
import { PlatesDto } from "src/plates/dto/plate.dto";
import { Plates } from "src/Types&Enums/plate.interface";
import { InquiryParticipantFieldsDto } from "src/common/dto/inquiry-participants.dto";
export class AddPlateDto extends InquiryParticipantFieldsDto {
plateId: string;
@ApiPropertyOptional({
type: String,
description: "Legacy third-party policyholder national code",
})
nationalCodeOfInsurer: string;
@ApiPropertyOptional({
type: String,
description: "Legacy driver national code",
})
nationalCodeOfDriver: string;
@ApiPropertyOptional({ type: String })
insurerLicense: string;
@ApiPropertyOptional({ type: String })
driverLicense: string;
@ApiPropertyOptional({
type: PlatesDto,
description:
"Legacy/current plate; may be supplied as vehicle.currentPlate",
})
plate: Plates;
@ApiPropertyOptional({
type: Boolean,
description: "Legacy role relationship",
})
driverIsInsurer: boolean;
@ApiProperty({ type: Boolean, required: true, default: false })
isNewCar: boolean;
@ApiPropertyOptional({
type: Boolean,
description: "Legacy; use driver.hasDrivingLicense",
})
userNoCertificate: boolean;
@ApiPropertyOptional({
type: Number,
description: "Legacy third-party policyholder birthday",
})
insurerBirthday: number;
@ApiProperty({
type: String,
required: false,
})
driverBirthday: string | null;
}
export class AddPlateDto extends InquiryParticipantFieldsDto {}
export class AddPlateProfileDto {
plateId: string;

View File

@@ -1,11 +1,5 @@
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import {
IsNotEmpty,
IsOptional,
IsString,
MaxLength,
ValidateIf,
} from "class-validator";
import { IsNotEmpty, IsOptional, IsString } 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";
@@ -229,70 +223,8 @@ export class BlameConfessionDtoV2 {
imGuilty?: boolean;
}
/**
* V2 initial-form step submitted with a VIN/chassis number instead of a plate.
* 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 extends InquiryParticipantFieldsDto {
@ApiPropertyOptional({
type: String,
required: false,
description: "17-character VIN / chassis number (شماره شاسی)",
example: "NAAM01E15HK123456",
maxLength: 17,
})
@IsString()
@IsNotEmpty()
@MaxLength(17)
@ValidateIf((dto) => !dto.vehicle?.vin)
vin: string;
@ApiPropertyOptional({
type: String,
description: "Legacy third-party policyholder national code",
})
@ValidateIf((dto) => !dto.thirdPartyPolicyholder)
nationalCodeOfInsurer: string;
@ApiPropertyOptional({
type: String,
description: "Legacy driver national code",
})
@ValidateIf((dto) => !dto.driver)
nationalCodeOfDriver: string;
@ApiPropertyOptional({ type: String })
insurerLicense: string;
@ApiPropertyOptional({ type: String })
driverLicense: string;
@ApiPropertyOptional({
type: Boolean,
description: "Legacy role relationship",
})
@ValidateIf((dto) => !dto.driver)
driverIsInsurer: boolean;
@ApiPropertyOptional({ type: Boolean, default: false })
isNewCar: boolean;
@ApiPropertyOptional({
type: Boolean,
description: "Legacy; use driver.hasDrivingLicense",
})
userNoCertificate: boolean;
@ApiPropertyOptional({
type: Number,
description: "Legacy policyholder birthday",
})
insurerBirthday: number;
@ApiPropertyOptional({ type: String, required: false })
driverBirthday: string | null;
}
/** V2 initial-form step submitted with the shared structured vehicle contract. */
export class InitialFormVinDto extends InquiryParticipantFieldsDto {}
// export class DocsOfThisFile {
// @ApiProperty()

View File

@@ -1,182 +1,14 @@
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import {
IsBoolean,
IsNotEmpty,
IsOptional,
IsString,
MaxLength,
ValidateIf,
ValidateNested,
} from "class-validator";
import { Type } from "class-transformer";
import { InquiryParticipantFieldsDto } from "src/common/dto/inquiry-participants.dto";
class PlateV6Dto {
@ApiProperty({ example: "44", description: "Left two digits" })
@IsString()
@IsNotEmpty()
leftDigits: string;
@ApiProperty({ example: "ب", description: "Center alphabet letter" })
@IsString()
@IsNotEmpty()
centerAlphabet: string;
@ApiProperty({ example: "111", description: "Center three digits" })
@IsString()
@IsNotEmpty()
centerDigits: string;
@ApiProperty({
example: "22",
description: "Right two digits (Iran region code)",
})
@IsString()
@IsNotEmpty()
ir: string;
}
/**
* 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 extends InquiryParticipantFieldsDto {
@ApiPropertyOptional({
type: PlateV6Dto,
description: "Plate segments — Tejarat block / third-party inquiry.",
})
@ValidateNested()
@Type(() => PlateV6Dto)
@ValidateIf((dto) => !dto.vehicle?.currentPlate)
plate: PlateV6Dto;
@ApiPropertyOptional({
example: "1234567890",
description: "Legacy third-party policyholder national code",
})
@IsString()
@IsNotEmpty()
@ValidateIf((dto) => !dto.thirdPartyPolicyholder)
nationalCodeOfInsurer: string;
@ApiPropertyOptional({
example: "1234567890",
description: "Legacy driver national code",
})
@IsString()
@IsNotEmpty()
@ValidateIf((dto) => !dto.driver)
nationalCodeOfDriver: string;
@ApiPropertyOptional({
example: true,
description: "Legacy role relationship",
})
@IsBoolean()
@ValidateIf((dto) => !dto.driver)
driverIsInsurer: boolean;
@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.",
})
@IsOptional()
driverBirthday?: number | string | null;
@ApiPropertyOptional({
example: "123456789",
description: "Driver license (required when driverIsInsurer is false).",
})
@IsOptional()
@IsString()
driverLicense?: string;
@ApiPropertyOptional({
example: "123456789",
description: "Insurer license (required when driverIsInsurer is true).",
})
@IsOptional()
@IsString()
insurerLicense?: string;
}
export class RunCallCenterInquiryV6Dto extends InquiryParticipantFieldsDto {}
/**
* VIN / chassis variant of the V6 call-center inquiry.
* Identical to `RunCallCenterInquiryV6Dto` but replaces `plate` with `vin`.
* Sheba (IBAN) is intentionally absent — the user provides it themselves via the link.
*/
export class RunCallCenterInquiryVinV6Dto extends InquiryParticipantFieldsDto {
@ApiPropertyOptional({
example: "NAAM01E15HK123456",
description: "17-character VIN / chassis number (شماره شاسی)",
maxLength: 17,
})
@IsString()
@IsNotEmpty()
@MaxLength(17)
@ValidateIf((dto) => !dto.vehicle?.vin)
vin: string;
@ApiPropertyOptional({
example: "1234567890",
description: "Legacy third-party policyholder national code",
})
@IsString()
@IsNotEmpty()
@ValidateIf((dto) => !dto.thirdPartyPolicyholder)
nationalCodeOfInsurer: string;
@ApiPropertyOptional({
example: "1234567890",
description: "Legacy driver national code",
})
@IsString()
@IsNotEmpty()
@ValidateIf((dto) => !dto.driver)
nationalCodeOfDriver: string;
@ApiPropertyOptional({
example: true,
description: "Legacy role relationship",
})
@IsBoolean()
@ValidateIf((dto) => !dto.driver)
driverIsInsurer: boolean;
@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.",
})
@IsOptional()
driverBirthday?: number | string | null;
@ApiPropertyOptional({
example: "123456789",
description: "Driver license (required when driverIsInsurer is false).",
})
@IsOptional()
@IsString()
driverLicense?: string;
@ApiPropertyOptional({
example: "123456789",
description: "Insurer license (required when driverIsInsurer is true).",
})
@IsOptional()
@IsString()
insurerLicense?: string;
}
export class RunCallCenterInquiryVinV6Dto extends InquiryParticipantFieldsDto {}

View File

@@ -1,95 +1,12 @@
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import {
IsBoolean,
IsNotEmpty,
IsOptional,
IsString,
MaxLength,
ValidateIf,
ValidateNested,
} from "class-validator";
import { Type } from "class-transformer";
import { ApiPropertyOptional } from "@nestjs/swagger";
import { IsOptional, IsString } from "class-validator";
import { InquiryParticipantFieldsDto } from "src/common/dto/inquiry-participants.dto";
class PlateV3Dto {
@ApiProperty({ example: "44", description: "Left two digits" })
@IsString()
@IsNotEmpty()
leftDigits: string;
@ApiProperty({ example: "ب", description: "Center alphabet letter" })
@IsString()
@IsNotEmpty()
centerAlphabet: string;
@ApiProperty({ example: "111", description: "Center three digits" })
@IsString()
@IsNotEmpty()
centerDigits: string;
@ApiProperty({
example: "22",
description: "Right two digits (Iran region code)",
})
@IsString()
@IsNotEmpty()
ir: string;
}
/**
* Body for `POST run-inquiries/:requestId`.
* First call = guilty party (+ auto claim). Second call = damaged party (THIRD_PARTY only).
*/
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;
@ApiPropertyOptional({
example: "1234567890",
description: "Legacy third-party policyholder national code",
})
@IsString()
@IsNotEmpty()
@ValidateIf((dto) => !dto.thirdPartyPolicyholder)
nationalCodeOfInsurer: string;
@ApiPropertyOptional({
example: "1234567890",
description: "Legacy driver national code",
})
@IsString()
@IsNotEmpty()
@ValidateIf((dto) => !dto.driver)
nationalCodeOfDriver: string;
@ApiPropertyOptional({
example: true,
description: "Legacy role relationship",
})
@IsBoolean()
@ValidateIf((dto) => !dto.driver)
driverIsInsurer: boolean;
@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.",
})
@IsOptional()
driverBirthday?: number | string | null;
@ApiPropertyOptional({
example: "IR123456789012345678901234",
description:
@@ -98,32 +15,6 @@ export class RunInquiriesV3Dto extends InquiryParticipantFieldsDto {
@IsOptional()
@IsString()
sheba?: string;
@ApiPropertyOptional({
example: "123456789",
description: "Driver license (required when driverIsInsurer is false).",
})
@IsOptional()
@IsString()
driverLicense?: string;
@ApiPropertyOptional({
example: "123456789",
description: "Insurer license (required when driverIsInsurer is true).",
})
@IsOptional()
@IsString()
insurerLicense?: string;
@ApiPropertyOptional({
example: "1",
description:
"Driving licence type code from GET /lookups/driving-licence-types. " +
"Optional — used in V4 FileMaker flow.",
})
@IsOptional()
@IsString()
licenseType?: string;
}
/**
@@ -132,57 +23,6 @@ export class RunInquiriesV3Dto extends InquiryParticipantFieldsDto {
* First call = guilty party (+ auto claim). Second call = damaged party (THIRD_PARTY only).
*/
export class RunInquiriesVinV3Dto extends InquiryParticipantFieldsDto {
@ApiPropertyOptional({
example: "NAAM01E15HK123456",
description: "17-character VIN / chassis number (شماره شاسی)",
maxLength: 17,
})
@IsString()
@IsNotEmpty()
@MaxLength(17)
@ValidateIf((dto) => !dto.vehicle?.vin)
vin: string;
@ApiPropertyOptional({
example: "1234567890",
description: "Legacy third-party policyholder national code",
})
@IsString()
@IsNotEmpty()
@ValidateIf((dto) => !dto.thirdPartyPolicyholder)
nationalCodeOfInsurer: string;
@ApiPropertyOptional({
example: "1234567890",
description: "Legacy driver national code",
})
@IsString()
@IsNotEmpty()
@ValidateIf((dto) => !dto.driver)
nationalCodeOfDriver: string;
@ApiPropertyOptional({
example: true,
description: "Legacy role relationship",
})
@IsBoolean()
@ValidateIf((dto) => !dto.driver)
driverIsInsurer: boolean;
@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.",
})
@IsOptional()
driverBirthday?: number | string | null;
@ApiPropertyOptional({
example: "IR123456789012345678901234",
description:
@@ -191,30 +31,4 @@ export class RunInquiriesVinV3Dto extends InquiryParticipantFieldsDto {
@IsOptional()
@IsString()
sheba?: string;
@ApiPropertyOptional({
example: "123456789",
description: "Driver license (required when driverIsInsurer is false).",
})
@IsOptional()
@IsString()
driverLicense?: string;
@ApiPropertyOptional({
example: "123456789",
description: "Insurer license (required when driverIsInsurer is true).",
})
@IsOptional()
@IsString()
insurerLicense?: string;
@ApiPropertyOptional({
example: "1",
description:
"Driving licence type code from GET /lookups/driving-licence-types. " +
"Optional — used in V4 FileMaker flow.",
})
@IsOptional()
@IsString()
licenseType?: string;
}

View File

@@ -66,22 +66,34 @@ describe("inquiry participant resolver", () => {
).toBe(VehicleRegistrationState.CURRENT);
});
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);
it("rejects the removed flat inquiry contract", () => {
expect(() =>
resolveInquiryParticipants(BlameRequestType.THIRD_PARTY, {
nationalCodeOfDriver: "0012345678",
driverBirthday: "1370/01/01",
driverLicense: "123456789",
nationalCodeOfInsurer: "0098765432",
insurerBirthday: "1360/02/02",
driverIsInsurer: false,
} as any),
).toThrow(BadRequestException);
});
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("rejects phone numbers in participant inquiry identity", () => {
expect(() =>
resolveInquiryParticipants(BlameRequestType.THIRD_PARTY, {
driver: {
nationalCode: "0012345678",
birthday: "1370/01/01",
hasDrivingLicense: false,
phoneNumber: "09120000000",
},
vehicleOwner: { sameAs: InquiryParticipantRole.DRIVER },
thirdPartyPolicyholder: {
sameAs: InquiryParticipantRole.DRIVER,
},
} as any),
).toThrow(BadRequestException);
});
it("keeps third-party and car-body policyholders distinct", () => {
@@ -116,11 +128,13 @@ describe("inquiry participant resolver", () => {
it("projects role-complete input onto the existing inquiry fields", () => {
const normalized = normalizeInquirySubmission(BlameRequestType.CAR_BODY, {
plate: {
leftDigits: "44",
centerAlphabet: "ب",
centerDigits: "111",
ir: "22",
vehicle: {
currentPlate: {
leftDigits: "44",
centerAlphabet: "ب",
centerDigits: "111",
ir: "22",
},
},
driver: {
nationalCode: "0012345678",

View File

@@ -15,7 +15,6 @@ export interface ResolvedInquiryParticipant {
nationalCode: string;
birthday: string;
fullName?: string;
phoneNumber?: string;
hasDrivingLicense?: boolean;
licenseNumber?: string;
licenseType?: string;
@@ -90,6 +89,11 @@ function requiredIdentity(
role: InquiryParticipantRole,
input: InquiryParticipantInputDto,
): ResolvedInquiryParticipant {
if (Object.prototype.hasOwnProperty.call(input, "phoneNumber")) {
throw new BadRequestException(
`${role} does not accept phoneNumber; phone numbers are collected separately from inquiry identity.`,
);
}
const nationalCode = String(input.nationalCode ?? "").trim();
const birthday = String(input.birthday ?? "").trim();
if (!nationalCode || !birthday) {
@@ -118,7 +122,6 @@ function requiredIdentity(
nationalCode,
birthday,
...(input.fullName ? { fullName: input.fullName } : {}),
...(input.phoneNumber ? { phoneNumber: input.phoneNumber } : {}),
...(input.hasDrivingLicense != null
? { hasDrivingLicense: input.hasDrivingLicense }
: {}),
@@ -129,52 +132,17 @@ function requiredIdentity(
export function resolveInquiryParticipants(
caseType: BlameRequestType,
input: InquiryParticipantFieldsDto & Record<string, any>,
input: Partial<InquiryParticipantFieldsDto> & Record<string, any>,
options: InquiryParticipantResolutionOptions = {},
): ResolvedInquiryParticipants {
assertStructuredInquiryInput(input);
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,
};
throw new BadRequestException(
"driver, vehicleOwner, and thirdPartyPolicyholder are required in the structured inquiry format.",
);
}
if (
caseType === BlameRequestType.THIRD_PARTY &&
@@ -367,6 +335,33 @@ function normalizePlateForComparison(
.join("|");
}
const LEGACY_INQUIRY_FIELDS = [
"nationalCodeOfDriver",
"driverBirthday",
"driverLicense",
"licenseType",
"nationalCodeOfInsurer",
"insurerBirthday",
"insurerLicense",
"driverIsInsurer",
"userNoCertificate",
"plate",
"plateId",
"vin",
"isNewCar",
] as const;
function assertStructuredInquiryInput(input: Record<string, any>): void {
const legacyFields = LEGACY_INQUIRY_FIELDS.filter((field) =>
Object.prototype.hasOwnProperty.call(input, field),
);
if (legacyFields.length > 0) {
throw new BadRequestException(
`Legacy inquiry fields are not accepted: ${legacyFields.join(", ")}. Use driver, vehicleOwner, thirdPartyPolicyholder, carBodyPolicyholder, and vehicle.`,
);
}
}
export function assertPreviousPlateInquiryMatchesVin(
expectedVin: string,
mapped: Record<string, any>,
@@ -517,7 +512,11 @@ export function normalizeInquirySubmission<T extends Record<string, any>>(
input: T,
options: InquiryParticipantResolutionOptions = {},
): NormalizedInquirySubmission<T> {
const participants = resolveInquiryParticipants(caseType, input, options);
const participants = resolveInquiryParticipants(
caseType,
input as any,
options,
);
const driver = participantForRole(
participants,
InquiryParticipantRole.DRIVER,
@@ -539,43 +538,32 @@ export function normalizeInquirySubmission<T extends Record<string, any>>(
participants,
InquiryParticipantRole.CAR_BODY_POLICYHOLDER,
);
const vehicle = input.vehicle
? resolveInquiryVehicle(input.vehicle as InquiryVehicleInputDto)
: undefined;
if (
vehicle &&
input.plate &&
normalizePlateForComparison(vehicle.currentPlate) !==
normalizePlateForComparison(input.plate)
) {
throw new BadRequestException(
"plate and vehicle.currentPlate must identify the same current plate.",
);
}
const vehicle = resolveInquiryVehicle(
input.vehicle as InquiryVehicleInputDto,
);
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 } : {}),
plate: vehicle.currentPlate,
vin: vehicle.vin,
nationalCodeOfDriver: driver.nationalCode,
driverBirthday: driver.birthday,
driverLicense: driver.licenseNumber,
licenseType: driver.licenseType,
userNoCertificate:
driver.hasDrivingLicense == null
? input.userNoCertificate
? undefined
: !driver.hasDrivingLicense,
nationalCodeOfInsurer: thirdPartyPolicyholder.nationalCode,
insurerBirthday: thirdPartyPolicyholder.birthday,
driverIsInsurer: sameDriverAndPolicyholder,
insurerLicense: sameDriverAndPolicyholder
? driver.licenseNumber
: input.insurerLicense,
: undefined,
isNewCar: vehicle.isNewCar,
} as NormalizedInquirySubmission<T>["dto"],
participants,
driver,