Add lib/google-ads.ts (lazy gtag init, gclid capture, Enhanced Conversions,
trackGoogleAdsConversion). Fire sign-up conversion on email registration and
purchase conversion on Stripe success. Gated on VITE_GOOGLE_ADS_* env; no-op
when unset. Mirrors the Meta Pixel/CAPI pattern.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
Floating launcher covered content on small screens. Bubble is hidden
below 768px via injected CSS; programmatic openChatwoot() callers
(subscription page, onboarding modal) still open the fullscreen widget.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sidebar linked /blog and /contact pointed at public marketing pages, so
clicking them dropped the user out of the dashboard shell. Extract page
content into shared components (blog-content, contact-content) and add
dashboard-wrapped routes /dashboard/blog, /dashboard/blog/$slug and
/dashboard/contact; sidebar now links to those. Public SEO pages stay
unchanged and remain the canonical URLs.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
'Çalışmadı mı? Bildir' opened the Sentry feedback dialog; most users
bail at a form. Now a single click captures a catalog_issue_reported
PostHog event with the vehicle/category context (vin, vehicle_label,
category_name, source, page) and flips to 'Bildirildi — teşekkürler'.
The identified person already carries the reporter's email; the Süper
Panel lists these under /analytics/reported-categories. The automatic
Sentry degradation report (with session replay) still fires via
useReportCatalogDegradation — unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New "Bu kod kataloğunuzda" section lists the user's decoded vehicles
whose parts include the queried OEM code, each linking to the schema
page that shows it. Pure sase data (parts.oem_code → vehicles, indexed
exact match) — no TecDoc / vehicle-structure dependency.
GET /parts/oem-vehicles?code=.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
POST /vehicles/decode returned the full vehicles row while every consumer
(search, landing, service-test) only navigates by id and re-fetches via
GET /vehicles/:id. That shipped the raw upstream decode payload to the
client on every decode — rawData is 19KB avg / 127KB p95 / 207KB max in
prod — plus the provider name, for nothing.
The response is now { id, brandName, model, year } on both the DB-hit and
fresh-decode paths. Frontend drops the (now absent) source property from
the vin_decode_success event; decode source remains queryable server-side
via query_logs.source.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The multi-candidate decode response leaked provider internals (source name,
pcat car ids, EMEX _ssd/_vid/_quickGroupsUrl/catalogId) and made the client
carry them between requests: the frontend stored candidateSource and echoed
pcatCarId/emexCarIndex back on selection.
Now the candidate list returned to the client carries only display fields
(name, description, parameters) plus an opaque key, and the provider mapping
is stashed in Redis (vin:candidates:*, 30m TTL, resolve-cache fallback). The
pick request sends just { vin, candidate }. Legacy pcatCarId/emexCarIndex
body params still work for already-loaded bundles.
Also drops `source` from the public /vehicles/preview response — no consumer
used it, and provider names must never be public (same policy as
teaser-stats).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Polish pass on the survey popover:
- Entrance: slide-up + fade on mount (300ms ease-out, respects
prefers-reduced-motion via motion-reduce:transition-none).
- Step progress: "1 / N" indicator on multi-question surveys so users know
how much is left.
- Esc closes the card (counts as a dismissal); listener attached once with a
dismissRef so it captures current answers, not a stale first-render closure.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Design review of the live survey popover surfaced three issues:
1. NPS 0–10 wrapped to two rows (flex-wrap + flex-1 made the 8/9/10 row
stretch absurdly wide) — now a single equal-width grid row (grid-cols-N
by scale; static map so Tailwind keeps the classes).
2. Single-question ratings forced a redundant "Devam" tap — tapping a score
now auto-advances (450ms debounce to allow changing the pick) and the
submit button is hidden on rating questions.
3. On mobile the card crowded the Chatwoot launcher — bumped to bottom-28
(sm:bottom-24 keeps desktop unchanged).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Remove the EAN line from the matched-article cards and stop fetching/
shipping eans in /p/oem (its only consumer was that line). Snapshot
still holds the data; the endpoint just no longer queries it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
OEM detail "slow load" was two things, not the query (DB 135ms / API
13-120ms):
1. The parts-panel link opened a NEW TAB → full SPA cold boot every
click. Switch to in-app client navigation on plain click (real href
kept, so ctrl/cmd/middle-click still opens a new tab).
2. The /p/oem response shipped each article's oeNumbers + compatible
lists (up to 200 each × 60 articles) that the UI never renders — 96%
of a 370 KB payload. Ship lean articles; aggregates already carry
the cross-refs. 60-article code: 370 KB → ~12 KB.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
trial_urgency_banner_viewed fires ~1s into the page load and raced the lazy
surveys extension — events captured before its hook exists can never activate
an event-triggered survey (affects real users, not just e2e). capture() now
awaits surveysReceiverReady() (onSurveysLoaded, 3s safety timeout) and runs the
eligibility check after the event has gone through.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Long cross-reference lists (some parts have 90+ aftermarket numbers,
160+ OE codes) were hard to scan. Add a filter input that matches
across all three sections (matched articles, aftermarket equivalents,
OE cross-refs) by brand (substring) or code (normalised, so "1j0 973"
finds "1J0 973 702"). Clear button + "no results" state.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
posthog-js silently drops every capture when userAgentData.brands contains
HeadlessChrome (UA string/webdriver masking doesn't help), which also starves
event-triggered surveys in Playwright runs against dev.sase.tr. Production
keeps the bot filter.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Formbricks CE turned out to gate person-based targeting (setUserId/attributes
→ 403 enterprise) — the exact flexibility surveys need. PostHog already holds
the person properties, so surveys move back there in API mode: posthog-js
evaluates eligibility (event triggers, targeting flags like
subscription_status=active, wait periods, per-distinct_id dedup) via
getActiveMatchingSurveys, and we render the popover ourselves — zero PostHog
branding, sase.tr dark-theme styling.
- lib/surveys.ts: display manager + capture payload builders that mirror
posthog-js's own "survey shown/dismissed/sent" shapes exactly
($survey_response_<qid>, $set $survey_dismissed/<id>[/iter], seenSurvey_*,
lastSeenSurveyDate) so the PostHog Surveys results UI works unchanged
- components/survey-popover.tsx: single_choice (+Diğer), open text, rating/NPS
- posthog.ts: capture/pageview/identify now schedule survey checks; register
deploy_env (dev.sase.tr ships the key now → staging traffic is filterable)
- remove @formbricks/js, its CSP entries and build args
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every OEM code was a link → unmatched codes (the majority — fasteners,
clips, body parts TecDoc doesn't carry) opened an empty "no
equivalents" page. Now the schema page batch-checks its codes against
the snapshot (POST /p/matched, one indexed query) and links ONLY the
matched ones; unmatched codes render as plain text. Copy stays for all.
No more dead-end empty pages from the parts panel.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- cover_file uploads in Directus land in the public blog-assets MinIO bucket;
API resolves them to storage.sase.tr URLs (Directus itself is Tailscale-only)
- POST /blog/cache/purge (automation token) lets a Directus Flow drop the
30-min list cache the moment a post changes in the CMS
- blog list + detail pages now render purely from the API; the 4 hand-authored
posts were migrated to Directus earlier and the JSX copies are removed
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
"P karşılığı bulunamadı" / "P kapsamında" read as nonsense — "P" is
the internal name, not a user-facing word. Use plain Turkish: "Bu OEM
kodu için uyumlu parça bulunamadı" and describe the catalog gap
without a brand or single-letter label.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Finishes 3b14431 (which only captured the file renames): apply the
identifier/endpoint/env/UI changes so the code matches the new paths
— PModule/PController/PSourceDbService, @Controller("p"), /p/oem,
config key `p`, P_DB_ENABLED/P_DB_URL, "P kataloğundan…" copy.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The snapshot stores scrape-local '/_debug/...' image paths that 404
off-host. Filter the OEM lookup to http(s) image URLs and render the
thumbnail only when one exists, so the detail page shows clean
code-focused cards instead of a grid of broken-image placeholders.
Real CDN URLs will surface automatically once present.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Full marketing-copy audit. Removes price-comparison claims (pricing
step, hero variant B, blog tip) that aren't built; normalises VIN→şase
across TR copy (plan features, e-commerce section, FAQ, blog, about) —
keeping VIN only in the educational "VIN nedir" post and the EN file;
de-consumerises framing (FAQ "aracınız", about "bireysel kullanıcılar").
Fabricated metrics/testimonials left untouched pending review.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Resolve a catalog OEM code to its TecDoc equivalents on a new
/dashboard/oem/$code page: the aftermarket parts that carry it
(brand + article number + image + EAN), buyable supplier
substitutes, and OE cross-references (same part under other makes).
- API: TecdocModule (read-only postgres-js client to the imported
`td` snapshot), GET /tecdoc/oem?code=. Normalisation-based match
(TecDoc stores `1J0 973 702`, catalog gives `1J0973702`); exact
match recovers ~1/10 vs normalised ~5/10 on real codes. Self-
disables without TECDOC_DB_* env → { matched: false }.
- Web: OEM code in the parts panel is now a link (new tab) to the
detail page; "N/A" stays plain text.
- Mirrors CatalogSourceDbModule (raw queries, no Drizzle modelling).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The landing OEM card, the manual-vs-Sase comparison table, and a blog
post advertised "price comparison" / "order history & part tracking" —
features that aren't built. Replaced with real capabilities
(exploded-diagram matching, interactive part diagrams, brand catalogs)
in both tr + en. Mirrors the same accuracy fix already shipped to the
Novu lifecycle emails.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Only ~1.6% of trials reach checkout: the 30-day full-access trial shows the
(urgency) banner solely in its final 7 days, leaving ~3 weeks with no in-app
push, and the value moment (first few queries) goes unconverted.
Add TrialValueUpsell — a calm, value-framed banner that fires on PROVEN VALUE
(user has decoded >= 3 customer vehicles, read server-truth via /vehicles/history)
and only OUTSIDE the urgency window (days > 7), so it fills the gap without ever
overlapping or competing with the urgency banner. B2B copy: frames around
"müşteri araçlarını sınırsız sorgulama", never personal-vehicle wording.
Tracks viewed/cta_clicked/dismissed in PostHog; dismissal persists per trial.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- initSentry() is now idempotent via a cached promise, and the degradation
reporter / feedback opener await it — so an empty/error state that renders
before the SDK's dynamic import resolves no longer drops the event (not missing
the failure is the whole point of the auto-capture).
- environment now resolves hostname-first (sase.tr → production, dev.sase.tr →
staging), so the compose-baked VITE_SENTRY_ENVIRONMENT default ("production")
no longer makes dev browser events indistinguishable from prod.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>