From f031facfc3c83f7c88ae65929e63e7e0e91c74ed Mon Sep 17 00:00:00 2001 From: Semih Yesilyurt Date: Sat, 26 Sep 2026 21:06:38 +0300 Subject: [PATCH] =?UTF-8?q?fix(carcatonline):=20groups2=20404=20'No=20grou?= =?UTF-8?q?ps=20found'=20=3D=20bo=C5=9F=20dal,=20tarama=20devam=20eder;=20?= =?UTF-8?q?failed=20kayd=C4=B1=20e=C5=9Fleme/varyant=20ilerlemesini=20koru?= =?UTF-8?q?r?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5.1 --- .../carcatonline/carcatonline.tree.spec.ts | 31 +++++++++++++++++++ .../carcatonline/carcatonline.tree.ts | 10 +++++- .../carcatonline-backfill.processor.ts | 11 ++++++- 3 files changed, 50 insertions(+), 2 deletions(-) diff --git a/apps/api/src/integrations/carcatonline/carcatonline.tree.spec.ts b/apps/api/src/integrations/carcatonline/carcatonline.tree.spec.ts index f093e97..d534c86 100644 --- a/apps/api/src/integrations/carcatonline/carcatonline.tree.spec.ts +++ b/apps/api/src/integrations/carcatonline/carcatonline.tree.spec.ts @@ -117,6 +117,37 @@ describe("crawlGroupTree", () => { expect(nodes.find((n) => n.id === "g2")?.illustration).toBe("47-010"); }); + it("treats a 404 'No groups found' subgroup fetch as an empty branch and keeps crawling", async () => { + const client = { + carsParameters: vi.fn(), + cars: vi.fn(), + groups: vi.fn(async (_c: string, _car: string, groupId?: string) => { + if (!groupId) { + return [ + { id: "dead", name: "Dead", hasSubgroups: true, hasParts: false }, + { id: "ok", name: "Ok", hasSubgroups: true, hasParts: false }, + ] as CarcatGroup[]; + } + if (groupId === "dead") throw Object.assign(new Error("No groups found"), { status: 404 }); + return [{ id: "ok1", name: "Leaf", hasSubgroups: false, hasParts: true }] as CarcatGroup[]; + }), + }; + const cache = new Map(); + const cachingHooks = { + beforeCall: vi.fn().mockResolvedValue(undefined), + cache: { + get: async (k: string) => cache.get(k) ?? null, + set: async (k: string, g: CarcatGroup[]) => { + cache.set(k, g); + }, + }, + }; + const { nodes, calls } = await crawlGroupTree(client, cachingHooks, "pl_skoda", "car"); + expect(calls).toBe(3); + expect(nodes.map((n) => n.id)).toEqual(["dead", "ok", "ok1"]); + expect(cache.get("pl_skoda:car:dead")).toEqual([]); + }); + it("aborts when the window closes mid-crawl", async () => { const client = { carsParameters: vi.fn(), diff --git a/apps/api/src/integrations/carcatonline/carcatonline.tree.ts b/apps/api/src/integrations/carcatonline/carcatonline.tree.ts index cbf8c9b..f22c495 100644 --- a/apps/api/src/integrations/carcatonline/carcatonline.tree.ts +++ b/apps/api/src/integrations/carcatonline/carcatonline.tree.ts @@ -141,7 +141,15 @@ export async function crawlGroupTree( let groups = (await hooks.cache?.get(cacheKey)) ?? null; if (!groups) { await hooks.beforeCall(); - groups = await client.groups(catalogId, carId, groupId ?? undefined); + try { + groups = await client.groups(catalogId, carId, groupId ?? undefined); + } catch (err) { + // A node flagged hasSubgroups can still answer 404 "No groups found" + // (seen on pl_skoda). That is a dead branch, not a failure: record it + // as empty (and cache it) so the rest of the tree still lands. + if ((err as { status?: number }).status !== 404) throw err; + groups = []; + } calls += 1; await hooks.cache?.set(cacheKey, groups); } diff --git a/apps/api/src/jobs/processors/carcatonline-backfill.processor.ts b/apps/api/src/jobs/processors/carcatonline-backfill.processor.ts index ae52d37..7b3087c 100644 --- a/apps/api/src/jobs/processors/carcatonline-backfill.processor.ts +++ b/apps/api/src/jobs/processors/carcatonline-backfill.processor.ts @@ -234,6 +234,8 @@ async function fillVehicle( .where(eq(catalogVehicles.id, catalogVehicleId)); }; + /** What we already resolved, kept on a failure record so a retry can resume. */ + let progress: Partial = {}; try { // 1. brand → catalog candidates → model if (isNonVehicleModel(cv.model)) { @@ -287,8 +289,10 @@ async function fillVehicle( parameters: (prev.parameters ?? []).map((p) => ({ ...p, name: p.key })), } : null; + progress = { ...match }; const car = resumable ?? (await resolveRepresentativeCar(client, hooks, match.catalogId, match.modelId)); + if (car) progress = { ...progress, carId: car.carId }; if (car && !resumable) { await setMeta({ status: "matched", @@ -359,7 +363,12 @@ async function fillVehicle( if (err instanceof CarcatonlineWindowClosedError) await reschedule(err.retryAfterMs + 60_000, "window closed mid-crawl"); const message = err instanceof Error ? err.message : String(err); - await setMeta({ status: "failed", error: message.slice(0, 300), at: now().toISOString() }); + await setMeta({ + status: "failed", + ...progress, + error: message.slice(0, 300), + at: now().toISOString(), + }); log.warn(`[carcatonline] ${catalogVehicleId} failed: ${message}`); throw err; }