Commit Graph

48 Commits

Author SHA1 Message Date
6610bdf720 feat(prefetch): kalıcı vehicles.fully_fetched kolonu (güvenilir tamlık ölçümü)
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
Redis prefetch:complete:* 21g TTL'li → "% tam çekilmiş"i güvenilir ölçemiyorduk.
Migration 0034: vehicles'a fully_fetched (bool, default false) + fully_fetched_at
(ts) + index. incrementCompleted chain parça ile bitince kalıcı flag'i set eder
(Redis marker'a EK — operasyonel skip mantığı değişmedi). fully_fetched_at her
re-drill tamamlanışında güncellenir (son-doğrulanma). Idempotent SQL.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-25 10:40:40 +03:00
5ed3959a12 feat(canonical): şablon ENTRY-seviyesi — decode anında tam gezinilebilir yapı
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
Kullanıcının orijinal tasarımı: aynı (kaynak, marka)da kategori yapısı birebir
aynı — değişen yalnız parça kodları/şemalar. Önceki şablon sadece kova/alt-yaprak
BAŞLIKLARINI overlay ediyordu → taze araçta "Aydınlatma — 0 kategori". Yeni:
canonical_template_entries (migration 0031) — markanın paylaşılan kaynak-kategori
ADLARI (≥2 araç); backfill'de rebuild. overlayTemplate eksik adları disabled
placeholder olarak basar (unavailable=true, CategoryGrid zaten soluk+tıklanamaz
render ediyor) → decode anında her kova tam adlarıyla dolu; drill tamamlanınca
placeholder'lar gerçek satıra dönüşür.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 07:23:43 +03:00
7956fc9e9b feat: kategori sağlık paketi — dedup+index, prefetch throughput, v1 taxonomy, Birleşik default
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
(1) migration 0030: 57.780 duplike kategori merge (prod'da uygulandı, idempotent)
+ categories unique index NULLS NOT DISTINCT → decode retry duplike üretimi
KALICI olarak durdu (onConflictDoNothing artık NULL sınıflarında da çalışıyor).
(2) prefetch throughput: backlog %89 pcat + net-büyüyordu (82k→94k). pcat
20→90/dk (DataImpulse rotating gateway), pace 5s→1.5s, conc 3→8, global 180.
pl24/emex dokunulmadı (ban riski).
(3) GET /v1/taxonomy: araçtan bağımsız kanonik yapı kontratı (B2C navigasyon).
(4) Birleşik tekrar varsayılan görünüm (edge-case'ler çözüldü).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 06:26:54 +03:00
f25ef2d54f feat(canonical): marka/platform yapı şablonu — decode anında tam kanonik yapı
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
canonical_structure_template(source, brand, bucket, sub, vehicle_count): drill
edilmiş araçlardan her (kaynak, marka) için görülen kova/alt-yaprak kümesi.
Backfill sonunda yeniden üretilir (global run). Serving overlayTemplate ile
aracın ağacına markanın bilinen ama bu araçta henüz drill edilmemiş kova/alt-
yapraklarını BOŞ ekler → VIN decode olur olmaz FULL kanonik yapı görünür;
parçalar backfill ile dolar. migration 0029. vehicle + catalog akışı.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 08:46:59 +03:00
9dd3b2c858 feat(canonical): 2. seviye alt-kategori mapping (otoparcasan-türevi)
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
Kendi kanonik yapımıza tam geçiş — B2C API için alt-kategori. migration 0026:
canonical_categories'e 143 alt-yaprak (parent_id=üst kova) + categories/
category_canonical_map'e canonical_subcategory_id. canonical-sublexicon.ts:
bucket başına satılabilir parça yaprakları + Türkçe varyant (kaliyer/hidroliği)
+ Bağlantı&Donanım catch-all. Backfill artık isim→kova→alt-yaprak deterministik
materialize ediyor (inheritance dahil). Eşleşmeyen = null → SP manuel. Testler +.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 16:32:25 +03:00
7b5e678c55 feat(catalog): kanonik taksonomi şeması + backfill job'ı
Kaynak/marka-bağımsız tek parça taksonomisi. Ham OEM ağaçları (categories.parent_id
+ pl24/emex/pcat aynaları) dokunulmadan kalır; kanonik omurga onlara dik.

- migration 0024: canonical_categories (18 üst kova seed) + category_canonical_map
  (isim-düzeyi eşleme) + categories.canonical_category_id kolonu
- canonical-lexicon.ts: iki-aşamalı eşleme (classifyNode: parça/yapısal/çöp;
  mapToCanonical: 18-kova keyword). VW+Opel pilotunda kanıtlandı — Opel ağacının
  %87'si model-seçici düğüm, bunlar kovaya girmez.
- canonical-backfill BullMQ job'ı (on-demand): distinct foldlanmış adları
  sınıflandırır, category_canonical_map'e upsert eder (is_manual korumalı),
  categories.canonical_category_id'yi join ile materyalize eder. brands/sources/
  dryRun parametreli.
- 8 unit test (classifyNode/mapToCanonical)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 16:54:24 +03:00
7a4fe51d09 feat(api): public API POC — API-key auth + /api/v1 decode & OEM xref
Kapalı-beta public API temeli (rapor: api-widget-analizi-2026-07-03):
- better-auth apiKey plugin (defaultPrefix sase_, enableMetadata, anahtar
  başına 120 istek/dk); enableSessionForAPIKeys KAPALI — anahtar dashboard
  oturumu yerine geçmez, cookie AuthGuard davranışı değişmez
- apikeys tablosu (migration 0023, plugin şemasının birebir karşılığı)
- ApiKeyGuard: Authorization Bearer / x-api-key → verifyApiKey → kullanıcı
  yüklenir (status kontrolü), request.user + request.apiKey doldurulur
- /api/v1/vin/decode: mevcut slim kontrat + aday akışı; günlük başarılı-decode
  kotası (Redis, TR günü, aynı VIN idempotent; limit: key metadata
  dailyDecodeLimit → PUBLIC_API_DAILY_DECODE_LIMIT → 100) ve
  X-Decode-Quota-* header'ları
- /api/v1/oem/:code: P çapraz-referans (kotasız)
- internal/admin/api-keys: x-internal-token ile anahtar üret/listele/kapat
  (düz metin yalnız create cevabında)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 16:15:17 +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
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
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
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
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
2c848df97d change(gamification): sezon ödülleri otomatik tanımlanmaz — kazananlar admin'e maillenir
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
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>
2026-06-12 11:53:02 +03:00
fe35bbd826 feat(gamification): aylık uzman sezonları — ilk 3'e otomatik üyelik uzatması
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>
2026-06-12 10:48:36 +03:00
4072c6e736 feat(catalog): OEM oyları detay sayfasına taşındı + topluluk muadil önerileri
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>
2026-06-12 00:26:20 +03:00
8fddd09087 feat(catalog): OEM uyumluluk oylaması + Parça Uzmanları liderlik tablosu
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>
2026-06-12 00:00:51 +03:00
45f7e3f5a4 feat(proxy): proxy_logs telemetry — per-attempt provider/ban/latency tracking
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
One row per proxied upstream attempt (pcat call/capture/validate, emex http)
written fire-and-forget by the new ProxyTelemetryService (buffered, capped,
errors swallowed — telemetry can never hurt the request path).

- banned = upstream 403/429 (IP-block signal), distinct from auth/data errors
- sticky legs (pcat capture, emex floxy) carry a session_key; pcat capture
  also resolves the actual residential exit IP via a parallel ipify probe
  through the same sticky session → concrete banned-IP tracking
- rotating legs log provider + outcome (ban *rate* instead of per-IP)
- 30-day retention piggybacked on the query-cleanup job

Feeds the Süper Panel /analytics/proxy page (provider grading + banned IPs).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 17:27:48 +03:00
11975d8b5a feat(payments): remove EFT/havale, Stripe-only checkout + Turkish locale
EFT/Havale was retired; Stripe is now the sole payment method. Remove the
EFT code surface (shared PaymentMethod "eft" + EftPaymentInput + eftReceiptUrl,
EFT_RECEIPT_REQUIRED error code, billing UI receipt/filter/label paths,
payments.service eft read paths). DB columns (eft_receipt_url, bank_account_id,
bank_accounts) are kept and marked @deprecated to preserve historical records
and avoid a destructive migration — same pattern as the retired iyzico column.

Faz 3 conversion lever: set locale "tr" on the Stripe Checkout session. The
audience is Turkish B2B and ~60% of sessions reached the foreign-language
hosted page but never started a payment intent (pure abandonment, not decline).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 19:17:10 +03:00
Claude (audit §9.4)
bdbdd07566 feat(notifications): operability tier — send_limit + open-pixel + signed-URL exp + retention + sent-flag (audit §9.4)
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
Lands the §9.4 operability tier of postal/mailAudit.md as one PR on top of
the §9.3 stack (PR #101). Seven items, all independent of each other but
sharing the same notifications surface.

#15 Postal send_limit fuse — set per-hour cap (already applied LIVE in DB:
  servers.send_limit = 500). A runaway loop now hits Postal's own throttle
  long before flooding recipient ISPs into a rate-limit penalty.

#16 noreply@sase.tr decommission — change default fromAddress in both
  email.service.ts and config/configuration.ts to destek@sase.tr. `noreply@`
  had no inbound route so user replies bounced; `destek@` lands in the
  SnappyMail destek inbox. Overridable via POSTAL_FROM_ADDRESS env per workflow
  that genuinely shouldn't accept replies.

#21 Welcome CTA fallback — flip the {{else}} branch in novu-welcome.html
  from https://sase.tr to https://sase.tr/dashboard (the actual onboarding
  entry, not the marketing page). Already pushed live to Novu Mongo too.

#17 Open-pixel embed — new buildTrackPixelUrl() in novu.ts; injected
  trackPixel payload into welcome/trial-ending/win-back/referral×3/
  payment×2 NovuService methods + lifecycle-email.processor. Auth flows
  (email-verification, password-reset) deliberately skip the pixel.
  Templates updated with {{#if trackPixel}}<img ...>{{/if}} just before the
  footer; 8 templates touched, 2 (auth) skipped. Novu Mongo updated.

#18 Signed-URL exp / replay-resistance — track.sase.tr Worker /c endpoint
  now expects `e=<unix-ms>` + `s=HMAC(MID|TARGET|EXP)`. Expired signatures
  return 410. Legacy signatures (no `e=`) still accepted while in-flight
  mail with old links drains; remove that branch ~30 days post-deploy.
  buildTrackedUrl() now mints exp=now+30d.

#19 D1 retention cron — Cloudflare Cron Trigger added to mailtrack worker
  (`17 4 * * *` UTC, after Europe/Istanbul cron settles). `scheduled` handler
  DELETEs events older than RETENTION_DAYS (default 90). Both code and the
  cron schedule are LIVE on the production worker.

#20 Lifecycle sent-flag idempotency — new `lifecycle_email_sent` table
  (migration 0012) keyed (user_id, workflow). Replaces the 1-day endDate
  window's at-most-once trick that lost cohorts on skipped days. Cron now
  LEFT JOINs and writes the row immediately after each successful trigger.
  Historical seed in scripts/backfill-lifecycle-sent.sql (19 trial-ending +
  5 win-back users — generated from postal-server-1.messages) so the first
  post-deploy cron doesn't re-send to users we already mailed.

## Live infrastructure (deploy-independent)

- Postal MariaDB: `UPDATE servers SET send_limit = 500`.
- Cloudflare Worker mailtrack redeployed with new /c logic + scheduled handler.
- Cloudflare Worker: cron `17 4 * * *` registered on production env.
- Cloudflare Worker: RETENTION_DAYS=90 plain_text binding.
- Novu Mongo: 16 messagetemplates updated with pixel + Welcome /dashboard.

## Companion deploy steps post-merge

1. `pnpm db:generate` to refresh drizzle snapshots for 0011 + 0012.
2. Run `scripts/backfill-lifecycle-sent.sql` against prod + dev BEFORE the
   first cron tick post-deploy.
3. Apply host-side novu-patches/apply-headers-patch.sh again if Novu
   container rolled (idempotent).

## Verification

  curl /c?…e=<future>… ⇒ 302    new-style signature accepted
  curl /c?…(no e)…    ⇒ 302    legacy signature still accepted (drain)
  curl /c?…e=<past>…  ⇒ 410    expired signature rejected
  curl /c?…s=bad…     ⇒ 403    bad signature rejected
  CF API schedules    ⇒ `17 4 * * *` live on mailtrack worker.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-04 17:04:17 +03:00
Claude (audit §9.3)
327d698945 feat(notifications): TR-only templates + name canonicalisation + MTA-STS + 2048-bit DKIM + unsubscribe (audit §9.3)
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
Lands the §9.3 "compliance + brand" tier of mailAudit.md as one PR. Six
changes share enough surface (notifications, shared utils, infrastructure)
that splitting them would require multiple stacked PRs.

#9 — Turkish-locale title-case for names at signup
   • New `normalizeName()` in @sase/shared, locale-aware (İ/ı pairs handled
     via toLocaleLowerCase('tr-TR') + matching toLocaleUpperCase). Hyphen-
     aware, collapses whitespace, idempotent.
   • Wired into better-auth's `user.create.before` hook so every new signup
     gets canonicalised before the row lands in Postgres.
   • 28 unit tests in packages/shared/src/index.spec.ts.
   • Backfill script at `scripts/backfill-user-names.ts` (already run
     against prod + dev — 210/402 prod users and 72/153 dev users
     canonicalised, plus 71 Novu subscribers).

#10 — Email typo correction at signup
   • New `suggestEmailFix()` in @sase/shared: exact-match typo dictionary
     for the addresses we've actually suppressed (icould.com, gmial.com,
     xn--gmail-bgd.com, …) plus Levenshtein ≤ 2 fallback against popular
     providers.
   • Inline UI hint on the register form — "Bunu mu demek istedin? <link>"
     that swaps the email on click; PostHog event tracks acceptance.

#11 — Strip EN branches (decision: TR-only)
   • 0/205 prod subscribers have locale='en' and there's no marketing in
     English — the {{#equals subscriber.locale "en"}}…{{else}}…{{/equals}}
     framework was dead code in all 10 templates.
   • Templates updated in-place (avg ~30 % smaller). Renamed
     `novu-welcome-tr.html` → `novu-welcome.html` for consistency with the
     other 9 files.
   • Novu workflow definitions in both Dev + Prod envs updated via Mongo:
     subjects collapsed to TR-only, content replaced with new HTML
     (mongodump/restore-safe).
   • App code: `NovuRecipient.locale` and `NovuUser.locale` removed; the
     `...(user.locale === "en" ? { locale: "en" } : {})` spread in NovuService
     is gone.

#12 — DKIM rotated to 2048-bit RSA
   • Postal default was 1024-bit (selector `postal-YeIm3w`). Generated new
     2048-bit key, added DNS TXT `postal-2k260604._domainkey.sase.tr`,
     atomically swapped `domains.dkim_identifier_string` +
     `dkim_private_key` in Postal MariaDB, restarted Postal SMTP.
   • Verified: outgoing welcome mail now signs with `s=postal-2k260604`
     and a 256-byte signature body (vs the previous 128-byte 1024-bit
     signature). Pubkey on DNS matches the private key.
   • OLD TXT record (`postal-YeIm3w._domainkey`) stays in DNS for ~7 days
     as a grace window for in-flight mail.

#13 — MTA-STS + TLS-RPT
   • Extended the existing mailtrack Cloudflare Worker to also serve
     `mta-sts.sase.tr/.well-known/mta-sts.txt` (`mode: enforce, mx:
     mx.postal.sase.tr, max_age: 604800`). Workers Domain bound to the
     mailtrack service via Cloudflare API.
   • DNS:
       `_mta-sts.sase.tr`        TXT  "v=STSv1; id=20260604111347"
       `_smtp._tls.sase.tr`      TXT  "v=TLSRPTv1; rua=mailto:dmarc@sase.tr"
   • Verified policy fetch returns 200 with the expected body; cert valid
     (sase.tr SAN issued by GTS).

#14 — Unsubscribe preferences + RFC 8058 one-click endpoint
   • New `email_preferences` table (migration 0011) keyed
     (user_id, workflow), captures source for audit
     (one_click / manual_link / settings_page).
   • New `UnsubscribeController` at `/api/email/unsubscribe`:
       - POST: Gmail/Yahoo one-click bot path (200 fast)
       - GET:  human-visit, renders a Turkish confirmation page
     Both validate an HMAC-SHA256(`userId|workflow`) token under
     `UNSUBSCRIBE_SECRET` — stateless, no DB lookup to validate, secret
     rotation invalidates all outstanding tokens.
   • `triggerNovu()` now mints the per-call `overrides.email.headers`:
       `List-Unsubscribe: <https://…?u=&w=&t=>, <mailto:unsubscribe@…>`
       `List-Unsubscribe-Post: List-Unsubscribe=One-Click`
     Auth + payment workflows opt out via NO_UNSUBSCRIBE_WORKFLOWS so the
     unsubscribe URL never appears on transactional mail.
   • `NovuService.trigger()` pre-flight-checks `isOptedOut()` and skips the
     trigger entirely if the user opted out. Fail-open on DB error so a
     transient blip can't swallow auth mail.
   • `lifecycle-email.processor.ts` (standalone BullMQ worker — no NestJS
     DI) does the same check inline via a LEFT JOIN on
     `email_preferences WHERE opted_out IS NULL`.
   • Coolify env wired in both Prod and Dev apps:
       `UNSUBSCRIBE_SECRET` (32-byte hex, distinct per env)
       `UNSUBSCRIBE_URL_BASE` = `https://(dev.)sase.tr/api/email/unsubscribe`

## Companion sibling changes (already applied, NOT in this PR)

- Cloudflare worker `mailtrack` redeployed with mta-sts.sase.tr custom domain.
- Postal MariaDB `domains.dkim_identifier_string` + `dkim_private_key`
  updated to the new 2k260604 selector (live since 2026-06-04 11:18).
- `postal-2k260604._domainkey.sase.tr` TXT record live at Cloudflare.
- `_mta-sts.sase.tr` + `_smtp._tls.sase.tr` TXT records live at Cloudflare.
- Novu Mongo notification + message templates updated to TR-only.
- 282 user names canonicalised across prod + dev + Novu subscribers.

## Verification snapshot

- Postal raw_headers (ID 157, post-rotation): `s=postal-2k260604` + 256-byte b=
- `dig +short TXT _mta-sts.sase.tr @1.1.1.1` ⇒ live id=20260604111347
- `curl https://mta-sts.sase.tr/.well-known/mta-sts.txt` ⇒ 200 with policy
- 28 unit tests (normalizeName + suggestEmailFix) all green via Node sanity.

## Deploy notes

- Re-run `pnpm db:generate` to regenerate the drizzle snapshot for 0011
  (added the journal entry manually because no drizzle-kit on this box).
- Run `pnpm tsx scripts/backfill-user-names.ts --apply` against any DB not
  yet canonicalised (already done for prod + dev today).
- The host-side Novu nodemailer-headers patch at
  `postal/novu-patches/apply-headers-patch.sh` must be re-run after every
  Novu container redeploy or the List-Unsubscribe header is silently dropped
  before reaching Postal (see audit §9.1 #3 for the upstream cause).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-04 14:28:39 +03:00
10dcf2d6ae fix(parts): dedupe ingest path + clean ~108k duplicate rows
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
Backfill + reactive prefetch can re-drill the same category multiple
times, and the parts insert path had no dedupe guard. Result: 8.2%
duplicate rows on pl24, 14.5% on parts-catalogs, and 33.7% on emex —
~108k extra rows across 7,571 categories on 224 vehicles. Every drilled
catalog page rendered each part twice (the Tampon example: 32 rows for
19 distinct OEMs).

* Migration 0010 — phase 1 deletes existing dupes preserving the
  oldest row per (vehicle_id, category_id, oem_code, name, position)
  group; phase 2 adds a UNIQUE INDEX over the same tuple with NULLS NOT
  DISTINCT (PG 15+) so null position/vehicle_id collapse like equal
  values rather than each counting as its own "distinct" row. Idempotent
  CREATE UNIQUE INDEX IF NOT EXISTS so the runner is safe to re-apply.
* All five insert(parts).values(...).returning() call sites
  (parts.service, categories.service ×3, catalog.service) get
  .onConflictDoNothing() so future re-drills no-op instead of erroring
  on the new constraint. `.returning()` continues to surface only the
  newly-inserted rows; existing logs read `Stored N parts` as actual
  net insertions, which is what we want.

Dry-run on dev DB: 524,540 → 425,186 parts (99,354 dupes deleted), index
created cleanly. Same delta expected on prod (~108k drop).

drizzle-orm 0.41 doesn't expose .nullsNotDistinct() on the index builder
so the constraint is owned by raw SQL — see the inline comment in the
parts schema and the migration file. Future schema generators should NOT
try to drop or rewrite this index.
2026-06-02 02:08:57 +03:00
3926e276fd feat(referrals): rework reward engine + email-verified landing
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
Reward engine:
- Recurring milestones (every 3 → +7d, every 5 → +14d) instead of one-time
  tiers capped at 5; idempotent + transactional grants serialised per
  referrer so concurrent qualifications can't double-count.
- Rewards now gated on the referred user's email verification
  (afterEmailVerification hook); already-verified referees (OAuth) qualify
  at apply time.
- Reward days banked as users.referral_credit_days when the referrer has no
  live subscription, consumed on next trial start / activation (no more
  silently lost rewards).
- Accurate cumulative rewardDays in stats; getMyReferrals returns referee
  name/masked email/status.

Hardening / cleanup:
- onConflictDoNothing makes apply idempotent (no unhandled unique violation).
- Anti-fraud: normalizeEmail blocks self-referral via gmail dot/+tag aliases.
- Collision-safe referral code generation at signup.
- Single apply path (welcome onboarding modal); removed duplicate calls in
  register + subscription pages. Input validation on the apply code.

Email verification UX:
- Verification link now lands on a dedicated /email-verified confirmation
  page instead of the deep-linked VIN/search page.

Schema: referrals.status + qualified_at, users.referral_credit_days (0009).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 13:28:32 +03:00
e5c852b537 feat(blog): data-driven blog API + DB for content-pipeline publishing
Makes the blog publishable from the Süper Panel content pipeline (via n8n).
Backend mirrors the changelog module; frontend keeps the existing hand-authored
posts and merges in API-backed ones (no content migration, no regression).

API (apps/api):
- blog_posts Drizzle table (slug unique, title, meta_description, body_markdown,
  tags, cover_image, status, source, published_at)
- blog module: GET /blog/posts (public list), GET /blog/posts/:slug (public),
  POST /blog/posts/internal (Bearer BLOG_AUTOMATION_TOKEN, mirrors
  changelog/internal) — returns { ...post, url }
- BlogService: Drizzle + Redis cache, defensive Turkish-aware slugify +
  uniqueness; registered in app.module

Web (apps/web):
- use-blog hooks (list + by-slug, react-query, 30m staleTime)
- blog list: static + API merged, dedup by slug, newest first
- blog detail: static post renders as before; API post renders body_markdown
  via react-markdown (remark-gfm) styled to match existing prose
- add react-markdown + remark-gfm

Deploy: set BLOG_AUTOMATION_TOKEN (and optional PUBLIC_WEB_URL) on the api,
run drizzle-kit db:push to create blog_posts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 01:48:10 +03:00
aa4d055c77 fix(catalog): pcat parent groups no longer show empty "0 parça"
parts-catalogs (pcat) catalogs are deep trees, but every pcat node was
stored as an apparent leaf (pcat: linkPath, no children) until drilled.
Opening an intermediate group called parts2, which returns HTTP 400
("The list of parts is empty"); the drill-to-children fallback only fired
on that exact error string, so any other failure (cold JWT capture,
timeout, 5xx) left both parts and children empty → the UI rendered
"0 parça / bulunamadı" on a node whose parts live 1-3 levels deeper.

#1 Persist the groups2 hasSubgroups/hasParts flags on categories
   (new nullable columns, migration 0007) and use them to classify:
   - getCategoryWithParts routes a known parent (hasSubgroups, !hasParts)
     straight to getChildren, never calling parts2.
   - enrichWithSchemaImages treats flagged parents as expandable, flagged
     parts-leaves as leaves, and only falls back to the "any pcat: is a
     leaf" heuristic for pre-migration rows (flags null).

#2 Make discovery resilient: drill to sub-groups whenever the parts fetch
   produces nothing (empty 200 OR a thrown error), not only on HTTP 400.
   Mark a node unavailable only on a definitive HTTP 400 with no
   sub-groups; transient failures are left for a later retry.

Verified live on prod (VW Passat, "Süspansiyon Çarpanlar"): full path is
Süspansiyon Çarpanlar → Süspansiyon → Süspansiyon 1…5 → 74 parts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 01:02:00 +03:00
96a9d11015 feat(internal-admin): user lifecycle — suspend / reactivate / ban
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
Süper Panel Phase 7 — Phase B. Founder can suspend, reactivate, or ban a
Sase user from the panel. Status enforced in the AuthGuard so blocked
users can no longer make authenticated requests.

Schema (migration 0006)
- users.status varchar(20) default 'active' — active|suspended|banned
- users.status_reason text — free-text reason set on transition
- users.status_changed_at, status_changed_by uuid — audit metadata
- users_status_idx

Auth
- AuthGuard rejects 'suspended' / 'banned' with TR-localized message.
- auth.ts: declared `status` as a Better Auth additionalField so the
  session.user object exposes it (matches how `role` is wired).

Endpoints (InternalTokenGuard)
- POST /internal/admin/users/:id/suspend     { reason, founderId }
- POST /internal/admin/users/:id/reactivate  { founderId }
- POST /internal/admin/users/:id/ban         { reason, founderId }

Service
- LifecycleService.setStatus():
  - refuses to touch admin-role users
  - refuses no-op transitions (already in target state)
  - refuses suspended→banned→suspended downgrade path (must reactivate first)
  - on suspend/ban: deletes all sessions for the user (immediate sign-out)
  - returns { from, to, sessionsKilled, changedAt }

Wiring
- LifecycleService + LifecycleController added to InternalAdminModule.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 09:34:52 +03:00
2583b781ec feat(internal-admin): readonly impersonation for Süper Panel
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
Lets the founder open a target user's session in a new tab from the
panel for debugging. Read-only enforced server-side — any non-GET
request from an impersonated session returns 403.

Schema
- sessions.impersonated_by (uuid, nullable) — founder Better Auth user id
- sessions.impersonation_readonly (bool, default false)
- index on impersonated_by

Service
- ImpersonationService.createReadonlySession({ targetUserId, founderId,
  ttlMinutes, reason, ipAddress, userAgent }):
  - Random sessionId + token (32 bytes hex each)
  - TTL clamped 1..60 min, default 15
  - Refuses to impersonate admin users
  - Inserts sessions row; signs cookie value with HMAC-SHA256(BETTER_AUTH_SECRET)
    matching better-call's signCookieValue format
  - Returns { cookieName, cookieValue, expiresAt, sessionId }

Guard
- ImpersonationReadonlyGuard runs after AuthGuard, before RolesGuard.
- GET/HEAD/OPTIONS pass through.
- For other methods: looks up sessions.impersonated_by + impersonation_readonly
  by request.session.id; throws ForbiddenException if both truthy.

Endpoints (InternalAdminModule)
- POST /internal/admin/users/:id/impersonate-readonly [InternalTokenGuard]
  body: { ttlMinutes, reason, founderId }
  returns: { redirectUrl, expiresAt, sessionIdPrefix }
  Hand-off is via signed consume URL (cross-origin Set-Cookie limitations).
- GET /admin/impersonate/consume?t=<signed> [@Public]
  Verifies HMAC-signed payload (<=60s validity), sets the Better Auth session
  cookie on sase.tr, redirects to /. One-shot.

Wiring
- InternalAdminModule imported in AppModule.
- ImpersonationReadonlyGuard registered as APP_GUARD between Auth and Roles.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 00:48:39 +03:00
Fusion
561b02b3d2 feat(FN-343): remove lingering iyzico references after Stripe migration (+1 more)
Commits merged:
- chore(FN-343): remove lingering iyzico references from docs, config, and scripts
- feat(FN-343): remove lingering iyzico references after Stripe migration

Files changed:
CLAUDE.md                                    | 10 +++++-----
 README.md                                    |  2 +-
 apps/api/src/database/schema/core.ts         |  1 +
 apps/web/src/messages/en.json                |  1 -
 apps/web/src/messages/tr.json                |  1 -
 apps/web/src/routes/dashboard/billing.tsx    |  6 +++---
 docker-compose.coolify.yml                   |  5 ++---
 docs/INDEX.md                                | 27 ++++++++++++-------------
 knowledge.md                                 | 30 ++++++++++++++--------------
 packages/shared/src/constants/error-codes.ts |  1 -
 packages/shared/src/index.ts                 |  1 -
 packages/shared/src/types/payment.ts         | 12 +----------
 scripts/fn342-pw-verify.mjs                  |  2 +-
 scripts/validate-env.sh                      |  3 +--
 14 files changed, 43 insertions(+), 59 deletions(-)

Fusion-Task-Id: FN-343
2026-05-14 02:31:42 +00:00
Sase Dev
6a0b36a7a0 feat: replace Iyzico with Stripe + single-page subscription stepper
Subscription/checkout flow rewritten end-to-end. The plan-card "Devam Et"
button silently wiped brand selection on re-click and the actual proceed
button lived offscreen — fixed by collapsing plan/brands/payment/confirm
into a single vertical stepper with one sticky CTA.

Backend
- Stripe Hosted Checkout (`/payments/stripe/checkout`) + webhook
  (`/payments/stripe/webhook`, raw body) replacing the stubbed Iyzico
  module. Webhook activates subscription on `checkout.session.completed`,
  expires the pending subscription on cancel/expire so users can retry.
- New `bank_accounts` table — multiple rows, single `is_active` enforced
  by a partial unique index. Admin CRUD under `/admin/bank-accounts`
  with multipart QR upload to MinIO; atomic `/activate` swap in a
  transaction; `GET /payments/bank-info` returns the active row.
- `payments` gains `stripe_session_id`, `stripe_payment_intent_id`,
  `bank_account_id`. EFT flow now reads the active bank account at
  payment time and stores the FK for reconciliation.
- Env: `IYZICO_*` removed, `STRIPE_*` added (validated by zod schema).
- `main.ts` `rawBody: true` for Stripe signature verification.
- Drizzle 0003 snapshot id collision fixed (VIEW-only migration shared
  prevId with 0002, blocking new generates).

Frontend
- `/dashboard/subscription` rewritten as a 4-step vertical stepper with
  step-aware sticky bottom CTA; plan re-selection is idempotent and
  preserves brand state. `/dashboard/subscription/pay` deleted; Stripe
  returns to the same page via `?stripe=success|cancelled` and the UI
  polls `/subscriptions/me` until the webhook activates the row.
- New components: `bank-transfer-card.tsx` (DB-driven IBAN + Kolay Adres
  + uploaded QR image + receipt upload) and `stripe-checkout-button.tsx`.
- Active subscription view, trial onboarding/urgency banner, downgrade
  and cancel dialogs preserved.
- TR/EN i18n: new `subscription.steps.*`, `subscription.stickyCta.*`,
  `payment.stripe.*`, `payment.bank.*`; provider label updated.

PostHog: `method: "iyzico"` → `"stripe"`; new events `iban_copied`,
`kolay_adres_copied`, `qr_viewed`, `eft_initiated`,
`stripe_redirect_returned`.

Deploy runs `db:migrate` which applies 0004_hot_quicksilver
(additive: new table + nullable columns; safe to apply on prod).
Operator must add `STRIPE_SECRET_KEY`, `STRIPE_PUBLISHABLE_KEY`,
`STRIPE_WEBHOOK_SECRET` to env and create the first
`bank_accounts` row via the admin endpoint before the bank tab works.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 22:42:28 +00:00
Sase Dev
c4f6cc1caf perf(vehicles): dedup, abort budget, circuit breaker for VIN decode
Production p99 was 18min for failed decodes and 15min for PL24 successes;
same VIN could trigger N parallel 10+ min decodes. Three layered fixes:

- In-flight dedup via Redis SETNX (vin🔒*); concurrent same-VIN requests
  poll the resolve cache instead of re-firing the upstream chain.
- 24h positive cache (vin:resolve:*) and 6h negative cache
  (vin:resolve:neg:*); previously 5min positive / no negative.
- 25s hard abort budget via AbortController; PCAT gets the signal natively
  (AbortSignal.any), PL24/EMEX wrapped with raceWithSignal at the boundary.
  Aborted decodes don't poison the negative cache.
- PCAT/EMEX real race: first definitive single-result wins; the slower
  source is skipped (previously PCAT was always awaited first).
- PL24 circuit breaker: 3 consecutive failures opens a 30s cooldown
  (pl24:cb:cooldown_until); successes reset the counter.
- Stage-level timings in query_logs.timings (jsonb): pcat/emex/pl24/
  lock_wait/cache_hit/aborted. Failed source now logged as "none" or
  "aborted" instead of misleading "corgi".

Verified locally with 3 parallel decodes of a fresh VIN: 1 real decode
(3.59s), 2 lock-waits (3.53s) sharing the result, 4th request 23ms cache
hit. Previously this would have been 3 separate 10+ min PL24 decodes.

Migration 0002 adds query_logs.timings jsonb (NULL default). Must be
applied manually before deploy (deploy.sh does not run db:push).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 11:31:25 +00:00
Fusion
ff90497a08 feat(FN-193): rewrite migrate.ts with hash-based runner (+4 more)
Some checks failed
Sync dev → Gitea / Mirror dev to Gitea (push) Has been cancelled
Commits merged:
- docs(FN-193): update migration runner docs
- fix(FN-193): lint/typecheck fixes
- test(FN-193): rewrite migrate tests for runMigrations
- feat(FN-193): make baseline migration idempotent (IF NOT EXISTS)
- feat(FN-193): rewrite migrate.ts with hash-based runner

Files changed:
apps/api/drizzle/0000_brief_guardian.sql        | 364 ++++++++++++------------
 apps/api/src/database/__tests__/migrate.spec.ts | 354 ++++++++++++-----------
 apps/api/src/database/migrate.ts                | 173 +++++------
 docs/INDEX.md                                   |  17 +-
 4 files changed, 466 insertions(+), 442 deletions(-)

Fusion-Task-Id: FN-193
2026-05-12 07:22:51 +00:00
Fusion
0f80f5292b feat(FN-192): DB schema + Migration (+7 more)
Some checks failed
Sync dev → Gitea / Mirror dev to Gitea (push) Has been cancelled
Commits merged:
- feat(FN-192): complete Step 8 — Documentation & Delivery
- feat(FN-192): complete Step 7 — Testing & Verification
- feat(FN-192): complete Step 6 — i18n keys replace stage with changeType
- feat(FN-192): complete Step 5 — ChangelogTab rename stage to changeType
- feat(FN-192): complete Step 4 — Badge component rename stage to changeType
- feat(FN-192): complete Step 3 — API Service + Controller Spec
- feat(FN-192): complete Step 2 — Shared Zod schemas + type exports + DTO
- feat(FN-192): complete Step 1 — DB schema + Migration

Files changed:
apps/api/drizzle/0001_charming_quicksand.sql       |    2 +
 apps/api/drizzle/meta/0001_snapshot.json           | 5167 ++++++++++++++++++++
 apps/api/drizzle/meta/_journal.json                |    9 +-
 .../api/src/changelog/changelog.controller.spec.ts |    2 +-
 apps/api/src/changelog/changelog.dto.ts            |    2 +-
 apps/api/src/changelog/changelog.service.spec.ts   |    4 +-
 apps/api/src/changelog/changelog.service.ts        |    4 +-
 apps/api/src/database/schema/core.ts               |    2 +-
 apps/web/src/components/settings/changelog-tab.tsx |   11 +-
 apps/web/src/messages/en.json                      |    8 +-
 apps/web/src/messages/tr.json                      |    8 +-
 docs/INDEX.md                                      |    4 +-
 packages/shared/src/index.ts                       |    4 +-
 packages/shared/src/schemas/changelog.ts           |    8 +-
 packages/shared/src/types/changelog.ts             |    2 +-
 packages/ui/src/badge.tsx                          |   12 +-
 16 files changed, 5215 insertions(+), 34 deletions(-)

Fusion-Task-Id: FN-192
2026-05-12 06:13:34 +00:00
Fusion
c9c168b89c feat(FN-191): replace manual migration comment with working db:migrate step (+4 more)
Some checks failed
Sync dev → Gitea / Mirror dev to Gitea (push) Has been cancelled
Commits merged:
- fix(FN-191): fix typecheck and lint issues for bootstrap changes
- test(FN-191): complete Step 4 — update tests for new multi-table bootstrap logic
- fix(FN-191): complete Step 3 — strengthen bootstrap canary to detect partial-baseline state
- fix(FN-191): complete Step 2 — add dotenv bootstrap loader to migrate.ts
- fix(FN-191): complete Step 1 — replace manual migration comment with working db:migrate step

Files changed:
apps/api/src/database/__tests__/migrate.spec.ts | 146 ++++++++++++++++++------
 apps/api/src/database/migrate.ts                | 124 ++++++++++++++------
 scripts/deploy.sh                               |  12 +-
 3 files changed, 203 insertions(+), 79 deletions(-)

Fusion-Task-Id: FN-191
2026-05-12 05:25:37 +00:00
Fusion
e87aba8829 feat(FN-190): move drizzle-kit to production dependencies (+8 more)
Some checks failed
Sync dev → Gitea / Mirror dev to Gitea (push) Has been cancelled
Commits merged:
- feat(FN-190): complete Step 9 — add Migration Workflow section to docs/INDEX.md
- fix(FN-190): apply biome import sorting and formatting
- feat(FN-190): complete Step 7 — write unit tests for migrate runner (5 tests)
- feat(FN-190): complete Step 6 — replace db:push with db:migrate in deploy workflow
- feat(FN-190): complete Step 5 — update Dockerfile with drizzle/ copy and start.sh CMD
- feat(FN-190): complete Step 4 — add db:migrate and db:migrate:dist scripts
- feat(FN-190): complete Step 3 — write migrate runner with bootstrap logic
- feat(FN-190): complete Step 2 — generate baseline Drizzle migration (46 tables)
- feat(FN-190): complete Step 1 — move drizzle-kit to production dependencies

Files changed:
.github/workflows/deploy.yml                    |    2 +-
 Dockerfile                                      |    6 +-
 apps/api/drizzle/0000_brief_guardian.sql        |  658 +++
 apps/api/drizzle/meta/0000_snapshot.json        | 5167 +++++++++++++++++++++++
 apps/api/drizzle/meta/_journal.json             |   13 +
 apps/api/package.json                           |    4 +-
 apps/api/src/database/__tests__/migrate.spec.ts |  179 +
 apps/api/src/database/migrate.ts                |  160 +
 apps/api/start.sh                               |    8 +
 docs/INDEX.md                                   |   24 +-
 pnpm-lock.yaml                                  |    6 +-
 11 files changed, 6220 insertions(+), 7 deletions(-)

Fusion-Task-Id: FN-190
2026-05-11 22:03:33 +00:00
Fusion
85ebd35048 feat(FN-188): add Changelog tab to dashboard settings
Some checks failed
Sync dev → Gitea / Mirror dev to Gitea (push) Has been cancelled
Squash-recovery of fusion/fn-188 onto dev — original branch was based on
main (dccd4fa) due to baseBranch=None drift in Fusion settings; rebase
onto dev surfaced unrelated main-only commits (EMEX/PL24/Corgi) as false
conflicts. This commit applies only FN-188's 23-file changelog patch.

- apps/api: ChangelogModule (controller, service, DTO, spec, schema)
- apps/web: ChangelogTab (timeline + accordion), useChangelog hook, i18n
- packages/shared: changelog Zod schemas + types
- packages/ui: Accordion component + Badge stage variant
- docs/INDEX.md: changelog feature documented

Lint, typecheck, all 169 api tests + 2 web tests pass.
2026-05-11 20:05:16 +00:00
Fusion
f4fea1e429 feat(FN-094): add comment line for deployment verification
Some checks failed
Sync dev → Gitea / Mirror dev to Gitea (push) Has been cancelled
- Added a comment line to main.ts for deployment verification purposes
2026-05-11 02:07:03 +00:00
3710251117 feat: EMEX VIN decode → DB catalog entegrasyonu
Some checks failed
CI / Lint, Typecheck, Test & Build (push) Has been cancelled
- CATALOG_MAP'e ~40 yeni WMI eklendi (Mitsubishi, Nissan, Volvo, vb.)
- brandFromCatalogCode listesi 55 DB kataloğuna genişletildi
- brand="UNKNOWN" olsa bile catalogCode varsa EMEX sonucu kabul edilir
- emex-catalog: linkPath'li kategorilerde parçalar DB'den yüklenir (scraper bug fix)
- VIN cache: emex_vehicle_vins tablosu ile tekrar VIN'lerde emexdwc.ae atlanır
- getCategoryTree Step 0: VIN cache lookup (~10ms)
- Match sonrası otomatik VIN cache doldurma
- Proaktif cache: ilk decode sonrası matchByName + saveVinCache

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 08:32:34 +00:00
1e8a049219 feat: EMEX catalog integration — schema, data migration, backend & frontend
Some checks failed
CI / Lint, Typecheck, Test & Build (push) Has been cancelled
Redesign emex_* tables from vehicle-centric to catalog-centric structure with
junction tables (emex_vehicle_group_links, emex_vehicle_part_links). Migrate
92M+ rows from emex DB via postgres_fdw. Add EmexCatalogService for browse
and VIN pre-scraped matching, with Redis caching. Frontend catalog page now
has PL24/Emex tabs with full drill-down routes (brand → vehicle → group → parts).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 04:06:29 +00:00
Sase Dev
72c0de65de docs: update INDEX.md + add catalog module, Ford/PSA legacy catalog, shared vehicles
- CatalogModule: VIN-less PL24 catalog browser (brands, models, categories, parts)
  - Supports P5 Modern (REST) and P4 Legacy (Ford, PSA) catalog architectures
  - Ford variant selector (model-year/engine/gearbox), PSA variant selector (body/engine/gearbox)
  - New API endpoints: ford-config, psa-bodies, psa-engines, psa-gearboxes, brands/:name/catalogs
- Shared vehicles: vehicles table decoupled from users via userVehicles junction table
- PL24 Ford Legacy service: comprehensive HTML-scraping for Ford/PSA/Hyundai/Kia/Nissan/Opel/Volvo
- PL24 types and service updated for P4 Legacy brand support
- Categories/parts service updated for dual FK (vehicleId + catalogVehicleId) pattern
- Catalog browser frontend routes and components
- docs/INDEX.md: updated with all new endpoints, components, hooks, routes (2026-03-02)
- docs/pl24-catalog/: per-brand catalog exploration docs
- scripts/migration-shared-vehicles.sql, pl24-catalog-explorer.js, posthog-dashboards.sh

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 00:31:15 +00:00
Sase Dev
f1a27810db feat: add parts catalogs integration, catalog prefetch worker, and vehicle select modal
Integrate external parts catalogs API with auth service, add BullMQ-based
catalog prefetch worker for background data caching, expand vehicles service
with shared vehicle support, and add vehicle select modal to frontend.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 18:14:36 +00:00
Sase Dev
f3f3f57756 feat: propagate PL24 part metadata, improve auth UX with Google sign-in prominence
- Add unavailable/remark/modelCodes/presel fields to categories and parts DB schema
- Pass PL24 unavailable flag through pipeline instead of filtering out records
- Show unavailable categories/parts at reduced opacity in grid, tree, and parts panel
- Display part remark and model codes as secondary info in parts table
- Move Google sign-in button above email form on login/register pages with branded icon
- Add public email existence check endpoint for better login error messages
- Update INDEX.md documentation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 11:11:28 +00:00
Sase Dev
f2126754a6 feat: add OEM code copy tracking, PostHog analytics, Postal email integration
- Add oem_code_copies table and analytics module for tracking part code copies
- Integrate PostHog for frontend product analytics (VIN decode, OEM copy events)
- Switch email service from stub to Postal API with proper error handling
- Add copy button to parts panel with clipboard + server-side logging
- Add admin copy-logs page with filtering and top-copied-codes view
- Add VIN report endpoint for users to flag unrecognized chassis numbers
- Add Postal email env vars to config schema

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 18:09:07 +00:00
Sase Dev
dcbeb83ccc feat: add OpenTelemetry observability, Faro frontend monitoring, remove legacy Next.js app
- Add OpenTelemetry SDK with tracing, metrics, and OTLP export for API and worker
- Integrate Grafana Faro for frontend real-user monitoring
- Instrument health checks, database, Bull queues, and HTTP exception filter
- Add Grafana dashboard JSON for service overview
- Remove deprecated apps/web-nj (Next.js) — fully replaced by Vite+React frontend
- Update nginx config with OTEL collector proxy
- Minor UI fixes in schema viewer, category components, and subscription flow

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 17:20:49 +00:00
Sase Dev
87d8eea298 feat: onboarding trial flow, Google sign-in, dark mode hotspot fixes
- Move trial creation from auth hook to dedicated /subscriptions/trial endpoint
  with eligibility checks (3-day Full Paket trial)
- Add animated onboarding progress (Remotion) with confetti on completion
- Register redirects to subscription page with welcome flow
- Add Google social login/signup support with config injection
- Improve hotspot overlay visibility in dark mode
- Show "Panele Git" on landing page when authenticated
- Turkish translations for API error messages
- Add toast utility wrapper, UUID generation for auth IDs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 14:53:57 +00:00
Sase Dev
4a06ba5fdb feat: Ford legacy support, EMEX browser pooling, collapsible sidebar
- Add PL24 Ford legacy service for fordt_parts architecture
- Refactor EMEX to use persistent browser pool instead of per-call instances
- Make vehicle decode resilient: fallback to PL24 when Corgi doesn't recognize VIN
- Add collapsible sidebar with persistent user preference
- Improve brand access guard and categories service
- Add debug/test scripts for VIN e2e testing

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 22:25:06 +00:00
Sase Dev
a8eb8f4bdc refactor(emex): rewrite EMEX integration, add category fallback
Replace browser-based Puppeteer scraper with standalone scraper wrapper.
Add EMEX as fallback source for categories when PL24 is unavailable.
Update vehicle decoding to use new synchronous EMEX API.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 19:48:03 +00:00
Sase Dev
996d614d50 feat: admin user creation, Vite migration, dialog fix, pl24 integration
- Add POST /admin/users endpoint with password hashing and role support
- Add user creation dialog to admin users page
- Migrate web from Next.js to Vite + TanStack Router
- Fix Dialog component positioning for Tailwind CSS v4
- Add @source directive for @sase/ui package scanning
- Add pl24 integration parsers and vehicle decode flow
- Backup old Next.js app to apps/web-nj

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 14:24:58 +00:00
Sase Dev
ffa9781eb9 fix: auth login flow, Next.js 16 upgrade, ESM/CJS compatibility
- Upgrade Next.js 15 → 16.1 and rename middleware.ts → proxy.ts
- Fix ESM/CJS compatibility: remove "type": "module" from config/shared packages, switch to commonjs
- Fix Better Auth integration: text IDs for sessions/accounts/verifications, wildcard route @All("**"), proper password hash for admin seed
- Fix auth-client to use window.location.origin instead of hardcoded localhost:4000
- Fix api-client, forgot-password, reset-password to use relative /api/ URLs (via Next.js proxy)
- Fix TimeoutInterceptor DI by using useValue instead of useClass
- Add Playwright E2E login tests for localhost and v2.sase.tr
- Add missing UI components: vin-input, vehicle-card, category-tree

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 02:54:15 +00:00
Sase Dev
56a3c8bfaa feat: sase.tr v2 full application implementation
Complete rewrite of sase.tr VIN lookup platform with modern stack:

Backend (NestJS 10 + Drizzle ORM + PostgreSQL + Redis + BullMQ):
- 34 DB models (core + PL24 + EMEX schemas)
- Auth via Better Auth (email/password + social)
- Brands, Plans, Subscriptions, Payments (iyzico + EFT)
- VIN decode orchestration (Corgi + PL24 + EMEX + NHTSA)
- Interactive schema viewer backend (MinIO storage)
- EMEX scraping integration (Puppeteer + BullMQ workers)
- Translation module (EN→TR automotive dictionary)
- Admin dashboard API (stats, user mgmt, payment approval)
- Rate limiting, Helmet security, file upload validation

Frontend (Next.js 15 + Tailwind v4 + shadcn/ui + TanStack Query + Zustand):
- 20 routes: auth, dashboard, VIN search, schema viewer, admin
- Interactive schema viewer with zoom/pan/hotspot highlighting
- Subscription management with brand selector
- Payment flow (iyzico 3D Secure + EFT with receipt upload)
- i18n support (TR/EN)
- Error boundaries, loading skeletons, 404 page

Infrastructure:
- 85 tests (52 backend + 33 frontend, Vitest)
- CI/CD (GitHub Actions: lint, typecheck, test, build, deploy)
- Zero-downtime deploy script (PM2)
- Env validation script

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 02:03:56 +00:00