defmodule Codat.Webhooks.Handler do
@moduledoc """
Behaviour for implementing typed Codat webhook event handlers.
## Example
defmodule MyApp.CodatWebhookHandler do
use Codat.Webhooks.Handler
@impl true
def handle_event("invoices.write.successful", payload, _metadata) do
MyApp.InvoiceSync.handle_success(payload["companyId"], payload["data"])
:ok
end
@impl true
def handle_event(_event_type, _payload, _metadata), do: :ok
end
## Return Values
- `:ok` — processed; Plug responds `200 OK`
- `{:error, reason}` — app error; Plug logs and responds `200 OK`
- `{:retry, reason}` — Plug responds `500` to trigger Codat retry
"""
@type event_type :: String.t()
@type payload :: map()
@type metadata :: %{
required(:message_id) => String.t(),
required(:timestamp) => integer(),
required(:attempt_number) => non_neg_integer() | nil
}
@type result :: :ok | {:error, term()} | {:retry, term()}
@callback handle_event(event_type(), payload(), metadata()) :: result()
@callback on_exception(event_type(), payload(), Exception.t(), list()) :: :ok
@optional_callbacks [on_exception: 4]
defmacro __using__(_opts) do
quote do
@behaviour Codat.Webhooks.Handler
require Logger
@impl Codat.Webhooks.Handler
def on_exception(event_type, _payload, exception, stacktrace) do
Logger.error(
"[Codat.Webhooks] Unhandled exception in #{inspect(__MODULE__)} " <>
"for event #{event_type}: #{Exception.message(exception)}\n" <>
Exception.format_stacktrace(stacktrace)
)
:ok
end
defoverridable on_exception: 4
end
end
end
defmodule Codat.Webhooks.NoOpHandler do
@moduledoc "A no-op webhook handler for testing and development. Logs all events."
use Codat.Webhooks.Handler
require Logger
@impl Codat.Webhooks.Handler
def handle_event(event_type, payload, metadata) do
Logger.debug(fn ->
"[Codat.Webhooks.NoOpHandler] #{event_type} " <>
"(msg_id: #{metadata.message_id}, company: #{payload["companyId"]})"
end)
:ok
end
end
defmodule Codat.Webhooks.BroadcastHandler do
@moduledoc """
A webhook handler that broadcasts events to a `Phoenix.PubSub` topic.
## Usage
defmodule MyApp.CodatBroadcaster do
use Codat.Webhooks.BroadcastHandler,
pubsub: MyApp.PubSub,
topic: "codat_events"
end
"""
defmacro __using__(opts) do
pubsub_name = Keyword.fetch!(opts, :pubsub)
topic = Keyword.get(opts, :topic, "codat_events")
quote do
use Codat.Webhooks.Handler
@impl Codat.Webhooks.Handler
def handle_event(event_type, payload, _metadata) do
Phoenix.PubSub.broadcast(
unquote(pubsub_name),
unquote(topic),
{:codat_event, event_type, payload}
)
:ok
end
defoverridable handle_event: 3
end
end
end