From 5b77241f2894d5eeaa133cd302428738cfd73a52 Mon Sep 17 00:00:00 2001 From: SepehrYahyaee <7heycallmegray@gmail.com> Date: Sun, 13 Sep 2026 14:13:05 +0330 Subject: [PATCH] Require structured inquiry participant inputs --- docs/inquiry-participants-proposal.fa.md | 13 +- docs/inquiry-participants-proposal.md | 13 +- src/common/dto/inquiry-participants.dto.ts | 80 ++++++-- src/profile/dto/user/AddPlateDto.ts | 56 +----- .../dto/create-request-management.dto.ts | 74 +------ .../dto/run-call-center-inquiry-v6.dto.ts | 172 +--------------- .../dto/run-inquiries-v3.dto.ts | 190 +----------------- .../inquiry-participant-resolver.spec.ts | 54 +++-- .../inquiry-participant-resolver.ts | 112 +++++------ 9 files changed, 162 insertions(+), 602 deletions(-) diff --git a/docs/inquiry-participants-proposal.fa.md b/docs/inquiry-participants-proposal.fa.md index 7185953..1feeb48 100644 --- a/docs/inquiry-participants-proposal.fa.md +++ b/docs/inquiry-participants-proposal.fa.md @@ -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` را با خطای اعتبارسنجی رد می‌کند. احراز هویت تلفنی و جریان‌های تماس با طرفین از جمع‌آوری هویت برای استعلام جدا هستند. ## تصمیم پیشنهادی diff --git a/docs/inquiry-participants-proposal.md b/docs/inquiry-participants-proposal.md index 594e498..17a8623 100644 --- a/docs/inquiry-participants-proposal.md +++ b/docs/inquiry-participants-proposal.md @@ -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 diff --git a/src/common/dto/inquiry-participants.dto.ts b/src/common/dto/inquiry-participants.dto.ts index dc19876..3cc97c2 100644 --- a/src/common/dto/inquiry-participants.dto.ts +++ b/src/common/dto/inquiry-participants.dto.ts @@ -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; } diff --git a/src/profile/dto/user/AddPlateDto.ts b/src/profile/dto/user/AddPlateDto.ts index 3e9f8bd..1431c24 100644 --- a/src/profile/dto/user/AddPlateDto.ts +++ b/src/profile/dto/user/AddPlateDto.ts @@ -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; diff --git a/src/request-management/dto/create-request-management.dto.ts b/src/request-management/dto/create-request-management.dto.ts index 6d80bef..97d9a16 100644 --- a/src/request-management/dto/create-request-management.dto.ts +++ b/src/request-management/dto/create-request-management.dto.ts @@ -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() diff --git a/src/request-management/dto/run-call-center-inquiry-v6.dto.ts b/src/request-management/dto/run-call-center-inquiry-v6.dto.ts index 0b331f3..36e29f2 100644 --- a/src/request-management/dto/run-call-center-inquiry-v6.dto.ts +++ b/src/request-management/dto/run-call-center-inquiry-v6.dto.ts @@ -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 {} diff --git a/src/request-management/dto/run-inquiries-v3.dto.ts b/src/request-management/dto/run-inquiries-v3.dto.ts index 71e0922..b104050 100644 --- a/src/request-management/dto/run-inquiries-v3.dto.ts +++ b/src/request-management/dto/run-inquiries-v3.dto.ts @@ -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; } diff --git a/src/request-management/inquiry-participant-resolver.spec.ts b/src/request-management/inquiry-participant-resolver.spec.ts index 49c8541..c463ec3 100644 --- a/src/request-management/inquiry-participant-resolver.spec.ts +++ b/src/request-management/inquiry-participant-resolver.spec.ts @@ -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", diff --git a/src/request-management/inquiry-participant-resolver.ts b/src/request-management/inquiry-participant-resolver.ts index 439089b..1d7448a 100644 --- a/src/request-management/inquiry-participant-resolver.ts +++ b/src/request-management/inquiry-participant-resolver.ts @@ -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, + input: Partial & Record, 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): 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, @@ -517,7 +512,11 @@ export function normalizeInquirySubmission>( input: T, options: InquiryParticipantResolutionOptions = {}, ): NormalizedInquirySubmission { - 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>( 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["dto"], participants, driver,