AuroraMeter.Schema.CreditLot (Aurora Meter v1.0.0)

View Source

One grant's worth of credit, with the five buckets its value can be in.

A lot is created by a grant and never merged with another, so "which grant paid for this" and "which payment funded this grant" both have an answer. The five quantities always add up to amount, and the database refuses a row where they do not:

  • available: spendable now, unless the lot is past expires_at
  • reserved: held by a pending hold, and spoken for
  • consumed: spent by a debit, a settlement or a repayment of debt
  • reversed: taken back by a refund or a chargeback
  • expired: destroyed by expiry, and never spendable again

state is a total function of the quantities (reversed when reversed = amount, else expired when nothing is available or reserved and something expired, else exhausted when nothing is available or reserved, else open), and a CHECK constraint keeps every writer honest about it.

Ordering

seq is the order, not granted_at and not inserted_at. The clock that produces a timestamp is not monotonic and steps backwards on an NTP correction, a leap second or a VM pause; a Postgres identity column cannot. granted_at is for display and reporting.

Spend order

Promotional before paid (adjustment sorts with paid), then the earliest non-null expires_at, then the oldest grant, then seq. Non-expiring lots sort last within their category. The key is total, so the order a query happens to return lots in cannot change what a debit spends.

Rows are written only by AuroraMeter.Credits, inside one transaction, under the wallet's balance row lock.

Summary

Functions

The lot categories.

Builds a changeset for a lot.

The five buckets a lot's value can be in, in the order the conservation check adds them up.

The state a lot with these quantities must be in.

The lot states.

Types

category()

@type category() :: :paid | :promotional | :adjustment

state()

@type state() :: :open | :exhausted | :expired | :reversed

t()

@type t() :: %AuroraMeter.Schema.CreditLot{
  __meta__: term(),
  amount: term(),
  available: term(),
  category: term(),
  consumed: term(),
  expired: term(),
  expires_at: term(),
  grant_transaction_id: term(),
  granted_at: term(),
  id: term(),
  inserted_at: term(),
  reference: term(),
  reserved: term(),
  reversed: term(),
  seq: term(),
  source: term(),
  state: term(),
  tenant_key: term(),
  updated_at: term()
}

Functions

categories()

@spec categories() :: [category()]

The lot categories.

Examples

iex> AuroraMeter.Schema.CreditLot.categories()
[:paid, :promotional, :adjustment]

changeset(lot, attrs)

@spec changeset(t(), map()) :: Ecto.Changeset.t()

Builds a changeset for a lot.

The validations mirror every CHECK constraint the table carries, so a bug in a writer surfaces as a changeset error in a test and as a constraint error in production, and neither can be reached without the other firing.

quantities()

@spec quantities() :: [atom()]

The five buckets a lot's value can be in, in the order the conservation check adds them up.

Examples

iex> AuroraMeter.Schema.CreditLot.quantities()
[:available, :reserved, :consumed, :reversed, :expired]

state_for(arg1)

@spec state_for(map()) :: state()

The state a lot with these quantities must be in.

It is a function of the quantities alone, so it is order free: a writer cannot leave a stale state behind, and two writers that computed it at different moments cannot disagree. The database holds the same expression as a CHECK constraint.

Examples

iex> AuroraMeter.Schema.CreditLot.state_for(%{amount: 10, available: 10, reserved: 0, consumed: 0, reversed: 0, expired: 0})
:open

iex> AuroraMeter.Schema.CreditLot.state_for(%{amount: 10, available: 0, reserved: 0, consumed: 10, reversed: 0, expired: 0})
:exhausted

iex> AuroraMeter.Schema.CreditLot.state_for(%{amount: 10, available: 0, reserved: 0, consumed: 4, reversed: 0, expired: 6})
:expired

iex> AuroraMeter.Schema.CreditLot.state_for(%{amount: 10, available: 0, reserved: 0, consumed: 0, reversed: 10, expired: 0})
:reversed

states()

@spec states() :: [state()]

The lot states.

Examples

iex> :exhausted in AuroraMeter.Schema.CreditLot.states()
true