package store import ( "context" "errors" "fmt" "time" "github.com/jackc/pgx/v5" ) // Antrag ist ein Vorhaben aus dem Fragebogen (Abschnitt A) mit den // vollständigen Antworten aus B/C/D als JSON — der Fragebogen ist // adaptiv (Folgefragen hängen von vorherigen Antworten ab), ein starres // Spaltenschema könnte das nicht abbilden. Antworten ist bewusst // []byte (rohes JSON), nicht ein aufgelöster Go-Typ — die Struktur der // Antworten ist Sache von internal/rules (Stufe 2), store speichert nur. type Antrag struct { ID string AccountID string ErstellerUserID string AbteilungID *string Titel string Beschreibung string Ergebnis string Haeufigkeit string Antworten []byte Status string CreatedAt time.Time UpdatedAt time.Time } const antragColumns = `id, account_id, ersteller_user_id, abteilung_id, titel, beschreibung, ergebnis, haeufigkeit, antworten, status, created_at, updated_at` func scanAntrag(row interface { Scan(dest ...any) error }) (Antrag, error) { var a Antrag err := row.Scan( &a.ID, &a.AccountID, &a.ErstellerUserID, &a.AbteilungID, &a.Titel, &a.Beschreibung, &a.Ergebnis, &a.Haeufigkeit, &a.Antworten, &a.Status, &a.CreatedAt, &a.UpdatedAt, ) return a, err } // CreateAntrag legt einen neuen Antrag im Status "entwurf" an. func (s *Store) CreateAntrag(ctx context.Context, accountID, erstellerUserID string, abteilungID *string, titel string) (Antrag, error) { row := s.db(ctx).QueryRow(ctx, ` INSERT INTO antrag (account_id, ersteller_user_id, abteilung_id, titel) VALUES ($1, $2, $3, $4) RETURNING `+antragColumns, accountID, erstellerUserID, abteilungID, titel, ) a, err := scanAntrag(row) if err != nil { return Antrag{}, fmt.Errorf("store: create antrag: %w", err) } return a, nil } // GetAntrag liest einen Antrag anhand seiner ID — ohne Mandanten-Prüfung, // das ist Sache des Aufrufers (siehe Antrag.AccountID). func (s *Store) GetAntrag(ctx context.Context, id string) (Antrag, error) { row := s.db(ctx).QueryRow(ctx, `SELECT `+antragColumns+` FROM antrag WHERE id = $1`, id) a, err := scanAntrag(row) if errors.Is(err, pgx.ErrNoRows) { return Antrag{}, ErrNotFound } if err != nil { return Antrag{}, fmt.Errorf("store: get antrag: %w", err) } return a, nil } // UpdateAntragFelder aktualisiert die Fragebogen-Felder eines Antrags — // solange er im Entwurf ist, kann der Fragebogen adaptiv weiter // ausgefüllt werden (Sache der Anwendungsschicht, store erzwingt den // Status hier nicht). func (s *Store) UpdateAntragFelder(ctx context.Context, id, titel, beschreibung, ergebnis, haeufigkeit string, antworten []byte) (Antrag, error) { row := s.db(ctx).QueryRow(ctx, ` UPDATE antrag SET titel = $2, beschreibung = $3, ergebnis = $4, haeufigkeit = $5, antworten = $6, updated_at = now() WHERE id = $1 RETURNING `+antragColumns, id, titel, beschreibung, ergebnis, haeufigkeit, antworten, ) a, err := scanAntrag(row) if errors.Is(err, pgx.ErrNoRows) { return Antrag{}, ErrNotFound } if err != nil { return Antrag{}, fmt.Errorf("store: update antrag felder: %w", err) } return a, nil } // SetAntragStatus setzt den Status eines Antrags (entwurf -> eingereicht // -> entschieden). Antrag ist, anders als bewertung/entscheidung, NICHT // append-only — der Lebenszyklus ist eine normale Zustandsänderung. func (s *Store) SetAntragStatus(ctx context.Context, id, status string) error { tag, err := s.db(ctx).Exec(ctx, `UPDATE antrag SET status = $2, updated_at = now() WHERE id = $1`, id, status) if err != nil { return fmt.Errorf("store: set antrag status: %w", err) } if tag.RowsAffected() == 0 { return ErrNotFound } return nil } // ListAntraegeForUser liefert alle Anträge, die ein bestimmter Nutzer // gestellt hat, neueste zuerst — für Ebene 2 ("eigene Anträge // einsehen"), im Unterschied zu ListAntraegeForAccount, das alle // Anträge eines Mandanten liefert (Ebene 3, Posteingang). func (s *Store) ListAntraegeForUser(ctx context.Context, erstellerUserID string) ([]Antrag, error) { rows, err := s.db(ctx).Query(ctx, ` SELECT `+antragColumns+` FROM antrag WHERE ersteller_user_id = $1 ORDER BY created_at DESC `, erstellerUserID) if err != nil { return nil, fmt.Errorf("store: list antraege for user: %w", err) } defer rows.Close() var out []Antrag for rows.Next() { a, err := scanAntrag(rows) if err != nil { return nil, fmt.Errorf("store: scan antrag: %w", err) } out = append(out, a) } if err := rows.Err(); err != nil { return nil, fmt.Errorf("store: list antraege for user: %w", err) } return out, nil } // ListAntraegeForAccount liefert alle Anträge eines Mandanten, neueste // zuerst. func (s *Store) ListAntraegeForAccount(ctx context.Context, accountID string) ([]Antrag, error) { rows, err := s.db(ctx).Query(ctx, ` SELECT `+antragColumns+` FROM antrag WHERE account_id = $1 ORDER BY created_at DESC `, accountID) if err != nil { return nil, fmt.Errorf("store: list antraege for account: %w", err) } defer rows.Close() var out []Antrag for rows.Next() { a, err := scanAntrag(rows) if err != nil { return nil, fmt.Errorf("store: scan antrag: %w", err) } out = append(out, a) } if err := rows.Err(); err != nil { return nil, fmt.Errorf("store: list antraege for account: %w", err) } return out, nil }