Skip to main content

lib/railsr.ex

defmodule Railsr do
  @moduledoc """
  Production-grade Elixir client for the **Railsr Embedded Finance API**.

  ## Overview

  `railsr` provides a complete, idiomatic Elixir interface to every Railsr v1/v2
  endpoint group: Endusers, Ledgers, Transactions, Beneficiaries, Cards, Direct
  Debit / Mandates, Compliance Firewall, Webhooks, and Customer management.

  Key design goals:

  - **Zero external secrets in code** — all configuration via `Application.get_env/3`
    with `nimble_options` validation at startup.
  - **Automatic OAuth 2.0 token management** — tokens are fetched once, cached in
    an ETS-backed `GenServer`, and silently refreshed before expiry.
  - **Full-jitter exponential backoff** on transient failures (429, 5xx).
  - **Idempotency keys** generated automatically on all mutating requests.
  - **Telemetry** — every HTTP call emits `[:railsr, :request, :start/stop/exception]`
    events compatible with `Telemetry.Metrics`.
  - **Typed returns** — all public functions return `{:ok, struct}` or
    `{:error, %Railsr.Error{}}`.

  ## Installation

  ```elixir
  def deps do
    [{:railsr, "~> 0.1"}]
  end
  ```

  ## Configuration

  ```elixir
  # config/config.exs
  config :railsr,
    client_id: System.get_env("RAILSR_CLIENT_ID"),
    client_secret: System.get_env("RAILSR_CLIENT_SECRET"),
    environment: :live,          # :play | :play_live | :live
    timeout: 30_000,             # ms
    max_retries: 3,
    base_backoff_ms: 200,
    telemetry_prefix: [:railsr]
  ```

  ## Quick Start

  ```elixir
  {:ok, enduser} = Railsr.Endusers.create(%{
    person: %{
      name: %{family_name: "Smith", given_name: "Alice"},
      email: "alice@example.com",
      date_of_birth: "1990-01-15",
      nationality: "GB",
      country_of_residence: ["GB"],
      address: %{
        address_number: "14",
        address_street: "High Street",
        address_city: "London",
        address_postal_code: "EC1A 1BB",
        address_iso_country: "GB"
      }
    }
  })

  {:ok, _kyc_check} = Railsr.Endusers.create_kyc_check(enduser.enduser_id)

  {:ok, ledger} = Railsr.Ledgers.create(%{
    holder_id: enduser.enduser_id,
    ledger_type: "standard-gbp",
    asset_class: "currency",
    asset_type: "gbp"
  })

  {:ok, _tx} = Railsr.Transactions.send_money(%{
    ledger_id: ledger.ledger_id,
    beneficiary_id: "ben_xxxxx",
    amount: 1000,
    currency: "GBP",
    payment_type: "faster-payment",
    reason: "Invoice #42"
  })
  ```
  """

  alias Railsr.Error
  alias Railsr.Resources.Cards
  alias Railsr.Resources.Endusers
  alias Railsr.Resources.Ledgers
  alias Railsr.Resources.Transactions

  # Delegate convenience functions to resource modules so callers can use
  # either `Railsr.Resources.Endusers.create/1` or `Railsr.create_enduser/1`.
  # Note: defdelegate does not support default arguments; list_endusers uses
  # an explicit wrapper below.

  defdelegate create_enduser(params), to: Endusers, as: :create
  defdelegate get_enduser(id), to: Endusers, as: :get

  @doc "List endusers — delegates to `Railsr.Resources.Endusers.list/2`."
  @spec list_endusers(map(), keyword()) :: {:ok, list()} | {:error, Error.t()}
  def list_endusers(query \\ %{}, opts \\ []) do
    Endusers.list(query, opts)
  end

  defdelegate create_ledger(params), to: Ledgers, as: :create
  defdelegate get_ledger(id), to: Ledgers, as: :get

  defdelegate send_money(params), to: Transactions, as: :send_money
  defdelegate get_transaction(id), to: Transactions, as: :get

  defdelegate create_card(params), to: Cards, as: :create
  defdelegate get_card(id), to: Cards, as: :get

  @doc "Returns the current package version."
  @spec version() :: String.t()
  def version, do: to_string(Application.spec(:railsr, :vsn) || "0.1.0")
end