feat(api): identify-but-no-catalog fallback instead of a bare "unsupported"

When no catalog source can decode a VIN, the decode dead-ended with "Şase
numarası tanınamadı. Marka desteklenmiyor." even for valid, recognizable cars.
Now, before that error, run a best-effort identification (offline Corgi WMI +
NHTSA); if it yields a brand/model/year, tell the dealer what the car is and that
the request was logged ("Bu araç X olarak tanındı, ancak ... katalog henüz yok.
Talebiniz kaydedildi."). Logged distinctly (identified_no_catalog) so the coverage
backlog can be mined. Skipped on budget-abort.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-25 01:47:36 +03:00
parent afc68fa2d7
commit 59e40d6ccd

View File

@@ -121,8 +121,32 @@ export class VehiclesService {
const resolved = await this.resolveVin(vin, pcatCarId, emexCarIndex, userId, ctx); const resolved = await this.resolveVin(vin, pcatCarId, emexCarIndex, userId, ctx);
if (!resolved) { if (!resolved) {
const finalSource = ctx.timings.aborted ? "aborted" : "none"; const aborted = !!ctx.timings.aborted;
const errMsg = ctx.timings.aborted // No catalog source has this VIN. Best-effort identification (offline Corgi
// WMI + NHTSA) so we don't dead-end with a bare "unsupported" — at least tell
// the dealer what car it is. The miss is logged (with wmi) for the coverage
// backlog either way. Skip on budget-abort (already a timeout, don't pile on).
if (!aborted) {
const basic = await this.identifyBasic(vin);
if (basic) {
ctx.timings.identified_no_catalog = 1;
await this.logQuery(
userId,
vin,
null,
"none",
false,
Date.now() - startTime,
`No catalog — identified as ${basic}`,
ctx.timings,
);
throw new BadRequestException(
`Bu araç ${basic} olarak tanındı, ancak bu şase için parça kataloğu henüz mevcut değil. Talebiniz kaydedildi.`,
);
}
}
const finalSource = aborted ? "aborted" : "none";
const errMsg = aborted
? `Decode budget exceeded (${VehiclesService.RESOLVE_BUDGET_MS}ms)` ? `Decode budget exceeded (${VehiclesService.RESOLVE_BUDGET_MS}ms)`
: "Unknown VIN/brand"; : "Unknown VIN/brand";
await this.logQuery( await this.logQuery(
@@ -500,6 +524,31 @@ export class VehiclesService {
return "timeout"; return "timeout";
} }
/**
* Best-effort identification for VINs no catalog could decode: offline Corgi WMI
* → brand, NHTSA → model/year. Returns a display string ("Renault Clio 2018") or
* null if even the brand is unknown. Only used to give the user a meaningful
* "identified but no catalog yet" message instead of a bare "unsupported".
*/
private async identifyBasic(vin: string): Promise<string | null> {
const corgi = this.corgiService.decodeVin(vin);
let brand: string | null = corgi?.isKnown ? corgi.brandName : null;
let model: string | null = null;
let year: string | number | null = corgi?.modelYear ?? null;
try {
const nhtsa = await this.vinApiService.decodeVin(vin);
if (nhtsa) {
brand = brand || nhtsa.make || null;
model = nhtsa.model || null;
year = year || nhtsa.modelYear || null;
}
} catch {
// NHTSA is best-effort; a brand from Corgi alone is still useful.
}
if (!brand && !model) return null;
return [brand, model, year].filter(Boolean).join(" ").trim() || null;
}
/** Actual decode chain — does NOT touch the cache or lock. */ /** Actual decode chain — does NOT touch the cache or lock. */
private async doResolveVin( private async doResolveVin(
vin: string, vin: string,