// Package notify owns notification write-side production and live dashboard fan-out. package notify import ( "context" "errors" "fmt" "time" "github.com/google/uuid" "github.com/aoagents/agent-orchestrator/backend/internal/domain" "github.com/aoagents/agent-orchestrator/backend/internal/ports" ) // Store is the write-side notification persistence boundary. type Store interface { CreateNotification(ctx context.Context, rec domain.NotificationRecord) (domain.NotificationRecord, bool, error) } // Publisher pushes newly persisted notifications to live dashboard subscribers. type Publisher interface { Publish(ctx context.Context, rec domain.NotificationRecord) error } // Intent is the lifecycle-to-notification producer contract. type Intent = ports.NotificationIntent // Manager validates lifecycle intents, enriches them into stored rows, persists // unread notifications, and publishes newly inserted rows to live subscribers. type Manager struct { store Store publisher Publisher clock func() time.Time newID func() string } // Deps configures a Manager. type Deps struct { Store Store Publisher Publisher Clock func() time.Time NewID func() string } // New constructs a write-side notification manager. func New(d Deps) *Manager { m := &Manager{store: d.Store, publisher: d.Publisher, clock: d.Clock, newID: d.NewID} if m.clock == nil { m.clock = time.Now } if m.newID == nil { m.newID = func() string { return "ntf_" + uuid.NewString() } } return m } // Notify stores one notification intent and publishes it after persistence. // Duplicate unread rows are treated as a clean no-op. func (m *Manager) Notify(ctx context.Context, intent Intent) error { if m == nil || m.store == nil { return errors.New("notify: store is required") } if intent.CreatedAt.IsZero() { intent.CreatedAt = m.clock().UTC() } rec, err := enrich(intent) if err != nil { return fmt.Errorf("notify enrich: %w", err) } rec.ID = m.newID() created, inserted, err := m.store.CreateNotification(ctx, rec) if err != nil { return fmt.Errorf("notify store: %w", err) } if !inserted || m.publisher == nil { return nil } if err := m.publisher.Publish(ctx, created); err != nil { return fmt.Errorf("notify publish: %w", err) } return nil }