pcat/emex vehicles decode into a tree whose top level is ~100% folders with
zero parts (6746/9458 root nodes are parents, 0 direct leaves across 477
vehicles). Users — serkan filazi among them — saw an opaque wall of identical
folder cards, assumed the parts feature was missing, and bounced without ever
drilling (his session: 3 view-mode toggles, 0 category clicks; his data was
2-3 clicks deep and fully reachable).
Two changes, no upstream/proxy cost:
- getCategoryTree now rolls up a subtree partCount per node (one grouped parts
read + JS post-order sum). The grid already renders "N parça", so folders
now read "Fren sistemi · 142 parça" and steer drilling toward the branches
that actually contain parts; empty branches stay quiet. Counts reflect
drilled data and grow as branches are visited.
- A one-line drill hint on a "cold" tree (nothing drilled yet → no counts) so
first-visit users know parts live inside the subcategories instead of reading
the folder wall as a broken/empty catalog. Drops away once any branch is hit.
Verified: subtree roll-up unit test + existing suite (10/10), tsc + biome clean
across api/web/shared.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The browser pixel under-counts signups badly: ~96% of paid traffic is mobile
in-app browsers where iOS ITP / ad-blockers drop client events, and the OAuth
path never fired it reliably. Meta recorded ~0 registrations for a 7.5K-spend
campaign while PostHog saw 98 facebook signups — so Meta could neither optimize
toward nor attribute signups, which is the main driver of the low signup rate.
This adds a server-side CAPI CompleteRegistration:
- MetaCapiService + @Global module. Fail-open: no-ops unless META_CAPI_PIXEL_ID
+ META_CAPI_ACCESS_TOKEN are set; never throws (signup must not break).
SHA-256 hashed email + fbp/fbc/IP/UA.
- Fired from the better-auth user.create.after hook for ALL signups (reliable,
covers Google OAuth which the browser pixel missed entirely).
- A session-gated POST /analytics/meta/complete-registration endpoint adds
fbp/fbc/IP/UA (ad-click attribution) for the email path.
- The browser pixel now passes a shared event_id (signup_<userId>); the
premature Google client-pixel fire (fired on click, before completion) is
removed.
- All sources dedupe via event_id=signup_<userId>.
Activate by setting META_CAPI_PIXEL_ID + META_CAPI_ACCESS_TOKEN (Events Manager)
in the api env; META_CAPI_TEST_EVENT_CODE routes to Test Events for verification.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
So a non-prod env can hold the (public) project key and evaluate flags - kill
switches, rollout - WITHOUT shipping analytics events to the shared prod
PostHog project. The posthog-node client (which powers both capture and local
flag eval) now initializes whenever the project key is present; capture() is
gated on POSTHOG_CAPTURE_ENABLED (default on; set "false" on dev). Wired into
the api + worker compose blocks.
This lets dev.sase.tr exercise the kill switches end-to-end while keeping the
prod analytics project clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add server-side PostHog feature-flag evaluation to the API and wire three
high-leverage uses. The flags live in PostHog (project 127747), dormant and
fail-open, so this commit is a no-op until POSTHOG_PERSONAL_API_KEY is set and
a switch is flipped.
Phase 1 - upstream kill switches:
PostHogService gains local flag evaluation (onlyEvaluateLocally +
sendFeatureFlagEvents:false -> zero decode latency, no VIN leak) and
isSourceLive(). Guards at each source's decode entry (parts-catalogs, emex,
pl24 whole-source + per-brand via LEGACY_ARCH_SOURCE_TAG) let a flailing
upstream be disabled from the PostHog UI in ~5s instead of a
code-fix -> dev -> prod redeploy. Fail-open: any unresolved flag keeps the
source live, so a PostHog outage can never black out decoding.
Phase 2 - guarded rollout primitive:
isEnabled()/variant() with VIN bucketing, ready to ramp a new decode/parser
path 0->100% behind a decode-*-v2 flag (recipe in feature-flags-strategy.md).
Phase 4 - remote-config ops tuning:
prefetch-worker reads cfg-backfill-tuning to retune backfill
batchSize/maxBacklog/businessHoursOnly live; malformed/missing -> the
compiled-in constants.
POSTHOG_PERSONAL_API_KEY wired into the api + worker compose blocks (empty ->
flags inert, no added latency). Tests updated for the new constructor params.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
PSA (Peugeot/Citroën/DS) VIN decode was systemically broken: the catalog
vin-group page returns all families unfiltered, so decode fell back to the first
family/salesType (a manual base variant) — yielding "{Brand} {VIN}" model names,
empty transmission, wrong model year, and manual-only parts trees (automatic
gearbox parts missing). Reported for a 1999 Peugeot 106 automatic shown as a 2029
manual with no automatic parts.
- New self-contained PL24PsaService: consumes PL24's FI flow (vin.action →
hintstoken → FI page → json-vin-main-groups → json-vin-illustrations →
vin-image-board). Reads model/year/transmission from the FI identification
table; builds the VIN-indexed parts tree (correct per actual VIN). Does not
touch Ford/Volvo/Nissan/Opel/Hyundai-Kia/Fiat.
- Orchestrator + categories.service route PSA VIN decode/drill to the new service.
- Cycle-correct extractModelYear in @sase/shared (X→1999, not 2029): resolve the
30-yr VIN year code to the most-recent plausible year (≤ now+1); dedupe 6 copies.
Validated live against 13 already-decoded PSA VINs: 12/13 full trees with real
model/year/transmission; automatics correctly detected (106 BVA, 206 AL4, 3008 BVA8).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Second batch from undecoded-vin-rca.md. Every entry verified (authoritative
WMI registry + prod sibling + VDS fingerprint + live PL24 probe) before adding
— a wrong brand label or a dead PL24 route both have real downside.
Q5 — WMI_BRAND_MAP (packages/shared, UX/telemetry only; does NOT change which
catalog decodes). Adds 9: W1V=Mercedes-Benz, VXF=Fiat, YAR=Toyota, NL1=Hyundai,
KPA=SsangYong, PL1=Proton, LSV=Volkswagen, LVV=Chery, ZCF=Iveco. Resolves the
RCA conflicts: VXF=Fiat (not Opel), YAR=Toyota ProAce (not Opel); NL1=Hyundai
(not TOGG — MY char predates TOGG). Deliberately NOT added: WF1/WAA/W0W (1-char
corruptions of WF0/WAU/WVW — mapping them would mislabel typo classes), VE1
(unresolved, deferred).
Q6 — PL24_WMI_SERVICE_MAP (real decode routing). Both target services are
proven live, so these only append WMIs to working routes (no dead-route →
isDecodeable()=true → 25s-budget/circuit-breaker hazard):
- SJN/VSK/MNT → nissan_parts (Sunderland/Spain/Thailand) — regression restore,
dropped in the 757905f refactor; nissan_parts proven (JN1 decodes in prod).
- W1V → mercedesvans_parts (Sprinter/Vito) — retires the RCA "never tried" flag:
mercedesvans_parts decodes WDF44770513186937 live today via the same P5 path.
Tests: shared getBrandFromWmi (adds + corruption-skips return null), pl24 route
map. Full api suite green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
PCAT_SOURCE_DB_URL and EMEX_SOURCE_DB_URL are declared as
`z.string().url().optional()` (and `.optional()` resp). docker-compose's
`${VAR:-}` substitution ALWAYS sets the env var, even to "", so when the
Coolify env is unset the container receives PCAT_SOURCE_DB_URL="".
zod's `.optional()` only accepts undefined, so `.url()` then rejects ""
and the api crashes on boot with "Invalid url".
This is exactly what took prod down on commit 939e4dc — the dev→main
merge brought in the catalog-source env schema without the empty-string
preprocess. Hotfixed by setting the env to a dummy URL via Coolify DB;
this patch makes the schema resilient permanently so future env edits
that clear the value won't recrash boot.
Apply a preprocess that maps empty/whitespace strings to undefined
before the URL check fires. Mirror the same treatment on EMEX so it can
also be unset without surprises.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The catalog-wide bridge in EmexSourceDbService.fetchCategoryParts was
measured against vehicle_parts on 2026-06-01 and found to return 7-114x
more parts than belong to the requesting vehicle, with 49-98 wrong OEM
codes per 100 served. That directly violates the project rule that the
user must never see a wrong OEM.
Per-catalog noiseRatio sample (catalog-wide / per-vehicle):
RENAULT201910 51x | FFIAT84 45x | VOLVO201410 24x | MB201810 14x
AU1587 8x | BMW202501 70x (+ gid namespace mismatch ETK vs numeric)
GM_C201809 114x | MINI202501 12x | LRE201412 7x | MAZDA2020 54x
GM_OP201809 dump has only 1 wildcard vehicle (unique_key="_") so the
single Crossland X "owns" all 47k Opel parts — same firehose served
to any Opel sub-model in sase prod.
All alternative bridges were proven dead:
SSD eşleştirme - session-bound, 0/91 sase SSDs match dump
scrape_queue_v2.vehicle_ssd - same session SSD format
api_cache replay - table empty (0 rows)
wizard_parameters - table empty (0 rows)
VIN direct - no VIN column in dump
The only viable per-vehicle bridge is vehicles.unique_key reconstruction
from raw_data.parsedOptions, but sase currently stores the required 4
wizard fields on just 5/103 emex vehicles (all Renault). That work is
follow-up; this patch only stops the bleeding.
Change:
- Add EMEX_SOURCE_DB_ALLOWED_CATALOGS env (comma-separated, default "")
- EmexSourceDbService.fetchCategoryParts returns null unless catalogCode
is in the allowlist. Empty allowlist = service is effectively off for
parts, full fallthrough to live emex.
- Connection pool stays alive so the follow-up per-vehicle bridge /
schema-only path can use it without flipping env.
- Boot logs warn loudly when connected with an empty allowlist.
Prod was never affected — CATALOG_SOURCE_DB_ENABLED was unset there. This
fixes dev branch behaviour (default-on since commit 3a3a7d3) and keeps
prod safe by default once main is promoted.
Files:
- packages/config/src/index.ts env schema + audit notes
- apps/api/src/config/configuration.ts parse allowlist into string[]
- apps/api/src/integrations/catalog-source-db/emex-source-db.service.ts
allowlist field, init logging, fetchCategoryParts gate, class doc
- docker-compose.coolify.yml env injection for api + worker
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Verified 2026-06-01 against dev's 103 unique pcat carIds: the current pcat
dump's deep-scrape (7.978 cars with real parts data via schema_parts or
part_groups+part_group_items) targets a US/JDM-market subset — Toyota 2112,
Nissan 1508, Audi 1311, Chevy 1050, Hyundai 745. **None** of sase's TR-market
vehicles intersect that rich subset:
- 18/103 sase carIds are in dump.cars at all (registry only)
- 0/103 yield parts via Bridge A (schema_images → schema_parts)
- 0/103 yield parts via Bridge B (part_groups → part_group_items)
Even the cars that match by exact carId (Fiat Doblo 368 schemas, Renault
Megane, Bravo 456 schemas) have only diagram metadata — no parts annotation.
The dump scraper finished tier-1 (catalog/model/car listing) and tier-2
(schema diagrams) for these, but stopped before tier-3 (parts annotation).
Under the strict "always correct OEM" constraint there is no safe pcat lookup
today. Disable it. The container stays up for future use cases (OEM cross-
reference search, alt-part matching) and so we can flip the env back without
a code change if a richer dump arrives.
EMEX stays on (its catalog-allowlist is the next step).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds an optional local-dump lookup layer in front of the live PartsCatalogs and
EMEX scrapes. When enabled, getCategoryWithPartsInner queries a Postgres
(pcat) or MariaDB (emex) dump for the requested schema/group's parts and
hotspots; on miss it falls through to the existing upstream call unchanged.
Hits avoid the live API, its cooldown, and its rate-limits — direct DB latency.
- New CatalogSourceDbModule with PcatSourceDbService + EmexSourceDbService
(raw SQL, no Drizzle schema modeling — dump shapes are frozen snapshots).
- pcat lookup keys on schema_images.schema_ext_id (the dump's column that
matches sase's pcat groupId; observed ~7% hit rate on prod's 6596 unique
groupIds, of which ~10% have schema_parts → ~3-5% net parts coverage).
Joins schema_parts → parts directly; the dump's part_groups+part_group_items
linkage covers 0 of our hits, so we skip that path entirely.
- emex lookup uses (catalog_id, ssd) → vehicles.id then (vehicle_id, group_id)
→ vehicle_parts → parts + part_images. The ssd is already persisted into
vehicles.rawData.ssd by the existing emex.mapper, no extra capture needed.
Gated behind CATALOG_SOURCE_DB_ENABLED + PCAT_SOURCE_DB_URL / EMEX_SOURCE_DB_URL.
All three default unset, so this commit is a no-op until prod env is configured.
Adds mysql2 dep for the MariaDB client.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Site-wide live-chat widget served from the self-hosted Chatwoot at
destek.sase.tr, with verified user identity and vehicle context.
- apps/web: lib/chatwoot.ts loads the SDK lazily (mirrors the PostHog
init pattern), init in main.tsx, identify logged-in users in __root
via a server-computed HMAC, and attach the viewed vehicle (VIN/brand/
model) as contact custom attributes on the vehicle detail page.
- apps/api: GET /api/chatwoot/identity (AuthGuard-protected) returns
HMAC-SHA256(user.id) so the widget can use verified identity.
- env: VITE_CHATWOOT_BASE_URL + VITE_CHATWOOT_WEBSITE_TOKEN (build-time,
wired through docker-compose.coolify.yml build args + Dockerfile ARG)
and CHATWOOT_HMAC_TOKEN (api runtime). All optional — widget and
endpoint no-op when unset.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Route all lifecycle/transactional emails through Novu
(api.bildirim.semih.ai, delivered via Postal). A framework-agnostic
client is shared by the NestJS API and the standalone BullMQ worker.
- welcome + referral on signup (better-auth user.create.after)
- email-verification + password-reset (auth.ts; token links never
track-wrapped so the one-time token survives)
- referral-qualified / referral-reward to the referrer on qualification
- payment-success / payment-failed in the Stripe webhook handlers
- trial-ending + win-back via a new daily lifecycle-email cron (worker),
idempotent via a 1-day endDate window (no sent-flag column)
- signed track.sase.tr CTA links when MAILTRACK_SECRET is set
- NOVU_* / APP_PUBLIC_URL / MAILTRACK_SECRET env added to config,
validation, .env.example and both compose service blocks
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the click-to-logout profile trap with a proper DropdownMenu
(settings / subscription / logout), swap the hand-rolled mobile overlay
for a Radix-Dialog Sheet (focus-trap, Esc, scroll-lock), and fix
accessibility gaps: aria-labels on icon buttons, aria-current + labelled
nav landmarks, and effect-based auth redirect instead of navigating
during render.
Also: reorder nav (Main → Account → Support → Admin), full i18n of
labels/sections, conditional "upgrade plan" button, header page title,
collapsed-mode active indicator + tooltips, contrast/touch-target
tweaks, and a typed NavItem to drop the `as any` exact cast.
Adds DropdownMenu, Tooltip and Sheet primitives to @sase/ui.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- WMI_BRAND_MAP + getBrandFromWmi packages/shared'e taşındı (tek kaynak);
corgi.service artık buradan import ediyor (davranış aynı, testler geçiyor)
- VinBrandIcon: VIN'in WMI'ı bilinen markaya denk gelince büyüteç yerine
marka logosu pop animasyonuyla görünür (landing + search VIN textbox)
- WMI haritasına prod DB'de decode edilmiş VIN'lerden 23 eksik WMI eklendi
(Türkiye fabrikaları NM4/NMT/NLA/NLH/NMB dahil; tüm DB WMI'ları artık tanınıyor)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
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>
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.
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>
- 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>
- 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>
- 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>
Browser autofill forces a light background on inputs, making white text
invisible in dark mode. Added -webkit-autofill overrides in globals.css
and switched Input from bg-transparent to bg-background with explicit
text-foreground.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- 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>