Files
KanzlAI-mGMT/backend/internal/services/deadline_rule_service.go
m 42a62d45bf feat: add deadline CRUD, calculator, and holiday services (Phase 1C)
- 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)
2026-03-25 13:31:29 +01:00

176 lines
5.9 KiB
Go

package services
import (
"fmt"
"github.com/jmoiron/sqlx"
"mgit.msbls.de/m/KanzlAI-mGMT/internal/models"
)
// DeadlineRuleService handles deadline rule queries
type DeadlineRuleService struct {
db *sqlx.DB
}
// NewDeadlineRuleService creates a new deadline rule service
func NewDeadlineRuleService(db *sqlx.DB) *DeadlineRuleService {
return &DeadlineRuleService{db: db}
}
// List returns deadline rules, optionally filtered by proceeding type
func (s *DeadlineRuleService) List(proceedingTypeID *int) ([]models.DeadlineRule, error) {
var rules []models.DeadlineRule
var err error
if proceedingTypeID != nil {
err = s.db.Select(&rules,
`SELECT id, proceeding_type_id, parent_id, code, name, description,
primary_party, event_type, is_mandatory, duration_value, duration_unit,
timing, rule_code, deadline_notes, sequence_order, condition_rule_id,
alt_duration_value, alt_duration_unit, alt_rule_code, is_active,
created_at, updated_at
FROM deadline_rules
WHERE proceeding_type_id = $1 AND is_active = true
ORDER BY sequence_order`, *proceedingTypeID)
} else {
err = s.db.Select(&rules,
`SELECT id, proceeding_type_id, parent_id, code, name, description,
primary_party, event_type, is_mandatory, duration_value, duration_unit,
timing, rule_code, deadline_notes, sequence_order, condition_rule_id,
alt_duration_value, alt_duration_unit, alt_rule_code, is_active,
created_at, updated_at
FROM deadline_rules
WHERE is_active = true
ORDER BY proceeding_type_id, sequence_order`)
}
if err != nil {
return nil, fmt.Errorf("listing deadline rules: %w", err)
}
return rules, nil
}
// RuleTreeNode represents a deadline rule with its children
type RuleTreeNode struct {
models.DeadlineRule
Children []RuleTreeNode `json:"children,omitempty"`
}
// GetRuleTree returns a hierarchical tree of rules for a proceeding type
func (s *DeadlineRuleService) GetRuleTree(proceedingTypeCode string) ([]RuleTreeNode, error) {
// First resolve proceeding type code to ID
var pt models.ProceedingType
err := s.db.Get(&pt,
`SELECT id, code, name, description, jurisdiction, default_color, sort_order, is_active
FROM proceeding_types
WHERE code = $1 AND is_active = true`, proceedingTypeCode)
if err != nil {
return nil, fmt.Errorf("resolving proceeding type %q: %w", proceedingTypeCode, err)
}
// Get all rules for this proceeding type
var rules []models.DeadlineRule
err = s.db.Select(&rules,
`SELECT id, proceeding_type_id, parent_id, code, name, description,
primary_party, event_type, is_mandatory, duration_value, duration_unit,
timing, rule_code, deadline_notes, sequence_order, condition_rule_id,
alt_duration_value, alt_duration_unit, alt_rule_code, is_active,
created_at, updated_at
FROM deadline_rules
WHERE proceeding_type_id = $1 AND is_active = true
ORDER BY sequence_order`, pt.ID)
if err != nil {
return nil, fmt.Errorf("listing rules for type %q: %w", proceedingTypeCode, err)
}
return buildTree(rules), nil
}
// GetByIDs returns deadline rules by their IDs
func (s *DeadlineRuleService) GetByIDs(ids []string) ([]models.DeadlineRule, error) {
if len(ids) == 0 {
return nil, nil
}
query, args, err := sqlx.In(
`SELECT id, proceeding_type_id, parent_id, code, name, description,
primary_party, event_type, is_mandatory, duration_value, duration_unit,
timing, rule_code, deadline_notes, sequence_order, condition_rule_id,
alt_duration_value, alt_duration_unit, alt_rule_code, is_active,
created_at, updated_at
FROM deadline_rules
WHERE id IN (?) AND is_active = true
ORDER BY sequence_order`, ids)
if err != nil {
return nil, fmt.Errorf("building IN query: %w", err)
}
query = s.db.Rebind(query)
var rules []models.DeadlineRule
err = s.db.Select(&rules, query, args...)
if err != nil {
return nil, fmt.Errorf("fetching rules by IDs: %w", err)
}
return rules, nil
}
// GetRulesForProceedingType returns all active rules for a proceeding type ID
func (s *DeadlineRuleService) GetRulesForProceedingType(proceedingTypeID int) ([]models.DeadlineRule, error) {
var rules []models.DeadlineRule
err := s.db.Select(&rules,
`SELECT id, proceeding_type_id, parent_id, code, name, description,
primary_party, event_type, is_mandatory, duration_value, duration_unit,
timing, rule_code, deadline_notes, sequence_order, condition_rule_id,
alt_duration_value, alt_duration_unit, alt_rule_code, is_active,
created_at, updated_at
FROM deadline_rules
WHERE proceeding_type_id = $1 AND is_active = true
ORDER BY sequence_order`, proceedingTypeID)
if err != nil {
return nil, fmt.Errorf("listing rules for proceeding type %d: %w", proceedingTypeID, err)
}
return rules, nil
}
// ListProceedingTypes returns all active proceeding types
func (s *DeadlineRuleService) ListProceedingTypes() ([]models.ProceedingType, error) {
var types []models.ProceedingType
err := s.db.Select(&types,
`SELECT id, code, name, description, jurisdiction, default_color, sort_order, is_active
FROM proceeding_types
WHERE is_active = true
ORDER BY sort_order`)
if err != nil {
return nil, fmt.Errorf("listing proceeding types: %w", err)
}
return types, nil
}
// buildTree converts a flat list of rules into a hierarchical tree
func buildTree(rules []models.DeadlineRule) []RuleTreeNode {
nodeMap := make(map[string]*RuleTreeNode, len(rules))
var roots []RuleTreeNode
// Create nodes
for _, r := range rules {
node := RuleTreeNode{DeadlineRule: r}
nodeMap[r.ID.String()] = &node
}
// Build tree
for _, r := range rules {
node := nodeMap[r.ID.String()]
if r.ParentID != nil {
parentKey := r.ParentID.String()
if parent, ok := nodeMap[parentKey]; ok {
parent.Children = append(parent.Children, *node)
continue
}
}
roots = append(roots, *node)
}
return roots
}