package store import ( "context" "errors" "fmt" "time" "github.com/jackc/pgx/v5" ) // Participant ist ein Beteiligter an einer Submission (Verantwortungs- // matrix). Anders als finding/extraction/evidence_package ist participant // NICHT append-only — wer vorgegeben/freigegeben hat, kann sich klären // oder korrigieren, ohne dass das ein Beweis-Eintrag ist. type Participant struct { ID string SubmissionID string Role string Name string Vorgegeben bool Freigegeben bool ApprovedAt *time.Time CreatedAt time.Time } // CreateParticipant fügt einen Beteiligten zu einer Submission hinzu. func (s *Store) CreateParticipant(ctx context.Context, submissionID, role, name string, vorgegeben, freigegeben bool) (Participant, error) { var p Participant err := s.Pool.QueryRow(ctx, ` INSERT INTO participant (submission_id, role, name, vorgegeben, freigegeben, approved_at) VALUES ($1, $2, $3, $4, $5, CASE WHEN $5 THEN now() ELSE NULL END) RETURNING id, submission_id, role, name, vorgegeben, freigegeben, approved_at, created_at `, submissionID, role, name, vorgegeben, freigegeben).Scan( &p.ID, &p.SubmissionID, &p.Role, &p.Name, &p.Vorgegeben, &p.Freigegeben, &p.ApprovedAt, &p.CreatedAt, ) if err != nil { return Participant{}, fmt.Errorf("store: create participant: %w", err) } return p, nil } // ListParticipants liefert alle Beteiligten einer Submission. func (s *Store) ListParticipants(ctx context.Context, submissionID string) ([]Participant, error) { rows, err := s.Pool.Query(ctx, ` SELECT id, submission_id, role, name, vorgegeben, freigegeben, approved_at, created_at FROM participant WHERE submission_id = $1 ORDER BY created_at `, submissionID) if err != nil { return nil, fmt.Errorf("store: list participants: %w", err) } defer rows.Close() var participants []Participant for rows.Next() { var p Participant if err := rows.Scan(&p.ID, &p.SubmissionID, &p.Role, &p.Name, &p.Vorgegeben, &p.Freigegeben, &p.ApprovedAt, &p.CreatedAt); err != nil { return nil, fmt.Errorf("store: scan participant: %w", err) } participants = append(participants, p) } if err := rows.Err(); err != nil { return nil, fmt.Errorf("store: list participants: %w", err) } return participants, nil } // GetParticipant liest einen Beteiligten anhand seiner ID — u. a. um vor // einem Update/Delete zu prüfen, zu welcher Submission (und damit zu // welchem Account) er gehört. func (s *Store) GetParticipant(ctx context.Context, id string) (Participant, error) { var p Participant err := s.Pool.QueryRow(ctx, ` SELECT id, submission_id, role, name, vorgegeben, freigegeben, approved_at, created_at FROM participant WHERE id = $1 `, id).Scan(&p.ID, &p.SubmissionID, &p.Role, &p.Name, &p.Vorgegeben, &p.Freigegeben, &p.ApprovedAt, &p.CreatedAt) if errors.Is(err, pgx.ErrNoRows) { return Participant{}, ErrNotFound } if err != nil { return Participant{}, fmt.Errorf("store: get participant: %w", err) } return p, nil } // UpdateParticipant setzt vorgegeben/freigegeben. approved_at wird beim // ersten Wechsel zu freigegeben=true gesetzt und danach nicht mehr // verändert (er hält fest, wann zuerst freigegeben wurde). func (s *Store) UpdateParticipant(ctx context.Context, id string, vorgegeben, freigegeben bool) (Participant, error) { var p Participant err := s.Pool.QueryRow(ctx, ` UPDATE participant SET vorgegeben = $2, freigegeben = $3, approved_at = CASE WHEN $3 AND approved_at IS NULL THEN now() ELSE approved_at END WHERE id = $1 RETURNING id, submission_id, role, name, vorgegeben, freigegeben, approved_at, created_at `, id, vorgegeben, freigegeben).Scan( &p.ID, &p.SubmissionID, &p.Role, &p.Name, &p.Vorgegeben, &p.Freigegeben, &p.ApprovedAt, &p.CreatedAt, ) if errors.Is(err, pgx.ErrNoRows) { return Participant{}, ErrNotFound } if err != nil { return Participant{}, fmt.Errorf("store: update participant: %w", err) } return p, nil } // DeleteParticipant entfernt einen Beteiligten (z. B. versehentlich // falsch angelegt). func (s *Store) DeleteParticipant(ctx context.Context, id string) error { tag, err := s.Pool.Exec(ctx, `DELETE FROM participant WHERE id = $1`, id) if err != nil { return fmt.Errorf("store: delete participant: %w", err) } if tag.RowsAffected() == 0 { return ErrNotFound } return nil }