package services import ( "context" "database/sql" "fmt" "time" "github.com/google/uuid" "github.com/jmoiron/sqlx" "mgit.msbls.de/m/KanzlAI-mGMT/internal/models" ) // DeadlineService handles CRUD operations for case deadlines type DeadlineService struct { db *sqlx.DB audit *AuditService } // NewDeadlineService creates a new deadline service func NewDeadlineService(db *sqlx.DB, audit *AuditService) *DeadlineService { return &DeadlineService{db: db, audit: audit} } // ListAll returns all deadlines for a tenant, ordered by due_date func (s *DeadlineService) ListAll(tenantID uuid.UUID) ([]models.Deadline, error) { query := `SELECT id, tenant_id, case_id, title, description, due_date, original_due_date, warning_date, source, rule_id, status, completed_at, caldav_uid, caldav_etag, notes, created_at, updated_at FROM deadlines WHERE tenant_id = $1 ORDER BY due_date ASC` var deadlines []models.Deadline err := s.db.Select(&deadlines, query, tenantID) if err != nil { return nil, fmt.Errorf("listing all deadlines: %w", err) } return deadlines, nil } // ListForCase returns all deadlines for a case, scoped to tenant func (s *DeadlineService) ListForCase(tenantID, caseID uuid.UUID) ([]models.Deadline, error) { query := `SELECT id, tenant_id, case_id, title, description, due_date, original_due_date, warning_date, source, rule_id, status, completed_at, caldav_uid, caldav_etag, notes, created_at, updated_at FROM deadlines WHERE tenant_id = $1 AND case_id = $2 ORDER BY due_date ASC` var deadlines []models.Deadline err := s.db.Select(&deadlines, query, tenantID, caseID) if err != nil { return nil, fmt.Errorf("listing deadlines for case: %w", err) } return deadlines, nil } // GetByID returns a single deadline by ID, scoped to tenant func (s *DeadlineService) GetByID(tenantID, deadlineID uuid.UUID) (*models.Deadline, error) { query := `SELECT id, tenant_id, case_id, title, description, due_date, original_due_date, warning_date, source, rule_id, status, completed_at, caldav_uid, caldav_etag, notes, created_at, updated_at FROM deadlines WHERE tenant_id = $1 AND id = $2` var d models.Deadline err := s.db.Get(&d, query, tenantID, deadlineID) if err != nil { if err == sql.ErrNoRows { return nil, nil } return nil, fmt.Errorf("getting deadline: %w", err) } return &d, nil } // CreateDeadlineInput holds the fields for creating a deadline type CreateDeadlineInput struct { CaseID uuid.UUID `json:"case_id"` Title string `json:"title"` Description *string `json:"description,omitempty"` DueDate string `json:"due_date"` WarningDate *string `json:"warning_date,omitempty"` Source string `json:"source"` RuleID *uuid.UUID `json:"rule_id,omitempty"` Notes *string `json:"notes,omitempty"` } // Create inserts a new deadline func (s *DeadlineService) Create(ctx context.Context, tenantID uuid.UUID, input CreateDeadlineInput) (*models.Deadline, error) { id := uuid.New() source := input.Source if source == "" { source = "manual" } query := `INSERT INTO deadlines (id, tenant_id, case_id, title, description, due_date, warning_date, source, rule_id, status, notes, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, 'pending', $10, NOW(), NOW()) RETURNING id, tenant_id, case_id, title, description, due_date, original_due_date, warning_date, source, rule_id, status, completed_at, caldav_uid, caldav_etag, notes, created_at, updated_at` var d models.Deadline err := s.db.Get(&d, query, id, tenantID, input.CaseID, input.Title, input.Description, input.DueDate, input.WarningDate, source, input.RuleID, input.Notes) if err != nil { return nil, fmt.Errorf("creating deadline: %w", err) } s.audit.Log(ctx, "create", "deadline", &id, nil, d) return &d, nil } // UpdateDeadlineInput holds the fields for updating a deadline type UpdateDeadlineInput struct { Title *string `json:"title,omitempty"` Description *string `json:"description,omitempty"` DueDate *string `json:"due_date,omitempty"` WarningDate *string `json:"warning_date,omitempty"` Notes *string `json:"notes,omitempty"` Status *string `json:"status,omitempty"` RuleID *uuid.UUID `json:"rule_id,omitempty"` } // Update modifies an existing deadline func (s *DeadlineService) Update(ctx context.Context, tenantID, deadlineID uuid.UUID, input UpdateDeadlineInput) (*models.Deadline, error) { // First check it exists and belongs to tenant existing, err := s.GetByID(tenantID, deadlineID) if err != nil { return nil, err } if existing == nil { return nil, nil } query := `UPDATE deadlines SET title = COALESCE($1, title), description = COALESCE($2, description), due_date = COALESCE($3, due_date), warning_date = COALESCE($4, warning_date), notes = COALESCE($5, notes), status = COALESCE($6, status), rule_id = COALESCE($7, rule_id), updated_at = NOW() WHERE id = $8 AND tenant_id = $9 RETURNING id, tenant_id, case_id, title, description, due_date, original_due_date, warning_date, source, rule_id, status, completed_at, caldav_uid, caldav_etag, notes, created_at, updated_at` var d models.Deadline err = s.db.Get(&d, query, input.Title, input.Description, input.DueDate, input.WarningDate, input.Notes, input.Status, input.RuleID, deadlineID, tenantID) if err != nil { return nil, fmt.Errorf("updating deadline: %w", err) } s.audit.Log(ctx, "update", "deadline", &deadlineID, existing, d) return &d, nil } // Complete marks a deadline as completed func (s *DeadlineService) Complete(ctx context.Context, tenantID, deadlineID uuid.UUID) (*models.Deadline, error) { query := `UPDATE deadlines SET status = 'completed', completed_at = $1, updated_at = NOW() WHERE id = $2 AND tenant_id = $3 RETURNING id, tenant_id, case_id, title, description, due_date, original_due_date, warning_date, source, rule_id, status, completed_at, caldav_uid, caldav_etag, notes, created_at, updated_at` var d models.Deadline err := s.db.Get(&d, query, time.Now(), deadlineID, tenantID) if err != nil { if err == sql.ErrNoRows { return nil, nil } return nil, fmt.Errorf("completing deadline: %w", err) } s.audit.Log(ctx, "update", "deadline", &deadlineID, map[string]string{"status": "pending"}, map[string]string{"status": "completed"}) return &d, nil } // Delete removes a deadline func (s *DeadlineService) Delete(ctx context.Context, tenantID, deadlineID uuid.UUID) error { query := `DELETE FROM deadlines WHERE id = $1 AND tenant_id = $2` result, err := s.db.Exec(query, deadlineID, tenantID) if err != nil { return fmt.Errorf("deleting deadline: %w", err) } rows, err := result.RowsAffected() if err != nil { return fmt.Errorf("checking delete result: %w", err) } if rows == 0 { return fmt.Errorf("deadline not found") } s.audit.Log(ctx, "delete", "deadline", &deadlineID, nil, nil) return nil }