Skip to main content

lib/solaris/lending/consumer_loans.ex

defmodule Solaris.Lending.ConsumerLoans do
  @moduledoc """
  Consumer loan application and origination flow.

  ## Application Flow

  ```
  1. (Optional) Get anonymous offer to show preliminary rate
  2. Create application → triggers credit scoring
  3. (Optional) Link account snapshot for final credit check
  4. (Optional) Upload purchase contract for financed goods
  5. Review offers → download SECCI pre-contract PDF (required by law)
  6. (Optional) Subsidize an offer to reduce interest rate
  7. Retrieve final contract PDF → present to customer for e-signing
  8. PUT consumer_loan → creates loan and initiates payout
  ```

  ## Legal Requirements

  Per EU Consumer Credit Directive, the SECCI pre-contract document **must**
  be shown to the customer **before** they sign. Always call `get_secci/3`
  and present it before proceeding to contract signing.

  ## Webhook

  Monitor `CONSUMER_LOAN_APPLICATION` for status transitions:
  - `OFFERED` — offers available for review
  - `ACCOUNT_SNAPSHOT_REQUIRED` — snapshot needed for final scoring
  - `REJECTED` — application declined
  - `LOAN_CREATED` — loan issued, payout initiated
  """

  alias Solaris.{Client, Error}

  @base "/v1/persons"

  # ── Anonymous Offer ────────────────────────────────────────────────────────

  @doc """
  Returns a non-binding loan offer based on self-declared financial info.

  No person record required — useful for pre-qualification UX.

  ## Examples

      {:ok, offer} = Solaris.Lending.ConsumerLoans.get_anonymous_offer(%{
        amount: 5_000_00,
        currency: "EUR",
        term_months: 24,
        monthly_income_cents: 300_000,
        employment_status: "EMPLOYEE"
      })
  """
  @spec get_anonymous_offer(map(), keyword()) :: {:ok, map()} | {:error, Error.t()}
  def get_anonymous_offer(attrs, opts \\ []) do
    Client.post("/v1/anonymous_consumer_loan_offers", attrs, opts)
  end

  # ── Two-Thirds Rates ───────────────────────────────────────────────────────

  @doc """
  Returns representative 2/3 interest rates per loan term (regulatory disclosure).

  ## Examples

      {:ok, rates} = Solaris.Lending.ConsumerLoans.get_two_thirds_rates()
  """
  @spec get_two_thirds_rates(keyword()) :: {:ok, map()} | {:error, Error.t()}
  def get_two_thirds_rates(opts \\ []) do
    Client.get("/v1/consumer_loans/two_thirds_rates", opts)
  end

  # ── Application Lifecycle ──────────────────────────────────────────────────

  @doc """
  Creates a consumer loan application for a person.

  Triggers credit scoring. Monitor `CONSUMER_LOAN_APPLICATION` webhook
  for status transitions.

  ## Required fields

  - `:amount` — loan amount in cents
  - `:currency` — e.g. `"EUR"`
  - `:term_months` — loan term in months
  - `:purpose` — loan purpose code

  ## Examples

      {:ok, application} = Solaris.Lending.ConsumerLoans.create_application("cper_123", %{
        amount: 10_000_00,
        currency: "EUR",
        term_months: 36,
        purpose: "CONSUMER_GOODS"
      })
  """
  @spec create_application(String.t(), map(), keyword()) ::
          {:ok, map()} | {:error, Error.t()}
  def create_application(person_id, attrs, opts \\ []) do
    Client.post("#{@base}/#{person_id}/consumer_loan_applications", attrs, opts)
  end

  @doc "Retrieves a consumer loan application."
  @spec get_application(String.t(), String.t(), keyword()) ::
          {:ok, map()} | {:error, Error.t()}
  def get_application(person_id, application_id, opts \\ []) do
    Client.get("#{@base}/#{person_id}/consumer_loan_applications/#{application_id}", opts)
  end

  @doc """
  Skips the account snapshot step in the application.

  Use when the customer declines to share account data.
  """
  @spec skip_account_snapshot(String.t(), String.t(), keyword()) ::
          {:ok, map()} | {:error, Error.t()}
  def skip_account_snapshot(person_id, application_id, opts \\ []) do
    Client.put(
      "#{@base}/#{person_id}/consumer_loan_applications/#{application_id}/skip_account_snapshot",
      %{},
      opts
    )
  end

  @doc """
  Links an account snapshot to the application for final credit scoring.

  `snapshot_id` comes from `Solaris.Lending.AccountSnapshots`.
  """
  @spec link_account_snapshot(String.t(), String.t(), String.t(), keyword()) ::
          {:ok, map()} | {:error, Error.t()}
  def link_account_snapshot(person_id, application_id, snapshot_id, opts \\ []) do
    Client.put(
      "#{@base}/#{person_id}/consumer_loan_applications/#{application_id}/account_snapshot",
      %{account_snapshot_id: snapshot_id},
      opts
    )
  end

  @doc """
  Uploads a purchase contract for a financed good (e.g. hire-purchase).
  """
  @spec upload_purchase_contract(String.t(), String.t(), binary(), keyword()) ::
          {:ok, map()} | {:error, Error.t()}
  def upload_purchase_contract(person_id, application_id, file_data, opts \\ []) do
    url =
      "#{@base}/#{person_id}/consumer_loan_applications" <>
        "/#{application_id}/upload_purchase_contract"

    upload_opts =
      Keyword.merge(opts,
        filename: Keyword.get(opts, :filename, "contract.pdf"),
        content_type: "application/pdf"
      )

    Client.upload(url, file_data, upload_opts)
  end

  # ── Offers ─────────────────────────────────────────────────────────────────

  @doc """
  Subsidizes a loan offer to reduce the interest rate.

  Partners can subsidize offers to make them more attractive,
  taking on part of the interest cost.
  """
  @spec subsidize_offer(String.t(), String.t(), String.t(), map(), keyword()) ::
          {:ok, map()} | {:error, Error.t()}
  def subsidize_offer(person_id, application_id, offer_id, attrs, opts \\ []) do
    url =
      "#{@base}/#{person_id}/consumer_loan_applications" <>
        "/#{application_id}/offers/#{offer_id}/subsidize"

    Client.post(url, attrs, opts)
  end

  @doc """
  Downloads the SECCI pre-contract PDF for a loan offer.

  **Required by EU Consumer Credit Directive** — must be shown to the
  customer before they sign the loan contract.

  ## Examples

      {:ok, pdf_binary} = Solaris.Lending.ConsumerLoans.get_secci(
        "cper_123",
        "capp_456",
        "coffer_789"
      )

      File.write!("secci.pdf", pdf_binary)
  """
  @spec get_secci(String.t(), String.t(), String.t(), keyword()) ::
          {:ok, binary()} | {:error, Error.t()}
  def get_secci(person_id, application_id, offer_id, opts \\ []) do
    url =
      "#{@base}/#{person_id}/consumer_loan_applications" <>
        "/#{application_id}/offers/#{offer_id}/pre_contract"

    Client.get(url, opts)
  end

  @doc """
  Downloads the final loan contract PDF for e-signing.

  Only available after SECCI has been presented.
  """
  @spec get_contract(String.t(), String.t(), String.t(), keyword()) ::
          {:ok, binary()} | {:error, Error.t()}
  def get_contract(person_id, application_id, offer_id, opts \\ []) do
    url =
      "#{@base}/#{person_id}/consumer_loan_applications" <>
        "/#{application_id}/offers/#{offer_id}/contract"

    Client.get(url, opts)
  end

  # ── Loan Creation ──────────────────────────────────────────────────────────

  @doc """
  Creates the loan and initiates payout.

  This is the final step — after the customer has reviewed the SECCI
  and signed the contract. Sets application status to `loan_created`.

  ## Examples

      {:ok, loan} = Solaris.Lending.ConsumerLoans.create_loan(
        "cper_123",
        "capp_456",
        %{offer_id: "coffer_789", signing_id: "csign_012"}
      )
  """
  @spec create_loan(String.t(), String.t(), map(), keyword()) ::
          {:ok, map()} | {:error, Error.t()}
  def create_loan(person_id, application_id, attrs, opts \\ []) do
    Client.put(
      "#{@base}/#{person_id}/consumer_loan_applications/#{application_id}/consumer_loan",
      attrs,
      opts
    )
  end
end