feat: Posteingang und Entscheiden für die Fachebene (Schritt 5 der Baureihenfolge)
Ebene 3 (Rollen verantwortlicher/pruefer) bekommt GET /faelle
(Posteingang aller offenen Anträge des Mandanten) und GET/POST
/faelle/{id} zum Entscheiden. Eine Entscheidung friert Regelwerk-,
Katalog- und den vollständigen Werkzeugdatensatz ein, verlangt eine
Begründung bei Abweichung vom abgeleiteten Vorschlag, erlaubt bei
Genehmigung nur ein durch die Bewertung zulässiges Werkzeug und
protokolliert die Entscheidung im Audit-Log. entscheidung ist
append-only wie bewertung.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -24,29 +24,31 @@ 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
|
||||
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
|
||||
}
|
||||
|
||||
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{},
|
||||
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{},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -243,6 +245,18 @@ func (f *fakeStore) ListAntraegeForUser(ctx context.Context, erstellerUserID str
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (f *fakeStore) ListAntraegeForAccount(ctx context.Context, accountID string) ([]store.Antrag, error) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
var out []store.Antrag
|
||||
for _, a := range f.antraege {
|
||||
if a.AccountID == accountID {
|
||||
out = append(out, a)
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (f *fakeStore) ListWerkzeugeForAccount(ctx context.Context, accountID string) ([]store.Werkzeug, error) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
@@ -297,6 +311,28 @@ func (f *fakeStore) GetLatestBewertungForAntrag(ctx context.Context, antragID st
|
||||
return bs[len(bs)-1], nil
|
||||
}
|
||||
|
||||
func (f *fakeStore) CreateEntscheidung(ctx context.Context, in store.EntscheidungInput) (store.Entscheidung, error) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
e := store.Entscheidung{
|
||||
ID: f.newID(), AntragID: in.AntragID, BewertungID: in.BewertungID, EntscheiderUserID: in.EntscheiderUserID,
|
||||
Entscheidung: in.Entscheidung, WerkzeugID: in.WerkzeugID, WerkzeugSnapshot: in.WerkzeugSnapshot,
|
||||
Begruendung: in.Begruendung, GueltigBis: in.GueltigBis, CreatedAt: time.Now(),
|
||||
}
|
||||
f.entscheidungen[in.AntragID] = append(f.entscheidungen[in.AntragID], e)
|
||||
return e, nil
|
||||
}
|
||||
|
||||
func (f *fakeStore) GetLatestEntscheidungForAntrag(ctx context.Context, antragID string) (store.Entscheidung, error) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
es := f.entscheidungen[antragID]
|
||||
if len(es) == 0 {
|
||||
return store.Entscheidung{}, store.ErrNotFound
|
||||
}
|
||||
return es[len(es)-1], nil
|
||||
}
|
||||
|
||||
// ─── Test-Setup ───────────────────────────────────────────────────────
|
||||
|
||||
// loadTestRegelwerk lädt die echten rules/*.yaml-Dateien — dieselben,
|
||||
@@ -358,6 +394,31 @@ func seedAccountWithRole(t *testing.T, fs *fakeStore, accountName, email, role s
|
||||
return &http.Cookie{Name: testSessionCookie, Value: token}
|
||||
}
|
||||
|
||||
// seedUserInAccount legt einen weiteren Nutzer in einem bereits
|
||||
// existierenden Account an — für Tests, die mehrere Rollen im selben
|
||||
// Mandanten brauchen (z. B. mitarbeiter stellt einen Antrag,
|
||||
// verantwortlicher entscheidet darüber).
|
||||
func seedUserInAccount(t *testing.T, fs *fakeStore, accountID, email, role string) *http.Cookie {
|
||||
t.Helper()
|
||||
ctx := context.Background()
|
||||
hash, err := auth.HashPassword("test-passwort-123")
|
||||
if err != nil {
|
||||
t.Fatalf("HashPassword: %v", err)
|
||||
}
|
||||
user, err := fs.CreateUser(ctx, accountID, email, hash, role)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateUser: %v", err)
|
||||
}
|
||||
token, err := auth.NewSessionToken()
|
||||
if err != nil {
|
||||
t.Fatalf("NewSessionToken: %v", err)
|
||||
}
|
||||
if _, err := fs.CreateSession(ctx, token, user.ID, time.Now().Add(time.Hour)); err != nil {
|
||||
t.Fatalf("CreateSession: %v", err)
|
||||
}
|
||||
return &http.Cookie{Name: testSessionCookie, Value: token}
|
||||
}
|
||||
|
||||
func seedAccount(t *testing.T, fs *fakeStore, accountName, email string) *http.Cookie {
|
||||
t.Helper()
|
||||
return seedAccountWithRole(t, fs, accountName, email, "mitarbeiter")
|
||||
|
||||
Reference in New Issue
Block a user