From c7fb6174a025a5e54694fd9051770d508b1994a3 Mon Sep 17 00:00:00 2001 From: "s.hajizadeh" Date: Tue, 23 Jun 2026 18:23:09 +0330 Subject: [PATCH] lookups update per client --- src/expert-blame/expert-blame.module.ts | 2 + src/expert-blame/expert-blame.service.ts | 18 +- src/fanavaran/fanavaran-lookup.config.ts | 58 ++++++ src/fanavaran/fanavaran-lookup.module.ts | 10 + src/fanavaran/fanavaran-lookup.service.ts | 241 ++++++++++++++++++++++ src/lookups/lookups.controller.ts | 1 + src/lookups/lookups.module.ts | 2 + src/lookups/lookups.service.ts | 169 +++++++++++++-- 8 files changed, 469 insertions(+), 32 deletions(-) create mode 100644 src/fanavaran/fanavaran-lookup.config.ts create mode 100644 src/fanavaran/fanavaran-lookup.module.ts create mode 100644 src/fanavaran/fanavaran-lookup.service.ts diff --git a/src/expert-blame/expert-blame.module.ts b/src/expert-blame/expert-blame.module.ts index 0471094..d118bfd 100644 --- a/src/expert-blame/expert-blame.module.ts +++ b/src/expert-blame/expert-blame.module.ts @@ -7,6 +7,7 @@ import { ExpertBlameV2Controller } from "./expert-blame.v2.controller"; import { ExpertBlameService } from "./expert-blame.service"; import { RequestManagementModule } from "src/request-management/request-management.module"; import { SmsOrchestrationModule } from "src/sms-orchestration/sms-orchestration.module"; +import { LookupsModule } from "src/lookups/lookups.module"; import { ClaimRequestManagementModule } from "src/claim-request-management/claim-request-management.module"; @Module({ @@ -17,6 +18,7 @@ import { ClaimRequestManagementModule } from "src/claim-request-management/claim PlatesModule, SmsOrchestrationModule, ClaimRequestManagementModule, + LookupsModule, ], controllers: [ExpertBlameController, ExpertBlameV2Controller], providers: [ExpertBlameService], diff --git a/src/expert-blame/expert-blame.service.ts b/src/expert-blame/expert-blame.service.ts index f7de7e0..87991aa 100644 --- a/src/expert-blame/expert-blame.service.ts +++ b/src/expert-blame/expert-blame.service.ts @@ -30,6 +30,7 @@ import { } from "src/helpers/unified-file-status"; import { applyListQueryV2, isInListDateRange, parseListDateRange } from "src/helpers/list-query-v2"; import { ClaimCaseDbService } from "src/claim-request-management/entites/db-service/claim-case.db.service"; +import { LookupsService } from "src/lookups/lookups.service"; import { ListQueryV2Dto } from "src/common/dto/list-query-v2.dto"; import { UnifiedFileStatusReportDto, @@ -126,6 +127,7 @@ export class ExpertBlameService { private readonly smsOrchestrationService: SmsOrchestrationService, private readonly expertFileActivityDbService: ExpertFileActivityDbService, private readonly claimCaseDbService: ClaimCaseDbService, + private readonly lookupsService: LookupsService, ) {} private async loadClaimsByPublicIds( @@ -2424,21 +2426,7 @@ export class ExpertBlameService { } async getAccidentField() { - try { - const ac_reason = await readFile( - "src/static/ACCIDENT_REASON.json", - "utf-8", - ); - const ac_type = await readFile("src/static/ACCIDENT_TYPE.json", "utf-8"); - const ac_way = await readFile("src/static/ACCIDENT_WAY.json", "utf-8"); - return { - accidentReason: JSON.parse(ac_reason), - accidentType: JSON.parse(ac_type), - accidentWay: JSON.parse(ac_way), - }; - } catch (err) { - this.logger.error(err); - } + return await this.lookupsService.getAccidentFields(); } async inPersonVisit(requestId: string, actorDetail: any) { diff --git a/src/fanavaran/fanavaran-lookup.config.ts b/src/fanavaran/fanavaran-lookup.config.ts new file mode 100644 index 0000000..597f3b3 --- /dev/null +++ b/src/fanavaran/fanavaran-lookup.config.ts @@ -0,0 +1,58 @@ +import type { FanavaranClientKey } from "src/core/config/fanavaran-client.config"; + +export const FANAVARAN_LOOKUP_BASE_URL = + "https://apimanager.iraneit.com/BimeApiManager/api/BimeApi/v2.0"; + +export interface FanavaranRemoteLookupDefinition { + name: string; + url: string; + cacheFile: string; +} + +/** Fanavaran API lookups synced per client (Parsian fetches on first call). */ +export const FANAVARAN_REMOTE_LOOKUPS: FanavaranRemoteLookupDefinition[] = [ + { + name: "accident-causes", + url: `${FANAVARAN_LOOKUP_BASE_URL}/car/base-info/accident-causes`, + cacheFile: "accident-causes.json", + }, + { + name: "accident-report-type", + url: `${FANAVARAN_LOOKUP_BASE_URL}/car/code-list/accident-report-type`, + cacheFile: "accident-report-type.json", + }, + { + name: "vehicle-use-types", + url: `${FANAVARAN_LOOKUP_BASE_URL}/car/base-info/vehicle-use-types`, + cacheFile: "vehicle-use-types.json", + }, + { + name: "dmg-pay-method", + url: `${FANAVARAN_LOOKUP_BASE_URL}/car/code-list/dmg-pay-method`, + cacheFile: "dmg-pay-method.json", + }, + { + name: "driving-licence-types", + url: `${FANAVARAN_LOOKUP_BASE_URL}/car/base-info/driving-licence-types`, + cacheFile: "driving-licence-types.json", + }, + { + name: "accident-culprit-type", + url: `${FANAVARAN_LOOKUP_BASE_URL}/car/code-list/accident-culprit-type`, + cacheFile: "accident-culprit-type.json", + }, +]; + +export const TEJARAT_STATIC_ACCIDENT_FILES = { + accidentReason: "ACCIDENT_REASON.json", + accidentWay: "ACCIDENT_WAY.json", + accidentType: "ACCIDENT_TYPE.json", +} as const; + +export function fanavaranLookupCacheDir(clientKey: FanavaranClientKey): string { + return `${process.cwd()}/files/fanavaran-lookups/${clientKey}`; +} + +export function tejaratStaticAccidentFilePath(fileName: string): string { + return `${process.cwd()}/src/static/${fileName}`; +} diff --git a/src/fanavaran/fanavaran-lookup.module.ts b/src/fanavaran/fanavaran-lookup.module.ts new file mode 100644 index 0000000..5a376f9 --- /dev/null +++ b/src/fanavaran/fanavaran-lookup.module.ts @@ -0,0 +1,10 @@ +import { Module } from "@nestjs/common"; +import { HttpModule } from "@nestjs/axios"; +import { FanavaranLookupService } from "./fanavaran-lookup.service"; + +@Module({ + imports: [HttpModule], + providers: [FanavaranLookupService], + exports: [FanavaranLookupService], +}) +export class FanavaranLookupModule {} diff --git a/src/fanavaran/fanavaran-lookup.service.ts b/src/fanavaran/fanavaran-lookup.service.ts new file mode 100644 index 0000000..e281f97 --- /dev/null +++ b/src/fanavaran/fanavaran-lookup.service.ts @@ -0,0 +1,241 @@ +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { HttpService } from "@nestjs/axios"; +import { + BadGatewayException, + Injectable, + Logger, + NotFoundException, +} from "@nestjs/common"; +import { firstValueFrom } from "rxjs"; +import { isAxiosError } from "axios"; +import { + getFanavaranClientProfile, + type FanavaranClientKey, +} from "src/core/config/fanavaran-client.config"; +import { + fanavaranLookupCacheDir, + tejaratStaticAccidentFilePath, +} from "./fanavaran-lookup.config"; + +@Injectable() +export class FanavaranLookupService { + private readonly logger = new Logger(FanavaranLookupService.name); + + private readonly getAppTokenUrl = + "https://apimanager.iraneit.com/BimeApiManager/api/EITAuthentication/GetAppToken"; + private readonly loginUrl = + "https://apimanager.iraneit.com/BimeApiManager/api/EITAuthentication/Login"; + + constructor(private readonly httpService: HttpService) {} + + private cacheFilePath(clientKey: FanavaranClientKey, fileName: string): string { + return join(fanavaranLookupCacheDir(clientKey), fileName); + } + + async readCacheFile( + clientKey: FanavaranClientKey, + fileName: string, + ): Promise { + try { + const content = await readFile( + this.cacheFilePath(clientKey, fileName), + "utf-8", + ); + return JSON.parse(content) as T; + } catch { + return null; + } + } + + async writeCacheFile( + clientKey: FanavaranClientKey, + fileName: string, + data: unknown, + ): Promise { + const dir = fanavaranLookupCacheDir(clientKey); + await mkdir(dir, { recursive: true }); + await writeFile( + join(dir, fileName), + `${JSON.stringify(data, null, 2)}\n`, + "utf-8", + ); + this.logger.log( + `Cached Fanavaran lookup ${fileName} for client ${clientKey}`, + ); + } + + async readTejaratStaticAccidentFile(fileName: string): Promise { + const cached = await readFile(tejaratStaticAccidentFilePath(fileName), "utf-8"); + return JSON.parse(cached) as T; + } + + private async getAppToken(config: { + appName: string; + secret: string; + }): Promise { + const response = await firstValueFrom( + this.httpService.post(this.getAppTokenUrl, "", { + headers: { + appname: config.appName, + secret: config.secret, + "Content-Length": "0", + }, + transformRequest: [ + (_data, headers) => { + if (headers) { + delete headers["Content-Type"]; + delete headers["content-type"]; + } + return _data; + }, + ], + }), + ); + + const appToken = + response.headers.apptoken || + response.headers.appToken || + response.headers["apptoken"] || + response.headers["appToken"]; + + if (!appToken) { + throw new BadGatewayException("Failed to get Fanavaran appToken"); + } + + return appToken; + } + + private async login( + appToken: string, + config: { username: string; password: string }, + ): Promise { + const response = await firstValueFrom( + this.httpService.post(this.loginUrl, "", { + headers: { + appToken, + userName: config.username, + password: config.password, + "Content-Length": "0", + }, + transformRequest: [ + (_data, headers) => { + if (headers) { + delete headers["Content-Type"]; + delete headers["content-type"]; + } + return _data; + }, + ], + }), + ); + + const authenticationToken = + response.headers.authenticationtoken || + response.headers.authenticationToken || + response.headers["authenticationtoken"] || + response.headers["authenticationToken"] || + response.data?.authenticationtoken || + response.data?.authenticationToken || + response.data?.authentication_token; + + if (!authenticationToken) { + throw new BadGatewayException("Failed to get Fanavaran authenticationToken"); + } + + return authenticationToken; + } + + async fetchFromFanavaran( + clientKey: FanavaranClientKey, + url: string, + ): Promise { + const profile = getFanavaranClientProfile(clientKey); + + try { + const appToken = await this.getAppToken(profile.auth); + const authenticationToken = await this.login(appToken, profile.auth); + + const response = await firstValueFrom( + this.httpService.get(url, { + headers: { + authenticationToken, + CorpId: profile.auth.corpId, + ContractId: profile.auth.contractId, + Location: profile.auth.location, + "Content-Type": "application/json", + }, + timeout: 20000, + }), + ); + + return response.data; + } catch (error) { + const message = isAxiosError(error) + ? error.response?.data?.Message || + error.response?.data?.message || + error.message + : error instanceof Error + ? error.message + : "Fanavaran lookup request failed"; + + this.logger.error( + `Fanavaran lookup fetch failed for ${clientKey} (${url}): ${message}`, + ); + throw new BadGatewayException(String(message)); + } + } + + async getRemoteLookup( + clientKey: FanavaranClientKey, + url: string, + cacheFile: string, + fallback?: () => Promise, + ): Promise { + const cached = await this.readCacheFile(clientKey, cacheFile); + if (cached !== null) { + return cached; + } + + try { + const data = await this.fetchFromFanavaran(clientKey, url); + await this.writeCacheFile(clientKey, cacheFile, data); + return data; + } catch (error) { + if (fallback) { + this.logger.warn( + `Fanavaran lookup fetch failed for ${clientKey}/${cacheFile}; using fallback`, + ); + const data = await fallback(); + await this.writeCacheFile(clientKey, cacheFile, data); + return data; + } + throw error; + } + } + + mapFanavaranAccidentCausesToReasonOptions( + causes: unknown, + ): { id: number; label: string; fanavaran: number }[] { + if (!Array.isArray(causes)) { + throw new NotFoundException("Fanavaran accident-causes response is invalid"); + } + + return causes + .filter( + (item) => + item && + typeof item === "object" && + (item as { IsActive?: number }).IsActive === 1 && + typeof (item as { Id?: unknown }).Id === "number", + ) + .map((item) => { + const row = item as { Id: number; Caption?: string }; + return { + id: row.Id, + label: row.Caption ?? String(row.Id), + fanavaran: row.Id, + }; + }); + } +} diff --git a/src/lookups/lookups.controller.ts b/src/lookups/lookups.controller.ts index caafd56..6223343 100644 --- a/src/lookups/lookups.controller.ts +++ b/src/lookups/lookups.controller.ts @@ -163,6 +163,7 @@ export class LookupsController { schema: { type: "object", properties: { + client: { type: "string", example: "tejaratno" }, accidentWay: { type: "array", items: { diff --git a/src/lookups/lookups.module.ts b/src/lookups/lookups.module.ts index 0b60716..0719158 100644 --- a/src/lookups/lookups.module.ts +++ b/src/lookups/lookups.module.ts @@ -1,5 +1,6 @@ import { Module } from "@nestjs/common"; import { MongooseModule } from "@nestjs/mongoose"; +import { FanavaranLookupModule } from "src/fanavaran/fanavaran-lookup.module"; import { LookupDbService } from "./entities/db-service/lookup.db.service"; import { LookupModel, LookupSchema } from "./entities/schema/lookup.schema"; import { LookupsController } from "./lookups.controller"; @@ -7,6 +8,7 @@ import { LookupsService } from "./lookups.service"; @Module({ imports: [ + FanavaranLookupModule, MongooseModule.forFeature([ { name: LookupModel.name, schema: LookupSchema }, ]), diff --git a/src/lookups/lookups.service.ts b/src/lookups/lookups.service.ts index 43bb4bc..5551cc3 100644 --- a/src/lookups/lookups.service.ts +++ b/src/lookups/lookups.service.ts @@ -1,15 +1,47 @@ -import { readFile } from "node:fs/promises"; import { Injectable, Logger, NotFoundException } from "@nestjs/common"; +import { resolveFanavaranClientKey } from "src/core/config/fanavaran-client.config"; +import { + FANAVARAN_REMOTE_LOOKUPS, + TEJARAT_STATIC_ACCIDENT_FILES, +} from "src/fanavaran/fanavaran-lookup.config"; +import { FanavaranLookupService } from "src/fanavaran/fanavaran-lookup.service"; import { LookupDbService } from "./entities/db-service/lookup.db.service"; +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) {} + constructor( + private readonly lookupDbService: LookupDbService, + private readonly fanavaranLookupService: FanavaranLookupService, + ) {} + + private activeClientKey() { + return resolveFanavaranClientKey(); + } + + private findRemoteLookup(name: string) { + const lookup = FANAVARAN_REMOTE_LOOKUPS.find((item) => item.name === name); + if (!lookup) { + throw new Error(`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 }); @@ -20,42 +52,114 @@ export class LookupsService { return doc.response; } + private async getClientRemoteLookup(lookupName: string): Promise { + const clientKey = this.activeClientKey(); + const definition = this.findRemoteLookup(lookupName); + + return this.fanavaranLookupService.getRemoteLookup( + clientKey, + definition.url, + definition.cacheFile, + clientKey === "tejaratno" + ? async () => this.getLookup(lookupName) + : undefined, + ); + } + async getAccidentCauses(): Promise { - return await this.getLookup("accident-causes"); + return await this.getClientRemoteLookup("accident-causes"); } async getAccidentReportType(): Promise { - return await this.getLookup("accident-report-type"); + return await this.getClientRemoteLookup("accident-report-type"); } async getVehicleUseTypes(): Promise { - return await this.getLookup("vehicle-use-types"); + return await this.getClientRemoteLookup("vehicle-use-types"); } async getDmgPayMethod(): Promise { - return await this.getLookup("dmg-pay-method"); + return await this.getClientRemoteLookup("dmg-pay-method"); } async getDrivingLicenceTypes(): Promise { - return await this.getLookup("driving-licence-types"); + return await this.getClientRemoteLookup("driving-licence-types"); } async getAccidentCulpritType(): Promise { - return await this.getLookup("accident-culprit-type"); + return await this.getClientRemoteLookup("accident-culprit-type"); } async getAccidentWay(): Promise<{ id: number; label: string }[]> { - const raw: { id: number; persianLabel: string }[] = JSON.parse( - await readFile("src/static/ACCIDENT_WAY.json", "utf-8"), - ); + 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 raw: { id: number; persianLabel: string; fanavaranID: number }[] = - JSON.parse(await readFile("src/static/ACCIDENT_REASON.json", "utf-8")); + 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, @@ -64,9 +168,34 @@ export class LookupsService { } async getAccidentType(): Promise<{ id: number; label: string }[]> { - const raw: { id: number; persianLabel: string }[] = JSON.parse( - await readFile("src/static/ACCIDENT_TYPE.json", "utf-8"), - ); + 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 })); } @@ -74,12 +203,18 @@ export class LookupsService { 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 { accidentWay, accidentReason, accidentType }; + return { + client: this.activeClientKey(), + accidentWay, + accidentReason, + accidentType, + }; } }