Files
yara724-api/src/fanavaran/fanavaran-lookup.service.ts
2026-09-16 16:05:54 +03:30

496 lines
15 KiB
TypeScript

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 { InjectModel } from "@nestjs/mongoose";
import { Model } from "mongoose";
import { firstValueFrom } from "rxjs";
import { isAxiosError } from "axios";
import { type FanavaranClientKey } from "src/core/config/fanavaran-client.config";
import {
LookupModel,
type LookupDocument,
} from "src/lookups/entities/schema/lookup.schema";
import {
FANAVARAN_LOOKUP_BASE_URL,
fanavaranLookupCacheDir,
tejaratStaticAccidentFilePath,
} from "./fanavaran-lookup.config";
import { FanavaranAuthService } from "./fanavaran-auth.service";
@Injectable()
export class FanavaranLookupService {
private readonly logger = new Logger(FanavaranLookupService.name);
constructor(
private readonly httpService: HttpService,
private readonly fanavaranAuthService: FanavaranAuthService,
@InjectModel(LookupModel.name)
private readonly lookupModel: Model<LookupDocument>,
) {}
private cacheFilePath(clientKey: FanavaranClientKey, fileName: string): string {
return join(fanavaranLookupCacheDir(clientKey), fileName);
}
async readCacheFile<T>(
clientKey: FanavaranClientKey,
fileName: string,
): Promise<T | null> {
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<void> {
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<T>(fileName: string): Promise<T> {
const cached = await readFile(tejaratStaticAccidentFilePath(fileName), "utf-8");
return JSON.parse(cached) as T;
}
async fetchFromFanavaran(
clientKey: FanavaranClientKey,
url: string,
options?: { contractIdOverride?: string },
): Promise<unknown> {
try {
const headers = await this.fanavaranAuthService.getRequestHeaders(
clientKey,
{ contractIdOverride: options?.contractIdOverride },
);
this.logger.log(
`[${clientKey}] Calling Fanavaran lookup API: ${url}`,
);
const response = await firstValueFrom(
this.httpService.get(url, {
headers: {
...headers,
"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.fanavaranAuthService.clearBackoff(clientKey);
return response.data;
} catch (error) {
this.fanavaranAuthService.registerFailure(clientKey, 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));
}
}
/**
* Resolve a remote Fanavaran lookup.
*
* Parsian (default): file → `lookups` collection → Fanavaran API
* Tejaratno with dbSource: file → Fanavaran → DB on API failure
* Others: file → Fanavaran
*/
async getRemoteLookup(
clientKey: FanavaranClientKey,
url: string,
cacheFile: string,
options?:
| (() => Promise<unknown>)
| {
/** Soft/hard DB read — return null/undefined to miss. */
dbSource?: () => Promise<unknown | null | undefined>;
/** When true: try DB after file miss, before Fanavaran. */
preferDbBeforeRemote?: boolean;
},
): Promise<unknown> {
const normalized =
typeof options === "function"
? { dbSource: options, preferDbBeforeRemote: false }
: options ?? {};
const preferDbBeforeRemote =
normalized.preferDbBeforeRemote ?? clientKey === "parsian";
const dbSource =
normalized.dbSource ??
(preferDbBeforeRemote
? () => this.readLookupsCollectionByCacheFile(cacheFile)
: undefined);
const cached = await this.readCacheFile(clientKey, cacheFile);
if (cached !== null) {
return cached;
}
if (preferDbBeforeRemote && dbSource) {
const fromDb = await this.tryDbSource(clientKey, cacheFile, dbSource);
if (fromDb !== null) {
await this.writeCacheFile(clientKey, cacheFile, fromDb);
return fromDb;
}
}
try {
const data = await this.fetchFromFanavaran(clientKey, url);
await this.writeCacheFile(clientKey, cacheFile, data);
return data;
} catch (error) {
if (dbSource && !preferDbBeforeRemote) {
this.logger.warn(
`Fanavaran lookup fetch failed for ${clientKey}/${cacheFile}; using DB fallback`,
);
const data = await dbSource();
if (data != null) {
await this.writeCacheFile(clientKey, cacheFile, data);
return data;
}
}
throw error;
}
}
private lookupNameFromCacheFile(cacheFile: string): string {
return cacheFile.replace(/\.json$/i, "");
}
private async readLookupsCollectionByCacheFile(
cacheFile: string,
): Promise<unknown | null> {
const name = this.lookupNameFromCacheFile(cacheFile);
const doc = await this.lookupModel.findOne({ name }).lean().exec();
if (!doc || doc.response == null) {
return null;
}
return doc.response;
}
private async tryDbSource(
clientKey: FanavaranClientKey,
cacheFile: string,
dbSource: () => Promise<unknown | null | undefined>,
): Promise<unknown | null> {
try {
const data = await dbSource();
if (data == null) {
this.logger.debug(
`[${clientKey}] No DB lookup for ${cacheFile}; will try Fanavaran`,
);
return null;
}
this.logger.log(
`[${clientKey}] Using lookups collection for ${cacheFile} (before Fanavaran)`,
);
return data;
} catch (error) {
this.logger.warn(
`[${clientKey}] DB lookup miss for ${cacheFile}: ${
error instanceof Error ? error.message : error
}; will try Fanavaran`,
);
return null;
}
}
async inquiryByVin(
clientKey: FanavaranClientKey,
vin: string,
options?: { contractIdOverride?: string },
): Promise<unknown> {
const url = `${FANAVARAN_LOOKUP_BASE_URL}/car/vehicles/inquiry-by-vin?vin=${encodeURIComponent(vin)}`;
return this.fetchFromFanavaran(clientKey, url, options);
}
async myPolicies(
clientKey: FanavaranClientKey,
nationalCode: string,
insuranceLineId: number = 5,
options?: { contractIdOverride?: string },
): Promise<unknown> {
const url =
`${FANAVARAN_LOOKUP_BASE_URL}/common/Policies/inquiry-my-policies` +
`?InsuranceLineId=${insuranceLineId}` +
`&NationalCode=${encodeURIComponent(nationalCode)}`;
return this.fetchFromFanavaran(clientKey, url, options);
}
async thirdPartyPolicyById(
clientKey: FanavaranClientKey,
policyId: number,
options?: { contractIdOverride?: string },
): Promise<unknown> {
const url = `${FANAVARAN_LOOKUP_BASE_URL}/car/third-party-car-policies/${policyId}`;
return this.fetchFromFanavaran(clientKey, url, options);
}
async bodyPolicyById(
clientKey: FanavaranClientKey,
policyId: number,
options?: { contractIdOverride?: string },
): Promise<unknown> {
const url = `${FANAVARAN_LOOKUP_BASE_URL}/car/vehicle-hull-policies/${policyId}`;
return this.fetchFromFanavaran(clientKey, url, options);
}
/** GEN.06 VehicleHullAccessoryId — requires hull PolicyId (کد رایانه بیمه‌نامه). */
async vehicleHullDmgAccessoriesByPolicyId(
clientKey: FanavaranClientKey,
policyId: number,
options?: { contractIdOverride?: string },
): Promise<unknown> {
const url = `${FANAVARAN_LOOKUP_BASE_URL}/car/vehicle-hull-policies/${policyId}/dmg-accessories`;
return this.fetchFromFanavaran(clientKey, url, options);
}
async vehicleById(
clientKey: FanavaranClientKey,
vehicleId: number,
versionNo?: number,
options?: { contractIdOverride?: string },
): Promise<unknown> {
const query = versionNo == null ? "" : `?versionno=${versionNo}`;
const url = `${FANAVARAN_LOOKUP_BASE_URL}/car/vehicles/${vehicleId}${query}`;
return this.fetchFromFanavaran(clientKey, url, options);
}
async customerById(
clientKey: FanavaranClientKey,
customerId: number,
options?: { contractIdOverride?: string },
): Promise<unknown> {
const url = `${FANAVARAN_LOOKUP_BASE_URL}/common/customers/${customerId}`;
return this.fetchFromFanavaran(clientKey, url, options);
}
async inquiryByUniqueIdentifier(
clientKey: FanavaranClientKey,
params: {
nationalCode: string;
birthYear: number;
birthMonth: number;
birthDay: number;
},
): Promise<unknown> {
const url =
`${FANAVARAN_LOOKUP_BASE_URL}/common/parties/inquiry-by-unique-identifier` +
`?NationalCode=${encodeURIComponent(params.nationalCode)}` +
`&BirthYear=${params.birthYear}` +
`&BirthMonth=${params.birthMonth}` +
`&BirthDay=${params.birthDay}`;
return this.fetchFromFanavaran(clientKey, url);
}
async getOtherPerson(
clientKey: FanavaranClientKey,
personId: number,
): Promise<unknown> {
const url = `${FANAVARAN_LOOKUP_BASE_URL}/common/other-people/${personId}`;
return this.fetchFromFanavaran(clientKey, url);
}
async createOtherPerson(
clientKey: FanavaranClientKey,
payload: Record<string, unknown>,
): Promise<unknown> {
const url = `${FANAVARAN_LOOKUP_BASE_URL}/common/other-people`;
return this.postToFanavaran(clientKey, url, payload);
}
async postToFanavaran(
clientKey: FanavaranClientKey,
url: string,
payload: Record<string, unknown>,
): Promise<unknown> {
try {
const headers = await this.fanavaranAuthService.getRequestHeaders(
clientKey,
);
this.logger.log(`[${clientKey}] POST Fanavaran: ${url}`);
const response = await firstValueFrom(
this.httpService.post(url, payload, {
headers: {
...headers,
"Content-Type": "application/json",
},
timeout: 20000,
}),
);
this.fanavaranAuthService.clearBackoff(clientKey);
return response.data;
} catch (error) {
this.fanavaranAuthService.registerFailure(clientKey, error);
const message = isAxiosError(error)
? error.response?.data?.Message ||
error.response?.data?.message ||
error.message
: error instanceof Error
? error.message
: "Fanavaran POST request failed";
this.logger.error(
`Fanavaran POST failed for ${clientKey} (${url}): ${message}`,
);
throw new BadGatewayException(String(message));
}
}
async resolveInsuranceCorpId(
clientKey: FanavaranClientKey,
): Promise<number | null> {
const caption = process.env.INSURANCE_CORP_ID?.trim();
if (!caption) {
this.logger.warn("resolveInsuranceCorpId: INSURANCE_CORP_ID env not set");
return null;
}
// Check resolved cache first
const cachedId = await this.readCacheFile<number>(clientKey, "insurance-corp-id-resolved.json");
if (cachedId !== null) {
this.logger.log(`resolveInsuranceCorpId: using cached id=${cachedId} for "${caption}"`);
return cachedId;
}
// Try fetching the full list directly from Fanavaran
let companies: unknown;
const url = `${FANAVARAN_LOOKUP_BASE_URL}/common/code-list/insurance-corp`;
try {
companies = await this.fetchFromFanavaran(clientKey, url);
} catch (error) {
this.logger.warn(
`resolveInsuranceCorpId: Fanavaran fetch failed, trying local lookup endpoint`,
);
// Fallback: call our own local lookup endpoint
try {
const localPort = process.env.PORT || 3000;
const response = await firstValueFrom(
this.httpService.get(`http://localhost:${localPort}/lookups/fanavaran/insurance-corp`, {
timeout: 10000,
}),
);
companies = response.data;
} catch (localError) {
this.logger.error(
`resolveInsuranceCorpId: both Fanavaran and local lookup failed`,
);
return null;
}
}
if (!Array.isArray(companies)) {
this.logger.warn(
`resolveInsuranceCorpId: insurance-corp response is not an array, type=${typeof companies}`,
);
return null;
}
this.logger.log(
`resolveInsuranceCorpId: got ${companies.length} companies, searching for "${caption}"`,
);
const normalizedCaption = caption
.replace(/\s+/g, " ")
.toLowerCase()
.trim();
const match = companies.find((c: any) => {
if (c?.IsActive !== 1) return false;
if (typeof c?.Id !== "number") return false;
const cCaption = typeof c?.Caption === "string" ? c.Caption : "";
const normalized = cCaption.replace(/\s+/g, " ").toLowerCase().trim();
return normalized.includes(normalizedCaption) || normalizedCaption.includes(normalized);
});
if (!match) {
this.logger.warn(
`resolveInsuranceCorpId: no active company matching "${caption}". Available: ` +
companies
.filter((c: any) => c?.IsActive === 1)
.map((c: any) => `${c.Caption} (${c.Id})`)
.join(", "),
);
return null;
}
const corpId = match.Id as number;
// Cache both the resolved id and the full list for future use
await this.writeCacheFile(clientKey, "insurance-corp-id-resolved.json", corpId);
await this.writeCacheFile(clientKey, "insurance-corp.json", companies);
this.logger.log(`resolveInsuranceCorpId: resolved id=${corpId} for "${caption}"`);
return corpId;
}
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,
};
});
}
}