Add user credit balance to absorb settlement overpay/round mismatch (prerequisite for #33) #41
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What
Libra has no account type for "the user paid more than they currently owe." Today this manifests as:
/receivables/settlefor that amount, and either: (a) the books drift quietly out of sync with reality (overpay just disappears into an unbalanced ledger), or (b) the proposed strict-matching fix in #33 returns 400, leaving the operator holding cash with no clean way to record it.A per-user
Liabilities:Credit:User-{id}account (or equivalent — naming TBD) gives libra a place to record "we owe this user X EUR going forward, against future obligations or refundable."Why this is a prerequisite for #33
#33's clean fix requires the settlement endpoint to validate that cash matches what's being settled. Without a credit balance, any mismatch on the cash side has no honest accounting home — the only options are reject (400, operator stuck) or silently drift (which is what's happening today and is exactly what #33 was filed about). Credit balance is the third option: settle what you can, absorb the rest as a recorded liability.
Established pattern (this is not novel)
AR/AP netting and customer credit are both textbook accounting patterns with strong precedent:
So
Liabilities:Credit:User-{id_short}is a direct application of the Beancount-recommended pattern with libra's existing per-user naming convention. No invention required.Concrete behaviour the credit account enables
Nancy's #33 scenario, expanded:
Liabilities:Credit:User-X.rcv-50, rcv-30, rcv-20)settled_entry_links=[rcv-50]. 2-leg settles that entry exactly. Other two stay open. No credit involved.rcv-50, rcv-30, rcv-20)[rcv-50, rcv-30](sum 80). 2-leg settles those two + 10 EUR moves to credit.rcv-20stays open.Sketch of the data model
LIABILITYwith a structured name):Liabilities:Credit:User-{id_short}auto-created on first use, like Payable and Receivable.Display contract — applies to BOTH libra extension and webapp
This is the load-bearing surface for users, so the credit account is only meaningful if it flows into the displayed per-user balance:
GET /libra/api/v1/balance(and/balances/all) returns a per-user net that already accounts for credit:net_fiat[CCY] = sum(Receivable[X]) - sum(Payable[X])net_fiat[CCY] = sum(Receivable[X]) - sum(Payable[X]) - sum(Credit[X])accounts: [...]array) gains a credit row when non-zero so the UI can render it as a distinct line item ("you have 40 EUR credit on file").total_expenses_fiatandtotal_income_fiatare not affected — those track originally-entered lifetime totals per the existingmodels.py:93convention. Credit is orthogonal: it's a settlement artefact, not an original entry.UI surfaces that read this and need to render credit appropriately
/balanceand/balances/all. Showing the credit-inclusive net keeps the existing "what does this user owe (or get back)" semantics correct. Per-account breakdown lets cards optionally surface "(includes 40 EUR credit)" subtext.aiolabs/webapp—src/modules/expenses/services/ExpensesAPI.ts,TransactionsPage.vue, the Hub balance display) — same endpoint, same net. Webapp may want a separate "Your credit: X EUR" pill or section if the breakdown is in the response; that's an incremental UX task on webapp, not blocking #41.The minimum bar for #41 to ship is: the displayed net everywhere already accounts for credit, so neither UI ever shows a stale number while credit is on the books. Surfacing credit as a distinct line is incremental polish on top.
Out of scope for this issue (file separately when needed)
/payables/pay.Acceptance
Liabilities:Credit:User-{id}(final name TBD) gets auto-created on first overflow.POST /receivables/settlewith cash > settle_target writes the overflow leg.GET /balanceand/balances/allper-user net subtracts credit so the displayed balance is honest (positive → user owes net, negative → libra owes net including any credit).Sequencing with #33
Best done together as a single PR — #33's fix is what makes the "cash != settle_target" path land somewhere, and the credit account is what makes it land somewhere correct. Test file
tests/test_settlement_api.py(task #6 of the test plan) will exercise both together once they ship.