feat(part-prices): parça kodu bazlı tedarikçi fiyat geçmişi + OEM sayfasında p50/p95/p99 grafiği

- pg: part_price_tracks + part_price_daily (0018) — (kod, kaynak, gün) başına
  stoktaki tekliflerin p50/p95/p99 + teklif sayısı; source='supplier' şimdilik,
  perakende ileride aynı tabloya 'retail' olarak girer. Tedarikçi kimliği yok.
- API: GET /part-prices/series (ilk istekte takip history'sinden lazy-backfill,
  sonrası salt-pg + Redis) ve POST /part-prices/current-batch (sayfadaki kodlar
  için canlı güncel istatistik). P-servisi sözleşmesi: asla throw yok, fail-open.
- Worker: part-price-refresh cron'u 19:30 Europe/Istanbul (takip sync'i 19:05'te
  bitiyor) — izlenen kodlara bugünün satırını upsert eder, sku_map'i artımlı
  bakar, Redis cache düşürür. SUPPLIER_PRICE_DB_* yoksa sessiz no-op.
- Kaynak köprüsü: takip.sku_map (code_norm → product_id; tam sku / ilk-boşluk /
  ilk-tire sonrası normalize adayları) vmi MySQL'inde kurulu; 6,8M satır.
- Web: OEM detayında "Tedarikçi fiyat analizi" kartı (güncel medyan + P95/P99 +
  teklif sayısı + 30g delta, 30G/90G/Tümü aralıklı step grafik, recharts) ve
  article/muadil/OE satırlarında fiyat çipi → dialog'da tam geçmiş.
- Fix(p): td snapshot'ında gerçek üretici kodu articles.name'de (article_number
  %96 upstream sayısal ID) — sayfa artık kopyalanabilir gerçek kodu gösteriyor.
- compose: SUPPLIER_PRICE_DB_ENABLED/URL api+worker bloklarına eklendi.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-12 13:33:37 +03:00
parent 2c848df97d
commit 532e1ad9ef
27 changed files with 2047 additions and 22 deletions

View File

@@ -1,10 +1,12 @@
import {
boolean,
date,
index,
integer,
jsonb,
numeric,
pgTable,
primaryKey,
text,
timestamp,
uniqueIndex,
@@ -770,3 +772,36 @@ export const oemExpertRewards = pgTable(
index("oem_expert_rewards_user_id_idx").on(table.userId),
],
);
// ─── Part Price History (tedarikçi fiyat verisi) ──────
// Parça kodu bazlı piyasa fiyat geçmişi; kaynak takip MySQL'i (Tailscale).
// tracks = izlenen kodlar: ilk seriyi API lazy-backfill yazar (kod ilk kez
// görüntülendiğinde), günlük cron yalnızca buradaki kodlara bugünün satırını
// ekler. daily = (kod, kaynak, gün) başına stoktaki tekliflerin p50/p95/p99
// dağılımı + teklif sayısı; sıfır-teklif günleri null fiyat + offerCount 0
// ile saklanır ki "stok yok" bilgisi de seriye işlensin. source şimdilik hep
// 'supplier'; perakende fiyatı geldiğinde aynı tabloya 'retail' satırları
// girer. Tedarikçi kimliği bilinçli olarak hiçbir yerde tutulmaz.
export const partPriceTracks = pgTable("part_price_tracks", {
codeNorm: varchar("code_norm", { length: 64 }).primaryKey(),
firstSeenAt: timestamp("first_seen_at", { withTimezone: true }).defaultNow().notNull(),
backfilledAt: timestamp("backfilled_at", { withTimezone: true }),
lastRefreshedAt: timestamp("last_refreshed_at", { withTimezone: true }),
});
export const partPriceDaily = pgTable(
"part_price_daily",
{
codeNorm: varchar("code_norm", { length: 64 }).notNull(),
source: varchar("source", { length: 16 }).default("supplier").notNull(),
date: date("date").notNull(),
p50: numeric("p50", { precision: 15, scale: 4 }),
p95: numeric("p95", { precision: 15, scale: 4 }),
p99: numeric("p99", { precision: 15, scale: 4 }),
offerCount: integer("offer_count").default(0).notNull(),
},
(table) => [
primaryKey({ columns: [table.codeNorm, table.source, table.date] }),
index("part_price_daily_code_date_idx").on(table.codeNorm, table.date),
],
);