fix(part-prices): OEM ana kartında yalnız orijinal (OE) fiyatları göster
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
OEM detay ana fiyat kartı seriyi markasız istiyordu; allowBrandless uzun kodlarda orijinal + yan sanayi tekliflerini tek havuzda medyanlıyordu → hiçbir satıcının fiyatı olmayan "hayalet medyan" ve marka stok girip- çıkmasından fiyat değişmeden sahte grafik sıçraması (2026-07-14 ölçümü: 9827622780 pg p50 4.495₺ = PSA 5.531 ile OPAR 3.459'un ortası). - filterOffersForBrand: markasız istek artık yalnız orijinal (OE/dağıtıcı etiketli) teklifleri kullanır; araç-markalı OE çapraz-ref çipleri de OE-only'ye iner, yan sanayi markasına orijinal fallback'i kaldırıldı. - Orijinal sınıflaması otoritesi = kokpit marka tur='orjinal' seti (39 marka, statik; PSA/MAIS/OPAR/ORJINAL... — FORD/GM/BMW/Mercedes/VW kokpit'te yansanayi, bilinçli dışarıda). Runtime kokpit bağımlılığı yok. - Web: kart başlığı "Orijinal (OE) fiyat analizi" + açıklama/dipnot orijinal bilgisini yazıyor; ana OEM kodunun ölü markasız batch push'u kaldırıldı. - Redis cache v2→v3 (anahtar üreticileri export'lanıp worker ile paylaşıldı); migration 0032 eski karma-havuz part_price satırlarını siler → OE-only kurallarla lazy re-backfill. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -6,10 +6,12 @@ import {
|
||||
brandCompatible,
|
||||
computeStats,
|
||||
filterOffersForBrand,
|
||||
isOeSupplyLabel,
|
||||
istanbulToday,
|
||||
normPartCode,
|
||||
percentile,
|
||||
reconstructDailySeries,
|
||||
selectOeOffers,
|
||||
} from "./part-prices.logic";
|
||||
|
||||
describe("normPartCode", () => {
|
||||
@@ -174,17 +176,68 @@ describe("filterOffersForBrand", () => {
|
||||
expect(filterOffersForBrand(offers, "", "27155")).toEqual([]);
|
||||
});
|
||||
|
||||
it("uzun kodda markasız istek tüm teklifleri kullanır (OE vakası)", () => {
|
||||
const oe = [
|
||||
{ brandNorm: "MAIS", price: 195 },
|
||||
{ brandNorm: "RENAULT", price: 247 },
|
||||
it("uzun kodda markasız istek YALNIZ orijinal teklifleri kullanır", () => {
|
||||
// 2026-07-14 vakası: orijinal+yan sanayi tek havuzda "hayalet medyan"
|
||||
// üretiyordu (9827622780 → 4.495₺, hiçbir satıcının fiyatı değil).
|
||||
// kokpit tur'una göre MAIS+OPAR orijinal; SAGEMFRANS/FEBI değil.
|
||||
const mixed = [
|
||||
{ brandNorm: "MAIS", price: 245 },
|
||||
{ brandNorm: "OPAR", price: 247 },
|
||||
{ brandNorm: "SAGEMFRANS", price: 210 },
|
||||
{ brandNorm: "FEBI", price: 160 },
|
||||
];
|
||||
expect(filterOffersForBrand(oe, "", "8200768913")).toHaveLength(2);
|
||||
const out = filterOffersForBrand(mixed, "", "8200768913");
|
||||
expect(out.map((o) => o.brandNorm).sort()).toEqual(["MAIS", "OPAR"]);
|
||||
});
|
||||
|
||||
it("uzun kod + uyumsuz marka etiketi → dağıtıcı fallback'i (tümü)", () => {
|
||||
it("markasız istekte hiç orijinal teklif yoksa boş (yan sanayi OE gibi gösterilmez)", () => {
|
||||
const amOnly = [{ brandNorm: "BRUCKE", price: 890 }];
|
||||
expect(filterOffersForBrand(amOnly, "", "46456072")).toEqual([]);
|
||||
});
|
||||
|
||||
it("uzun kod + araç markası (OE çapraz-ref çipi) → yalnız orijinal teklifler", () => {
|
||||
const oe = [
|
||||
{ brandNorm: "MAIS", price: 195 },
|
||||
{ brandNorm: "FEBI", price: 90 },
|
||||
];
|
||||
// RENAULT çipi: MAIS orijinal → gelir, FEBI (yan sanayi) elenir.
|
||||
expect(filterOffersForBrand(oe, "RENAULT", "8200768913")).toEqual([
|
||||
{ brandNorm: "MAIS", price: 195 },
|
||||
]);
|
||||
});
|
||||
|
||||
it("yan sanayi markasına orijinal fallback'i YOK — uyumsuzsa boş", () => {
|
||||
const oe = [{ brandNorm: "MAIS", price: 195 }];
|
||||
expect(filterOffersForBrand(oe, "RENAULT", "8200768913")).toHaveLength(1);
|
||||
// FEBI çipi: MAIS'e (orijinal) düşmez, yan sanayi altında OE fiyatı gösterilmez.
|
||||
expect(filterOffersForBrand(oe, "FEBIBILSTEIN", "8200768913")).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isOeSupplyLabel / selectOeOffers (kaynak: kokpit tur='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("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
|
||||
expect(isOeSupplyLabel("")).toBe(false);
|
||||
});
|
||||
|
||||
it("selectOeOffers karma havuzdan yalnız orijinalleri seçer", () => {
|
||||
const out = selectOeOffers([
|
||||
{ brandNorm: "OPAR", price: 767 },
|
||||
{ brandNorm: "FORD", price: 480 },
|
||||
{ brandNorm: "IBR", price: 909 },
|
||||
]);
|
||||
expect(out).toEqual([{ brandNorm: "OPAR", price: 767 }]);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -31,6 +31,145 @@ 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ı).
|
||||
//
|
||||
// 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));
|
||||
}
|
||||
|
||||
// İstenen marka bir ARAÇ markası mı? OEM çapraz-referans çipleri araç markası
|
||||
// (RENAULT) geçer ama teklifler dağıtıcı (MAIS) etiketli olur; bu durumda
|
||||
// uyumsuz-fallback OE-only tekliflere iner. Yan sanayi markaları (FEBI/SWAG)
|
||||
// bu sette olmadığından onlar için OE'ye düşmez (yan sanayi çipi OE fiyatı
|
||||
// göstermez). Salt gate amaçlı — teklif sınıflaması OE_SUPPLY_LABELS'tır.
|
||||
const VEHICLE_BRANDS: ReadonlySet<string> = new Set([
|
||||
"RENAULT",
|
||||
"DACIA",
|
||||
"FIAT",
|
||||
"ALFAROMEO",
|
||||
"LANCIA",
|
||||
"ABARTH",
|
||||
"FORD",
|
||||
"OPEL",
|
||||
"VAUXHALL",
|
||||
"PEUGEOT",
|
||||
"CITROEN",
|
||||
"DS",
|
||||
"VW",
|
||||
"VOLKSWAGEN",
|
||||
"AUDI",
|
||||
"SEAT",
|
||||
"SKODA",
|
||||
"CUPRA",
|
||||
"BMW",
|
||||
"MINI",
|
||||
"MERCEDES",
|
||||
"MERCEDESBENZ",
|
||||
"SMART",
|
||||
"TOYOTA",
|
||||
"LEXUS",
|
||||
"HONDA",
|
||||
"NISSAN",
|
||||
"INFINITI",
|
||||
"HYUNDAI",
|
||||
"KIA",
|
||||
"VOLVO",
|
||||
"IVECO",
|
||||
"GM",
|
||||
"CHEVROLET",
|
||||
"MAZDA",
|
||||
"PORSCHE",
|
||||
"JEEP",
|
||||
"DODGE",
|
||||
"CHRYSLER",
|
||||
"SUZUKI",
|
||||
"MITSUBISHI",
|
||||
"SUBARU",
|
||||
"DAEWOO",
|
||||
"SSANGYONG",
|
||||
]);
|
||||
|
||||
/** İstenen marka araç markası mı (OE çapraz-ref çipi fallback'i için). */
|
||||
export function isVehicleBrand(brandNorm: string): boolean {
|
||||
return VEHICLE_BRANDS.has(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";
|
||||
export const partPriceSeriesCacheKey = (codeNorm: string, brandNorm: string) =>
|
||||
`partprice:series:${PART_PRICE_CACHE_VERSION}:${codeNorm}::${brandNorm}`;
|
||||
export const partPriceCurrentCacheKey = (codeNorm: string, brandNorm: string) =>
|
||||
`partprice:cur:${PART_PRICE_CACHE_VERSION}:${codeNorm}::${brandNorm}`;
|
||||
|
||||
/** a'nın tüm karakterleri b içinde aynı sırayla geçiyor mu (BCH ⊂ BOSCH). */
|
||||
export function inOrderSubsequence(a: string, b: string): boolean {
|
||||
let i = 0;
|
||||
@@ -61,10 +200,16 @@ export function allowBrandless(codeNorm: string): boolean {
|
||||
|
||||
/**
|
||||
* Teklifleri istenen markaya süz:
|
||||
* - marka istendi → uyumlular; hiçbiri uymuyorsa ve kod markasız-güvenliyse
|
||||
* hepsi (OE/dağıtıcı etiketi vakası: RENAULT istenir, teklifler MAIS taşır);
|
||||
* kısa kodda boş (yanlış veri göstermekten iyidir),
|
||||
* - marka istenmedi → kod markasız-güvenliyse hepsi, değilse boş.
|
||||
* - marka istendi → etiketi uyumlular; hiçbiri uymuyorsa ve istenen marka bir
|
||||
* ARAÇ markasıysa (OEM çapraz-ref çipi: RENAULT istenir, teklifler MAIS
|
||||
* dağıtıcı etiketi taşır) orijinal teklifler. Yan sanayi markasına (FEBI)
|
||||
* orijinal fallback'i YOKTUR — yan sanayi çipi altında OE fiyatı göstermek
|
||||
* yanlış veri olur,
|
||||
* - marka istenmedi (OEM detay ana kartı) → yalnız orijinal (OE/dağıtıcı)
|
||||
* teklifler; yan sanayi karışımı hayalet medyan üretir (v2 davranışı
|
||||
* "hepsi" idi — 2026-07-14'te bilinçli daraltıldı),
|
||||
* - kısa/çakışmaya açık kodlarda (allowBrandless=false) markasız ve
|
||||
* fallback yolları kapalıdır (yanlış veri göstermekten iyidir).
|
||||
*/
|
||||
export function filterOffersForBrand<T extends { brandNorm: string }>(
|
||||
offers: T[],
|
||||
@@ -74,9 +219,12 @@ export function filterOffersForBrand<T extends { brandNorm: string }>(
|
||||
if (requestedBrandNorm) {
|
||||
const compat = offers.filter((o) => brandCompatible(o.brandNorm, requestedBrandNorm));
|
||||
if (compat.length > 0) return compat;
|
||||
return allowBrandless(codeNorm) ? offers : [];
|
||||
if (isVehicleBrand(requestedBrandNorm) && allowBrandless(codeNorm)) {
|
||||
return selectOeOffers(offers);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
return allowBrandless(codeNorm) ? offers : [];
|
||||
return allowBrandless(codeNorm) ? selectOeOffers(offers) : [];
|
||||
}
|
||||
|
||||
export interface SupplierOffer {
|
||||
|
||||
@@ -17,6 +17,8 @@ import {
|
||||
filterOffersForBrand,
|
||||
istanbulToday,
|
||||
normPartCode,
|
||||
partPriceCurrentCacheKey,
|
||||
partPriceSeriesCacheKey,
|
||||
reconstructDailySeries,
|
||||
} from "./part-prices.logic";
|
||||
import { type SupplierPriceSource, createSupplierPriceSource } from "./supplier-price-source";
|
||||
@@ -54,10 +56,8 @@ const BATCH_CACHE_TTL = 1800;
|
||||
const MAX_BATCH_PARTS = 400;
|
||||
|
||||
export const partPriceKey = (codeNorm: string, brandNorm: string) => `${codeNorm}::${brandNorm}`;
|
||||
const seriesKey = (codeNorm: string, brandNorm: string) =>
|
||||
`partprice:series:v2:${codeNorm}::${brandNorm}`;
|
||||
const currentKey = (codeNorm: string, brandNorm: string) =>
|
||||
`partprice:cur:v2:${codeNorm}::${brandNorm}`;
|
||||
const seriesKey = partPriceSeriesCacheKey;
|
||||
const currentKey = partPriceCurrentCacheKey;
|
||||
|
||||
/**
|
||||
* Parça (kod + marka) bazlı tedarikçi fiyat görünümleri. P-servisi sözleşmesi:
|
||||
@@ -67,7 +67,8 @@ const currentKey = (codeNorm: string, brandNorm: string) =>
|
||||
* Kimlik (kod, marka): kısa sayısal kodlar markalar arası çakışır (FEBI 27155
|
||||
* ≠ GROS 27155 ≠ İBRAŞ 27155 — farklı fiziksel parçalar). Teklifler sku_map'in
|
||||
* marka etiketiyle istenen markaya süzülür (filterOffersForBrand); markasız
|
||||
* sorgu yalnızca uzun/benzersiz kodlarda tüm teklifleri kullanır.
|
||||
* sorgu (OEM detay ana kartı) yalnızca ORİJİNAL (OE/dağıtıcı) teklifleri
|
||||
* kullanır — orijinal+yan-sanayi havuzu hayalet medyan üretir (2026-07-14).
|
||||
*
|
||||
* - Seri: parça ilk kez istendiğinde takip history'sinden lazy-backfill edilir
|
||||
* ve pg'ye (part_price_tracks + part_price_daily) kalıcı yazılır; sonraki
|
||||
|
||||
Reference in New Issue
Block a user