Files
warmbly/internal/models/webhook.go
T
Matthew Meszaros d5147659a7 style: gofmt struct alignment after burst_multiplier removal
removing the BurstMultiplier field changed column widths in several
struct literals; let gofmt realign them. no semantic change.
2026-05-27 16:17:53 +00:00

159 lines
6.2 KiB
Go

package models
import (
"encoding/json"
"time"
"github.com/google/uuid"
)
// WebhookEventType is the canonical event-name identifier carried both in
// the subscription filter and the delivery payload. Keep these stable —
// renaming a value breaks every customer subscribed to it.
type WebhookEventType string
const (
// Email account lifecycle
WebhookEventEmailAccountConnected WebhookEventType = "email_account.connected"
WebhookEventEmailAccountRemoved WebhookEventType = "email_account.removed"
// Campaign send pipeline
WebhookEventCampaignEmailSent WebhookEventType = "campaign.email_sent"
WebhookEventCampaignEmailDelivered WebhookEventType = "campaign.email_delivered"
WebhookEventCampaignEmailOpened WebhookEventType = "campaign.email_opened"
WebhookEventCampaignEmailClicked WebhookEventType = "campaign.email_clicked"
WebhookEventCampaignEmailBounced WebhookEventType = "campaign.email_bounced"
WebhookEventCampaignReplyReceived WebhookEventType = "campaign.reply_received"
WebhookEventCampaignUnsubscribed WebhookEventType = "campaign.unsubscribed"
WebhookEventCampaignStarted WebhookEventType = "campaign.started"
WebhookEventCampaignPaused WebhookEventType = "campaign.paused"
WebhookEventCampaignCompleted WebhookEventType = "campaign.completed"
// Warmup
WebhookEventWarmupEmailSent WebhookEventType = "warmup.email_sent"
WebhookEventWarmupHealthChanged WebhookEventType = "warmup.health_changed"
WebhookEventWarmupPlacementInSpam WebhookEventType = "warmup.placement_in_spam"
WebhookEventWarmupQuarantined WebhookEventType = "warmup.quarantined"
WebhookEventWarmupBlocked WebhookEventType = "warmup.blocked"
// Deliverability
WebhookEventDeliverabilityBounce WebhookEventType = "deliverability.bounce"
WebhookEventDeliverabilityComplaint WebhookEventType = "deliverability.complaint"
)
// AllWebhookEventTypes lists every emitted event so the CRUD endpoint can
// validate `event_types` filters and the UI can render a picker.
var AllWebhookEventTypes = []WebhookEventType{
WebhookEventEmailAccountConnected,
WebhookEventEmailAccountRemoved,
WebhookEventCampaignEmailSent,
WebhookEventCampaignEmailDelivered,
WebhookEventCampaignEmailOpened,
WebhookEventCampaignEmailClicked,
WebhookEventCampaignEmailBounced,
WebhookEventCampaignReplyReceived,
WebhookEventCampaignUnsubscribed,
WebhookEventCampaignStarted,
WebhookEventCampaignPaused,
WebhookEventCampaignCompleted,
WebhookEventWarmupEmailSent,
WebhookEventWarmupHealthChanged,
WebhookEventWarmupPlacementInSpam,
WebhookEventWarmupQuarantined,
WebhookEventWarmupBlocked,
WebhookEventDeliverabilityBounce,
WebhookEventDeliverabilityComplaint,
}
func IsValidWebhookEventType(s string) bool {
for _, t := range AllWebhookEventTypes {
if string(t) == s {
return true
}
}
return false
}
// WebhookEndpoint is a customer's subscription to events.
type WebhookEndpoint struct {
ID uuid.UUID `json:"id"`
OrganizationID uuid.UUID `json:"organization_id"`
URL string `json:"url"`
Description string `json:"description"`
EventTypes []string `json:"event_types"`
Enabled bool `json:"enabled"`
LastSuccessAt *time.Time `json:"last_success_at,omitempty"`
LastFailureAt *time.Time `json:"last_failure_at,omitempty"`
LastFailureReason *string `json:"last_failure_reason,omitempty"`
ConsecutiveFailures int `json:"consecutive_failures"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// WebhookEndpointWithSecret is returned once at creation time so the
// client can capture the secret. Subsequent reads do not include it.
type WebhookEndpointWithSecret struct {
WebhookEndpoint
Secret string `json:"secret"`
}
// Subscribes returns true if this endpoint should receive the given event.
// An endpoint with an empty event_types array receives all events.
func (e *WebhookEndpoint) Subscribes(event WebhookEventType) bool {
if !e.Enabled {
return false
}
if len(e.EventTypes) == 0 {
return true
}
for _, t := range e.EventTypes {
if t == string(event) {
return true
}
}
return false
}
// WebhookDeliveryStatus tracks where a delivery attempt sits in its
// lifecycle. 'abandoned' = retries exhausted.
type WebhookDeliveryStatus string
const (
WebhookDeliveryPending WebhookDeliveryStatus = "pending"
WebhookDeliveryInFlight WebhookDeliveryStatus = "in_flight"
WebhookDeliveryDelivered WebhookDeliveryStatus = "delivered"
WebhookDeliveryFailed WebhookDeliveryStatus = "failed"
WebhookDeliveryAbandoned WebhookDeliveryStatus = "abandoned"
)
// WebhookDelivery is one attempt-history record. Multiple rows may exist
// per (event, endpoint) across retries — each row updates in place as
// attempts progress.
type WebhookDelivery struct {
ID uuid.UUID `json:"id"`
EndpointID uuid.UUID `json:"endpoint_id"`
OrganizationID uuid.UUID `json:"organization_id"`
EventType string `json:"event_type"`
EventID uuid.UUID `json:"event_id"`
Payload json.RawMessage `json:"payload"`
Status WebhookDeliveryStatus `json:"status"`
AttemptCount int `json:"attempt_count"`
MaxAttempts int `json:"max_attempts"`
NextAttemptAt time.Time `json:"next_attempt_at"`
LastAttemptAt *time.Time `json:"last_attempt_at,omitempty"`
ResponseStatus *int `json:"response_status,omitempty"`
ResponseBodyExcerpt *string `json:"response_body_excerpt,omitempty"`
ErrorReason *string `json:"error_reason,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// WebhookPayload is the JSON body the dispatcher POSTs to a subscriber.
type WebhookPayload struct {
ID uuid.UUID `json:"id"`
EventType WebhookEventType `json:"event_type"`
OrganizationID uuid.UUID `json:"organization_id"`
CreatedAt time.Time `json:"created_at"`
Data any `json:"data"`
}