# AGENTS.md – ERP Nutzfahrzeuge Implementation Guide **Version:** 1.0.0 **Datum:** 2026-07-12 --- ## 1. Project Overview ERP-System für Nutzfahrzeug-/Baumaschinen-Handel (~10 Nutzer). - **Backend:** Python 3.12 / FastAPI / SQLAlchemy 2.0 async / PostgreSQL 16 / Redis 7 - **Frontend:** Next.js 14 (App Router) / React 18 / TypeScript / Tailwind CSS / next-intl - **Hosting:** Coolify (Docker Compose) auf coolify-01 (46.225.91.159) - **KI:** OpenRouter (Qwen2.5-VL OCR, Flux.1-Pro Bild, Claude/GPT-4 Copilot) - **External:** mobile.de Seller API (Push-Only) ### Architecture Reference - `docs/architecture.md` – Complete architecture, DB schema, API design, ADRs - `docs/task_graph.json` – Task breakdown with test specs - `docs/requirements.md` – Full requirements (8 modules, 23 features) - `docs/component_inventory.md` – UI components and states --- ## 2. Build & Test Commands ### Backend ```bash # Install dependencies cd backend pip install -r requirements.txt # Run dev server uvicorn app.main:app --reload --host 0.0.0.0 --port 8000 # Run all tests python -m pytest tests/ -v --cov=app --cov-report=term-missing --cov-report=html # Run specific test file python -m pytest tests/test_vehicles.py -v # Run with coverage for specific module python -m pytest tests/test_auth.py -v --cov=app/services/auth_service --cov-report=term-missing # Lint ruff check app/ ruff format app/ --check # Database migrations alembic revision --autogenerate -m "description" alembic upgrade head alembic downgrade -1 # Type check (optional but recommended) mypy app/ --ignore-missing-imports ``` ### Frontend ```bash cd frontend npm install # Run dev server npm run dev # Run all tests npx vitest run --coverage # Run specific test npx vitest run src/components/vehicles --coverage # Lint npx eslint src/ --max-warnings 0 npx prettier --check src/ # Build npm run build # Type check npx tsc --noEmit ``` ### Docker ```bash # Build and start all services docker-compose up -d --build # View logs docker-compose logs -f backend docker-compose logs -f frontend # Run database migration in container docker-compose exec backend alembic upgrade head # Run tests in container docker-compose exec backend python -m pytest tests/ -v ``` --- ## 3. Test Rules (MANDATORY) ### TDD Principle - **Write tests first or alongside implementation** – never after - Every PR must include tests - Coverage target: **≥80%** per module ### Do NOT Modify Existing Tests - Tests are written by the task spec and must not be modified to make them pass - If a test fails, fix the **implementation**, not the test - Exception: test data fixtures can be extended, but existing assertions must not be weakened ### Backend Test Conventions - **Framework:** pytest + pytest-asyncio + httpx.AsyncClient - **Test DB:** Separate PostgreSQL database `erp_test` (NOT SQLite) - **Fixtures:** `conftest.py` provides: - `async_db` – async SQLAlchemy session (rolled back after each test) - `client` – httpx AsyncClient with app - `auth_headers` – JWT headers for admin/verkaeufer/buchhaltung roles - `seed_data` – minimal seed data (users, vehicle, contact) - **Mocking:** OpenRouter API MUST be mocked in all tests (no real API calls) - **Naming:** `test__.py` or `test_.py` ### Frontend Test Conventions - **Framework:** Vitest + React Testing Library - **E2E:** Playwright for critical paths (login, vehicle create, sale wizard) - **Mocking:** API calls mocked via `vi.mock()` or MSW (Mock Service Worker) - **Naming:** `ComponentName.test.tsx` in `__tests__/` folder ### Test Spec Compliance Every task in `task_graph.json` has a `test_spec` with: - `commands` – Exact commands to run - `expected_results` – What success looks like - `test_files` – Expected test file paths - `coverage_target` – Minimum coverage percentage **ALL commands in test_spec.commands MUST pass before a task is considered done.** --- ## 4. Code Conventions ### Python (Backend) ```python # Naming snake_case for variables, functions, methods, modules PascalCase for classes (Models, Schemas, Services) UPPER_SNAKE_CASE for constants # Imports (ruff isort) # Standard library import os from datetime import datetime # Third party from fastapi import APIRouter, Depends, HTTPException from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession # Local from app.deps import get_db, get_current_user from app.models.vehicle import Vehicle from app.schemas.vehicle import VehicleCreate, VehicleResponse # Type hints (mandatory) async def get_vehicle(vehicle_id: UUID, db: AsyncSession) -> Vehicle: ... # Async everywhere (SQLAlchemy 2.0 async) result = await db.execute(select(Vehicle).where(Vehicle.id == vehicle_id)) vehicle = result.scalar_one_or_none() # Error handling if vehicle is None: raise HTTPException(status_code=404, detail={"error": {"code": "NOT_FOUND", "message": "Vehicle not found"}}) ``` ### TypeScript (Frontend) ```typescript // Naming camelCase for variables, functions, hooks PascalCase for components, types, interfaces UPPER_SNAKE_CASE for constants // Imports order // 1. React/Next import { useState, useEffect } from 'react'; import { useRouter } from 'next/navigation'; // 2. Third party import { useTranslations } from 'next-intl'; // 3. Local import { Button } from '@/components/ui/Button'; import { apiClient } from '@/lib/api-client'; // Components: function declaration with explicit return type export function FahrzeugListe(): JSX.Element { ... } // Types: interface for props type FahrzeugListeProps = { vehicles: Vehicle[]; isLoading: boolean; }; ``` ### File Organization - **One model per file** in `models/` - **One router per module** in `routers/` - **One service per domain** in `services/` - **One schema set per module** in `schemas/` - **Components grouped by feature** in `components//` --- ## 5. Forbidden Patterns ### Backend - ❌ **No raw SQL queries** – always use SQLAlchemy ORM - ❌ **No synchronous database calls** – always async (`await db.execute(...)`) - ❌ **No secrets in code** – use environment variables / pydantic-settings - ❌ **No `print()` in production code** – use Python `logging` - ❌ **No bare `except:`** – always catch specific exceptions - ❌ **No `# type: ignore`** without comment explaining why - ❌ **No circular imports** – use dependency injection - ❌ **No business logic in routers** – routers only validate + call service - ❌ **No direct model access from routers** – always go through service layer - ❌ **No unencrypted PII** – Ausweisdaten MUST be AES-256 encrypted - ❌ **No real OpenRouter API calls in tests** – always mock - ❌ **No `commit()` in services without explicit reason** – let caller control transactions ### Frontend - ❌ **No `any` type** – use proper TypeScript types - ❌ **No `console.log` in production** – use a logger utility - ❌ **No inline styles** – use Tailwind classes or CSS modules - ❌ **No direct `fetch()` without auth wrapper** – use `apiClient` - ❌ **No hardcoded API URLs** – use `NEXT_PUBLIC_API_URL` env var - ❌ **No hardcoded translation strings** – use `t('key')` from next-intl - ❌ **No prop drilling > 2 levels** – use context or state management - ❌ **No `useEffect` for data fetching without loading/error states** - ❌ **No forms without validation** – all inputs must have validation - ❌ **No missing `aria-label`** on icon-only buttons ### General - ❌ **No commits to main without passing tests** - ❌ **No large PRs** – max 800 lines per task - ❌ **No TODO comments without ticket reference** - ❌ **No commented-out code in PRs** - ❌ **No files > 500 lines** – split into modules --- ## 6. Token Rule for File Operations When working with files (reading, creating, updating): ### Backend (Forgejo API) ```json // READ file (only for MODIFY, not for reference) {"tool_name":"forgejo","tool_args":{"action":"files_get","owner":"Leopoldadmin","repo":"erp-nutzfahrzeuge","path":"backend/app/models/vehicle.py"}} // CREATE new file {"tool_name":"forgejo","tool_args":{"action":"files_create","owner":"Leopoldadmin","repo":"erp-nutzfahrzeuge","path":"backend/app/models/vehicle.py","content":"","message":"Add vehicle model","branch":"main"}} // UPDATE existing file (requires SHA from files_get) {"tool_name":"forgejo","tool_args":{"action":"files_update","owner":"Leopoldadmin","repo":"erp-nutzfahrzeuge","path":"backend/app/models/vehicle.py","content":"","sha":"","message":"Update vehicle model","branch":"main"}} // CHECK file existence {"tool_name":"forgejo","tool_args":{"action":"files_list","owner":"Leopoldadmin","repo":"erp-nutzfahrzeuge","path":"backend/app/models/"}} ``` ### Rules - Use `files_get` ONLY when you need to modify an existing file (need SHA) - Use `files_create` for new files - Use `files_list` to check existence before creating - **NEVER** copy file contents inline in task descriptions – reference by path - **NEVER** read entire files just for reference – use `curl + sed` for snippets - For large reference content (architecture.md, requirements.md): reference by path, don't inline --- ## 7. Task Execution Workflow ### For implementation_engineer 1. **Read the task** from `task_graph.json` (task ID: T0X) 2. **Read architecture.md** sections relevant to the task (DB schema, API design, ADRs) 3. **Read requirements.md** for the specific feature IDs listed in `requirement_ids` 4. **Implement backend first:** Model → Schema → Service → Router → Tests 5. **Implement frontend:** Components → Pages → Tests 6. **Run ALL test_spec.commands** and ensure they pass 7. **Run lint** (ruff for backend, eslint for frontend) 8. **Run build** (npm run build for frontend) 9. **Report results** with test output evidence (not just "done") ### Evidence Requirements (MANDATORY) - Test command output showing pass/fail counts - Coverage report showing ≥80% - Lint output showing 0 errors - Build output showing success - "File written" is NOT evidence. "Commit made" is NOT evidence. - Test output with pass counts IS evidence. ### Task Completion Checklist - [ ] All acceptance_criteria from task_graph.json verified - [ ] All test_spec.commands pass - [ ] Coverage ≥ coverage_target for covered modules - [ ] Ruff lint clean (backend) - [ ] ESLint clean (frontend) - [ ] Build succeeds (frontend) - [ ] No forbidden patterns introduced - [ ] Test evidence provided in report --- ## 8. Environment Variables (Names Only) ### Backend ``` DATABASE_URL=postgresql+asyncpg://erp_user:${DB_PASSWORD}@postgres:5432/erp_db REDIS_URL=redis://redis:6379/0 JWT_SECRET=${JWT_SECRET} JWT_ALGORITHM=HS256 JWT_ACCESS_TTL_MINUTES=15 JWT_REFRESH_TTL_DAYS=7 ENCRYPTION_KEY=${ENCRYPTION_KEY} OPENROUTER_API_KEY=${OPENROUTER_API_KEY} MOBILE_DE_API_KEY=${MOBILE_DE_API_KEY} MOBILE_DE_SELLER_ID=${MOBILE_DE_SELLER_ID} UPLOAD_DIR=/data/uploads MAX_FILE_SIZE_MB=50 CORS_ORIGINS=https://erp.domain.tld ``` ### Frontend ``` NEXT_PUBLIC_API_URL=http://backend:8000/api/v1 NEXTAUTH_SECRET=${NEXTAUTH_SECRET} ``` --- ## 9. Git Workflow ### Branches - `main` – Production branch, deployed via Coolify - `feature/T0X-` – Feature branch per task - `fix/T0X-` – Fix branch ### Commit Messages ``` feat(T02): add vehicle CRUD with type-specific fields fix(T02): correct FIN validation for 17-char check test(T02): add mobile.de batch push tests refactor(T02): extract field mapping to separate function docs(T02): update vehicle API documentation ``` ### PR Process 1. Create feature branch from main 2. Implement + test + lint + build 3. Commit with conventional commit format 4. Push branch 5. Create PR with task ID reference 6. CI runs: pytest, ruff, vitest, eslint, build 7. Merge after CI passes --- ## 10. Database Migration Rules - **Always use Alembic** for schema changes - **Never** modify database directly (psql, pgadmin) - **One migration per schema change** – don't bundle multiple changes - **Test migration up AND down** before committing - **Seed data:** Use a separate seed script (`scripts/seed.py`), not migrations - **Cleanup after migration tasks:** ```bash alembic upgrade head # Apply # Verify all tables created python -c "from app.database import engine; from sqlalchemy import inspect; insp = inspect(engine); print(insp.get_table_names())" # Run seed data python scripts/seed.py # Verify foreign keys docker-compose exec postgres psql -U erp_user -d erp_db -c "\d vehicles" ``` --- ## 11. OpenRouter Integration Rules - **Always use** `openrouter_client.py` shared client – never call OpenRouter API directly - **Always set timeout** to 30 seconds - **Always log** interaction to `ai_interactions` table (type, model, tokens, metadata) - **Never send** Ausweisdaten or personal customer data to OpenRouter - **Mock OpenRouter** in all automated tests - **Handle errors gracefully:** API error → 502, timeout → 504, rate limit → 429 - **Model selection** via config, not hardcoded in service --- ## 12. Security Checklist - [ ] JWT authentication on all endpoints (except /auth/login, /auth/refresh, /health) - [ ] RBAC enforced (require_role decorator on every router) - [ ] Input validation via Pydantic schemas on all endpoints - [ ] SQL injection protection via SQLAlchemy ORM (no raw SQL) - [ ] File upload: MIME-type allowlist + size limit - [ ] Ausweisdaten: AES-256-GCM encryption - [ ] CORS: only configured origins - [ ] Audit log: all create/update/delete actions logged - [ ] No secrets in code or git - [ ] HTTPS via Traefik/Let's Encrypt - [ ] DSGVO: no PII to OpenRouter, soft-delete with retention concept