- Holiday service with German federal holidays, Easter calculation, DB loading - Deadline calculator adapted from youpc.org (duration calc + non-working day adjustment) - Deadline CRUD service (tenant-scoped: list, create, update, complete, delete) - Deadline rule service (list, filter by proceeding type, hierarchical rule trees) - HTTP handlers for all endpoints with tenant resolution via X-Tenant-ID header - Router wired with all new endpoints under /api/ - Tests for holiday and calculator services (8 passing)
181 lines
6.2 KiB
Go
181 lines
6.2 KiB
Go
package services
|
|
|
|
import (
|
|
"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
|
|
}
|
|
|
|
// NewDeadlineService creates a new deadline service
|
|
func NewDeadlineService(db *sqlx.DB) *DeadlineService {
|
|
return &DeadlineService{db: db}
|
|
}
|
|
|
|
// 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(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)
|
|
}
|
|
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(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)
|
|
}
|
|
return &d, nil
|
|
}
|
|
|
|
// Complete marks a deadline as completed
|
|
func (s *DeadlineService) Complete(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)
|
|
}
|
|
return &d, nil
|
|
}
|
|
|
|
// Delete removes a deadline
|
|
func (s *DeadlineService) Delete(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")
|
|
}
|
|
return nil
|
|
}
|