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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user