package store import ( "context" "encoding/json" "errors" "fmt" "time" "github.com/jackc/pgx/v5" ) // Entscheidung ist die Entscheidung eines/einer Verantwortlichen über // einen Antrag, auf Basis einer Bewertung (Vorschlag). Append-only — // siehe Migration. WerkzeugSnapshot ist der eingefrorene, vollständige // Werkzeug-Datensatz zum Entscheidungszeitpunkt (nil, wenn kein // Werkzeug gewählt wurde, z. B. bei "abgelehnt") — ein späterer // Katalog-Wandel darf nicht rückwirkend verändern, worauf die // Entscheidung beruhte. type Entscheidung struct { ID string AntragID string BewertungID string EntscheiderUserID string Entscheidung string WerkzeugID *string WerkzeugSnapshot *Werkzeug Begruendung string GueltigBis *time.Time CreatedAt time.Time } const entscheidungColumns = `id, antrag_id, bewertung_id, entscheider_user_id, entscheidung, werkzeug_id, werkzeug_snapshot, begruendung, gueltig_bis, created_at` func scanEntscheidung(row interface { Scan(dest ...any) error }) (Entscheidung, error) { var e Entscheidung var snapshotRaw []byte err := row.Scan( &e.ID, &e.AntragID, &e.BewertungID, &e.EntscheiderUserID, &e.Entscheidung, &e.WerkzeugID, &snapshotRaw, &e.Begruendung, &e.GueltigBis, &e.CreatedAt, ) if err != nil { return Entscheidung{}, err } if snapshotRaw != nil { var w Werkzeug if err := json.Unmarshal(snapshotRaw, &w); err != nil { return Entscheidung{}, fmt.Errorf("store: werkzeug_snapshot unmarshal: %w", err) } e.WerkzeugSnapshot = &w } return e, nil } // EntscheidungInput bündelt die Felder einer neuen Entscheidung. type EntscheidungInput struct { AntragID string BewertungID string EntscheiderUserID string Entscheidung string WerkzeugID *string WerkzeugSnapshot *Werkzeug Begruendung string GueltigBis *time.Time } // CreateEntscheidung speichert eine Entscheidung über einen Antrag. func (s *Store) CreateEntscheidung(ctx context.Context, in EntscheidungInput) (Entscheidung, error) { var snapshotJSON []byte if in.WerkzeugSnapshot != nil { var err error snapshotJSON, err = json.Marshal(in.WerkzeugSnapshot) if err != nil { return Entscheidung{}, fmt.Errorf("store: werkzeug_snapshot marshal: %w", err) } } row := s.Pool.QueryRow(ctx, ` INSERT INTO entscheidung ( antrag_id, bewertung_id, entscheider_user_id, entscheidung, werkzeug_id, werkzeug_snapshot, begruendung, gueltig_bis ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING `+entscheidungColumns, in.AntragID, in.BewertungID, in.EntscheiderUserID, in.Entscheidung, in.WerkzeugID, snapshotJSON, in.Begruendung, in.GueltigBis, ) e, err := scanEntscheidung(row) if err != nil { return Entscheidung{}, fmt.Errorf("store: create entscheidung: %w", err) } return e, nil } // GetLatestEntscheidungForAntrag liefert die zuletzt getroffene // Entscheidung eines Antrags. Liefert ErrNotFound, wenn noch keine // Entscheidung existiert. func (s *Store) GetLatestEntscheidungForAntrag(ctx context.Context, antragID string) (Entscheidung, error) { row := s.Pool.QueryRow(ctx, ` SELECT `+entscheidungColumns+` FROM entscheidung WHERE antrag_id = $1 ORDER BY created_at DESC LIMIT 1 `, antragID) e, err := scanEntscheidung(row) if errors.Is(err, pgx.ErrNoRows) { return Entscheidung{}, ErrNotFound } if err != nil { return Entscheidung{}, fmt.Errorf("store: get latest entscheidung: %w", err) } return e, nil } // ListAktiveGenehmigungenForAccount liefert alle Genehmigungen // ("genehmigt"/"genehmigt_mit_auflagen") eines Mandanten — Grundlage // für die Wiedervorlage (Schritt 7): abgelaufene/bald ablaufende // Genehmigungen und solche, deren zugrunde liegendes Werkzeug sich seit // der Entscheidung im Katalog geändert hat, muss der/die Verantwortliche // erneut prüfen. entscheidung trägt selbst kein account_id — der Bezug // zum Mandanten läuft über den zugehörigen antrag. func (s *Store) ListAktiveGenehmigungenForAccount(ctx context.Context, accountID string) ([]Entscheidung, error) { rows, err := s.Pool.Query(ctx, ` SELECT e.id, e.antrag_id, e.bewertung_id, e.entscheider_user_id, e.entscheidung, e.werkzeug_id, e.werkzeug_snapshot, e.begruendung, e.gueltig_bis, e.created_at FROM entscheidung e JOIN antrag a ON a.id = e.antrag_id WHERE a.account_id = $1 AND e.entscheidung IN ('genehmigt', 'genehmigt_mit_auflagen') ORDER BY e.created_at DESC `, accountID) if err != nil { return nil, fmt.Errorf("store: list aktive genehmigungen for account: %w", err) } defer rows.Close() var out []Entscheidung for rows.Next() { e, err := scanEntscheidung(rows) if err != nil { return nil, fmt.Errorf("store: scan entscheidung: %w", err) } out = append(out, e) } if err := rows.Err(); err != nil { return nil, fmt.Errorf("store: list aktive genehmigungen for account: %w", err) } return out, nil }