Files
warmbly/internal/models/webhook.go
T
2026-06-08 06:25:40 +02:00

177 lines
7.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"
// campaign.deliverability_warning fires when a campaign's rolling
// bounce/complaint rate enters the early-warning band (half the pause
// threshold) — a graduated signal short of an auto-pause.
WebhookEventCampaignDeliverabilityWarning WebhookEventType = "campaign.deliverability_warning"
// campaign.action fires from a "notify" action node in a sequence flow.
WebhookEventCampaignAction WebhookEventType = "campaign.action"
// 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"
// Meetings (inbound from Calendly / Cal.com). A booked call is one of the
// most actionable signals an outbound team gets, so it is a first-class
// event other connections can subscribe to (Slack ping, CRM upsert).
WebhookEventMeetingBooked WebhookEventType = "meeting.booked"
WebhookEventMeetingRescheduled WebhookEventType = "meeting.rescheduled"
WebhookEventMeetingCanceled WebhookEventType = "meeting.canceled"
)
// 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,
WebhookEventCampaignDeliverabilityWarning,
WebhookEventCampaignAction,
WebhookEventWarmupEmailSent,
WebhookEventWarmupHealthChanged,
WebhookEventWarmupPlacementInSpam,
WebhookEventWarmupQuarantined,
WebhookEventWarmupBlocked,
WebhookEventDeliverabilityBounce,
WebhookEventDeliverabilityComplaint,
WebhookEventMeetingBooked,
WebhookEventMeetingRescheduled,
WebhookEventMeetingCanceled,
}
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"`
}