diff --git a/src/case-expert-report/case-expert-report.builder.spec.ts b/src/case-expert-report/case-expert-report.builder.spec.ts index 5bc0058..5734b2f 100644 --- a/src/case-expert-report/case-expert-report.builder.spec.ts +++ b/src/case-expert-report/case-expert-report.builder.spec.ts @@ -483,6 +483,7 @@ describe("buildInsurerFileReport", () => { nationalCode: "0012345678", birthday: "1370/01/01", hasDrivingLicense: true, + driverLicenseDate: "1400/01/01", licenseNumber: "123456789", licenseType: "BASE_2", }, diff --git a/src/case-expert-report/case-expert-report.builder.ts b/src/case-expert-report/case-expert-report.builder.ts index a9bcd32..2c4c490 100644 --- a/src/case-expert-report/case-expert-report.builder.ts +++ b/src/case-expert-report/case-expert-report.builder.ts @@ -732,7 +732,10 @@ function buildDriverSection( }, { label: PR.licenseDate, - value: licenseDate ?? asString(person.driverLicense), + value: + asString(person.driverLicenseDate) || + licenseDate || + asString(person.driverLicense), }, { label: PR.phone, value: asString(person.phoneNumber) }, { label: PR.nationalCode, value: asString(person.nationalCodeOfDriver) }, diff --git a/src/claim-request-management/claim-request-management.service.ts b/src/claim-request-management/claim-request-management.service.ts index 5ccb1ab..ecea466 100644 --- a/src/claim-request-management/claim-request-management.service.ts +++ b/src/claim-request-management/claim-request-management.service.ts @@ -4870,7 +4870,8 @@ export class ClaimRequestManagementService { LicenceCityId: null, LicenceCountryId: null, LicenceForeignCityName: null, - LicenceIssuDate: person.driverBirthday ?? "1394/10/13", + LicenceIssuDate: + person.driverLicenseDate ?? person.driverBirthday ?? "1394/10/13", // Must never be empty/null; prefer stored DB value, else use dummy. LicenceNo: this.resolveFanavaranLicenceNoFromParty({ driverIsInsurer: person.driverIsInsurer, @@ -12443,6 +12444,7 @@ export class ClaimRequestManagementService { currentStep: c.workflow?.currentStep || "", createdAt: c.createdAt, blameRequestId: c.blameRequestId?.toString(), + partyIdentities: c.parties ?? c.snapshot?.parties, blameType: blameForItem?.type ?? undefined, creationMethod: blameForItem?.creationMethod ?? undefined, unifiedFileStatus: resolveUnifiedFileStatus({ diff --git a/src/common/dto/inquiry-participants.dto.ts b/src/common/dto/inquiry-participants.dto.ts index db53ff5..d099f7c 100644 --- a/src/common/dto/inquiry-participants.dto.ts +++ b/src/common/dto/inquiry-participants.dto.ts @@ -81,6 +81,14 @@ export class InquiryParticipantInputDto { @IsOptional() @IsString() licenseType?: string; + + @ApiPropertyOptional({ + description: + "Driver licence issue date. Required only for a driver who has a licence; Jalali YYYY/MM/DD is recommended.", + example: "1400/01/15", + }) + @IsOptional() + driverLicenseDate?: string | number; } export class InquiryVehicleInputDto { @@ -168,6 +176,9 @@ export class InquiryParticipantFieldsDto { @ApiHideProperty() driverLicense?: string; + @ApiHideProperty() + driverLicenseDate?: string; + @ApiHideProperty() licenseType?: string; diff --git a/src/common/dto/list-query-v2.dto.ts b/src/common/dto/list-query-v2.dto.ts index c8c9da9..67c7eb9 100644 --- a/src/common/dto/list-query-v2.dto.ts +++ b/src/common/dto/list-query-v2.dto.ts @@ -86,6 +86,26 @@ export class ListQueryV2Dto { @MaxLength(64) search?: string; + @ApiPropertyOptional({ + description: "Filter cases by a phone number found on any party.", + example: "09121234567", + maxLength: 32, + }) + @IsOptional() + @IsString() + @MaxLength(32) + phoneNumber?: string; + + @ApiPropertyOptional({ + description: "Filter cases by a national code found on any party.", + example: "0012345678", + maxLength: 32, + }) + @IsOptional() + @IsString() + @MaxLength(32) + nationalCode?: string; + @ApiPropertyOptional({ enum: UNIFIED_FILE_STATUS_KEYS, description: diff --git a/src/expert-claim/expert-claim.service.ts b/src/expert-claim/expert-claim.service.ts index 847aa11..ccef351 100644 --- a/src/expert-claim/expert-claim.service.ts +++ b/src/expert-claim/expert-claim.service.ts @@ -778,6 +778,7 @@ export class ExpertClaimService { creationMethod?: string; carBodyFirstForm?: { car?: boolean; object?: boolean }; blameStatus?: string; + partyIdentities?: Array>; } { if (!blame?.type) return {}; const blameRequestType = blame.type as BlameRequestType; @@ -786,12 +787,26 @@ export class ExpertClaimService { creationMethod?: string; carBodyFirstForm?: { car?: boolean; object?: boolean }; blameStatus?: string; + partyIdentities?: Array>; } = { blameRequestType }; if (blame.creationMethod) out.creationMethod = blame.creationMethod; out.blameStatus = blame.blameStatus; - if (blameRequestType !== BlameRequestType.CAR_BODY) return out; - const parties = blame.parties; + if (Array.isArray(parties) && parties.length > 0) { + out.partyIdentities = parties.map((party: any) => ({ + phoneNumber: party?.person?.phoneNumber, + nationalCode: party?.person?.nationalCode, + nationalCodeOfInsurer: party?.person?.nationalCodeOfInsurer, + nationalCodeOfDriver: party?.person?.nationalCodeOfDriver, + inquiryParticipants: Array.isArray(party?.inquiryParticipants) + ? party.inquiryParticipants.map((participant: any) => ({ + phoneNumber: participant?.phoneNumber, + nationalCode: participant?.nationalCode, + })) + : undefined, + })); + } + if (blameRequestType !== BlameRequestType.CAR_BODY) return out; if (!Array.isArray(parties) || parties.length === 0) return out; const first = @@ -4393,6 +4408,8 @@ export class ExpertClaimService { ? { carName: v.carName, carModel: v.carModel, carType: v.carType } : undefined, ...fileCtx, + partyIdentities: + fileCtx.partyIdentities ?? c.parties ?? c.snapshot?.parties, blameRequestId: c.blameRequestId?.toString(), createdAt: c.createdAt, awaitingFactorValidation, @@ -4486,6 +4503,8 @@ export class ExpertClaimService { ? { carName: v.carName, carModel: v.carModel, carType: v.carType } : undefined, ...fileCtx, + partyIdentities: + fileCtx.partyIdentities ?? c.parties ?? c.snapshot?.parties, blameRequestId: c.blameRequestId?.toString(), createdAt: c.createdAt, awaitingFactorValidation: claimIsAwaitingExpertFactorValidationV2(c), @@ -4625,6 +4644,8 @@ export class ExpertClaimService { ? { carName: v.carName, carModel: v.carModel, carType: v.carType } : undefined, ...fileCtx, + partyIdentities: + fileCtx.partyIdentities ?? c.parties ?? c.snapshot?.parties, blameRequestId: c.blameRequestId?.toString(), createdAt: c.createdAt, awaitingFactorValidation: claimIsAwaitingExpertFactorValidationV2(c), @@ -4720,6 +4741,8 @@ export class ExpertClaimService { ? { carName: v.carName, carModel: v.carModel, carType: v.carType } : undefined, ...fileCtx, + partyIdentities: + fileCtx.partyIdentities ?? c.parties ?? c.snapshot?.parties, blameRequestId: c.blameRequestId?.toString(), createdAt: c.createdAt, awaitingFactorValidation: claimIsAwaitingExpertFactorValidationV2(c), diff --git a/src/helpers/list-query-v2.spec.ts b/src/helpers/list-query-v2.spec.ts index b7c8bc1..62ad1e1 100644 --- a/src/helpers/list-query-v2.spec.ts +++ b/src/helpers/list-query-v2.spec.ts @@ -26,6 +26,45 @@ describe("applyListQueryV2", () => { expect(r.list[0].publicId).toBe("A200"); }); + it("filters nested party identities and normalizes Persian digits", () => { + const identityRows = [ + { + publicId: "PHONE", + createdAt: "2026-01-01", + status: "OPEN", + parties: [ + { + person: { + phoneNumber: "0912-123-4567", + nationalCodeOfDriver: "۰۰۱۲۳۴۵۶۷۸", + }, + }, + ], + }, + { + publicId: "OTHER", + createdAt: "2026-01-02", + status: "OPEN", + parties: [{ person: { phoneNumber: "09351234567", nationalCode: "1111111111" } }], + }, + ]; + const identityGetters = { + publicId: (r: (typeof identityRows)[0]) => r.publicId, + createdAt: (r: (typeof identityRows)[0]) => r.createdAt, + status: (r: (typeof identityRows)[0]) => r.status, + }; + + const byPhone = applyListQueryV2(identityRows, identityGetters, { + phoneNumber: "09121234567", + }); + expect(byPhone.list.map((row) => row.publicId)).toEqual(["PHONE"]); + + const byNationalCode = applyListQueryV2(identityRows, identityGetters, { + nationalCode: "0012345678", + }); + expect(byNationalCode.list.map((row) => row.publicId)).toEqual(["PHONE"]); + }); + it("sorts by publicId ascending", () => { const r = applyListQueryV2(rows, getters, { sortBy: "publicId", diff --git a/src/helpers/list-query-v2.ts b/src/helpers/list-query-v2.ts index 06a0fc6..e40d171 100644 --- a/src/helpers/list-query-v2.ts +++ b/src/helpers/list-query-v2.ts @@ -53,6 +53,48 @@ function normalizeSearch(raw?: string): string | undefined { return t || undefined; } +function normalizeIdentityValue(raw?: unknown): string { + return String(raw ?? "") + .trim() + .replace(/[۰-۹]/g, (digit) => String("۰۱۲۳۴۵۶۷۸۹".indexOf(digit))) + .replace(/[٠-٩]/g, (digit) => String("٠١٢٣٤٥٦٧٨٩".indexOf(digit))) + .replace(/\D/g, ""); +} + +function isPhoneKey(key: string): boolean { + return /phone|mobile|tel/i.test(key); +} + +function isNationalCodeKey(key: string): boolean { + return /national[_-]?(code|id)|code[_-]?mell?i|mell?i[_-]?code/i.test(key); +} + +function collectIdentityValues( + value: unknown, + matchKey: (key: string) => boolean, +): string[] { + const values: string[] = []; + const seen = new WeakSet(); + const visit = (current: unknown, depth: number) => { + if (current == null || depth > 8 || typeof current !== "object") return; + const object = current as Record; + if (seen.has(object)) return; + seen.add(object); + for (const [key, child] of Object.entries(object)) { + if ( + matchKey(key) && + (typeof child === "string" || typeof child === "number") + ) { + const normalized = normalizeIdentityValue(child); + if (normalized) values.push(normalized); + } + if (child && typeof child === "object") visit(child, depth + 1); + } + }; + visit(value, 0); + return values; +} + function toTime(v: Date | string | number | undefined): number { if (v == null) return 0; const n = v instanceof Date ? v.getTime() : new Date(v as string | number).getTime(); @@ -75,12 +117,28 @@ export function applyListQueryV2( totalPages?: number; } { const search = normalizeSearch(query.search); + const phoneNumber = normalizeIdentityValue(query.phoneNumber); + const nationalCode = normalizeIdentityValue(query.nationalCode); let filtered = rows; if (query.fileType && getters.fileType) { filtered = filtered.filter( (row) => getters.fileType!(row) === query.fileType, ); } + if (phoneNumber) { + filtered = filtered.filter((row) => + collectIdentityValues(row, isPhoneKey).some((value) => + value.includes(phoneNumber), + ), + ); + } + if (nationalCode) { + filtered = filtered.filter((row) => + collectIdentityValues(row, isNationalCodeKey).some((value) => + value.includes(nationalCode), + ), + ); + } if (search) { filtered = filtered.filter((row) => { const chunks: string[] = []; diff --git a/src/request-management/dto/create-request-management.dto.ts b/src/request-management/dto/create-request-management.dto.ts index 97d9a16..ddb9639 100644 --- a/src/request-management/dto/create-request-management.dto.ts +++ b/src/request-management/dto/create-request-management.dto.ts @@ -1,5 +1,5 @@ import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; -import { IsNotEmpty, IsOptional, IsString } from "class-validator"; +import { 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"; @@ -51,8 +51,13 @@ export class FirstPartyFileDto { } export class DescriptionDto { - @ApiProperty({ description: "Accident description (required for all types)" }) - desc: string; + @ApiPropertyOptional({ + description: + "Accident description. Optional for THIRD_PARTY; CAR_BODY requires at least 100 non-whitespace characters.", + }) + @IsOptional() + @IsString() + desc?: string; @ApiPropertyOptional({ description: "CAR_BODY only. Ignored for THIRD_PARTY.", @@ -94,10 +99,13 @@ export class DescriptionDto { * front-end can display them consistently regardless of blame type. */ export class DescriptionV4Dto { - @ApiProperty({ description: "Accident description" }) + @ApiPropertyOptional({ + description: + "Accident description. Optional for THIRD_PARTY; CAR_BODY requires at least 100 non-whitespace characters.", + }) + @IsOptional() @IsString() - @IsNotEmpty() - desc: string; + desc?: string; @ApiPropertyOptional({ description: diff --git a/src/request-management/entities/schema/partyRole.enum.ts b/src/request-management/entities/schema/partyRole.enum.ts index b827092..ca1c9cd 100644 --- a/src/request-management/entities/schema/partyRole.enum.ts +++ b/src/request-management/entities/schema/partyRole.enum.ts @@ -23,6 +23,7 @@ export class Person { @Prop() nationalCodeOfDriver?: string; @Prop() insurerLicense?: string; @Prop() driverLicense?: string; + @Prop() driverLicenseDate?: string; /** Driving licence type code from the Fanavaran lookup (GET /lookups/driving-licence-types). */ @Prop() licenseType?: string; @@ -66,6 +67,7 @@ export class InquiryParticipant { @Prop({ type: Boolean }) hasDrivingLicense?: boolean; @Prop() licenseNumber?: string; @Prop() licenseType?: string; + @Prop() driverLicenseDate?: string; } export const InquiryParticipantSchema = SchemaFactory.createForClass(InquiryParticipant); diff --git a/src/request-management/entities/schema/request-management.schema.ts b/src/request-management/entities/schema/request-management.schema.ts index fcfbd07..53c24d3 100644 --- a/src/request-management/entities/schema/request-management.schema.ts +++ b/src/request-management/entities/schema/request-management.schema.ts @@ -53,6 +53,7 @@ export class CarDetail { insurerBirthday?: string; driverBirthday?: string | null; + driverLicenseDate?: string; } export class InsuranceDetail { diff --git a/src/request-management/inquiry-participant-persistence.spec.ts b/src/request-management/inquiry-participant-persistence.spec.ts index 77d5711..ab77da1 100644 --- a/src/request-management/inquiry-participant-persistence.spec.ts +++ b/src/request-management/inquiry-participant-persistence.spec.ts @@ -16,6 +16,7 @@ describe("inquiry participant persistence", () => { nationalCode: "0022324224", birthday: "1378/04/26", hasDrivingLicense: true, + driverLicenseDate: "1400/01/01", licenseNumber: "1001011111", licenseType: "10", }, @@ -60,6 +61,9 @@ describe("inquiry participant persistence", () => { expect(request.validateSync()).toBeUndefined(); expect(request.parties[0].person.insurerBirthday).toBe("1378/06/27"); expect(request.parties[0].person.driverBirthday).toBe("1378/04/26"); + expect(request.parties[0].participants?.[0].driverLicenseDate).toBe( + "1400/01/01", + ); }); it("recasts populated party subdocuments during an inquiry-audit update", () => { @@ -89,6 +93,7 @@ describe("inquiry participant persistence", () => { nationalCode: "0022324224", birthday: "1378/04/26", hasDrivingLicense: true, + driverLicenseDate: "1400/01/01", licenseNumber: "1001011111", licenseType: "10", }, diff --git a/src/request-management/inquiry-participant-resolver.spec.ts b/src/request-management/inquiry-participant-resolver.spec.ts index b807e18..a6f369b 100644 --- a/src/request-management/inquiry-participant-resolver.spec.ts +++ b/src/request-management/inquiry-participant-resolver.spec.ts @@ -14,15 +14,60 @@ import { resolveInquiryVehicle, runCurrentPlateInquiry, sanitizeStoredInquiryParticipants, + validateDriverLicenseDate, } from "./inquiry-participant-resolver"; describe("inquiry participant resolver", () => { + it("accepts a driver licence date within the ten-year window", () => { + expect(validateDriverLicenseDate("2016-09-26", "2026-09-26")).toBe( + "2016-09-26", + ); + }); + + it("rejects future and older-than-ten-year driver licence dates", () => { + expect(() => + validateDriverLicenseDate("2026-09-27", "2026-09-26"), + ).toThrow("نمی‌تواند در آینده"); + expect(() => + validateDriverLicenseDate("2016-09-25", "2026-09-26"), + ).toThrow("بیشتر از ۱۰ سال قبل"); + }); + + it("requires the date only for a licensed driver", () => { + expect(() => + resolveInquiryParticipants(BlameRequestType.THIRD_PARTY, { + driver: { + nationalCode: "0012345678", + birthday: "1370/01/01", + hasDrivingLicense: true, + licenseNumber: "123456789", + licenseType: "1", + }, + vehicleOwner: { sameAs: InquiryParticipantRole.DRIVER }, + thirdPartyPolicyholder: { sameAs: InquiryParticipantRole.DRIVER }, + }), + ).toThrow("تاریخ صدور گواهینامه"); + + expect(() => + resolveInquiryParticipants(BlameRequestType.THIRD_PARTY, { + driver: { + nationalCode: "0012345678", + birthday: "1370/01/01", + hasDrivingLicense: false, + }, + vehicleOwner: { sameAs: InquiryParticipantRole.DRIVER }, + thirdPartyPolicyholder: { sameAs: InquiryParticipantRole.DRIVER }, + }), + ).not.toThrow(); + }); + it("links several roles to one explicitly shared person", () => { const resolved = resolveInquiryParticipants(BlameRequestType.THIRD_PARTY, { driver: { nationalCode: "0012345678", birthday: "1370/01/01", hasDrivingLicense: true, + driverLicenseDate: "1400/01/01", licenseNumber: "123456789", licenseType: "1", }, @@ -231,6 +276,7 @@ describe("inquiry participant resolver", () => { nationalCode: "0012345678", birthday: "1370/01/01", hasDrivingLicense: true, + driverLicenseDate: "1400/01/01", licenseNumber: "D-1", licenseType: "1", }, diff --git a/src/request-management/inquiry-participant-resolver.ts b/src/request-management/inquiry-participant-resolver.ts index de9e8e0..1a336c3 100644 --- a/src/request-management/inquiry-participant-resolver.ts +++ b/src/request-management/inquiry-participant-resolver.ts @@ -18,6 +18,7 @@ export interface ResolvedInquiryParticipant { hasDrivingLicense?: boolean; licenseNumber?: string; licenseType?: string; + driverLicenseDate?: string; } export interface InquiryParticipantRoleAssignments { @@ -38,6 +39,7 @@ export interface NormalizedInquirySubmission> { nationalCodeOfDriver: string; driverBirthday: string; driverLicense?: string; + driverLicenseDate?: string; licenseType?: string; userNoCertificate?: boolean; nationalCodeOfInsurer: string; @@ -215,6 +217,29 @@ function requiredIdentity( "شماره و نوع گواهینامه برای راننده دارای گواهینامه الزامی است.", ); } + if ( + role !== InquiryParticipantRole.DRIVER && + Object.prototype.hasOwnProperty.call(input, "driverLicenseDate") + ) { + throw new BadRequestException( + "تاریخ صدور گواهینامه فقط برای راننده قابل ثبت است.", + ); + } + const driverLicenseDate = + role === InquiryParticipantRole.DRIVER && input.hasDrivingLicense === true + ? validateDriverLicenseDate(input.driverLicenseDate) + : undefined; + if ( + role === InquiryParticipantRole.DRIVER && + input.hasDrivingLicense === false && + Object.prototype.hasOwnProperty.call(input, "driverLicenseDate") && + input.driverLicenseDate != null && + String(input.driverLicenseDate).trim() !== "" + ) { + throw new BadRequestException( + "تاریخ صدور گواهینامه برای راننده فاقد گواهینامه قابل ثبت نیست.", + ); + } return { participantId: role, nationalCode, @@ -225,9 +250,38 @@ function requiredIdentity( : {}), ...(input.licenseNumber ? { licenseNumber: input.licenseNumber } : {}), ...(input.licenseType ? { licenseType: input.licenseType } : {}), + ...(driverLicenseDate ? { driverLicenseDate } : {}), }; } +/** Validate a driver licence issue date against the current Iran date. */ +export function validateDriverLicenseDate( + value: unknown, + todayGregorian: string = gregorianDateInIran(new Date()), +): string { + const raw = String(value ?? "").trim(); + const parsed = jalaliToGregorianDate(raw); + if (!parsed) { + throw new BadRequestException("تاریخ صدور گواهینامه راننده نامعتبر است."); + } + const today = jalaliToGregorianDate(todayGregorian) ?? todayGregorian; + const todayDate = new Date(`${today}T00:00:00Z`); + const minimumDate = new Date(todayDate); + minimumDate.setUTCFullYear(minimumDate.getUTCFullYear() - 10); + const minimum = minimumDate.toISOString().slice(0, 10); + if (parsed > today) { + throw new BadRequestException( + "تاریخ صدور گواهینامه راننده نمی‌تواند در آینده باشد.", + ); + } + if (parsed < minimum) { + throw new BadRequestException( + "تاریخ صدور گواهینامه راننده نباید بیشتر از ۱۰ سال قبل باشد.", + ); + } + return raw; +} + export function resolveInquiryParticipants( caseType: BlameRequestType, input: Partial & Record, @@ -413,6 +467,7 @@ const LEGACY_INQUIRY_FIELDS = [ "nationalCodeOfDriver", "driverBirthday", "driverLicense", + "driverLicenseDate", "licenseType", "nationalCodeOfInsurer", "insurerBirthday", @@ -555,6 +610,7 @@ export function normalizeInquirySubmission>( nationalCodeOfDriver: driver.nationalCode, driverBirthday: driver.birthday, driverLicense: driver.licenseNumber, + driverLicenseDate: driver.driverLicenseDate, licenseType: driver.licenseType, userNoCertificate: driver.hasDrivingLicense == null diff --git a/src/request-management/request-management.service.ts b/src/request-management/request-management.service.ts index b707401..82de477 100644 --- a/src/request-management/request-management.service.ts +++ b/src/request-management/request-management.service.ts @@ -211,6 +211,19 @@ export class RequestManagementService { } /** Keep eligibility rejections distinct from an unavailable CAR_BODY provider. */ + private assertCarBodyDescriptionMinimum( + requestType: BlameRequestType | string | undefined, + description: unknown, + ): void { + if (requestType !== BlameRequestType.CAR_BODY) return; + const length = String(description ?? "").trim().length; + if (length < 100) { + throw new BadRequestException( + "CAR_BODY description must contain at least 100 non-whitespace characters.", + ); + } + } + private throwCarBodyInquiryFailure( err: unknown, context: "carBodyPlate" | "carBodyVin" = "carBodyPlate", @@ -2021,6 +2034,7 @@ export class RequestManagementService { party.person.nationalCodeOfDriver = body.nationalCodeOfDriver; party.person.insurerLicense = body.insurerLicense; party.person.driverLicense = body.driverLicense; + party.person.driverLicenseDate = body.driverLicenseDate; party.person.driverIsInsurer = body.driverIsInsurer; party.person.isNewCar = body.isNewCar; party.person.userNoCertificate = body.userNoCertificate; @@ -2448,6 +2462,7 @@ export class RequestManagementService { party.person.nationalCodeOfDriver = body.nationalCodeOfDriver; party.person.insurerLicense = body.insurerLicense; party.person.driverLicense = body.driverLicense; + party.person.driverLicenseDate = body.driverLicenseDate; party.person.driverIsInsurer = body.driverIsInsurer; party.person.isNewCar = body.isNewCar; party.person.userNoCertificate = body.userNoCertificate; @@ -2817,6 +2832,8 @@ export class RequestManagementService { this.isBlameOnBehalfActor(req, user), ); + this.assertCarBodyDescriptionMinimum(req.type, body.desc); + // Set userId for party if not already set (important for second party) if (!party.person) party.person = {} as any; if ( @@ -4120,6 +4137,8 @@ export class RequestManagementService { throw new NotFoundException("Request not found"); } + this.assertCarBodyDescriptionMinimum(request.type, body.desc); + // --- First Party Flow --- if (request.firstPartyDetails.firstPartyId == user.sub) { // Build base update payload with proper MongoDB operators @@ -6611,11 +6630,26 @@ export class RequestManagementService { ); const obj = req.toObject(); + const partyIdentities = Array.isArray(req.parties) + ? req.parties.map((entry: any) => ({ + phoneNumber: entry?.person?.phoneNumber, + nationalCode: entry?.person?.nationalCode, + nationalCodeOfInsurer: entry?.person?.nationalCodeOfInsurer, + nationalCodeOfDriver: entry?.person?.nationalCodeOfDriver, + inquiryParticipants: Array.isArray(entry?.inquiryParticipants) + ? entry.inquiryParticipants.map((participant: any) => ({ + phoneNumber: participant?.phoneNumber, + nationalCode: participant?.nationalCode, + })) + : undefined, + })) + : []; delete obj.parties; return { ...obj, + partyIdentities, userSide: party?.role ?? null, initiatedByMe: isInitiator, unifiedFileStatus: resolveUnifiedFileStatus({ @@ -7091,9 +7125,10 @@ export class RequestManagementService { if (!formData.carBodyForm) { throw new BadRequestException("carBodyForm is required."); } - if (!formData?.expertDescription?.desc) { - throw new BadRequestException("expertDescription.desc is required."); - } + this.assertCarBodyDescriptionMinimum( + req.type, + formData?.expertDescription?.desc, + ); const firstPartyUserId = await this.getOrCreateUserByPhoneNumber( formData.firstPartyPhoneNumber, @@ -7272,6 +7307,7 @@ export class RequestManagementService { nationalCodeOfDriver: firstPartyPlate.nationalCodeOfDriver, insurerLicense: firstPartyPlate.insurerLicense, driverLicense: firstPartyPlate.driverLicense, + driverLicenseDate: firstPartyPlate.driverLicenseDate, driverIsInsurer: firstPartyPlate.driverIsInsurer, isNewCar: firstPartyPlate.isNewCar, userNoCertificate: firstPartyPlate.userNoCertificate, @@ -7560,6 +7596,7 @@ export class RequestManagementService { nationalCodeOfDriver: plateDto.nationalCodeOfDriver, insurerLicense: plateDto.insurerLicense, driverLicense: plateDto.driverLicense, + driverLicenseDate: plateDto.driverLicenseDate, driverIsInsurer: plateDto.driverIsInsurer, isNewCar: plateDto.isNewCar, userNoCertificate: plateDto.userNoCertificate, @@ -8286,6 +8323,7 @@ export class RequestManagementService { carName: sandHubReport?.MapTypNam || sandHubReport?.CarName, insurerBirthday: firstPartyPlate.insurerBirthday, driverBirthday: firstPartyPlate.driverBirthday, + driverLicenseDate: firstPartyPlate.driverLicenseDate, carType: `${sandHubReport?.UsageField || ""} / ${sandHubReport?.MapUsageName || "-"}`, isNewCar: firstPartyPlate.isNewCar, }; @@ -8632,6 +8670,11 @@ export class RequestManagementService { ); } + this.assertCarBodyDescriptionMinimum( + request.type, + formData.firstPartyDescription?.desc, + ); + this.assertAccidentDateTimeNotInFuture({ accidentDate: formData.firstPartyDescription?.accidentDate, accidentTime: formData.firstPartyDescription?.accidentTime, @@ -8772,6 +8815,7 @@ export class RequestManagementService { carName: sandHubReport?.MapTypNam || sandHubReport?.CarName, insurerBirthday: firstPartyPlate.insurerBirthday, driverBirthday: firstPartyPlate.driverBirthday, + driverLicenseDate: firstPartyPlate.driverLicenseDate, carType: `${sandHubReport?.UsageField || ""} / ${sandHubReport?.MapUsageName || "-"}`, isNewCar: firstPartyPlate.isNewCar, }; @@ -10107,6 +10151,8 @@ export class RequestManagementService { party.person.insurerLicense = partyData.insurerLicense; if (partyData.driverLicense) party.person.driverLicense = partyData.driverLicense; + if ((partyData as any).driverLicenseDate) + party.person.driverLicenseDate = (partyData as any).driverLicenseDate; if ((partyData as any).licenseType) (party.person as any).licenseType = (partyData as any).licenseType; @@ -11015,6 +11061,8 @@ export class RequestManagementService { party.person.insurerLicense = partyData.insurerLicense; if (partyData.driverLicense) party.person.driverLicense = partyData.driverLicense; + if ((partyData as any).driverLicenseDate) + party.person.driverLicenseDate = (partyData as any).driverLicenseDate; if ((partyData as any).licenseType) (party.person as any).licenseType = (partyData as any).licenseType; @@ -11829,6 +11877,7 @@ export class RequestManagementService { await this.verifyExpertAccessForBlameV2(req, actor); const role = this.resolvePartyRoleV3(req, partyRole); this.assertBlameV3PartyDetailPhase(req, role); + this.assertCarBodyDescriptionMinimum(req.type, body.desc); // accidentDate and accidentTime are only required for the first (guilty) // party — the second (damaged) party can skip them. @@ -13198,6 +13247,7 @@ export class RequestManagementService { driverBirthday: p.person?.driverBirthday, insurerLicense: p.person?.insurerLicense, driverLicense: p.person?.driverLicense, + driverLicenseDate: p.person?.driverLicenseDate, userId: p.person?.userId ? String(p.person.userId) : undefined, clientId: p.person?.clientId ? String(p.person.clientId) : undefined, licenseType: p.person?.licenseType ?? undefined, @@ -13440,6 +13490,7 @@ export class RequestManagementService { driverBirthday: p.person?.driverBirthday, insurerLicense: p.person?.insurerLicense, driverLicense: p.person?.driverLicense, + driverLicenseDate: p.person?.driverLicenseDate, userId: p.person?.userId ? String(p.person.userId) : undefined, clientId: p.person?.clientId ? String(p.person.clientId) : undefined, licenseType: p.person?.licenseType ?? undefined,