CampaignFlow.MockServer.Webhooks.Dispatcher (CampaignFlow Client v2.2.0)

View Source

Outbound webhook dispatch for the mock server.

Responsibilities:

  • Persist every event to cf_mock_webhook_events (one row per attempt lifecycle).
  • Sign and POST the payload to the configured webhook_url via Req.
  • Track delivery status, attempts, and next_retry_at on the row.
  • Drive sync vs async delivery based on the webhook_delivery_mode config flag.

The Webhooks.RetryScheduler GenServer drives retries by periodically calling list_pending_due/0 and re-dispatching each event via deliver_now/1.

Summary

Functions

Builds the payload for a campaign_status_change webhook event.

Synchronously delivers (or re-delivers) a single webhook event by id.

Persists a webhook event row and dispatches delivery (sync or async depending on webhook_delivery_mode config).

Builds the payload for a finance_application_status_change webhook event.

Returns webhook events that are eligible for delivery: status pending, attempts below max, and next_retry_at either nil or in the past.

Functions

campaign_status_change(campaign, previous_status)

@spec campaign_status_change(
  CampaignFlow.MockServer.Schema.Campaign.t(),
  String.t() | nil
) ::
  {String.t(), map()}

Builds the payload for a campaign_status_change webhook event.

Returns {event_type, payload} for direct passing to enqueue/2.

deliver_now(event_id)

@spec deliver_now(integer()) ::
  {:ok,
   %CampaignFlow.MockServer.Schema.WebhookEvent{
     __meta__: term(),
     attempts: term(),
     delivered_at: term(),
     event_type: term(),
     id: term(),
     inserted_at: term(),
     last_attempt_at: term(),
     last_error: term(),
     max_attempts: term(),
     next_retry_at: term(),
     payload: term(),
     response_body: term(),
     response_status: term(),
     status: term(),
     target_url: term(),
     updated_at: term()
   }}
  | {:error, term()}

Synchronously delivers (or re-delivers) a single webhook event by id.

Idempotent: events already in a terminal status (delivered, failed, skipped) are returned unchanged.

Returns {:ok, event} for both happy and retry-able failure outcomes — callers introspect the row to determine status. Only terminal config errors return {:error, reason}.

enqueue(event_type, payload)

@spec enqueue(String.t(), map()) ::
  {:ok,
   %CampaignFlow.MockServer.Schema.WebhookEvent{
     __meta__: term(),
     attempts: term(),
     delivered_at: term(),
     event_type: term(),
     id: term(),
     inserted_at: term(),
     last_attempt_at: term(),
     last_error: term(),
     max_attempts: term(),
     next_retry_at: term(),
     payload: term(),
     response_body: term(),
     response_status: term(),
     status: term(),
     target_url: term(),
     updated_at: term()
   }}
  | {:error, term()}

Persists a webhook event row and dispatches delivery (sync or async depending on webhook_delivery_mode config).

finance_application_status_change(campaign, application, previous_application_status, campaign_previous_status)

@spec finance_application_status_change(
  CampaignFlow.MockServer.Schema.Campaign.t(),
  CampaignFlow.MockServer.Schema.FinanceApplication.t(),
  String.t() | nil,
  String.t() | nil
) :: {String.t(), map()}

Builds the payload for a finance_application_status_change webhook event.

The payload includes a snapshot of the linked campaign so receivers can correlate the event to a campaign without an extra round-trip.

The campaign_previous_status argument controls what appears in the finance event payload's campaign.previousStatus field:

  • When the finance status change does NOT trigger a campaign side-effect, callers should pass campaign.status so the field mirrors the current status (matching the documented payload shape for non-cascading events).
  • When the finance status change DOES trigger a campaign side-effect, callers should pass nil — the actual campaign transition is reported by a separate campaign_status_change event with its own proper previousStatus, so embedding it here would be redundant.

Returns {event_type, payload} for direct passing to enqueue/2.

list_pending_due()

@spec list_pending_due() :: [
  %CampaignFlow.MockServer.Schema.WebhookEvent{
    __meta__: term(),
    attempts: term(),
    delivered_at: term(),
    event_type: term(),
    id: term(),
    inserted_at: term(),
    last_attempt_at: term(),
    last_error: term(),
    max_attempts: term(),
    next_retry_at: term(),
    payload: term(),
    response_body: term(),
    response_status: term(),
    status: term(),
    target_url: term(),
    updated_at: term()
  }
]

Returns webhook events that are eligible for delivery: status pending, attempts below max, and next_retry_at either nil or in the past.