package services import ( "fmt" "time" "github.com/jmoiron/sqlx" "mgit.msbls.de/m/KanzlAI-mGMT/internal/models" ) // DetermineService handles event-driven deadline determination. // It walks the proceeding event tree and calculates cascading dates. type DetermineService struct { rules *DeadlineRuleService calculator *DeadlineCalculator } // NewDetermineService creates a new determine service func NewDetermineService(db *sqlx.DB, calculator *DeadlineCalculator) *DetermineService { return &DetermineService{ rules: NewDeadlineRuleService(db), calculator: calculator, } } // TimelineEvent represents a calculated event in the proceeding timeline type TimelineEvent struct { ID string `json:"id"` Code string `json:"code,omitempty"` Name string `json:"name"` Description string `json:"description,omitempty"` PrimaryParty string `json:"primary_party,omitempty"` EventType string `json:"event_type,omitempty"` IsMandatory bool `json:"is_mandatory"` DurationValue int `json:"duration_value"` DurationUnit string `json:"duration_unit"` RuleCode string `json:"rule_code,omitempty"` DeadlineNotes string `json:"deadline_notes,omitempty"` IsSpawn bool `json:"is_spawn"` SpawnLabel string `json:"spawn_label,omitempty"` HasCondition bool `json:"has_condition"` ConditionRuleID string `json:"condition_rule_id,omitempty"` AltRuleCode string `json:"alt_rule_code,omitempty"` AltDurationValue *int `json:"alt_duration_value,omitempty"` AltDurationUnit string `json:"alt_duration_unit,omitempty"` Date string `json:"date,omitempty"` OriginalDate string `json:"original_date,omitempty"` WasAdjusted bool `json:"was_adjusted"` Children []TimelineEvent `json:"children,omitempty"` } // DetermineRequest is the input for POST /api/deadlines/determine type DetermineRequest struct { ProceedingType string `json:"proceeding_type"` TriggerEventDate string `json:"trigger_event_date"` Conditions map[string]bool `json:"conditions"` } // DetermineResponse is the output of the determine endpoint type DetermineResponse struct { ProceedingType string `json:"proceeding_type"` ProceedingName string `json:"proceeding_name"` ProceedingColor string `json:"proceeding_color"` TriggerDate string `json:"trigger_event_date"` Timeline []TimelineEvent `json:"timeline"` TotalDeadlines int `json:"total_deadlines"` } // GetTimeline returns the proceeding event tree (without date calculations) func (s *DetermineService) GetTimeline(proceedingTypeCode string) ([]TimelineEvent, *models.ProceedingType, error) { rules, pt, err := s.rules.GetFullTimeline(proceedingTypeCode) if err != nil { return nil, nil, err } tree := buildTimelineTree(rules) return tree, pt, nil } // Determine calculates the full timeline with cascading dates func (s *DetermineService) Determine(req DetermineRequest) (*DetermineResponse, error) { timeline, pt, err := s.GetTimeline(req.ProceedingType) if err != nil { return nil, fmt.Errorf("loading timeline: %w", err) } triggerDate, err := time.Parse("2006-01-02", req.TriggerEventDate) if err != nil { return nil, fmt.Errorf("invalid trigger_event_date: %w", err) } conditions := req.Conditions if conditions == nil { conditions = make(map[string]bool) } total := s.calculateDates(timeline, triggerDate, conditions) return &DetermineResponse{ ProceedingType: pt.Code, ProceedingName: pt.Name, ProceedingColor: pt.DefaultColor, TriggerDate: req.TriggerEventDate, Timeline: timeline, TotalDeadlines: total, }, nil } // calculateDates walks the tree and calculates dates for each node func (s *DetermineService) calculateDates(events []TimelineEvent, parentDate time.Time, conditions map[string]bool) int { total := 0 for i := range events { ev := &events[i] // Skip inactive spawns: if this is a spawn node and conditions don't include it, skip if ev.IsSpawn && !conditions[ev.ID] { continue } durationValue := ev.DurationValue durationUnit := ev.DurationUnit ruleCode := ev.RuleCode // Apply conditional logic if ev.HasCondition && ev.ConditionRuleID != "" { if conditions[ev.ConditionRuleID] { if ev.AltDurationValue != nil { durationValue = *ev.AltDurationValue } if ev.AltDurationUnit != "" { durationUnit = ev.AltDurationUnit } if ev.AltRuleCode != "" { ruleCode = ev.AltRuleCode } } } // Calculate this node's date if durationValue > 0 { rule := models.DeadlineRule{ DurationValue: durationValue, DurationUnit: durationUnit, } adjusted, original, wasAdjusted := s.calculator.CalculateEndDate(parentDate, rule) ev.Date = adjusted.Format("2006-01-02") ev.OriginalDate = original.Format("2006-01-02") ev.WasAdjusted = wasAdjusted } else { ev.Date = parentDate.Format("2006-01-02") ev.OriginalDate = parentDate.Format("2006-01-02") } ev.RuleCode = ruleCode total++ // Recurse: children's dates cascade from this node's date if len(ev.Children) > 0 { childDate, _ := time.Parse("2006-01-02", ev.Date) total += s.calculateDates(ev.Children, childDate, conditions) } } return total } // buildTimelineTree converts flat rules to a tree of TimelineEvents func buildTimelineTree(rules []models.DeadlineRule) []TimelineEvent { nodeMap := make(map[string]*TimelineEvent, len(rules)) var roots []TimelineEvent // Create event nodes for _, r := range rules { ev := ruleToEvent(r) nodeMap[r.ID.String()] = &ev } // Build tree by parent_id for _, r := range rules { ev := nodeMap[r.ID.String()] if r.ParentID != nil { parentKey := r.ParentID.String() if parent, ok := nodeMap[parentKey]; ok { parent.Children = append(parent.Children, *ev) continue } } roots = append(roots, *ev) } return roots } func ruleToEvent(r models.DeadlineRule) TimelineEvent { ev := TimelineEvent{ ID: r.ID.String(), Name: r.Name, IsMandatory: r.IsMandatory, DurationValue: r.DurationValue, DurationUnit: r.DurationUnit, IsSpawn: r.IsSpawn, HasCondition: r.ConditionRuleID != nil, } if r.Code != nil { ev.Code = *r.Code } if r.Description != nil { ev.Description = *r.Description } if r.PrimaryParty != nil { ev.PrimaryParty = *r.PrimaryParty } if r.EventType != nil { ev.EventType = *r.EventType } if r.RuleCode != nil { ev.RuleCode = *r.RuleCode } if r.DeadlineNotes != nil { ev.DeadlineNotes = *r.DeadlineNotes } if r.SpawnLabel != nil { ev.SpawnLabel = *r.SpawnLabel } if r.ConditionRuleID != nil { ev.ConditionRuleID = r.ConditionRuleID.String() } if r.AltRuleCode != nil { ev.AltRuleCode = *r.AltRuleCode } ev.AltDurationValue = r.AltDurationValue if r.AltDurationUnit != nil { ev.AltDurationUnit = *r.AltDurationUnit } return ev }