Some checks are pending
QA Gate (P0/P1) / Test affected app (pull_request) Waiting to run
pro.carcatonline.com (PartsLink24 aynası, pcat-benzeri API) yeni katalog kaynağı. Hedef: source='pl24', architecture P5_MODERN ve hiç kategorisi olmayan catalog_vehicles satırları (prod'da ~770). Sadece 20:00–07:00 Europe/Istanbul penceresinde çalışır; hem tarama hem araç işi pencere dışında kendini pencere başına erteler, pencere kapanınca tarama yarıda kesilir. - integrations/carcatonline: istemci (login → /widget HTML'deki 6 aylık JWT → api.carcatonline.com Bearer; Redis'te paylaşılan token), marka→katalog + model eşleyici (birebir / bölge-yıl soyma / gövde kodu katmanları, ölçülen prod verisiyle test), kademeli varyant seçimi + tam ağaç tarayıcı, isim tekilleştirme (categories unique index'i), throttle (1 çağrı/2,2 s Redis slot, 429 → 40 dk kilit, günlük çağrı tavanı, gece penceresi). - carcatonline-backfill kuyruğu: 30 dk'da bir `scan` (pencere içinde, kuyruk boşsa 20 araç), `vehicle` işi ağacı `carcatonline` kaynaklı categories satırları olarak yazar (link_path `carcat:<catalog>:<car>:<group>`), catalog_vehicles.metadata.carcatonline'a eşleme/varyant/sonuç kaydeder, ağaç cache'ini siler, isimleri LLM çeviri kuyruğuna verir (processor 'carcatonline' kaynağını da geri-doldurur). Eşleşmeyen/hatalı araçlar 7 gün sonra tekrar denenir. - CatalogService: carcatonline düğümleri PL24 drill'ine gönderilmez; yaprak açıldığında parçalar + plate görseli on-demand tek çağrıyla çekilir (aynı throttle/kilit/bütçe, parça adları translateMany ile Türkçe). - Env: CARCATONLINE_ENABLED/EMAIL/PASSWORD/DAILY_CALL_CAP/WINDOW_START/ WINDOW_END/MIN_INTERVAL_MS/LOCKOUT_SECONDS/SCAN_BATCH (compose api+worker). Legacy P4 mimarileri (Hyundai/Kia/Nissan/Opel/Ford) kapsam dışı: katalog sayfası onları önce PL24'ten okuyor. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
134 lines
4.9 KiB
TypeScript
134 lines
4.9 KiB
TypeScript
/**
|
|
* Map our PL24 `catalog_vehicles` (brand + model label) onto carcatonline
|
|
* catalogs/models. carcatonline's `pl_*` catalogs mirror PartsLink24, so most
|
|
* model labels are identical (measured 2026-09-25 on prod's empty catalogs:
|
|
* Hyundai 226/226, Kia 145/145, Volvo 59/59, Mitsubishi 56/56, Toyota 39/39,
|
|
* Alfa 23/23 exact; Renault 45/65, VW 38/58, Porsche 26/64 exact). The rest
|
|
* differ by region suffixes ("ALTIMA (EL)" vs "ALTIMA"), year ranges
|
|
* ("B-MAX (2012- 2017)" vs "B-MAX"), or body-code-only labels (Mercedes "C107"
|
|
* vs "Mercedes-Benz R107 / C107 …"), handled by the relaxed / code tiers.
|
|
*/
|
|
import type { CarcatModel } from "./carcatonline.client";
|
|
|
|
/** Ordered catalog candidates per brand; the first one with a model match wins. */
|
|
export const BRAND_CATALOGS: Record<string, string[]> = {
|
|
abarth: ["pl_abarth"],
|
|
"alfa romeo": ["pl_alfa_romeo"],
|
|
alpine: ["pl_alpine"],
|
|
audi: ["pl_audi"],
|
|
bentley: ["pl_bentley"],
|
|
bmw: ["pl_bmw"],
|
|
citroen: ["pl_citroen"],
|
|
citroën: ["pl_citroen"],
|
|
cupra: ["pl_cupra", "pl_seat"],
|
|
dacia: ["pl_dacia", "pl_renault"],
|
|
ds: ["pl_ds"],
|
|
fiat: ["pl_fiat", "pl_fiat_professional"],
|
|
ford: ["pl_ford", "pl_ford_pro"],
|
|
hyundai: ["pl_hyundai"],
|
|
infiniti: ["pl_infiniti", "pl_nissan"],
|
|
iveco: ["pl_iveco"],
|
|
jaguar: ["pl_jaguar"],
|
|
jeep: ["pl_jeep"],
|
|
kia: ["pl_kia"],
|
|
lancia: ["pl_lancia"],
|
|
"land rover": ["pl_land_rover"],
|
|
lexus: ["pl_lexus", "pl_toyota"],
|
|
man: ["pl_man"],
|
|
"mercedes-benz": ["pl_mercedes", "pl_mercedes_vans", "pl_mercedes_truck"],
|
|
mercedes: ["pl_mercedes", "pl_mercedes_vans", "pl_mercedes_truck"],
|
|
mini: ["pl_mini"],
|
|
mitsubishi: ["pl_mitsubishi"],
|
|
nissan: ["pl_nissan"],
|
|
opel: ["pl_opel", "pl_psa_opel", "pl_vauxhall"],
|
|
peugeot: ["pl_peugeot"],
|
|
polestar: ["pl_polestar"],
|
|
porsche: ["pl_porsche", "pl_porsche_classic"],
|
|
renault: ["pl_renault"],
|
|
seat: ["pl_seat", "pl_cupra"],
|
|
skoda: ["pl_skoda"],
|
|
smart: ["pl_smart"],
|
|
suzuki: ["pl_suzuki"],
|
|
toyota: ["pl_toyota"],
|
|
vauxhall: ["pl_vauxhall", "pl_psa_vauxhall"],
|
|
volkswagen: ["pl_volkswagen", "pl_volkswagen_commercial", "pl_volkswagen_classic"],
|
|
volvo: ["pl_volvo"],
|
|
};
|
|
|
|
export function catalogsForBrand(brandName: string | null | undefined): string[] {
|
|
const key = (brandName ?? "").trim().toLowerCase();
|
|
return BRAND_CATALOGS[key] ?? [];
|
|
}
|
|
|
|
/** PL24 pseudo-models that are not vehicles (VW group "Kimyasal maddeler", "Özel kataloglar" …). */
|
|
const NON_VEHICLE_LABELS = new Set<string>([
|
|
"KIMYASAL MADDELER",
|
|
"OZEL KATALOGLAR",
|
|
"DIGER URUNLER",
|
|
"URETICI SAYFALARI",
|
|
"KISALTMALAR",
|
|
"AOS",
|
|
"ELEKTRIKL BAGLANTI",
|
|
"KAMP ARACI",
|
|
"KAMYONET",
|
|
"KOMB ISI GUC SANTR",
|
|
]);
|
|
|
|
export function isNonVehicleModel(label: string): boolean {
|
|
return NON_VEHICLE_LABELS.has(normalizeLabel(label));
|
|
}
|
|
|
|
/** Uppercase, diacritic-folded, punctuation collapsed, PL24 backslash escapes removed. */
|
|
export function normalizeLabel(s: string | null | undefined): string {
|
|
return (s ?? "")
|
|
.toUpperCase()
|
|
.normalize("NFD")
|
|
.replace(/\p{M}/gu, "")
|
|
.replace(/\\/g, "")
|
|
.replace(/[^A-Z0-9]+/g, " ")
|
|
.trim();
|
|
}
|
|
|
|
/** Drop region suffixes "(EL)/(ER)", year ranges "(2012- 2017)", "2004 - 2014", and 4-digit years. */
|
|
export function relaxLabel(s: string | null | undefined): string {
|
|
const stripped = (s ?? "")
|
|
.replace(/\((EL|ER|EUR|JPN|USA|GCC|RUS)\)/gi, " ")
|
|
.replace(/\((19|20)\d\d\s*-\s*(19|20)?\d{0,2}\)/g, " ")
|
|
.replace(/\b(19|20)\d\d\s*-\s*(19|20)?\d{0,2}\b/g, " ")
|
|
.replace(/\b(19|20)\d\d\b/g, " ");
|
|
return normalizeLabel(stripped);
|
|
}
|
|
|
|
export interface ModelMatch {
|
|
model: CarcatModel;
|
|
tier: "exact" | "relaxed" | "code";
|
|
}
|
|
|
|
/**
|
|
* Pick the carcatonline model for one of our PL24 model labels.
|
|
* - exact: normalized labels identical
|
|
* - relaxed: identical after stripping region/year decorations on both sides
|
|
* - code: our label is a short body/chassis code (≤6 chars, has a digit) that
|
|
* appears as a whole token in exactly ONE carcatonline label (Mercedes C107)
|
|
* Returns null when ambiguous or absent — the caller records "unmatched".
|
|
*/
|
|
export function matchCarcatModel(ourLabel: string, models: CarcatModel[]): ModelMatch | null {
|
|
const target = normalizeLabel(ourLabel);
|
|
if (!target) return null;
|
|
const exact = models.filter((m) => normalizeLabel(m.name) === target);
|
|
if (exact.length === 1) return { model: exact[0], tier: "exact" };
|
|
if (exact.length > 1) return { model: exact[0], tier: "exact" }; // identical labels → same PL24 model, first wins
|
|
|
|
const relaxedTarget = relaxLabel(ourLabel);
|
|
if (relaxedTarget) {
|
|
const relaxed = models.filter((m) => relaxLabel(m.name) === relaxedTarget);
|
|
if (relaxed.length === 1) return { model: relaxed[0], tier: "relaxed" };
|
|
}
|
|
|
|
if (/^[A-Z]{1,2}\d{2,4}[A-Z]?$/.test(target) && target.length <= 6) {
|
|
const code = models.filter((m) => normalizeLabel(m.name).split(" ").includes(target));
|
|
if (code.length === 1) return { model: code[0], tier: "code" };
|
|
}
|
|
return null;
|
|
}
|