diff --git a/src/claim-request-management/claim-request-management.service.ts b/src/claim-request-management/claim-request-management.service.ts index e3fea44..7566dae 100644 --- a/src/claim-request-management/claim-request-management.service.ts +++ b/src/claim-request-management/claim-request-management.service.ts @@ -191,6 +191,24 @@ import { } from "src/fanavaran/schema/fanavaran-audit-log.schema"; import { SmsOrchestrationService } from "src/sms-orchestration/sms-orchestration.service"; import { selectLatestActiveFanavaranPolicy } from "./fanavaran-policy-selection"; +import { + selectAccidentVehicleUsedId, + vehicleGroupIdForKind, +} from "./fanavaran-accident-vehicle-used"; +import { + asPartyInquiryRows, + pickPersonBirthday, + pickPersonNationalCode, + selectFanavaranDriverId, +} from "./fanavaran-driver-inquiry"; +import { + asObjectRecord, + parseFanavaranId, + pickFanavaranPlaqueLookupFields, + pickFanavaranVehicleFromInquiry, + pickVehicleId, + pickVinFromPartyVehicle, +} from "src/lookups/fanavaran-last-car-policy"; export interface FanavaranAutoSubmitResult { attempted: boolean; @@ -3826,6 +3844,177 @@ export class ClaimRequestManagementService { } } + private async resolveAccidentVehicleUsedFromVehicleKind( + clientKey: FanavaranClientKey, + vehicleKindId: number | null, + preferredId: number | null, + ): Promise { + if (vehicleKindId == null) return preferredId; + try { + const [kinds, useTypes] = await Promise.all([ + this.getFanavaranLookupRows(clientKey, "vehicle-kinds"), + this.getFanavaranLookupRows(clientKey, "vehicle-use-types"), + ]); + const groupId = vehicleGroupIdForKind(kinds, vehicleKindId); + const usedId = selectAccidentVehicleUsedId( + useTypes, + groupId, + preferredId, + ); + this.logger.log( + `Resolved AccidentVehicleUsedId=${usedId} from VehicleKindId=${vehicleKindId} VehicleGroupId=${groupId ?? "none"} (UsedId=${preferredId ?? "none"})`, + ); + return usedId; + } catch (error) { + this.logger.warn( + `Failed to resolve AccidentVehicleUsedId from VehicleKindId=${vehicleKindId}: ${ + error instanceof Error ? error.message : error + }`, + ); + return preferredId; + } + } + + /** + * GEN.03 lookup-filter: AccidentVehicleUsedId comes from Fanavaran vehicle + * UsedId (VIN inquiry or vehicle GET), not Mongo tenant defaults. + */ + private async resolveAccidentVehicleUsedFromVehicleRecord( + clientKey: FanavaranClientKey, + vehicle: Record | null, + ): Promise<{ usedId: number | null; vehicleKindId: number | null }> { + const vehicleKindId = parseFanavaranId(vehicle?.VehicleKindId); + const vehicleUsedId = parseFanavaranId(vehicle?.UsedId); + const usedId = await this.resolveAccidentVehicleUsedFromVehicleKind( + clientKey, + vehicleKindId, + vehicleUsedId, + ); + return { usedId, vehicleKindId }; + } + + private async resolveAccidentVehicleUsedFromVin( + clientKey: FanavaranClientKey, + vin: string, + vehicleId?: number | null, + ): Promise<{ usedId: number | null; vehicleKindId: number | null }> { + try { + const vehicle = await this.fetchFanavaranVehicleByVin( + clientKey, + vin, + vehicleId, + ); + if (!vehicle) { + this.logger.warn(`VIN inquiry for ${vin} returned no vehicle`); + return { usedId: null, vehicleKindId: null }; + } + this.logger.log( + `VIN inquiry ${vin} VehicleId=${pickVehicleId(vehicle) ?? "none"} VehicleKindId=${parseFanavaranId(vehicle.VehicleKindId) ?? "none"} UsedId=${parseFanavaranId(vehicle.UsedId) ?? "none"} PlaqueKindId=${parseFanavaranId(vehicle.PlaqueKindId) ?? "none"}`, + ); + return this.resolveAccidentVehicleUsedFromVehicleRecord(clientKey, vehicle); + } catch (error) { + this.logger.warn( + `Failed to resolve AccidentVehicleUsedId from VIN=${vin}: ${ + error instanceof Error ? error.message : error + }`, + ); + return { usedId: null, vehicleKindId: null }; + } + } + + private async fetchFanavaranVehicleByVin( + clientKey: FanavaranClientKey, + vin: string, + vehicleId?: number | null, + ): Promise | null> { + const inquired = await this.fanavaranLookupService.inquiryByVin( + clientKey, + vin, + ); + return pickFanavaranVehicleFromInquiry(inquired, vehicleId); + } + + /** + * Fallback when VIN is missing: policy GET → vehicle GET → UsedId/VehicleKindId. + */ + private async resolveAccidentVehicleUsedFromPolicy( + clientKey: FanavaranClientKey, + policyId: number, + ): Promise<{ usedId: number | null; vehicleKindId: number | null }> { + try { + const policy = asObjectRecord( + await this.fanavaranLookupService.thirdPartyPolicyById( + clientKey, + policyId, + ), + ); + const vehicleId = pickVehicleId(policy?.VehicleId ?? policy?.vehicleId); + if (vehicleId === null) { + this.logger.warn(`Policy ${policyId} has no VehicleId`); + return { usedId: null, vehicleKindId: null }; + } + + const versionNo = parseFanavaranId(policy?.VehicleVersionNo); + const vehicle = asObjectRecord( + await this.fanavaranLookupService.vehicleById( + clientKey, + vehicleId, + versionNo ?? undefined, + ), + ); + this.logger.log( + `Policy ${policyId} VehicleId=${vehicleId} version=${versionNo ?? "latest"} VehicleKindId=${parseFanavaranId(vehicle?.VehicleKindId) ?? "none"} UsedId=${parseFanavaranId(vehicle?.UsedId) ?? "none"}`, + ); + return this.resolveAccidentVehicleUsedFromVehicleRecord(clientKey, vehicle); + } catch (error) { + this.logger.warn( + `Failed to resolve AccidentVehicleUsedId from PolicyId=${policyId}: ${ + error instanceof Error ? error.message : error + }`, + ); + return { usedId: null, vehicleKindId: null }; + } + } + + private async resolveAccidentVehicleUsedFromApis(input: { + clientKey: FanavaranClientKey; + parties: unknown[]; + guiltyPartyId?: unknown; + policyId?: number | null; + }): Promise<{ + usedId: number | null; + vehicleKindId: number | null; + source: "vin-inquiry" | "policy-vehicle" | null; + }> { + const guiltyParty = (input.parties ?? []).find( + (party: any) => + String(party?.person?.userId ?? "") === String(input.guiltyPartyId ?? ""), + ); + const vin = pickVinFromPartyVehicle(guiltyParty); + + if (vin) { + const fromVin = await this.resolveAccidentVehicleUsedFromVin( + input.clientKey, + vin, + ); + if (fromVin.usedId != null) { + return { ...fromVin, source: "vin-inquiry" }; + } + } + + if (typeof input.policyId === "number") { + const fromPolicy = await this.resolveAccidentVehicleUsedFromPolicy( + input.clientKey, + input.policyId, + ); + if (fromPolicy.usedId != null) { + return { ...fromPolicy, source: "policy-vehicle" }; + } + } + + return { usedId: null, vehicleKindId: null, source: null }; + } + private parseJalaliBirthday( birthday: string | number | null | undefined, ): { year: number; month: number; day: number } | null { @@ -3869,7 +4058,7 @@ export class ClaimRequestManagementService { private async resolveDriverFanavaranId( clientKey: FanavaranClientKey, nationalCode: string | null | undefined, - driverBirthday: string | null | undefined, + driverBirthday: string | number | null | undefined, driverIsInsurer: boolean | undefined, ): Promise { if (!nationalCode) { @@ -3893,7 +4082,7 @@ export class ClaimRequestManagementService { ); try { - const response = (await this.fanavaranLookupService.inquiryByUniqueIdentifier( + const response = await this.fanavaranLookupService.inquiryByUniqueIdentifier( clientKey, { nationalCode, @@ -3901,36 +4090,27 @@ export class ClaimRequestManagementService { birthMonth: parsed.month, birthDay: parsed.day, }, - )) as Array<{ - Id: number; - RoleId: number; - NationalCode: string; - Name?: string; - LastName?: string; - }>; + ); this.logger.log( `[resolveDriverFanavaranId] RESPONSE: ${JSON.stringify(response)}`, ); - if (!Array.isArray(response) || response.length === 0) { - this.logger.warn( - `[resolveDriverFanavaranId] EMPTY response for nationalCode=${nationalCode}`, - ); - return null; - } - - const match = response.find((entry) => entry.RoleId === targetRoleId); - if (match) { + const driverId = selectFanavaranDriverId(response, driverIsInsurer); + if (driverId != null) { + const rows = asPartyInquiryRows(response); + const match = rows.find((row) => Number(row.Id) === driverId); this.logger.log( - `[resolveDriverFanavaranId] MATCH: Id=${match.Id} RoleId=${match.RoleId} name=${match.Name} ${match.LastName} for nationalCode=${nationalCode}`, + `[resolveDriverFanavaranId] MATCH: Id=${driverId} RoleId=${match?.RoleId ?? "none"} name=${match?.Name ?? ""} ${match?.LastName ?? ""} for nationalCode=${nationalCode}`, ); - return match.Id; + return driverId; } this.logger.warn( - `[resolveDriverFanavaranId] NO MATCH for targetRoleId=${targetRoleId} (driverIsInsurer=${driverIsInsurer}) nationalCode=${nationalCode}. ` + - `Available entries: ${response.map((e) => `{Id=${e.Id}, RoleId=${e.RoleId}, Name=${e.Name} ${e.LastName}}`).join("; ")}`, + `[resolveDriverFanavaranId] NO MATCH for nationalCode=${nationalCode} driverIsInsurer=${driverIsInsurer}. ` + + `Available entries: ${asPartyInquiryRows(response) + .map((e) => `{Id=${e.Id}, RoleId=${e.RoleId}, Name=${e.Name} ${e.LastName}}`) + .join("; ")}`, ); return null; } catch (error) { @@ -3957,13 +4137,10 @@ export class ClaimRequestManagementService { FaultPercent: number; }; }): Promise> { - const damagedParty = Array.isArray(input.blameCase?.parties) - ? input.blameCase.parties.find( - (party: any) => - String(party?.person?.userId ?? "") === - String(input.claimCase?.owner?.userId ?? ""), - ) - : null; + const damagedParty = this.pickDamagedPartyForFanavaran( + input.blameCase, + input.claimCase, + ); const person = damagedParty?.person ?? {}; const vehicle = damagedParty?.vehicle ?? {}; const insurance = damagedParty?.insurance ?? {}; @@ -3983,8 +4160,10 @@ export class ClaimRequestManagementService { this.logger.log( `[buildFanavaranDamageCasePayload] damagedParty resolved: userId=${damagedParty?.person?.userId ?? "NONE"}, ` + + `nationalCode=${pickPersonNationalCode(person) ?? "MISSING"}, ` + `nationalCodeOfDriver=${person.nationalCodeOfDriver ?? "MISSING"}, ` + - `driverBirthday=${person.driverBirthday ?? "MISSING"}, ` + + `nationalCodeOfInsurer=${person.nationalCodeOfInsurer ?? "MISSING"}, ` + + `driverBirthday=${pickPersonBirthday(person) ?? "MISSING"}, ` + `driverIsInsurer=${person.driverIsInsurer ?? "MISSING"}, ` + `driverLicense=${person.driverLicense ?? "MISSING"}`, ); @@ -4032,14 +4211,40 @@ export class ClaimRequestManagementService { "ModelField", "ModelCii", ]); - const builtYear = builtYearRaw ? Number(builtYearRaw) || null : null; + const builtYearFromInquiry = builtYearRaw ? Number(builtYearRaw) || null : null; + + const damagedVin = + pickVinFromPartyVehicle(damagedParty) ?? + this.pickPartyInquiryField(inquiryMapped, inquiryRaw, [ + "VIN", + "vin", + "VinNumberField", + ]); + let apiVehicle: Record | null = null; + if (damagedVin) { + try { + apiVehicle = await this.fetchFanavaranVehicleByVin( + input.clientKey, + damagedVin, + ); + } catch (error) { + this.logger.warn( + `[buildFanavaranDamageCasePayload] VIN inquiry failed for ${damagedVin}: ${ + error instanceof Error ? error.message : error + }`, + ); + } + } + const apiPlaque = pickFanavaranPlaqueLookupFields(apiVehicle); + const builtYear = + parseFanavaranId(apiVehicle?.BuiltYear) ?? builtYearFromInquiry; const carType = input.claimCase?.vehicle?.carType as string | undefined; const cachedDamage = (input.claimCase as any)?.fanavaranSync?.damageCase ?? {}; - let vehicleKindId = - cachedDamage.vehicleKindId != null - ? Number(cachedDamage.vehicleKindId) - : null; + let vehicleKindId = parseFanavaranId(apiVehicle?.VehicleKindId); + if (vehicleKindId == null && cachedDamage.vehicleKindId != null) { + vehicleKindId = Number(cachedDamage.vehicleKindId); + } if (vehicleKindId == null) { vehicleKindId = await this.resolveVehicleKindId( input.clientKey, @@ -4058,17 +4263,17 @@ export class ClaimRequestManagementService { } else { driverFanavaranId = await this.resolveDriverFanavaranId( input.clientKey, - person.nationalCodeOfDriver, - person.driverBirthday, + pickPersonNationalCode(person), + pickPersonBirthday(person), person.driverIsInsurer, ); // Persist the resolved ID back to the blame case party for future use if (driverFanavaranId && input.blameCase?._id && damagedParty) { - const partyIndex = input.blameCase.parties.findIndex( - (p: any) => + const partyIndex = (input.blameCase.parties ?? []).findIndex( + (p: any) => p === damagedParty || String(p?.person?.userId ?? "") === - String(input.claimCase?.owner?.userId ?? ""), + String(damagedParty?.person?.userId ?? ""), ); if (partyIndex >= 0) { await this.blameRequestDbService.findByIdAndUpdate( @@ -4092,19 +4297,67 @@ export class ClaimRequestManagementService { ); } + const cachedUsedId = + cachedDamage.accidentVehicleUsedId != null + ? Number(cachedDamage.accidentVehicleUsedId) + : (input.claimCase as any)?.fanavaranSync?.baseClaim + ?.accidentVehicleUsedId != null + ? Number( + (input.claimCase as any).fanavaranSync.baseClaim + .accidentVehicleUsedId, + ) + : null; + const cachedUsedSource = + cachedDamage.accidentVehicleUsedSource ?? + (input.claimCase as any)?.fanavaranSync?.baseClaim + ?.accidentVehicleUsedSource; + const canReuseApiUsedId = + cachedUsedId != null && + cachedUsedId > 0 && + (cachedUsedSource === "vin-inquiry" || + cachedUsedSource === "policy-vehicle"); + let accidentVehicleUsedId: number | null = canReuseApiUsedId + ? cachedUsedId + : null; + let usedSource: "vin-inquiry" | "policy-vehicle" | null = canReuseApiUsedId + ? cachedUsedSource + : null; + if (accidentVehicleUsedId == null) { + const resolved = await this.resolveAccidentVehicleUsedFromApis({ + clientKey: input.clientKey, + parties: input.blameCase?.parties ?? [], + guiltyPartyId: this.resolveGuiltyPartyIdV2( + input.blameCase?.parties ?? [], + input.blameCase?.expert?.decision?.guiltyPartyId, + ), + policyId: (input.claimCase as any)?.fanavaranSync?.baseClaim?.policyId, + }); + accidentVehicleUsedId = resolved.usedId; + usedSource = resolved.source; + } + if (accidentVehicleUsedId == null) { + accidentVehicleUsedId = await this.resolveAccidentVehicleUsedFromVehicleKind( + input.clientKey, + vehicleKindId, + null, + ); + } + this.logger.log( - `[buildFanavaranDamageCasePayload] Final DriverId=${driverFanavaranId ?? "NULL"} for nationalCodeOfDriver=${person.nationalCodeOfDriver ?? "MISSING"}, ` + + `[buildFanavaranDamageCasePayload] Final DriverId=${driverFanavaranId ?? "NULL"} for nationalCode=${pickPersonNationalCode(person) ?? "MISSING"}, ` + `PolicyNo=${policyNo ?? "NULL"}, PolicyCINumber=${policyCINumber ?? "NULL"}`, ); const payload = { BeginDate: beginDate, BuiltYear: builtYear, - ChassisNo: this.pickPartyInquiryField(inquiryMapped, inquiryRaw, [ - "ShsNum", - "ChassisNumberField", - "shsNam", - ]), + ChassisNo: + this.pickPartyInquiryField(inquiryMapped, inquiryRaw, [ + "ShsNum", + "ChassisNumberField", + "shsNam", + ]) ?? + (typeof apiVehicle?.ChassisNo === "string" ? apiVehicle.ChassisNo : null), DmgHistoryStatus: input.defaults.DmgHistoryStatus, Desc: this.formatFanavaranSelectedPartsDesc(input.selectedParts), DmgCaseTypeId: input.defaults.DmgCaseTypeId, @@ -4125,33 +4378,38 @@ export class ClaimRequestManagementService { insurerLicense: person.insurerLicense, }), LicenceTypeId: input.defaults.CulpritLicenceTypeId, - MotorNo: this.pickPartyInquiryField(inquiryMapped, inquiryRaw, [ - "MtrNum", - "EngineNumberField", - "mtrnum", - ]), + MotorNo: + this.pickPartyInquiryField(inquiryMapped, inquiryRaw, [ + "MtrNum", + "EngineNumberField", + "mtrnum", + ]) ?? (typeof apiVehicle?.MotorNo === "string" ? apiVehicle.MotorNo : null), OwnerId: null, - PlaqueCityId: null, - PlaqueKindId: plate ? input.defaults.PlaqueKindId : null, - PlaqueLeftNo: plate ? String(plate.leftDigits) : null, - PlaqueMiddleCodeId: this.getFanavaranPlateMiddleCode( - plate?.centerAlphabet, - ), + PlaqueCityId: apiPlaque.cityId, + PlaqueKindId: apiPlaque.kindId, + PlaqueLeftNo: + apiPlaque.leftNo ?? (plate ? String(plate.leftDigits) : null), + PlaqueMiddleCodeId: + apiPlaque.middleCodeId ?? + this.getFanavaranPlateMiddleCode(plate?.centerAlphabet), PlaqueNo: + apiPlaque.plaqueNo ?? this.pickPartyInquiryField(inquiryMapped, inquiryRaw, ["Plk"]) ?? this.formatFanavaranPlateNo(plate), - PlaqueRightNo: plate ? String(plate.centerDigits) : null, - PlaqueSampleId: plate ? input.defaults.PlaqueSampleId : null, - PlaqueSerial: plate ? String(plate.ir) : null, + PlaqueRightNo: + apiPlaque.rightNo ?? (plate ? String(plate.centerDigits) : null), + PlaqueSampleId: apiPlaque.sampleId, + PlaqueSerial: apiPlaque.serial ?? (plate ? String(plate.ir) : null), PolicyNo: policyNo, PreviousPolicyEndDate: endDate ?? "", VehicleKindId: vehicleKindId, - VIN: this.pickPartyInquiryField(inquiryMapped, inquiryRaw, [ - "VIN", - "vin", - "VinNumberField", - ]), - AccidentVehicleUsedId: input.defaults.AccidentVehicleUsedId, + VIN: + this.pickPartyInquiryField(inquiryMapped, inquiryRaw, [ + "VIN", + "vin", + "VinNumberField", + ]) ?? (typeof apiVehicle?.VIN === "string" ? apiVehicle.VIN : null), + AccidentVehicleUsedId: accidentVehicleUsedId, PolicyCINumber: policyCINumber, }; @@ -4167,6 +4425,24 @@ export class ClaimRequestManagementService { ...(vehicleKindId != null ? { "fanavaranSync.damageCase.vehicleKindId": vehicleKindId } : {}), + ...(apiPlaque.kindId != null + ? { "fanavaranSync.damageCase.plaqueKindId": apiPlaque.kindId } + : {}), + ...(apiPlaque.sampleId != null + ? { "fanavaranSync.damageCase.plaqueSampleId": apiPlaque.sampleId } + : {}), + ...(accidentVehicleUsedId != null + ? { + "fanavaranSync.damageCase.accidentVehicleUsedId": + accidentVehicleUsedId, + ...(usedSource + ? { + "fanavaranSync.damageCase.accidentVehicleUsedSource": + usedSource, + } + : {}), + } + : {}), ...(insuranceCorpId != null ? { "fanavaranSync.damageCase.insuranceCorpId": insuranceCorpId } : {}), @@ -4633,6 +4909,45 @@ export class ClaimRequestManagementService { } } + /** + * زیان‌دیده for GEN.12: claim owner when linked to a blame party, + * else the party that is not the guilty one. + */ + private pickDamagedPartyForFanavaran( + blameCase: any, + claimCase: any, + ): any | null { + const parties: any[] = Array.isArray(blameCase?.parties) + ? blameCase.parties + : []; + if (parties.length === 0) return null; + + const ownerId = claimCase?.owner?.userId; + if (ownerId) { + const byOwner = parties.find( + (party) => + String(party?.person?.userId ?? "") === String(ownerId), + ); + if (byOwner) return byOwner; + } + + const guiltyPartyId = this.resolveGuiltyPartyIdV2( + parties, + blameCase?.expert?.decision?.guiltyPartyId, + ); + if (guiltyPartyId) { + const injured = parties.find( + (party) => + String(party?.person?.userId ?? "") !== String(guiltyPartyId), + ); + if (injured) return injured; + } + + return ( + parties.find((party) => party?.role === PartyRole.FIRST) ?? parties[0] + ); + } + private getNationalCodeOfInsurerForGuiltyPartyV2( parties: Array<{ role?: PartyRole; @@ -4703,11 +5018,17 @@ export class ClaimRequestManagementService { auditSession?: FanavaranAuditSession; /** Persist built payload onto fanavaranSync.baseClaim.lastPayload (default true). */ persistPayload?: boolean; + /** + * Re-run VIN inquiry / vehicle GET for AccidentVehicleUsedId. + * Submit always sets this so Mongo tenant default 1 cannot be reused. + */ + forceRefreshUsedId?: boolean; }, ): Promise { const profile = getFanavaranClientProfile(clientKey); const logPrefix = `[Fanavaran ${clientKey} V2] claimCaseId=${claimCaseId}`; const forceRefreshPolicy = options?.forceRefreshPolicy === true; + const forceRefreshUsedId = options?.forceRefreshUsedId === true; const requirePolicyId = options?.requirePolicyId === true; const persistPayload = options?.persistPayload !== false; const auditSession = @@ -4877,6 +5198,43 @@ export class ClaimRequestManagementService { }, }); + const cachedUsedId = + forceRefreshUsedId || forceRefreshPolicy + ? null + : (claimCase as any)?.fanavaranSync?.baseClaim?.accidentVehicleUsedId; + const cachedUsedSource = (claimCase as any)?.fanavaranSync?.baseClaim + ?.accidentVehicleUsedSource; + const canReuseApiUsedId = + !forceRefreshUsedId && + !forceRefreshPolicy && + cachedUsedId != null && + Number(cachedUsedId) > 0 && + (cachedUsedSource === "vin-inquiry" || + cachedUsedSource === "policy-vehicle"); + + delete payload.AccidentVehicleUsedId; + let usedSource: "vin-inquiry" | "policy-vehicle" | null = null; + if (canReuseApiUsedId) { + payload.AccidentVehicleUsedId = Number(cachedUsedId); + usedSource = cachedUsedSource; + } else { + const guiltyPartyIdForVin = this.resolveGuiltyPartyIdV2( + blameCase.parties ?? [], + expertDecision?.guiltyPartyId, + ); + const resolved = await this.resolveAccidentVehicleUsedFromApis({ + clientKey, + parties: blameCase.parties ?? [], + guiltyPartyId: guiltyPartyIdForVin, + policyId: + typeof payload.PolicyId === "number" ? payload.PolicyId : null, + }); + if (resolved.usedId != null) { + payload.AccidentVehicleUsedId = resolved.usedId; + usedSource = resolved.source; + } + } + if (persistPayload) { await this.claimCaseDbService.findByIdAndUpdate(claimCaseId, { $set: { @@ -4885,6 +5243,18 @@ export class ClaimRequestManagementService { ...(payload.PolicyId != null ? { "fanavaranSync.baseClaim.policyId": payload.PolicyId } : {}), + ...(typeof payload.AccidentVehicleUsedId === "number" + ? { + "fanavaranSync.baseClaim.accidentVehicleUsedId": + payload.AccidentVehicleUsedId, + ...(usedSource + ? { + "fanavaranSync.baseClaim.accidentVehicleUsedSource": + usedSource, + } + : {}), + } + : {}), }, }); } @@ -7280,6 +7650,7 @@ export class ClaimRequestManagementService { requirePolicyId: true, auditSession, persistPayload: true, + forceRefreshUsedId: true, })); // Guardrail: never send empty/null licence fields even if manual overrides arrive. diff --git a/src/claim-request-management/entites/schema/claim-cases.schema.ts b/src/claim-request-management/entites/schema/claim-cases.schema.ts index c5c341e..5516ef7 100644 --- a/src/claim-request-management/entites/schema/claim-cases.schema.ts +++ b/src/claim-request-management/entites/schema/claim-cases.schema.ts @@ -93,6 +93,25 @@ export class FanavaranSyncStage { @Prop({ type: Number }) vehicleKindId?: number; + /** Cached Fanavaran PlaqueKindId from VIN inquiry / vehicle GET. */ + @Prop({ type: Number }) + plaqueKindId?: number; + + /** Cached Fanavaran PlaqueSampleId from VIN inquiry / vehicle GET. */ + @Prop({ type: Number }) + plaqueSampleId?: number; + + /** + * Cached Fanavaran AccidentVehicleUsedId, resolved from VIN inquiry / vehicle GET + * UsedId (lookup-filter safe). Not the Mongo tenant default. + */ + @Prop({ type: Number }) + accidentVehicleUsedId?: number; + + /** How AccidentVehicleUsedId was resolved: vin-inquiry | policy-vehicle */ + @Prop({ type: String }) + accidentVehicleUsedSource?: string; + /** Cached Fanavaran InsuranceCorpId. */ @Prop({ type: Number }) insuranceCorpId?: number; diff --git a/src/claim-request-management/fanavaran-accident-vehicle-used.spec.ts b/src/claim-request-management/fanavaran-accident-vehicle-used.spec.ts new file mode 100644 index 0000000..0e606f5 --- /dev/null +++ b/src/claim-request-management/fanavaran-accident-vehicle-used.spec.ts @@ -0,0 +1,36 @@ +import { + selectAccidentVehicleUsedId, + vehicleGroupIdForKind, +} from "./fanavaran-accident-vehicle-used"; + +describe("selectAccidentVehicleUsedId", () => { + const useTypes = [ + { Id: 1, Caption: "شخصي", IsActive: 1, VehicleGroupId: 1 }, + { Id: 3, Caption: "تاکسي درون شهري", IsActive: 1, VehicleGroupId: 1 }, + { Id: 35, Caption: "آمبولانس", IsActive: 1, VehicleGroupId: 2 }, + { Id: 36, Caption: "حمل مواد سريع الاشتعال", IsActive: 1, VehicleGroupId: 3 }, + ]; + + const kinds = [ + { Id: 8671, VehicleGroupId: 1, IsActive: 1 }, + { Id: 5904, VehicleGroupId: 3, IsActive: 1 }, + ]; + + it("keeps preferred id 1 when the car is passenger group 1", () => { + expect(vehicleGroupIdForKind(kinds, 8671)).toBe(1); + expect(selectAccidentVehicleUsedId(useTypes, 1, 1)).toBe(1); + }); + + it("keeps the vehicle UsedId when it is valid for that group", () => { + expect(selectAccidentVehicleUsedId(useTypes, 3, 36)).toBe(36); + }); + + it("picks an active use type for a non-passenger group instead of 1", () => { + expect(vehicleGroupIdForKind(kinds, 5904)).toBe(3); + expect(selectAccidentVehicleUsedId(useTypes, 3, 1)).toBe(36); + }); + + it("falls back to the vehicle UsedId when group is unknown", () => { + expect(selectAccidentVehicleUsedId(useTypes, null, 84)).toBe(84); + }); +}); diff --git a/src/claim-request-management/fanavaran-accident-vehicle-used.ts b/src/claim-request-management/fanavaran-accident-vehicle-used.ts new file mode 100644 index 0000000..a3385a7 --- /dev/null +++ b/src/claim-request-management/fanavaran-accident-vehicle-used.ts @@ -0,0 +1,71 @@ +export type FanavaranVehicleKindRow = { + Id?: unknown; + VehicleGroupId?: unknown; + IsActive?: unknown; + Caption?: unknown; +}; + +export type FanavaranVehicleUseTypeRow = { + Id?: unknown; + VehicleGroupId?: unknown; + IsActive?: unknown; + Caption?: unknown; +}; + +function asPositiveId(value: unknown): number | null { + if (value === null || value === undefined) return null; + const id = Number(value); + return Number.isFinite(id) && id > 0 ? id : null; +} + +export function vehicleGroupIdForKind( + kinds: unknown, + vehicleKindId: number, +): number | null { + if (!Array.isArray(kinds)) return null; + const match = kinds.find( + (row) => asPositiveId((row as FanavaranVehicleKindRow)?.Id) === vehicleKindId, + ) as FanavaranVehicleKindRow | undefined; + return asPositiveId(match?.VehicleGroupId); +} + +/** + * Fanavaran filters AccidentVehicleUsedId by the policy car's VehicleGroupId. + * `preferredId` is the vehicle UsedId from VIN inquiry / vehicle GET — never a Mongo tenant default. + */ +export function selectAccidentVehicleUsedId( + useTypes: unknown, + vehicleGroupId: number | null, + preferredId: number | null, +): number | null { + if (!Array.isArray(useTypes) || vehicleGroupId == null) { + return preferredId; + } + + const active = useTypes.filter((row) => { + const item = row as FanavaranVehicleUseTypeRow; + return ( + item?.IsActive === 1 && + asPositiveId(item.VehicleGroupId) === vehicleGroupId && + asPositiveId(item.Id) != null + ); + }) as FanavaranVehicleUseTypeRow[]; + + if (active.length === 0) { + return preferredId; + } + + const preferred = active.find((row) => asPositiveId(row.Id) === preferredId); + if (preferred && preferredId != null) { + return preferredId; + } + + const personal = active.find((row) => + String(row.Caption ?? "").includes("شخص"), + ); + if (personal) { + return asPositiveId(personal.Id) ?? preferredId; + } + + return asPositiveId(active[0].Id) ?? preferredId; +} diff --git a/src/claim-request-management/fanavaran-driver-inquiry.spec.ts b/src/claim-request-management/fanavaran-driver-inquiry.spec.ts new file mode 100644 index 0000000..99ce826 --- /dev/null +++ b/src/claim-request-management/fanavaran-driver-inquiry.spec.ts @@ -0,0 +1,61 @@ +import { + asPartyInquiryRows, + parseJalaliDateParts, + pickPersonBirthday, + pickPersonNationalCode, + selectFanavaranDriverId, +} from "./fanavaran-driver-inquiry"; + +describe("selectFanavaranDriverId", () => { + const rows = [ + { Id: 10, RoleId: 161, Name: "insurer" }, + { Id: 20, RoleId: 166, Name: "driver" }, + ]; + + it("prefers insurer role 161 when the driver is the owner", () => { + expect(selectFanavaranDriverId(rows, true)).toBe(10); + }); + + it("prefers driver role 166 when the driver is not the owner", () => { + expect(selectFanavaranDriverId(rows, false)).toBe(20); + }); + + it("uses the other role when the preferred role is missing", () => { + expect(selectFanavaranDriverId([{ Id: 20, RoleId: 166 }], true)).toBe(20); + }); + + it("unwraps a wrapped Fanavaran payload", () => { + expect(selectFanavaranDriverId({ value: rows }, true)).toBe(10); + }); +}); + +describe("pickPersonNationalCode / birthday", () => { + it("uses insurer national code when driverIsInsurer", () => { + expect( + pickPersonNationalCode({ + driverIsInsurer: true, + nationalCodeOfInsurer: "0012345678", + nationalCodeOfDriver: "", + }), + ).toBe("0012345678"); + }); + + it("falls back across birthday fields", () => { + expect( + pickPersonBirthday({ + driverBirthday: null, + birthday: "13480313", + }), + ).toBe("13480313"); + }); +}); + +describe("asPartyInquiryRows", () => { + it("parses compact Jalali birthday 13480313", () => { + expect(parseJalaliDateParts("13480313")).toEqual({ + year: 1348, + month: 3, + day: 13, + }); + }); +}); diff --git a/src/claim-request-management/fanavaran-driver-inquiry.ts b/src/claim-request-management/fanavaran-driver-inquiry.ts new file mode 100644 index 0000000..6db9e76 --- /dev/null +++ b/src/claim-request-management/fanavaran-driver-inquiry.ts @@ -0,0 +1,114 @@ +export const FANAVARAN_INSURER_ROLE_ID = 161; +export const FANAVARAN_DRIVER_ROLE_ID = 166; + +export type FanavaranPartyInquiryRow = { + Id?: unknown; + RoleId?: unknown; + NationalCode?: unknown; + Name?: unknown; + LastName?: unknown; +}; + +function asPositiveId(value: unknown): number | null { + if (value === null || value === undefined) return null; + const id = Number(value); + return Number.isFinite(id) && id > 0 ? id : null; +} + +export function asPartyInquiryRows(value: unknown): FanavaranPartyInquiryRow[] { + if (Array.isArray(value)) { + return value.filter( + (row): row is FanavaranPartyInquiryRow => + !!row && typeof row === "object" && !Array.isArray(row), + ); + } + if (!value || typeof value !== "object") return []; + const record = value as Record; + for (const key of ["value", "Value", "items", "Items", "data", "Data"]) { + const nested = record[key]; + if (Array.isArray(nested)) return asPartyInquiryRows(nested); + } + return []; +} + +/** + * DriverId for GEN.12 زیان‌دیده. Prefer insurer (161) or driver (166) + * based on driverIsInsurer, then any returned party id. + */ +export function selectFanavaranDriverId( + rows: unknown, + driverIsInsurer: boolean | undefined, +): number | null { + const parties = asPartyInquiryRows(rows); + if (parties.length === 0) return null; + + const preferredRole = driverIsInsurer + ? FANAVARAN_INSURER_ROLE_ID + : FANAVARAN_DRIVER_ROLE_ID; + const preferred = parties.find( + (row) => asPositiveId(row.RoleId) === preferredRole, + ); + if (preferred) return asPositiveId(preferred.Id); + + const fallbackRole = driverIsInsurer + ? FANAVARAN_DRIVER_ROLE_ID + : FANAVARAN_INSURER_ROLE_ID; + const fallback = parties.find( + (row) => asPositiveId(row.RoleId) === fallbackRole, + ); + if (fallback) return asPositiveId(fallback.Id); + + return asPositiveId(parties[0].Id); +} + +export function pickPersonNationalCode(person: { + driverIsInsurer?: boolean; + nationalCodeOfDriver?: unknown; + nationalCodeOfInsurer?: unknown; +} | null | undefined): string | null { + if (!person) return null; + const driver = String(person.nationalCodeOfDriver ?? "").trim(); + const insurer = String(person.nationalCodeOfInsurer ?? "").trim(); + if (person.driverIsInsurer) { + return insurer || driver || null; + } + return driver || insurer || null; +} + +export function pickPersonBirthday(person: { + driverBirthday?: unknown; + birthday?: unknown; + insurerBirthday?: unknown; +} | null | undefined): string | number | null { + if (!person) return null; + for (const value of [ + person.driverBirthday, + person.birthday, + person.insurerBirthday, + ]) { + if (value == null || String(value).trim() === "") continue; + return value as string | number; + } + return null; +} + +export function parseJalaliDateParts( + birthday: string | number | null | undefined, +): { year: number; month: number; day: number } | null { + if (birthday == null) return null; + const str = String(birthday).trim(); + if (/^\d{8}$/.test(str)) { + const year = parseInt(str.slice(0, 4), 10); + const month = parseInt(str.slice(4, 6), 10); + const day = parseInt(str.slice(6, 8), 10); + if (!year || !month || !day) return null; + return { year, month, day }; + } + const parts = str.split(/[/\-.]/); + if (parts.length < 3) return null; + const year = parseInt(parts[0], 10); + const month = parseInt(parts[1], 10); + const day = parseInt(parts[2], 10); + if (!year || !month || !day) return null; + return { year, month, day }; +} diff --git a/src/lookups/fanavaran-last-car-policy.spec.ts b/src/lookups/fanavaran-last-car-policy.spec.ts index 57d2866..a9f96b1 100644 --- a/src/lookups/fanavaran-last-car-policy.spec.ts +++ b/src/lookups/fanavaran-last-car-policy.spec.ts @@ -2,7 +2,10 @@ import { filterPoliciesByLine, insuranceLineLabel, parseLastCarPolicyInput, + pickFanavaranPlaqueLookupFields, + pickFanavaranVehicleFromInquiry, pickVehicleId, + pickVinFromPartyVehicle, plaqueLetterFromMiddleCode, plaqueMatchesVehicle, selectLastAmongCarMatches, @@ -84,6 +87,57 @@ describe("fanavaran last car policy", () => { ); }); + it("picks the inquiry row matching policy VehicleId and VIN from the party", () => { + const inquired = [ + { Id: 1, UsedId: 1, VehicleKindId: 10 }, + { + Id: 3379978, + UsedId: 84, + VehicleKindId: 5950, + VIN: "NAS451100P4934023", + }, + ]; + expect(pickFanavaranVehicleFromInquiry(inquired, 3379978)).toEqual( + inquired[1], + ); + expect( + pickVinFromPartyVehicle({ + vehicle: { vin: "nas451100p4934023" }, + }), + ).toBe("NAS451100P4934023"); + expect( + pickVinFromPartyVehicle({ + vehicle: { + inquiry: { mapped: { VIN: "NAS451100P4934023" } }, + }, + }), + ).toBe("NAS451100P4934023"); + }); + + it("reads plaque kind and sample from VIN inquiry for the lookup filter", () => { + expect( + pickFanavaranPlaqueLookupFields({ + PlaqueKindId: 15, + PlaqueSampleId: 24, + PlaqueCityId: null, + PlaqueLeftNo: "62", + PlaqueMiddleCodeId: 14, + PlaqueRightNo: "278", + PlaqueSerial: "50", + PlaqueNo: "278و62", + }), + ).toEqual({ + kindId: 15, + sampleId: 24, + cityId: null, + leftNo: "62", + middleCodeId: 14, + rightNo: "278", + serial: "50", + plaqueNo: "278و62", + }); + }); + it("matches VIN against ChassisNo when VIN is empty", () => { expect( vehicleMatchesCar( diff --git a/src/lookups/fanavaran-last-car-policy.ts b/src/lookups/fanavaran-last-car-policy.ts index c69eab8..17d6602 100644 --- a/src/lookups/fanavaran-last-car-policy.ts +++ b/src/lookups/fanavaran-last-car-policy.ts @@ -233,6 +233,71 @@ export function pickVehicleId(source: unknown): number | null { return null; } +/** Prefer the inquiry row whose Id matches the policy VehicleId. */ +export function pickFanavaranVehicleFromInquiry( + inquired: unknown, + vehicleId?: number | null, +): Record | null { + if (Array.isArray(inquired)) { + if (vehicleId != null) { + const match = inquired.find((row) => pickVehicleId(row) === vehicleId); + if (match) return asObjectRecord(match); + } + return asObjectRecord(inquired[0]); + } + return asObjectRecord(inquired); +} + +export function pickVinFromPartyVehicle(party: { + vehicle?: { vin?: unknown; inquiry?: unknown }; +} | null | undefined): string | null { + const direct = normalizeVin(party?.vehicle?.vin); + if (direct) return direct; + + const inquiry = party?.vehicle?.inquiry; + const nested = asObjectRecord(inquiry); + const mapped = pickVehicleVin(asObjectRecord(nested?.mapped)); + if (mapped) return mapped; + const raw = pickVehicleVin(asObjectRecord(nested?.raw ?? inquiry)); + return raw || null; +} + +export type FanavaranPlaqueLookupFields = { + kindId: number | null; + sampleId: number | null; + cityId: number | null; + leftNo: string | null; + middleCodeId: number | null; + rightNo: string | null; + serial: string | null; + plaqueNo: string | null; +}; + +function asPlaqueText(value: unknown): string | null { + const text = normalizePlaquePart(value); + return text || null; +} + +/** Plaque lookup-filter fields from VIN inquiry / vehicle GET. */ +export function pickFanavaranPlaqueLookupFields( + vehicle: Record | null, +): FanavaranPlaqueLookupFields { + return { + kindId: parseFanavaranId(vehicle?.PlaqueKindId ?? vehicle?.plaqueKindId), + sampleId: parseFanavaranId( + vehicle?.PlaqueSampleId ?? vehicle?.plaqueSampleId, + ), + cityId: parseFanavaranId(vehicle?.PlaqueCityId ?? vehicle?.plaqueCityId), + leftNo: asPlaqueText(vehicle?.PlaqueLeftNo ?? vehicle?.plaqueLeftNo), + middleCodeId: parseFanavaranId( + vehicle?.PlaqueMiddleCodeId ?? vehicle?.plaqueMiddleCodeId, + ), + rightNo: asPlaqueText(vehicle?.PlaqueRightNo ?? vehicle?.plaqueRightNo), + serial: asPlaqueText(vehicle?.PlaqueSerial ?? vehicle?.plaqueSerial), + plaqueNo: asPlaqueText(vehicle?.PlaqueNo ?? vehicle?.plaqueNo), + }; +} + function plaqueNumberEquals(left: unknown, right: unknown): boolean { const a = normalizePlaquePart(left); const b = normalizePlaquePart(right); diff --git a/src/lookups/lookups.controller.ts b/src/lookups/lookups.controller.ts index ef58435..eab1306 100644 --- a/src/lookups/lookups.controller.ts +++ b/src/lookups/lookups.controller.ts @@ -331,7 +331,7 @@ export class LookupsController { @ApiOperation({ summary: "Fanavaran vehicle inquiry by VIN", description: - "Returns vehicle details from Fanavaran for the given VIN number via car/vehicles/inquiry-by-vin.", + "GET car/vehicles/inquiry-by-vin. Response includes UsedId (AccidentVehicleUsedId), VehicleKindId, plus vehicleKind/used lookup rows. Prefer this over vehicle GET when you already have the VIN.", }) @ApiQuery({ name: "vin", @@ -346,6 +346,42 @@ export class LookupsController { return await this.lookupsService.inquiryByVin(vin); } + @Get("inquiry-by-unique-identifier") + @ApiOperation({ + summary: "Fanavaran party inquiry by national code and birthday", + description: + "GET common/parties/inquiry-by-unique-identifier. Use this to resolve GEN.12 DriverId. Pass birthday as 13480313 or 1348/03/13, or birthYear+birthMonth+birthDay.", + }) + @ApiQuery({ name: "nationalCode", example: "0012345678" }) + @ApiQuery({ + name: "birthday", + required: false, + example: "13480313", + description: "Jalali birthday compact (13480313) or delimited (1348/03/13)", + }) + @ApiQuery({ name: "birthYear", required: false, example: 1348 }) + @ApiQuery({ name: "birthMonth", required: false, example: 3 }) + @ApiQuery({ name: "birthDay", required: false, example: 13 }) + @ApiOkResponse({ + description: "Fanavaran parties plus picked driverId / insurerId", + schema: { type: "object" }, + }) + async inquiryByUniqueIdentifier( + @Query("nationalCode") nationalCode?: string, + @Query("birthday") birthday?: string, + @Query("birthYear") birthYear?: string, + @Query("birthMonth") birthMonth?: string, + @Query("birthDay") birthDay?: string, + ) { + return await this.lookupsService.inquiryByUniqueIdentifier({ + nationalCode, + birthday, + birthYear, + birthMonth, + birthDay, + }); + } + @Get("fanavaran") @ApiOperation({ summary: "List configured Fanavaran remote lookups", @@ -627,4 +663,41 @@ export class LookupsController { async bodyPolicyById(@Param("policyId", ParseIntPipe) policyId: number) { return await this.lookupsService.bodyPolicyById(policyId); } + + @Get("vehicle/:vehicleId") + @ApiOperation({ + summary: "Fanavaran vehicle by id", + description: + "GET car/vehicles/{vehicleId} after a third-party/hull policy returns VehicleId. Pass the policy VehicleVersionNo when you need that exact version; omit it for the latest version. Response includes vehicleKind (VehicleGroupId) and used (UsedId) lookup rows.", + }) + @ApiParam({ + name: "vehicleId", + description: "Fanavaran vehicle id from the policy VehicleId field", + example: 3379978, + }) + @ApiQuery({ + name: "versionNo", + required: false, + description: + "Policy VehicleVersionNo. Omit to fetch the latest vehicle version.", + example: 2, + }) + @ApiOkResponse({ + description: + "Vehicle record plus vehicleKind, used, plaque, and color placeholders", + schema: { type: "object" }, + }) + async vehicleById( + @Param("vehicleId", ParseIntPipe) vehicleId: number, + @Query("versionNo") versionNo?: string, + ) { + const parsedVersion = + versionNo == null || String(versionNo).trim() === "" + ? undefined + : Number(versionNo); + return await this.lookupsService.vehicleById( + vehicleId, + Number.isFinite(parsedVersion) ? parsedVersion : undefined, + ); + } } diff --git a/src/lookups/lookups.service.ts b/src/lookups/lookups.service.ts index b532cc8..522fda3 100644 --- a/src/lookups/lookups.service.ts +++ b/src/lookups/lookups.service.ts @@ -14,6 +14,10 @@ import { TEJARAT_STATIC_ACCIDENT_FILES, } from "src/fanavaran/fanavaran-lookup.config"; import { FanavaranLookupService } from "src/fanavaran/fanavaran-lookup.service"; +import { + parseJalaliDateParts, + selectFanavaranDriverId, +} from "src/claim-request-management/fanavaran-driver-inquiry"; import { LookupDbService } from "./entities/db-service/lookup.db.service"; import { asObjectRecord, @@ -178,9 +182,81 @@ export class LookupsService { return await this.getClientRemoteLookup("dmg-business-line"); } + async inquiryByUniqueIdentifier(query: { + nationalCode?: string; + birthday?: string; + birthYear?: string | number; + birthMonth?: string | number; + birthDay?: string | number; + }): Promise { + const nationalCode = String(query.nationalCode ?? "").trim(); + if (!nationalCode) { + throw new BadRequestException("nationalCode is required"); + } + + const fromParts = { + year: Number(query.birthYear), + month: Number(query.birthMonth), + day: Number(query.birthDay), + }; + const parsed = + Number.isFinite(fromParts.year) && + fromParts.year > 0 && + Number.isFinite(fromParts.month) && + fromParts.month > 0 && + Number.isFinite(fromParts.day) && + fromParts.day > 0 + ? fromParts + : parseJalaliDateParts(query.birthday); + + if (!parsed) { + throw new BadRequestException( + "birthday (e.g. 13480313) or birthYear+birthMonth+birthDay is required", + ); + } + + const clientKey = this.activeClientKey(); + const rows = await this.fanavaranLookupService.inquiryByUniqueIdentifier( + clientKey, + { + nationalCode, + birthYear: parsed.year, + birthMonth: parsed.month, + birthDay: parsed.day, + }, + ); + return { + nationalCode, + birthday: parsed, + driverId: selectFanavaranDriverId(rows, undefined), + insurerId: selectFanavaranDriverId(rows, true), + parties: rows, + }; + } + async inquiryByVin(vin: string): Promise { const clientKey = this.activeClientKey(); - return this.fanavaranLookupService.inquiryByVin(clientKey, vin); + const raw = await this.fanavaranLookupService.inquiryByVin(clientKey, vin); + if (Array.isArray(raw)) { + if (raw.length === 0) { + throw new NotFoundException( + `Fanavaran vehicle for VIN ${vin} was not found.`, + ); + } + return Promise.all( + raw.map(async (item) => { + const vehicle = asObjectRecord(item); + return vehicle ? this.enrichVehicle(vehicle) : item; + }), + ); + } + const vehicle = asObjectRecord(raw); + if (!vehicle) { + throw new NotFoundException( + `Fanavaran vehicle for VIN ${vin} was not found.`, + ); + } + return this.enrichVehicle(vehicle); } async myPolicies( @@ -514,11 +590,19 @@ export class LookupsService { async vehicleById(vehicleId: number, versionNo?: number): Promise { const clientKey = this.activeClientKey(); - return this.fanavaranLookupService.vehicleById( - clientKey, - vehicleId, - versionNo, + const vehicle = asObjectRecord( + await this.fanavaranLookupService.vehicleById( + clientKey, + vehicleId, + versionNo, + ), ); + if (!vehicle) { + throw new NotFoundException( + `Fanavaran vehicle ${vehicleId} was not found.`, + ); + } + return this.enrichVehicle(vehicle); } async customerById(customerId: number): Promise { diff --git a/src/lookups/lookups.service.vehicle.spec.ts b/src/lookups/lookups.service.vehicle.spec.ts new file mode 100644 index 0000000..1d7c3b6 --- /dev/null +++ b/src/lookups/lookups.service.vehicle.spec.ts @@ -0,0 +1,145 @@ +import { NotFoundException } from "@nestjs/common"; +import { LookupsService } from "./lookups.service"; + +describe("LookupsService.vehicleById", () => { + const originalClient = process.env.FANAVARAN_CLIENT; + + beforeAll(() => { + process.env.FANAVARAN_CLIENT = "parsian"; + }); + + afterAll(() => { + process.env.FANAVARAN_CLIENT = originalClient; + }); + + function createService(lookup: Record) { + return new LookupsService({} as any, lookup as any); + } + + it("returns the vehicle with kind, used, and plaque lookups", async () => { + const lookup = { + vehicleById: jest.fn().mockResolvedValue({ + Id: 3379978, + VehicleKindId: 5904, + UsedId: 36, + VIN: "IRNKAEK4150012345", + PlaqueLeftNo: "12", + PlaqueMiddleCodeId: 2, + PlaqueRightNo: "345", + PlaqueSerial: "67", + VersionNo: 2, + }), + getRemoteLookup: jest.fn().mockImplementation((_key, url: string) => { + if (url.includes("vehicle-kinds")) { + return Promise.resolve([ + { Id: 5904, Caption: "بنز", VehicleGroupId: 3, IsActive: 1 }, + ]); + } + if (url.includes("vehicle-use-types")) { + return Promise.resolve([ + { Id: 36, Caption: "حمل مواد سريع الاشتعال", VehicleGroupId: 3, IsActive: 1 }, + ]); + } + return Promise.resolve([]); + }), + }; + + const service = createService(lookup); + const result = (await service.vehicleById(3379978, 2)) as Record< + string, + unknown + >; + + expect(lookup.vehicleById).toHaveBeenCalledWith("parsian", 3379978, 2); + expect(result.Id).toBe(3379978); + expect(result.vehicleKind).toEqual({ + Id: 5904, + Caption: "بنز", + VehicleGroupId: 3, + IsActive: 1, + }); + expect(result.used).toEqual({ + Id: 36, + Caption: "حمل مواد سريع الاشتعال", + VehicleGroupId: 3, + IsActive: 1, + }); + expect(result.plaque).toEqual({ + leftTwoDigits: "12", + serialLetter: "ب", + threeDigits: "345", + rightTwoDigits: "67", + }); + }); + + it("enriches inquiry-by-vin rows with used and vehicleKind", async () => { + const lookup = { + inquiryByVin: jest.fn().mockResolvedValue([ + { + Id: 3379978, + UsedId: 84, + VehicleKindId: 5950, + VIN: "NAS451100P4934023", + }, + ]), + getRemoteLookup: jest.fn().mockImplementation((_key, url: string) => { + if (url.includes("vehicle-kinds")) { + return Promise.resolve([ + { Id: 5950, Caption: "کامیون", VehicleGroupId: 3, IsActive: 1 }, + ]); + } + if (url.includes("vehicle-use-types")) { + return Promise.resolve([ + { Id: 84, Caption: "حمل بار", VehicleGroupId: 3, IsActive: 1 }, + ]); + } + return Promise.resolve([]); + }), + }; + + const service = createService(lookup); + const result = (await service.inquiryByVin("NAS451100P4934023")) as Array< + Record + >; + + expect(lookup.inquiryByVin).toHaveBeenCalledWith( + "parsian", + "NAS451100P4934023", + ); + expect(result[0].UsedId).toBe(84); + expect(result[0].used).toEqual({ + Id: 84, + Caption: "حمل بار", + VehicleGroupId: 3, + IsActive: 1, + }); + expect(result[0].vehicleKind).toEqual({ + Id: 5950, + Caption: "کامیون", + VehicleGroupId: 3, + IsActive: 1, + }); + }); + + it("unwraps an array payload and 404s when Fanavaran returns nothing", async () => { + const lookup = { + vehicleById: jest + .fn() + .mockResolvedValueOnce([{ Id: 3379978, VehicleKindId: 1 }]) + .mockResolvedValueOnce([]), + getRemoteLookup: jest.fn().mockResolvedValue([]), + }; + + const service = createService(lookup); + const unwrapped = (await service.vehicleById(3379978)) as Record< + string, + unknown + >; + expect(unwrapped.Id).toBe(3379978); + expect(unwrapped.color).toBeNull(); + + await expect(service.vehicleById(1)).rejects.toBeInstanceOf( + NotFoundException, + ); + }); +});