feat(catalog): Ford de variant-drill — parse json-sub-group → image-board parts
Ford parts are licensed only on the de account. The de drill chain is:
group.action (engine/transmission restriction nodes, HTML) → json-main-group.action
(maingroups JSON) → json-sub-group.action (subgroups JSON) → image-board.action (parts).
The existing json-sub-group handler only knew the Nissan shape ({caption,code,url});
Ford de subgroups are {caption,id,subheader,jsonUrl:null,url:null} — leaves carry no
URL. Add parseFordSubGroupsJson: skip subheader rows, and for each leaf construct the
illustration URL (subGroupId = mainGroupId+subheaderId, bomId = subGroupId+leafId →
image-board.action?bomId=…), which drills to parts via the existing image-board path.
Route Ford (fordp/fordt) to de alongside Hyundai/Kia/Nissan.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -49,7 +49,7 @@ export interface P4BrandHooks {
|
||||
// Brands whose parts are licensed only on the de (account 2) PL24 subscription —
|
||||
// the tr account serves a NOT_LOGGED_IN_DEMO page for these. All P4 fetches for
|
||||
// these services are routed to de (see fetchP4Page + resolveAccount).
|
||||
const HYUNDAI_KIA_NISSAN_DE = new Set([
|
||||
const LEGACY_DE_SERVICES = new Set([
|
||||
"hyundai_parts",
|
||||
"kia_parts",
|
||||
"nissan_parts",
|
||||
@@ -263,13 +263,18 @@ export class PL24FordLegacyService {
|
||||
// Nissan/Infiniti/Opel: json-sub-group.action
|
||||
if (linkPath.includes("json-sub-group.action")) {
|
||||
const basePath = linkPath.substring(0, linkPath.lastIndexOf("/") + 1);
|
||||
// Nissan format: { subgroups: [...] }
|
||||
// Nissan format: { subgroups: [{caption, code, url}] }
|
||||
// Opel format: may use same { subgroups } or { vCfgData }
|
||||
let groups = this.parseNissanSubGroupJson(html, basePath);
|
||||
if (groups.length === 0) {
|
||||
// Try Opel vCfgData format
|
||||
groups = this.parseOpelVCfgDataJson(html, basePath);
|
||||
}
|
||||
if (groups.length === 0) {
|
||||
// Ford de format: { subgroups: [{caption, id, subheader, jsonUrl:null}] } →
|
||||
// construct image-board.action leaves (bomId = mainGroupId+subheaderId+leafId).
|
||||
groups = this.parseFordSubGroupsJson(html, linkPath, basePath);
|
||||
}
|
||||
if (groups.length === 0) {
|
||||
// Debug: log the response
|
||||
this.logger.log(
|
||||
@@ -1802,59 +1807,6 @@ export class PL24FordLegacyService {
|
||||
`Ford: ${groups.length} groups for ${serviceName}/${familyId} catCode=${catCode ?? "none"} arch=${arch}`,
|
||||
);
|
||||
|
||||
// TEMP DEBUG: map the de Ford variant-tree → drill structure.
|
||||
if (arch === "LEGACY_FORD" && groups.length > 0) {
|
||||
this.logger.warn(
|
||||
`FORD-MAP nodes: ${JSON.stringify(groups.slice(0, 14).map((g) => ({ n: g.nameTr?.slice(0, 28), lp: g.linkPath?.slice(0, 90) })))}`,
|
||||
);
|
||||
// Follow the full Ford de JSON chain: maingroup → subgroup → (parts?)
|
||||
const eng = groups.find((x) => /Diesel|Sigma|Zetec|GTDI|Fox/i.test(x.nameTr || "")) || groups[0];
|
||||
if (eng?.linkPath) {
|
||||
const mg = await this.fetchP4Page(eng.linkPath, serviceName, false);
|
||||
let mgj: any = null;
|
||||
try {
|
||||
mgj = JSON.parse(mg as string);
|
||||
} catch {}
|
||||
const mgs = mgj?.maingroups || [];
|
||||
this.logger.warn(`FORD-MAP L2 maingroups=${mgs.length}: ${JSON.stringify(mgs.slice(0, 4).map((x: any) => ({ c: x.caption, u: (x.jsonUrl || "").slice(0, 60) })))}`);
|
||||
if (mgs[0]?.jsonUrl) {
|
||||
const sg = await this.fetchP4Page(mgs[0].jsonUrl, serviceName, false);
|
||||
let sgj: any = null;
|
||||
try {
|
||||
sgj = JSON.parse(sg as string);
|
||||
} catch {}
|
||||
const subs = sgj?.subgroups || [];
|
||||
const realLeaf = subs.find((x: any) => x.subheader !== true && x.id) || subs[0];
|
||||
this.logger.warn(`FORD-MAP L3 subs=${subs.length} realLeaf id=${realLeaf?.id} cap="${realLeaf?.caption}" subheader=${realLeaf?.subheader}`);
|
||||
// probe candidate parts endpoints for the leaf, derived from the json-sub-group URL
|
||||
if (realLeaf?.id) {
|
||||
const base = mgs[0].jsonUrl as string;
|
||||
const sel = await this.fetchP4Page(`${base}&subGroupId=${realLeaf.id}`, serviceName, false);
|
||||
let selj: any = null;
|
||||
try {
|
||||
selj = JSON.parse(sel as string);
|
||||
} catch {}
|
||||
if (selj) {
|
||||
const keys = Object.keys(selj);
|
||||
const arrays = keys.filter((k) => Array.isArray(selj[k])).map((k) => `${k}[${selj[k].length}]`);
|
||||
this.logger.warn(`FORD-MAP sel keys=${keys.join(",")} arrays=${arrays.join(",")}`);
|
||||
// dump first item of any non-subgroups array (potential parts)
|
||||
for (const k of keys) {
|
||||
if (k !== "subgroups" && Array.isArray(selj[k]) && selj[k].length) {
|
||||
this.logger.warn(`FORD-MAP sel.${k}[0]=${JSON.stringify(selj[k][0]).slice(0, 300)}`);
|
||||
}
|
||||
}
|
||||
// does the SELECTED subgroup object now carry parts/url?
|
||||
const selSub = (selj.subgroups || []).find((x: any) => x.id === realLeaf.id);
|
||||
this.logger.warn(`FORD-MAP selSub keys=${selSub ? Object.keys(selSub).join(",") : "none"} url=${selSub?.url} jsonUrl=${selSub?.jsonUrl}`);
|
||||
} else {
|
||||
this.logger.warn(`FORD-MAP sel not-json len=${sel?.length} head=${sel?.slice(0, 120).replace(/\s+/g, " ")}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (groups.length > 0) {
|
||||
await this.redis.setJson(cacheKey, groups, 7200);
|
||||
}
|
||||
@@ -1955,7 +1907,7 @@ export class PL24FordLegacyService {
|
||||
if (serviceName && ["fiatp_parts", "fiatt_parts"].includes(serviceName)) {
|
||||
return "de";
|
||||
}
|
||||
if (serviceName && HYUNDAI_KIA_NISSAN_DE.has(serviceName)) {
|
||||
if (serviceName && LEGACY_DE_SERVICES.has(serviceName)) {
|
||||
return "de";
|
||||
}
|
||||
|
||||
@@ -2694,6 +2646,67 @@ export class PL24FordLegacyService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse Ford (de account) json-sub-group.action response into leaf illustrations.
|
||||
*
|
||||
* Ford de subgroups are a flat list of { caption, id, subheader, jsonUrl:null, url:null }.
|
||||
* `subheader:true` rows are section labels (skipped); the leaves below them are the
|
||||
* actual part illustrations but carry NO url. The illustration (BOM) URL is
|
||||
* constructed: subGroupId = mainGroupId + subheaderId, bomId = subGroupId + leafId,
|
||||
* leaf → image-board.action?bomId=… (same parts endpoint as Nissan/Hyundai). Verified
|
||||
* on dev: the selected-subgroup response exposes exactly this image-board url.
|
||||
*/
|
||||
private parseFordSubGroupsJson(
|
||||
jsonStr: string,
|
||||
linkPath: string,
|
||||
basePath: string,
|
||||
): PL24MainGroup[] {
|
||||
try {
|
||||
const data = JSON.parse(jsonStr) as {
|
||||
subgroups?: {
|
||||
caption: string;
|
||||
id: string;
|
||||
subheader?: boolean;
|
||||
gray?: boolean;
|
||||
jsonUrl?: string | null;
|
||||
url?: string | null;
|
||||
}[];
|
||||
};
|
||||
const subs = data.subgroups ?? [];
|
||||
const q = (k: string) => linkPath.match(new RegExp(`[?&]${k}=([^&]+)`))?.[1] ?? "";
|
||||
const catCode = q("catCode");
|
||||
const mainGroupId = q("mainGroupId");
|
||||
const sharedCatCode = q("sharedCatCode");
|
||||
const modelFamily = q("modelFamily");
|
||||
const mode = q("mode");
|
||||
const upds = q("upds");
|
||||
if (!mainGroupId) return [];
|
||||
const out: PL24MainGroup[] = [];
|
||||
const seen = new Set<string>();
|
||||
let currentSubheaderId = "";
|
||||
for (const sg of subs) {
|
||||
if (sg.gray) continue;
|
||||
if (sg.subheader) {
|
||||
currentSubheaderId = sg.id || currentSubheaderId;
|
||||
continue;
|
||||
}
|
||||
if (!sg.id) continue;
|
||||
const subGroupId = `${mainGroupId}${currentSubheaderId}`;
|
||||
const bomId = `${subGroupId}${sg.id}`;
|
||||
if (seen.has(bomId)) continue;
|
||||
seen.add(bomId);
|
||||
const leafLink =
|
||||
`${basePath}image-board.action?bomId=${bomId}&catCode=${catCode}&lang=${this.language}` +
|
||||
`&mainGroupId=${mainGroupId}&modelFamily=${modelFamily}&sharedCatCode=${sharedCatCode}` +
|
||||
`&startup=false&subGroupId=${subGroupId}&mode=${mode}&upds=${upds}`;
|
||||
out.push({ id: bomId, code: bomId, name: sg.caption, linkPath: leafLink });
|
||||
}
|
||||
return out;
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse Ford json-main-group.action response.
|
||||
* Ford uses { maingroups: [{ caption, databaseKey, identifier, gray, subheader, jsonUrl, url }] } format.
|
||||
@@ -3279,7 +3292,7 @@ export class PL24FordLegacyService {
|
||||
// fetch for these brands to de (which carries the Korean/Japanese license),
|
||||
// and ensure the de service token is authorized. Verified on dev: de yields
|
||||
// 266 Hyundai / 169 Kia models, non-demo, with real drill content.
|
||||
if (HYUNDAI_KIA_NISSAN_DE.has(serviceName)) {
|
||||
if (LEGACY_DE_SERVICES.has(serviceName)) {
|
||||
account = "de";
|
||||
await this.authService.authorizeServiceForAccount(serviceName, "de");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user