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