chore: initial project setup with docs and guidelines
This commit is contained in:
commit
03010f2fd8
7 changed files with 1304 additions and 0 deletions
46
.gitignore
vendored
Normal file
46
.gitignore
vendored
Normal 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
196
AGENTS.md
Normal 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
288
CODING_GUIDELINES.md
Normal 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
137
README.md
Normal 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
587
REQUIREMENTS.md
Normal 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 1–3 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 | ~10–15 SEK per request | ~3–5 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 | ~12–18 SEK total (estimated) | Yes |
|
||||
| Strålfors Document Delivery API | Print + envelope + mail, enterprise grade | ~8–15 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: 2–4 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 | 10–15 SEK | 3–5 SEK |
|
||||
| PostNord postage / print+mail | 22 SEK (postage only) | 12–18 SEK |
|
||||
| Stripe processing (~1.5% + 2 SEK) | ~3 SEK | ~3 SEK |
|
||||
| Envelope, paper, printer (manual) | ~5 SEK | 0 SEK (included) |
|
||||
| **Total cost per letter** | **~40–45 SEK** | **~18–26 SEK** |
|
||||
|
||||
### Pricing Tiers
|
||||
|
||||
| Tier | Monthly price | Letters included | Extra letters | Est. margin (Phase 1) |
|
||||
|------|--------------|-----------------|---------------|----------------------|
|
||||
| Pay-as-you-go | — | 0 | 49 SEK | 23–31 SEK |
|
||||
| Basic | 99 SEK/mån | 3 | 39 SEK | 13–21 SEK |
|
||||
| Pro | 199 SEK/mån | 10 | 29 SEK | 3–11 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. ~10–15 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:** ~4–6 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:** ~6–10 weeks (1 person, full-time)
|
||||
**Cost:** API fees, legal consultation (~10,000–30,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
8
opencode.json
Normal 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
42
trello-import.csv
Normal 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
|
||||
|
Loading…
Reference in a new issue