Some checks failed
Build + Deploy / build-backend-compliance (push) Successful in 2m43s
Build + Deploy / build-admin-compliance (push) Successful in 1m46s
Build + Deploy / build-ai-sdk (push) Successful in 47s
Build + Deploy / build-developer-portal (push) Successful in 1m0s
Build + Deploy / build-tts (push) Successful in 1m14s
Build + Deploy / build-document-crawler (push) Successful in 37s
Build + Deploy / build-dsms-gateway (push) Successful in 20s
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / loc-budget (push) Failing after 19s
CI / secret-scan (push) Has been skipped
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / nodejs-build (push) Successful in 2m35s
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / test-go (push) Successful in 42s
CI / test-python-backend (push) Successful in 42s
CI / test-python-document-crawler (push) Successful in 24s
CI / test-python-dsms-gateway (push) Successful in 27s
CI / validate-canonical-controls (push) Successful in 23s
Build + Deploy / trigger-orca (push) Failing after 2h32m34s
Zeigt anstehende regulatorische Fristen im Dashboard an, abgeleitet aus den bestehenden Obligation v2 JSON-Dateien. Keine neue DB-Tabelle. Erster News-Eintrag: Widerrufsbutton-Pflicht ab 19.06.2026 (EU-RL 2023/2673, §356a BGB) — eigener Text, keine externe Quelle. Features: - Go Service: scannt Obligations nach Fristen, berechnet Urgency - API: GET /sdk/v1/regulatory-news mit Countdown + Farbcodierung - Dashboard: RegulatoryNewsFeed Sektion mit Countdown-Badges - Vorlage: news-Feld in v2 JSON fuer zukuenftige regulatorische Updates - 11 Tests (Sortierung, Urgency, Deadline-Parsing, Real-File-Test) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
147 lines
3.9 KiB
Go
147 lines
3.9 KiB
Go
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, ", ")
|
|
}
|