fix(part-prices): fiyat kimliği marka+kod oldu — kısa kodlarda marka karışması giderildi

Kısa sayısal kodlar markalar arası çakışıyor ve FARKLI fiziksel parçaların
fiyatları tek havuzda karışıyordu (vaka: FEBI 27155 ₺160 iken çip GROS+İBRAŞ+
NIFEA karışımı ₺566 medyan gösterdi).

- sku_map'e brand_norm eklendi (takip p10 kolonunun marka kısmı; yoksa sku
  öneki) — vmi'de yeniden kuruldu (6,8M satır, %99,4 markalı)
- filterOffersForBrand: önek/sıralı-altdizi marka eşleme (BCH→BOSCH,
  BLP→BLUEPRINT, BRA→İBRAŞ); markasız sorgu yalnızca uzun/benzersiz kodlarda
  (≥8 ya da ≥5+harf+rakam) tüm teklifleri kullanır; kısa salt-sayısal kod
  uyumsuzsa boş döner (yanlış veri göstermek yok)
- pg kimliği (code_norm, brand_norm) — 0019 drop+recreate (yalnız 3 test kodu
  vardı, lazy-backfill yeniden doldurur)
- API: series?code&brand; current-batch artık { parts: [{code, brand}] } alır,
  cevap "CODE::BRAND" anahtarlı; Redis anahtarları v2
- Web: satır çipleri ve dialog markayı geçirir; ana OEM kodu markasız kalır
  (uzun-kod kuralıyla eşleşir); OE çiplerinde dağıtıcı etiketi fallback'i
  (MAIS taşıyan tekliflere RENAULT isteği uzun kodda izinli)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-12 14:55:44 +03:00
parent 51d8104cdc
commit f394897a08
13 changed files with 434 additions and 142 deletions

View File

@@ -57,7 +57,7 @@ export function PartPriceChip({ code, brand, current, className }: PartPriceChip
</DialogTitle>
<DialogDescription>Tedarikçi fiyat geçmişi</DialogDescription>
</DialogHeader>
{open && <PartPriceSection code={code} variant="plain" />}
{open && <PartPriceSection code={code} brand={brand} variant="plain" />}
</DialogContent>
</Dialog>
);

View File

@@ -29,14 +29,19 @@ const RANGES = [
type RangeKey = (typeof RANGES)[number]["key"];
export function usePartPriceSeries(code: string, enabled = true) {
export function usePartPriceSeries(code: string, brand?: string, enabled = true) {
const codeNorm = normPartCode(code);
const brandNorm = normPartCode(brand ?? "");
return useQuery({
queryKey: ["part-price-series", codeNorm],
queryKey: ["part-price-series", codeNorm, brandNorm],
enabled: enabled && codeNorm.length >= 5,
staleTime: 10 * 60 * 1000,
queryFn: () =>
api.get<PartPriceSeriesView>(`/part-prices/series?code=${encodeURIComponent(code)}`),
api.get<PartPriceSeriesView>(
`/part-prices/series?code=${encodeURIComponent(code)}${
brand ? `&brand=${encodeURIComponent(brand)}` : ""
}`,
),
});
}
@@ -61,13 +66,21 @@ function DeltaBadge({ pct }: { pct: number }) {
interface PartPriceSectionProps {
code: string;
/** Parça markası — kısa kodlarda teklifleri doğru markaya süzer; OEM
* detayın ana kodu gibi markasız bağlamlarda boş bırakılır. */
brand?: string;
/** Ana sayfa yerleşiminde kart çerçevesi; dialog içinde çıplak. */
variant?: "card" | "plain";
className?: string;
}
export function PartPriceSection({ code, variant = "card", className }: PartPriceSectionProps) {
const { data, isLoading } = usePartPriceSeries(code);
export function PartPriceSection({
code,
brand,
variant = "card",
className,
}: PartPriceSectionProps) {
const { data, isLoading } = usePartPriceSeries(code, brand);
const [range, setRange] = useState<RangeKey>("all");
const todayIso = useMemo(() => istanbulTodayIso(), []);
@@ -75,6 +88,7 @@ export function PartPriceSection({ code, variant = "card", className }: PartPric
if (data?.matched) {
capture("part_price_viewed", {
code: data.codeNorm,
brand: data.brandNorm || undefined,
points: data.series.length,
offers: data.latest?.offerCount ?? 0,
});