Skip to content

The ledger

tilttek/points

Accounts

Every movement goes from one Account to another. The account's type says what it stands for:

TypeNamed byWritten asIts balance
Memberthe member's referencemember:member:44The member's points
Issuancethe rule or the source that issuesissuance:games, issuance:manualNegative by what it gave
Redemptionwhat the points pay forredemption:reward:12What was spent on it
Expiry—expiry:expiredWhat expired
Erased—erased:memberWhat erased members gave or received from others

Account::fromString() reads back what toString() wrote. A name other than a member's is 1 to 255 lower-case letters, digits, _, ., : or -, starting with a letter or a digit.

Movements

A Movement moves 1 to 10^15 points of one currency from its debited account to its credited account, which differ. Besides its key and its kind, it carries:

  • occurredAt, the moment it counts at: the activity's moment for points earned and reversed, the end of a credit's lifetime for points expired, the moment it was asked for otherwise;
  • cause, the idempotency key of the activity that earned or reversed the points;
  • reverses, the key of the movement it undoes — the earning a reversal cancels, the spending a refund gives back — or of the credit it expires.
KindFromToWritten by
Earnedthe rule's issuance accountthe memberPointsProjection
Reversedthe memberthe rule's issuance accountPointsProjection, on a cancellation
Expiredthe memberexpiry:expiredPoints::expire()
Granteda source's issuance accountthe memberPoints::grant()
Revokedthe membera source's issuance accountPoints::revoke()
Spentthe membera redemption accountPoints::spend()
Refundedthe redemption accountthe memberPoints::refund()
Transferreda memberanother memberPoints::transfer()

The first three are derived: they follow from the activity and the balances, and a rebuild recomputes them. The others are decided, and nothing recomputes them.

Keys

A MovementKey makes a movement written once: the same movement written again under its key returns the entry already there, and the same key for another movement — another kind, currency, account or amount — raises MovementConflict. The moment is left out of the comparison, so an operation asked again later is still a duplicate.

The application gives the key of each operation it decides, built from what makes the operation unique — order:2026-0193:redeem, gift:2026-11-07:44:12 — never from the time it runs. The keys the library derives start with #, which the application's keys cannot start with: #earn:<rule>:<activity key>, #reverse:<earning key>, #expire:<credit key>, #refund:<spending key>. A key is 1 to 400 bytes, without whitespace or control characters.

Balances and histories

$points->balance($member);            // in the default currency
$points->balance($member, 'stars');
$points->history($member);            // the 50 first movements, oldest first
$points->history($member, 'stars', after: 120, limit: 100);

A balance is the sum of the account's movements in the currency: what it was credited less what it was debited. A history is read by position, at most 1,000 movements at a time; LedgerEntry::toArray() gives each line as plain values, and Movement::amountFor($account) the signed amount for the member.