Skip to main content

lib/solaris/onboarding/kyc.ex

defmodule Solaris.Onboarding.KYC do
  @moduledoc """
  KYC identification flows for persons and businesses.

  Supports: VideoIdent (IDnow), Bankident, AutoIdent (re-KYC), Fourthline.

  ## Supported Identification Methods

  - `"idnow"` — Video identification via IDnow
  - `"bank_ident"` — Identification via existing bank account
  - `"fourthline"` — Fourthline mobile SDK identification
  - `"auto_ident"` — Automated re-identification

  ## Identification Flow

  1. Create an identification session → get `url` to redirect user
  2. User completes identification on external provider
  3. Solaris sends `IDENTIFICATION` webhook on completion
  4. Poll or handle webhook for final status

  ## Signing Flow

  Some products require the person to digitally sign documents:
  1. Create a signing via the appropriate identification method
  2. User completes the signing
  3. Receive `SIGNING_COMPLETED` webhook
  """

  alias Solaris.{Client, Error}

  # ── Person Identifications ─────────────────────────────────────────────────

  @doc """
  Creates a new identification session for a person.

  Returns a URL to redirect the user to complete the identification.

  ## Examples

      {:ok, identification} = Solaris.Onboarding.KYC.create_person_identification("cper_123", %{
        method: "idnow",
        language: "de"
      })

      redirect_to(identification["url"])
  """
  @spec create_person_identification(String.t(), map(), keyword()) ::
          {:ok, map()} | {:error, Error.t()}
  def create_person_identification(person_id, attrs, opts \\ []) do
    Client.post("/v1/persons/#{person_id}/identifications", attrs, opts)
  end

  @doc "Lists all identification sessions for a person."
  @spec list_person_identifications(String.t(), keyword()) ::
          {:ok, list(map())} | {:error, Error.t()}
  def list_person_identifications(person_id, opts \\ []) do
    Client.get("/v1/persons/#{person_id}/identifications", opts)
  end

  @doc "Retrieves a specific identification session."
  @spec get_person_identification(String.t(), String.t(), keyword()) ::
          {:ok, map()} | {:error, Error.t()}
  def get_person_identification(person_id, identification_id, opts \\ []) do
    Client.get("/v1/persons/#{person_id}/identifications/#{identification_id}", opts)
  end

  # ── Person Signings ────────────────────────────────────────────────────────

  @doc """
  Initiates a document signing for a person.

  ## Examples

      {:ok, signing} = Solaris.Onboarding.KYC.create_person_signing("cper_123", %{
        document_id: "cdoc_456",
        method: "idnow"
      })
  """
  @spec create_person_signing(String.t(), map(), keyword()) ::
          {:ok, map()} | {:error, Error.t()}
  def create_person_signing(person_id, attrs, opts \\ []) do
    Client.post("/v1/persons/#{person_id}/signings", attrs, opts)
  end

  @doc "Lists all signings for a person."
  @spec list_person_signings(String.t(), keyword()) ::
          {:ok, list(map())} | {:error, Error.t()}
  def list_person_signings(person_id, opts \\ []) do
    Client.get("/v1/persons/#{person_id}/signings", opts)
  end

  @doc "Retrieves a specific signing."
  @spec get_person_signing(String.t(), String.t(), keyword()) ::
          {:ok, map()} | {:error, Error.t()}
  def get_person_signing(person_id, signing_id, opts \\ []) do
    Client.get("/v1/persons/#{person_id}/signings/#{signing_id}", opts)
  end

  # ── Business Identifications ────────────────────────────────────────────────

  @doc "Creates a business identification session."
  @spec create_business_identification(String.t(), map(), keyword()) ::
          {:ok, map()} | {:error, Error.t()}
  def create_business_identification(business_id, attrs, opts \\ []) do
    Client.post("/v1/businesses/#{business_id}/identifications", attrs, opts)
  end

  @doc "Lists all identification sessions for a business."
  @spec list_business_identifications(String.t(), keyword()) ::
          {:ok, list(map())} | {:error, Error.t()}
  def list_business_identifications(business_id, opts \\ []) do
    Client.get("/v1/businesses/#{business_id}/identifications", opts)
  end

  @doc "Retrieves a specific business identification session."
  @spec get_business_identification(String.t(), String.t(), keyword()) ::
          {:ok, map()} | {:error, Error.t()}
  def get_business_identification(business_id, identification_id, opts \\ []) do
    Client.get("/v1/businesses/#{business_id}/identifications/#{identification_id}", opts)
  end

  # ── Business Screener ───────────────────────────────────────────────────────

  @doc "Lists screener hits for a business (AML/sanctions results)."
  @spec list_screener_hits(String.t(), keyword()) :: {:ok, list(map())} | {:error, Error.t()}
  def list_screener_hits(business_id, opts \\ []) do
    Client.get("/v1/businesses/#{business_id}/screener_hits", opts)
  end

  @doc "Retrieves a specific screener hit."
  @spec get_screener_hit(String.t(), String.t(), keyword()) ::
          {:ok, map()} | {:error, Error.t()}
  def get_screener_hit(business_id, hit_id, opts \\ []) do
    Client.get("/v1/businesses/#{business_id}/screener_hits/#{hit_id}", opts)
  end

  @doc "Acknowledges/resolves a screener hit."
  @spec resolve_screener_hit(String.t(), String.t(), map(), keyword()) ::
          {:ok, map()} | {:error, Error.t()}
  def resolve_screener_hit(business_id, hit_id, attrs, opts \\ []) do
    Client.patch("/v1/businesses/#{business_id}/screener_hits/#{hit_id}", attrs, opts)
  end

  # ── IDnow Sandbox Helpers ──────────────────────────────────────────────────

  @doc """
  **Sandbox only.** Runs through the identification process using a robot.

  Use `X-AUTOTEST-{robot_name}` as the person's first or last name.
  """
  @spec sandbox_identify_with_robot(String.t(), keyword()) ::
          {:ok, map()} | {:error, Error.t()}
  def sandbox_identify_with_robot(robot_name, opts \\ []) do
    guard_sandbox!()

    Client.get(
      "/idnow/identify_with_robot",
      Keyword.put(opts, :query, robot: robot_name)
    )
  end

  @doc """
  **Sandbox only.** Runs through the signing process using a robot.
  """
  @spec sandbox_sign_with_robot(String.t(), keyword()) ::
          {:ok, map()} | {:error, Error.t()}
  def sandbox_sign_with_robot(robot_name, opts \\ []) do
    guard_sandbox!()

    Client.get(
      "/idnow/sign_with_robot",
      Keyword.put(opts, :query, robot: robot_name)
    )
  end

  # ── Private ────────────────────────────────────────────────────────────────

  defp guard_sandbox! do
    if Solaris.Config.environment() == :production do
      raise ArgumentError, "This endpoint is only available in the Sandbox environment"
    end
  end
end