Document that ./gradlew check must pass before every commit. Add scripts to run the same verification as CI and optionally install a git hook. Co-authored-by: Cursor <cursoragent@cursor.com>
324 lines
13 KiB
Markdown
324 lines
13 KiB
Markdown
# AGENTS.md — BilHej / Bilhälsning.se
|
|
|
|
Project-specific instructions for OpenCode. Commit this file.
|
|
|
|
---
|
|
|
|
## Project Identity
|
|
|
|
BilHej is a web platform letting Swedish residents send physical letters to
|
|
vehicle owners by entering a registration number. The sender composes a letter,
|
|
pays 49 SEK, and BilHej prints+mails it via PostNord. The sender never sees
|
|
the recipient's name or address.
|
|
|
|
**Phase 0 (current):** Manual workflow. No Transportstyrelsen or PostNord API
|
|
integration yet. Owner address is obtained manually by a human and entered into
|
|
the admin panel.
|
|
|
|
Tech stack: Vue.js 3 (Vite, Pinia) frontend + Java 21 Spring Boot 4 backend +
|
|
PostgreSQL 16. Deployed via Docker Compose.
|
|
|
|
---
|
|
|
|
## Build, Lint, Test & Run Commands
|
|
|
|
Always run these after making changes to verify nothing is broken.
|
|
|
|
Gradle lives at repo root. All commands below run from the repo root unless noted.
|
|
|
|
### Quick start (everything)
|
|
|
|
```bash
|
|
cp .env.example .env # first time only, then fill in keys
|
|
docker compose up -d # starts postgres, backend, frontend
|
|
./gradlew up # same as above (Gradle wrapper)
|
|
```
|
|
|
|
### All-in-one
|
|
|
|
```bash
|
|
./gradlew check # frontend lint → frontend test → backend test+coverage → E2E (Docker)
|
|
./gradlew coverage # backend + frontend tests with coverage reports
|
|
./gradlew up # docker compose up -d
|
|
./gradlew down # docker compose down
|
|
./gradlew reset # docker compose down -v && docker compose up -d (full DB reset)
|
|
```
|
|
|
|
### Frontend (Vue.js 3 + Vite)
|
|
|
|
```bash
|
|
cd frontend
|
|
npm install # first time only
|
|
npm run dev # dev server on :3000 with HMR
|
|
npm run build # production build
|
|
npm run lint # ESLint
|
|
npm run test # vitest
|
|
npm run test:coverage # vitest with coverage (HTML at frontend/coverage/)
|
|
```
|
|
|
|
### Backend (Spring Boot 4 + Java 21)
|
|
|
|
```bash
|
|
./gradlew :backend:bootRun # dev server on :8080
|
|
./gradlew :backend:test # JUnit 5 + Mockito (backend only)
|
|
```
|
|
|
|
### Stripe webhooks (local testing)
|
|
|
|
```bash
|
|
stripe listen --forward-to localhost:8080/api/webhooks/stripe
|
|
```
|
|
|
|
### Database
|
|
|
|
Flyway migrations run automatically on Spring Boot startup. Migration files
|
|
live in `backend/src/main/resources/db/migration/`. Naming: `V<number>__descriptive_name.sql`.
|
|
|
|
**Before adding a migration:** run `./scripts/next-flyway-version.sh` and use that
|
|
version. Never reuse a version number already on `master`. Never edit a migration
|
|
after it has merged — add a new higher version instead. CI runs
|
|
`scripts/check-flyway-migrations.sh` against `origin/master`.
|
|
|
|
If local dev Postgres fails with Flyway checksum / “migration not resolved locally”
|
|
after switching branches, run `./gradlew reset` (wipes the Docker DB volume).
|
|
|
|
To reset: `docker compose down -v && docker compose up -d`.
|
|
|
|
Flyway schema migrations live in `db/migration/`; dev-only seeds (test users,
|
|
sample orders) are in `db/dev-migration/` and run only without the `prod` profile.
|
|
Production admin is created from `ADMIN_EMAIL` / `ADMIN_PASSWORD` on first boot.
|
|
|
|
---
|
|
|
|
## Project Structure
|
|
|
|
```
|
|
bilhej/
|
|
├── frontend/ # Vue.js 3 SPA (Vite)
|
|
│ ├── src/
|
|
│ │ ├── pages/ # Route-level page components
|
|
│ │ ├── components/ # Reusable UI components
|
|
│ │ ├── composables/ # useXxx.ts shared logic
|
|
│ │ ├── stores/ # Pinia stores
|
|
│ │ ├── api/ # API client modules
|
|
│ │ ├── router/ # Vue Router config
|
|
│ │ └── assets/ # Static files, CSS
|
|
│ └── ...
|
|
├── backend/ # Spring Boot 4 (Java 21) — Gradle subproject
|
|
│ ├── build.gradle # Spring Boot plugin, Java deps, test config
|
|
│ ├── src/main/java/se/bilhalsning/
|
|
│ │ ├── config/ # @Configuration classes
|
|
│ │ ├── controller/ # REST controllers
|
|
│ │ ├── dto/ # Request/response DTOs
|
|
│ │ ├── entity/ # JPA entities
|
|
│ │ ├── repository/ # Spring Data JPA repos
|
|
│ │ ├── service/ # Business logic
|
|
│ │ ├── security/ # JWT filter, UserDetailsService
|
|
│ │ ├── exception/ # Custom exceptions + @ControllerAdvice
|
|
│ │ └── mapper/ # Entity ↔ DTO mapping
|
|
│ └── src/main/resources/
|
|
│ ├── application.yml # default (H2, IDE dev)
|
|
│ ├── application-docker.yml # docker profile (PostgreSQL)
|
|
│ └── db/migration/ # Flyway migrations
|
|
├── docker/ # Dockerfiles
|
|
│ ├── backend.Dockerfile # dev: JDK 21 + gradle :backend:bootRun
|
|
│ ├── backend.prod.Dockerfile # prod: multi-stage (Gradle build → JRE Alpine, non-root)
|
|
│ ├── frontend.Dockerfile # dev: Node 24 + vite dev server
|
|
│ ├── frontend.prod.Dockerfile # prod: multi-stage (Node build → nginx)
|
|
│ ├── nginx.conf # prod: SPA fallback + /api reverse proxy
|
|
│ └── entrypoint.sh # prod: self-signed cert generation
|
|
├── docker-compose.yml # dev: postgres + backend (bootRun) + frontend (Vite HMR)
|
|
├── docker-compose.prod.yml # prod: multi-stage images, no source mounts, restart always
|
|
├── gradlew # Gradle wrapper (repo root)
|
|
├── gradle/
|
|
│ └── wrapper/
|
|
├── settings.gradle # rootProject.name + include 'backend'
|
|
├── build.gradle # convenience tasks: check, up, down, reset
|
|
├── .env.example
|
|
├── AGENTS.md # This file
|
|
├── README.md
|
|
├── REQUIREMENTS.md
|
|
└── CODING_GUIDELINES.md
|
|
```
|
|
|
|
---
|
|
|
|
## Conventions (Summary)
|
|
|
|
Full details in `@CODING_GUIDELINES.md`. Key rules:
|
|
|
|
### Both sides
|
|
- Code and comments in English. User-facing strings in Swedish.
|
|
- No commented-out code. Delete it.
|
|
- Functions stay small (<30 lines).
|
|
|
|
### Git
|
|
- Create `feature/*`, `fix/*`, or `chore/*` branches from `develop`.
|
|
- Never commit directly to `master` or `develop`.
|
|
- Merge strategy: fast-forward or merge — either is fine.
|
|
- Commit messages must be thorough: describe what changed, why, and
|
|
list concrete changes as bullet points. Never write single-line
|
|
"feat: add X" messages.
|
|
|
|
**Before every commit (mandatory — agents must not skip):**
|
|
|
|
```bash
|
|
# from repo root; needs Docker running
|
|
export POSTGRES_DB=bilhej POSTGRES_USER=bilhej POSTGRES_PASSWORD=test_pw_ci_123
|
|
export JWT_SECRET=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
|
export STRIPE_SECRET_KEY=sk_test_fake STRIPE_WEBHOOK_SECRET=whsec_fake STRIPE_PRICE_ID=price_fake
|
|
./gradlew check
|
|
```
|
|
|
|
This runs frontend lint, frontend unit tests (242), backend tests (163), coverage
|
|
thresholds, Flyway checks, and **all 90 E2E tests in Docker**. **Do not commit or
|
|
push if this fails.** Optional local guard: `./scripts/install-pre-commit-hook.sh`
|
|
(runs the same `check` on every `git commit`).
|
|
|
|
### Frontend (Vue.js 3)
|
|
- `<script setup>` with Composition API only. Never Options API.
|
|
- File naming: PascalCase for pages/components, camelCase (`useXxx`) for composables.
|
|
- API calls live in `api/` modules, never in components.
|
|
- Component styles are scoped.
|
|
|
|
### Backend (Spring Boot 4)
|
|
- Constructor injection with `@RequiredArgsConstructor`. No `@Autowired`.
|
|
- DTOs: prefer Java records. No bare entities in responses.
|
|
- Controllers stay thin. All logic in services.
|
|
- Use `@ControllerAdvice` for consistent error responses (`{ "message": "..." }`).
|
|
- Lombok: `@RequiredArgsConstructor`, `@Getter`, `@Setter`, `@NoArgsConstructor` are all fine. Prefer records for DTOs.
|
|
|
|
### Database
|
|
- Table names: snake_case, plural. PKs: UUID, generated in code.
|
|
- Timestamps: `created_at`, `updated_at`. Use `Instant` in Java.
|
|
- Enums: stored as VARCHAR.
|
|
- Index every FK and every column used in WHERE.
|
|
|
|
---
|
|
|
|
## Critical Gotchas
|
|
|
|
### Phase 0: Address lookup is MANUAL
|
|
There is no Transportstyrelsen API integration yet. When an order is paid, a
|
|
human must manually request the owner address via the "Fråga om fordonsägare"
|
|
form and update the admin panel. Do NOT write code that calls an API that
|
|
doesn't exist yet. The backend should store the order and wait for an admin
|
|
to mark it as processed.
|
|
|
|
### Never store recipient addresses
|
|
After the address is used to mail the letter, it must be deleted. The Order
|
|
entity must NOT have an address field. The address lookup and mailing are
|
|
external/human processes in Phase 0.
|
|
|
|
### E2E must use Docker (not host Playwright)
|
|
See **Testing Approach → E2E (Playwright) — Docker only** above. Do not run `npx playwright install` or `npm run test:e2e` on the host when verifying E2E.
|
|
|
|
### Local email (Mailpit)
|
|
`docker compose up` includes Mailpit (`ghcr.io/axllent/mailpit:v1.28`); password-reset mail appears at http://localhost:8025. E2E verifies SMTP via Mailpit API (`frontend/e2e/helpers/mailpit.ts`). Production uses Resend SMTP—see docs/production-email-checklist.md.
|
|
|
|
### Password reset test token (never in production)
|
|
`app.password-reset.expose-token` must stay **false** in prod/default; it is only enabled in `application-docker.yml` for CI E2E so Playwright can read `testToken` from the forgot-password response.
|
|
|
|
### Stripe webhook signature verification
|
|
Always verify `stripe-signature` header using `STRIPE_WEBHOOK_SECRET`.
|
|
Webhook endpoint is public (no auth). Without signature verification an
|
|
attacker could mark orders as paid.
|
|
|
|
### Swedish UI strings
|
|
All text visible to end users must be in Swedish. Button labels, error
|
|
messages, validation text, email content, template bodies. Only developer
|
|
facing content is in English.
|
|
|
|
### JWT in Authorization header
|
|
Backend expects `Authorization: Bearer <token>`. Frontend interceptor must
|
|
attach this to all API calls. Unauthorized APIs (register, login, webhook,
|
|
public vehicle info) must be excluded from the Spring Security filter chain.
|
|
|
|
---
|
|
|
|
## Testing Approach
|
|
|
|
This project follows **Test-Driven Development (TDD)**. Write tests before
|
|
or alongside implementation. Every feature ticket should include tests in
|
|
the same PR — never merge code without corresponding tests.
|
|
|
|
### Backend
|
|
- JUnit 5 + Mockito for service layer tests.
|
|
- `@WebMvcTest` for controller tests.
|
|
- Test naming: `shouldXxxWhenYyy`.
|
|
- Use test profile with H2 or Testcontainers for DB-dependent tests.
|
|
- Flyway migrations must run in test profile too.
|
|
|
|
### Frontend
|
|
- Vitest for composables and utility functions.
|
|
- Component tests with Vue Test Utils where needed.
|
|
- E2E tests with Playwright in `frontend/e2e/`.
|
|
|
|
### E2E (Playwright) — **Docker only**
|
|
|
|
**Agents and humans: never run Playwright on the host.**
|
|
|
|
| Do **not** run | Why |
|
|
|----------------|-----|
|
|
| `npx playwright test` | Wrong environment; needs Docker stack |
|
|
| `npm run test:e2e` | Same — host Playwright, not supported for agents |
|
|
| `npx playwright install` | Do not install browsers on the host; the Playwright image already includes them |
|
|
|
|
**Always use Docker** (`docker-compose.e2e.yml`): isolated postgres (tmpfs), backend, frontend, Mailpit, and the official Playwright container on the `e2e` network (`PLAYWRIGHT_BASE_URL=http://frontend`).
|
|
|
|
**Full E2E suite** (same as Forgejo CI / `./gradlew check`):
|
|
|
|
```bash
|
|
# from repo root — set env (or use .env; see .env.example)
|
|
cd frontend && npm run test:e2e:ci
|
|
# equivalent:
|
|
./gradlew frontendE2E
|
|
```
|
|
|
|
**Single spec or project** (stack must be reachable on the `e2e` network):
|
|
|
|
```bash
|
|
# from repo root, after exporting the same vars as frontendE2E / .env
|
|
docker compose -f docker-compose.e2e.yml up -d --build postgres mailpit backend frontend
|
|
docker compose -f docker-compose.e2e.yml run --rm --build playwright \
|
|
sh -c 'npx playwright test admin-fulfillment.spec.ts --project=chromium-serial --reporter=list'
|
|
docker compose -f docker-compose.e2e.yml down
|
|
```
|
|
|
|
- Config: `frontend/playwright.config.ts`
|
|
- Tests: `frontend/e2e/*.spec.ts`
|
|
- Serial specs (shared Mailpit / DB state): `deferred-payment-admin`, `admin-fulfillment`, `account-settings`, `password-reset` — Playwright project `chromium-serial`, `workers: 1`
|
|
|
|
### CI (future)
|
|
- `./gradlew check` and `npm run test && npm run lint` must pass before merge.
|
|
|
|
---
|
|
|
|
## Coverage
|
|
|
|
```bash
|
|
./gradlew coverage # backend + frontend tests with coverage
|
|
```
|
|
|
|
Coverage thresholds are enforced during `./gradlew check`. PRs must maintain
|
|
or improve coverage.
|
|
|
|
| Layer | Lines | Branches | Functions |
|
|
|----------|-------|----------|-----------|
|
|
| Backend | 70% | 60% | — |
|
|
| Frontend | 70% | 60% | 70% |
|
|
|
|
HTML reports:
|
|
- Backend: `backend/build/reports/jacoco/index.html`
|
|
- Frontend: `frontend/coverage/index.html`
|
|
|
|
---
|
|
|
|
## External References
|
|
|
|
For detailed conventions, load `@CODING_GUIDELINES.md`.
|
|
For product requirements and business logic, load `@REQUIREMENTS.md`.
|
|
For setup and quick start, load `@README.md`.
|
|
|
|
These are lazy-loaded by OpenCode — only read them when the task at hand
|
|
needs the detail.
|