Commit Graph

1071 Commits

Author SHA1 Message Date
5b640988c4 Merge pull request 'dev' (#154) from dev into main
Reviewed-on: #154
2026-07-01 16:28:55 +03:00
8735eece13 Merge branch 'feat/trial-7-day' into dev
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
Shorten the free trial from 30 to 7 days: backend source of truth
(subscriptions.service TRIAL_DAYS), in-app banner rescale to the 7-day
window, tr/en copy (17 keys each), hardcoded CTAs, and the trial-progress
test. Existing trials keep their stored endDate — new sign-ups only.

Clean 3-way merge with the discount-code billing feature: trial and promo
changes touched disjoint regions of subscription/index.tsx and the message
catalogs. Post-merge verified — web+api typecheck clean, web tests 92/93
(the 1 red is the pre-existing landing index.test.tsx, unrelated to trial).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 16:26:02 +03:00
373af248b4 Merge pull request 'fix(vinpin): launch VinPower from the Horizon app-launcher on the permanent seat' (#153) from dev into main
Reviewed-on: #153
2026-07-01 10:02:38 +03:00
5763e7fbff fix(vinpin): launch VinPower from the Horizon app-launcher on the permanent seat
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
The permanent (paid) Vinpin seat does NOT auto-launch VinPower after login —
it lands on the Horizon HTML-Access app launcher ("Available: VINPIN"), so the
old brand-grid→Fiat flow failed with "Fiat ePER did not open". Add an
OCR-gated ensureBrandGrid step before the ePER-open loop that:

  - detects the launcher (Available / Running / No Running Items) and clicks the
    VINPIN app tile to launch VinPower;
  - satisfies VinPower's own secondary login dialog (username pre-filled,
    password blank) via the webclient-frame DOM password field, with a
    coordinate fallback;
  - dismisses a stray Horizon "Disconnected" dialog / waits through the blank
    connecting screen;
  - no-ops when the brand grid is already up (trial-style auto-launch), keeping
    it backward-compatible.

New coords/waits tuned for the permanent seat live in vinpin.constants.ts.
Never throws (failures fall through to the caller's retry → null). Verified
live on the permanent account: NM426300006A69963 → DOBLO, NM435600006H43436 →
TIPO-EGEA.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 09:52:33 +03:00
9df85c335f Merge pull request 'dev' (#152) from dev into main
Reviewed-on: #152
2026-07-01 09:07:08 +03:00
346ebaa0e6 feat(billing): indirim kodu sistemi (admin panel + Stripe promo + ödeme)
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
Admin paneli (admin@sase.tr) kod üretir/takip eder, ödeme sayfası kodu
uygular. Kodlar Stripe Coupon + Promotion Code olarak yaşar (limit/süre/
kullanım Stripe redemption'da enforce edilir); yerel discount_codes aynası
panel listeleme + checkout-anı preview içindir.

- migration 0022_discount_codes + schema (discount_codes)
- DiscountCodesModule: admin CRUD (@Roles admin) + /discount-codes/validate (login)
- StripeService: createPromotion / deactivatePromotion / getPromotionRedemptions
  / previewDiscount + resolvePlanAmount; createCheckoutSession opsiyonel
  discountCode -> discounts[{promotion_code}] + metadata.discount_code; webhook
  gerçek tahsilatı (session.amount_total) yazar + ayna times_redeemed++
- web: /dashboard/admin/discount-codes paneli + ödeme adımı indirim alanı
  (validate preview -> indirimli özet -> kod checkout'a); tr/en çeviriler
- 7 birim test (previewDiscount: % / sabit / süre / geçersiz/pasif/dolu/limit)

Not: 2026-04-22 Stripe API promotionCodes.create `promotion:{type:'coupon'}`
kullanıyor (üst-seviye `coupon` değil). dev'de Stripe key yok -> CREATE dev'de
503; E2E için dev'e Stripe TEST key gerekir.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 03:40:18 +03:00
a4e53f3cfd feat(trial): shorten free trial from 30 to 7 days
Backend source of truth (subscriptions.service.startFullTrial) now grants a
7-day trial via a named TRIAL_DAYS constant. Existing trials keep their stored
endDate — only new sign-ups are affected, so no migration is needed.

In-app conversion UX rescaled to the 7-day window, preserving the two-banner
no-overlap design:
  - urgency banner: visible the last 3 days (soft d3 / urgent d2 / critical d1)
  - value-upsell: owns days > 3 (the opening days), still proven-value gated
  - progress bars now derive the total span from real start->end dates, so both
    7-day and any remaining legacy 30-day trials render an accurate bar
Subscription-page thresholds (<=3 / >3) and the analytics gate updated to match.

Copy: 17 trial strings each in tr.json/en.json plus hardcoded CTAs in
site-header, _auth, register and demo-footer-cta -> "7 gun / 7 Gun / 7-Day".

Lifecycle emails need no change: trial-ending fires 3-4 days before endDate
and win-back after expiry (both already relative to endDate), and templates
use the dynamic {{daysLeft}} variable (no hardcoded 30).

Test: trial-progress-banner spec updated to 7-day scenarios.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 16:37:21 +03:00
3de7fa4989 ux(vinpin): brand the decoding state as Sase, drop external-catalog wording
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 14:13:22 +03:00
fff873a64d fix(vinpin): BullMQ jobId cannot contain ':' — use 'vinpin-<vin>'
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 14:02:30 +03:00
38c2afa587 chore(deploy): wire VINPIN_* env into api+worker compose (flag off by default)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 13:43:35 +03:00
8e45d00173 feat(vinpin): vision-loop decode driver (OCR) — reliable VDI automation
Replace the brittle clipboard/fixed-coord extraction with an OCR vision loop:
screenshot -> detect state (login/grid/portal/eper/modal via tesseract keywords)
-> act -> verify -> retry/recover. Adds portal->catalogue detection (the seat
non-deterministically opens a multi-brand portal with no VIN field), not-found
handling, and ffmpeg crop+upscale -> tesseract.js for the decode modal. Only the
model token + year are needed (matcher-tolerant), so OCR garbling is harmless.

Validated live against a trial seat: 6/6 of the ePER-present Fiat VINs decoded to
the correct model (EGEA/DOBLO); 2 genuine "vehicle not found" coverage gaps
reported as null. 8/8 correct outcomes. Still flag-gated off by default; tesseract
is dynamically imported only inside the worker. Warm-session persistence under
swiftshader is the remaining hardening item (currently re-logins per VIN).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 11:00:07 +03:00
dfe90fb968 feat(vehicles): automatic Vinpin VIN-decode fallback (flag-gated, off by default)
When PL24/pcat/emex can't decode a Fiat VIN, decode it via the Vinpin ePER
web catalog (warm-session Playwright worker, single seat, BullMQ concurrency 1),
cache the exact vehicle in vinpin_decodes, match it to PL24's existing
catalog_vehicle for that model, and serve the parts from there. Vinpin = decode
oracle only; PL24 already holds the parts (e.g. Egea/Linea/Doblo).

Strictly gated behind VINPIN_ENABLED (default false) + a Fiat-only brand
allowlist: with the flag off, decodeVin behaviour is byte-identical and the
queue is never touched (covered by tests). Coordinates/selectors in
vinpin.constants.ts are marked TUNE-AGAINST-LIVE-PAID-SEAT.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 07:36:39 +03:00
72889796e5 Merge pull request 'perf(prefetch): pcat rate 8->20/dk + pace 15s->5s (ramp adim 1)' (#151) from dev into main 2026-06-25 09:56:24 +03:00
3096d35113 perf(prefetch): pcat rate 8→20/dk + pace 15s→5s (ramp adım 1)
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
pcat kuyruğun %67'si (~26.5k) ve en yavaş drenajı. Proxy = DataImpulse rotating
gateway (74.81.81.81:10000-10999, ~1000 port-rotated çıkış IP — 2026-06-25
doğrulandı: 5 port→5 farklı Alman residential IP), per-IP ban yükü dağılıyor →
8/dk shared cap gereksiz muhafazakâr. Adım 1: 20/dk + pace 5s. (Coolify env yolu
duplike-girdi yüzünden container'a inmedi; kod-default güvenilir yol.) Env override
hâlâ mümkün (PREFETCH_RATE_PCAT/_PCAT_DELAY_MS). 429/403/tunnel izlenecek.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 09:56:04 +03:00
a4103a8e08 Merge pull request 'fix(subscription): default billing period to monthly, not yearly' (#150) from dev into main
Reviewed-on: #150
2026-06-23 10:09:08 +03:00
31fbcf2821 fix(subscription): default billing period to monthly, not yearly
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
Yearly upfront (₺9.990 Full) is the top checkout-abandonment point, so an
annual commitment shouldn't be the default selection. New users now land on
monthly; the yearly tab keeps its +2-month gift badge, and an existing/
pending subscriber's or marketing-preselected period still wins.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 09:57:39 +03:00
0760989b52 Merge pull request 'feat(vehicles): model-browse fallback for no-catalog VINs' (#149) from dev into main
Reviewed-on: #149
2026-06-23 09:13:10 +03:00
9232e40942 feat(vehicles): model-browse fallback for no-catalog VINs
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
When a VIN can't be decoded by any source but its WMI brand is known and
has a browse-able catalog (SERVICE_TO_BRAND), return a structured
{ noCatalog: { brandName, display }, vin } 200 response instead of
dead-ending. The web surfaces a "kataloğunu modelden incele" CTA that
deep-links into the existing /dashboard/catalog browse, where the parts
usually exist (Fiat Egea NM4356 -> PL24 TIPO-EGEA; old Renault VF1 -> 147k
emex parts) but aren't reachable by the specific VIN's index entry.

Brands with no browse catalog (Honda, Maserati, Alfa, ...) keep the
existing informative dead-end. New analytics: vin_decode_no_catalog +
vin_no_catalog_browse_clicked (this case no longer emits vin_decode_error).

Tests: 2 api (browseable -> fallback, non-browseable -> throws) + 1 web
(CTA renders, no error banner). RCA writeup:
/home/s/ss/katalogsiz-vin-rca-2026-06-23.md

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 08:58:58 +03:00
00d1e8c961 Merge pull request 'fix(faz6): generic-model skip kaldir (PSA false-poison) — cap-only' (#148) from dev into main 2026-06-23 06:36:23 +03:00
b3dd119c99 fix(prefetch/faz6): generic-model skip'i KALDIR — PSA'yı yanlış zehirliyordu
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
Tespit: generic-model (model==brand) sadece Opel'i değil PSA'yı (Peugeot/Citroën)
da kapsıyor; ama PSA generic-model PATLAMIYOR (~200 kat) ve PARÇA ALIYOR (512/290).
Skip onları yanlışlıkla poison'lar + prefetch'i keserdi. Tek doğru guard =
CATEGORY_CAP (marka-bağımsız; yalnız gerçek patlamada >3000 tetiklenir → Opel'i
durdurur, küçük PSA'yı korur). isGenericModel + processInit gate kaldırıldı,
spec sadeleşti (cap + scan-skip kaldı).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 06:35:59 +03:00
335a3b0cae Merge pull request 'Faz 6: anti-poison (generic-model skip + per-arac kategori tavani)' (#147) from dev into main 2026-06-23 06:33:20 +03:00
95061dfa53 feat(prefetch/faz6): anti-poison — generic-model skip + per-araç kategori tavanı
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
5 Opel aracı (model="Opel" decode çözememiş) generic ROOT kataloğa düşüp
419k/102k/21k kategori + 0 PARÇA üretti → 97k kuyruğu zehirledi. İki guard:
- isGenericModel (model==brand veya boş) → processInit skip + markPoison
- CATEGORY_CAP (3000, env PREFETCH_CATEGORY_CAP): processChildren progress.total
  bu tavanı aşınca drill'i durdurur + markPoison
markPoison → prefetch:poison:<id> (30g TTL) + scheduled/progress temizler;
processInit & tryPick poison'ı atlar. +3 spec (generic skip / cap / scan skip).
Full api suite 321 pass. Çöp kategori temizliği ayrıca yapılacak.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 06:33:04 +03:00
fd884271b7 Merge pull request 'Tam katalog kapsama: 4-faz prefetch stratejisi (derinlik+skip-completed+per-source+env)' (#146) from dev into main 2026-06-22 11:30:48 +03:00
d119b1eb2c feat(prefetch/faz4): PREFETCH_* env'i api compose bloğuna wire et (Coolify-tunable)
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
Memory gotcha: Coolify env yalnız compose service `environment:` bloğu ${VAR}
referans ederse container'a ulaşır. Prefetch worker api container'ında in-process
→ 13 anahtar api bloğuna eklendi (CATALOG_BACKFILL_ENABLED + PREFETCH_CONCURRENCY/
RATE_MAX/MAX_DEPTH/RATE_PL24/RATE_EMEX/RATE_PCAT/PCAT_DELAY_MS/PL24_START/PL24_END/
NORESULT_MAX/NORESULT_TTL_DAYS/COMPLETE_TTL_DAYS). Hepsi `:-` boş→kod-default
(davranış değişmez); artık rate/concurrency Coolify UI'dan deploy'suz ramp edilebilir.
PL24 rate yükseltme rotating proxy + izleme ile (Faz 5).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 11:27:19 +03:00
5200d5a61d feat(prefetch/faz3): per-source paralel rate limit (PL24/EMEX/pcat bağımsız)
Tek global 20/dk limiter tüm kaynakları paylaştırıyordu → yavaş pcat PL24/emex'i
açlığa düşürüyordu. Artık her kaynak Redis fixed-window ile BAĞIMSIZ kısılıyor
(SOURCE_RATE_MAX: pl24=20, emex=20, pcat=8; env PREFETCH_RATE_PL24/_EMEX/_PCAT).
Global WORKER_RATE_MAX güvenlik tavanı (20→60). RateLimitError yeni cause
"source-rate" → process() per-job defer eder (emex/pcat PL24'ü beklemez); cooldown
hâlâ tüm worker'ı duraklatır. PL24 default'u 20'de KALDI (ban riski; yüksek-rate
Faz 4 rotating proxy ile). +spec: under/over ceiling + unknown-source unlimited.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 11:25:41 +03:00
7c715af0c5 feat(prefetch/faz2): tamamlanan araçları işaretle + Phase-2 rescan'de atla
Chain parça ile tamamlanınca prefetch:complete:<id> set edilir (TTL 21g,
env PREFETCH_COMPLETE_TTL_DAYS). tryPick bu işaretliyi atlar → Phase-2 bitmiş
ağaçları her dalga yeniden gezmez (eski "sonsuz churn" durur). Başarısız job
isFinished'e ulaştırmaz → kısmi ağaç asla complete işaretlenmez, gap-fill korunur.
TTL periyodik re-validasyon sağlar. +spec: complete-marker'lı araç Phase-2'de atlanır.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 11:22:02 +03:00
61436db48f feat(prefetch/faz1): tam-derinlik (MAX_DEPTH 2→12) + no-op children job üretimini durdur
Hedef = decode edilmiş tüm araçların TAM kategori+parça ağacı DB'de. Cap artık
hedef değil güvenlik tavanı (gerçek ağaçlar ~5-7 derin; doğal yaprak tespiti
durduruyor). queueCategoryJob non-leaf dalı `depth < MAX_DEPTH` ile kapılandı:
processChildren'ın early-return'ünü aynalar → kuyruğun ~%85'ini oluşturan
boş depth-job'ları (rate-limit slotu harcayan no-op) hiç üretilmez.
+spec: depth>=cap'te children enqueue YOK, cap altında VAR.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 11:17:36 +03:00
a36008017c Merge pull request 'Iletisim: Merkez adresini tek satira sadelestir' (#145) from dev into main 2026-06-22 09:42:29 +03:00
70b6597811 feat(contact): Merkez adresini tek satıra sadeleştir
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
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>
2026-06-22 09:39:45 +03:00
64b2c9fb06 Merge pull request 'Iletisim: Merkez/Turkiye Ofisi adresleri + click-to-call telefon' (#144) from dev into main 2026-06-22 09:31:38 +03:00
690bdeb647 feat(contact): USA adresini "Merkez" yap + Türkiye Ofisi ekle + click-to-call telefon
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
İletişim sayfası: mevcut THINXTRA LLC (USA) adresi "Merkez" başlığı altına alındı,
"Türkiye Ofisi" (Bahçekapı mah, 2473. cad. No: 98 Etimesgut / Ankara) eklendi.
Destek kartında mailin yanına 0 850 840 8939 tel: linkiyle (click-to-call) eklendi.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 09:26:48 +03:00
3cadf80286 Merge pull request 'Backfill BFS derinligini 5->2'ye indir (sig pre-warm)' (#143) from dev into main 2026-06-21 16:59:54 +03:00
50cef50289 feat(prefetch): backfill BFS derinliğini 5→2'ye indir (sığ pre-warm + tembel derin)
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
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>
2026-06-21 10:05:31 +03:00
34d68510e3 Merge pull request 'PL24 scrape penceresini devre disi birak (7/24 akis)' (#142) from dev into main 2026-06-21 09:24:46 +03:00
bee68b6a34 feat(prefetch): PL24 scrape penceresini varsayılan olarak devre dışı bırak (7/24 akış)
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
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>
2026-06-21 09:14:06 +03:00
5aad3e8b22 Merge pull request 'feat(proxy): otomatik Floxy→DataImpulse failover (decode regresyonu)' (#140) from dev into main
Reviewed-on: #140
2026-06-13 18:30:45 +00:00
b992c0075b feat(proxy): otomatik Floxy→DataImpulse failover (decode regresyonu)
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
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>
2026-06-13 21:03:23 +03:00
aa4503f1c9 Merge pull request 'fix(web): Turnstile yüklenemeyince kullanıcıya uyarı göster' (#139) from dev into main
Reviewed-on: #139
2026-06-12 13:51:18 +00:00
aba8e5eb9b fix(web): Turnstile yüklenemeyince kullanıcıya uyarı göster
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
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>
2026-06-12 16:45:26 +03:00
97c1dbcece Merge pull request 'fix(pcat): robust year extraction across key spelling + date format' (#138) from dev into main
Reviewed-on: #138
2026-06-12 13:11:22 +00:00
d5362a5390 fix(pcat): robust year extraction across key spelling + date format
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
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>
2026-06-12 16:09:14 +03:00
7571438a6b Merge pull request 'dev' (#137) from dev into main 2026-06-12 13:08:00 +00:00
e57a9968b2 fix(pcat): extract real Year param, not the year=All filter axis
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
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>
2026-06-12 16:07:11 +03:00
6c6b74a8e5 feat(payments): recurring billing — Stripe Checkout switches to subscription mode
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
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>
2026-06-12 15:55:44 +03:00
4879f7cdef fix(subscriptions): keep trial alive until payment actually lands
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>
2026-06-12 15:15:47 +03:00
f394897a08 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>
2026-06-12 14:55:44 +03:00
51d8104cdc polish(part-prices): dialog'da çift başlık kaldırıldı, 'son güncelleme' → 'son fiyat değişimi'
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 13:45:58 +03:00
500738fcd4 chore(deploy): SUPPLIER_PRICE_DB env'leri sonrası dev redeploy tetikleyici
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 13:41:05 +03:00
532e1ad9ef 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>
2026-06-12 13:33:37 +03:00
6a54e63c0f Merge pull request 'fix(backfill): in-process scan timer (cron marker buried behind 444k backlog)' (#136) from fix/backfill-inprocess-scan into main 2026-06-12 09:14:25 +00:00