Bilhälsning.se — license plate letter service
Find a file
Joakim Mörling d27bde2fbe test: add PaymentControllerTest with 4 cases
- shouldReturn403WhenNotAuthenticated: verifies the endpoint requires
  a valid JWT token (anyRequest().authenticated() enforcement)
- shouldMarkOrderAsPaidSuccessfully: calls POST with @WithMockUser,
  verifies response includes id, status=paid, and amountPaid=49.00
- shouldReturn404WhenOrderNotFound: mocks service to throw
  OrderNotFoundException, expects 404 response
- Test helper creates minimal Order entity with explicitly set id,
  plate, status, and amountPaid for realistic response mapping
2026-05-15 20:30:02 +02:00
backend test: add PaymentControllerTest with 4 cases 2026-05-15 20:30:02 +02:00
docker refactor: move Gradle wrapper to repo root, add convenience tasks 2026-05-01 18:40:18 +02:00
frontend test: add tracking entry vitest and e2e tests, fix pre-existing flaky tests 2026-05-15 19:59:00 +02:00
gradle/wrapper refactor: move Gradle wrapper to repo root, add convenience tasks 2026-05-01 18:40:18 +02:00
.dockerignore chore: add Docker CI compose, Gradle E2E task, and .dockerignore 2026-05-13 19:17:55 +02:00
.env.example chore: add JWT secret env config, jjwt deps, and docker-compose prod fixes 2026-05-01 17:38:03 +02:00
.gitignore docs: update README for Gradle at root, add convenience task docs 2026-05-01 18:44:05 +02:00
AGENTS.md docs: add coverage thresholds, ./gradlew coverage, and LSP warning discipline 2026-05-15 12:16:16 +02:00
build.gradle chore: add root gradle coverage and frontendCoverage tasks 2026-05-15 12:16:04 +02:00
CODING_GUIDELINES.md docs: add coverage thresholds, ./gradlew coverage, and LSP warning discipline 2026-05-15 12:16:16 +02:00
docker-compose.ci.yml chore: add Docker CI compose, Gradle E2E task, and .dockerignore 2026-05-13 19:17:55 +02:00
docker-compose.prod.yml chore: add JWT secret env config, jjwt deps, and docker-compose prod fixes 2026-05-01 17:38:03 +02:00
docker-compose.yml chore: add Docker build volume and configure OpenCode 2026-05-14 12:39:34 +02:00
gradlew refactor: move Gradle wrapper to repo root, add convenience tasks 2026-05-01 18:40:18 +02:00
opencode.json chore: add Docker build volume and configure OpenCode 2026-05-14 12:39:34 +02:00
README.md docs: update README for Gradle at root, add convenience task docs 2026-05-01 18:44:05 +02:00
REQUIREMENTS.md docs: add TDD policy, update Spring Boot 4 references, configure OpenCode tools 2026-05-13 19:18:43 +02:00
settings.gradle refactor: move Gradle wrapper to repo root, add convenience tasks 2026-05-01 18:40:18 +02:00

BilHej / Bilhälsning.se

Send a physical letter to a Swedish car owner — just by knowing their license plate.

The user enters a registration number, composes a letter (from a template or free text), pays, and BilHej handles the rest: owner address lookup via Transportstyrelsen, printing and mailing via PostNord. The sender never sees the recipient's name or address.


Tech Stack

Layer Technology
Frontend Vue.js 3 (Composition API), Vite, Pinia
Backend Java 21, Spring Boot 4, Gradle
Database PostgreSQL 16
Auth Spring Security + JWT
Payments Stripe (cards + Swish)
Deployment Docker, Docker Compose

Prerequisites

  • Docker & Docker Compose
  • Java 21 (for local IDE development)
  • Node.js 20+ (for local frontend dev)
  • A Stripe account (test mode for development)

Quick Start

git clone <repo-url> bilhej
cd bilhej
cp .env.example .env          # fill in your keys
docker compose up -d          # or: ./gradlew up

The app will be available at:

  • Frontend: http://localhost:3000
  • Backend API: http://localhost:8080
  • PostgreSQL: localhost:5432

Architecture inside Docker Compose

 Browser                  Docker Compose network
 ───────                  ─────────────────────
 │                         ┌──────────────────┐
 │  http://localhost:3000  │  frontend (Vite)  │
 ├────────────────────────→│  :3000            │
 │                         │  proxy: /api →    │
 │  GET /api/orders        │  backend:8080     │
 │                         └────────┬─────────┘
 │                                  │
 │                         ┌────────▼─────────┐
 │                         │  backend (Spring) │
 │                         │  :8080            │
 │                         │  profile: docker  │
 │                         └────────┬─────────┘
 │                                  │
 │                         ┌────────▼─────────┐
 │                         │  postgres (16)    │
 │                         │  :5432            │
 │                         └──────────────────┘

Vite proxy: The Vite dev server proxies /api/* requests to the backend container. No CORS configuration needed in development — the browser never calls the backend directly.

Spring profiles:

Profile Datasource Use
default H2 in-memory Local IDE dev (./gradlew :backend:bootRun)
docker PostgreSQL via docker-compose.yml Docker Compose dev
prod PostgreSQL (production config) Deploy (docker-compose.prod.yml)

Environment Variables

Copy .env.example to .env and fill in:

Variable Description
POSTGRES_DB Database name (default: bilhej)
POSTGRES_USER Database user
POSTGRES_PASSWORD Database password
JWT_SECRET Secret key for JWT signing
STRIPE_SECRET_KEY Stripe secret key
STRIPE_WEBHOOK_SECRET Stripe webhook signing secret
STRIPE_PRICE_ID Stripe price ID for single letter

Project Structure

bilhej/
├── frontend/                 # Vue.js 3 SPA
│   ├── src/
│   │   ├── components/       # Reusable UI components
│   │   ├── composables/      # Shared composition functions
│   │   ├── layouts/          # Page layouts
│   │   ├── pages/            # Route-level page components
│   │   ├── router/           # Vue Router config
│   │   ├── stores/           # Pinia stores
│   │   ├── api/              # API client and endpoints
│   │   ├── assets/           # Static assets, CSS
│   │   ├── App.vue
│   │   └── main.ts
│   ├── index.html
│   ├── vite.config.ts
│   └── package.json
├── backend/                  # Spring Boot 4
│   ├── src/main/java/se/bilhalsning/
│   │   ├── BilHejApplication.java
│   │   ├── config/           # Security, CORS, Stripe config
│   │   ├── controller/       # REST controllers
│   │   ├── dto/              # Data transfer objects
│   │   ├── entity/           # JPA entities
│   │   ├── repository/       # Spring Data repositories
│   │   ├── service/          # Business logic
│   │   └── security/         # JWT filter, user details
│   └── src/main/resources/
│       ├── application.yml              # default profile (H2)
│       ├── application-docker.yml       # docker profile (PostgreSQL)
│       └── db/migration/                # Flyway migrations
├── docker-compose.yml         # dev: postgres + backend (bootRun) + frontend (Vite HMR)
├── docker-compose.prod.yml    # prod: multi-stage builds, no source mounts, restart: unless-stopped
├── docker/
│   ├── backend.Dockerfile         # dev: JDK + gradle :backend:bootRun
│   ├── backend.prod.Dockerfile    # prod: multi-stage (Gradle → JRE Alpine, non-root)
│   ├── frontend.Dockerfile        # dev: Node + vite dev server
│   ├── frontend.prod.Dockerfile   # prod: multi-stage (Node → nginx)
│   ├── nginx.conf                 # prod: SPA fallback + /api proxy
│   └── entrypoint.sh              # prod: self-signed cert generation
├── gradlew                    # Gradle wrapper (run from repo root)
├── gradle/
│   └── wrapper/
├── settings.gradle            # rootProject.name + include 'backend'
├── build.gradle               # convenience tasks: check, up, down, reset
├── .env.example
├── README.md
├── REQUIREMENTS.md
└── CODING_GUIDELINES.md

Development vs Production

Aspect docker compose up -d docker compose -f docker-compose.prod.yml up -d
Backend ./gradlew :backend:bootRun (compiles on change) Multi-stage build → java -jar app.jar
Backend image eclipse-temurin:21-jdk (~400 MB) eclipse-temurin:21-jre-alpine (~200 MB)
Backend user root bilhej (non-root)
Frontend Vite dev server (HMR, --host 0.0.0.0) nginx serving static dist/
API proxy Vite built-in proxy (/apibackend:8080) nginx proxy_pass
SSL None Self-signed cert auto-generated on first start (.certs/ volume)
Source mounts Yes (live edit) No (files baked into image)
Restart policy Manual unless-stopped
Purpose Write code, instant feedback Verify production build

Development

All-in-one (from repo root)

./gradlew check         # lint → frontend test → backend test → integration test
./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 (dev server with HMR)

cd frontend
npm install          # first time only
npm run dev          # :3000 with HMR

Backend (IDE or CLI)

./gradlew :backend:bootRun    # :8080, profile: default (H2)

Stripe Webhooks (local testing)

stripe listen --forward-to localhost:8080/api/webhooks/stripe

Database reset

./gradlew reset    # wipes DB volume and restarts containers