package ucca import ( "fmt" "sort" "strings" "time" ) // RegulatoryNewsItem is a single news item for dashboard display. type RegulatoryNewsItem struct { ID string `json:"id"` Headline string `json:"headline"` Summary string `json:"summary"` LegalReference string `json:"legal_reference"` Deadline string `json:"deadline"` DaysRemaining int `json:"days_remaining"` Urgency string `json:"urgency"` // critical, high, medium, low Affected string `json:"affected"` ActionRequired string `json:"action_required"` ActionLink string `json:"action_link"` Regulation string `json:"regulation"` Sanctions string `json:"sanctions,omitempty"` } // RegulatoryNewsFilter controls which news items are returned. type RegulatoryNewsFilter struct { BusinessModel string `json:"business_model,omitempty"` HorizonDays int `json:"horizon_days,omitempty"` // default 365 Limit int `json:"limit,omitempty"` // default 5 } // GetRegulatoryNews scans all v2 obligations for upcoming deadlines // and returns formatted news items sorted by urgency. func GetRegulatoryNews(regulations map[string]*V2RegulationFile, filter RegulatoryNewsFilter) []RegulatoryNewsItem { if filter.HorizonDays <= 0 { filter.HorizonDays = 365 } if filter.Limit <= 0 { filter.Limit = 5 } today := time.Now().UTC().Truncate(24 * time.Hour) horizon := today.AddDate(0, 0, filter.HorizonDays) var items []RegulatoryNewsItem for _, reg := range regulations { for _, obl := range reg.Obligations { deadline, ok := resolveDeadline(obl) if !ok || deadline.Before(today) || deadline.After(horizon) { continue } days := int(deadline.Sub(today).Hours() / 24) item := buildNewsItem(obl, reg.Regulation, deadline, days) items = append(items, item) } } sort.Slice(items, func(i, j int) bool { return items[i].DaysRemaining < items[j].DaysRemaining }) if len(items) > filter.Limit { items = items[:filter.Limit] } return items } func buildNewsItem(obl V2Obligation, regulation string, deadline time.Time, days int) RegulatoryNewsItem { item := RegulatoryNewsItem{ ID: obl.ID, Deadline: deadline.Format("2006-01-02"), DaysRemaining: days, Urgency: computeUrgency(days), Regulation: regulation, } // Use hand-crafted news if available if obl.News != nil { item.Headline = obl.News.Headline item.Summary = obl.News.Summary item.ActionRequired = obl.News.ActionRequired item.Affected = obl.News.Affected item.ActionLink = obl.News.ActionLink } else { // Auto-generate from obligation data item.Headline = fmt.Sprintf("%s — ab %s", obl.Title, deadline.Format("02.01.2006")) item.Summary = obl.Description item.ActionRequired = "Pruefen Sie die Anforderungen und ergreifen Sie Massnahmen." item.ActionLink = obl.BreakpilotFeature } item.LegalReference = formatLegalReference(obl.LegalBasis) if obl.Sanctions != nil { item.Sanctions = obl.Sanctions.MaxFine } return item } func resolveDeadline(obl V2Obligation) (time.Time, bool) { // Check explicit deadline.date first if obl.Deadline != nil && obl.Deadline.Date != "" { t, err := time.Parse("2006-01-02", obl.Deadline.Date) if err == nil { return t, true } } // Fallback to valid_from if obl.ValidFrom != "" { t, err := time.Parse("2006-01-02", obl.ValidFrom) if err == nil { return t, true } } return time.Time{}, false } func computeUrgency(daysRemaining int) string { switch { case daysRemaining <= 30: return "critical" case daysRemaining <= 90: return "high" case daysRemaining <= 180: return "medium" default: return "low" } } func formatLegalReference(bases []V2LegalBasis) string { if len(bases) == 0 { return "" } var refs []string for _, b := range bases { ref := b.Article if b.Norm != "" { ref = b.Article + " " + b.Norm } refs = append(refs, ref) } return strings.Join(refs, ", ") }