fanavaran lookups done

This commit is contained in:
2026-08-03 15:53:36 +03:30
parent 767317cce8
commit 738344a9d4
3 changed files with 102 additions and 7 deletions

View File

@@ -3,6 +3,10 @@ import { HttpModule } from "@nestjs/axios";
import { ConfigModule, ConfigService } from "@nestjs/config";
import { MongooseModule } from "@nestjs/mongoose";
import { createHttpModuleOptions } from "src/core/config/http-proxy.factory";
import {
LookupModel,
LookupSchema,
} from "src/lookups/entities/schema/lookup.schema";
import { FanavaranAuditModule } from "./fanavaran-audit.module";
import { FanavaranAuthService } from "./fanavaran-auth.service";
import { FanavaranClientConfigService } from "./fanavaran-client-config.service";
@@ -29,6 +33,7 @@ import {
name: FanavaranClientConfig.name,
schema: FanavaranClientConfigSchema,
},
{ name: LookupModel.name, schema: LookupSchema },
]),
FanavaranAuditModule,
],

View File

@@ -7,9 +7,15 @@ import {
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,
@@ -24,6 +30,8 @@ export class FanavaranLookupService {
constructor(
private readonly httpService: HttpService,
private readonly fanavaranAuthService: FanavaranAuthService,
@InjectModel(LookupModel.name)
private readonly lookupModel: Model<LookupDocument>,
) {}
private cacheFilePath(clientKey: FanavaranClientKey, fileName: string): string {
@@ -117,34 +125,114 @@ export class FanavaranLookupService {
}
}
/**
* 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,
fallback?: () => Promise<unknown>,
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 (fallback) {
if (dbSource && !preferDbBeforeRemote) {
this.logger.warn(
`Fanavaran lookup fetch failed for ${clientKey}/${cacheFile}; using fallback`,
`Fanavaran lookup fetch failed for ${clientKey}/${cacheFile}; using DB fallback`,
);
const data = await 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,

View File

@@ -61,12 +61,14 @@ export class LookupsService {
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"
? async () => this.getLookup(lookupName)
? { dbSource: () => this.getLookup(lookupName) }
: undefined,
);
}