AGENTS.md:
- Add "./gradlew coverage" to All-in-one quick-start section
- Add "npm run test:coverage" to Frontend commands
- Add Coverage section: command, threshold table (70% lines, 60%
branches, 70% functions), HTML report paths for both layers
- Note that coverage is enforced during ./gradlew check
CODING_GUIDELINES.md:
- Section 1 (General Principles): add "Treat warnings as mistakes"
rule — LSP diagnostics, compiler warnings, and lint warnings are
bugs that must be fixed before commit
- Known false positives (Lombok, getActivePinia) must be suppressed
explicitly at the narrowest scope with a comment explaining why
- Uncommented suppressions are treated as errors
- Section 7 (Testing): add Coverage subsection with thresholds table,
command reference, report paths, and enforcement rule (PRs must
maintain or improve coverage)
352 lines
13 KiB
Markdown
352 lines
13 KiB
Markdown
# 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.
|
|
- **Treat warnings as mistakes.** LSP diagnostics, compiler warnings, and lint
|
|
warnings are bugs. Never commit code that produces them. If a warning is a
|
|
known false positive (e.g. Lombok `@RequiredArgsConstructor` triggering
|
|
"uninitialized final field"), suppress it explicitly at the narrowest scope
|
|
with a comment explaining why:
|
|
- Java: `@SuppressWarnings("...") // Lombok generates constructor`
|
|
- TypeScript: `// @ts-expect-error — pinia getActivePinia returns null in test context`
|
|
Uncommented suppressions are indistinguishable from ignoring a real problem
|
|
and are treated as errors.
|
|
|
|
---
|
|
|
|
## 2. Git
|
|
|
|
### Branch Model
|
|
|
|
Three permanent branches, plus short-lived work branches:
|
|
|
|
```
|
|
master ──────────────────────────────────────●──● (production, deployed)
|
|
↗ ↗
|
|
develop ────────●──────────●────────●───────●──● (integration)
|
|
↗ ↗ ↗
|
|
feature/foo ──●──●──●───→┘ │
|
|
│
|
|
feature/bar ───────────────────────●──●─────→┘
|
|
```
|
|
|
|
| Branch | Purpose | Branches from | Merges to |
|
|
|--------|---------|---------------|-----------|
|
|
| `master` | Production — what runs on the server | — | — |
|
|
| `develop` | Integration — all work branches land here | `master` | `master` (on release) |
|
|
| `feature/*` | New feature work | `develop` | `develop` |
|
|
| `fix/*` | Bug fix | `develop` | `develop` |
|
|
| `chore/*` | Maintenance, upgrades, docs | `develop` | `develop` |
|
|
|
|
### Rules
|
|
|
|
- **Never commit directly to `master` or `develop`.** All work happens in feature/fix/chore branches.
|
|
- **Merge strategy**: fast-forward or merge — either is fine for a solo dev project. Prefer simple over complex.
|
|
- **Keep branches short-lived.** Merge back to `develop` within a few days. Don't let branches drift.
|
|
- **Update before merging.** Pull latest `develop` into your branch before merging back to avoid surprises.
|
|
- **Tag releases on `master`**: `v0.1.0`, `v0.2.0`, etc. These mark deployable states.
|
|
- **Rebase**: only rebase your own unpublished branches. Once pushed/shared, merge instead.
|
|
- **CI triggers**: push to `develop` → run tests. Push to `master` → run tests + deploy.
|
|
- **Squash trivial fix commits** before pushing (typo fixes, formatting, etc.).
|
|
- **Never commit `.env`, credentials, or secrets. `.env.example` only.**
|
|
|
|
### 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`
|
|
|
|
---
|
|
|
|
## 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.ts`, `usePayment.ts` |
|
|
| Store | camelCase, in `stores/` | `authStore.ts`, `orderStore.ts` |
|
|
| API module | camelCase, in `api/` | `orders.ts`, `templates.ts` |
|
|
|
|
### 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/*.ts` modules, not in components.
|
|
- Use `fetch` or `axios` via a single configured instance (base URL, auth header interceptor).
|
|
|
|
---
|
|
|
|
## 4. Backend — Spring Boot 4
|
|
|
|
### 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/`.
|
|
- Lombok: `@RequiredArgsConstructor`, `@Getter`, `@Setter`, `@NoArgsConstructor` are all fine. Prefer records for DTOs.
|
|
|
|
### 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.ts — 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
|
|
|
|
This project follows **Test-Driven Development (TDD)**. Write tests before
|
|
or alongside implementation. Every feature ticket should include tests in
|
|
the same PR — never merge code without corresponding tests.
|
|
|
|
- Backend: JUnit 5 + Mockito. Service layer tests as unit tests. Controller tests with `@WebMvcTest`.
|
|
- Frontend: Vitest for composables and utility functions. Component tests with Vue Test Utils.
|
|
- E2E: Playwright (`npm run test:e2e`). Tests in `frontend/e2e/`. Requires `docker compose up`.
|
|
- Test naming: `shouldXxxWhenYyy` — e.g., `shouldReturn404WhenPlateNotFound`.
|
|
- Aim for test coverage on business logic, not on getters/setters/boilerplate.
|
|
- All database interaction in tests must go through JPA repositories
|
|
or EntityManager. Never use JdbcTemplate, DataSource queries, or
|
|
raw SQL in test code. Tests interact with the database the same way
|
|
production code does: through the ORM.
|
|
|
|
### Coverage
|
|
|
|
```bash
|
|
./gradlew coverage # backend + frontend tests with coverage
|
|
```
|
|
|
|
Coverage is enforced via `./gradlew check`. Thresholds:
|
|
|
|
| Layer | Lines | Branches | Functions |
|
|
|----------|-------|----------|-----------|
|
|
| Backend | 70% | 60% | — |
|
|
| Frontend | 70% | 60% | 70% |
|
|
|
|
- Backend: JaCoCo (`backend/build/reports/jacoco/index.html`).
|
|
- Frontend: Vitest v8 provider (`frontend/coverage/index.html`).
|
|
- PRs must maintain or improve coverage levels. If a new feature changes
|
|
coverage, update the test suite — never lower thresholds without discussion.
|
|
|
|
---
|
|
|
|
## 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
|