Skip to main content

lib/paysafe.ex

defmodule Paysafe do
  @moduledoc """
  Production-grade Elixir client for the Paysafe API.

  ## Overview

  This library provides complete coverage of the Paysafe developer platform:

    - **Payments API** — Payment handles, payments, settlements, refunds, payouts,
      verifications, and customer/vault management.
    - **Payment Scheduler API** — Plans and subscriptions for recurring billing.
    - **Applications API** — Programmatic merchant onboarding.
    - **Value Added Services** — FX Rates, Customer Identity (KYC), Bank Account
      Validation, Network Tokenization, Account Updater.
    - **Webhooks** — HMAC-SHA256 verification and typed event parsing.

  ## Configuration

  Build a config struct from your credentials:

      config = Paysafe.Config.new!(
        username: "1001062690",
        password: "B-qa2-0-...",
        environment: :test,
        account_id: "1009688230"
      )

  Or load from `config/config.exs`:

      # config/config.exs
      config :paysafe,
        username: System.get_env("PAYSAFE_USERNAME"),
        password: System.get_env("PAYSAFE_PASSWORD"),
        environment: :test,
        account_id: "1009688230"

      # runtime:
      config = Paysafe.Config.from_env!()

  ## Quick start — card payment

      config = Paysafe.Config.new!(
        username: "...",
        password: "...",
        environment: :test,
        account_id: "1009688230"
      )

      # 1. Create a payment handle
      {:ok, handle} = Paysafe.create_payment_handle(config, %{
        merchant_ref_num: "order-#{:rand.uniform(999_999)}",
        amount: 5000,
        currency_code: "USD",
        payment_type: "CARD",
        transaction_type: "PAYMENT",
        card: %{
          card_num: "4111111111111111",
          card_expiry: %{month: 12, year: 2030},
          cvv: "123",
          holder_name: "Jane Doe"
        },
        billing_details: %{
          street: "123 Main St",
          city: "New York",
          state: "NY",
          country: "US",
          zip: "10001"
        },
        settle_with_auth: true
      })

      # 2. Use the token in a payment
      {:ok, payment} = Paysafe.create_payment(config, %{
        merchant_ref_num: "payment-001",
        amount: 5000,
        currency_code: "USD",
        settle_with_auth: true,
        payment_handle_token: handle.payment_handle_token
      })

  ## Payment method coverage

  The Payments API supports 30+ payment methods:

    - **Cards** — Visa, Mastercard, Amex, Discover, Debit, Prepaid, Corporate.
    - **Digital Wallets** — Apple Pay, Google Pay, PayPal, Venmo, Skrill, Neteller.
    - **Bank Transfers** — iDEAL, EPS, BLIK, Interac e-Transfer, Mazooma, Pay by Bank.
    - **Direct Debit** — ACH (US), BACS (UK), EFT (CA), SEPA (EU).
    - **Cash** — PaysafeCash.
    - **Vouchers** — PaysafeCard, Openbucks, Multibanco.
    - **LatAm** — SafetyPay (Pix, Boleto, KHIPU, MACH), PagoEfectivo, Rapid Transfer.
    - **Crypto** — Pay with Crypto.

  ## Architecture

    - Config validated at construction via `NimbleOptions`.
    - All requests have exponential backoff retry on transient failures.
    - Token-bucket rate limiting per account via `ExRated`.
    - Telemetry events on every request (`:paysafe, :request, :start/:stop/:exception`).
    - HMAC-SHA256 webhook verification with constant-time comparison.
    - All public functions return `{:ok, result} | {:error, %Paysafe.Error{}}`.

  """

  alias Paysafe.Payments.{
    Customers,
    PaymentHandles,
    Payments,
    Payouts,
    Refunds,
    Settlements,
    Verifications
  }

  alias Paysafe.Scheduler.{Plans, Subscriptions}
  alias Paysafe.Webhooks

  # ── Payments API ─────────────────────────────────────────────────────────────

  @doc """
  Create a payment handle (tokenize a payment instrument).

  See `Paysafe.Payments.PaymentHandles.create/3` for full parameter docs.
  """
  defdelegate create_payment_handle(config, params, opts \\ []), to: PaymentHandles, as: :create

  @doc """
  Retrieve a payment handle by ID.
  """
  defdelegate get_payment_handle(config, handle_id, opts \\ []), to: PaymentHandles, as: :get

  @doc """
  Create a payment using a `paymentHandleToken`.

  See `Paysafe.Payments.Payments.create/3` for full parameter docs.
  """
  defdelegate create_payment(config, params, opts \\ []), to: Payments, as: :create

  @doc """
  Retrieve a payment by ID.
  """
  defdelegate get_payment(config, payment_id, opts \\ []), to: Payments, as: :get

  @doc """
  List payments with optional filters.
  """
  defdelegate list_payments(config, opts \\ []), to: Payments, as: :list

  @doc """
  Cancel an authorized (pre-settlement) payment.
  """
  defdelegate cancel_payment(config, payment_id, opts \\ []), to: Payments, as: :cancel

  @doc """
  Create a settlement for an authorized payment.
  """
  defdelegate create_settlement(config, payment_id, params, opts \\ []),
    to: Settlements,
    as: :create

  @doc """
  Cancel a pending settlement.
  """
  defdelegate cancel_settlement(config, settlement_id, opts \\ []),
    to: Settlements,
    as: :cancel

  @doc """
  Issue a refund for a completed payment or settlement.

  Pass the settlement ID (or the payment ID, if `settle_with_auth` was
  `true` on the original payment).
  """
  defdelegate create_refund(config, settlement_id, params, opts \\ []), to: Refunds, as: :create

  @doc """
  Cancel a pending refund.
  """
  defdelegate cancel_refund(config, refund_id, opts \\ []), to: Refunds, as: :cancel

  @doc """
  Create a standalone credit (payout for non-iGaming merchants).
  """
  defdelegate standalone_credit(config, params, opts \\ []), to: Payouts, as: :standalone_credit

  @doc """
  Create an original credit (payout for iGaming merchants).
  """
  defdelegate original_credit(config, params, opts \\ []), to: Payouts, as: :original_credit

  @doc """
  Create a card verification (zero-value auth check).
  """
  defdelegate create_verification(config, params, opts \\ []), to: Verifications, as: :create

  @doc """
  Create a customer profile in the vault.
  """
  defdelegate create_customer(config, params, opts \\ []), to: Customers, as: :create

  @doc """
  Retrieve a customer profile.
  """
  defdelegate get_customer(config, customer_id, opts \\ []), to: Customers, as: :get

  @doc """
  Retrieve a customer profile by your own `merchantCustomerId`.
  """
  defdelegate get_customer_by_merchant_customer_id(config, merchant_customer_id, opts \\ []),
    to: Customers,
    as: :get_by_merchant_customer_id

  @doc """
  Update a customer profile.
  """
  defdelegate update_customer(config, customer_id, params, opts \\ []),
    to: Customers,
    as: :update

  @doc """
  Delete a customer profile.
  """
  defdelegate delete_customer(config, customer_id, opts \\ []), to: Customers, as: :delete

  @doc """
  Create a multi-use payment handle (saved instrument) for a customer.
  """
  defdelegate create_customer_payment_handle(config, customer_id, params, opts \\ []),
    to: Customers,
    as: :create_payment_handle

  @doc """
  List saved payment handles for a customer.
  """
  defdelegate list_customer_payment_handles(config, customer_id, opts \\ []),
    to: Customers,
    as: :list_payment_handles

  @doc """
  Create a single-use customer token (SUCT), tokenizing a customer's entire
  saved profile (cards, addresses, bank mandates) for 900 seconds.
  """
  defdelegate create_single_use_customer_token(config, customer_id, opts \\ []),
    to: Customers,
    as: :create_single_use_customer_token

  # ── Scheduler API ─────────────────────────────────────────────────────────────

  @doc """
  Create a recurring billing plan.

  See `Paysafe.Scheduler.Plans.create/3` for full parameter docs.
  """
  defdelegate create_plan(config, params, opts \\ []), to: Plans, as: :create

  @doc """
  Retrieve a billing plan by ID.
  """
  defdelegate get_plan(config, plan_id, opts \\ []), to: Plans, as: :get

  @doc """
  List all billing plans.
  """
  defdelegate list_plans(config, opts \\ []), to: Plans, as: :list

  @doc """
  Update a billing plan.
  """
  defdelegate update_plan(config, plan_id, params, opts \\ []), to: Plans, as: :update

  @doc """
  Delete (discontinue) a billing plan.
  """
  defdelegate delete_plan(config, plan_id, opts \\ []), to: Plans, as: :delete

  @doc """
  Create a subscription for a customer under a plan.

  See `Paysafe.Scheduler.Subscriptions.create/3` for full parameter docs.
  """
  defdelegate create_subscription(config, params, opts \\ []), to: Subscriptions, as: :create

  @doc """
  Retrieve a subscription by ID.
  """
  defdelegate get_subscription(config, subscription_id, opts \\ []),
    to: Subscriptions,
    as: :get

  @doc """
  List subscriptions.
  """
  defdelegate list_subscriptions(config, opts \\ []), to: Subscriptions, as: :list

  @doc """
  Update a subscription.
  """
  defdelegate update_subscription(config, subscription_id, params, opts \\ []),
    to: Subscriptions,
    as: :update

  @doc """
  Cancel a subscription.
  """
  defdelegate cancel_subscription(config, subscription_id, opts \\ []),
    to: Subscriptions,
    as: :cancel

  @doc """
  Suspend a subscription (billing paused; can be re-activated).
  """
  defdelegate suspend_subscription(config, subscription_id, opts \\ []),
    to: Subscriptions,
    as: :suspend

  @doc """
  Re-activate a suspended subscription.
  """
  defdelegate reactivate_subscription(config, subscription_id, opts \\ []),
    to: Subscriptions,
    as: :reactivate

  # ── Webhooks ─────────────────────────────────────────────────────────────────

  @doc """
  Verify the HMAC-SHA256 signature and parse a webhook payload.

  See `Paysafe.Webhooks.verify_and_parse/3` for full docs.
  """
  defdelegate verify_webhook(raw_body, signature, hmac_key), to: Webhooks, as: :verify_and_parse

  @doc """
  Parse a verified webhook body.
  """
  defdelegate parse_webhook(raw_body), to: Webhooks, as: :parse

  # ── Convenience helpers ────────────────────────────────────────────────────

  @doc """
  Check if a `PaymentHandle` requires a customer redirect.

  Returns `{:redirect, url}` or `:proceed`.

  ## Example

      {:ok, handle} = Paysafe.create_payment_handle(config, params)
      case Paysafe.handle_action(handle) do
        {:redirect, url} -> redirect(conn, url)
        :proceed -> create_payment(config, payment_params)
      end
  """
  @spec handle_action(Paysafe.Types.PaymentHandle.t()) :: {:redirect, String.t()} | :proceed
  def handle_action(%Paysafe.Types.PaymentHandle{action: :redirect, links: links}) do
    redirect_url =
      Enum.find_value(links, fn
        %{"rel" => "payment_redirect", "href" => href} -> href
        _ -> nil
      end)

    {:redirect, redirect_url}
  end

  def handle_action(%Paysafe.Types.PaymentHandle{action: :none}), do: :proceed
  def handle_action(%Paysafe.Types.PaymentHandle{}), do: :proceed

  @doc """
  Returns the currently configured package version.
  """
  @spec version() :: String.t()
  def version, do: Application.spec(:paysafe, :vsn) |> to_string()
end