package store import ( "context" "errors" "fmt" "time" "github.com/jackc/pgx/v5" ) // Submission ist ein eingereichter Beitrag. AccountID ist der Mandant, // dem der Beitrag gehört (Mandantentrennung) — jede Abfrage, die einen // Beitrag ausliefert, muss AccountID gegen den angemeldeten Account // prüfen (siehe internal/web-Middleware), store selbst erzwingt das // nicht auf Zeilenebene. type Submission struct { ID string AccountID string Platform string PostType string Caption string Status string CreatedAt time.Time UpdatedAt time.Time } // CreateSubmission legt einen neuen Beitrag für einen Mandanten an // (Status "draft"). func (s *Store) CreateSubmission(ctx context.Context, accountID, platform, postType, caption string) (Submission, error) { var sub Submission err := s.Pool.QueryRow(ctx, ` INSERT INTO submission (account_id, platform, post_type, caption) VALUES ($1, $2, $3, $4) RETURNING id, account_id, platform, post_type, caption, status, created_at, updated_at `, accountID, platform, postType, caption).Scan( &sub.ID, &sub.AccountID, &sub.Platform, &sub.PostType, &sub.Caption, &sub.Status, &sub.CreatedAt, &sub.UpdatedAt, ) if err != nil { return Submission{}, fmt.Errorf("store: create submission: %w", err) } return sub, nil } // GetSubmission liest einen Beitrag anhand seiner ID — ohne // Mandanten-Prüfung, das ist Sache des Aufrufers (siehe Submission.AccountID). func (s *Store) GetSubmission(ctx context.Context, id string) (Submission, error) { var sub Submission err := s.Pool.QueryRow(ctx, ` SELECT id, account_id, platform, post_type, caption, status, created_at, updated_at FROM submission WHERE id = $1 `, id).Scan( &sub.ID, &sub.AccountID, &sub.Platform, &sub.PostType, &sub.Caption, &sub.Status, &sub.CreatedAt, &sub.UpdatedAt, ) if errors.Is(err, pgx.ErrNoRows) { return Submission{}, ErrNotFound } if err != nil { return Submission{}, fmt.Errorf("store: get submission: %w", err) } return sub, nil } // SubmissionSummary ist eine Submission plus einer Kurzfassung ihrer // aktuell gültigen Findings, wie sie eine Übersichtsliste braucht (ohne // für jede Zeile extra ListCurrentFindings aufzurufen). type SubmissionSummary struct { Submission FindingCount int HighestSeverity string // "" wenn keine Findings } // ListSubmissionsForAccount liefert alle Beiträge eines Mandanten, // neueste zuerst, mit Findings-Kurzfassung. func (s *Store) ListSubmissionsForAccount(ctx context.Context, accountID string) ([]SubmissionSummary, error) { rows, err := s.Pool.Query(ctx, ` SELECT s.id, s.account_id, s.platform, s.post_type, s.caption, s.status, s.created_at, s.updated_at, COUNT(f.id) AS finding_count, COALESCE(MAX(CASE f.severity WHEN 'hoch' THEN 3 WHEN 'mittel' THEN 2 WHEN 'niedrig' THEN 1 ELSE 0 END), 0) AS severity_rank FROM submission s LEFT JOIN finding f ON f.submission_id = s.id AND NOT EXISTS (SELECT 1 FROM finding f2 WHERE f2.supersedes = f.id) WHERE s.account_id = $1 GROUP BY s.id ORDER BY s.created_at DESC `, accountID) if err != nil { return nil, fmt.Errorf("store: list submissions for account: %w", err) } defer rows.Close() var out []SubmissionSummary for rows.Next() { var sub SubmissionSummary var severityRank int if err := rows.Scan( &sub.ID, &sub.AccountID, &sub.Platform, &sub.PostType, &sub.Caption, &sub.Status, &sub.CreatedAt, &sub.UpdatedAt, &sub.FindingCount, &severityRank, ); err != nil { return nil, fmt.Errorf("store: scan submission summary: %w", err) } switch severityRank { case 3: sub.HighestSeverity = "hoch" case 2: sub.HighestSeverity = "mittel" case 1: sub.HighestSeverity = "niedrig" } out = append(out, sub) } if err := rows.Err(); err != nil { return nil, fmt.Errorf("store: list submissions for account: %w", err) } return out, nil } // SetSubmissionStatus setzt den Status eines Beitrags (submission ist, // anders als extraction/finding/evidence_package, NICHT append-only — // der Lebenszyklus draft → checked → published → archived ist eine // normale Zustandsänderung, kein Beweis-Eintrag). func (s *Store) SetSubmissionStatus(ctx context.Context, id, status string) error { tag, err := s.Pool.Exec(ctx, ` UPDATE submission SET status = $2, updated_at = now() WHERE id = $1 `, id, status) if err != nil { return fmt.Errorf("store: set submission status: %w", err) } if tag.RowsAffected() == 0 { return fmt.Errorf("store: set submission status: submission %s nicht gefunden", id) } return nil }