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