Update CLAUDE.md to match actual implementation (opgave 1-7)
Datamodel section now reflects reality: PurchaseRound's season/year was replaced with opens_at/order_deadline_at/pickup_at + a free-text name (per user feedback during task 2); WineOffering's vintage was dropped (folded into name); added the MailTemplate/MailLog models and Route's sender_name/sender_email that opgave 7 introduced; noted Order/OrderLine exist as models but have no API yet. Fase 1 task list marks 1-7 done and expands 7 into its actual 7a/7b/7c shape. Added a short auth line (JWT + sudo-style elevation) and resolved the "Mail: SMTP-udbyder — afklares" placeholder now that Postal is wired up. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
4eb8800872
commit
53a5138d45
1 changed files with 97 additions and 34 deletions
131
CLAUDE.md
131
CLAUDE.md
|
|
@ -12,65 +12,128 @@ vinbonden Horcher-familiens egne ruter i Frankrig/Belgien), selvom kun
|
|||
- Backend: FastAPI + SQLModel + PostgreSQL + Alembic (migrations)
|
||||
- Frontend: React (admin-UI) + eksisterende offentlige bestillingsside
|
||||
(fortsat intet login for deltagere)
|
||||
- Mail: SMTP-udbyder — afklares
|
||||
- Mail: Postal (selvhostet), `postal.carsteng.dk`, domæne `vinindkoeb.dk`
|
||||
- Auth: JWT (`Authorization: Bearer`), 2 timers levetid. Superadmin-
|
||||
handlinger kræver derudover en kortlivet (5 min) "eleveret" session
|
||||
(`POST /auth/elevate`, ingen ny password-indtastning) — en bevidst
|
||||
"ja, jeg mener det"-bekræftelse mod ved-uheld-handlinger, ikke et
|
||||
forsvar mod en stjålet session
|
||||
|
||||
## Datamodel
|
||||
|
||||
- **WineCategory** — global, delt af ALLE organisationer/ruter
|
||||
(samme vinbonde, samme kategoristruktur: TRADITION, SELECTION,
|
||||
LES IMPERTINENTS, GRANDS CRUS, VENDANGES TARDIVES, CREMANT, MAGNUM,
|
||||
SANS ALCOOL osv.)
|
||||
- navn, sortering
|
||||
- **WineCategory** — global, delt af ALLE organisationer/ruter (samme
|
||||
vinbonde, samme kategoristruktur). Seedet via migration med:
|
||||
TRADITION, SELECTION, LES IMPERTINENTS, GRANDS CRUS, VENDANGES
|
||||
TARDIVES, CREMANT, MAGNUM, SANS ALCOOL (i denne rækkefølge).
|
||||
- `name` (unik), `sort_order`
|
||||
|
||||
- **Organization** — administrativ gruppe (fx "Fælles Vinindkøb DK",
|
||||
"Horcher Frankrig")
|
||||
- navn
|
||||
- `name` (unik)
|
||||
|
||||
- **User** — admin-login, tilhører en Organization
|
||||
- simpelt login v1 (password-hash + session/JWT), struktureret så
|
||||
passkey/WebAuthn kan tilføjes senere uden brud
|
||||
- `email` (unik), `name`, `is_active`, `is_superadmin`,
|
||||
`hashed_password` (argon2id via `pwdlib`)
|
||||
- Passkey/WebAuthn kan tilføjes senere som en separat
|
||||
credentials-tabel uden ændringer på `User` selv
|
||||
|
||||
- **Route** — tilhører en Organization (fx "Tyskland" for jer;
|
||||
"Belgique" / "Paris" / "Massif Central" for Horcher-familien)
|
||||
- navn, mødested/kontaktperson/telefon (fritekst, forudfyldes fra
|
||||
forrige runde)
|
||||
- `name`, `meeting_info`/`contact_person`/`contact_phone` (fritekst,
|
||||
forudfyldes fra forrige runde i UI'et senere)
|
||||
- `sender_name`/`sender_email` — afsenderidentitet til mails,
|
||||
forskellig pr. rute (fx "Finn Gram - Fælles Vinindkøb
|
||||
<finn@vinindkoeb.dk>")
|
||||
|
||||
- **Participant** — tilhører en Route
|
||||
- navn, mail, telefon, is_active
|
||||
- `name`, `email`, `phone`, `is_active`
|
||||
- `email` er påkrævet ved oprettelse af nye deltagere; `phone` er
|
||||
påkrævet for nye deltagere via API'et, men nullable i databasen
|
||||
(historiske deltagere fra MongoDB-migreringen mangler det)
|
||||
- `UNIQUE(route_id, email)` — forhindrer dubletter pr. rute
|
||||
- Soft-delete via `is_active` er den normale vej (bevarer historik
|
||||
for deltagere der forlader og kommer tilbage); en rigtig `DELETE`
|
||||
findes også, kun til GDPR-sletningsanmodninger
|
||||
|
||||
- **PurchaseRound** — tilhører en Route
|
||||
- sæson (forår/efterår), år, status (kladde/åben/lukket)
|
||||
- eur_dkk_rate (valgfri — ikke alle organisationer skal omregne)
|
||||
- intro_text, pickup_info_text (forudfyldes fra forrige runde,
|
||||
kun dato/sæson skal typisk rettes)
|
||||
- `name` (frit, fx "Forår 2026" — intet sæson/år-felt, da det ikke
|
||||
er universelt på tværs af fremtidige ruter)
|
||||
- `status`: `draft` / `open` / `closed`
|
||||
- `opens_at`, `order_deadline_at`, `pickup_at` — alle tre påkrævede
|
||||
så snart status ikke er `draft` (håndhævet af en DB-constraint)
|
||||
- `eur_dkk_rate` (valgfri — ikke alle organisationer skal omregne)
|
||||
- `intro_text`, `pickup_info_text` (fritekst, forudfyldes fra
|
||||
forrige runde via "kopiér fra forrige runde")
|
||||
- Sletning: `draft` kan slettes af enhver admin; `open` kan aldrig
|
||||
slettes; `closed` kræver en eleveret superadmin
|
||||
|
||||
- **WineOffering** — tilhører PurchaseRound + (global) WineCategory
|
||||
- navn (inkl. størrelse, fx "75cl" — ikke separat felt v1)
|
||||
- vintage/årgang, pris (EUR)
|
||||
- is_organic (boolean — sat via checkbox, kilde: AB-mærket på
|
||||
producentens prisliste)
|
||||
- `name` (inkl. størrelse og årgang som fri tekst, fx "SYLVANER 2021
|
||||
- 75cl" — ingen separate felter for det)
|
||||
- `price` (EUR, `Decimal`), `is_organic` (boolean)
|
||||
|
||||
- **Order** — deltager + runde
|
||||
- payment_status (ubetalt/betalt), paid_at
|
||||
- `payment_status` (`unpaid`/`paid`), `paid_at`, `created_at`
|
||||
- *Model findes, men ingen CRUD/API endnu — kommer med opgave 8/9*
|
||||
|
||||
- **OrderLine** — ordre + wine_offering + antal
|
||||
- *Model findes, men ingen CRUD/API endnu — kommer med opgave 8/9*
|
||||
|
||||
- **MailTemplate** — tilhører Route, én pr. `(route, event_type)`
|
||||
- `event_type`: `round_announced` / `order_confirmed` /
|
||||
`payment_confirmed` (matcher de tre mail-events nedenfor)
|
||||
- `subject`, `body_html` — simpel `{{variabel}}`-substitution, ingen
|
||||
loops/logik i skabelonen (lister som vinkataloget genereres i
|
||||
kode og injiceres som færdig HTML)
|
||||
|
||||
- **MailLog** — ét forsøg pr. deltager pr. afsendelse
|
||||
- `participant_id`, `purchase_round_id`, `mail_template_id`,
|
||||
`event_type`, `rendered_subject` (denormaliseret — viser hvad der
|
||||
faktisk blev sendt, selvom skabelonen ændres senere)
|
||||
- `status`: `sent` (Postal tog imod) → `delivered`/`bounced`/
|
||||
`held`/`delayed`/`failed` (opdateret asynkront via webhook)
|
||||
- `postal_message_id`/`postal_token` (til korrelation med webhook),
|
||||
`error_message`, `sent_at`, `opened_at`, `clicked_at`
|
||||
|
||||
## Mail-events
|
||||
1. Runde åbnes → mail til alle aktive deltagere på ruten
|
||||
2. Ordre afgivet → kvittering til deltager + notifikation til admin (findes allerede)
|
||||
3. **Nyt:** Admin markerer ordre "betalt" → automatisk kvitteringsmail til deltager
|
||||
SMTP: Postal (selvhostet)
|
||||
1. Runde åbnes → mail til alle aktive deltagere på ruten. **Implementeret**
|
||||
(opgave 7a-c): skabelon pr. rute (`MailTemplate`,
|
||||
`event_type=round_announced`), udløses manuelt via
|
||||
`POST /purchase-rounds/{id}/announce`, logges i `MailLog`,
|
||||
statusopdateres løbende via Postal-webhook (`POST /webhooks/postal`,
|
||||
RSA-SHA256-signatur verificeret mod DKIM-nøglen fra DNS).
|
||||
2. Ordre afgivet → kvittering til deltager + notifikation til admin
|
||||
(findes allerede i den gamle offentlige bestillingsside — flyttes i
|
||||
opgave 8)
|
||||
3. **Nyt:** Admin markerer ordre "betalt" → automatisk kvitteringsmail
|
||||
til deltager (opgave 9)
|
||||
|
||||
Event #2 og #3 er modelleret i `MailEventType`
|
||||
(`order_confirmed`/`payment_confirmed`) og kan allerede have skabeloner
|
||||
oprettet via 7a's CRUD, men selve udløsningen (fra ordre-flowet i
|
||||
opgave 8/9) er ikke bygget endnu — 7b's afsendelses-/logmønster
|
||||
(rendering, `send_mail`, `MailLog`) er genbrugbart når det sker.
|
||||
|
||||
## Fase 1 — nuværende scope
|
||||
1. Sæt FastAPI + SQLModel + PostgreSQL op med Alembic
|
||||
2. Definér modellerne ovenfor (inkl. Organization/Route-hierarki,
|
||||
selvom kun én organisation/rute findes i dag)
|
||||
3. Manuel dataindtastning: deltagere (og evt. en aktiv runde) fra MongoDB til PostgreSQL
|
||||
4. Simpelt login til User
|
||||
5. Admin CRUD: deltagere (inkl. aktiv/inaktiv — erstatter "tom bestilling"-tricket)
|
||||
6. Admin CRUD: runder + vinliste, med "kopiér fra forrige runde"
|
||||
(alle felter, inkl. kategori, forudfyldt og frit redigerbare)
|
||||
7. "Annoncér runde": skabelonmail til alle aktive deltagere på ruten
|
||||
|
||||
**Færdige opgaver (1-7):**
|
||||
1. ✅ FastAPI + SQLModel + PostgreSQL + Alembic scaffolding
|
||||
2. ✅ Datamodellerne (Organization/Route-hierarki)
|
||||
3. ✅ Deltager-migrering fra MongoDB (308 deltagere importeret,
|
||||
dedupliceret på `(route, email)`)
|
||||
4. ✅ Login til User (JWT) + efterfølgende "sudo-stil" elevation til
|
||||
superadmin-handlinger
|
||||
5. ✅ Admin CRUD: deltagere (inkl. aktiv/inaktiv, unik email pr. rute,
|
||||
GDPR-hård-sletning)
|
||||
6. ✅ Admin CRUD: runder + vinliste, "kopiér fra forrige runde"
|
||||
(inkl. vinliste og kategori)
|
||||
7. ✅ "Annoncér runde" — se detaljer under Mail-events og nedenfor:
|
||||
- **7a**: `MailTemplate`-CRUD (`POST/GET/PATCH/DELETE /mail-templates`)
|
||||
- **7b**: Afsendelse via Postal + `MailLog`
|
||||
(`POST /purchase-rounds/{id}/announce`, `GET /mail-logs`)
|
||||
- **7c**: Postal-webhook til leveringsstatus (`POST /webhooks/postal`)
|
||||
|
||||
**Resterende opgaver:**
|
||||
8. Flyt offentlig bestillingsformular til nyt API (fortsat uden login)
|
||||
9. Admin-ordreoversigt pr. runde med "markér betalt" → trigger mail #3
|
||||
10. Genskab afhentningsliste (HTML-udtræk til print) mod ny datamodel
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue