package store import ( "context" "encoding/json" "errors" "fmt" "time" "github.com/jackc/pgx/v5" ) // BewertungAnforderung ist eine abgeleitete Anforderung mit Herleitung, // wie sie in bewertung.anforderungen (JSONB) gespeichert wird. type BewertungAnforderung struct { ID string `json:"id"` Beschreibung string `json:"beschreibung"` Herleitung string `json:"herleitung"` } // BewertungAusschluss hält fest, warum ein Werkzeug für eine Bewertung // ausgeschlossen wurde. type BewertungAusschluss struct { WerkzeugID string `json:"werkzeug_id"` NichtErfuellt []string `json:"nicht_erfuellt"` } // Bewertung ist der berechnete Vorschlag für einen Antrag. Append-only // — siehe Migration. type Bewertung struct { ID string AntragID string Datenklasse string DatenklasseHerleitung string Einstufung string EinstufungHerleitung string Verboten bool Anforderungen []BewertungAnforderung ZulaessigeWerkzeuge []string AusgeschlosseneWerkzeuge []BewertungAusschluss RegelwerkVersion string KatalogVersion string CreatedAt time.Time } const bewertungColumns = `id, antrag_id, datenklasse, datenklasse_herleitung, einstufung, einstufung_herleitung, verboten, anforderungen, zulaessige_werkzeuge, ausgeschlossene_werkzeuge, regelwerk_version, katalog_version, created_at` func scanBewertung(row interface { Scan(dest ...any) error }) (Bewertung, error) { var b Bewertung var anforderungenRaw, ausschlussRaw []byte err := row.Scan( &b.ID, &b.AntragID, &b.Datenklasse, &b.DatenklasseHerleitung, &b.Einstufung, &b.EinstufungHerleitung, &b.Verboten, &anforderungenRaw, &b.ZulaessigeWerkzeuge, &ausschlussRaw, &b.RegelwerkVersion, &b.KatalogVersion, &b.CreatedAt, ) if err != nil { return Bewertung{}, err } if err := json.Unmarshal(anforderungenRaw, &b.Anforderungen); err != nil { return Bewertung{}, fmt.Errorf("store: anforderungen unmarshal: %w", err) } if err := json.Unmarshal(ausschlussRaw, &b.AusgeschlosseneWerkzeuge); err != nil { return Bewertung{}, fmt.Errorf("store: ausgeschlossene_werkzeuge unmarshal: %w", err) } return b, nil } // BewertungInput bündelt die Felder einer neuen Bewertung — bei elf // Werten lesbarer als eine positionale Parameterliste (gleiches Muster // wie WerkzeugInput). type BewertungInput struct { AntragID string Datenklasse string DatenklasseHerleitung string Einstufung string EinstufungHerleitung string Verboten bool Anforderungen []BewertungAnforderung ZulaessigeWerkzeuge []string AusgeschlosseneWerkzeuge []BewertungAusschluss RegelwerkVersion string KatalogVersion string } // CreateBewertung speichert eine berechnete Bewertung für einen Antrag. func (s *Store) CreateBewertung(ctx context.Context, in BewertungInput) (Bewertung, error) { if in.ZulaessigeWerkzeuge == nil { in.ZulaessigeWerkzeuge = []string{} } if in.Anforderungen == nil { in.Anforderungen = []BewertungAnforderung{} } if in.AusgeschlosseneWerkzeuge == nil { in.AusgeschlosseneWerkzeuge = []BewertungAusschluss{} } anforderungenJSON, err := json.Marshal(in.Anforderungen) if err != nil { return Bewertung{}, fmt.Errorf("store: anforderungen marshal: %w", err) } ausschlussJSON, err := json.Marshal(in.AusgeschlosseneWerkzeuge) if err != nil { return Bewertung{}, fmt.Errorf("store: ausgeschlossene_werkzeuge marshal: %w", err) } row := s.Pool.QueryRow(ctx, ` INSERT INTO bewertung ( antrag_id, datenklasse, datenklasse_herleitung, einstufung, einstufung_herleitung, verboten, anforderungen, zulaessige_werkzeuge, ausgeschlossene_werkzeuge, regelwerk_version, katalog_version ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) RETURNING `+bewertungColumns, in.AntragID, in.Datenklasse, in.DatenklasseHerleitung, in.Einstufung, in.EinstufungHerleitung, in.Verboten, anforderungenJSON, in.ZulaessigeWerkzeuge, ausschlussJSON, in.RegelwerkVersion, in.KatalogVersion, ) b, err := scanBewertung(row) if err != nil { return Bewertung{}, fmt.Errorf("store: create bewertung: %w", err) } return b, nil } // GetLatestBewertungForAntrag liefert die zuletzt berechnete Bewertung // eines Antrags. Liefert ErrNotFound, wenn noch keine Bewertung // existiert. func (s *Store) GetLatestBewertungForAntrag(ctx context.Context, antragID string) (Bewertung, error) { row := s.Pool.QueryRow(ctx, ` SELECT `+bewertungColumns+` FROM bewertung WHERE antrag_id = $1 ORDER BY created_at DESC LIMIT 1 `, antragID) b, err := scanBewertung(row) if errors.Is(err, pgx.ErrNoRows) { return Bewertung{}, ErrNotFound } if err != nil { return Bewertung{}, fmt.Errorf("store: get latest bewertung: %w", err) } return b, nil }