Allow income receipt to settle another user's receivable (debt assumption) #16

Open
opened 2026-05-16 21:44:32 +00:00 by padreug · 1 comment
Owner

Summary

Today, submitting income creates a receivable on the submitter: DR Assets:Receivable:User-B / CR Income:X. But a common real-world case is that the money User B physically receives is being paid by User A to settle User A's existing debt to the entity. In that case the correct effect is:

  • User A's Assets:Receivable:User-A decreases (their debt is paid)
  • User B's Assets:Receivable:User-B does not increase (B isn't holding entity cash — they handed it on, or already passed it to the entity)
  • Income (if it's also a revenue event) is unaffected by this settlement leg

Right now this requires two manual entries (an income entry on B, then a manual receivable settlement) and there's no link between them. We should let an income receipt optionally indicate that it's settling some other user's outstanding receivable.

Possible Shape

Add an optional settles_user_id (or settles_receivable_links: list[str]) to IncomeEntry. When set:

  • Skip creating the receivable on the submitter (User B).
  • DR Assets:Cash/Bank/Lightning directly (a new required field, or a settings-default), CR Income:X.
  • Additionally, create a settlement entry against User A's receivable (CR Assets:Receivable:User-A, DR same asset account) — or wire it through the existing /settle-receivable path so the settlement is linked back to the original receivable entry.

Alternatively model it as "User B assumes User A's debt" — receivable transferred between users — but that's harder to reason about and less common.

Out of scope (handled separately)

  • The basic income → submitter-receivable model is already in #13.
  • Generic receivable settlement (cash/bank/lightning) already exists at POST /api/v1/settle-receivable.

Why this is non-trivial

  • Authorization: User B can't unilaterally claim they're paying down User A's debt; this probably needs admin approval (! flag, super-user review) or an out-of-band confirmation.
  • Audit: the link between the two users' entries needs to be queryable from Fava (likely via shared ^settle-{id} link).
  • UI: the income form would need an optional "settles another user's debt" branch with a target-user picker.

Discovered while wiring up #9 — the simple model is correct for most cases; this is the next refinement once that lands.

## Summary Today, submitting income creates a receivable on the submitter: DR `Assets:Receivable:User-B` / CR `Income:X`. But a common real-world case is that the money User B physically receives is being paid *by* User A to settle User A's existing debt to the entity. In that case the correct effect is: - User A's `Assets:Receivable:User-A` decreases (their debt is paid) - User B's `Assets:Receivable:User-B` does **not** increase (B isn't holding entity cash — they handed it on, or already passed it to the entity) - Income (if it's also a revenue event) is unaffected by this settlement leg Right now this requires two manual entries (an income entry on B, then a manual receivable settlement) and there's no link between them. We should let an income receipt optionally indicate that it's settling some other user's outstanding receivable. ## Possible Shape Add an optional `settles_user_id` (or `settles_receivable_links: list[str]`) to `IncomeEntry`. When set: - Skip creating the receivable on the submitter (User B). - DR `Assets:Cash`/`Bank`/`Lightning` directly (a new required field, or a settings-default), CR `Income:X`. - Additionally, create a settlement entry against User A's receivable (CR `Assets:Receivable:User-A`, DR same asset account) — or wire it through the existing `/settle-receivable` path so the settlement is linked back to the original receivable entry. Alternatively model it as "User B assumes User A's debt" — receivable transferred between users — but that's harder to reason about and less common. ## Out of scope (handled separately) - The basic income → submitter-receivable model is already in #13. - Generic receivable settlement (cash/bank/lightning) already exists at `POST /api/v1/settle-receivable`. ## Why this is non-trivial - Authorization: User B can't unilaterally claim they're paying down User A's debt; this probably needs admin approval (`!` flag, super-user review) or an out-of-band confirmation. - Audit: the link between the two users' entries needs to be queryable from Fava (likely via shared `^settle-{id}` link). - UI: the income form would need an optional "settles another user's debt" branch with a target-user picker. Discovered while wiring up #9 — the simple model is correct for most cases; this is the next refinement once that lands.
Author
Owner

Open: which unit-of-account convention?

Concrete case from the design discussion:

  • T1 — User A's $200 debt is recorded when 1 BTC = $100k → entry sees 200,000 sats.
  • T2 — User A pays $200 USD when 1 BTC = $80k → that same $200 is now 250,000 sats.

The 50,000 sats difference has to land somewhere. The choice forces a unit-of-account question that's bigger than #16 — every income/expense entry already crosses sats↔fiat at the time of accrual.

What Libra does today (fiat-first, @@ price notation)

2026-01-01 ! "Debt accrual"
  Assets:Receivable:User-A    200.00 USD @@ 200000 SATS
  Income:Sales               -200.00 USD @@ 200000 SATS

The holding is 200.00 USD; SATS is metadata about that day's price, not the unit of account. A later settlement at the same fiat amount nets to zero in USD; the sats difference disappears because each posting carries its own price annotation. Balances stay stable for users ("you owe $200" stays $200), and stakeholders unfamiliar with BTC can still read the books.

What bitcoin-first would look like (cost-basis {} notation)

2026-01-01 ! "Debt accrual"
  Assets:Receivable:User-A    200000 SATS {0.00100000 USD}
  Income:Sales               -200000 SATS {0.00100000 USD}

2026-02-01 * "Settle User A debt"
  Assets:Cash                 250000 SATS @@ 200.00 USD
  Assets:Receivable:User-A   -200000 SATS {} @@ 200.00 USD
  Income:PnL                                                  ; auto-balances 50000 SATS gain

SATS is the unit of account, USD is the cost basis on the lot. When the receivable is reduced, Beancount's PNL plugin routes the 50k sats delta to a configured gain/loss account — same mechanism the canonical Beancount stock-trading docs describe (trading_with_beancount). Community "bitcoin-only" Beancount setups follow this pattern.

Trade-off: balances now move with BTC price. "You hold 200,000 sats of receivable" is stable in sats but reads as a fluctuating USD number to anyone looking at the books.

Hybrid option

Keep day-to-day postings fiat-first (stable for users today), but add Beancount price directives so any reporting date can re-derive a SATS valuation. Doesn't change the unit of account; gives us a reversible path. The fiat-first ledger remains the source of truth; a sats-perspective report becomes a derived view.

Bearing on #16

If the convention stays fiat-first, this issue's cross-user settlement is fiat-clean: User B's $200 income settles User A's $200 receivable, balances net, no FX gain account needed. The BTC volatility is invisible in the ledger but recoverable from price metadata.

If we eventually flip to bitcoin-first, this issue grows: every settlement (cross-user or not) needs gain/loss routing, all existing @@-notated entries need migration to {} cost-basis, and the user-facing UI has to communicate fluctuating sats balances.

Suggestion

The unit-of-account decision is upstream of #16 and probably deserves its own design issue once we have a concrete use case forcing it. For now this issue can proceed assuming the current fiat-first convention — implementing it that way doesn't preclude a later flip, and a flip would touch far more than this one feature anyway.

Open question worth filing separately: "Choose unit-of-account convention: fiat-first vs bitcoin-first (and migration path)". Want me to file that?

## Open: which unit-of-account convention? Concrete case from the design discussion: - **T1** — User A's $200 debt is recorded when 1 BTC = $100k → entry sees 200,000 sats. - **T2** — User A pays $200 USD when 1 BTC = $80k → that same $200 is now 250,000 sats. The 50,000 sats difference has to land somewhere. The choice forces a unit-of-account question that's bigger than #16 — every income/expense entry already crosses sats↔fiat at the time of accrual. ### What Libra does today (fiat-first, `@@` price notation) ```beancount 2026-01-01 ! "Debt accrual" Assets:Receivable:User-A 200.00 USD @@ 200000 SATS Income:Sales -200.00 USD @@ 200000 SATS ``` The holding is **200.00 USD**; SATS is metadata about that day's price, not the unit of account. A later settlement at the same fiat amount nets to zero in USD; the sats difference disappears because each posting carries its own price annotation. Balances stay stable for users ("you owe $200" stays $200), and stakeholders unfamiliar with BTC can still read the books. ### What bitcoin-first would look like (cost-basis `{}` notation) ```beancount 2026-01-01 ! "Debt accrual" Assets:Receivable:User-A 200000 SATS {0.00100000 USD} Income:Sales -200000 SATS {0.00100000 USD} 2026-02-01 * "Settle User A debt" Assets:Cash 250000 SATS @@ 200.00 USD Assets:Receivable:User-A -200000 SATS {} @@ 200.00 USD Income:PnL ; auto-balances 50000 SATS gain ``` SATS is the unit of account, USD is the cost basis on the lot. When the receivable is reduced, Beancount's PNL plugin routes the 50k sats delta to a configured gain/loss account — same mechanism the canonical Beancount stock-trading docs describe ([trading_with_beancount](https://beancount.github.io/docs/trading_with_beancount.html)). Community "bitcoin-only" Beancount setups follow this pattern. Trade-off: balances now move with BTC price. "You hold 200,000 sats of receivable" is stable in sats but reads as a fluctuating USD number to anyone looking at the books. ### Hybrid option Keep day-to-day postings fiat-first (stable for users today), but add Beancount `price` directives so any reporting date can re-derive a SATS valuation. Doesn't change the unit of account; gives us a reversible path. The fiat-first ledger remains the source of truth; a sats-perspective report becomes a derived view. ### Bearing on #16 If the convention stays **fiat-first**, this issue's cross-user settlement is fiat-clean: User B's $200 income settles User A's $200 receivable, balances net, no FX gain account needed. The BTC volatility is invisible in the ledger but recoverable from price metadata. If we eventually flip to **bitcoin-first**, this issue grows: every settlement (cross-user or not) needs gain/loss routing, all existing `@@`-notated entries need migration to `{}` cost-basis, and the user-facing UI has to communicate fluctuating sats balances. ### Suggestion The unit-of-account decision is upstream of #16 and probably deserves its own design issue once we have a concrete use case forcing it. For now this issue can proceed assuming the **current fiat-first convention** — implementing it that way doesn't preclude a later flip, and a flip would touch far more than this one feature anyway. Open question worth filing separately: **"Choose unit-of-account convention: fiat-first vs bitcoin-first (and migration path)"**. Want me to file that?
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
aiolabs/libra#16
No description provided.