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); this.logger.log( `[${clientKey}] Calling Fanavaran lookup API: ${url}`, ); 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, }), ); const dataCount = Array.isArray(response.data) ? response.data.length : typeof response.data === "object" && response.data !== null ? Object.keys(response.data).length : 0; this.logger.log( `[${clientKey}] Fanavaran lookup response status=${response.status} dataCount=${dataCount}`, ); this.logger.log( `[${clientKey}] Fanavaran lookup raw response: ${JSON.stringify( response.data, null, 2, )}`, ); if (Array.isArray(response.data) && response.data.length > 0) { const firstItem = response.data[0]; if (firstItem && typeof firstItem === "object") { this.logger.log( `[${clientKey}] Fanavaran lookup first item keys: ${Object.keys( firstItem, ).join(", ")}`, ); } } 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, }; }); } }