package services import ( "context" "database/sql" "fmt" "time" "mgit.msbls.de/m/KanzlAI-mGMT/internal/models" "github.com/google/uuid" "github.com/jmoiron/sqlx" ) type CaseService struct { db *sqlx.DB audit *AuditService } func NewCaseService(db *sqlx.DB, audit *AuditService) *CaseService { return &CaseService{db: db, audit: audit} } type CaseFilter struct { Status string Type string Search string Limit int Offset int } type CaseDetail struct { models.Case Parties []models.Party `json:"parties"` RecentEvents []models.CaseEvent `json:"recent_events"` DeadlinesCount int `json:"deadlines_count"` } type CreateCaseInput struct { CaseNumber string `json:"case_number"` Title string `json:"title"` CaseType *string `json:"case_type,omitempty"` Court *string `json:"court,omitempty"` CourtRef *string `json:"court_ref,omitempty"` Status string `json:"status"` } type UpdateCaseInput struct { CaseNumber *string `json:"case_number,omitempty"` Title *string `json:"title,omitempty"` CaseType *string `json:"case_type,omitempty"` Court *string `json:"court,omitempty"` CourtRef *string `json:"court_ref,omitempty"` Status *string `json:"status,omitempty"` } func (s *CaseService) List(ctx context.Context, tenantID uuid.UUID, filter CaseFilter) ([]models.Case, int, error) { if filter.Limit <= 0 { filter.Limit = 20 } if filter.Limit > 100 { filter.Limit = 100 } // Build WHERE clause where := "WHERE tenant_id = $1" args := []interface{}{tenantID} argIdx := 2 if filter.Status != "" { where += fmt.Sprintf(" AND status = $%d", argIdx) args = append(args, filter.Status) argIdx++ } if filter.Type != "" { where += fmt.Sprintf(" AND case_type = $%d", argIdx) args = append(args, filter.Type) argIdx++ } if filter.Search != "" { where += fmt.Sprintf(" AND (title ILIKE $%d OR case_number ILIKE $%d)", argIdx, argIdx) args = append(args, "%"+filter.Search+"%") argIdx++ } // Count total var total int countQuery := "SELECT COUNT(*) FROM cases " + where if err := s.db.GetContext(ctx, &total, countQuery, args...); err != nil { return nil, 0, fmt.Errorf("counting cases: %w", err) } // Fetch page query := fmt.Sprintf("SELECT * FROM cases %s ORDER BY updated_at DESC LIMIT $%d OFFSET $%d", where, argIdx, argIdx+1) args = append(args, filter.Limit, filter.Offset) var cases []models.Case if err := s.db.SelectContext(ctx, &cases, query, args...); err != nil { return nil, 0, fmt.Errorf("listing cases: %w", err) } return cases, total, nil } func (s *CaseService) GetByID(ctx context.Context, tenantID, caseID uuid.UUID) (*CaseDetail, error) { var c models.Case err := s.db.GetContext(ctx, &c, "SELECT * FROM cases WHERE id = $1 AND tenant_id = $2", caseID, tenantID) if err != nil { if err == sql.ErrNoRows { return nil, nil } return nil, fmt.Errorf("getting case: %w", err) } detail := &CaseDetail{Case: c} // Parties if err := s.db.SelectContext(ctx, &detail.Parties, "SELECT * FROM parties WHERE case_id = $1 AND tenant_id = $2 ORDER BY name", caseID, tenantID); err != nil { return nil, fmt.Errorf("getting parties: %w", err) } // Recent events (last 20) if err := s.db.SelectContext(ctx, &detail.RecentEvents, "SELECT * FROM case_events WHERE case_id = $1 AND tenant_id = $2 ORDER BY created_at DESC LIMIT 20", caseID, tenantID); err != nil { return nil, fmt.Errorf("getting events: %w", err) } // Deadlines count if err := s.db.GetContext(ctx, &detail.DeadlinesCount, "SELECT COUNT(*) FROM deadlines WHERE case_id = $1 AND tenant_id = $2", caseID, tenantID); err != nil { return nil, fmt.Errorf("counting deadlines: %w", err) } return detail, nil } func (s *CaseService) Create(ctx context.Context, tenantID uuid.UUID, userID uuid.UUID, input CreateCaseInput) (*models.Case, error) { if input.Status == "" { input.Status = "active" } id := uuid.New() now := time.Now() _, err := s.db.ExecContext(ctx, `INSERT INTO cases (id, tenant_id, case_number, title, case_type, court, court_ref, status, metadata, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, '{}', $9, $9)`, id, tenantID, input.CaseNumber, input.Title, input.CaseType, input.Court, input.CourtRef, input.Status, now) if err != nil { return nil, fmt.Errorf("creating case: %w", err) } // Create case_created event createEvent(ctx, s.db, tenantID, id, userID, "case_created", "Case created", nil) var c models.Case if err := s.db.GetContext(ctx, &c, "SELECT * FROM cases WHERE id = $1", id); err != nil { return nil, fmt.Errorf("fetching created case: %w", err) } s.audit.Log(ctx, "create", "case", &id, nil, c) return &c, nil } func (s *CaseService) Update(ctx context.Context, tenantID, caseID uuid.UUID, userID uuid.UUID, input UpdateCaseInput) (*models.Case, error) { // Fetch current to detect status change var current models.Case err := s.db.GetContext(ctx, ¤t, "SELECT * FROM cases WHERE id = $1 AND tenant_id = $2", caseID, tenantID) if err != nil { if err == sql.ErrNoRows { return nil, nil } return nil, fmt.Errorf("fetching case for update: %w", err) } // Build SET clause dynamically sets := []string{} args := []interface{}{} argIdx := 1 if input.CaseNumber != nil { sets = append(sets, fmt.Sprintf("case_number = $%d", argIdx)) args = append(args, *input.CaseNumber) argIdx++ } if input.Title != nil { sets = append(sets, fmt.Sprintf("title = $%d", argIdx)) args = append(args, *input.Title) argIdx++ } if input.CaseType != nil { sets = append(sets, fmt.Sprintf("case_type = $%d", argIdx)) args = append(args, *input.CaseType) argIdx++ } if input.Court != nil { sets = append(sets, fmt.Sprintf("court = $%d", argIdx)) args = append(args, *input.Court) argIdx++ } if input.CourtRef != nil { sets = append(sets, fmt.Sprintf("court_ref = $%d", argIdx)) args = append(args, *input.CourtRef) argIdx++ } if input.Status != nil { sets = append(sets, fmt.Sprintf("status = $%d", argIdx)) args = append(args, *input.Status) argIdx++ } if len(sets) == 0 { return ¤t, nil } sets = append(sets, fmt.Sprintf("updated_at = $%d", argIdx)) args = append(args, time.Now()) argIdx++ query := fmt.Sprintf("UPDATE cases SET %s WHERE id = $%d AND tenant_id = $%d", joinStrings(sets, ", "), argIdx, argIdx+1) args = append(args, caseID, tenantID) if _, err := s.db.ExecContext(ctx, query, args...); err != nil { return nil, fmt.Errorf("updating case: %w", err) } // Log status change event if input.Status != nil && *input.Status != current.Status { desc := fmt.Sprintf("Status changed from %s to %s", current.Status, *input.Status) createEvent(ctx, s.db, tenantID, caseID, userID, "status_changed", desc, nil) } var updated models.Case if err := s.db.GetContext(ctx, &updated, "SELECT * FROM cases WHERE id = $1", caseID); err != nil { return nil, fmt.Errorf("fetching updated case: %w", err) } s.audit.Log(ctx, "update", "case", &caseID, current, updated) return &updated, nil } func (s *CaseService) Delete(ctx context.Context, tenantID, caseID uuid.UUID, userID uuid.UUID) error { result, err := s.db.ExecContext(ctx, "UPDATE cases SET status = 'archived', updated_at = $1 WHERE id = $2 AND tenant_id = $3 AND status != 'archived'", time.Now(), caseID, tenantID) if err != nil { return fmt.Errorf("archiving case: %w", err) } rows, _ := result.RowsAffected() if rows == 0 { return sql.ErrNoRows } createEvent(ctx, s.db, tenantID, caseID, userID, "case_archived", "Case archived", nil) s.audit.Log(ctx, "delete", "case", &caseID, map[string]string{"status": "active"}, map[string]string{"status": "archived"}) return nil } func createEvent(ctx context.Context, db *sqlx.DB, tenantID, caseID uuid.UUID, userID uuid.UUID, eventType, title string, description *string) { now := time.Now() db.ExecContext(ctx, `INSERT INTO case_events (id, tenant_id, case_id, event_type, title, description, event_date, created_by, metadata, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, '{}', $7, $7)`, uuid.New(), tenantID, caseID, eventType, title, description, now, userID) } func joinStrings(strs []string, sep string) string { result := "" for i, s := range strs { if i > 0 { result += sep } result += s } return result }