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>
322 lines
12 KiB
TypeScript
322 lines
12 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
||
import {
|
||
type HistoryEvent,
|
||
type SupplierOffer,
|
||
allowBrandless,
|
||
brandCompatible,
|
||
computeStats,
|
||
filterOffersForBrand,
|
||
isOeSupplyLabel,
|
||
istanbulToday,
|
||
normPartCode,
|
||
oeFamilyOf,
|
||
percentile,
|
||
reconstructDailySeries,
|
||
selectOeOffers,
|
||
} from "./part-prices.logic";
|
||
|
||
describe("normPartCode", () => {
|
||
it("uppercases and strips non-alphanumerics (web normCode ile aynı kural)", () => {
|
||
expect(normPartCode("1j0 973-702")).toBe("1J0973702");
|
||
expect(normPartCode("62 92 9423")).toBe("62929423");
|
||
expect(normPartCode(" ")).toBe("");
|
||
});
|
||
});
|
||
|
||
describe("percentile", () => {
|
||
it("interpolates linearly (PERCENTILE_CONT)", () => {
|
||
expect(percentile([10], 99)).toBe(10);
|
||
expect(percentile([10, 20], 50)).toBe(15);
|
||
expect(percentile([1, 2, 3, 4, 5], 50)).toBe(3);
|
||
expect(percentile([1, 2, 3, 4, 5], 95)).toBeCloseTo(4.8);
|
||
});
|
||
});
|
||
|
||
describe("computeStats", () => {
|
||
it("filters out-of-stock and zero-priced offers", () => {
|
||
const stats = computeStats([
|
||
{ price: 100, stock: 2 },
|
||
{ price: 0, stock: 5 },
|
||
{ price: 200, stock: 0 },
|
||
{ price: 300, stock: 1 },
|
||
]);
|
||
expect(stats).toEqual({ p50: 200, p95: 290, p99: 298, offerCount: 2 });
|
||
});
|
||
|
||
it("returns null prices when nothing is in stock", () => {
|
||
expect(computeStats([{ price: 100, stock: 0 }])).toEqual({
|
||
p50: null,
|
||
p95: null,
|
||
p99: null,
|
||
offerCount: 0,
|
||
});
|
||
});
|
||
});
|
||
|
||
describe("reconstructDailySeries", () => {
|
||
const W = "2026-01-29";
|
||
const TODAY = "2026-06-12";
|
||
|
||
it("hiç event yoksa güncel teklifleri pencere başından beri sabit sayar", () => {
|
||
const current: SupplierOffer[] = [
|
||
{ productId: 1, price: 100, stock: 3 },
|
||
{ productId: 2, price: 140, stock: 1 },
|
||
];
|
||
const series = reconstructDailySeries([], current, W, TODAY);
|
||
expect(series).toHaveLength(1);
|
||
expect(series[0]).toMatchObject({ date: W, p50: 120, offerCount: 2 });
|
||
});
|
||
|
||
it("ilk event'in prev değerleri baseline olur, takibe yeni girenler olmaz", () => {
|
||
const events: HistoryEvent[] = [
|
||
// 1 numara fiyat değiştirdi: 100 → 150
|
||
{ productId: 1, date: "2026-03-01", price: 150, stock: 2, prevPrice: 100, prevStock: 2 },
|
||
// 2 numara o gün takibe girdi (prev'ler 0) → baseline'da yok
|
||
{ productId: 2, date: "2026-03-01", price: 200, stock: 1, prevPrice: 0, prevStock: 0 },
|
||
];
|
||
const current: SupplierOffer[] = [
|
||
{ productId: 1, price: 150, stock: 2 },
|
||
{ productId: 2, price: 200, stock: 1 },
|
||
];
|
||
const series = reconstructDailySeries(events, current, W, TODAY);
|
||
expect(series[0]).toMatchObject({ date: W, p50: 100, offerCount: 1 });
|
||
expect(series[1]).toMatchObject({ date: "2026-03-01", p50: 175, offerCount: 2 });
|
||
// current son durumla aynı → bugün için fazladan nokta yok
|
||
expect(series).toHaveLength(2);
|
||
});
|
||
|
||
it("stok bitişini null-fiyatlı nokta olarak işler ve aynı istatistiği tekrarlamaz", () => {
|
||
const events: HistoryEvent[] = [
|
||
{ productId: 1, date: "2026-02-10", price: 100, stock: 0, prevPrice: 100, prevStock: 5 },
|
||
// istatistiği değiştirmeyen event (fiyat aynı, hâlâ stok dışı)
|
||
{ productId: 1, date: "2026-02-20", price: 110, stock: 0, prevPrice: 100, prevStock: 0 },
|
||
];
|
||
const current: SupplierOffer[] = [{ productId: 1, price: 110, stock: 0 }];
|
||
const series = reconstructDailySeries(events, current, W, TODAY);
|
||
expect(series[0]).toMatchObject({ date: W, p50: 100, offerCount: 1 });
|
||
expect(series[1]).toMatchObject({ date: "2026-02-10", p50: null, offerCount: 0 });
|
||
expect(series).toHaveLength(2);
|
||
});
|
||
|
||
it("current ile history çelişirse bugünü current'a göre düzeltir", () => {
|
||
const events: HistoryEvent[] = [
|
||
{ productId: 1, date: "2026-04-01", price: 90, stock: 1, prevPrice: 80, prevStock: 1 },
|
||
];
|
||
// history'nin bilmediği daha taze gerçek
|
||
const current: SupplierOffer[] = [{ productId: 1, price: 95, stock: 2 }];
|
||
const series = reconstructDailySeries(events, current, W, TODAY);
|
||
const last = series[series.length - 1];
|
||
expect(last).toMatchObject({ date: TODAY, p50: 95, offerCount: 1 });
|
||
});
|
||
|
||
it("aynı güne birden çok event tek nokta üretir", () => {
|
||
const events: HistoryEvent[] = [
|
||
{ productId: 1, date: "2026-05-05", price: 100, stock: 1, prevPrice: 0, prevStock: 0 },
|
||
{ productId: 2, date: "2026-05-05", price: 300, stock: 1, prevPrice: 0, prevStock: 0 },
|
||
];
|
||
const current: SupplierOffer[] = [
|
||
{ productId: 1, price: 100, stock: 1 },
|
||
{ productId: 2, price: 300, stock: 1 },
|
||
];
|
||
const series = reconstructDailySeries(events, current, W, TODAY);
|
||
expect(series).toHaveLength(1);
|
||
expect(series[0]).toMatchObject({ date: "2026-05-05", p50: 200, offerCount: 2 });
|
||
});
|
||
});
|
||
|
||
describe("brandCompatible", () => {
|
||
it("önek ve kısaltmaları tanır", () => {
|
||
expect(brandCompatible("BOSCH", "BOSCH")).toBe(true);
|
||
expect(brandCompatible("BCH", "BOSCH")).toBe(true); // sıralı altdizi
|
||
expect(brandCompatible("B", "BOSCH")).toBe(true); // tek harf önek
|
||
expect(brandCompatible("BLP", "BLUEPRINT")).toBe(true);
|
||
expect(brandCompatible("BLUEPRNT", "BLUEPRINT")).toBe(true); // Türkçe ı düşmüş
|
||
expect(brandCompatible("FEBI", "FEBIBILSTEIN")).toBe(true);
|
||
expect(brandCompatible("MANN", "MANNFILTER")).toBe(true);
|
||
expect(brandCompatible("BRA", "IBRAS")).toBe(true); // İBRAŞ → BRA
|
||
});
|
||
|
||
it("alakasız markaları reddeder", () => {
|
||
expect(brandCompatible("GROS", "FEBIBILSTEIN")).toBe(false);
|
||
expect(brandCompatible("NIFEA", "FEBIBILSTEIN")).toBe(false);
|
||
expect(brandCompatible("MAIS", "RENAULT")).toBe(false); // dağıtıcı — uzun-kod fallback'i halleder
|
||
expect(brandCompatible("", "BOSCH")).toBe(false);
|
||
});
|
||
});
|
||
|
||
describe("allowBrandless", () => {
|
||
it("uzun kodlar ve harf+rakam karışımı serbest, kısa salt-sayısal yasak", () => {
|
||
expect(allowBrandless("8200768913")).toBe(true); // 10 hane OE
|
||
expect(allowBrandless("46805832")).toBe(true); // 8 hane OE
|
||
expect(allowBrandless("W7008")).toBe(true); // harf+rakam
|
||
expect(allowBrandless("0249C6")).toBe(true); // PSA kısa OE, harf içerir
|
||
expect(allowBrandless("27155")).toBe(false); // kısa salt-sayısal — çakışma sınıfı
|
||
expect(allowBrandless("615014")).toBe(false);
|
||
});
|
||
});
|
||
|
||
describe("filterOffersForBrand", () => {
|
||
// 2026-06-12 vakası: 27155 kodu FEBI/GROS/İBRAŞ/NIFEA'da FARKLI parçalar.
|
||
const offers = [
|
||
{ brandNorm: "FEBI", price: 160 },
|
||
{ brandNorm: "GROS", price: 224 },
|
||
{ brandNorm: "IBR", price: 909 },
|
||
{ brandNorm: "NIFEA", price: 1229 },
|
||
];
|
||
|
||
it("kısa kodda istenen markanın tekliflerine süzer", () => {
|
||
const out = filterOffersForBrand(offers, "FEBIBILSTEIN", "27155");
|
||
expect(out).toEqual([{ brandNorm: "FEBI", price: 160 }]);
|
||
});
|
||
|
||
it("kısa kod + uyumsuz marka → boş (yanlış veri göstermez)", () => {
|
||
expect(filterOffersForBrand(offers, "TRW", "27155")).toEqual([]);
|
||
});
|
||
|
||
it("kısa kod + markasız istek → boş", () => {
|
||
expect(filterOffersForBrand(offers, "", "27155")).toEqual([]);
|
||
});
|
||
|
||
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 },
|
||
];
|
||
const out = filterOffersForBrand(mixed, "", "8200768913");
|
||
expect(out.map((o) => o.brandNorm).sort()).toEqual(["MAIS", "OPAR"]);
|
||
});
|
||
|
||
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("araç markası → SADECE o markanın OE ailesi (PSA vs OPAR ayrışır)", () => {
|
||
// 9827622780 vakası: aynı koda PSA (Peugeot ailesi) ve OPAR (Fiat ailesi)
|
||
// düşüyor. Peugeot kodu istenince yalnız PSA gelir, OPAR elenir → medyan
|
||
// artık iki dağıtıcının ortası (hayalet) değil.
|
||
const mixed = [
|
||
{ brandNorm: "PSA", price: 5531 },
|
||
{ brandNorm: "OPAR", price: 3459 },
|
||
{ brandNorm: "FEBI", price: 900 },
|
||
];
|
||
expect(filterOffersForBrand(mixed, "PEUGEOT", "9827622780")).toEqual([
|
||
{ brandNorm: "PSA", price: 5531 },
|
||
]);
|
||
// Aynı kod Fiat aracından istenirse OPAR gelir.
|
||
expect(filterOffersForBrand(mixed, "FIAT", "9827622780")).toEqual([
|
||
{ brandNorm: "OPAR", price: 3459 },
|
||
]);
|
||
});
|
||
|
||
it("araç markası dağıtıcı etiketine eşlenir (RENAULT → MAIS), yan sanayi elenir", () => {
|
||
const oe = [
|
||
{ brandNorm: "MAIS", price: 195 },
|
||
{ brandNorm: "FEBI", price: 90 },
|
||
{ brandNorm: "OPAR", price: 210 }, // farklı aile → elenir
|
||
];
|
||
expect(filterOffersForBrand(oe, "RENAULT", "8200768913")).toEqual([
|
||
{ brandNorm: "MAIS", price: 195 },
|
||
]);
|
||
});
|
||
|
||
it("araç markası + jenerik ORJINAL → elenir (aile belirsiz)", () => {
|
||
const oe = [
|
||
{ brandNorm: "PSA", price: 5531 },
|
||
{ brandNorm: "ORJINAL", price: 111 },
|
||
];
|
||
expect(filterOffersForBrand(oe, "CITROEN", "9827622780")).toEqual([
|
||
{ brandNorm: "PSA", price: 5531 },
|
||
]);
|
||
});
|
||
|
||
it("yan sanayi markası çipi → yalnız etiketi uyumlular, OE'ye düşmez", () => {
|
||
const oe = [
|
||
{ brandNorm: "MAIS", price: 195 },
|
||
{ brandNorm: "FEBI", price: 160 },
|
||
];
|
||
// FEBI çipi FEBI'yi alır; MAIS (orijinal) gösterilmez.
|
||
expect(filterOffersForBrand(oe, "FEBIBILSTEIN", "8200768913")).toEqual([
|
||
{ brandNorm: "FEBI", price: 160 },
|
||
]);
|
||
});
|
||
});
|
||
|
||
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); // jenerik orijinal
|
||
expect(isOeSupplyLabel("VECO")).toBe(true); // Iveco OE alias'ı
|
||
});
|
||
|
||
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("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 }, // artık orijinal
|
||
{ brandNorm: "IBR", price: 909 }, // İbraş = yan sanayi
|
||
]);
|
||
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"]);
|
||
});
|
||
});
|
||
|
||
describe("istanbulToday", () => {
|
||
it("UTC gece yarısı civarında İstanbul gününü döner", () => {
|
||
// 2026-06-11 22:30 UTC = 2026-06-12 01:30 İstanbul
|
||
expect(istanbulToday(new Date("2026-06-11T22:30:00Z"))).toBe("2026-06-12");
|
||
});
|
||
});
|