Some checks failed
Sync dev → Gitea / Mirror dev to Gitea (push) Has been cancelled
Commits merged: - docs(FN-317): complete Step 3 — update docs and memory for sase-web removal - feat(FN-317): complete Step 1 — remove legacy sase-web PM2 entry Files changed: .changeset/remove-pm2-sase-web.md | 9 +++++++++ .fusion/memory/MEMORY.md | 2 +- CLAUDE.md | 2 +- docs/INDEX.md | 8 +++----- ecosystem.config.js | 16 ---------------- knowledge.md | 3 +-- 6 files changed, 15 insertions(+), 25 deletions(-) Fusion-Task-Id: FN-317
354 lines
18 KiB
Markdown
354 lines
18 KiB
Markdown
# SASE v2 — Claude Code Project Guide
|
|
|
|
> VIN/chassis number lookup + auto parts catalog platform for the Turkish market.
|
|
> **URL:** https://sase.tr | **Repo root:** `/home/s/ss`
|
|
|
|
## Tech Stack
|
|
|
|
| Layer | Technology |
|
|
|-------|-----------|
|
|
| **Monorepo** | pnpm 10.29 workspaces + Turborepo |
|
|
| **Backend** | NestJS 10.4, TypeScript 5.7, Node 22 |
|
|
| **Database** | PostgreSQL 17 + Drizzle ORM 0.41 |
|
|
| **Cache/Queue** | Redis 7.4 + ioredis + BullMQ |
|
|
| **Auth** | Better Auth 1.2 (cookie-based sessions) |
|
|
| **Frontend** | Vite 6.3, React 19, TanStack Router 1.120, TanStack Query 5 |
|
|
| **Styling** | Tailwind CSS 4, shadcn/ui (Radix primitives) |
|
|
| **State** | Zustand 5 |
|
|
| **Payments** | Iyzico (card) + EFT (bank transfer) |
|
|
| **Email** | Postal (transactional) |
|
|
| **Storage** | MinIO (S3-compatible) |
|
|
| **Analytics** | PostHog (product analytics) |
|
|
| **Observability** | OpenTelemetry (API) + Grafana Faro (frontend) |
|
|
| **Testing** | Vitest 3, Playwright 1.50 |
|
|
| **Linting** | Biome (2-space indent, double quotes, semicolons, trailing commas) |
|
|
| **CI/CD** | GitHub Actions → SSH deploy → PM2 |
|
|
|
|
## Project Structure
|
|
|
|
```
|
|
ss/
|
|
├── apps/
|
|
│ ├── api/ # NestJS backend (port 4000, prefix /api)
|
|
│ │ └── src/
|
|
│ │ ├── main.ts # Bootstrap (Helmet, CORS, rate limiting)
|
|
│ │ ├── app.module.ts # Root module (global guards/interceptors/filters)
|
|
│ │ ├── worker.ts # BullMQ worker process
|
|
│ │ ├── database/schema/ # Drizzle ORM schemas (core.ts, emex.ts, pl24.ts, parts-catalogs.ts, relations.ts)
|
|
│ │ ├── common/ # Guards, interceptors, filters, decorators, pipes, DTOs
|
|
│ │ ├── integrations/ # corgi/, pl24/, emex/, parts-catalogs/, vin-api/
|
|
│ │ └── [modules]/ # auth, users, brands, plans, subscriptions, payments,
|
|
│ │ # referrals, vehicles, categories, parts, catalog,
|
|
│ │ # translations, analytics, admin, jobs, email, storage, redis,
|
|
│ │ # telemetry
|
|
│ └── web/ # Vite + React frontend (port 3000)
|
|
│ └── src/
|
|
│ ├── main.tsx # Entry (RouterProvider, QueryClientProvider, Faro, PostHog)
|
|
│ ├── routes/ # TanStack Router file-based routes
|
|
│ ├── components/ # admin/, schema/, vehicles/, categories/, payment/, settings/, subscription/
|
|
│ ├── hooks/ # useAuth, useParts, useSchemaInteraction
|
|
│ ├── stores/ # Zustand: auth.store.ts, schema.store.ts
|
|
│ ├── lib/ # api-client, auth-client, i18n, posthog, faro, toast, user-settings, category-icons
|
|
│ └── messages/ # tr.json, en.json (i18n)
|
|
├── packages/
|
|
│ ├── shared/ # @sase/shared — types, Zod schemas, constants, utils
|
|
│ ├── config/ # @sase/config — Zod env validation schema
|
|
│ └── ui/ # @sase/ui — shadcn-based React components
|
|
├── docker/ # docker-compose.yml (PostgreSQL, Redis, MinIO) + nginx configs
|
|
├── scripts/ # deploy.sh, test/debug scripts
|
|
├── docs/ # INDEX.md + detailed docs (00-13)
|
|
└── ecosystem.config.js # PM2 config (api, worker)
|
|
```
|
|
|
|
## Key Commands
|
|
|
|
```bash
|
|
pnpm dev # Start all apps (Turbo)
|
|
pnpm build # Build all packages + apps
|
|
pnpm test # Run all tests (Vitest)
|
|
pnpm lint # Biome lint check
|
|
pnpm typecheck # TypeScript --noEmit
|
|
|
|
# Database (run from apps/api/)
|
|
pnpm db:push # Push schema to DB (Drizzle)
|
|
pnpm db:studio # Open Drizzle Studio
|
|
pnpm db:seed # Seed database
|
|
pnpm db:generate # Generate migration
|
|
|
|
# Single app
|
|
pnpm dev --filter=api # API only
|
|
pnpm dev --filter=web # Web only
|
|
|
|
# Route generation (auto on dev/build, manual if needed)
|
|
pnpm --filter web exec tsr generate
|
|
```
|
|
|
|
## Coding Conventions
|
|
|
|
- **Formatter:** Biome — 2-space indent, 100-char line width, double quotes, semicolons, trailing commas
|
|
- **Module pattern:** NestJS feature modules — each domain has `module.ts`, `service.ts`, `controller.ts`, `*.dto.ts`, `*.spec.ts`
|
|
- **API response format:** All responses wrapped in `{ success: true, data: ... }` via `TransformInterceptor`
|
|
- **Error format:** `{ success: false, error: { code, message } }` via `HttpExceptionFilter`
|
|
- **Auth:** Cookie-based via Better Auth. Skip with `@Public()` decorator. Get user with `@CurrentUser()`.
|
|
- **Roles:** `@Roles("admin")` decorator + global `RolesGuard`
|
|
- **Validation:** Zod schemas in `@sase/shared`, imported by both API and Web
|
|
- **DB naming:** snake_case columns, camelCase in TypeScript (Drizzle mapping)
|
|
- **Frontend aliases:** `@/` maps to `apps/web/src/` in Vite. `@sase/*` maps to `packages/*/src` in tsconfig.
|
|
- **Route files:** TanStack Router auto-generates `routeTree.gen.ts` — never edit manually
|
|
- **i18n:** Turkish default (`tr.json`), English available (`en.json`). Use `useTranslation()` hook → `t("key")`
|
|
|
|
## Backend Modules
|
|
|
|
| Module | Purpose |
|
|
|--------|---------|
|
|
| **AuthModule** | Better Auth (email/password + Google OAuth) |
|
|
| **UsersModule** | Profile CRUD, password change, OAuth connections, account deletion |
|
|
| **BrandsModule** | Brand CRUD (cached, admin-managed) |
|
|
| **PlansModule** | Pricing plan CRUD (cached, admin-managed) |
|
|
| **SubscriptionsModule** | Create, activate, cancel, resume, extend subscriptions |
|
|
| **PaymentsModule** | Iyzico card + EFT with receipt upload + admin approval |
|
|
| **ReferralsModule** | Referral code generation, tier-based rewards |
|
|
| **VehiclesModule** | VIN decode (multi-source fallback), vehicle history, brand access check |
|
|
| **CategoriesModule** | Hierarchical category tree, schema pictures |
|
|
| **PartsModule** | Parts by category, OEM code search |
|
|
| **CatalogModule** | VIN-less catalog browser — PL24 brands, models, category trees, parts |
|
|
| **TranslationsModule** | Automotive term translation (Redis → DB → Dictionary fallback) |
|
|
| **AnalyticsModule** | OEM code copy tracking, usage analytics |
|
|
| **AdminModule** | Dashboard stats, user management, payment approval |
|
|
| **EmailModule** | Postal transactional emails (welcome, payment confirmation, password reset) |
|
|
| **StorageModule** | S3/MinIO file upload/download |
|
|
| **RedisModule** | Key-value cache operations |
|
|
| **JobsModule** | BullMQ queues + processors + prefetch worker |
|
|
| **TelemetryModule** | OpenTelemetry SDK (tracing, metrics) |
|
|
|
|
## Architecture Patterns
|
|
|
|
- **Global guards order:** ThrottlerGuard → AuthGuard → RolesGuard
|
|
- **Global interceptors:** TransformInterceptor → LoggingInterceptor → TimeoutInterceptor (30s)
|
|
- **Global filters:** HttpExceptionFilter, DrizzleExceptionFilter (unique constraint → 409)
|
|
- **Vite proxy:** `/api` requests → `http://localhost:4000` in dev
|
|
|
|
### VIN Decode Fallback Chain
|
|
Corgi (offline WMI) → **PartsCatalogs API** → PL24 API → EMEX scraper → NHTSA VIN API
|
|
|
|
> Note: PartsCatalogs was added between Corgi and PL24 as it has broader VIN coverage. If multiple car matches return, the frontend prompts the user to select.
|
|
|
|
### Category Fetch Chain (VIN-based)
|
|
DB cache → PL24 → PartsCatalogs → EMEX (lazy, source-based in `getCategoryTree`)
|
|
|
|
### Catalog Browse Flow (VIN-less)
|
|
1. `GET /catalog/brands` → check user subscription access per brand
|
|
2. `GET /catalog/brands/:name/models` → PL24 `fetchVehicleList()` → stored in `catalogVehicles` table
|
|
3. `GET /catalog/vehicles/:id/categories` → PL24 `fetchMainGroups()` → stored in `categories` with `catalogVehicleId`
|
|
4. `GET /catalog/vehicles/:id/categories/:categoryId` → PL24 `fetchSubGroupsByPath()` / `fetchPartsByPath()` → stored lazily
|
|
|
|
### PL24 Catalog Architectures
|
|
- **P5_MODERN** (REST JSON API): VW Group, BMW, Mini, Mercedes, Porsche, Renault, Dacia, Alpine, Jaguar, Land Rover, Toyota, Lexus, MAN, Mitsubishi, Suzuki, etc.
|
|
- **LEGACY_PSA** (HTML scraping): Citroën, Peugeot
|
|
- **LEGACY_FORD** (HTML scraping): Ford passenger (wf0_parts) + commercial (fordt_parts)
|
|
- **LEGACY_HYUNDAI_KIA** (HTML scraping): Hyundai, Kia
|
|
- **LEGACY_NISSAN** (HTML scraping): Nissan, Infiniti
|
|
- **LEGACY_OPEL** (HTML scraping): Opel, Vauxhall
|
|
- **LEGACY_VOLVO** (HTML scraping): Volvo, Polestar
|
|
|
|
Our PL24 account (tr-903645) supports **VAG group only** for VIN-less catalog. Other brands may return errors on model listing. All P4 Legacy VIN decodes route through `PL24FordLegacyService.decodeVinForService(vin, serviceName)`.
|
|
|
|
### Caching Strategy
|
|
- Redis: VIN decode results (24h), category trees (2h for catalog browser), parts fetches (1h), translations
|
|
- HTTP Cache: brands, plans (30min `Cache-Control`)
|
|
- Sessions: Better Auth (5min Redis)
|
|
|
|
### Job Queues (BullMQ)
|
|
| Queue | Trigger | Schedule |
|
|
|-------|---------|----------|
|
|
| `EMEX_SCRAPE` | On-demand (VIN decode) | — |
|
|
| `CATALOG_PREFETCH` | After VIN decode | — (depth-limited, rate-limited, cooldown-guarded) |
|
|
| `SUBSCRIPTION_EXPIRY` | Cron | Daily 3:00 AM |
|
|
| `QUERY_CLEANUP` | Cron | Weekly Sunday 4:00 AM |
|
|
|
|
### Subscription & Access Control
|
|
- Plans have `brandCount` field: `0` = unlimited access, `N` = limited to N brands
|
|
- Brand access tracked in `userBrands` junction table (userId + subscriptionId + brandId)
|
|
- Full plan (brandCount=0) auto-adds all active brands on activation
|
|
- `BrandAccessGuard` (per-route) verifies user's subscription includes the requested brand
|
|
|
|
## Database Schema Summary
|
|
|
|
**Schema files:** `apps/api/src/database/schema/`
|
|
- `core.ts` — Main application tables
|
|
- `emex.ts` — EMEX scraper cache tables
|
|
- `pl24.ts` — PL24 catalog cache tables
|
|
- `parts-catalogs.ts` — PartsCatalogs API cache tables
|
|
- `relations.ts` — Drizzle ORM relationships
|
|
|
|
**Key tables in `core.ts`:**
|
|
- `users`, `sessions`, `accounts`, `verifications` — Better Auth managed
|
|
- `brands`, `plans` — Catalog of available brands/plans (admin-managed)
|
|
- `userSubscriptions` — status: pending/active/trial/cancelled/expired
|
|
- `userBrands` — junction table controlling brand access per subscription
|
|
- `payments` — Iyzico or EFT, status tracking
|
|
- `vehicles` — one per unique VIN, shared across users via `userVehicles`
|
|
- `userVehicles` — junction (userId + vehicleId unique), tracks lastAccessedAt
|
|
- `categories` — parent-child hierarchy; has both `vehicleId` (VIN-based) and `catalogVehicleId` (VIN-less) FKs (nullable for the other mode)
|
|
- `parts` — OEM code, quantity, hotspot index; same dual FK pattern as categories
|
|
- `schemaPics` — exploded view images + hotspots JSONB, linked to categories
|
|
- `catalogVehicles` — VIN-less catalog: one record per PL24 service vehicle (unique on source + serviceVehicleId)
|
|
- `queryLogs` — VIN decode audit trail
|
|
- `oemCodeCopies` — OEM code copy events (analytics)
|
|
- `referrals`, `passwordResetTokens`, `emexCategoryTranslations`
|
|
|
|
## Integrations
|
|
|
|
| Integration | Type | Path | Notes |
|
|
|-------------|------|------|-------|
|
|
| **Corgi** | Offline DB | `integrations/corgi/` | WMI database for brand ID |
|
|
| **PL24** | REST API + HTML scraper | `integrations/pl24/` | Multi-brand catalog; P5 (REST) + P4 Legacy (HTML). Services: `pl24.service.ts`, `pl24-ford-legacy.service.ts` |
|
|
| **PartsCatalogs** | REST API + Playwright JWT | `integrations/parts-catalogs/` | Broad VIN coverage; JWT captured via Playwright from partner sites; IP-bound via DataImpulse proxy |
|
|
| **EMEX** | Browser scraper | `integrations/emex/` | Playwright-based (emexdwc.ae), async via BullMQ |
|
|
| **VIN-API** | REST API | `integrations/vin-api/` | NHTSA VIN decoder (last-resort fallback) |
|
|
|
|
## Frontend Routes
|
|
|
|
**Public:** `/`, `/pricing`, `/about`, `/contact`, `/blog`, `/demo`, `/privacy`, `/terms`, `/kvkk`
|
|
|
|
**Auth (layout `_auth.tsx`):** `/login`, `/register`, `/forgot-password`, `/reset-password`
|
|
|
|
**Dashboard (protected, layout `dashboard.tsx`):**
|
|
- `/dashboard` — Home
|
|
- `/dashboard/search` — VIN decode input
|
|
- `/dashboard/history` — Past VIN searches
|
|
- `/dashboard/subscription` — Plan/brand selection
|
|
- `/dashboard/subscription/pay` — Payment (Iyzico or EFT)
|
|
- `/dashboard/billing` — Payment history
|
|
- `/dashboard/settings` — Profile, Security, Connections, Referral tabs
|
|
- `/dashboard/vehicles/$id` — Vehicle details
|
|
- `/dashboard/vehicles/$id/categories/$categoryId` — Interactive schema + parts
|
|
|
|
**Catalog Browser (VIN-less, protected):**
|
|
- `/dashboard/catalog` — Brand grid with access flags
|
|
- `/dashboard/catalog/$brandName` — Model list (from PL24)
|
|
- `/dashboard/catalog/$brandName/$modelId` — Category tree/grid view
|
|
- `/dashboard/catalog/$brandName/$modelId/categories/$categoryId` — Sub-categories or schema+parts
|
|
|
|
**Admin (role-based):**
|
|
- `/dashboard/admin` — Stats + charts
|
|
- `/dashboard/admin/users` — User management
|
|
- `/dashboard/admin/payments` — EFT approval workflow
|
|
- `/dashboard/admin/referrals` — Referral tracking
|
|
- `/dashboard/admin/analytics` — Daily query stats
|
|
- `/dashboard/admin/copy-logs` — OEM code copy tracking
|
|
|
|
## Auth & Test Credentials
|
|
|
|
- **Admin:** `admin@sase.tr` / `Sase2026`
|
|
- **Login:** `POST /api/auth/sign-in/email` (returns session cookie)
|
|
- **Cookie:** `better-auth.session_token` (or `__Secure-` prefix with HTTPS)
|
|
- **Test VIN:** VW `WVWZZZ1JZ3W597935`
|
|
|
|
## Environment Variables
|
|
|
|
**Required:** `DATABASE_URL`, `REDIS_PASSWORD`, `BETTER_AUTH_SECRET` (min 32 chars), `BETTER_AUTH_URL`, `MINIO_ENDPOINT`, `MINIO_ACCESS_KEY`, `MINIO_SECRET_KEY`, `MINIO_PUBLIC_URL`, `CORS_ORIGIN`
|
|
|
|
**Optional (grouped):**
|
|
- `PORT` (4000), `REDIS_HOST` (127.0.0.1), `REDIS_PORT` (6379), `MINIO_BUCKET_NAME` (sase-schemas), `MINIO_USE_SSL` (false)
|
|
- `GOOGLE_CLIENT_ID/SECRET` — Google OAuth
|
|
- `IYZICO_API_KEY/SECRET_KEY/BASE_URL` — Payment processing
|
|
- `PL24_BASE_URL/COMPANY_CODE/USERNAME/PASSWORD` — PL24 catalog API
|
|
- `EMEX_USERNAME/PASSWORD` — EMEX scraper
|
|
- `PCAT_USE_PROXY` (true), `PCAT_PROXY_HOST` (gw.dataimpulse.com), `PCAT_PROXY_USER/PASS` — PartsCatalogs proxy
|
|
- `POSTAL_API_URL/API_KEY`, `POSTAL_FROM_ADDRESS` (noreply@sase.tr), `POSTAL_FROM_NAME` (Sase.tr) — Email
|
|
- `OTEL_ENABLED` (false), `OTEL_EXPORTER_OTLP_ENDPOINT`, `OTEL_SERVICE_NAME` (sase-api), `OTEL_TRACE_SAMPLE_RATE` (1.0) — OpenTelemetry
|
|
- `ML_PREDICTION_ENABLED` (false)
|
|
|
|
Full schema: `packages/config/src/index.ts`
|
|
|
|
## Common Gotchas
|
|
|
|
- **Frontend is Vite + TanStack Router** — NOT Next.js
|
|
- **Categories & Parts tables have dual FKs:** `vehicleId` (VIN-based, nullable) and `catalogVehicleId` (VIN-less, nullable) — always check which context you're in
|
|
- **PartsCatalogs JWT is IP-bound** via DataImpulse proxy; the auth service manages a warm pool of JWTs using Playwright
|
|
- **P4 Legacy brands have no REST API** — Ford, PSA, Hyundai/Kia, Nissan, Opel, Volvo all use HTML scraping through `PL24FordLegacyService`
|
|
- **`catalogVehicles` unique key** is `(source, serviceVehicleId)` — not by VIN (these vehicles may not have VINs)
|
|
- **`routeTree.gen.ts`** is auto-generated by TanStack Router — never edit manually
|
|
- **DrizzleExceptionFilter** catches unique constraint violations → 409 Conflict
|
|
- **File uploads:** PNG/JPG/PDF only, max 5MB (middleware in `main.ts`)
|
|
- **Env validation** uses Zod from `@sase/config` — app won't start if env vars invalid
|
|
- **Playwright (EMEX/PartsCatalogs):** `waitUntil: 'networkidle'` (not `networkidle2`); `page.context().cookies()` (not `page.cookies()`)
|
|
- **VIN decode may return candidates** if PartsCatalogs finds multiple matches — frontend shows selection modal
|
|
|
|
## ast-grep — Structural Code Search & Refactoring
|
|
|
|
ast-grep (`sg`) does AST-aware pattern matching — finds code by structure, not text. Unlike `grep`, it understands syntax so `foo( bar )` and `foo(bar)` both match the pattern `foo($X)`.
|
|
|
|
**Install:** `npm i -g @ast-grep/cli` (already installed globally)
|
|
|
|
### Pattern syntax
|
|
|
|
| Syntax | Meaning |
|
|
|--------|---------|
|
|
| `$VAR` | Matches any **single** AST node (expression, identifier, etc.) |
|
|
| `$$$ARGS` | Matches **zero or more** nodes (variadic — use for argument lists, statements) |
|
|
| Literal code | Matches exact syntax structure |
|
|
|
|
### Common commands
|
|
|
|
```bash
|
|
# Search by pattern in TypeScript files
|
|
ast-grep -p 'console.log($$$)' -l ts apps/
|
|
|
|
# Search and preview rewrite (no changes yet)
|
|
ast-grep -p '$A && $A()' --rewrite '$A?.()' -l ts apps/
|
|
|
|
# Interactive rewrite — confirm each change
|
|
ast-grep -p '$A && $A()' --rewrite '$A?.()' --interactive -l ts apps/
|
|
|
|
# Apply all rewrites without confirmation
|
|
ast-grep -p '$A && $A()' --rewrite '$A?.()' --update-all -l ts apps/
|
|
|
|
# Output matches as JSON (useful for scripting)
|
|
ast-grep -p 'useQuery($$$)' -l tsx --json apps/web/src/
|
|
|
|
# Show surrounding context lines
|
|
ast-grep -p 'db.select()' -l ts -C 3 apps/api/src/
|
|
|
|
# Limit to specific file globs
|
|
ast-grep -p '@Public()' -l ts --globs 'apps/api/src/**/*.controller.ts' .
|
|
```
|
|
|
|
### Language flags for this project
|
|
|
|
| Flag | Use for |
|
|
|------|---------|
|
|
| `-l ts` | API services, guards, modules, DTOs |
|
|
| `-l tsx` | React components, route files |
|
|
| `-l json` | i18n message files |
|
|
|
|
### Useful patterns for this codebase
|
|
|
|
```bash
|
|
# Find all @Public() decorated endpoints
|
|
ast-grep -p '@Public()' -l ts apps/api/src/
|
|
|
|
# Find all useQuery calls (TanStack Query)
|
|
ast-grep -p 'useQuery({$$$})' -l tsx apps/web/src/
|
|
|
|
# Find Drizzle inserts
|
|
ast-grep -p 'db.insert($TABLE).values($$$)' -l ts apps/api/src/
|
|
|
|
# Find all Redis cache sets
|
|
ast-grep -p 'this.redis.set($$$)' -l ts apps/api/src/
|
|
|
|
# Find t() translation calls missing a key
|
|
ast-grep -p 't($KEY)' -l tsx apps/web/src/
|
|
|
|
# Find all BullMQ queue.add() calls
|
|
ast-grep -p '$QUEUE.add($$$)' -l ts apps/api/src/jobs/
|
|
```
|
|
|
|
## Documentation
|
|
|
|
- **Project index:** `docs/INDEX.md` (comprehensive — routes, API endpoints, DB schema, components)
|
|
- **Marketing context:** `.claude/product-marketing-context.md`
|
|
- **Detailed docs:** `docs/00-overview.md` through `docs/13-analytics-posthog.md`
|
|
- **Memory:** `.claude/projects/-home-s-ss/memory/MEMORY.md`
|