fix(pl24): correct PSA VIN decode via FI/VIN-indexed flow + cycle-correct model year
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
PSA (Peugeot/Citroën/DS) VIN decode was systemically broken: the catalog
vin-group page returns all families unfiltered, so decode fell back to the first
family/salesType (a manual base variant) — yielding "{Brand} {VIN}" model names,
empty transmission, wrong model year, and manual-only parts trees (automatic
gearbox parts missing). Reported for a 1999 Peugeot 106 automatic shown as a 2029
manual with no automatic parts.
- New self-contained PL24PsaService: consumes PL24's FI flow (vin.action →
hintstoken → FI page → json-vin-main-groups → json-vin-illustrations →
vin-image-board). Reads model/year/transmission from the FI identification
table; builds the VIN-indexed parts tree (correct per actual VIN). Does not
touch Ford/Volvo/Nissan/Opel/Hyundai-Kia/Fiat.
- Orchestrator + categories.service route PSA VIN decode/drill to the new service.
- Cycle-correct extractModelYear in @sase/shared (X→1999, not 2029): resolve the
30-yr VIN year code to the most-recent plausible year (≤ now+1); dedupe 6 copies.
Validated live against 13 already-decoded PSA VINs: 12/13 full trees with real
model/year/transmission; automatics correctly detected (106 BVA, 206 AL4, 3008 BVA8).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -469,6 +469,27 @@ describe("extractModelYear", () => {
|
||||
it("handles lowercase input", () => {
|
||||
expect(extractModelYear("wvwzzz1jzaw597935")).toBe(2010);
|
||||
});
|
||||
|
||||
// VIN yıl kodu 30 yılda bir tekrarlar; gelecekteki imkânsız yıla değil, makul
|
||||
// en yeni (geçmiş) yıla çözülmeli. Peugeot 106 (X = 1999), 2029 DEĞİL.
|
||||
it("resolves an ambiguous code to the most recent plausible (past) year, not the future", () => {
|
||||
expect(extractModelYear("VF31AKFXLXM000752", 2026)).toBe(1999);
|
||||
});
|
||||
|
||||
it("tolerates the next model year being stamped early (+1 window)", () => {
|
||||
// 'V' = 2027; 2026'da satılan bir 2027 modeli geçerli, 1997'ye düşmemeli.
|
||||
expect(extractModelYear("WVWZZZ1JZVW597935", 2026)).toBe(2027);
|
||||
});
|
||||
|
||||
it("pulls a code more than one year ahead back into the previous cycle", () => {
|
||||
// 'W' = 2028; 2026 referansında makul değil → 1998.
|
||||
expect(extractModelYear("WVWZZZ1JZWW597935", 2026)).toBe(1998);
|
||||
});
|
||||
|
||||
it("advances the resolved year as the reference year moves forward", () => {
|
||||
// Aynı 'X' kodu, 2030 referansında artık 2029 olarak makul.
|
||||
expect(extractModelYear("VF31AKFXLXM000752", 2030)).toBe(2029);
|
||||
});
|
||||
});
|
||||
|
||||
// --------------- utils/currency ---------------
|
||||
|
||||
@@ -206,39 +206,62 @@ export function getBrandFromWmi(wmi: string): string | null {
|
||||
return WMI_BRAND_MAP[wmi.toUpperCase()] ?? null;
|
||||
}
|
||||
|
||||
export function extractModelYear(vin: string): number | null {
|
||||
const yearChar = vin.toUpperCase()[9];
|
||||
const yearMap: Record<string, number> = {
|
||||
A: 2010,
|
||||
B: 2011,
|
||||
C: 2012,
|
||||
D: 2013,
|
||||
E: 2014,
|
||||
F: 2015,
|
||||
G: 2016,
|
||||
H: 2017,
|
||||
J: 2018,
|
||||
K: 2019,
|
||||
L: 2020,
|
||||
M: 2021,
|
||||
N: 2022,
|
||||
P: 2023,
|
||||
R: 2024,
|
||||
S: 2025,
|
||||
T: 2026,
|
||||
V: 2027,
|
||||
W: 2028,
|
||||
X: 2029,
|
||||
Y: 2030,
|
||||
"1": 2001,
|
||||
"2": 2002,
|
||||
"3": 2003,
|
||||
"4": 2004,
|
||||
"5": 2005,
|
||||
"6": 2006,
|
||||
"7": 2007,
|
||||
"8": 2008,
|
||||
"9": 2009,
|
||||
};
|
||||
return yearMap[yearChar] ?? null;
|
||||
// VIN 10. karakter → model yılı kodu. Değerler 30 yıllık döngünün son penceresi
|
||||
// (2001-2030) için "çapa" yıllardır; extractModelYear() bunları referans yıla göre
|
||||
// gerçek yıla çözer. ISO 3779 / SAE: I, O, Q, U, Z ve "0" yıl kodu olarak kullanılmaz.
|
||||
const VIN_YEAR_CODE_ANCHOR: Record<string, number> = {
|
||||
"1": 2001,
|
||||
"2": 2002,
|
||||
"3": 2003,
|
||||
"4": 2004,
|
||||
"5": 2005,
|
||||
"6": 2006,
|
||||
"7": 2007,
|
||||
"8": 2008,
|
||||
"9": 2009,
|
||||
A: 2010,
|
||||
B: 2011,
|
||||
C: 2012,
|
||||
D: 2013,
|
||||
E: 2014,
|
||||
F: 2015,
|
||||
G: 2016,
|
||||
H: 2017,
|
||||
J: 2018,
|
||||
K: 2019,
|
||||
L: 2020,
|
||||
M: 2021,
|
||||
N: 2022,
|
||||
P: 2023,
|
||||
R: 2024,
|
||||
S: 2025,
|
||||
T: 2026,
|
||||
V: 2027,
|
||||
W: 2028,
|
||||
X: 2029,
|
||||
Y: 2030,
|
||||
};
|
||||
|
||||
/**
|
||||
* VIN'in 10. karakterinden model yılını çıkarır.
|
||||
*
|
||||
* VIN yıl kodu 30 yılda bir tekrarlar — örn. "X" hem 1999, hem 2029, hem 1969'a
|
||||
* karşılık gelir. Bu yüzden kodu tek başına bir yıla eşlemek yanlıştır. Burada
|
||||
* "makul en yeni" yıla çözeriz: referans yılından (varsayılan: içinde bulunulan
|
||||
* yıl) en fazla 1 yıl ileride olan en güncel aday. Böylece 1999 model bir araç
|
||||
* asla "2029" gibi imkânsız bir geleceğe düşmez. (+1 toleransı, üreticilerin bir
|
||||
* sonraki model yılını takvim yılından önce damgalamasını karşılar.)
|
||||
*
|
||||
* NOT: Bu yalnızca decode servisi bir yıl döndürmediğinde başvurulan son-çare
|
||||
* tahminidir. Servis (PL24/EMEX/NHTSA vb.) bir model yılı veriyorsa HER ZAMAN o
|
||||
* değer kullanılmalıdır; bu fonksiyon onun yerine geçmez.
|
||||
*/
|
||||
export function extractModelYear(vin: string, referenceYear?: number): number | null {
|
||||
if (!vin || vin.length < 10) return null;
|
||||
const anchor = VIN_YEAR_CODE_ANCHOR[vin.toUpperCase()[9]];
|
||||
if (anchor == null) return null;
|
||||
const cutoff = (referenceYear ?? new Date().getFullYear()) + 1;
|
||||
let year = anchor;
|
||||
while (year > cutoff) year -= 30; // imkânsız geleceği bir önceki 30-yıllık döngüye çek
|
||||
return year;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user