import { BadRequestException } from "@nestjs/common"; import { BlameRequestType } from "src/Types&Enums/blame-request-management/blameRequestType.enum"; import { jalaliToGregorianDate } from "src/helpers/date-jalali"; import { gregorianDateInIran } from "src/helpers/iran-datetime"; import { InquiryParticipantFieldsDto, InquiryParticipantInputDto, InquiryParticipantRole, InquiryVehicleInputDto, VehicleRegistrationState, } from "src/common/dto/inquiry-participants.dto"; export interface ResolvedInquiryParticipant { participantId: string; nationalCode: string; birthday: string; fullName?: string; hasDrivingLicense?: boolean; licenseNumber?: string; licenseType?: string; } export interface InquiryParticipantRoleAssignments { driver: string; vehicleOwner?: string; thirdPartyPolicyholder: string; carBodyPolicyholder?: string; } export interface ResolvedInquiryParticipants { participants: ResolvedInquiryParticipant[]; roles: InquiryParticipantRoleAssignments; legacy: boolean; } export interface NormalizedInquirySubmission> { dto: T & { nationalCodeOfDriver: string; driverBirthday: string; driverLicense?: string; licenseType?: string; userNoCertificate?: boolean; nationalCodeOfInsurer: string; insurerBirthday: string; driverIsInsurer: boolean; insurerLicense?: string; }; participants: ResolvedInquiryParticipants; driver: ResolvedInquiryParticipant; vehicleOwner?: ResolvedInquiryParticipant; thirdPartyPolicyholder: ResolvedInquiryParticipant; carBodyPolicyholder?: ResolvedInquiryParticipant; vehicle?: ResolvedInquiryVehicle; } export interface InquirySubjects { thirdPartyPolicyNationalCode: string; carBodyPolicyNationalCode?: string; shebaNationalCode: string; driverNationalCode: string; } const PARTICIPANT_ROLE_LABELS: Record = { [InquiryParticipantRole.DRIVER]: "راننده", [InquiryParticipantRole.VEHICLE_OWNER]: "مالک خودرو", [InquiryParticipantRole.THIRD_PARTY_POLICYHOLDER]: "بیمه‌گذار شخص ثالث", [InquiryParticipantRole.CAR_BODY_POLICYHOLDER]: "بیمه‌گذار بدنه", }; const PLATE_FIELD_LABELS = { leftDigits: "دو رقم سمت چپ پلاک", centerAlphabet: "حرف پلاک", centerDigits: "سه رقم میانی پلاک", ir: "کد ایران پلاک", } as const; /** * The single routing seam for external inquiries. Policy checks belong to * their policyholder, Sheba belongs to the vehicle owner, and licence data * belongs to the driver even when those roles resolve to different people. */ export function resolveInquirySubjects( submission: NormalizedInquirySubmission>, ): InquirySubjects { if (!submission.vehicleOwner) { throw new BadRequestException( "اطلاعات هویتی مالک خودرو برای استعلام شبا الزامی است.", ); } return { thirdPartyPolicyNationalCode: submission.thirdPartyPolicyholder.nationalCode, ...(submission.carBodyPolicyholder ? { carBodyPolicyNationalCode: submission.carBodyPolicyholder.nationalCode, } : {}), shebaNationalCode: submission.vehicleOwner.nationalCode, driverNationalCode: submission.driver.nationalCode, }; } export type ResolvedInquiryVehicle = InquiryVehicleInputDto & { registrationState: VehicleRegistrationState; }; function assertCompleteInquiryPlate( plate: InquiryVehicleInputDto["currentPlate"], path: "vehicle.currentPlate" | "vehicle.previousPlate", ): void { for (const field of [ "leftDigits", "centerAlphabet", "centerDigits", "ir", ] as const) { if (plate?.[field] == null || String(plate[field]).trim() === "") { const plateLabel = path === "vehicle.currentPlate" ? "پلاک فعلی" : "پلاک قبلی"; throw new BadRequestException( `${PLATE_FIELD_LABELS[field]} در ${plateLabel} الزامی است.`, ); } } } export function participantForRole( resolved: ResolvedInquiryParticipants, role: InquiryParticipantRole, ): ResolvedInquiryParticipant | undefined { const roleField = ROLE_FIELDS[ role ] as keyof InquiryParticipantRoleAssignments; const participantId = resolved.roles[roleField]; return resolved.participants.find( (participant) => participant.participantId === participantId, ); } /** Resolve a participant from the role links persisted on a blame party. */ export function participantForStoredPartyRole( party: { participants?: Array>; participantRoles?: Partial; }, role: InquiryParticipantRole, ): Record | undefined { const roleField = ROLE_FIELDS[ role ] as keyof InquiryParticipantRoleAssignments; const participantId = party.participantRoles?.[roleField]; if (!participantId || !Array.isArray(party.participants)) return undefined; return party.participants.find( (participant) => participant?.participantId === participantId, ); } /** Remove fields retired from the participant contract from historical records. */ export function sanitizeStoredInquiryParticipants( participants: unknown, ): Array> | undefined { if (!Array.isArray(participants)) return undefined; return participants.map((participant) => { const plain = participant && typeof participant === "object" && typeof (participant as { toObject?: unknown }).toObject === "function" ? (participant as { toObject: () => Record }).toObject() : { ...(participant as Record) }; delete plain.unknown; return plain; }); } const ROLE_FIELDS: Record< InquiryParticipantRole, keyof InquiryParticipantFieldsDto > = { [InquiryParticipantRole.DRIVER]: "driver", [InquiryParticipantRole.VEHICLE_OWNER]: "vehicleOwner", [InquiryParticipantRole.THIRD_PARTY_POLICYHOLDER]: "thirdPartyPolicyholder", [InquiryParticipantRole.CAR_BODY_POLICYHOLDER]: "carBodyPolicyholder", }; function requiredIdentity( role: InquiryParticipantRole, input: InquiryParticipantInputDto, ): ResolvedInquiryParticipant { if (Object.prototype.hasOwnProperty.call(input, "phoneNumber")) { throw new BadRequestException( `شماره همراه ${PARTICIPANT_ROLE_LABELS[role]} باید جدا از اطلاعات هویتی استعلام ارسال شود.`, ); } const nationalCode = String(input.nationalCode ?? "").trim(); const birthday = String(input.birthday ?? "").trim(); if (!nationalCode || !birthday) { throw new BadRequestException( `کد ملی و تاریخ تولد ${PARTICIPANT_ROLE_LABELS[role]} الزامی است.`, ); } if ( role === InquiryParticipantRole.DRIVER && typeof input.hasDrivingLicense !== "boolean" ) { throw new BadRequestException("وضعیت داشتن گواهینامه راننده الزامی است."); } if ( role === InquiryParticipantRole.DRIVER && input.hasDrivingLicense === true && (!String(input.licenseNumber ?? "").trim() || !String(input.licenseType ?? "").trim()) ) { throw new BadRequestException( "شماره و نوع گواهینامه برای راننده دارای گواهینامه الزامی است.", ); } return { participantId: role, nationalCode, birthday, ...(input.fullName ? { fullName: input.fullName } : {}), ...(input.hasDrivingLicense != null ? { hasDrivingLicense: input.hasDrivingLicense } : {}), ...(input.licenseNumber ? { licenseNumber: input.licenseNumber } : {}), ...(input.licenseType ? { licenseType: input.licenseType } : {}), }; } export function resolveInquiryParticipants( caseType: BlameRequestType, input: Partial & Record, ): ResolvedInquiryParticipants { assertStructuredInquiryInput(input); const hasRoleCompleteInput = Object.values(ROLE_FIELDS).some( (field) => input[field] != null, ); if (!hasRoleCompleteInput) { throw new BadRequestException( "اطلاعات راننده، مالک خودرو و بیمه‌گذار شخص ثالث برای استعلام الزامی است.", ); } if ( caseType === BlameRequestType.THIRD_PARTY && input.carBodyPolicyholder != null ) { throw new BadRequestException( "بیمه‌گذار بدنه برای پرونده شخص ثالث قابل ثبت نیست.", ); } const requiredRoles = [ InquiryParticipantRole.DRIVER, InquiryParticipantRole.VEHICLE_OWNER, InquiryParticipantRole.THIRD_PARTY_POLICYHOLDER, ...(caseType === BlameRequestType.CAR_BODY ? [InquiryParticipantRole.CAR_BODY_POLICYHOLDER] : []), ]; const participants = new Map(); const resolvedRoleIds = new Map(); const resolving = new Set(); const resolveRole = (role: InquiryParticipantRole): string => { const existing = resolvedRoleIds.get(role); if (existing) return existing; if (resolving.has(role)) { throw new BadRequestException( "ارتباط اشخاص یکسان در اطلاعات استعلام نامعتبر است.", ); } const value = input[ROLE_FIELDS[role]] as | InquiryParticipantInputDto | undefined; if (!value) { throw new BadRequestException( `اطلاعات ${PARTICIPANT_ROLE_LABELS[role]} الزامی است.`, ); } resolving.add(role); let participantId: string; if (Object.prototype.hasOwnProperty.call(value, "unknown")) { throw new BadRequestException( `ثبت ${PARTICIPANT_ROLE_LABELS[role]} به‌صورت نامشخص امکان‌پذیر نیست.`, ); } else if (value.sameAs) { const hasPersonSpecificFields = Object.entries(value).some( ([field, fieldValue]) => field !== "sameAs" && fieldValue != null, ); if (hasPersonSpecificFields) { throw new BadRequestException( `برای ${PARTICIPANT_ROLE_LABELS[role]} باید فقط ارتباط با شخص دیگر یا اطلاعات هویتی مستقل ارسال شود.`, ); } participantId = resolveRole(value.sameAs); } else { const participant = requiredIdentity(role, value); const duplicate = [...participants.values()].find( (item) => item.nationalCode === participant.nationalCode, ); if (duplicate) { throw new BadRequestException( `کد ملی ${PARTICIPANT_ROLE_LABELS[role]} تکراری است؛ ارتباط با شخص ثبت‌شده را انتخاب کنید.`, ); } participants.set(participant.participantId, participant); participantId = participant.participantId; } resolving.delete(role); resolvedRoleIds.set(role, participantId); return participantId; }; for (const role of requiredRoles) resolveRole(role); return { participants: [...participants.values()], roles: { driver: resolvedRoleIds.get(InquiryParticipantRole.DRIVER)!, vehicleOwner: resolvedRoleIds.get(InquiryParticipantRole.VEHICLE_OWNER)!, thirdPartyPolicyholder: resolvedRoleIds.get( InquiryParticipantRole.THIRD_PARTY_POLICYHOLDER, )!, ...(caseType === BlameRequestType.CAR_BODY ? { carBodyPolicyholder: resolvedRoleIds.get( InquiryParticipantRole.CAR_BODY_POLICYHOLDER, )!, } : {}), }, legacy: false, }; } export function resolveInquiryVehicle( input: InquiryVehicleInputDto, ): ResolvedInquiryVehicle { if (!input) { throw new BadRequestException("اطلاعات خودرو برای استعلام الزامی است."); } const registrationState = input.registrationState ?? VehicleRegistrationState.CURRENT; if (!Object.values(VehicleRegistrationState).includes(registrationState)) { throw new BadRequestException( "وضعیت پلاک خودرو باید «فعلی» یا «تازه تعویض‌شده» باشد.", ); } if (input.isNewCar != null && typeof input.isNewCar !== "boolean") { throw new BadRequestException("وضعیت صفر بودن خودرو نامعتبر است."); } const previousPolicyholderNationalCode = String( input.previousPolicyholderNationalCode ?? "", ).trim(); if (!input.currentPlate) { throw new BadRequestException("پلاک فعلی خودرو برای استعلام الزامی است."); } assertCompleteInquiryPlate(input.currentPlate, "vehicle.currentPlate"); const vin = String(input.vin ?? "").trim(); if (vin && vin.length !== 17) { throw new BadRequestException( "شماره شاسی (VIN) باید دقیقاً ۱۷ کاراکتر باشد.", ); } if ( registrationState === VehicleRegistrationState.RECENTLY_TRANSFERRED && (!input.previousPlate || !vin || !previousPolicyholderNationalCode) ) { throw new BadRequestException( "برای خودروی تازه تعویض‌پلاک‌شده، پلاک قبلی، شماره شاسی (VIN) و کد ملی بیمه‌گذار قبلی الزامی است.", ); } if ( registrationState !== VehicleRegistrationState.RECENTLY_TRANSFERRED && (input.previousPlate || input.previousPolicyholderNationalCode != null) ) { throw new BadRequestException( "پلاک و کد ملی بیمه‌گذار قبلی فقط برای خودروی تازه تعویض‌پلاک‌شده قابل ثبت است.", ); } if (input.previousPlate) { assertCompleteInquiryPlate(input.previousPlate, "vehicle.previousPlate"); } return { ...input, registrationState, ...(vin ? { vin } : {}), ...(previousPolicyholderNationalCode ? { previousPolicyholderNationalCode } : {}), }; } /** A dated result is usable only while the returned policy has not expired. */ export function isMappedPolicyCurrent( mapped: Record, todayGregorian: string = gregorianDateInIran(new Date()), ): boolean { const rawEndDate = mapped?.EndDate ?? mapped?.HEndDte ?? mapped?.persianEndDate ?? mapped?.PolicyEndDate ?? mapped?.endDate; if (rawEndDate == null || String(rawEndDate).trim() === "") return true; const endDate = jalaliToGregorianDate(rawEndDate); return endDate != null && endDate >= todayGregorian; } 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( "ساختار قدیمی اطلاعات استعلام پذیرفته نمی‌شود؛ اطلاعات اشخاص و خودرو را در بخش‌های جدید ارسال کنید.", ); } } /** * Run a policy inquiry only for the submitted current plate. Recent-transfer * data is retained as case metadata, but must never trigger an inquiry for a * previous plate or a previous policyholder. */ export async function runCurrentPlateInquiry(options: { currentPlate: InquiryVehicleInputDto["currentPlate"]; vin?: string; query: (plate: InquiryVehicleInputDto["currentPlate"]) => Promise; isUsable: (value: T) => boolean; errorMessage?: (value: T) => string | undefined; }): Promise<{ value: T; plateKind: "CURRENT"; attempts: Array<{ plateKind: "CURRENT"; plate: InquiryVehicleInputDto["currentPlate"]; vin?: string; succeeded: boolean; usable?: boolean; error?: string; }>; }> { const attempts: Array<{ plateKind: "CURRENT"; plate: InquiryVehicleInputDto["currentPlate"]; vin?: string; succeeded: boolean; usable?: boolean; error?: string; }> = []; try { const value = await options.query(options.currentPlate); const usable = options.isUsable(value); if (!usable) { attempts.push({ plateKind: "CURRENT", plate: options.currentPlate, ...(options.vin ? { vin: options.vin } : {}), succeeded: true, usable: false, }); const error = new BadRequestException( options.errorMessage?.(value) || "بیمه‌نامه معتبر و فعالی مطابق مشخصات خودرو و کد ملی واردشده یافت نشد.", ) as BadRequestException & { attempts?: typeof attempts }; error.attempts = attempts; throw error; } attempts.push({ plateKind: "CURRENT", plate: options.currentPlate, ...(options.vin ? { vin: options.vin } : {}), succeeded: true, usable: true, }); return { value, plateKind: "CURRENT", attempts }; } catch (error) { const alreadyRecorded = typeof error === "object" && error != null && Array.isArray((error as { attempts?: unknown }).attempts); if (!alreadyRecorded) { attempts.push({ plateKind: "CURRENT", plate: options.currentPlate, ...(options.vin ? { vin: options.vin } : {}), succeeded: false, error: error instanceof Error ? error.message : String(error), }); } if (typeof error === "object" && error != null) { (error as { attempts?: typeof attempts }).attempts = attempts; } throw error; } } export function normalizeInquirySubmission>( caseType: BlameRequestType, input: T, ): NormalizedInquirySubmission { const participants = resolveInquiryParticipants(caseType, input as any); const driver = participantForRole( participants, InquiryParticipantRole.DRIVER, ); const thirdPartyPolicyholder = participantForRole( participants, InquiryParticipantRole.THIRD_PARTY_POLICYHOLDER, ); if (!driver || !thirdPartyPolicyholder) { throw new BadRequestException( "اطلاعات هویتی راننده و بیمه‌گذار شخص ثالث الزامی است.", ); } const vehicleOwner = participantForRole( participants, InquiryParticipantRole.VEHICLE_OWNER, ); const carBodyPolicyholder = participantForRole( participants, InquiryParticipantRole.CAR_BODY_POLICYHOLDER, ); const vehicle = resolveInquiryVehicle( input.vehicle as InquiryVehicleInputDto, ); const sameDriverAndPolicyholder = participants.roles.driver === participants.roles.thirdPartyPolicyholder; return { dto: { ...input, plate: vehicle.currentPlate, vin: vehicle.vin, nationalCodeOfDriver: driver.nationalCode, driverBirthday: driver.birthday, driverLicense: driver.licenseNumber, licenseType: driver.licenseType, userNoCertificate: driver.hasDrivingLicense == null ? undefined : !driver.hasDrivingLicense, nationalCodeOfInsurer: thirdPartyPolicyholder.nationalCode, insurerBirthday: thirdPartyPolicyholder.birthday, driverIsInsurer: sameDriverAndPolicyholder, insurerLicense: sameDriverAndPolicyholder ? driver.licenseNumber : undefined, isNewCar: vehicle.isNewCar, } as NormalizedInquirySubmission["dto"], participants, driver, vehicleOwner, thirdPartyPolicyholder, carBodyPolicyholder, vehicle, }; }