This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
breakpilot-pwa/ai-compliance-sdk/internal/ucca/escalation_store.go
Benjamin Admin 21a844cb8a fix: Restore all files lost during destructive rebase
A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.

This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).

Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 09:51:32 +01:00

503 lines
14 KiB
Go

package ucca
import (
"context"
"encoding/json"
"fmt"
"time"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgxpool"
)
// EscalationStore handles database operations for escalations.
type EscalationStore struct {
pool *pgxpool.Pool
}
// NewEscalationStore creates a new escalation store.
func NewEscalationStore(pool *pgxpool.Pool) *EscalationStore {
return &EscalationStore{pool: pool}
}
// CreateEscalation creates a new escalation for an assessment.
func (s *EscalationStore) CreateEscalation(ctx context.Context, e *Escalation) error {
conditionsJSON, err := json.Marshal(e.Conditions)
if err != nil {
conditionsJSON = []byte("[]")
}
query := `
INSERT INTO ucca_escalations (
id, tenant_id, assessment_id, escalation_level, escalation_reason,
status, conditions, due_date, created_at, updated_at
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, NOW(), NOW()
)
`
e.ID = uuid.New()
e.CreatedAt = time.Now().UTC()
e.UpdatedAt = e.CreatedAt
_, err = s.pool.Exec(ctx, query,
e.ID, e.TenantID, e.AssessmentID, e.EscalationLevel, e.EscalationReason,
e.Status, conditionsJSON, e.DueDate,
)
return err
}
// GetEscalation retrieves an escalation by ID.
func (s *EscalationStore) GetEscalation(ctx context.Context, id uuid.UUID) (*Escalation, error) {
query := `
SELECT id, tenant_id, assessment_id, escalation_level, escalation_reason,
assigned_to, assigned_role, assigned_at, status, reviewer_id,
reviewer_notes, reviewed_at, decision, decision_notes, decision_at,
conditions, created_at, updated_at, due_date,
notification_sent, notification_sent_at
FROM ucca_escalations
WHERE id = $1
`
var e Escalation
var conditionsJSON []byte
err := s.pool.QueryRow(ctx, query, id).Scan(
&e.ID, &e.TenantID, &e.AssessmentID, &e.EscalationLevel, &e.EscalationReason,
&e.AssignedTo, &e.AssignedRole, &e.AssignedAt, &e.Status, &e.ReviewerID,
&e.ReviewerNotes, &e.ReviewedAt, &e.Decision, &e.DecisionNotes, &e.DecisionAt,
&conditionsJSON, &e.CreatedAt, &e.UpdatedAt, &e.DueDate,
&e.NotificationSent, &e.NotificationSentAt,
)
if err != nil {
return nil, err
}
if len(conditionsJSON) > 0 {
json.Unmarshal(conditionsJSON, &e.Conditions)
}
return &e, nil
}
// GetEscalationByAssessment retrieves an escalation for an assessment.
func (s *EscalationStore) GetEscalationByAssessment(ctx context.Context, assessmentID uuid.UUID) (*Escalation, error) {
query := `
SELECT id, tenant_id, assessment_id, escalation_level, escalation_reason,
assigned_to, assigned_role, assigned_at, status, reviewer_id,
reviewer_notes, reviewed_at, decision, decision_notes, decision_at,
conditions, created_at, updated_at, due_date,
notification_sent, notification_sent_at
FROM ucca_escalations
WHERE assessment_id = $1
ORDER BY created_at DESC
LIMIT 1
`
var e Escalation
var conditionsJSON []byte
err := s.pool.QueryRow(ctx, query, assessmentID).Scan(
&e.ID, &e.TenantID, &e.AssessmentID, &e.EscalationLevel, &e.EscalationReason,
&e.AssignedTo, &e.AssignedRole, &e.AssignedAt, &e.Status, &e.ReviewerID,
&e.ReviewerNotes, &e.ReviewedAt, &e.Decision, &e.DecisionNotes, &e.DecisionAt,
&conditionsJSON, &e.CreatedAt, &e.UpdatedAt, &e.DueDate,
&e.NotificationSent, &e.NotificationSentAt,
)
if err != nil {
return nil, err
}
if len(conditionsJSON) > 0 {
json.Unmarshal(conditionsJSON, &e.Conditions)
}
return &e, nil
}
// ListEscalations lists escalations for a tenant with optional filters.
func (s *EscalationStore) ListEscalations(ctx context.Context, tenantID uuid.UUID, status string, level string, assignedTo *uuid.UUID) ([]EscalationWithAssessment, error) {
query := `
SELECT e.id, e.tenant_id, e.assessment_id, e.escalation_level, e.escalation_reason,
e.assigned_to, e.assigned_role, e.assigned_at, e.status, e.reviewer_id,
e.reviewer_notes, e.reviewed_at, e.decision, e.decision_notes, e.decision_at,
e.conditions, e.created_at, e.updated_at, e.due_date,
e.notification_sent, e.notification_sent_at,
a.title, a.feasibility, a.risk_score, a.domain
FROM ucca_escalations e
JOIN ucca_assessments a ON e.assessment_id = a.id
WHERE e.tenant_id = $1
`
args := []interface{}{tenantID}
argCount := 1
if status != "" {
argCount++
query += fmt.Sprintf(" AND e.status = $%d", argCount)
args = append(args, status)
}
if level != "" {
argCount++
query += fmt.Sprintf(" AND e.escalation_level = $%d", argCount)
args = append(args, level)
}
if assignedTo != nil {
argCount++
query += fmt.Sprintf(" AND e.assigned_to = $%d", argCount)
args = append(args, *assignedTo)
}
query += " ORDER BY e.created_at DESC"
rows, err := s.pool.Query(ctx, query, args...)
if err != nil {
return nil, err
}
defer rows.Close()
var escalations []EscalationWithAssessment
for rows.Next() {
var e EscalationWithAssessment
var conditionsJSON []byte
err := rows.Scan(
&e.ID, &e.TenantID, &e.AssessmentID, &e.EscalationLevel, &e.EscalationReason,
&e.AssignedTo, &e.AssignedRole, &e.AssignedAt, &e.Status, &e.ReviewerID,
&e.ReviewerNotes, &e.ReviewedAt, &e.Decision, &e.DecisionNotes, &e.DecisionAt,
&conditionsJSON, &e.CreatedAt, &e.UpdatedAt, &e.DueDate,
&e.NotificationSent, &e.NotificationSentAt,
&e.AssessmentTitle, &e.AssessmentFeasibility, &e.AssessmentRiskScore, &e.AssessmentDomain,
)
if err != nil {
return nil, err
}
if len(conditionsJSON) > 0 {
json.Unmarshal(conditionsJSON, &e.Conditions)
}
escalations = append(escalations, e)
}
return escalations, nil
}
// AssignEscalation assigns an escalation to a reviewer.
func (s *EscalationStore) AssignEscalation(ctx context.Context, id uuid.UUID, assignedTo uuid.UUID, role string) error {
query := `
UPDATE ucca_escalations
SET assigned_to = $2, assigned_role = $3, assigned_at = NOW(),
status = 'assigned', updated_at = NOW()
WHERE id = $1
`
_, err := s.pool.Exec(ctx, query, id, assignedTo, role)
return err
}
// StartReview marks an escalation as being reviewed.
func (s *EscalationStore) StartReview(ctx context.Context, id uuid.UUID, reviewerID uuid.UUID) error {
query := `
UPDATE ucca_escalations
SET reviewer_id = $2, status = 'in_review', updated_at = NOW()
WHERE id = $1
`
_, err := s.pool.Exec(ctx, query, id, reviewerID)
return err
}
// DecideEscalation records a decision on an escalation.
func (s *EscalationStore) DecideEscalation(ctx context.Context, id uuid.UUID, decision EscalationDecision, notes string, conditions []string) error {
var newStatus EscalationStatus
switch decision {
case EscalationDecisionApprove:
newStatus = EscalationStatusApproved
case EscalationDecisionReject:
newStatus = EscalationStatusRejected
case EscalationDecisionModify:
newStatus = EscalationStatusReturned
case EscalationDecisionEscalate:
// Keep in review for re-assignment
newStatus = EscalationStatusPending
default:
newStatus = EscalationStatusPending
}
conditionsJSON, _ := json.Marshal(conditions)
query := `
UPDATE ucca_escalations
SET decision = $2, decision_notes = $3, decision_at = NOW(),
status = $4, conditions = $5, updated_at = NOW()
WHERE id = $1
`
_, err := s.pool.Exec(ctx, query, id, decision, notes, newStatus, conditionsJSON)
return err
}
// AddEscalationHistory adds an audit entry for an escalation.
func (s *EscalationStore) AddEscalationHistory(ctx context.Context, h *EscalationHistory) error {
query := `
INSERT INTO ucca_escalation_history (
id, escalation_id, action, old_status, new_status,
old_level, new_level, actor_id, actor_role, notes, created_at
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, NOW()
)
`
h.ID = uuid.New()
h.CreatedAt = time.Now().UTC()
_, err := s.pool.Exec(ctx, query,
h.ID, h.EscalationID, h.Action, h.OldStatus, h.NewStatus,
h.OldLevel, h.NewLevel, h.ActorID, h.ActorRole, h.Notes,
)
return err
}
// GetEscalationHistory retrieves the audit history for an escalation.
func (s *EscalationStore) GetEscalationHistory(ctx context.Context, escalationID uuid.UUID) ([]EscalationHistory, error) {
query := `
SELECT id, escalation_id, action, old_status, new_status,
old_level, new_level, actor_id, actor_role, notes, created_at
FROM ucca_escalation_history
WHERE escalation_id = $1
ORDER BY created_at ASC
`
rows, err := s.pool.Query(ctx, query, escalationID)
if err != nil {
return nil, err
}
defer rows.Close()
var history []EscalationHistory
for rows.Next() {
var h EscalationHistory
err := rows.Scan(
&h.ID, &h.EscalationID, &h.Action, &h.OldStatus, &h.NewStatus,
&h.OldLevel, &h.NewLevel, &h.ActorID, &h.ActorRole, &h.Notes, &h.CreatedAt,
)
if err != nil {
return nil, err
}
history = append(history, h)
}
return history, nil
}
// GetEscalationStats retrieves escalation statistics for a tenant.
func (s *EscalationStore) GetEscalationStats(ctx context.Context, tenantID uuid.UUID) (*EscalationStats, error) {
stats := &EscalationStats{
ByLevel: make(map[EscalationLevel]int),
}
// Count by status
statusQuery := `
SELECT status, COUNT(*) as count
FROM ucca_escalations
WHERE tenant_id = $1
GROUP BY status
`
rows, err := s.pool.Query(ctx, statusQuery, tenantID)
if err != nil {
return nil, err
}
for rows.Next() {
var status string
var count int
if err := rows.Scan(&status, &count); err != nil {
continue
}
switch EscalationStatus(status) {
case EscalationStatusPending:
stats.TotalPending = count
case EscalationStatusInReview, EscalationStatusAssigned:
stats.TotalInReview += count
case EscalationStatusApproved:
stats.TotalApproved = count
case EscalationStatusRejected:
stats.TotalRejected = count
}
}
rows.Close()
// Count by level
levelQuery := `
SELECT escalation_level, COUNT(*) as count
FROM ucca_escalations
WHERE tenant_id = $1 AND status NOT IN ('approved', 'rejected')
GROUP BY escalation_level
`
rows, err = s.pool.Query(ctx, levelQuery, tenantID)
if err != nil {
return nil, err
}
for rows.Next() {
var level string
var count int
if err := rows.Scan(&level, &count); err != nil {
continue
}
stats.ByLevel[EscalationLevel(level)] = count
}
rows.Close()
// Count overdue SLA
overdueQuery := `
SELECT COUNT(*)
FROM ucca_escalations
WHERE tenant_id = $1
AND status NOT IN ('approved', 'rejected')
AND due_date < NOW()
`
s.pool.QueryRow(ctx, overdueQuery, tenantID).Scan(&stats.OverdueSLA)
// Count approaching SLA (within 8 hours)
approachingQuery := `
SELECT COUNT(*)
FROM ucca_escalations
WHERE tenant_id = $1
AND status NOT IN ('approved', 'rejected')
AND due_date > NOW()
AND due_date < NOW() + INTERVAL '8 hours'
`
s.pool.QueryRow(ctx, approachingQuery, tenantID).Scan(&stats.ApproachingSLA)
// Average resolution time
avgQuery := `
SELECT COALESCE(AVG(EXTRACT(EPOCH FROM (decision_at - created_at)) / 3600), 0)
FROM ucca_escalations
WHERE tenant_id = $1 AND decision_at IS NOT NULL
`
s.pool.QueryRow(ctx, avgQuery, tenantID).Scan(&stats.AvgResolutionHours)
return stats, nil
}
// DSB Pool Operations
// AddDSBPoolMember adds a member to the DSB review pool.
func (s *EscalationStore) AddDSBPoolMember(ctx context.Context, m *DSBPoolMember) error {
query := `
INSERT INTO ucca_dsb_pool (
id, tenant_id, user_id, user_name, user_email, role,
is_active, max_concurrent_reviews, created_at, updated_at
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, NOW(), NOW()
)
ON CONFLICT (tenant_id, user_id) DO UPDATE
SET user_name = $4, user_email = $5, role = $6,
is_active = $7, max_concurrent_reviews = $8, updated_at = NOW()
`
if m.ID == uuid.Nil {
m.ID = uuid.New()
}
_, err := s.pool.Exec(ctx, query,
m.ID, m.TenantID, m.UserID, m.UserName, m.UserEmail, m.Role,
m.IsActive, m.MaxConcurrentReviews,
)
return err
}
// GetDSBPoolMembers retrieves active DSB pool members for a tenant.
func (s *EscalationStore) GetDSBPoolMembers(ctx context.Context, tenantID uuid.UUID, role string) ([]DSBPoolMember, error) {
query := `
SELECT id, tenant_id, user_id, user_name, user_email, role,
is_active, max_concurrent_reviews, current_reviews, created_at, updated_at
FROM ucca_dsb_pool
WHERE tenant_id = $1 AND is_active = true
`
args := []interface{}{tenantID}
if role != "" {
query += " AND role = $2"
args = append(args, role)
}
query += " ORDER BY current_reviews ASC, user_name ASC"
rows, err := s.pool.Query(ctx, query, args...)
if err != nil {
return nil, err
}
defer rows.Close()
var members []DSBPoolMember
for rows.Next() {
var m DSBPoolMember
err := rows.Scan(
&m.ID, &m.TenantID, &m.UserID, &m.UserName, &m.UserEmail, &m.Role,
&m.IsActive, &m.MaxConcurrentReviews, &m.CurrentReviews, &m.CreatedAt, &m.UpdatedAt,
)
if err != nil {
return nil, err
}
members = append(members, m)
}
return members, nil
}
// GetNextAvailableReviewer finds the next available reviewer for a role.
func (s *EscalationStore) GetNextAvailableReviewer(ctx context.Context, tenantID uuid.UUID, role string) (*DSBPoolMember, error) {
query := `
SELECT id, tenant_id, user_id, user_name, user_email, role,
is_active, max_concurrent_reviews, current_reviews, created_at, updated_at
FROM ucca_dsb_pool
WHERE tenant_id = $1 AND is_active = true AND role = $2
AND current_reviews < max_concurrent_reviews
ORDER BY current_reviews ASC
LIMIT 1
`
var m DSBPoolMember
err := s.pool.QueryRow(ctx, query, tenantID, role).Scan(
&m.ID, &m.TenantID, &m.UserID, &m.UserName, &m.UserEmail, &m.Role,
&m.IsActive, &m.MaxConcurrentReviews, &m.CurrentReviews, &m.CreatedAt, &m.UpdatedAt,
)
if err != nil {
return nil, err
}
return &m, nil
}
// IncrementReviewerCount increments the current review count for a DSB member.
func (s *EscalationStore) IncrementReviewerCount(ctx context.Context, userID uuid.UUID) error {
query := `
UPDATE ucca_dsb_pool
SET current_reviews = current_reviews + 1, updated_at = NOW()
WHERE user_id = $1
`
_, err := s.pool.Exec(ctx, query, userID)
return err
}
// DecrementReviewerCount decrements the current review count for a DSB member.
func (s *EscalationStore) DecrementReviewerCount(ctx context.Context, userID uuid.UUID) error {
query := `
UPDATE ucca_dsb_pool
SET current_reviews = GREATEST(0, current_reviews - 1), updated_at = NOW()
WHERE user_id = $1
`
_, err := s.pool.Exec(ctx, query, userID)
return err
}