feat: Add webhook client for carrier connector profile event notifications to API server

- Add WebhookClient struct with httpClient (10s timeout), webhookURL, and apiKey fields
- Add WebhookEvent struct with event_type, timestamp, profile_iccid, and data fields
- Add event type constants (profile.downloaded, activated, deactivated, deleted, status_changed, download.failed)
- Add SendNotification method with JSON marshaling, Bearer token auth, and error handling for 4xx/5xx responses
- Add helper
This commit is contained in:
nutcas3
2026-04-25 05:41:18 +03:00
parent 23da0f834f
commit b983e226f6
@@ -0,0 +1,126 @@
package webhook
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"time"
)
// WebhookClient sends notifications to the api-server
type WebhookClient struct {
httpClient *http.Client
webhookURL string
apiKey string
}
// NewWebhookClient creates a new webhook client
func NewWebhookClient(webhookURL, apiKey string) *WebhookClient {
return &WebhookClient{
httpClient: &http.Client{
Timeout: 10 * time.Second,
},
webhookURL: webhookURL,
apiKey: apiKey,
}
}
// WebhookEvent represents a webhook notification event
type WebhookEvent struct {
EventType string `json:"event_type"`
Timestamp time.Time `json:"timestamp"`
ProfileICCID string `json:"profile_iccid"`
Data map[string]interface{} `json:"data"`
}
// Event types
const (
EventProfileDownloaded = "profile.downloaded"
EventProfileActivated = "profile.activated"
EventProfileDeactivated = "profile.deactivated"
EventProfileDeleted = "profile.deleted"
EventProfileStatusChange = "profile.status_changed"
EventDownloadFailed = "download.failed"
)
// SendNotification sends a webhook notification to the api-server
func (w *WebhookClient) SendNotification(ctx context.Context, event *WebhookEvent) error {
if w.webhookURL == "" {
return nil // Webhook disabled
}
payload, err := json.Marshal(event)
if err != nil {
return fmt.Errorf("failed to marshal webhook event: %w", err)
}
req, err := http.NewRequestWithContext(ctx, "POST", w.webhookURL, bytes.NewBuffer(payload))
if err != nil {
return fmt.Errorf("failed to create webhook request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
if w.apiKey != "" {
req.Header.Set("Authorization", "Bearer "+w.apiKey)
}
resp, err := w.httpClient.Do(req)
if err != nil {
return fmt.Errorf("failed to send webhook: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
return fmt.Errorf("webhook returned status %d", resp.StatusCode)
}
return nil
}
// SendProfileDownloaded sends a notification when a profile is downloaded
func (w *WebhookClient) SendProfileDownloaded(ctx context.Context, iccid string, data map[string]interface{}) error {
event := &WebhookEvent{
EventType: EventProfileDownloaded,
Timestamp: time.Now(),
ProfileICCID: iccid,
Data: data,
}
return w.SendNotification(ctx, event)
}
// SendProfileActivated sends a notification when a profile is activated
func (w *WebhookClient) SendProfileActivated(ctx context.Context, iccid string, data map[string]interface{}) error {
event := &WebhookEvent{
EventType: EventProfileActivated,
Timestamp: time.Now(),
ProfileICCID: iccid,
Data: data,
}
return w.SendNotification(ctx, event)
}
// SendProfileDeleted sends a notification when a profile is deleted
func (w *WebhookClient) SendProfileDeleted(ctx context.Context, iccid string, data map[string]interface{}) error {
event := &WebhookEvent{
EventType: EventProfileDeleted,
Timestamp: time.Now(),
ProfileICCID: iccid,
Data: data,
}
return w.SendNotification(ctx, event)
}
// SendDownloadFailed sends a notification when a profile download fails
func (w *WebhookClient) SendDownloadFailed(ctx context.Context, iccid string, errorMessage string) error {
event := &WebhookEvent{
EventType: EventDownloadFailed,
Timestamp: time.Now(),
ProfileICCID: iccid,
Data: map[string]interface{}{
"error": errorMessage,
},
}
return w.SendNotification(ctx, event)
}