Firma Adı/Adres/Şehir/Ülke/Vergi No döküm yerine tek satır:
"1209 Mountain Road PL NE #11131, Albuquerque, NM 87110, USA" (Türkiye Ofisi ile tutarlı).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Exhaustive depth-5 pre-warm ~460k'lık sınırsız BFS fan-out üretiyordu, hiç
drenaj olmuyor ve faydalı sığ + reactive işi açlığa düşürüyordu. Üst ~2
seviyeyi her decode edilmiş araç için pre-warm etmek sınırlı iş (günlerde
biter), boş katalog sayfasını öldürür, ilk tıklamaları anında yapar. Derin
kategoriler on-view (getCategoryTree) tembel çekilip kalıcı cache'lenir.
Env-tunable: PREFETCH_MAX_DEPTH. MAX_DEPTH guard mevcut derin job'ları no-op'a
çevirir (PL24'e yük yok).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
PL24 prefetch artık 09:00-18:00 İstanbul penceresine bağlı değil; backfill
backlog'unun ~%96.7'si PL24 children olduğundan 9 saatlik pencere drenajı
boğuyordu. Pencere env ile geri daraltılabilir (PREFETCH_PL24_START/_END).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Floxy residential proxy bitince/çökünce (402 bakiye veya tünel reddi —
ERR_TUNNEL_CONNECTION_FAILED), EMEX + pcat decode istekleri ölü proxy'ye
çarpıp decode başarı oranını çökertiyordu (2026-06-13: ~17k tünel hatası/24s,
decode %78→%45). Önceki "DataImpulse last-ditch fallback" tarayıcı yolunda hiç
yoktu ve HTTP yolunda her istekte 2 ölü Floxy denemesi ziyan ediyordu.
Paylaşılan ProxyHealthService kapısı (in-memory cooldown): herhangi bir tüketici
bir Floxy bağlantı hatası görünce kapıyı tetikler; cooldown boyunca TÜM tüketiciler
DataImpulse'a düşer. Floxy'den ilk başarılı yanıt veya cooldown bitişi kapıyı
temizler (kendi kendini iyileştirir, periyodik yeniden-deneme). FLOXY_FAILOVER_COOLDOWN_MS
ile ayarlanır (varsayılan 180s).
Bağlanan tüketiciler:
- EMEX HTTP (fetchEmexHtml): kapı açıkken DataImpulse-öncelikli zamanlama
- EMEX tarayıcı (Playwright): launch'ta dinamik sağlayıcı seçimi + ensureSession
health-gate'i (Floxy ölünce DataImpulse'a relaunch, kapı temizlenince Floxy'i
yeniden dene); scrape-içi tünel ölümünde tripFloxyFailover
- pcat (buildProxy): hem call hem capture (Playwright JWT) bacakları DataImpulse'a düşer
isProxyConnectFailure(): yalnız gerçek bağlantı hatalarında tetikler — yavaş-ama-
canlı exit'in nav timeout'u failover'ı tetiklemez.
Test: ProxyHealthService + isProxyConnectFailure birim testleri; tüm api suite (303) yeşil.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Reklam engelleyici / gizlilik eklentisi challenges.cloudflare.com'u
kestiğinde widget sessizce kayboluyordu; form token'sız gidince
kullanıcı yanıltıcı 'E-posta veya şifre hatalı' görüyordu. Artık
script yüklenemediğinde veya widget error-callback verdiğinde
widget'ın altında açıklayıcı bir uyarı çıkıyor (login/register/iletişim).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Broadened extractYearFromPcatCar after auditing 176 null-year prod pcat
vehicles: the build date sits under "production date" (space) far more often
than "production_date" (underscore), and values come both year-first
(2006/02/27) and day-first (08/03/2016). Match the date key by spelling-agnostic
substring and pull the 4-digit year via regex so parseInt no longer reads the
day. Concrete Year param still wins over build date.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
pcat decode emits a faceting axis param key="year" value="All" BEFORE the
concrete per-car key="Year" value="2021". extractYearFromPcatCar used
find(includes("year")) which grabbed the axis → parseInt("All")=NaN → null,
so pcat-sourced vehicles (e.g. Opel ASTRA-J W0VPD5EC1MG063839) saved with a
blank year despite the year being present in raw_data. Scan all year-ish
params for a plausible value, then fall back to production_date.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Monthly/yearly purchases were one-time charges: our "subscription" was just
an end_date stamp, access silently died at period end and no renewal
machinery existed (no auto-charge, no reminder) — every paying customer had
to notice the lockout and re-buy by hand.
- Checkout now mode:"subscription" with inline recurring price_data; the
Stripe customer is stored on first purchase and reused (saved card +
invoice history on one record, with a stale-customer retry guard)
- invoice.paid webhook: extends end_date to the billing-line period end,
records a completed payment (deduped on stripe_invoice_id against webhook
retries), captures subscription_renewed with $revenue, mails the receipt;
late dunning recovery re-activates the row and re-grants Full-plan brands
- invoice.payment_failed webhook: dunning mail with Stripe's next retry
date; access is NOT cut — end_date governs and the nightly cron closes it
if every retry fails. Product rule: mail on success, mail on failure,
never a pre-charge reminder
- customer.subscription.deleted: stamps cancelledAt; renewals stop and
access runs out at end_date naturally
- cancel()/resume() sync cancel_at_period_end to Stripe (forwardRef pair) —
an in-app cancel that leaves the card being charged was unacceptable
- subscription_create invoices only enrich the checkout's payment row
(payment intent + invoice id for receipts/panel refunds); activation,
revenue and the receipt stay on checkout.session.completed
- migration 0020: users.stripe_customer_id,
user_subscriptions.stripe_subscription_id (+idx),
payments.stripe_invoice_id (+idx)
Legacy one-time subs (3 live payers) are untouched: they expire at their
end_date as before and board recurring on their next manual checkout.
Promote checklist: add invoice.paid / invoice.payment_failed /
customer.subscription.deleted to the prod webhook endpoint; verify Stripe
"Customer emails" upcoming-renewal reminders stay OFF.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Clicking "Öde" ran create(), which expired the user's live trial BEFORE
any money moved. Anyone who then abandoned the Stripe page lost their
remaining trial days and was fully locked out (brand-access requires
active|trial) — every abandoned checkout became silent churn (3 real
victims on prod: 06-02 and 2x 06-10, incl. a 9.990 TRY yearly intent).
- create(): no longer touches the trial; only stale pending rows are
expired at checkout start
- activateSubscription(): supersedes the trial here instead, once the
payment has actually landed (single legitimate early-end point;
idempotency guard unaffected)
- getMySubscription(): status-priority pick (active > pending > trial >
cancelled > rest) so the expired remains of an abandoned checkout —
always the newest row — no longer eclipse a live trial on the
subscription page
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
The BullMQ cron scan never fired on prod (ic frozen at 28 for days, 0
scans completed) even after the lifo change. Root cause, confirmed live:
a cron scheduler enqueues a DELAYED marker, and BullMQ promotes delayed
jobs to the wait-list HEAD via LPUSH — `promoteDelayedJobs` ignores lifo —
while the worker pops from the tail. Behind the ~444k deep-drill backlog
the scan marker sat at wait position 1 (head) with ~444k jobs ahead of it
→ unreachable for weeks. So scheduler-level lifo cannot work; the marker
must not go through the queue at all.
Run the scan on an in-process hourly timer in PrefetchWorkerService
instead (kickoff 60s after boot, then hourly), prod-host gated and
single-fired via a Redis NX lock. The scan's Phase-1 still enqueues
NON-delayed `lifo` init jobs, which DO jump the wait list (RPUSH tail →
popped next), so newly-decoded / zero-parts vehicles get onboarded ahead
of the backlog while it drains. jobs.module now removes the old cron
scheduler so stale markers stop being produced; the backfill-scan job
handler stays so any already-queued legacy marker runs harmlessly.
Tested: spec covers the NX lock (run when acquired / skip when held) plus
the existing lifo + Phase-1/Phase-2 gating; full api suite 272 passed,
`pnpm build` + biome green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
expert-rewards cron'u artık üyelik uzatmıyor: ay kapanışında ilk 3'ü
hesaplayıp admin@sase.tr'ye Postal üzerinden kazanan listesini yollar
(sıra, ad, e-posta, puan, ödül 30/15/7 gün, kullanıcı ID) ve
oem_expert_rewards'a denetim satırı yazar; günleri admin elle tanımlar.
Dev DB'den atılan mailler "[dev]" konu önekiyle ayrışır. Mail-önce-satır-
sonra sırası retry'da mail kaybını önler (nadir çift mail kabul edilir).
Abonelik/kredi dokunuşu tamamen kaldırıldı.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The prod docker build runs `nest build`, which compiles spec files too —
the untyped `vi.fn(async () => undefined)` mock made `queue.add.mock.calls[i]`
a zero-length tuple, so the destructures tripped TS2493/TS2352 and failed
`pnpm build` (tsc --noEmit had skipped the spec). Type the mock args as
unknown[]. No behaviour change; tests still 7/7, `pnpm build` green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Program (oylama + muadil önerisi + liderlik) artık iki katmanlı kapının
arkasında: PostHog "oem-expert-program" ana şalteri (local-eval, fail-open)
VE kullanım kriteri — en az 2 FARKLI başarılı VIN decode VE en az 2 FARKLI
OEM kodu kopyası. Karar sunucuda tek noktada (ExpertAccessService.check);
web GET /oem-votes/access ile bir kez sorar, kriterler istemciye sızmaz.
Kapalıyken: sidebar nav linki görünmez, OEM detayındaki oy kartı + öneri
bölümü render edilmez, /dashboard/uzmanlar kademeli-açılış mesajı gösterir
ve liderlik sorgusu atılmaz. Yazma uçları (oy, öneri) sunucu tarafında da
ForbiddenException ile korunur — UI gizlemek tek başına güven sınırı değil.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Parça Uzmanları sıralaması artık aylık sezon: liderlik tablosu içinde
bulunulan TR-ayının (Europe/Istanbul) puanlarını gösterir ve her ayın
1'i 00:00 TR'de kendiliğinden sıfırlanır. expert-rewards cron'u
(BullMQ scheduler, "0 0 1 * *" tz=Europe/Istanbul) aynı anda biten
sezonu kapatır ve ilk 3 oylayıcıya üyelik uzatması verir:
1. → 30 gün, 2. → 15 gün, 3. → 7 gün (EXPERT_REWARD_LADDER).
Ödül mekaniği referral'la birebir: canlı active/trial abonelik endDate
+gün uzar, yoksa günler users.referral_credit_days'e bankalanır (sonraki
trial/aktivasyonda tüketilir). oem_expert_rewards (migration 0017,
period+rank UNIQUE) hem denetim kaydı hem run-once garantisi — retry ya
da elle tetik çift ödül veremez. Sıralama ölçütü job ve leaderboard'da
birebir aynı (puan desc, eşitlikte puana erken ulaşan önde).
Web: uzmanlar sayfasına sezon şeridi ("Haziran 2026 sezonu" + 🥇1 ay ·
🥈15 gün · 🥉7 gün rozetleri); alt küçük-punto kural satırına aylık
sıfırlama + ödül notu eklendi. Leaderboard cevabına periodStart eklendi.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Uyumlu/Uyumsuz butonlarında oy sayıları artık görünmüyor (çoğunluğu ele
vermek oyu yönlendiriyordu); özet yine çekilir — kullanıcının kendi oyu
butonda işaretli kalır.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Topluluk oyu kartının koyu dolgusu kalktı (sayfa arkaplanıyla aynı, yalnız
çerçeve); desktop'ta başlıkta OEM kodunun sağına yerleşti, mobilde kodun
altına iner. Katalogdan gelişte araç etiketi + parça adı search-param'la
taşınır (v/p/vid) ve kartta butonların üstünde "araç · parça" satırı olarak
gösterilir — neyle neyin uyumlu olduğu tek alanda. vid oy kaydına analitik
bağlam olarak geri eklendi; doğrudan ziyarette genel soru metnine düşülür.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Üstteki üç-kartlı kural bloğu ve oy kartındaki kural cümlesi kaldırıldı;
kurallar tek satır küçük puntoyla sayfanın en altında ("Oy ver +1 ·
çoğunluğu tuttur +2 bonus · kodu ilk değerlendiren 3 puanı kapar").
Başlık ve boş-durum metinleri de oyunlaştırıldı; oy kartında yalnız
"Her oy puan kazandırır · Parça Uzmanları sıralaması" linki kaldı.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Parça tablosu sadeleşti: Uyum ve Adet kolonları kalktı, liste açılışındaki
toplu istekler (/oem-votes/lookup ve /p/matched) tamamen kaldırıldı. Her OEM
kodu artık koşulsuz /dashboard/oem/$code'a linklenir — P eşleşmesi olmayan
kodda da sayfa dolu: topluluk oyu kartı, muadil önerileri ve ters katalog.
OEM detay sayfası: OemVoteCard (uyumlu/uyumsuz, sayaçlar, puan toast'ı,
puanlama özeti + Parça Uzmanları linki) ve OemSuggestionsSection — eşleşme
bulunamayan kodlar için kullanıcıdan marka + parça kodu önerisi toplar.
Öneriler oem_suggestions tablosunda (kullanıcı+kod+normalize öneri başına
tek satır, ON CONFLICT yutulur), markaya+normalize koda göre gruplanıp
"× N kullanıcı" rozetiyle listelenir; önerilen kod kendi detayına linklenir.
Şimdilik öneri puan kazandırmaz; status kolonu moderasyon kancası.
API: oem-suggestions modülü (POST 10/dk throttle, GET ?code=), migration
0016_oem_suggestions. Eski tablo-içi OemVoteButtons bileşeni silindi.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The hourly catalog-backfill scan had gone effectively dead on prod: its
job sat behind a ~430k-deep wait list and, even when it ran, self-skipped
because the queue backlog (431k) was far over the maxBacklog ceiling
(1000). Net result: newly-decoded / zero-parts vehicles were never
onboarded — they starved behind the deep-drill backlog (ic stuck at 28
for days, 0 backfill-scan jobs ever completed).
Root constraint (verified against bullmq 5.68 lua): moveToActive drains
the `wait` list (RPOPLPUSH from the tail) BEFORE the `prioritized` ZSET,
so a `priority` job is starved behind an already-deep wait queue — the
opposite of what's wanted. The lever that works is `lifo`: it RPUSHes to
the tail, where the very next RPOPLPUSH picks it, ahead of the FIFO
backlog. addJobFromScheduler honours lifo too, so the scan job itself can
jump the queue.
Changes:
- Thread a `fast` flag through the init→children→parts chain; fast jobs
are enqueued with `lifo:true` so the whole chain jumps the backlog.
- Backfill scan: Phase-1 (zero-parts vehicles) now runs every wave in the
fast lane even when the backlog is over the ceiling; only Phase-2 (the
rolling rescan that piles on) is suspended while the queue is deep.
- Register the hourly scan job with `lifo:true` so it fires on the next
tick instead of being buried for days.
Tested: new prefetch-worker.service.spec (lifo wiring + Phase-1/Phase-2
gating), full api suite green (248 passed), typecheck + biome clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mirrors the PSA VIN-scope tests for the new …group.action parent gate: a Ford
json-vin-sub-group.action node with a positional link_wid routes to children
(not a silent empty leaf), and an empty drill surfaces loadError.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Parça satırlarına topluluk oyu eklendi: her OEM kodu için uyumlu/uyumsuz
ikilisi (👍/👎, sayaçlı). Oylar oem_votes'a (kullanıcı+kod başına tek oy,
fikir değişikliği günceller, puan üretmez), ödüller oem_vote_points
ledger'ına yazılır: oy +1, kesin çoğunlukla aynı yönde +2 (kodu ilk
oylayan her zaman 3 alır); ödüller oy anında kesinleşir, çoğunluk sonra
dönse de geri alınmaz. Aynı koda eşzamanlı oylar advisory lock ile
sıralanır.
/dashboard/uzmanlar: Trophy Gamification UI Kit'ten (ui.trophy.so, MIT)
uyarlanan kürsü + sıralama + puan rozetiyle "Parça Uzmanları" liderlik
sayfası; adlar KVKK-maskeli (S*** Y***), cevap kullanıcı id sızdırmaz.
Not: sidebar nav linki, tr/en i18n anahtarları ve routeTree 09a9487'de
gitmişti; bu commit eksik kalan rota/bileşen/API dosyalarını tamamlayarak
dev build'ini düzeltir. Migration: 0015_oem_votes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Drop /dashboard/changelog route, ChangelogTab component, useChangelog
hook, the sidebar nav item, and the related tr/en translation keys.
API-side changelog schemas are untouched.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
getCategoryWithPartsInner had parent gates for PSA (psa::/json-vin-main-groups),
pcat (hasSubgroups), pl24 link_wid *Group* tables, Volvo (vin-group.action) and
emex — but P4 VIN/legacy group nodes whose link_wid is a positional code
("1","2","CAP1"…) and whose path is a *…group.action* drill (Ford
json-vin-sub-group, Nissan/Opel json-sub-group, etc.) matched none of them. They
fell through to the leaf parts path, fetched no parts and rendered a silent empty
panel — the dominant current '0 parça' cluster (mekanik/kaporta/Elektrikli aksam…
across many vehicles; PostHog-confirmed). Add a gate that drills any pl24
…group.action node (excluding Volvo's own vin-group.action branch) → subgroups or
retryable loadError. Lazy-heals existing flag-less rows, no reseed.
Also set has_subgroups at seed time: PSA scopes → true (always parents);
P4-legacy cats → true when linkPath is a …group.action drill. Fixes folder
signposting + lets prefetch pre-drill them instead of treating them as leaves.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>