bilhej/AGENTS.md
Joakim Mörling afa552e18b
Some checks failed
CI / Lint, type check, unit tests, coverage (pull_request) Successful in 2m7s
CI / E2E browser tests (pull_request) Failing after 1m17s
Run Mailpit E2E specs serially to stop flakes.
account-settings and password-reset called clearMailpit in parallel with
other tests, wiping emails before waitForEmailChangeToken could read them.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-27 13:24:26 +02:00

12 KiB

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)

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

./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)

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)

./gradlew :backend:bootRun    # dev server on :8080
./gradlew :backend:test       # JUnit 5 + Mockito (backend only)

Stripe webhooks (local testing)

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.

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):

# 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):

# 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

./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.