forked from Yara724/api
496 lines
15 KiB
TypeScript
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,
|
|
};
|
|
});
|
|
}
|
|
|
|
}
|