chore: initial project setup with docs and guidelines

This commit is contained in:
Joakim Mörling 2026-04-30 15:26:40 +02:00
commit 03010f2fd8
7 changed files with 1304 additions and 0 deletions

46
.gitignore vendored Normal file
View file

@ -0,0 +1,46 @@
# Dependencies
node_modules/
frontend/node_modules/
# Build output
dist/
frontend/dist/
target/
*.class
*.jar
*.war
!.mvn/wrapper/maven-wrapper.jar
# Environment
.env
!.env.example
# IDE
.idea/
*.iml
.vscode/
.project
.classpath
.settings/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# Logs
*.log
# Obsidian (personal vault, not project)
.obsidian/
# Docker
docker-compose.override.yml
# Java
*.hprof
# Test
coverage/
frontend/coverage/

196
AGENTS.md Normal file
View file

@ -0,0 +1,196 @@
# 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 3 backend +
PostgreSQL 16. Deployed via Docker Compose.
---
## Build, Lint, Test & Run Commands
Always run these after making changes to verify nothing is broken.
### Quick start (everything)
```bash
cp .env.example .env # first time only, then fill in keys
docker compose up -d # starts postgres, backend, frontend
```
### 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
```
### Backend (Spring Boot 3 + Java 21)
```bash
cd backend
./mvnw spring-boot:run # dev server on :8080
./mvnw test # JUnit 5 + Mockito
./mvnw verify # full verification including integration tests
```
### 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`.
To reset: `docker compose down -v && docker compose up -d`.
---
## Project Structure
```
bilhej/
├── frontend/ # Vue.js 3 SPA (Vite)
│ ├── src/
│ │ ├── pages/ # Route-level page components
│ │ ├── components/ # Reusable UI components
│ │ ├── composables/ # useXxx.js shared logic
│ │ ├── stores/ # Pinia stores
│ │ ├── api/ # API client modules
│ │ ├── router/ # Vue Router config
│ │ └── assets/ # Static files, CSS
│ └── ...
├── backend/ # Spring Boot 3 (Java 21)
│ ├── 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
│ └── db/migration/ # Flyway migrations
├── docker/ # Dockerfiles
├── docker-compose.yml
├── docker-compose.prod.yml
├── .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).
### 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 3)
- 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": "..." }`).
- No Lombok beyond `@RequiredArgsConstructor`.
### 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.
### 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
### 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 deferred to Phase 1.
### CI (future)
- `./mvnw verify` and `npm run test && npm run lint` must pass before merge.
---
## 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.

288
CODING_GUIDELINES.md Normal file
View file

@ -0,0 +1,288 @@
# Coding Guidelines — BilHej
> **Note:** This file is loaded by OpenCode via `AGENTS.md` as an external
> instruction source. It is referenced by `opencode.json` instructions.
> Keep it current — OpenCode uses it for decision-making.
Conventions and standards for the BilHej codebase. These exist to keep the project consistent — especially important as a solo developer returning to the code after weeks or months away.
---
## 1. General Principles
- **Readability over cleverness.** Write code you can understand at 2 AM six months from now.
- **English for code, Swedish for user-facing strings.** Variable names, comments, commit messages: English. UI text, error messages shown to users, templates: Swedish.
- **No commented-out code.** Delete it. Git history keeps it if needed.
- **Keep functions small.** A function should do one thing. If it's over 30 lines, it probably does too much.
- **No magic numbers.** Use named constants or enums.
---
## 2. Git
### Branch Naming
```
feature/plate-lookup
fix/stripe-webhook-timeout
chore/upgrade-spring-boot-3.3
```
### Commit Messages
```
<type>: <imperative verb, lowercase, no period>
feat: add license plate input component with validation
fix: handle empty response from Transportstyrelsen lookup
refactor: extract letter preview into composable
chore: bump vue to 3.5
```
Types: `feat`, `fix`, `refactor`, `chore`, `docs`, `test`, `style`
### Workflow
- Work directly on `main` until you have a reason not to (MVP phase).
- Squash trivial fix commits before pushing.
- Never commit `.env`, credentials, or secrets. `.env.example` only.
---
## 3. Frontend — Vue.js 3
### File Naming
| Type | Convention | Example |
|--------------|-----------------------------|----------------------------------|
| Page | PascalCase, in `pages/` | `HomePage.vue`, `OrderHistoryPage.vue` |
| Component | PascalCase, in `components/`| `PlateInput.vue`, `LetterPreview.vue` |
| Composable | camelCase, `use` prefix | `useAuth.js`, `usePayment.js` |
| Store | camelCase, in `stores/` | `authStore.js`, `orderStore.js` |
| API module | camelCase, in `api/` | `orders.js`, `templates.js` |
### Component Structure
```vue
<script setup>
// 1. Imports
import { ref, computed } from 'vue'
import { useAuth } from '@/composables/useAuth'
// 2. Props & Emits
const props = defineProps({
plate: { type: String, required: true }
})
const emit = defineEmits(['submit'])
// 3. Composables
const { user } = useAuth()
// 4. Reactive state
const loading = ref(false)
// 5. Computed
const isValid = computed(() => /^[A-Z]{3}\d{3}$/.test(props.plate))
// 6. Methods
async function handleSubmit() {
loading.value = true
// ...
emit('submit')
}
</script>
<template>
<div class="plate-input">
<!-- template here -->
</div>
</template>
<style scoped>
.plate-input {
/* scoped styles */
}
</style>
```
### Conventions
- Use `<script setup>` with Composition API. No Options API.
- Props: type and required/default always declared.
- `v-model` bindings use `defineModel()` or explicit `modelValue` + `update:modelValue`.
- No global CSS unless it's a design token or reset. Component styles are scoped.
- Prefer Pinia stores over prop drilling for shared state (auth, current order).
- API calls live in `api/*.js` modules, not in components.
- Use `fetch` or `axios` via a single configured instance (base URL, auth header interceptor).
---
## 4. Backend — Spring Boot 3
### Package Structure
```
se.bilhalsning
├── config # @Configuration classes (SecurityConfig, StripeConfig, CorsConfig)
├── controller # @RestController classes, one per resource
├── dto # Request/response DTOs, use records where possible
├── entity # @Entity JPA classes
├── repository # Spring Data JPA repositories
├── service # @Service business logic
├── security # JwtFilter, UserDetailsServiceImpl
├── exception # Custom exceptions + @ControllerAdvice
└── mapper # Entity ↔ DTO mapping (MapStruct or manual)
```
### Naming Conventions
| Layer | Convention | Example |
|--------------|-----------------------------------|----------------------------------|
| Controller | `{Resource}Controller` | `OrderController` |
| Service | `{Resource}Service` | `OrderService` |
| Repository | `{Entity}Repository` | `OrderRepository` |
| Entity | `{SingularNoun}` | `Order`, `User`, `Template` |
| DTO request | `{Action}{Resource}Request` | `CreateOrderRequest` |
| DTO response | `{Resource}Response` | `OrderResponse` |
### Controller Pattern
```java
@RestController
@RequestMapping("/api/orders")
@RequiredArgsConstructor
public class OrderController {
private final OrderService orderService;
@PostMapping
public ResponseEntity<OrderResponse> create(
@Valid @RequestBody CreateOrderRequest request,
@AuthenticationPrincipal UserDetails user) {
return ResponseEntity.ok(orderService.create(request, user));
}
@GetMapping
public ResponseEntity<List<OrderResponse>> list(
@AuthenticationPrincipal UserDetails user) {
return ResponseEntity.ok(orderService.list(user));
}
}
```
### Conventions
- Controllers stay thin. No business logic. Delegate to services.
- Services contain business logic. Services call repositories and other services.
- Use constructor injection with `@RequiredArgsConstructor`. No `@Autowired` field injection.
- DTOs: prefer Java records over classes for immutable data.
- Validation: `@Valid` on controller parameters, `jakarta.validation` annotations on DTOs.
- Exception handling: `@ControllerAdvice` + `@ExceptionHandler`. Never return 500 with a stack trace to the client.
- All responses: `ResponseEntity<T>`. Never return bare entities.
- Entity fields use `snake_case` column naming explicitly (`@Column(name = "created_at")`).
- Database migrations: Flyway. All schema changes go through SQL migration files in `db/migration/`.
- No Lombok beyond `@RequiredArgsConstructor`. Prefer explicit getters/setters or records.
### API Path Conventions
```
GET /api/orders List user's orders
POST /api/orders Create order
GET /api/orders/{id} Get single order
GET /api/templates List active templates
POST /api/auth/register Register user
POST /api/auth/login Login, returns JWT
GET /api/vehicles/{plate} Get public vehicle info
```
---
## 5. Database
- Table names: `snake_case`, plural (`orders`, `users`, `templates`).
- Column names: `snake_case`.
- Primary keys: `UUID`, generated in application code (not DB-generated), column named `id`.
- Timestamps: `created_at`, `updated_at`. Use `Instant` in Java.
- Enums: stored as `VARCHAR` in DB, mapped to Java enums. Prefer enums over magic strings.
- Indexes: add for any column used in WHERE or JOIN clauses. Add for every FK.
---
## 6. Error Handling
### Frontend
```javascript
// api/client.js — centralized fetch wrapper
async function request(url, options) {
const response = await fetch(`${BASE_URL}${url}`, {
...options,
headers: {
'Content-Type': 'application/json',
...(authToken() && { Authorization: `Bearer ${authToken()}` }),
...options.headers,
},
})
if (!response.ok) {
const error = await response.json().catch(() => ({}))
throw new ApiError(response.status, error.message || 'Något gick fel')
}
return response.json()
}
```
### Backend
```java
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(OrderNotFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFound(OrderNotFoundException ex) {
return ResponseEntity.status(404).body(new ErrorResponse(ex.getMessage()));
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidation(MethodArgumentNotValidException ex) {
String message = ex.getBindingResult().getFieldErrors().stream()
.map(e -> e.getField() + ": " + e.getDefaultMessage())
.collect(Collectors.joining(", "));
return ResponseEntity.badRequest().body(new ErrorResponse(message));
}
}
```
- Always return a consistent JSON error shape: `{ "message": "..." }`
- Log the actual exception server-side. Never leak stack traces to the client.
---
## 7. Testing
- Backend: JUnit 5 + Mockito. Service layer tests as unit tests. Controller tests with `@WebMvcTest`.
- Frontend: Vitest for composables and utility functions. Cypress or Playwright for E2E (Phase 1).
- Test naming: `shouldXxxWhenYyy` — e.g., `shouldReturn404WhenPlateNotFound`.
- Aim for test coverage on business logic, not on getters/setters/boilerplate.
---
## 8. Linting & Formatting
| Layer | Tool |
|-----------|---------------|
| Frontend | ESLint + Prettier |
| Backend | Checkstyle or Spotless (Google Java Format) |
Run before every commit manually. CI will enforce it later.
---
## 9. Security Checklist (per PR)
- [ ] No secrets/logins in committed code
- [ ] JWT endpoints correctly annotated with `@PreAuthorize` or security filter chain
- [ ] User-visible strings use Swedish
- [ ] API responses never expose internal IDs or stack traces
- [ ] Input validated on both frontend and backend

137
README.md Normal file
View file

@ -0,0 +1,137 @@
# 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 3 |
| 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](https://stripe.com) account (test mode for development)
---
## Quick Start
```bash
git clone <repo-url> bilhej
cd bilhej
cp .env.example .env # fill in your keys
docker compose up -d
```
The app will be available at:
- Frontend: `http://localhost:3000`
- Backend API: `http://localhost:8080`
- PostgreSQL: `localhost:5432`
---
## 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.js
│ ├── index.html
│ ├── vite.config.js
│ └── package.json
├── backend/ # Spring Boot 3
│ ├── 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
│ └── db/migration/ # Flyway migrations
├── docker-compose.yml
├── docker/
│ ├── backend.Dockerfile
│ └── frontend.Dockerfile
├── .env.example
├── README.md
├── REQUIREMENTS.md
├── CODING_GUIDELINES.md
└── ARCHITECTURE.md
```
---
## Development
### Frontend (dev server with HMR)
```bash
cd frontend
npm install
npm run dev
```
### Backend (IDE or CLI)
```bash
cd backend
./mvnw spring-boot:run
```
### Stripe Webhooks (local testing)
```bash
stripe listen --forward-to localhost:8080/api/webhooks/stripe
```
---
## Related Documents
- [REQUIREMENTS.md](./REQUIREMENTS.md) — Full product requirements and business model
- [CODING_GUIDELINES.md](./CODING_GUIDELINES.md) — Code conventions and standards
- [ARCHITECTURE.md](./ARCHITECTURE.md) — Detailed architecture and data flow

587
REQUIREMENTS.md Normal file
View file

@ -0,0 +1,587 @@
# BilHej / Bilhälsning.se — Requirements Document
> **Note:** This file is loaded by OpenCode via `AGENTS.md` as an external
> instruction source. It is referenced by `opencode.json` instructions.
> **Version:** 0.1.0 (Draft)
> **Date:** April 2026
> **Status:** Pre-development
---
## 1. Executive Summary
BilHej (Bilhälsning.se) is a web platform that allows Swedish residents to contact the owner of a vehicle by entering a registration number, composing a message, and having a physical letter printed and mailed. The platform acts as a trusted intermediary — the user never sees the recipient's name or address. Revenue comes from charging users per letter, with a hybrid subscription + pay-as-you-go model.
---
## 2. Project Vision
A driver sees a car parked on the street. They want to:
- Compliment the owner on their well-kept Volvo 240
- Express interest in buying the car
- Let the owner know their tire is flat or lights were left on
- Give feedback about driving behavior or a honking incident
They visit Bilhälsning.se on their phone, enter the plate, write a short message, pay 49 SEK — and a physical letter arrives in the owner's mailbox 13 days later. The sender remains anonymous unless they choose to include their contact details in the letter body.
---
## 3. Target Users
- **Sender:** Swedish resident, 18+, with email and a payment method
- **Recipient:** Swedish vehicle owner (never directly interacts with the platform)
---
## 4. Data Flow & Privacy Design
```
Sender BilHej Third Parties
────── ────── ─────────────
Enters plate ──────────→ Plate number stored
Composes letter ───────→ Letter content stored
Pays (Stripe/Swish) ───→ Payment recorded
├─→ Transportstyrelsen: owner address lookup
│ (address used to mail envelope, then DELETED)
├─→ PostNord/Strålfors: letter content + address
│ (print, envelope, mail, return tracking ID)
Receives confirmation ←── Tracking + order status
```
**Key principles:**
- The sender never sees the recipient's name or address
- The recipient's address is only used transiently — obtained, used for envelope addressing, then deleted
- The recipient's name is never stored by BilHej
- Letter content is stored only for order history and moderation purposes
- The letter itself serves as GDPR Art. 14 notification (informs recipient their address was accessed)
---
## 5. Functional Requirements
### F1 — User Account (Sender)
| Detail | Description |
|--------|-------------|
| Registration | Email + password |
| Login | Email + password |
| Account page | View order history, manage subscription |
| Auth method | JWT tokens (Spring Security + stateless sessions) |
**Rationale:** BankID is not required. The user never accesses personal data — BilHej
(the company) is the sole data controller querying Transportstyrelsen. Email+password
keeps the user experience simple and avoids BankID integration costs and GDPR
complexity on the user side.
### F2 — License Plate Input
| Detail | Description |
|--------|-------------|
| Input | Registration number (e.g., "ABC123") |
| Validation | Swedish plate format (3 letters + 3 digits, or 3 letters + 2 digits + 1 letter) |
| Lookup — public vehicle info | Transportstyrelsen's open vehicle data (make, model, year, color). This is publicly available and free/low-cost. |
| Lookup — owner address | Transportstyrelsen "Fråga om fordonsägare" or "Fordonsregisterkund" API. NOT shown to the sender. Used internally to address the envelope. |
**Phase 0 (hobby/manual):** The vehicle info lookup can optionally use a free/open dataset or the Transportstyrelsen SMS service for make/model. Owner address is obtained manually via the "Fråga om fordonsägare" form.
**Phase 1 (business/automated):** Direct API integration with Transportstyrelsen as a "Fordonsregisterkund" for automated owner address lookup.
### F3 — Letter Composer
| Detail | Description |
|--------|-------------|
| Template selection | Choose a pre-written template or "Fritt meddelande" |
| Text editor | Textarea with character limit (e.g., 1000 chars) |
| Preview | Rendered preview showing exactly what the recipient will see (A4 page) |
| Sender identity | Optional: include name/email/phone in letter body. Default: anonymous |
| Language | Swedish (expandable to English later) |
### F4 — Letter Templates
| # | Template name | Purpose | Legal risk |
|---|---------------|---------|------------|
| 1 | **Komplimang** | Compliment on the car | Low |
| 2 | **Jag vill köpa din bil** | Purchase interest inquiry | Low |
| 3 | **Tips / servicebehov** | Heads-up about flat tire, lights, etc. | Low |
| 4 | **Synpunkter på körbeteende** | Feedback about driving behavior | **Medium** |
| 5 | **Tuta / frustration** | Honking incident follow-up (worded diplomatically) | **Medium** |
| 6 | **Fritt meddelande** | Free text, sender's own words and responsibility | Low (user liability) |
Templates 4 and 5 should always include clear disclaimers and respectful wording.
If legal counsel advises against them, merge them into "Fritt meddelande" where
the user assumes full responsibility for content.
### F5 — Payment Wall
| Detail | Description |
|--------|-------------|
| Triggers | After letter is composed and sender clicks "Send" |
| Before payment | Sender sees: plate number, vehicle info (if available), letter preview |
| After payment | Letter is queued for processing and dispatch |
| Payment provider | Stripe (cards + Swish) |
| Price tiers | See Section 10 |
### F6 — Letter Sending & Tracking
| Detail | Description |
|--------|-------------|
| Phase 0 (manual) | Owner address obtained via Transportstyrelsen form → manual print + envelope + PostNord "Digitalt frimärke" (22 SEK, no tracking) or Varubrev (22 SEK, tracked) → manual status update |
| Phase 1 (automated) | PostNord "Skicka Direkt" API or Strålfors API → print + envelope + mail + automatic tracking |
| Tracking (automated) | PostNord returns shipment ID → poll for status (CREATED → IN_TRANSIT → DELIVERED) |
| Tracking (manual) | Varubrev shipment ID entered manually |
| User-facing status | "Skickat", "På väg", "Levererat" |
### F7 — Order History
| Detail | Description |
|--------|-------------|
| Access | Authenticated users only |
| Contents | Date, plate number, template used, status, tracking link (if available) |
| Letter content | Stored for sender's reference |
| No PII | Recipient address/name NEVER shown, NEVER stored after sending |
### F8 — Admin Panel
| Detail | Description |
|--------|-------------|
| Access | Internal only (admin credentials) |
| Template management | Create/edit/disable letter templates |
| Order overview | View all orders, statuses, revenue |
| Moderation queue | Review "Fritt meddelande" content if flagged |
| Blocklist | Manage opt-out list (recipients who don't want letters) |
| Revenue dashboard | Stripe integration for transaction overview |
---
## 6. Non-Functional Requirements
| ID | Requirement | Detail |
|----|-------------|--------|
| N1 | Responsive design | Mobile-first. Most users will be on a phone, standing next to a car. |
| N2 | Page load time | <2 seconds on 4G mobile |
| N3 | Security | HTTPS, password hashing (bcrypt), JWT expiration, input sanitization |
| N4 | Privacy | Address deleted after PostNord dispatch. No PII logs. |
| N5 | Availability | Phase 0: best-effort (home server). Phase 1: 99.5% uptime |
| N6 | Localization | Swedish UI (expandable to English) |
| N7 | Audit log | Every address lookup logged with plate, timestamp, purpose. Required by Transportstyrelsen for API users. |
---
## 7. Technical Architecture
### Stack
| Layer | Technology |
|-------|-----------|
| Frontend | Vue.js 3 (Composition API), Vite, Pinia state management, Vue Router |
| Backend API | Java 21, Spring Boot 3, Spring Security (JWT), Spring Data JPA |
| Database | PostgreSQL 16 |
| Deployment | Docker, Docker Compose |
| Hosting (Phase 0) | Home server via dynamic DNS or static IP, Let's Encrypt SSL |
| Hosting (Phase 1) | Swedish VPS provider (Binero, FS Data, Elastx) |
| Payment | Stripe (Checkout / Payment Intents) — supports Swish + cards |
| Mail API (Phase 1) | PostNord "Skicka Direkt" or Strålfors Document Delivery API |
### Architecture Diagram
```
┌──────────────────────────────────────────┐
│ Internet │
│ │
│ ┌──────────────┐ ┌───────────────┐ │
│ │ User Phone │ │ User Desktop │ │
│ └──────┬───────┘ └───────┬───────┘ │
└─────────┼─────────────────────┼──────────┘
│ HTTPS │
┌─────────▼─────────────────────▼──────────┐
│ Nginx (reverse proxy) │
│ Let's Encrypt SSL │
└──────────────────┬───────────────────────┘
┌──────────────────▼───────────────────────┐
│ Vue.js 3 SPA (Vite) │
│ Port: 3000 (dev) │
│ Served as static files │
└──────────────────┬───────────────────────┘
│ REST API calls
┌──────────────────▼───────────────────────┐
│ Spring Boot 3 (Java 21) │
│ Port: 8080 │
│ ┌────────────┐ ┌────────────────────┐ │
│ │ Spring │ │ Service Layer │ │
│ │ Security │ │ - LetterService │ │
│ │ (JWT) │ │ - LookupService │ │
│ │ │ │ - MailService │ │
│ │ │ │ - PaymentService │ │
│ └────────────┘ └────────┬───────────┘ │
│ │ │
│ ┌────────────▼───────────┐ │
│ │ PostgreSQL 16 │ │
│ │ Port: 5432 │ │
│ └────────────────────────┘ │
└──────────────────┬───────────────────────┘
┌──────────────┼──────────────┐
▼ ▼ ▼
┌────────┐ ┌──────────┐ ┌──────────┐
│ Trans- │ │ PostNord │ │ Stripe │
│ port- │ │ / │ │ (cards + │
│ styrel │ │ Strålfors│ │ Swish) │
│ sen │ │ API │ │ │
└────────┘ └──────────┘ └──────────┘
```
### Database Schema (Core Tables)
```
users
id UUID PK
email VARCHAR(255) UNIQUE NOT NULL
password_hash VARCHAR(255) NOT NULL
created_at TIMESTAMP
subscription ENUM('none', 'basic', 'pro')
orders
id UUID PK
user_id UUID FK → users
plate VARCHAR(10) NOT NULL
template VARCHAR(50)
letter_text TEXT NOT NULL
status ENUM('pending_payment','paid','lookup_started','sent','delivered','failed')
amount_paid DECIMAL(10,2)
tracking_id VARCHAR(100)
created_at TIMESTAMP
updated_at TIMESTAMP
templates
id UUID PK
name VARCHAR(100)
body_template TEXT
is_active BOOLEAN
blocklist
id UUID PK
plate VARCHAR(10) UNIQUE
reason VARCHAR(255)
created_at TIMESTAMP
audit_log
id UUID PK
plate VARCHAR(10)
action VARCHAR(50)
timestamp TIMESTAMP
ip_address VARCHAR(45)
```
---
## 8. API Integrations
### 8.1 Transportstyrelsen
| Aspect | Phase 0 (Hobby) | Phase 1 (Business) |
|--------|----------------|-------------------|
| Method | "Fråga om fordonsägare" form (manual) | "Fordonsregisterkund" API (automated) |
| What you get | Owner name + address | Owner name + address (programmatic) |
| Cost | ~1015 SEK per request | ~35 SEK per query (estimated) |
| Turnaround | Days (manual processing) | Seconds (API) |
| Setup | None (open to individuals) | Formal application + agreement required |
| Name/address stored by BilHej | No (deleted after envelope is addressed) | No (deleted after envelope is addressed) |
**Open questions for Transportstyrelsen:**
- Can a small business / enskild firma get API access, or do they require a minimum volume?
- What is the actual per-query cost?
- What are the formal GDPR/data processing agreement requirements?
- How does the "Fordonsregisterkund" API handle address-update frequency (dagsaktuella uppgifter)?
**Email template for Transportstyrelsen:**
> Hej,
> Jag planerar att starta en tjänst där privatpersoner kan skicka brev till fordonsägare
> via registreringsnummer. Tjänsten fungerar så att avsändaren aldrig ser mottagarens
> adress — det är endast vi som mellanhand som hanterar adressen för att posta brevet,
> varefter adressen raderas.
> Jag undrar:
> 1. Kan vi som mindre företag få tillgång till ägaruppgifter via ert API
> (Fordonsregisterkund)?
> 2. Vad kostar det per uppslag?
> 3. Vilka krav ställer ni gällande GDPR och personuppgiftshantering?
> 4. Finns det någon minimivolym?
> Tack på förhand.
### 8.2 PostNord / Strålfors
| Service | Description | Cost (est.) | Tracking |
|---------|-------------|------------|----------|
| PostNord "Digitalt frimärke" | Buy postage digitally, print and mail yourself | 22 SEK (50g letter) | No |
| PostNord Varubrev 1:a klass | Tracked letter, postage only | ~22 SEK (50g) | Yes (basic) |
| PostNord "Skicka Direkt" API | Print + envelope + mail, fully automated | ~1218 SEK total (estimated) | Yes |
| Strålfors Document Delivery API | Print + envelope + mail, enterprise grade | ~815 SEK total (estimated) | Yes (webhook) |
PostNord API access is free. You only pay for postage/services.
**Open questions for PostNord:**
- What is the actual price for "Skicka Direkt" (print + mail) for a single-page A4 letter?
- Is there a minimum volume?
- Can we get a business account as an enskild firma?
### 8.3 Stripe
| Detail | Description |
|--------|-------------|
| Products | Stripe Checkout, Payment Intents |
| Payment methods | Cards (Visa, Mastercard) + Swish |
| Fees | ~1.5% + 2 SEK per transaction |
| Subscription support | Stripe Billing for recurring subscriptions (Phase 1) |
---
## 9. User Flow
```
Sender visits Bilhälsning.se
┌──────────────┐
│ Enter plate │ "ABC 123"
│ number │
└──────┬───────┘
┌──────────────┐
│ Vehicle info │ "Volvo V70, 2009, Silver"
│ displayed │ (public data, free)
└──────┬───────┘
┌──────────────┐
│ Choose │ Template dropdown or free text
│ template │
└──────┬───────┘
┌──────────────┐
│ Compose/edit │ Text editor + preview
│ letter │
└──────┬───────┘
┌──────────────┐
│ Preview + │ "Så här ser brevet ut"
│ confirm │ Total: 49 SEK
└──────┬───────┘
┌──────────────┐
│ Log in / │ Email + password
│ Register │ (or skip: guest checkout)
└──────┬───────┘
┌──────────────┐
│ Pay via │ Stripe Checkout
│ Stripe/Swish │ (card or Swish)
└──────┬───────┘
┌──────────────┐
│ Confirmation │ "Ditt brev är på väg!"
│ + tracking │ Estimated delivery: 24 days
└──────────────┘
```
**Behind the scenes (after payment):**
```
BilHej backend → Transportstyrelsen lookup (owner address)
→ PostNord/Strålfors API (print + mail)
→ Address deleted from BilHej
→ Order status updated
→ Tracking webhook/callback → status updated
```
---
## 10. Business Model & Pricing
### Cost Analysis (per letter)
| Cost item | Phase 0 (manual) | Phase 1 (automated) |
|-----------|-----------------|--------------------|
| Transportstyrelsen address lookup | 1015 SEK | 35 SEK |
| PostNord postage / print+mail | 22 SEK (postage only) | 1218 SEK |
| Stripe processing (~1.5% + 2 SEK) | ~3 SEK | ~3 SEK |
| Envelope, paper, printer (manual) | ~5 SEK | 0 SEK (included) |
| **Total cost per letter** | **~4045 SEK** | **~1826 SEK** |
### Pricing Tiers
| Tier | Monthly price | Letters included | Extra letters | Est. margin (Phase 1) |
|------|--------------|-----------------|---------------|----------------------|
| Pay-as-you-go | — | 0 | 49 SEK | 2331 SEK |
| Basic | 99 SEK/mån | 3 | 39 SEK | 1321 SEK |
| Pro | 199 SEK/mån | 10 | 29 SEK | 311 SEK |
### Example Economics (Phase 1, Pro subscriber sending 15 letters/month)
```
Revenue: 199 (subscription) + 5 × 29 (extra letters) = 344 SEK
Costs: 15 × ~22 SEK = 330 SEK
Gross margin: 14 SEK
(Low margin on Pro — designed for high-volume users, profit on Basic/Pay-as-you-go)
```
---
## 11. Legal & Compliance
### 11.1 GDPR Applicability
| Question | Answer |
|----------|--------|
| Does GDPR apply to a hobby project? | Technically yes if you process personal data of others. **However**, Art. 2(2)(c) exempts "purely personal or household activity." A paid public-facing website is **not** exempt. In practice, enforcement risk at small scale is low, but you should be aware. |
| Does GDPR apply to a registered company? | Yes, unequivocally. |
| Is a license plate personal data? | Yes (it directly identifies a vehicle owner). |
| Is an address personal data? | Yes. |
| What if we only process address transiently? | Data minimization is a GDPR principle (Art. 5(1)(c)). Transient processing with immediate deletion is a strong compliance posture. |
| Do we need to inform the recipient? | Yes, GDPR Art. 14 requires informing the data subject. The letter itself can serve this purpose — include a footer like: _"Detta brev skickades via BilHej.se. Din adress hämtades från Transportstyrelsens fordonsregister och har raderats efter utskick. För frågor: hej@bilhalsning.se"_ |
### 11.2 Transportstyrelsen Access
| For individuals | "Fråga om fordonsägare" form — open to anyone with a stated reason. ~1015 SEK. |
| For businesses | "Fordonsregisterkund" API — requires formal application, stated purpose, data processing agreement. Price not public. |
### 11.3 Blocklist / Right to Object
Under GDPR Art. 21, data subjects have the right to object to processing. BilHej must provide:
- An email/contact form where a recipient can say "don't send me letters"
- A blocklist (per plate) that prevents future letters to that vehicle
- A clear note in the letter footer with opt-out instructions
### 11.4 Recommended Legal Steps
1. **Before launch (Phase 0):** Read Transportstyrelsen's terms for "Fråga om fordonsägare." Include GDPR Art. 14 notice in letter footer. Set up blocklist. Keep address deletion strict.
2. **Before Phase 1:** Consult a Swedish IT-jurist. Register company. Formal data processing agreement with Transportstyrelsen. Appoint DPO if needed. Draft privacy policy and terms of service.
### 11.5 Company vs Private Individual
If the website makes money, you are conducting economic activity. The Swedish Tax Agency (Skatteverket) will consider this a business regardless of formal registration. You can:
- Register as **enskild firma** (sole trader) — simplest, personal tax number, unlimited liability
- Register as **aktiebolag (AB)** — limited liability, requires 25,000 SEK share capital, more administration
- Not register at all (Phase 0) — only viable at very small scale. VAT (moms) becomes an issue above 80,000 SEK/year.
For Phase 0 with manual processing, staying unregistered is workable. If revenue approaches 80,000 SEK/year or if you want automated API access, incorporation will be necessary.
---
## 12. Development Phases
### Phase 0 — MVP (Hobby/Manual)
| Feature | Description |
|---------|-------------|
| Vue.js landing page | Plate input, letter composer, template selection |
| Email+password auth | User registration and login |
| Stripe integration | One-time payments only (no subscriptions) |
| Letter queue | Backend stores orders in DB, admin manually processes |
| Order history | Users see past orders and status |
| Admin panel | Mark orders as processed, enter tracking IDs manually |
| No API integrations (yet) | Address lookup done manually by you |
| Home server deployment | Docker Compose, dynamic DNS, Let's Encrypt |
**Timeline:** ~46 weeks (1 person, part-time)
**Cost:** ~0 SEK (excluding subscriptions/services)
### Phase 1 — Automated
| Feature | Description |
|---------|-------------|
| Transportstyrelsen API integration | Automated owner address lookup |
| PostNord/Strålfors API integration | Automated print + mail + tracking |
| Subscription management | Stripe Billing, recurring payments |
| Automatic tracking | Webhook callbacks from PostNord |
| Blocklist system | Opt-out management |
| Moderation queue | Review flagged free-text letters |
| Company registration + legal | Formal agreements, privacy policy, terms |
| Production hosting | Swedish VPS |
**Timeline:** ~610 weeks (1 person, full-time)
**Cost:** API fees, legal consultation (~10,00030,000 SEK), VPS (~100 SEK/mån)
### Phase 2 — Growth
| Feature | Description |
|---------|-------------|
| Advanced templates | Rich text, optional images (car photo) |
| Analytics dashboard | Revenue, letter volume, user retention |
| Referral system | "Refer a friend, get a free letter" |
| English language support | UI and template localization |
| Mobile app (PWA) | Progressive Web App for app-like experience |
| Partner integrations | Dealerships, insurance companies, etc. |
---
## 13. Risks & Mitigations
| Risk | Severity | Mitigation |
|------|----------|------------|
| Transportstyrelsen denies API access | High | Phase 0 manual fallback works with form requests. SMS service as partial alternative. Investigate alternative data sources (fordonsfraga.se, biluppgifter.se). |
| High per-letter costs kill margins | Medium | Start with pay-as-you-go at 49 SEK (margin exists even at Phase 0 costs). Move to subscription+automation to reduce cost base. |
| Recipient complaints / negative PR | Medium | Polite, respectful templates. Clear opt-out in every letter. Blocklist system. Avoid "Tuta" template unless diplomatically worded. |
| GDPR complaint to IMY | Low (Phase 0) / Medium (Phase 1) | Address deletion, no name storage, Art. 14 notice in letter, blocklist. Legal review before Phase 1. |
| Home server downtime | Low | Phase 0 is best-effort. Not critical for low volume. |
| Stripe account closure (reputation risk) | Medium | Ensure terms of service compliance. Have backup payment method. |
| Competition | Low | No known competitor offers this exact service (physical letter via plate lookup). Car.info / biluppgifter.se show vehicle info but don't facilitate contact. |
---
## 14. Open Questions
| # | Question | Owner | Priority |
|---|----------|-------|----------|
| Q1 | What is Transportstyrelsen's actual per-query price for "Fordonsregisterkund" API? | To investigate | High |
| Q2 | Can we use "Fråga om fordonsägare" form for multiple requests as a commercial service? | To investigate | High |
| Q3 | What is PostNord "Skicka Direkt" API pricing for print-and-mail? | To investigate | High |
| Q4 | Does the SMS service give enough info to derive or supplement address data? | To investigate | Medium |
| Q5 | What are the exact legal requirements for API access — company registration? DPO? | To investigate | High |
| Q6 | Should we include the sender's name by default or default to anonymous? | Product decision | Low |
| Q7 | Should "Fritt meddelande" be moderated (reviewed before dispatch) or sent as-is? | Product decision | Medium |
| Q8 | Should we charge VAT (moms) from day one or only above 80,000 SEK? | Tax question | Medium |
---
## 15. Tech Stack Summary
```
Frontend: Vue.js 3, Vite, Pinia, Vue Router
Backend: Java 21, Spring Boot 3, Spring Security (JWT), JPA/Hibernate
Database: PostgreSQL 16
Deploy: Docker, Docker Compose, Nginx reverse proxy
Hosting: Home server (Phase 0) → Swedish VPS (Phase 1)
Payments: Stripe (cards + Swish)
Mail: Manual (Phase 0) → PostNord API / Strålfors API (Phase 1)
Vehicle: Manual form (Phase 0) → Transportstyrelsen API (Phase 1)
SSL: Let's Encrypt (Certbot)
CI/CD: GitHub Actions (optional)
```
---
## 16. Next Actions
1. **Email Transportstyrelsen** — Ask about API access, pricing, requirements
2. **Email PostNord** — Ask about "Skicka Direkt" print-and-mail pricing
3. **Set up development environment** — Docker Compose with Vue.js + Spring Boot + PostgreSQL
4. **Build plate input + vehicle info display** — First frontend component
5. **Build letter composer + template system** — Core user interaction
6. **Integrate Stripe Checkout** — One-time 49 SEK payment
7. **Build admin panel** — Order management, manual status updates
8. **Deploy to home server** — Docker Compose, Nginx, Let's Encrypt
9. **Test with one real letter** — End-to-end: plate → compose → pay → manual lookup → mail → track
10. **Decide: incorporate or stay manual** — Based on demand
---
*End of Requirements Document*

8
opencode.json Normal file
View file

@ -0,0 +1,8 @@
{
"$schema": "https://opencode.ai/config.json",
"instructions": ["CODING_GUIDELINES.md", "REQUIREMENTS.md"],
"permission": {
"edit": "ask",
"bash": "ask"
}
}

42
trello-import.csv Normal file
View file

@ -0,0 +1,42 @@
Card Name,Description,List,Labels
Scaffold Vue 3 + Vite project,"Run `npm create vue@latest` or `npm create vite@latest` with Vue template. Verify `npm run dev` serves blank page on port 3000.",Infra & Scaffolding,Frontend
Scaffold Spring Boot 3 project,"Generate via Spring Initializr with dependencies: Spring Web, Spring Security, Spring Data JPA, PostgreSQL Driver, Flyway, Validation, Lombok. Verify `./mvnw spring-boot:run` starts on port 8080.",Infra & Scaffolding,Backend
Docker Compose setup,"Create `docker-compose.yml` with 3 services: postgres (16), backend (Java 21), frontend (Node/Vite dev mode). Verify `docker compose up` starts all 3 services successfully.",Infra & Scaffolding,DevOps
.env.example + env config,"Create `.env.example` with all required vars: POSTGRES_DB, POSTGRES_USER, POSTGRES_PASSWORD, JWT_SECRET, STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, STRIPE_PRICE_ID. Wire backend and docker-compose to read from `.env`.",Infra & Scaffolding,DevOps
Flyway + initial migration,"Add Flyway dependency and config to Spring Boot. Create first migration file `V1__create_users_table.sql`. Verify migration runs automatically on startup and table exists in DB.",Infra & Scaffolding,Backend
Initial DB schema,"Write Flyway migrations for all core tables: users, orders, templates, blocklist, audit_log. All migrations run on startup. Tables exist and match the schema from REQUIREMENTS.md.",Infra & Scaffolding,Backend
User entity + repo + service,"Create JPA entity `User` (id UUID PK, email unique, password_hash, created_at, subscription enum). Create Spring Data repository. Create UserService with findByEmail and createUser methods.",Auth,Backend
JWT token generation + filter,"Create JwtUtil class (generate, validate, extract username). Create JwtAuthenticationFilter (OncePerRequestFilter, checks Authorization header). Configure SecurityFilterChain bean in SecurityConfig.",Auth,Backend
Register endpoint,"Create AuthController with POST /api/auth/register. Validate email format and password length. Hash password with BCrypt. Return JWT token. Return 409 on duplicate email.",Auth,Backend
Login endpoint,"Create POST /api/auth/login in AuthController. Authenticate credentials. Return JWT token on success. Return 401 on invalid credentials.",Auth,Backend
Register page,"Create RegisterPage.vue with email and password fields, validation, submit button. On success: store token via auth store, redirect to home. On error: show Swedish error message.",Auth,Frontend
Login page,"Create LoginPage.vue with email and password fields. On success: store token and redirect. On error: show ""Felaktig e-post eller lösenord"".",Auth,Frontend
Pinia auth store + API interceptor,"Create `stores/authStore.js` (token, user, login, logout, isAuthenticated). Create `api/client.js` (base URL, auto-attach Bearer token, handle 401 responses). Token persisted in localStorage.",Auth,Frontend
Vue Router auth guards,"Configure Vue Router with routes for home, login, register, compose, orders, admin. Add beforeEach guard: redirect to /login if route requires auth and user is not authenticated.",Auth,Frontend
Vehicle lookup controller,"Create VehicleController with GET /api/vehicles/{plate}. Validate plate format. Return mock/stub vehicle info (make, model, year, color) matching the plate. Create VehicleResponse DTO.",Vehicle Info,Backend
PlateInput component,"Create PlateInput.vue. Text input with auto-uppercase. Validate Swedish plate format (ABC123 or ABC12D) in real-time. Emit `lookup` event on valid submit. Show inline error on invalid format.",Vehicle Info,Frontend
Vehicle info display,"Create VehicleInfo.vue. Receives vehicle data as prop. Displays make, model, year, color in a clean card. Shows loading state while fetching. Shows ""Inget fordon hittades"" on lookup failure.",Vehicle Info,Frontend
Landing/home page,"Create HomePage.vue. Combines PlateInput and VehicleInfo. After successful lookup, shows ""Skicka ett brev till ägaren"" button that navigates to compose page with plate in query params.",Vehicle Info,Frontend
Template entity + seed data,"Create Template entity (id, name, body_template, is_active). Create Flyway migration for templates table. Create data.sql or Java seeder that inserts 5 default templates (Komplimang, Köpa, Tips, Körbeteende, Tuta).",Letter Composer,Backend
Templates API endpoint,"Create TemplateController with GET /api/templates. Return only active templates. Public endpoint (no auth required). Return TemplateResponse list (id, name, body_template).",Letter Composer,Frontend/Backend
Template selector,"Create TemplateSelector.vue. Dropdown/select listing templates fetched from API. Selecting one fills the editor with body_template text. Include ""Fritt meddelande"" option that leaves editor blank.",Letter Composer,Frontend
Letter editor,"Create LetterEditor.vue. Textarea with live character counter (e.g. ""247/1000""). Enforce 1000 char max. Editable even when template is selected. Emit content on change.",Letter Composer,Frontend
Letter preview,"Create LetterPreview.vue. Receives letter text as prop. Renders text styled as A4 letter (white background, letter layout, preview of what recipient sees).",Letter Composer,Frontend
Compose page,"Create ComposePage.vue. Combined flow: template selector → letter editor → letter preview. Read plate from route query. ""Skicka brev"" button at bottom triggers order creation.",Letter Composer,Frontend
Order entity + repo + service,"Create Order entity (id UUID, user_id FK, plate, template, letter_text, status enum, amount_paid, tracking_id, timestamps). Create repository. Create OrderService with createOrder method (status: pending_payment).",Orders & Payment,Backend
Create order endpoint,"Create OrderController with POST /api/orders (auth required). Accept CreateOrderRequest (plate, template, letter_text). Validate plate format, text length. Return OrderResponse with order ID and status.",Orders & Payment,Backend
Stripe config + checkout session,"Create StripeConfig (@ConfigurationProperties). Create PaymentService.createCheckoutSession(orderId). Call Stripe API to create session with product price. Return session URL. Store session ID on order.",Orders & Payment,Backend
Stripe webhook handler,"Create WebhookController with POST /api/webhooks/stripe (no auth, verify Stripe signature). Handle checkout.session.completed: mark order as paid. Log all events for debugging.",Orders & Payment,Backend
Payment trigger flow,"In ComposePage: ""Skicka brev"" → POST /api/orders (backend returns order with checkout URL) → redirect window to Stripe. Create PaymentRedirect.vue that extracts order data and redirects.",Orders & Payment,Frontend
Payment success page,"Create SuccessPage.vue shown after Stripe redirect. Receives session_id in query. Shows confirmation: ""Ditt brev är på väg!"". Shows order summary (plate, template). Links to order history.",Orders & Payment,Frontend
Order history page,"Create OrderHistoryPage.vue (auth required). Fetches GET /api/orders. Renders table/card list with: date, plate, template name, status badge (Sent/På väg/Delivered), tracking link if available.",Orders & Payment,Frontend
Admin order list endpoint,"Create AdminController with GET /api/admin/orders (admin auth). Returns all orders sorted by created_at DESC. Include user email in response. Admin-only via @PreAuthorize or role check.",Admin Panel,Backend
Admin status update endpoint,"Create PATCH /api/admin/orders/{id}/status in AdminController. Accept status string. Validate status enum values. Update order status and timestamp. Return updated order.",Admin Panel,Backend
Admin login page,"Create AdminLoginPage.vue separate from user login. Hardcoded admin credentials (MVP only — move to DB later). Store admin token separately in localStorage.",Admin Panel,Frontend
Admin dashboard,"Create AdminDashboard.vue (admin auth guard). Table of all orders with columns: date, user email, plate, template, status badge. Click row to expand letter content. Status dropdown to update order status.",Admin Panel,Frontend
Manual tracking entry,"In admin dashboard: editable tracking_id field per order. On save: PATCH /api/admin/orders/{id} with new tracking_id. Add PostNord tracking link helper (https://www.postnord.se/spara?id=...).",Admin Panel,Frontend
Backend Dockerfile,"Multi-stage Dockerfile: build stage (maven, compile), run stage (eclipse-temurin:21-jre-alpine, non-root user). Copy JAR. Entrypoint: java -jar. Expose 8080.",Deployment,DevOps
Frontend Dockerfile,"Multi-stage Dockerfile: build stage (node:20-alpine, npm ci, npm run build), run stage (nginx:alpine, copy dist to /usr/share/nginx/html). Include nginx.conf or rely on compose-provided nginx.",Deployment,DevOps
Nginx reverse proxy config,"Create nginx.conf that routes /api/* to backend:8080 and everything else to frontend static files. Add CORS headers. Enable gzip. Ready for production proxying.",Deployment,DevOps
Production Compose file,"Create docker-compose.prod.yml. Same services but: frontend serves built files via nginx internally, nginx reverse proxy handles SSL termination with cert volumes, PostgreSQL uses named volume for persistence.",Deployment,DevOps
Deploy to home server,"Copy repo and .env to host. Run `docker compose -f docker-compose.prod.yml up -d`. Configure dyndns if needed. Set up Certbot/LetsEncrypt cron for SSL renewal. Verify HTTPS access from external network.",Deployment,DevOps
1 Card Name Description List Labels
2 Scaffold Vue 3 + Vite project Run `npm create vue@latest` or `npm create vite@latest` with Vue template. Verify `npm run dev` serves blank page on port 3000. Infra & Scaffolding Frontend
3 Scaffold Spring Boot 3 project Generate via Spring Initializr with dependencies: Spring Web, Spring Security, Spring Data JPA, PostgreSQL Driver, Flyway, Validation, Lombok. Verify `./mvnw spring-boot:run` starts on port 8080. Infra & Scaffolding Backend
4 Docker Compose setup Create `docker-compose.yml` with 3 services: postgres (16), backend (Java 21), frontend (Node/Vite dev mode). Verify `docker compose up` starts all 3 services successfully. Infra & Scaffolding DevOps
5 .env.example + env config Create `.env.example` with all required vars: POSTGRES_DB, POSTGRES_USER, POSTGRES_PASSWORD, JWT_SECRET, STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, STRIPE_PRICE_ID. Wire backend and docker-compose to read from `.env`. Infra & Scaffolding DevOps
6 Flyway + initial migration Add Flyway dependency and config to Spring Boot. Create first migration file `V1__create_users_table.sql`. Verify migration runs automatically on startup and table exists in DB. Infra & Scaffolding Backend
7 Initial DB schema Write Flyway migrations for all core tables: users, orders, templates, blocklist, audit_log. All migrations run on startup. Tables exist and match the schema from REQUIREMENTS.md. Infra & Scaffolding Backend
8 User entity + repo + service Create JPA entity `User` (id UUID PK, email unique, password_hash, created_at, subscription enum). Create Spring Data repository. Create UserService with findByEmail and createUser methods. Auth Backend
9 JWT token generation + filter Create JwtUtil class (generate, validate, extract username). Create JwtAuthenticationFilter (OncePerRequestFilter, checks Authorization header). Configure SecurityFilterChain bean in SecurityConfig. Auth Backend
10 Register endpoint Create AuthController with POST /api/auth/register. Validate email format and password length. Hash password with BCrypt. Return JWT token. Return 409 on duplicate email. Auth Backend
11 Login endpoint Create POST /api/auth/login in AuthController. Authenticate credentials. Return JWT token on success. Return 401 on invalid credentials. Auth Backend
12 Register page Create RegisterPage.vue with email and password fields, validation, submit button. On success: store token via auth store, redirect to home. On error: show Swedish error message. Auth Frontend
13 Login page Create LoginPage.vue with email and password fields. On success: store token and redirect. On error: show "Felaktig e-post eller lösenord". Auth Frontend
14 Pinia auth store + API interceptor Create `stores/authStore.js` (token, user, login, logout, isAuthenticated). Create `api/client.js` (base URL, auto-attach Bearer token, handle 401 responses). Token persisted in localStorage. Auth Frontend
15 Vue Router auth guards Configure Vue Router with routes for home, login, register, compose, orders, admin. Add beforeEach guard: redirect to /login if route requires auth and user is not authenticated. Auth Frontend
16 Vehicle lookup controller Create VehicleController with GET /api/vehicles/{plate}. Validate plate format. Return mock/stub vehicle info (make, model, year, color) matching the plate. Create VehicleResponse DTO. Vehicle Info Backend
17 PlateInput component Create PlateInput.vue. Text input with auto-uppercase. Validate Swedish plate format (ABC123 or ABC12D) in real-time. Emit `lookup` event on valid submit. Show inline error on invalid format. Vehicle Info Frontend
18 Vehicle info display Create VehicleInfo.vue. Receives vehicle data as prop. Displays make, model, year, color in a clean card. Shows loading state while fetching. Shows "Inget fordon hittades" on lookup failure. Vehicle Info Frontend
19 Landing/home page Create HomePage.vue. Combines PlateInput and VehicleInfo. After successful lookup, shows "Skicka ett brev till ägaren" button that navigates to compose page with plate in query params. Vehicle Info Frontend
20 Template entity + seed data Create Template entity (id, name, body_template, is_active). Create Flyway migration for templates table. Create data.sql or Java seeder that inserts 5 default templates (Komplimang, Köpa, Tips, Körbeteende, Tuta). Letter Composer Backend
21 Templates API endpoint Create TemplateController with GET /api/templates. Return only active templates. Public endpoint (no auth required). Return TemplateResponse list (id, name, body_template). Letter Composer Frontend/Backend
22 Template selector Create TemplateSelector.vue. Dropdown/select listing templates fetched from API. Selecting one fills the editor with body_template text. Include "Fritt meddelande" option that leaves editor blank. Letter Composer Frontend
23 Letter editor Create LetterEditor.vue. Textarea with live character counter (e.g. "247/1000"). Enforce 1000 char max. Editable even when template is selected. Emit content on change. Letter Composer Frontend
24 Letter preview Create LetterPreview.vue. Receives letter text as prop. Renders text styled as A4 letter (white background, letter layout, preview of what recipient sees). Letter Composer Frontend
25 Compose page Create ComposePage.vue. Combined flow: template selector → letter editor → letter preview. Read plate from route query. "Skicka brev" button at bottom triggers order creation. Letter Composer Frontend
26 Order entity + repo + service Create Order entity (id UUID, user_id FK, plate, template, letter_text, status enum, amount_paid, tracking_id, timestamps). Create repository. Create OrderService with createOrder method (status: pending_payment). Orders & Payment Backend
27 Create order endpoint Create OrderController with POST /api/orders (auth required). Accept CreateOrderRequest (plate, template, letter_text). Validate plate format, text length. Return OrderResponse with order ID and status. Orders & Payment Backend
28 Stripe config + checkout session Create StripeConfig (@ConfigurationProperties). Create PaymentService.createCheckoutSession(orderId). Call Stripe API to create session with product price. Return session URL. Store session ID on order. Orders & Payment Backend
29 Stripe webhook handler Create WebhookController with POST /api/webhooks/stripe (no auth, verify Stripe signature). Handle checkout.session.completed: mark order as paid. Log all events for debugging. Orders & Payment Backend
30 Payment trigger flow In ComposePage: "Skicka brev" → POST /api/orders (backend returns order with checkout URL) → redirect window to Stripe. Create PaymentRedirect.vue that extracts order data and redirects. Orders & Payment Frontend
31 Payment success page Create SuccessPage.vue shown after Stripe redirect. Receives session_id in query. Shows confirmation: "Ditt brev är på väg!". Shows order summary (plate, template). Links to order history. Orders & Payment Frontend
32 Order history page Create OrderHistoryPage.vue (auth required). Fetches GET /api/orders. Renders table/card list with: date, plate, template name, status badge (Sent/På väg/Delivered), tracking link if available. Orders & Payment Frontend
33 Admin order list endpoint Create AdminController with GET /api/admin/orders (admin auth). Returns all orders sorted by created_at DESC. Include user email in response. Admin-only via @PreAuthorize or role check. Admin Panel Backend
34 Admin status update endpoint Create PATCH /api/admin/orders/{id}/status in AdminController. Accept status string. Validate status enum values. Update order status and timestamp. Return updated order. Admin Panel Backend
35 Admin login page Create AdminLoginPage.vue separate from user login. Hardcoded admin credentials (MVP only — move to DB later). Store admin token separately in localStorage. Admin Panel Frontend
36 Admin dashboard Create AdminDashboard.vue (admin auth guard). Table of all orders with columns: date, user email, plate, template, status badge. Click row to expand letter content. Status dropdown to update order status. Admin Panel Frontend
37 Manual tracking entry In admin dashboard: editable tracking_id field per order. On save: PATCH /api/admin/orders/{id} with new tracking_id. Add PostNord tracking link helper (https://www.postnord.se/spara?id=...). Admin Panel Frontend
38 Backend Dockerfile Multi-stage Dockerfile: build stage (maven, compile), run stage (eclipse-temurin:21-jre-alpine, non-root user). Copy JAR. Entrypoint: java -jar. Expose 8080. Deployment DevOps
39 Frontend Dockerfile Multi-stage Dockerfile: build stage (node:20-alpine, npm ci, npm run build), run stage (nginx:alpine, copy dist to /usr/share/nginx/html). Include nginx.conf or rely on compose-provided nginx. Deployment DevOps
40 Nginx reverse proxy config Create nginx.conf that routes /api/* to backend:8080 and everything else to frontend static files. Add CORS headers. Enable gzip. Ready for production proxying. Deployment DevOps
41 Production Compose file Create docker-compose.prod.yml. Same services but: frontend serves built files via nginx internally, nginx reverse proxy handles SSL termination with cert volumes, PostgreSQL uses named volume for persistence. Deployment DevOps
42 Deploy to home server Copy repo and .env to host. Run `docker compose -f docker-compose.prod.yml up -d`. Configure dyndns if needed. Set up Certbot/LetsEncrypt cron for SSL renewal. Verify HTTPS access from external network. Deployment DevOps