forked from Yara724/api
Merge pull request 'main' (#340) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#340
This commit is contained in:
@@ -483,6 +483,7 @@ describe("buildInsurerFileReport", () => {
|
||||
nationalCode: "0012345678",
|
||||
birthday: "1370/01/01",
|
||||
hasDrivingLicense: true,
|
||||
driverLicenseDate: "1400/01/01",
|
||||
licenseNumber: "123456789",
|
||||
licenseType: "BASE_2",
|
||||
},
|
||||
|
||||
@@ -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) },
|
||||
|
||||
@@ -170,6 +170,7 @@ import { RoleEnum } from "src/Types&Enums/role.enum";
|
||||
import { PartyRole } from "src/request-management/entities/schema/partyRole.enum";
|
||||
import { InquiryParticipantRole } from "src/common/dto/inquiry-participants.dto";
|
||||
import { participantForStoredPartyRole } from "src/request-management/inquiry-participant-resolver";
|
||||
import type { InquiryParticipantRoleAssignments } from "src/request-management/inquiry-participant-resolver";
|
||||
import { UserRatingDto } from "./dto/user-rating.dto";
|
||||
import {
|
||||
canFinalizeExpertResend,
|
||||
@@ -5905,7 +5906,7 @@ export class ClaimRequestManagementService {
|
||||
role?: PartyRole;
|
||||
person?: { userId?: Types.ObjectId; nationalCodeOfInsurer?: string };
|
||||
participants?: Array<Record<string, any>>;
|
||||
participantRoles?: Record<string, string | undefined>;
|
||||
participantRoles?: Partial<InquiryParticipantRoleAssignments>;
|
||||
statement?: { admitsGuilt?: boolean };
|
||||
}>,
|
||||
guiltyPartyId?: Types.ObjectId | string | null,
|
||||
@@ -12560,6 +12561,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({
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
type FanavaranCarPolicyProduct,
|
||||
} from "src/lookups/fanavaran-last-car-policy";
|
||||
import { resolvePolicyholderFromParty } from "./fanavaran-party-roles";
|
||||
import type { InquiryParticipantRoleAssignments } from "src/request-management/inquiry-participant-resolver";
|
||||
|
||||
export type FanavaranClaimProduct = FanavaranCarPolicyProduct;
|
||||
|
||||
@@ -103,7 +104,7 @@ type BlamePartyForCarBodyPolicy = {
|
||||
role?: string;
|
||||
person?: { nationalCodeOfInsurer?: unknown };
|
||||
participants?: Array<Record<string, any>>;
|
||||
participantRoles?: Record<string, string | undefined>;
|
||||
participantRoles?: Partial<InquiryParticipantRoleAssignments>;
|
||||
insurance?: {
|
||||
carBodyInsurance?: {
|
||||
policyId?: unknown;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { InquiryParticipantRole } from "src/common/dto/inquiry-participants.dto";
|
||||
import type { InquiryParticipantRoleAssignments } from "src/request-management/inquiry-participant-resolver";
|
||||
import { participantForStoredPartyRole } from "src/request-management/inquiry-participant-resolver";
|
||||
import {
|
||||
normalizeNationalCode,
|
||||
@@ -24,7 +25,7 @@ export type FanavaranRoleIdentity = {
|
||||
|
||||
type PartyLike = {
|
||||
participants?: Array<Record<string, any>>;
|
||||
participantRoles?: Record<string, string | undefined>;
|
||||
participantRoles?: Partial<InquiryParticipantRoleAssignments>;
|
||||
person?: Record<string, any> | null;
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -778,6 +778,7 @@ export class ExpertClaimService {
|
||||
creationMethod?: string;
|
||||
carBodyFirstForm?: { car?: boolean; object?: boolean };
|
||||
blameStatus?: string;
|
||||
partyIdentities?: Array<Record<string, unknown>>;
|
||||
} {
|
||||
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<Record<string, unknown>>;
|
||||
} = { 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),
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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<object>();
|
||||
const visit = (current: unknown, depth: number) => {
|
||||
if (current == null || depth > 8 || typeof current !== "object") return;
|
||||
const object = current as Record<string, unknown>;
|
||||
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<T>(
|
||||
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[] = [];
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -53,6 +53,7 @@ export class CarDetail {
|
||||
insurerBirthday?: string;
|
||||
|
||||
driverBirthday?: string | null;
|
||||
driverLicenseDate?: string;
|
||||
}
|
||||
|
||||
export class InsuranceDetail {
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
|
||||
@@ -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<T extends Record<string, any>> {
|
||||
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<InquiryParticipantFieldsDto> & Record<string, any>,
|
||||
@@ -413,6 +467,7 @@ const LEGACY_INQUIRY_FIELDS = [
|
||||
"nationalCodeOfDriver",
|
||||
"driverBirthday",
|
||||
"driverLicense",
|
||||
"driverLicenseDate",
|
||||
"licenseType",
|
||||
"nationalCodeOfInsurer",
|
||||
"insurerBirthday",
|
||||
@@ -555,6 +610,7 @@ export function normalizeInquirySubmission<T extends Record<string, any>>(
|
||||
nationalCodeOfDriver: driver.nationalCode,
|
||||
driverBirthday: driver.birthday,
|
||||
driverLicense: driver.licenseNumber,
|
||||
driverLicenseDate: driver.driverLicenseDate,
|
||||
licenseType: driver.licenseType,
|
||||
userNoCertificate:
|
||||
driver.hasDrivingLicense == null
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user