feat(catalog): wire Fiat (p5fiat) browse — families→models two-step + de routing
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
Fiat catalog browse returned 0 models because the generic single-endpoint P5 model-list flow can't express Fiat's two-level hierarchy and the drill defaulted to the tr account (Fiat is licensed only on de-708171). - fetchVehicleList: dispatch fiatp_parts/fiatt_parts to new fetchFiatVehicleList, which expands modelOverview (34 families) → models?modelFamily=N (model codes + year ranges) into flat catalog vehicles whose catalogPath is the maingroups endpoint. Verified live: 34 families → 123 models, end-to-end drill to parts+image. - fetchMainGroups / fetchP5Restrictions: account-aware (resolveAccount → de + DE proxy for Fiat; tr unchanged for every other P5 brand) so browse maingroups no longer hit the tr demo/empty page. - web: case-insensitive "/maingroup" gate so Fiat (lowercase /mdl/maingroups, already a maingroups endpoint) skips the empty restriction selector and loads categories directly. - formatFiatYear: "(2016,2020)" → "2016-2020"; +unit tests. Subgroups/parts/images already resolve account→de for Fiat (unchanged). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -629,16 +629,15 @@ export class PL24Service {
|
||||
}> {
|
||||
await this.touchActivity();
|
||||
try {
|
||||
await this.authService.authorizeService(serviceName);
|
||||
const headers = await this.authService.buildAuthHeaders(serviceName);
|
||||
// Account-aware (de + DE proxy for Fiat). resolveAccount(undefined, …) → "de"
|
||||
// only for fiatp_parts/fiatt_parts; all other P5 brands stay on tr (no proxy).
|
||||
const account = await this.resolveAccount(undefined, serviceName);
|
||||
await this.authService.authorizeServiceForAccount(serviceName, account);
|
||||
const headers = await this.authService.buildAuthHeadersForAccount(account, serviceName);
|
||||
const localizedPath = restrictionPath.replace(/lang=\w+/, `lang=${this.language}`);
|
||||
const url = `${this.baseUrl}${localizedPath}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers,
|
||||
signal: AbortSignal.timeout(this.timeout),
|
||||
});
|
||||
const response = await this.fetchWithRetry(url, headers, serviceName, account);
|
||||
|
||||
if (!response.ok) {
|
||||
this.logger.warn(`P5 restrictions fetch failed: HTTP ${response.status}`);
|
||||
@@ -711,9 +710,20 @@ export class PL24Service {
|
||||
}));
|
||||
}
|
||||
try {
|
||||
await this.authService.authorizeService(serviceName);
|
||||
const headers = await this.authService.buildAuthHeaders(serviceName);
|
||||
return this.fetchMainGroupsByPath(mainGroupsPath, headers);
|
||||
// Account-aware: tr for most P5 brands (no proxy), de + DE proxy for Fiat.
|
||||
// resolveAccount(undefined, …) → "de" only for fiatp_parts/fiatt_parts.
|
||||
const account = await this.resolveAccount(undefined, serviceName);
|
||||
await this.authService.authorizeServiceForAccount(serviceName, account);
|
||||
const headers = await this.authService.buildAuthHeadersForAccount(account, serviceName);
|
||||
const localizedPath = mainGroupsPath.replace(/lang=\w+/, `lang=${this.language}`);
|
||||
const response = await this.fetchWithRetry(
|
||||
`${this.baseUrl}${localizedPath}`,
|
||||
headers,
|
||||
serviceName,
|
||||
account,
|
||||
);
|
||||
const data = (await response.json()) as Record<string, any>;
|
||||
return this.parseMainGroupsResponse(data);
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
this.logger.error(`Fetch main groups error: ${err.message}`);
|
||||
@@ -1982,6 +1992,13 @@ export class PL24Service {
|
||||
return this.fordLegacyService.fetchVehicleListForVolvo(serviceName);
|
||||
}
|
||||
|
||||
// Fiat (p5fiat) has a two-level model hierarchy (families → model codes) that the
|
||||
// generic single-endpoint P5 flow below cannot express, and is licensed only on the
|
||||
// de-708171 account. Handle it via a dedicated expander.
|
||||
if (serviceName === "fiatp_parts" || serviceName === "fiatt_parts") {
|
||||
return this.fetchFiatVehicleList(serviceName);
|
||||
}
|
||||
|
||||
try {
|
||||
// Try de account; fall back to main token (demo mode) if that also fails
|
||||
let headers: Record<string, string>;
|
||||
@@ -2101,6 +2118,122 @@ export class PL24Service {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fiat (p5fiat) browse model list.
|
||||
*
|
||||
* Unlike other P5 backends, Fiat exposes a two-level hierarchy:
|
||||
* modelOverview → model FAMILIES (124 SPIDER, 500L, LINEA, TIPO …)
|
||||
* models?modelFamily=N → the actual model codes (+ year range)
|
||||
* Each model's link.path is the maingroups endpoint consumed by the generic P5
|
||||
* drill (getCategoryTree → fetchMainGroups → subgroups → bomDetails parts).
|
||||
*
|
||||
* Requires the de-708171 account (Fiat parts are licensed only there) and the
|
||||
* DataImpulse DE proxy — both applied via fetchWithRetry(account="de").
|
||||
*/
|
||||
private async fetchFiatVehicleList(serviceName: string): Promise<
|
||||
Array<{
|
||||
vehicleId: string;
|
||||
model: string;
|
||||
year?: string;
|
||||
engine?: string;
|
||||
bodyType?: string;
|
||||
transmission?: string;
|
||||
market?: string;
|
||||
catalogPath?: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
}>
|
||||
> {
|
||||
const account = "de" as const;
|
||||
try {
|
||||
await this.authService.authorizeServiceForAccount(serviceName, account);
|
||||
const headers = await this.authService.buildAuthHeadersForAccount(account, serviceName);
|
||||
const base = getServiceApiPath(serviceName); // "/p5fiat"
|
||||
|
||||
// 1) Model families
|
||||
const ovUrl = `${this.baseUrl}${base}/extern/vehicle/modelOverview?lang=${this.language}&serviceName=${serviceName}`;
|
||||
const ovResp = await this.fetchWithRetry(ovUrl, headers, serviceName, account);
|
||||
const ovData = (await ovResp.json()) as Record<string, any>;
|
||||
const families: any[] = (ovData.data?.records || ovData.records || []).filter(
|
||||
(f: any) => !f.unavailable,
|
||||
);
|
||||
if (families.length === 0) {
|
||||
this.logger.warn(`Fiat ${serviceName}: modelOverview returned 0 families`);
|
||||
return [];
|
||||
}
|
||||
|
||||
// 2) Expand each family → model codes (bounded concurrency to keep the one-time seed fast)
|
||||
type FiatVehicle = {
|
||||
vehicleId: string;
|
||||
model: string;
|
||||
year?: string;
|
||||
catalogPath?: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
};
|
||||
const out: FiatVehicle[] = [];
|
||||
const CONCURRENCY = 6;
|
||||
for (let i = 0; i < families.length; i += CONCURRENCY) {
|
||||
const batch = families.slice(i, i + CONCURRENCY);
|
||||
const batchResults = await Promise.all(
|
||||
batch.map(async (fam: any): Promise<FiatVehicle[]> => {
|
||||
const famName = String(fam.values?.description || fam.description || fam.id).trim();
|
||||
const modelsPath = fam.link?.path as string | undefined;
|
||||
const modelsUrl = modelsPath
|
||||
? modelsPath.startsWith("http")
|
||||
? modelsPath
|
||||
: `${this.baseUrl}${modelsPath}`
|
||||
: `${this.baseUrl}${base}/extern/vehicle/models?lang=${this.language}&serviceName=${serviceName}&modelFamily=${fam.id}`;
|
||||
try {
|
||||
const mResp = await this.fetchWithRetry(modelsUrl, headers, serviceName, account);
|
||||
const mData = (await mResp.json()) as Record<string, any>;
|
||||
const models: any[] = (mData.data?.records || mData.records || []).filter(
|
||||
(m: any) => !m.unavailable && m.link?.path,
|
||||
);
|
||||
return models.map((m: any): FiatVehicle => {
|
||||
const modelCode = String(m.values?.modelCode || m.id || "");
|
||||
const desc = String(m.values?.description || m.description || famName)
|
||||
.replace(/\\-/g, "-") // PL24 escapes hyphens as "\-"
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
const year = this.formatFiatYear(m.values?.year);
|
||||
return {
|
||||
vehicleId: `${fam.id}:${modelCode}`,
|
||||
model: year ? `${desc} (${year})` : desc,
|
||||
year: year || undefined,
|
||||
catalogPath: m.link.path as string,
|
||||
metadata: { modelFamily: String(fam.id), modelCode, family: famName },
|
||||
};
|
||||
});
|
||||
} catch (err) {
|
||||
this.logger.warn(
|
||||
`Fiat ${serviceName}: models fetch failed for family ${fam.id}: ${(err as Error).message}`,
|
||||
);
|
||||
return [];
|
||||
}
|
||||
}),
|
||||
);
|
||||
for (const r of batchResults) out.push(...r);
|
||||
}
|
||||
|
||||
this.logger.log(`Fiat ${serviceName}: ${families.length} families → ${out.length} models`);
|
||||
return out;
|
||||
} catch (err) {
|
||||
this.logger.warn(`fetchFiatVehicleList failed for ${serviceName}: ${(err as Error).message}`);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/** Fiat year values arrive as "(2016,2020)" → "2016-2020"; single/open ranges handled. */
|
||||
private formatFiatYear(raw: unknown): string | undefined {
|
||||
if (typeof raw !== "string") return undefined;
|
||||
const trimmed = raw.replace(/[()]/g, "").trim();
|
||||
if (!trimmed) return undefined;
|
||||
const parts = trimmed
|
||||
.split(",")
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
return parts.length >= 2 ? `${parts[0]}-${parts[parts.length - 1]}` : parts[0] || undefined;
|
||||
}
|
||||
|
||||
private parseVehicleListResponse(
|
||||
data: Record<string, any>,
|
||||
serviceName: string,
|
||||
|
||||
Reference in New Issue
Block a user