Files

413 lines
14 KiB
Markdown
Raw Permalink Normal View History

# 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_<feature>_<scenario>.py` or `test_<module>.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/<feature>/`
---
## 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":"<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":"<updated content>","sha":"<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-<short-name>` Feature branch per task
- `fix/T0X-<short-name>` 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