The 0004 migration file shipped in 6a0b36a but _journal.json did not
include its entry, so the hash-based migrate runner saw "0 applied,
4 already applied" on the previous deploy. Append idx 4 so the next
deploy picks it up.
Co-Authored-By: Claude Opus 4.7 (1M context) <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>
c4f6cc1 added this.redis.exists/setNx/incr/expire calls to vehicles.service
(dedup lock + circuit breaker) but vehicles.service.spec.ts redisService
mock was not updated. Result: 1 failing test broke pnpm test, which broke
Fusion merger's in-merge build verification on every subsequent task.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
For continuous optimization we need to attribute slow/failed decodes to
the right cause. query_logs.timings jsonb is now a structured decode-meta
blob, not just stage timings:
- wmi: first 3 chars of VIN (per-brand aggregation)
- result_kind: vehicle / pcat_candidates / emex_candidates / unknown / aborted
- cache_source: db_hit / redis_positive / redis_negative / lock_wait / miss
- candidate_pick: pcat / emex / none (when user picks from candidate modal)
- pcat_car_count, emex_candidate_count (cardinality, drives candidate-modal rate)
- pl24_circuit_open, pl24_skipped (CB state at request time)
- vin_api_used, vin_api timing (NHTSA fallback frequency)
Migration 0003 adds a query_log_insights VIEW that flattens these keys
into typed columns, so ad-hoc SQL doesn't need json operators. New meta
keys appear automatically as NULL; the VIEW stays stable.
docs/analytics-queries.sql has 8 starter queries: cache hit ratio,
per-source latency, slowest WMIs, stage breakdowns, CB/abort frequency,
candidate-modal rate, top failing VINs, dedup effectiveness.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous deploy of c4f6cc1 failed because I had applied the timings
column manually via psql before the deploy ran, leaving the hash-based
migrate runner with raw ADD COLUMN that conflicts with the existing column.
Hash differs after this change so the runner will retry on next deploy
and succeed (no-op on the already-applied column, success path records
the hash).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
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.
- 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>
Old queries cached empty arrays for groups and schemas. Bumping cache
key prefix from "pcat:" to "pcat2:" so new correct queries bypass stale
entries without needing manual Redis flush.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
car_groups junction table only has data for 2K of 68K cars. Derive groups
from schema_images.category_id which covers 92.6% of records. Also fix
getSchemaImages to filter by category_id instead of empty group_id.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Expand pcat API with getCars, getCarGroups, getSchemaImages, getSchemaDetail
endpoints. Add frontend routes for the complete flow:
catalog → model → car → groups → schema+parts with hotspots.
Reuses existing SchemaViewer component for interactive part diagrams.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Import parts-catalogs.com dump into pc schema (72 catalogs, 2788 models,
134K groups). Add pcat-catalog service/controller with Redis caching.
Create frontend routes for catalog → model → group navigation.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When EMEX DB has no vehicle match (Ford EU, Mercedes etc.), instead of
falling back to full on-demand VIN decode (~8s), use the VIN-decode SSD
directly with QuickGroups.aspx?c={catalog}&vid=0&ssd={vinSsd} (~2-3s).
Tested: Ford Focus CB4 VIN SSD returns 376 categories via QuickGroups.
New step 3.5 in the cascade:
1. pathData → matchByName (DB-only, ~50ms)
2. emexVehicleName → matchByName (DB-only, ~50ms)
3. matchBySsd wizard walk (DB, ~2-3s)
3.5. VIN SSD → QuickGroups shortcut (~2-3s) ← NEW
4. Fallback → full on-demand scrape (~8s)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
VIN-decode SSDs are too long/specific — EMEX returns HTML instead of
JSON when passed to GetWizard2. New approach:
1. Start from root SSD ("")
2. Advance through wizard steps (Europe → Passenger → ...)
3. Find pathData name (e.g. "Focus CB4 2008-2011") in model options
4. Use getWizardVehicles for DB matching
For Ford Focus: wizard finds it at step 3, but Ford DB has no EU
models so it still falls back to on-demand scrape. BMW/Renault with
good DB coverage will benefit from this flow.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The VIN-decode SSD is model-level, not variant-level. matchBySsd now:
1. Calls GetWizard2 with VIN SSD → gets sales designation options
2. Finds option matching pathData (e.g. "Focus CB4 2008-2011")
3. Uses that option's key SSD for Vehicles.aspx?ft=findByWizard2
4. Matches motor/variant names against DB
Note: Ford Focus still falls back to on-demand scrape because Ford
DB coverage is only 55 US models (no EU models like Focus/Fiesta).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Temporarily disable PartsCatalogs and PL24 decode steps to test
EMEX-only VIN resolution speed and reliability.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
ConfigService returns empty string (not the default) when env var
exists but is empty. Add || fallbacks in code and set proper defaults
in docker-compose to prevent invalid proxy URL like http://user:pass@:10000.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Node's global fetch doesn't support the dispatcher option. Use
undici's exported fetch when proxy is configured, which properly
handles ProxyAgent as a dispatcher.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
undici was in devDependencies, causing MODULE_NOT_FOUND crash in
production where only prod deps are installed. Move to dependencies
since emex.service.ts imports ProxyAgent at the top level.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
EMEX server blocks direct server IP requests (geo/bot protection),
causing all HTTP-based VIN decodes to return empty results. Add proxy
support to fetchEmexHtml() using undici ProxyAgent, reusing the same
EMEX_USE_PROXY / EMEX_PROXY_* env vars as the browser service.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
decodeVinOrCandidates returns { type: "notFound" } or { type: "error" }
instead of null, so data !== null was incorrectly marking these as
success. Now explicitly checks emex result type.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
testService() was swallowing errors — services like parts-catalogs catch
internally and return null, so the outer try/catch never fired. Now each
service call has its own try/catch, and null responses get an explicit
error message.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The api client unwraps response.data automatically, which conflicts
with our testService response having its own 'data' field.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Admin-only page at /dashboard/service-test with dropdown to test
each VIN decode service (Corgi, PartsCatalogs, PL24, EMEX, VIN API)
individually or the full cascade. Results shown as JSON on page,
no DB writes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Backend: add `model` query param to wizard-vehicles endpoint
- Backend: 3-tier matching: exact name → name prefix → model fuzzy
- Frontend: simplified wizard UI, show determined params as tags
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace static DB-based dropdown chain with live wizard from emexdwc.ae.
The wizard API (GetWizard2) works without auth and shows all models/options
for each catalog (e.g. AU1587 now shows 103 Audi models vs 11 before).
Backend: proxy GetWizard2 + search DB for matching vehicles after wizard completes.
Frontend: step-by-step wizard with breadcrumb navigation, search, and reset.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace stacked images + plain table with interactive SchemaViewer (same as PL24).
Backend transforms response to SchemaViewer-compatible format (Part/SchemaPic types).
Add prev/next pagination for groups with multiple schema images.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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>
Helmet's default CSP blocked images from storage.sase.tr (different
origin). Add it to img-src and connect-src directives.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move SPA fallback from main.ts catch-all (didn't work - NestJS 404
handler runs first) to HttpExceptionFilter. Now non-API GET 404s
serve index.html so client-side routing works for /dashboard/* etc.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
NestJS was returning 404 JSON for SPA routes like /dashboard/search
because ServeStaticModule couldn't find a matching file and the request
fell through to NestJS router. Add explicit catch-all GET handler after
app.init() to serve index.html for non-API routes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add multi-stage Dockerfile (node:22-alpine, pnpm, Chromium for Playwright)
- Add docker-compose.coolify.yml with api, worker, redis services
- Add @nestjs/serve-static for serving Vite frontend from NestJS
- Add .dockerignore
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace Playwright browser with pure HTTP for EMEX VIN decode:
- Vehicles.aspx, QuickGroups.aspx, QuickDetails.aspx all work
without authentication — no session/browser needed
- VIN decode drops from ~10-20s (Playwright) to ~1-5s (HTTP)
- Playwright browser kept as fallback if HTTP fails
Add multi-candidate selection for EMEX (mirrors PartsCatalogs flow):
- EmexCandidate interface matches PcatCar shape so VehicleSelectModal
renders without changes
- decodeVinOrCandidates(): single HTTP fetch returns vehicle OR
candidates (avoids double Vehicles.aspx request)
- decodeVinByIndex(): resolves user-selected candidate by index
- vehicles.service: emexCandidates in VinResolveResult, emexCarIndex
param in decodeVin/resolveVin, resolveEmexCarByIndex() method
- vehicles.controller: @Body("emexCarIndex") accepted
- search.tsx: candidateSource state, handleCandidateSelect sends
emexCarIndex or pcatCarId based on source
Test VIN NM417800006410193 (Fiat SIENA) now shows 2-option modal.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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 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>