lib/openagents/settlement/payment_gateway.ex

main at 58e6347eeb72 · 2 KB

defmodule OpenAgents.Settlement.PaymentGateway do
  @moduledoc """
  The treasury payment boundary for bounty settlement.

  The settlement domain never holds custody, keys, or a node connection. It
  hands an authorized, idempotency-keyed request to the configured gateway —
  the self-custodial MoneyDevKit treasury path in production — and records
  whatever exact evidence comes back.

  A gateway reports one of three answers:

    * `{:ok, settled}` — the payment reached the destination, with
      `payment_hash`, `preimage_digest`, `fee_sats`, `paid_at`, and
      `gateway_ref`.
    * `{:pending, reason_code}` — the gateway accepted the request but has no
      terminal answer yet. The intent stays payable under the same key.
    * `{:error, reason_code}` — the attempt failed. A retry uses the same key.

  `lookup/1` answers the same shapes for a key that was already dispatched,
  which is how a lost acknowledgement reconciles without a second payment.
  """

  @type request :: %{
          idempotency_key: String.t(),
          amount_sats: pos_integer(),
          destination_kind: String.t(),
          destination: String.t(),
          memo: String.t()
        }

  @type settled :: %{
          payment_hash: String.t(),
          preimage_digest: String.t(),
          fee_sats: non_neg_integer(),
          paid_at: DateTime.t(),
          gateway_ref: String.t()
        }

  @type answer ::
          {:ok, settled()} | {:pending, String.t()} | {:error, String.t()} | {:unknown, nil}

  @callback pay(request()) :: answer()
  @callback lookup(String.t()) :: answer()

  @doc "The configured gateway module, fail-closed when unset."
  def gateway do
    Application.get_env(
      :openagents,
      :settlement_payment_gateway,
      OpenAgents.Settlement.PaymentGateway.Unconfigured
    )
  end

  @doc "Dispatches one authorized payment request."
  def pay(request), do: gateway().pay(request)

  @doc "Reads the terminal state of a previously dispatched key."
  def lookup(idempotency_key), do: gateway().lookup(idempotency_key)
end