Files
yara724-api/src/lookups/lookups.service.ts

642 lines
18 KiB
TypeScript

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 { LookupDbService } from "./entities/db-service/lookup.db.service";
import {
asObjectRecord,
filterPoliciesByLine,
insuranceLineIdForProduct,
insuranceLineLabel,
parseFanavaranId,
parseLastCarPolicyInput,
pickVehicleId,
selectLastAmongCarMatches,
sortPoliciesNewestEndDateFirst,
toAppPlaque,
vehicleMatchesCar,
type FanavaranCarPolicyProduct,
type HydratedFanavaranCarPolicy,
} from "./fanavaran-last-car-policy";
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<any> {
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<unknown> {
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<any> {
return await this.getClientRemoteLookup("accident-causes");
}
async getAccidentReportType(): Promise<any> {
return await this.getClientRemoteLookup("accident-report-type");
}
async getVehicleUseTypes(): Promise<any> {
return await this.getClientRemoteLookup("vehicle-use-types");
}
async getDmgPayMethod(): Promise<any> {
return await this.getClientRemoteLookup("dmg-pay-method");
}
async getDrivingLicenceTypes(): Promise<any> {
return await this.getClientRemoteLookup("driving-licence-types");
}
async getAccidentCulpritType(): Promise<any> {
return await this.getClientRemoteLookup("accident-culprit-type");
}
async getInspectionPlace(): Promise<any> {
return await this.getClientRemoteLookup("inspection-place");
}
async getDropAmountStatus(): Promise<any> {
return await this.getClientRemoteLookup("drop-amount-status");
}
async getCarComponents(): Promise<any> {
return await this.getClientRemoteLookup("car-components");
}
async getAccidentLevel(): Promise<any> {
return await this.getClientRemoteLookup("accident-level");
}
async getExpertStatus(): Promise<any> {
return await this.getClientRemoteLookup("expert-status");
}
async getVehicleKinds(): Promise<any> {
return await this.getClientRemoteLookup("vehicle-kinds");
}
async getPersonRole(): Promise<any> {
return await this.getClientRemoteLookup("person-role");
}
async getFileTypes(): Promise<any> {
return await this.getClientRemoteLookup("file-types");
}
async getCities(): Promise<any> {
return await this.getClientRemoteLookup("cities");
}
async getProvinces(): Promise<any> {
return await this.getClientRemoteLookup("provinces");
}
async getDmgCaseType(): Promise<any> {
return await this.getClientRemoteLookup("dmg-case-type");
}
async getDmgHistoryStatus(): Promise<any> {
return await this.getClientRemoteLookup("dmg-history-status");
}
async getUsedPlace(): Promise<any> {
return await this.getClientRemoteLookup("used-place");
}
async getDmgBusinessLine(): Promise<any> {
return await this.getClientRemoteLookup("dmg-business-line");
}
async inquiryByVin(vin: string): Promise<unknown> {
const clientKey = this.activeClientKey();
return this.fanavaranLookupService.inquiryByVin(clientKey, vin);
}
async myPolicies(
nationalCode: string,
insuranceLineId: number = 5,
): Promise<unknown> {
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<string, unknown> | null;
vehicle: Record<string, unknown> | 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<string, unknown> | 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.`,
);
}
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<LookupsService["activeClientKey"]>,
customerId: number | null,
requestOptions: { contractIdOverride?: string },
): Promise<Record<string, unknown> | 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<Record<string, unknown> | 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<string, unknown> | null,
): Promise<Record<string, unknown> | 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<any> {
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<unknown> {
const clientKey = this.activeClientKey();
const policy = await this.fanavaranLookupService.thirdPartyPolicyById(clientKey, policyId);
return this.mapPolicyDetails(policy);
}
async bodyPolicyById(policyId: number): Promise<unknown> {
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<unknown> {
const clientKey = this.activeClientKey();
return this.fanavaranLookupService.vehicleById(
clientKey,
vehicleId,
versionNo,
);
}
async customerById(customerId: number): Promise<unknown> {
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,
};
}
}