F-6 from t-paliad-074 architecture audit. The Gitea repo was renamed m/patholo → mAi/paliad → m/paliad, but go.mod still declared `mgit.msbls.de/m/patholo` and every internal import echoed the pre-rebrand name. Sweep: - go.mod: module path → mgit.msbls.de/m/paliad - All *.go files: imports rewritten via sed - README.md, docs/design-kanzlai-integration.md: mAi/paliad → m/paliad - Frontend issue-reference comments (mAi/paliad#N → m/paliad#N) in i18n.ts, theme.ts, sidebar.ts, app.ts, Sidebar.tsx, PWAHead.tsx, global.css Verified: go build/vet/test ./... clean, bun run build clean, no remaining mgit.msbls.de/m/patholo or mAi/paliad references outside docs that intentionally describe the rename history.
168 lines
5.6 KiB
Go
168 lines
5.6 KiB
Go
package services
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/jmoiron/sqlx"
|
|
|
|
"mgit.msbls.de/m/paliad/internal/models"
|
|
)
|
|
|
|
// DeadlineRuleService reads paliad.deadline_rules + paliad.proceeding_types.
|
|
// Rules are static reference data; no visibility check needed.
|
|
type DeadlineRuleService struct {
|
|
db *sqlx.DB
|
|
}
|
|
|
|
// NewDeadlineRuleService wires the service to the pool.
|
|
func NewDeadlineRuleService(db *sqlx.DB) *DeadlineRuleService {
|
|
return &DeadlineRuleService{db: db}
|
|
}
|
|
|
|
const ruleColumns = `id, proceeding_type_id, parent_id, code, name, name_en,
|
|
description, primary_party, event_type, is_mandatory, duration_value,
|
|
duration_unit, timing, rule_code, deadline_notes, sequence_order,
|
|
condition_rule_id, condition_flag, alt_duration_value, alt_duration_unit, alt_rule_code,
|
|
anchor_alt, is_spawn, spawn_label, is_active, created_at, updated_at`
|
|
|
|
const proceedingTypeColumns = `id, code, name, name_en, description, jurisdiction,
|
|
category, default_color, sort_order, is_active`
|
|
|
|
// List returns active rules, optionally filtered by proceeding type.
|
|
func (s *DeadlineRuleService) List(ctx context.Context, proceedingTypeID *int) ([]models.DeadlineRule, error) {
|
|
var rules []models.DeadlineRule
|
|
var err error
|
|
|
|
if proceedingTypeID != nil {
|
|
err = s.db.SelectContext(ctx, &rules,
|
|
`SELECT `+ruleColumns+`
|
|
FROM paliad.deadline_rules
|
|
WHERE proceeding_type_id = $1 AND is_active = true
|
|
ORDER BY sequence_order`, *proceedingTypeID)
|
|
} else {
|
|
err = s.db.SelectContext(ctx, &rules,
|
|
`SELECT `+ruleColumns+`
|
|
FROM paliad.deadline_rules
|
|
WHERE is_active = true
|
|
ORDER BY proceeding_type_id, sequence_order`)
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list deadline rules: %w", err)
|
|
}
|
|
return rules, nil
|
|
}
|
|
|
|
// RuleTreeNode pairs a rule with its child rules in a parent_id hierarchy.
|
|
type RuleTreeNode struct {
|
|
models.DeadlineRule
|
|
Children []RuleTreeNode `json:"children,omitempty"`
|
|
}
|
|
|
|
// GetRuleTree returns rules for a proceeding type as a tree (same proceeding type only).
|
|
func (s *DeadlineRuleService) GetRuleTree(ctx context.Context, proceedingTypeCode string) ([]RuleTreeNode, error) {
|
|
var pt models.ProceedingType
|
|
if err := s.db.GetContext(ctx, &pt,
|
|
`SELECT `+proceedingTypeColumns+`
|
|
FROM paliad.proceeding_types
|
|
WHERE code = $1 AND is_active = true`, proceedingTypeCode); err != nil {
|
|
return nil, fmt.Errorf("resolve proceeding type %q: %w", proceedingTypeCode, err)
|
|
}
|
|
|
|
var rules []models.DeadlineRule
|
|
if err := s.db.SelectContext(ctx, &rules,
|
|
`SELECT `+ruleColumns+`
|
|
FROM paliad.deadline_rules
|
|
WHERE proceeding_type_id = $1 AND is_active = true
|
|
ORDER BY sequence_order`, pt.ID); err != nil {
|
|
return nil, fmt.Errorf("list rules for %q: %w", proceedingTypeCode, err)
|
|
}
|
|
return buildTree(rules), nil
|
|
}
|
|
|
|
// GetFullTimeline returns all rules in the tree starting at the given proceeding
|
|
// type, following parent_id even across proceeding types (for cross-type spawns
|
|
// like "Appeal" hanging off an INF Decision).
|
|
func (s *DeadlineRuleService) GetFullTimeline(ctx context.Context, proceedingTypeCode string) ([]models.DeadlineRule, *models.ProceedingType, error) {
|
|
var pt models.ProceedingType
|
|
if err := s.db.GetContext(ctx, &pt,
|
|
`SELECT `+proceedingTypeColumns+`
|
|
FROM paliad.proceeding_types
|
|
WHERE code = $1 AND is_active = true`, proceedingTypeCode); err != nil {
|
|
return nil, nil, fmt.Errorf("resolve proceeding type %q: %w", proceedingTypeCode, err)
|
|
}
|
|
|
|
var rules []models.DeadlineRule
|
|
err := s.db.SelectContext(ctx, &rules, `
|
|
WITH RECURSIVE tree AS (
|
|
SELECT * FROM paliad.deadline_rules
|
|
WHERE proceeding_type_id = $1 AND parent_id IS NULL AND is_active = true
|
|
UNION ALL
|
|
SELECT dr.* FROM paliad.deadline_rules dr
|
|
JOIN tree t ON dr.parent_id = t.id
|
|
WHERE dr.is_active = true
|
|
)
|
|
SELECT `+ruleColumns+` FROM tree ORDER BY sequence_order`, pt.ID)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("fetch timeline for %q: %w", proceedingTypeCode, err)
|
|
}
|
|
return rules, &pt, nil
|
|
}
|
|
|
|
// GetByIDs fetches a set of rules by UUID.
|
|
func (s *DeadlineRuleService) GetByIDs(ctx context.Context, ids []uuid.UUID) ([]models.DeadlineRule, error) {
|
|
if len(ids) == 0 {
|
|
return nil, nil
|
|
}
|
|
query, args, err := sqlx.In(
|
|
`SELECT `+ruleColumns+`
|
|
FROM paliad.deadline_rules
|
|
WHERE id IN (?) AND is_active = true
|
|
ORDER BY sequence_order`, ids)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("build IN query: %w", err)
|
|
}
|
|
query = s.db.Rebind(query)
|
|
|
|
var rules []models.DeadlineRule
|
|
if err := s.db.SelectContext(ctx, &rules, query, args...); err != nil {
|
|
return nil, fmt.Errorf("fetch rules by IDs: %w", err)
|
|
}
|
|
return rules, nil
|
|
}
|
|
|
|
// ListProceedingTypes returns active proceeding types ordered by sort_order.
|
|
func (s *DeadlineRuleService) ListProceedingTypes(ctx context.Context) ([]models.ProceedingType, error) {
|
|
var types []models.ProceedingType
|
|
if err := s.db.SelectContext(ctx, &types,
|
|
`SELECT `+proceedingTypeColumns+`
|
|
FROM paliad.proceeding_types
|
|
WHERE is_active = true
|
|
ORDER BY sort_order`); err != nil {
|
|
return nil, fmt.Errorf("list proceeding types: %w", err)
|
|
}
|
|
return types, nil
|
|
}
|
|
|
|
// buildTree converts a flat rule slice into a parent_id-rooted tree.
|
|
func buildTree(rules []models.DeadlineRule) []RuleTreeNode {
|
|
nodeMap := make(map[uuid.UUID]*RuleTreeNode, len(rules))
|
|
var roots []RuleTreeNode
|
|
|
|
for _, r := range rules {
|
|
nodeMap[r.ID] = &RuleTreeNode{DeadlineRule: r}
|
|
}
|
|
for _, r := range rules {
|
|
node := nodeMap[r.ID]
|
|
if r.ParentID != nil {
|
|
if parent, ok := nodeMap[*r.ParentID]; ok {
|
|
parent.Children = append(parent.Children, *node)
|
|
continue
|
|
}
|
|
}
|
|
roots = append(roots, *node)
|
|
}
|
|
return roots
|
|
}
|