docs: add sprint planning — dependency diagram + specs for all 8 stories

Analyzes all Taiga stories, maps hard/soft dependencies, and writes
full specs (Goal, Data Model, Components, Verification, Open Questions)
for every story before any implementation begins.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-16 11:53:24 +02:00
co-authored by Claude Sonnet 4.6
commit 1070ba3ac1
10 changed files with 1239 additions and 0 deletions
+157
View File
@@ -0,0 +1,157 @@
# Spec: 02 · Manage Fruits
**Taiga Story:** #2 · `02-manage-fruits`
**Status:** Ready
**Branch:** `feature/02-manage-fruits`
**Depends on:** `01-Bootstrap`
---
## 1. Goal
Maintainers can create, read, update, and delete fruits. Each fruit has a name, OSDB Kürzel (unique number), optional comment, synonyms list, a required type, and an images collection.
---
## 2. Background / Scope / Context / Constraints
- "OSDB Kürzel" is the public label for the internal `osdb_number` field (unique identifier used by legacy system).
- Fruit type is a fixed enum — no free-form entry.
- Images have an image type (`fruit`, `flower`, `tree`), a filename, and binary data stored in the DB.
- Synonyms are a 1:N child table (not a comma-separated string in the parent row).
- API must support listing, filtering (prepare for story #6), and CRUD.
- No authentication guard yet (added in story #8); all endpoints are public for now.
### Fruit Type Enum
```
Apfelsorten, Birnensorten, Quittensorten, Aprikosen, Pfirsiche,
Mirabellen, Renekloden, Pflaumen, Zwetschen, Sauerkirschen,
Süßkirschen, Brombeeren, Erdbeeren, Himbeeren, Johannisbeeren,
Stachelbeeren, Wein
```
---
## 3. Necessary Refactorings
None from prior stories (only bootstrap exists). Echo router structure should be established here as `GET /api/v1/fruits`, `POST /api/v1/fruits`, etc.
---
## 4. Related Data Model
```sql
-- Migration: 000002_create_fruits.up.sql
CREATE TYPE fruit_type AS ENUM (
'Apfelsorten', 'Birnensorten', 'Quittensorten', 'Aprikosen',
'Pfirsiche', 'Mirabellen', 'Renekloden', 'Pflaumen', 'Zwetschen',
'Sauerkirschen', 'Süßkirschen', 'Brombeeren', 'Erdbeeren',
'Himbeeren', 'Johannisbeeren', 'Stachelbeeren', 'Wein'
);
CREATE TABLE fruits (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
osdb_number VARCHAR(50) NOT NULL UNIQUE,
comment TEXT,
fruit_type fruit_type NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE fruit_synonyms (
id SERIAL PRIMARY KEY,
fruit_id INTEGER NOT NULL REFERENCES fruits(id) ON DELETE CASCADE,
synonym VARCHAR(255) NOT NULL
);
CREATE TABLE fruit_images (
id SERIAL PRIMARY KEY,
fruit_id INTEGER NOT NULL REFERENCES fruits(id) ON DELETE CASCADE,
filename VARCHAR(255),
data BYTEA NOT NULL,
image_type VARCHAR(20) NOT NULL CHECK (image_type IN ('fruit','flower','tree')),
title VARCHAR(255),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Note: thumbnail_data column added in story #07 migration
```
---
## 5. Affected Places / Related Components
### Backend
| File | Purpose |
|------|---------|
| `backend/internal/domain/fruit.go` | `Fruit`, `FruitSynonym`, `FruitImage` structs |
| `backend/internal/repository/fruit_repo.go` | DB queries (list, get, create, update, delete, images) |
| `backend/internal/handler/fruit_handler.go` | Echo handlers — request bind/validate, call repo, return JSON |
| `backend/cmd/server/router.go` | Register `/api/v1/fruits` route group |
| `migrations/000002_create_fruits.up.sql` | Schema as above |
| `migrations/000002_create_fruits.down.sql` | DROP TABLE fruit_images, fruit_synonyms, fruits; DROP TYPE fruit_type |
### Frontend
| File | Purpose |
|------|---------|
| `frontend/src/api/fruits.ts` | Axios/fetch wrapper — listFruits, getFruit, createFruit, updateFruit, deleteFruit, addImage, deleteImage |
| `frontend/src/stores/fruitStore.ts` | Pinia store |
| `frontend/src/views/FruitList.vue` | Table/card list, search bar (placeholder for #6), link to detail |
| `frontend/src/views/FruitDetail.vue` | Show + edit form, synonyms editor, image gallery with upload/delete |
| `frontend/src/views/FruitCreate.vue` | Create form |
| `frontend/src/router/index.ts` | Add routes: `/fruits`, `/fruits/:id`, `/fruits/new` |
### API Endpoints
| Method | Path | Description |
|--------|------|-------------|
| GET | `/api/v1/fruits` | List (query params: `name`, `type`) |
| POST | `/api/v1/fruits` | Create |
| GET | `/api/v1/fruits/:id` | Get by ID (includes synonyms) |
| PUT | `/api/v1/fruits/:id` | Full update |
| DELETE | `/api/v1/fruits/:id` | Delete (cascades images+synonyms) |
| GET | `/api/v1/fruits/:id/images` | List images (metadata only, no binary) |
| POST | `/api/v1/fruits/:id/images` | Upload image (multipart) |
| GET | `/api/v1/fruits/:id/images/:imageId` | Serve binary image |
| DELETE | `/api/v1/fruits/:id/images/:imageId` | Delete image |
---
## 6. Out of Scope
- Search/filter implementation (story #6)
- Authentication guards (story #8)
- Thumbnail generation (story #7)
- Bulk import (story #3)
- Publications link (story #4)
---
## 7. Verification
| Check | Expected |
|-------|---------|
| `POST /api/v1/fruits` with missing name | 422 Unprocessable Entity |
| `POST /api/v1/fruits` with duplicate osdb_number | 409 Conflict |
| `GET /api/v1/fruits/:id` | Returns fruit with synonyms array |
| `POST /api/v1/fruits/:id/images` (multipart JPEG) | 201 Created; GET image serves correct bytes |
| `DELETE /api/v1/fruits/:id` | 204; cascades synonyms+images |
| Frontend: create fruit → appears in list | No page reload needed (Pinia reactive) |
| Frontend: upload image → thumbnail shown | Image served from backend |
| `go test ./...` | All handler + repo tests green |
| `npm run test` | Vitest tests for store and API wrapper green |
---
## 8. Open Questions
| # | Question | Impact |
|---|----------|--------|
| 1 | Image upload max size limit? | Multipart config, DB row size |
| 2 | Should `GET /api/v1/fruits` return image URLs or inline base64 thumbnails? | Network perf vs simplicity |
| 3 | Pagination for fruit list (offset/limit) or full list? | UX + performance with large dataset |
| 4 | PUT vs PATCH for updates? | Client complexity |