Hub: add POST /api/v1/notify endpoint for customer notifications

- New notification relay endpoint: receives events from customer controllers,
  looks up customer email preferences, sends via Resend HTTP API
- New tables: customer_notifications (per-customer email + event prefs),
  notification_log (audit trail for all notification attempts)
- Hungarian email template with severity, event type, timestamp
- Config: notifications.resend_api_key + notifications.from_email
- Test events always pass event-type filter

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-16 19:29:55 +01:00
parent 869ff55fd1
commit e531516cfa
4 changed files with 273 additions and 8 deletions
+78
View File
@@ -69,10 +69,88 @@ func (s *Store) migrate() error {
CREATE INDEX IF NOT EXISTS idx_reports_customer
ON reports(customer_id, received_at DESC);
CREATE TABLE IF NOT EXISTS customer_notifications (
customer_id TEXT PRIMARY KEY,
email TEXT NOT NULL DEFAULT '',
enabled_events TEXT NOT NULL DEFAULT '[]',
created_at DATETIME DEFAULT (datetime('now')),
updated_at DATETIME DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS notification_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
customer_id TEXT NOT NULL,
event_type TEXT NOT NULL,
severity TEXT NOT NULL,
message TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'pending',
error_message TEXT,
created_at DATETIME DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_notification_log_customer
ON notification_log(customer_id, created_at DESC);
`)
return err
}
// NotificationPrefs holds per-customer notification preferences.
type NotificationPrefs struct {
CustomerID string
Email string
EnabledEvents []string
}
// GetNotificationPrefs returns notification preferences for a customer.
func (s *Store) GetNotificationPrefs(customerID string) (*NotificationPrefs, error) {
var email, eventsJSON string
err := s.db.QueryRow(
"SELECT email, enabled_events FROM customer_notifications WHERE customer_id = ?",
customerID,
).Scan(&email, &eventsJSON)
if err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
return nil, err
}
var events []string
json.Unmarshal([]byte(eventsJSON), &events)
return &NotificationPrefs{
CustomerID: customerID,
Email: email,
EnabledEvents: events,
}, nil
}
// SaveNotificationPrefs creates or updates notification preferences for a customer.
func (s *Store) SaveNotificationPrefs(customerID, email string, enabledEvents []string) error {
eventsJSON, _ := json.Marshal(enabledEvents)
_, err := s.db.Exec(`
INSERT INTO customer_notifications (customer_id, email, enabled_events, updated_at)
VALUES (?, ?, ?, datetime('now'))
ON CONFLICT(customer_id) DO UPDATE SET
email = excluded.email,
enabled_events = excluded.enabled_events,
updated_at = datetime('now')`,
customerID, email, string(eventsJSON),
)
return err
}
// LogNotification records a notification attempt.
func (s *Store) LogNotification(customerID, eventType, severity, message, status, errorMsg string) error {
_, err := s.db.Exec(`
INSERT INTO notification_log (customer_id, event_type, severity, message, status, error_message)
VALUES (?, ?, ?, ?, ?, ?)`,
customerID, eventType, severity, message, status, errorMsg,
)
return err
}
// SaveReport stores a new report. The reportJSON should be the raw JSON payload.
func (s *Store) SaveReport(customerID string, reportJSON []byte) error {
// Parse denormalized fields from the JSON