import { BadRequestException, Injectable, Logger, NotFoundException, } from "@nestjs/common"; import { resolveFanavaranClientKey, resolveFanavaranProductContractId, } from "src/core/config/fanavaran-client.config"; import { FANAVARAN_REMOTE_LOOKUPS, type FanavaranRemoteLookupDefinition, 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, filterPoliciesByLine, insuranceLineIdForProduct, insuranceLineLabel, isPolicyActiveOn, parseFanavaranId, parseLastCarPolicyInput, pickVehicleId, selectLastAmongCarMatches, sortPoliciesNewestEndDateFirst, toAppPlaque, vehicleMatchesCar, type FanavaranCarPolicyProduct, type HydratedFanavaranCarPolicy, } from "./fanavaran-last-car-policy"; import { gregorianDateInIran } from "src/helpers/iran-datetime"; type TejaratAccidentReasonRow = { id: number; persianLabel: string; fanavaranID: number; }; type TejaratAccidentLabelRow = { id: number; persianLabel: string; }; @Injectable() export class LookupsService { private readonly logger = new Logger(LookupsService.name); constructor( private readonly lookupDbService: LookupDbService, private readonly fanavaranLookupService: FanavaranLookupService, ) {} private activeClientKey() { return resolveFanavaranClientKey(); } listFanavaranRemoteLookups(): FanavaranRemoteLookupDefinition[] { return FANAVARAN_REMOTE_LOOKUPS; } private findRemoteLookup(name: string) { const lookup = FANAVARAN_REMOTE_LOOKUPS.find((item) => item.name === name); if (!lookup) { throw new NotFoundException(`Unknown Fanavaran remote lookup: ${name}`); } return lookup; } /** * Returns stored `response` for a lookup document by `name` in the lookups collection. * Legacy Tejarat seed data — used as fallback when live fetch is unavailable. */ async getLookup(lookupName: string): Promise { const doc = await this.lookupDbService.findOne({ name: lookupName }); if (!doc) { this.logger.warn(`Lookup not found in database: ${lookupName}`); throw new NotFoundException(`Lookup "${lookupName}" not found`); } return doc.response; } async getClientRemoteLookup(lookupName: string): Promise { const clientKey = this.activeClientKey(); const definition = this.findRemoteLookup(lookupName); // Parsian: file → lookups collection → Fanavaran (default inside FanavaranLookupService) // Tejaratno: file → Fanavaran → DB on API failure return this.fanavaranLookupService.getRemoteLookup( clientKey, definition.url, definition.cacheFile, clientKey === "tejaratno" ? { dbSource: () => this.getLookup(lookupName) } : undefined, ); } async getAccidentCauses(): Promise { return await this.getClientRemoteLookup("accident-causes"); } async getVehicleHullAccidentTypes(): Promise { return await this.getClientRemoteLookup("vehicle-hull-accident-types"); } async getVehicleHullAccidentCulpritType(): Promise { return await this.getClientRemoteLookup("vehicle-hull-accident-culprit-type"); } async getVehicleHullDmgKind(): Promise { return await this.getClientRemoteLookup("vehicle-hull-dmg-kind"); } async getVehicleHullDmgCostKinds(): Promise { return await this.getClientRemoteLookup("vehicle-hull-dmg-cost-kinds"); } async getVehicleHullDmgAccessoriesByPolicyId(policyId: number): Promise { const clientKey = this.activeClientKey(); return this.fanavaranLookupService.vehicleHullDmgAccessoriesByPolicyId( clientKey, policyId, { contractIdOverride: resolveFanavaranProductContractId( clientKey, "car-body", ), }, ); } async getAccidentReportType(): Promise { return await this.getClientRemoteLookup("accident-report-type"); } async getVehicleUseTypes(): Promise { return await this.getClientRemoteLookup("vehicle-use-types"); } async getDmgPayMethod(): Promise { return await this.getClientRemoteLookup("dmg-pay-method"); } async getDrivingLicenceTypes(): Promise { return await this.getClientRemoteLookup("driving-licence-types"); } async getAccidentCulpritType(): Promise { return await this.getClientRemoteLookup("accident-culprit-type"); } async getInspectionPlace(): Promise { return await this.getClientRemoteLookup("inspection-place"); } async getDropAmountStatus(): Promise { return await this.getClientRemoteLookup("drop-amount-status"); } async getCarComponents(): Promise { return await this.getClientRemoteLookup("car-components"); } async getAccidentLevel(): Promise { return await this.getClientRemoteLookup("accident-level"); } async getExpertStatus(): Promise { return await this.getClientRemoteLookup("expert-status"); } async getVehicleKinds(): Promise { return await this.getClientRemoteLookup("vehicle-kinds"); } async getPersonRole(): Promise { return await this.getClientRemoteLookup("person-role"); } async getFileTypes(): Promise { return await this.getClientRemoteLookup("file-types"); } async getCities(): Promise { return await this.getClientRemoteLookup("cities"); } async getProvinces(): Promise { return await this.getClientRemoteLookup("provinces"); } async getDmgCaseType(): Promise { return await this.getClientRemoteLookup("dmg-case-type"); } async getDmgHistoryStatus(): Promise { return await this.getClientRemoteLookup("dmg-history-status"); } async getUsedPlace(): Promise { return await this.getClientRemoteLookup("used-place"); } async getDmgBusinessLine(): Promise { return await this.getClientRemoteLookup("dmg-business-line"); } async getGender(): Promise { return await this.getClientRemoteLookup("gender"); } async getMaritalStatus(): Promise { return await this.getClientRemoteLookup("marital-status"); } async getAns(): Promise { return await this.getClientRemoteLookup("ans"); } async getCountries(): Promise { return await this.getClientRemoteLookup("countries"); } async getPersonKind(): Promise { return await this.getClientRemoteLookup("person-kind"); } async getCiiValidationStatus(): Promise { return await this.getClientRemoteLookup("cii-validation-status"); } async otherPersonById(personId: number): Promise { const clientKey = this.activeClientKey(); const person = await this.fanavaranLookupService.getOtherPerson( clientKey, personId, ); if (person == null) { throw new NotFoundException( `Fanavaran other-person ${personId} was not found.`, ); } return person; } 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(); 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( nationalCode: string, insuranceLineId: number = 5, ): Promise { const clientKey = this.activeClientKey(); return this.fanavaranLookupService.myPolicies( clientKey, nationalCode, insuranceLineId, ); } async findLastProcessedCarPolicy( product: FanavaranCarPolicyProduct, query: { nationalCode?: string; vin?: string; plaqueLeft?: string; plaqueLetter?: string; plaqueRight?: string; plaqueSerial?: string; }, ): Promise<{ product: FanavaranCarPolicyProduct; insuranceLine: "CAR_BODY" | "THIRD_PARTY" | null; insuranceLineId: number; policyId: number; policy: any; customer: Record | null; vehicle: Record | null; }> { const parsed = parseLastCarPolicyInput(query); if ("error" in parsed) { throw new BadRequestException(parsed.error); } const clientKey = this.activeClientKey(); const insuranceLineId = insuranceLineIdForProduct(product); const contractIdOverride = resolveFanavaranProductContractId( clientKey, product, ); const requestOptions = { contractIdOverride }; const listed = await this.fanavaranLookupService.myPolicies( clientKey, parsed.nationalCode, insuranceLineId, requestOptions, ); const candidates = sortPoliciesNewestEndDateFirst( filterPoliciesByLine(listed, insuranceLineId), ); let vinVehicleId: number | null = null; if (parsed.vin) { try { const inquired = await this.fanavaranLookupService.inquiryByVin( clientKey, parsed.vin, requestOptions, ); vinVehicleId = pickVehicleId(inquired); } catch (error) { this.logger.warn( `VIN vehicle helper failed; continuing with policy hydrate: ${ error instanceof Error ? error.message : error }`, ); } } const matches: HydratedFanavaranCarPolicy[] = []; let hydrationFailures = 0; for (const row of candidates) { const policyId = parseFanavaranId(row.PolicyId); if (policyId === null) continue; let policyRaw: unknown; try { policyRaw = product === "car-body" ? await this.fanavaranLookupService.bodyPolicyById( clientKey, policyId, requestOptions, ) : await this.fanavaranLookupService.thirdPartyPolicyById( clientKey, policyId, requestOptions, ); } catch (error) { hydrationFailures += 1; this.logger.warn( `Policy GET failed for PolicyId=${policyId}: ${ error instanceof Error ? error.message : error }`, ); continue; } const policy = asObjectRecord(policyRaw); if (!policy) continue; const vehicleId = pickVehicleId(policy.VehicleId ?? policy.vehicleId); if ( parsed.vin && vinVehicleId !== null && vehicleId !== null && vehicleId !== vinVehicleId ) { continue; } let vehicle: Record | null = null; if (vehicleId !== null) { try { vehicle = asObjectRecord( await this.fanavaranLookupService.vehicleById( clientKey, vehicleId, undefined, requestOptions, ), ); } catch (error) { this.logger.warn( `Vehicle GET failed for VehicleId=${vehicleId} (PolicyId=${policyId}): ${ error instanceof Error ? error.message : error }`, ); } } const matchedByVinVehicleId = parsed.vin != null && vinVehicleId != null && vehicleId != null && vinVehicleId === vehicleId; if ( !matchedByVinVehicleId && !vehicleMatchesCar(vehicle, parsed, vinVehicleId) ) { continue; } matches.push({ policyId, beginDate: policy.BeginDate ?? row.BeginDate, endDate: policy.EndDate ?? row.EndDate, vehicleId, policy, vehicle, }); } const selected = selectLastAmongCarMatches(matches); if (!selected) { if (candidates.length === 0) { throw new NotFoundException( `No Fanavaran ${product} policy was found for this national code and car.`, ); } if (hydrationFailures === candidates.length) { throw new NotFoundException( `Fanavaran ${product} policy details could not be loaded for this national code.`, ); } throw new NotFoundException( `No Fanavaran ${product} policy of that line was found for this car.`, ); } // A CAR_BODY case is payable only with current hull coverage. The generic // selector may intentionally return the latest expired policy for history; // that fallback is not valid for a new CAR_BODY case. if ( product === "car-body" && !isPolicyActiveOn(selected, gregorianDateInIran(new Date())) ) { throw new NotFoundException( "No active Fanavaran car-body policy was found for this car.", ); } return { product, insuranceLine: insuranceLineLabel(insuranceLineId), insuranceLineId, policyId: selected.policyId, policy: await this.mapPolicyDetails(selected.policy), customer: await this.fetchCustomer( clientKey, parseFanavaranId(selected.policy.CustomerId), requestOptions, ), vehicle: await this.enrichVehicle(selected.vehicle), }; } private async fetchCustomer( clientKey: ReturnType, customerId: number | null, requestOptions: { contractIdOverride?: string }, ): Promise | null> { if (customerId === null) return null; try { return asObjectRecord( await this.fanavaranLookupService.customerById( clientKey, customerId, requestOptions, ), ); } catch (error) { this.logger.warn( `Customer GET failed for CustomerId=${customerId}: ${ error instanceof Error ? error.message : error }`, ); return null; } } private async lookupRowById( lookupName: string, id: number | null, ): Promise | null> { if (id === null) return null; try { const rows = await this.getClientRemoteLookup(lookupName); if (!Array.isArray(rows)) return null; const match = rows.find( (row) => parseFanavaranId((row as { Id?: unknown })?.Id) === id, ); return asObjectRecord(match); } catch (error) { this.logger.warn( `Failed to resolve ${lookupName} id=${id}: ${ error instanceof Error ? error.message : error }`, ); return null; } } private async enrichVehicle( vehicle: Record | null, ): Promise | null> { if (!vehicle) return null; const vehicleKindId = parseFanavaranId(vehicle.VehicleKindId); const usedId = parseFanavaranId(vehicle.UsedId); return { ...vehicle, color: null, vehicleKind: await this.lookupRowById("vehicle-kinds", vehicleKindId), used: await this.lookupRowById("vehicle-use-types", usedId), plaque: toAppPlaque(vehicle), }; } async mapPolicyDetails(policy: any): Promise { if (!policy || typeof policy !== "object") { return policy; } const mappedPolicy = { ...policy }; // 1. Map PreviousInsuranceCorpId and TransferorInsuranceCorpId try { const companies = (await this.getClientRemoteLookup("insurance-corp")) as any[]; if (Array.isArray(companies)) { if (typeof policy.PreviousInsuranceCorpId === "number") { const match = companies.find((c) => c.Id === policy.PreviousInsuranceCorpId); if (match) { mappedPolicy.PreviousInsuranceCorpName = match.Caption; } } if (typeof policy.TransferorInsuranceCorpId === "number") { const match = companies.find((c) => c.Id === policy.TransferorInsuranceCorpId); if (match) { mappedPolicy.TransferorInsuranceCorpName = match.Caption; } } } } catch (e) { this.logger.warn(`Failed to map insurance-corp lookups: ${e.message}`); } // 2. Map PolicyUsageTypeId try { const useTypes = (await this.getClientRemoteLookup("vehicle-use-types")) as any[]; if (Array.isArray(useTypes) && typeof policy.PolicyUsageTypeId === "number") { const match = useTypes.find((t) => t.Id === policy.PolicyUsageTypeId); if (match) { mappedPolicy.PolicyUsageTypeName = match.Caption; } } } catch (e) { this.logger.warn(`Failed to map vehicle-use-types lookups: ${e.message}`); } return mappedPolicy; } async thirdPartyPolicyById(policyId: number): Promise { const clientKey = this.activeClientKey(); const policy = await this.fanavaranLookupService.thirdPartyPolicyById(clientKey, policyId); return this.mapPolicyDetails(policy); } async bodyPolicyById(policyId: number): Promise { const clientKey = this.activeClientKey(); const policy = await this.fanavaranLookupService.bodyPolicyById( clientKey, policyId, { contractIdOverride: resolveFanavaranProductContractId( clientKey, "car-body", ), }, ); return this.mapPolicyDetails(policy); } async vehicleById(vehicleId: number, versionNo?: number): Promise { const clientKey = this.activeClientKey(); 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 { const clientKey = this.activeClientKey(); return this.fanavaranLookupService.customerById(clientKey, customerId); } async getAccidentWay(): Promise<{ id: number; label: string }[]> { const clientKey = this.activeClientKey(); const fileName = TEJARAT_STATIC_ACCIDENT_FILES.accidentWay; const cached = await this.fanavaranLookupService.readCacheFile< TejaratAccidentLabelRow[] >(clientKey, fileName); const raw = cached ?? (clientKey === "tejaratno" ? await this.fanavaranLookupService.readTejaratStaticAccidentFile< TejaratAccidentLabelRow[] >(fileName) : null); if (!raw) { this.logger.warn( `No accident-way lookup for client ${clientKey}; falling back to tejarat static file`, ); const fallback = await this.fanavaranLookupService.readTejaratStaticAccidentFile< TejaratAccidentLabelRow[] >(fileName); return fallback.map((item) => ({ id: item.id, label: item.persianLabel, })); } return raw.map((item) => ({ id: item.id, label: item.persianLabel })); } async getAccidentReason(): Promise< { id: number; label: string; fanavaran: number }[] > { const clientKey = this.activeClientKey(); if (clientKey === "parsian") { const cacheFile = "accident-reason-options.json"; const cached = await this.fanavaranLookupService.readCacheFile< { id: number; label: string; fanavaran: number }[] >(clientKey, cacheFile); if (cached) { return cached; } const causes = await this.getAccidentCauses(); const mapped = this.fanavaranLookupService.mapFanavaranAccidentCausesToReasonOptions( causes, ); await this.fanavaranLookupService.writeCacheFile( clientKey, cacheFile, mapped, ); return mapped; } const fileName = TEJARAT_STATIC_ACCIDENT_FILES.accidentReason; const cached = await this.fanavaranLookupService.readCacheFile< TejaratAccidentReasonRow[] >(clientKey, fileName); const raw = cached ?? (await this.fanavaranLookupService.readTejaratStaticAccidentFile< TejaratAccidentReasonRow[] >(fileName)); return raw.map((item) => ({ id: item.id, label: item.persianLabel, fanavaran: item.fanavaranID, })); } async getAccidentType(): Promise<{ id: number; label: string }[]> { const clientKey = this.activeClientKey(); const fileName = TEJARAT_STATIC_ACCIDENT_FILES.accidentType; const cached = await this.fanavaranLookupService.readCacheFile< TejaratAccidentLabelRow[] >(clientKey, fileName); const raw = cached ?? (clientKey === "tejaratno" ? await this.fanavaranLookupService.readTejaratStaticAccidentFile< TejaratAccidentLabelRow[] >(fileName) : null); if (!raw) { this.logger.warn( `No accident-type lookup for client ${clientKey}; falling back to tejarat static file`, ); const fallback = await this.fanavaranLookupService.readTejaratStaticAccidentFile< TejaratAccidentLabelRow[] >(fileName); return fallback.map((item) => ({ id: item.id, label: item.persianLabel, })); } return raw.map((item) => ({ id: item.id, label: item.persianLabel })); } async getAccidentFields(): Promise<{ accidentWay: { id: number; label: string }[]; accidentReason: { id: number; label: string; fanavaran: number }[]; accidentType: { id: number; label: string }[]; client: string; }> { const [accidentWay, accidentReason, accidentType] = await Promise.all([ this.getAccidentWay(), this.getAccidentReason(), this.getAccidentType(), ]); return { client: this.activeClientKey(), accidentWay, accidentReason, accidentType, }; } }