feat: Registereintrag und Export (Schritt 6 der Baureihenfolge)

Jede Genehmigung (genehmigt/genehmigt_mit_auflagen) erzeugt jetzt
automatisch einen append-only registereintrag mit Zweck, Abteilung,
Werkzeug, Datenklasse, Einstufung, Auflagen, Verantwortliche/r und
Gültigkeit. Ebene 3 (verantwortlicher/pruefer) kann das Register unter
GET /registereintraege einsehen und als CSV oder PDF exportieren. Der
PDF-Export nutzt das bereits vorhandene, bisher ungenutzte go-pdf/fpdf
(keine neue Abhängigkeit) mit UnicodeTranslatorFromDescriptor für
deutsche Umlaute. Abgelehnte Entscheidungen und Rückfragen erzeugen
bewusst keinen Registereintrag.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
noroot
2026-08-29 12:55:24 +02:00
parent cff8c9eadf
commit d52b325424
12 changed files with 672 additions and 33 deletions

View File

@@ -24,31 +24,33 @@ const testSessionCookie = "deklarix_session"
// fakeStore ist eine In-Memory-Implementierung von web.Store, damit die
// Handler-Tests keine echte Postgres-Instanz brauchen.
type fakeStore struct {
mu sync.Mutex
nextID int
accounts map[string]store.Account
users map[string]store.User
usersByEmail map[string]string // email -> user id
sessions map[string]store.Session
auditLog []store.AuditEntry
abteilungen map[string][]store.Abteilung // accountID -> Abteilungen
antraege map[string]store.Antrag
werkzeuge map[string]store.Werkzeug
bewertungen map[string][]store.Bewertung // antragID -> Bewertungen, älteste zuerst
entscheidungen map[string][]store.Entscheidung // antragID -> Entscheidungen, älteste zuerst
mu sync.Mutex
nextID int
accounts map[string]store.Account
users map[string]store.User
usersByEmail map[string]string // email -> user id
sessions map[string]store.Session
auditLog []store.AuditEntry
abteilungen map[string][]store.Abteilung // accountID -> Abteilungen
antraege map[string]store.Antrag
werkzeuge map[string]store.Werkzeug
bewertungen map[string][]store.Bewertung // antragID -> Bewertungen, älteste zuerst
entscheidungen map[string][]store.Entscheidung // antragID -> Entscheidungen, älteste zuerst
registereintraege map[string][]store.Registereintrag // accountID -> Registereintraege
}
func newFakeStore() *fakeStore {
return &fakeStore{
accounts: map[string]store.Account{},
users: map[string]store.User{},
usersByEmail: map[string]string{},
sessions: map[string]store.Session{},
abteilungen: map[string][]store.Abteilung{},
antraege: map[string]store.Antrag{},
werkzeuge: map[string]store.Werkzeug{},
bewertungen: map[string][]store.Bewertung{},
entscheidungen: map[string][]store.Entscheidung{},
accounts: map[string]store.Account{},
users: map[string]store.User{},
usersByEmail: map[string]string{},
sessions: map[string]store.Session{},
abteilungen: map[string][]store.Abteilung{},
antraege: map[string]store.Antrag{},
werkzeuge: map[string]store.Werkzeug{},
bewertungen: map[string][]store.Bewertung{},
entscheidungen: map[string][]store.Entscheidung{},
registereintraege: map[string][]store.Registereintrag{},
}
}
@@ -187,6 +189,19 @@ func (f *fakeStore) ListAbteilungenForAccount(ctx context.Context, accountID str
return f.abteilungen[accountID], nil
}
func (f *fakeStore) GetAbteilung(ctx context.Context, id string) (store.Abteilung, error) {
f.mu.Lock()
defer f.mu.Unlock()
for _, list := range f.abteilungen {
for _, a := range list {
if a.ID == id {
return a, nil
}
}
}
return store.Abteilung{}, store.ErrNotFound
}
func (f *fakeStore) CreateAntrag(ctx context.Context, accountID, erstellerUserID string, abteilungID *string, titel string) (store.Antrag, error) {
f.mu.Lock()
defer f.mu.Unlock()
@@ -385,6 +400,26 @@ func (f *fakeStore) GetLatestEntscheidungForAntrag(ctx context.Context, antragID
return es[len(es)-1], nil
}
func (f *fakeStore) CreateRegistereintrag(ctx context.Context, in store.RegistereintragInput) (store.Registereintrag, error) {
f.mu.Lock()
defer f.mu.Unlock()
e := store.Registereintrag{
ID: f.newID(), AccountID: in.AccountID, AntragID: in.AntragID, EntscheidungID: in.EntscheidungID,
Zweck: in.Zweck, Abteilung: in.Abteilung, Werkzeug: in.Werkzeug,
Datenklasse: in.Datenklasse, Einstufung: in.Einstufung, Auflagen: in.Auflagen,
Verantwortlicher: in.Verantwortlicher, EntschiedenAm: in.EntschiedenAm, GueltigBis: in.GueltigBis,
CreatedAt: time.Now(),
}
f.registereintraege[in.AccountID] = append(f.registereintraege[in.AccountID], e)
return e, nil
}
func (f *fakeStore) ListRegistereintraegeForAccount(ctx context.Context, accountID string) ([]store.Registereintrag, error) {
f.mu.Lock()
defer f.mu.Unlock()
return f.registereintraege[accountID], nil
}
// ─── Test-Setup ───────────────────────────────────────────────────────
// loadTestRegelwerk lädt die echten rules/*.yaml-Dateien — dieselben,