fix(part-prices): araç-marka etiketlerini de orijinal say (FORD/GM/BMW/Mercedes/VW)
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled

Kullanıcı düzeltmesi: kokpit tur ham araç-marka etiketlerini "yansanayi"
sayıyordu — yanlış. FORD/GM/BMW/Mercedes/Volkswagen/Renault… OE etiketidir.
Orijinal tespiti artık kokpit tur='orjinal' setine değil, OE aile üyeliğine
dayanır: OE_SUPPLY_LABELS OE_FAMILIES'ten TÜRETİLİR (+ jenerik ORJINAL), böylece
OE-tespiti ile aile-filtresi drift etmez ve tüm araç markaları orijinal sayılır.
Gerçek yan sanayi PARÇA markaları (Bosch/Febi/Valeo/TRW) hiçbir ailede yok →
orijinal değil. GM Opel ailesine eklendi (TR'de Opel OE'si). Redis v3→v4 (eski
"miss" değerleri bayat). kokpit'e DOKUNULMADI.

Ölçülen etki (dev): grafikli kod %0,77→%0,84; artış sase kataloğu ile takip
beslemesinin bu markalardaki kod kesişimiyle sınırlı (ör. takip'te 4.859 Ford-
stokta koddan 335'i sase kataloğunda).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-14 15:34:10 +03:00
parent fcc02984dd
commit 66b39862c8
2 changed files with 92 additions and 97 deletions

View File

@@ -9,6 +9,7 @@ import {
isOeSupplyLabel,
istanbulToday,
normPartCode,
oeFamilyOf,
percentile,
reconstructDailySeries,
selectOeOffers,
@@ -246,31 +247,69 @@ describe("filterOffersForBrand", () => {
});
});
describe("isOeSupplyLabel / selectOeOffers (kaynak: kokpit tur='orjinal')", () => {
describe("isOeSupplyLabel / selectOeOffers (OE aileleri + jenerik ORJINAL)", () => {
it("dağıtıcı ve orijinal etiketleri tanır", () => {
expect(isOeSupplyLabel("MAIS")).toBe(true); // Renault dağıtıcısı
expect(isOeSupplyLabel("OPAR")).toBe(true); // Fiat/Tofaş
expect(isOeSupplyLabel("PSA")).toBe(true);
expect(isOeSupplyLabel("ORJINAL")).toBe(true); // kokpit tur='orjinal'
expect(isOeSupplyLabel("ORJINAL")).toBe(true); // jenerik orijinal
expect(isOeSupplyLabel("VECO")).toBe(true); // Iveco OE alias'ı
});
it("kokpit'in yansanayi/esdeger saydığı etiketler orijinal DEĞİL", () => {
// kokpit ham FORD/GM/BMW/Mercedes/VW etiketlerini yansanayi sayar.
expect(isOeSupplyLabel("FORD")).toBe(false);
expect(isOeSupplyLabel("GM")).toBe(false);
expect(isOeSupplyLabel("BOSCH")).toBe(false); // esdeger
expect(isOeSupplyLabel("FEBI")).toBe(false); // esdeger
it("ham araç-marka etiketleri de ORİJİNALDİR (kokpit'in yansanayi demesi yanlış)", () => {
// 2026-07-14 kullanıcı düzeltmesi: FORD/GM/BMW/Mercedes/VW OE etiketidir.
expect(isOeSupplyLabel("FORD")).toBe(true);
expect(isOeSupplyLabel("GM")).toBe(true); // Opel OE'si
expect(isOeSupplyLabel("BMW")).toBe(true);
expect(isOeSupplyLabel("MERCEDES")).toBe(true);
expect(isOeSupplyLabel("VOLKSWAGEN")).toBe(true);
expect(isOeSupplyLabel("RENAULT")).toBe(true);
});
it("gerçek yan sanayi PARÇA markaları orijinal DEĞİL", () => {
expect(isOeSupplyLabel("BOSCH")).toBe(false);
expect(isOeSupplyLabel("FEBI")).toBe(false);
expect(isOeSupplyLabel("VALEO")).toBe(false);
expect(isOeSupplyLabel("TRW")).toBe(false);
expect(isOeSupplyLabel("")).toBe(false);
});
it("selectOeOffers karma havuzdan yalnız orijinalleri seçer", () => {
it("GM Opel ailesine, FORD kendi ailesine eşlenir", () => {
expect(oeFamilyOf("GM")).toBe("OPEL");
expect(oeFamilyOf("FORD")).toBe("FORD");
expect(oeFamilyOf("MERCEDES")).toBe("MERCEDES");
});
it("selectOeOffers karma havuzdan orijinalleri (araç markaları dahil) seçer", () => {
const out = selectOeOffers([
{ brandNorm: "OPAR", price: 767 },
{ brandNorm: "FORD", price: 480 },
{ brandNorm: "IBR", price: 909 },
{ brandNorm: "FORD", price: 480 }, // artık orijinal
{ brandNorm: "IBR", price: 909 }, // İbraş = yan sanayi
]);
expect(out).toEqual([{ brandNorm: "OPAR", price: 767 }]);
expect(out.map((o) => o.brandNorm).sort()).toEqual(["FORD", "OPAR"]);
});
it("Ford kodu araç markası FORD → FORD teklifi gelir", () => {
const offers = [
{ brandNorm: "FORD", price: 480 },
{ brandNorm: "BOSCH", price: 300 },
];
expect(filterOffersForBrand(offers, "FORD", "1234567890")).toEqual([
{ brandNorm: "FORD", price: 480 },
]);
});
it("Opel kodu → OPEL + GM aynı aile, ikisi de gelir; PSA farklı aile elenir", () => {
const offers = [
{ brandNorm: "GM", price: 500 },
{ brandNorm: "OPEL", price: 520 },
{ brandNorm: "PSA", price: 610 },
];
expect(
filterOffersForBrand(offers, "OPEL", "9827622780")
.map((o) => o.brandNorm)
.sort(),
).toEqual(["GM", "OPEL"]);
});
});

View File

@@ -31,91 +31,29 @@ export function normPartCode(code: string): string {
// süzülür. Tedarikçiler markayı kısaltarak yazar (BCH/B→Bosch, BLP→Blue
// Print, IBR→İbraş) — eşleşme önek VEYA sıralı-altdizi ile yapılır.
// ─── Orijinal (OE/dağıtıcı) etiketleri ─────────────────────────────────────
// Markasız bağlam (OEM detayın ana kodu) yalnız ORİJİNAL teklifleri gösterir:
// orijinal + yan sanayi tek havuzda medyanlanınca hiçbir gerçek ürünün fiyatı
// olmayan "hayalet fiyat" çıkar ve marka stok girip-çıkması fiyat değişmeden
// sahte sıçrama çizer (2026-07-14 ölçümü: 9827622780 → pg p50 4.495₺ = PSA
// 5.531 ile OPAR 3.459'un ortası).
// ─── Orijinal (OE) etiketleri ve marka aileleri ────────────────────────────
// Markasız/araç-markalı bağlam (OEM detay kartı) yalnız ORİJİNAL teklifleri
// gösterir: orijinal + yan sanayi tek havuzda medyanlanınca hiçbir gerçek
// ürünün fiyatı olmayan "hayalet fiyat" çıkar (2026-07-14: 9827622780 →
// 4.495₺ = PSA 5.531 ile OPAR 3.459'un ortası).
//
// OTORİTE = kokpit (otoyedekparca.co) marka `tur` sınıflaması: orjinal /
// esdeger / yansanayi. Aşağıdaki set = tur='orjinal' markalarının normalize
// adları (39 marka), 2026-07-14 çekimi. Yenilemek için (kokpit_center PG):
// SELECT UPPER(regexp_replace(name,'[^A-Za-z0-9]','','g'))
// FROM product_service_manufacturers WHERE is_deleted=false AND tur='orjinal';
// NOT: kokpit Türkiye pazarını yansıtır — orijinal = DAĞITICI etiketi
// (Mais=Renault, Opar=Fiat, Psa, Opel) + ithal-araç OE markaları. Ham FORD/
// GM/BMW/MERCEDES/VOLKSWAGEN etiketleri kokpit'te 'yansanayi'dir, bilinçli
// dışarıda. Runtime kokpit bağımlılığı YOK (statik liste; P entegrasyonu gibi
// fail-open değil, çekirdek mantık) — kokpit listesi değişince buradan yenile.
const OE_SUPPLY_LABELS: ReadonlySet<string> = new Set([
"AUDI",
"BERCITROEN",
"BUICK",
"CHEVROLET",
"CHRYSLER",
"DACIA",
"DAEWOO",
"DAIHATSU",
"DFM",
"DODGE",
"HONDA",
"JEEP",
"KIA",
"LADA",
"LEXUS",
"LINCOLN",
"MAIS",
"MAZDA",
"MINI",
"NISSAN",
"OEVECO",
"OPAR",
"OPEL",
"ORJINAL",
"PORSCHE",
"PROTON",
"PSA",
"RANGE",
"ROVER",
"SAAB",
"SEAT",
"SKODA",
"SSANGYONG",
"TATA",
"TESLA",
"TOGG",
"TOYOTA",
"TOYOTAORJINAL",
"VOLVO",
// Alias: takip verisi Iveco OE'sini "VECO" olarak etiketliyor; kokpit
// karşılığı "Oe-İveco" (OEVECO). Aynı fiziksel orijinal → dahil.
"VECO",
]);
/** Teklifin etiketi orijinal (OE/dağıtıcı) mı — kokpit `tur='orjinal'` setine göre. */
export function isOeSupplyLabel(brandNorm: string): boolean {
return OE_SUPPLY_LABELS.has(brandNorm);
}
/** Yalnız orijinal (OE/dağıtıcı) etiketli teklifler. */
export function selectOeOffers<T extends { brandNorm: string }>(offers: T[]): T[] {
return offers.filter((o) => isOeSupplyLabel(o.brandNorm));
}
// ─── Marka → OE dağıtıcı ailesi ────────────────────────────────────────────
// Bir OEM kodu tek bir araç markasına aittir; teklifleri o markanın OE
// ailesine süzeriz. Böylece aynı koda düşen FARKLI dağıtıcılar (PSA ₺5.531 vs
// OPAR ₺3.459 — Stellantis) tek havuza karışıp anlamsız medyan üretmez:
// Peugeot kodu = PSA ailesi, Fiat kodu = FIAT ailesi (2026-07-14 kullanıcı
// talebi). Hem araç markası adları (istek tarafı: sase brands) hem dağıtıcı
// etiketleri (teklif tarafı: takip) aynı aileye eşlenir. `family → üyeler`
// olarak yazılır, yükleme anında etiket→aile map'ine çevrilir.
// Bir OEM kodu tek bir araç markasına aittir → teklifleri o markanın OE
// ailesine süzeriz (aynı koda düşen PSA vs OPAR karışmaz). `family → üyeler`;
// üyeler hem araç markası adları (istek/sase brands) hem dağıtıcı etiketleridir
// (teklif/takip: MAIS=Renault, OPAR=Fiat, PSA, VECO=Iveco, GM=Opel).
//
// ORİJİNAL TESPİTİ: bir teklif etiketi orijinal sayılır ancak bir OE ailesinin
// üyesiyse ya da jenerik "ORJINAL" ise. Kaynak = sase araç markaları + Türkiye
// OE-dağıtıcı etiketleri. NOT (2026-07-14 kullanıcı düzeltmesi): ham araç-marka
// etiketleri (FORD, GM, BMW, MERCEDES, VOLKSWAGEN, RENAULT, FIAT…) ORİJİNALDİR
// — kokpit `tur` bunları 'yansanayi' sayıyordu, bu YANLIŞ; burada aileleriyle
// birlikte orijinal kabul edilir. Gerçek yan sanayi PARÇA markaları (Bosch/
// Febi/Valeo/TRW…) hiçbir ailede değildir → orijinal sayılmaz.
const OE_FAMILIES: Readonly<Record<string, readonly string[]>> = {
RENAULT: ["RENAULT", "DACIA", "ALPINE", "MAIS"],
FIAT: ["FIAT", "ALFAROMEO", "LANCIA", "ABARTH", "OPAR", "TOFAS"],
PSA: ["PEUGEOT", "CITROEN", "DS", "PSA"],
OPEL: ["OPEL", "VAUXHALL"],
OPEL: ["OPEL", "VAUXHALL", "GM"], // GM = General Motors, TR'de Opel OE'si
VAG: ["VOLKSWAGEN", "VW", "AUDI", "SEAT", "SKODA", "CUPRA", "BENTLEY"],
FORD: ["FORD"],
BMW: ["BMW", "MINI"],
@@ -134,8 +72,7 @@ const OE_FAMILIES: Readonly<Record<string, readonly string[]>> = {
SUBARU: ["SUBARU"],
SUZUKI: ["SUZUKI"],
IVECO: ["IVECO", "VECO", "OEVECO"],
// kokpit orjinal setinde olup sase araç listesinde olmayanlar — kendi
// ailelerine (isteğe gelirse kendileriyle eşleşsinler).
// sase araç listesinde olmayan ama teklif/istek olarak gelebilecek markalar.
CHRYSLER: ["CHRYSLER", "DODGE", "JEEP", "MOPAR", "LINCOLN", "BUICK"],
CHEVROLET: ["CHEVROLET"],
DAEWOO: ["DAEWOO"],
@@ -163,11 +100,30 @@ export function oeFamilyOf(brandNorm: string): string | null {
return OE_FAMILY_OF.get(brandNorm) ?? null;
}
// Orijinal etiket seti = tüm OE aile üyeleri + jenerik "ORJINAL" (orijinal ama
// aile-belirsiz: markasız/taban görünümde sayılır, aile filtresinde elenir).
// Aileden TÜRETİLİR → OE-tespiti ile aile-filtresi asla drift etmez; yeni bir
// marka eklemek için tek yer OE_FAMILIES.
const OE_SUPPLY_LABELS: ReadonlySet<string> = new Set([
...Object.values(OE_FAMILIES).flat(),
"ORJINAL",
]);
/** Teklifin etiketi orijinal mi (bir OE ailesinin üyesi ya da jenerik ORJINAL). */
export function isOeSupplyLabel(brandNorm: string): boolean {
return OE_SUPPLY_LABELS.has(brandNorm);
}
/** Yalnız orijinal (OE) etiketli teklifler. */
export function selectOeOffers<T extends { brandNorm: string }>(offers: T[]): T[] {
return offers.filter((o) => isOeSupplyLabel(o.brandNorm));
}
// ─── Redis cache anahtarları ───────────────────────────────────────────────
// Service ve worker processor'ı AYNI üreticileri kullanır (literal kopya
// drift'i olmasın). v3 = markasız/araç-markalı görünümler OE-only süzgece
// geçti; v2 karma-havuz değerleri bayat.
export const PART_PRICE_CACHE_VERSION = "v3";
// drift'i olmasın). v4 = OE seti araç-marka etiketlerini (FORD/GM/BMW/MERCEDES/
// VW…) de kapsayacak şekilde genişledi → v3'teki "miss" değerleri bayat.
export const PART_PRICE_CACHE_VERSION = "v4";
export const partPriceSeriesCacheKey = (codeNorm: string, brandNorm: string) =>
`partprice:series:${PART_PRICE_CACHE_VERSION}:${codeNorm}::${brandNorm}`;
export const partPriceCurrentCacheKey = (codeNorm: string, brandNorm: string) =>