chore: hygiene sweep — dead code, role race, user lookup, stale files

One pass over the LOW-tier review items plus two folded issues:

- Delete validate_journal_entry (dead since the Fava migration; it
  validated the pre-string-amount model) with its exports, unused
  crud imports, and tests. Beancount validates entries now.
- Migration m006: UNIQUE index on user_roles(user_id, role_id) after
  deduping; assign_user_role inserts with ON CONFLICT DO NOTHING and
  returns the existing assignment — closes the auto-assign
  check-then-act race on concurrent logins.
- Extract _get_username_from_user_id (110 lines in views_api, fresh
  LNbits Database per call inside per-row hot paths) into
  user_lookup.py with one shared core-DB handle, a 60s TTL cache and
  a batch get_usernames API (review #18).
- Receivable-entry responses report CLEARED, matching the flag the
  formatter actually writes; PENDING misled the UI (libra-#35).
- Replace the remaining print() calls in tasks.py with logger.
- get_all_accounts derives valid roots from
  account_utils.ACCOUNT_TYPE_ROOTS instead of a hardcoded tuple, and
  the no-op per-test rate-limit reset is gone (libra-#54).
- Delete migrations_old.py.bak, MIGRATION_SQUASH_SUMMARY.md,
  docs/PHASE*_COMPLETE.md and the rendered .html; .gitignore data/
  (it holds the runtime .lnbits_auth_key secret).
- Track docs/CODE-REVIEW-2026-06.md with finding statuses updated for
  the PR #55-#59 + chore/hygiene series.
- CLAUDE.md notes LNbits pins Pydantic v1: keep .dict(), don't
  "modernize" to .model_dump().

Note: format_payment_entry's is_payable docstring (flagged in review
follow-up) turned out to be consistent with the body — no change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Padreug 2026-07-12 16:01:04 +02:00
commit ec6cac51f0
19 changed files with 454 additions and 2942 deletions

View file

@ -1,953 +0,0 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" lang="" xml:lang="">
<head>
<meta charset="utf-8" />
<meta name="generator" content="pandoc" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes" />
<title>ACCOUNTING-ANALYSIS-NET-SETTLEMENT</title>
<style>
code{white-space: pre-wrap;}
span.smallcaps{font-variant: small-caps;}
div.columns{display: flex; gap: min(4vw, 1.5em);}
div.column{flex: auto; overflow-x: auto;}
div.hanging-indent{margin-left: 1.5em; text-indent: -1.5em;}
/* The extra [class] is a hack that increases specificity enough to
override a similar rule in reveal.js */
ul.task-list[class]{list-style: none;}
ul.task-list li input[type="checkbox"] {
font-size: inherit;
width: 0.8em;
margin: 0 0.8em 0.2em -1.6em;
vertical-align: middle;
}
.display.math{display: block; text-align: center; margin: 0.5rem auto;}
/* CSS for syntax highlighting */
html { -webkit-text-size-adjust: 100%; }
pre > code.sourceCode { white-space: pre; position: relative; }
pre > code.sourceCode > span { display: inline-block; line-height: 1.25; }
pre > code.sourceCode > span:empty { height: 1.2em; }
.sourceCode { overflow: visible; }
code.sourceCode > span { color: inherit; text-decoration: inherit; }
div.sourceCode { margin: 1em 0; }
pre.sourceCode { margin: 0; }
@media screen {
div.sourceCode { overflow: auto; }
}
@media print {
pre > code.sourceCode { white-space: pre-wrap; }
pre > code.sourceCode > span { text-indent: -5em; padding-left: 5em; }
}
pre.numberSource code
{ counter-reset: source-line 0; }
pre.numberSource code > span
{ position: relative; left: -4em; counter-increment: source-line; }
pre.numberSource code > span > a:first-child::before
{ content: counter(source-line);
position: relative; left: -1em; text-align: right; vertical-align: baseline;
border: none; display: inline-block;
-webkit-touch-callout: none; -webkit-user-select: none;
-khtml-user-select: none; -moz-user-select: none;
-ms-user-select: none; user-select: none;
padding: 0 4px; width: 4em;
color: #aaaaaa;
}
pre.numberSource { margin-left: 3em; border-left: 1px solid #aaaaaa; padding-left: 4px; }
div.sourceCode
{ }
@media screen {
pre > code.sourceCode > span > a:first-child::before { text-decoration: underline; }
}
code span.al { color: #ff0000; font-weight: bold; } /* Alert */
code span.an { color: #60a0b0; font-weight: bold; font-style: italic; } /* Annotation */
code span.at { color: #7d9029; } /* Attribute */
code span.bn { color: #40a070; } /* BaseN */
code span.bu { color: #008000; } /* BuiltIn */
code span.cf { color: #007020; font-weight: bold; } /* ControlFlow */
code span.ch { color: #4070a0; } /* Char */
code span.cn { color: #880000; } /* Constant */
code span.co { color: #60a0b0; font-style: italic; } /* Comment */
code span.cv { color: #60a0b0; font-weight: bold; font-style: italic; } /* CommentVar */
code span.do { color: #ba2121; font-style: italic; } /* Documentation */
code span.dt { color: #902000; } /* DataType */
code span.dv { color: #40a070; } /* DecVal */
code span.er { color: #ff0000; font-weight: bold; } /* Error */
code span.ex { } /* Extension */
code span.fl { color: #40a070; } /* Float */
code span.fu { color: #06287e; } /* Function */
code span.im { color: #008000; font-weight: bold; } /* Import */
code span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Information */
code span.kw { color: #007020; font-weight: bold; } /* Keyword */
code span.op { color: #666666; } /* Operator */
code span.ot { color: #007020; } /* Other */
code span.pp { color: #bc7a00; } /* Preprocessor */
code span.sc { color: #4070a0; } /* SpecialChar */
code span.ss { color: #bb6688; } /* SpecialString */
code span.st { color: #4070a0; } /* String */
code span.va { color: #19177c; } /* Variable */
code span.vs { color: #4070a0; } /* VerbatimString */
code span.wa { color: #60a0b0; font-weight: bold; font-style: italic; } /* Warning */
</style>
<link rel="stylesheet" href="https://latex.now.sh/style.css" />
</head>
<body>
<nav id="TOC" role="doc-toc">
<ul>
<li><a href="#accounting-analysis-net-settlement-entry-pattern"
id="toc-accounting-analysis-net-settlement-entry-pattern">Accounting
Analysis: Net Settlement Entry Pattern</a>
<ul>
<li><a href="#executive-summary" id="toc-executive-summary">Executive
Summary</a></li>
<li><a href="#background-the-technical-challenge"
id="toc-background-the-technical-challenge">Background: The Technical
Challenge</a></li>
<li><a href="#current-implementation"
id="toc-current-implementation">Current Implementation</a>
<ul>
<li><a href="#transaction-example"
id="toc-transaction-example">Transaction Example</a></li>
<li><a href="#code-implementation" id="toc-code-implementation">Code
Implementation</a></li>
</ul></li>
<li><a href="#accounting-issues-identified"
id="toc-accounting-issues-identified">Accounting Issues Identified</a>
<ul>
<li><a href="#issue-1-zero-amount-postings"
id="toc-issue-1-zero-amount-postings">Issue 1: Zero-Amount
Postings</a></li>
<li><a href="#issue-2-redundant-satoshi-tracking"
id="toc-issue-2-redundant-satoshi-tracking">Issue 2: Redundant Satoshi
Tracking</a></li>
<li><a href="#issue-3-no-exchange-gainloss-recognition"
id="toc-issue-3-no-exchange-gainloss-recognition">Issue 3: No Exchange
Gain/Loss Recognition</a></li>
<li><a href="#issue-4-semantic-misuse-of-price-notation"
id="toc-issue-4-semantic-misuse-of-price-notation">Issue 4: Semantic
Misuse of Price Notation</a></li>
<li><a href="#issue-5-misnamed-function-and-incorrect-usage"
id="toc-issue-5-misnamed-function-and-incorrect-usage">Issue 5: Misnamed
Function and Incorrect Usage</a></li>
</ul></li>
<li><a href="#traditional-accounting-approaches"
id="toc-traditional-accounting-approaches">Traditional Accounting
Approaches</a>
<ul>
<li><a
href="#approach-1-record-bitcoin-at-fair-market-value-tax-compliant"
id="toc-approach-1-record-bitcoin-at-fair-market-value-tax-compliant">Approach
1: Record Bitcoin at Fair Market Value (Tax Compliant)</a></li>
<li><a href="#approach-2-simplified-eur-only-ledger-no-sats-positions"
id="toc-approach-2-simplified-eur-only-ledger-no-sats-positions">Approach
2: Simplified EUR-Only Ledger (No SATS Positions)</a></li>
<li><a
href="#approach-3-true-net-settlement-when-both-obligations-exist"
id="toc-approach-3-true-net-settlement-when-both-obligations-exist">Approach
3: True Net Settlement (When Both Obligations Exist)</a></li>
</ul></li>
<li><a href="#recommendations"
id="toc-recommendations">Recommendations</a>
<ul>
<li><a href="#priority-1-immediate-fixes-easy-wins"
id="toc-priority-1-immediate-fixes-easy-wins">Priority 1: Immediate
Fixes (Easy Wins)</a></li>
<li><a href="#priority-2-medium-term-improvements-compliance"
id="toc-priority-2-medium-term-improvements-compliance">Priority 2:
Medium-Term Improvements (Compliance)</a></li>
<li><a href="#priority-3-long-term-architectural-decisions"
id="toc-priority-3-long-term-architectural-decisions">Priority 3:
Long-Term Architectural Decisions</a></li>
</ul></li>
<li><a href="#code-files-requiring-changes"
id="toc-code-files-requiring-changes">Code Files Requiring Changes</a>
<ul>
<li><a href="#high-priority-immediate-fixes"
id="toc-high-priority-immediate-fixes">High Priority (Immediate
Fixes)</a></li>
<li><a href="#medium-priority-compliance"
id="toc-medium-priority-compliance">Medium Priority
(Compliance)</a></li>
</ul></li>
<li><a href="#testing-requirements"
id="toc-testing-requirements">Testing Requirements</a>
<ul>
<li><a href="#test-case-1-simple-receivable-payment-no-payable"
id="toc-test-case-1-simple-receivable-payment-no-payable">Test Case 1:
Simple Receivable Payment (No Payable)</a></li>
<li><a href="#test-case-2-true-net-settlement"
id="toc-test-case-2-true-net-settlement">Test Case 2: True Net
Settlement</a></li>
<li><a href="#test-case-3-exchange-gainloss-future"
id="toc-test-case-3-exchange-gainloss-future">Test Case 3: Exchange
Gain/Loss (Future)</a></li>
</ul></li>
<li><a href="#conclusion" id="toc-conclusion">Conclusion</a>
<ul>
<li><a href="#summary-of-issues" id="toc-summary-of-issues">Summary of
Issues</a></li>
<li><a href="#professional-assessment"
id="toc-professional-assessment">Professional Assessment</a></li>
<li><a href="#next-steps" id="toc-next-steps">Next Steps</a></li>
</ul></li>
<li><a href="#references" id="toc-references">References</a></li>
</ul></li>
</ul>
</nav>
<h1 id="accounting-analysis-net-settlement-entry-pattern">Accounting
Analysis: Net Settlement Entry Pattern</h1>
<p><strong>Date</strong>: 2025-01-12 <strong>Prepared By</strong>:
Senior Accounting Review <strong>Subject</strong>: Libra Extension -
Lightning Payment Settlement Entries <strong>Status</strong>: Technical
Review</p>
<hr />
<h2 id="executive-summary">Executive Summary</h2>
<p>This document provides a professional accounting assessment of
Libras net settlement entry pattern used for recording Lightning
Network payments that settle fiat-denominated receivables. The analysis
identifies areas where the implementation deviates from traditional
accounting best practices and provides specific recommendations for
improvement.</p>
<p><strong>Key Findings</strong>: - ✅ Double-entry integrity maintained
- ✅ Functional for intended purpose - ❌ Zero-amount postings violate
accounting principles - ❌ Redundant satoshi tracking - ❌ No exchange
gain/loss recognition - ⚠️ Mixed currency approach lacks clear
hierarchy</p>
<hr />
<h2 id="background-the-technical-challenge">Background: The Technical
Challenge</h2>
<p>Libra operates as a Lightning Network-integrated accounting system
for collectives (co-living spaces, makerspaces). It faces a unique
accounting challenge:</p>
<p><strong>Scenario</strong>: User creates a receivable in EUR (e.g.,
€200 for room rent), then pays via Lightning Network in satoshis
(225,033 sats).</p>
<p><strong>Challenge</strong>: Record the payment while: 1. Clearing the
exact EUR receivable amount 2. Recording the exact satoshi amount
received 3. Handling cases where users have both receivables (owe
Libra) and payables (Libra owes them) 4. Maintaining Beancount
double-entry balance</p>
<hr />
<h2 id="current-implementation">Current Implementation</h2>
<h3 id="transaction-example">Transaction Example</h3>
<pre class="beancount"><code>; Step 1: Receivable Created
2025-11-12 * &quot;room (200.00 EUR)&quot; #receivable-entry
user-id: &quot;375ec158&quot;
source: &quot;libra-api&quot;
sats-amount: &quot;225033&quot;
Assets:Receivable:User-375ec158 200.00 EUR
sats-equivalent: &quot;225033&quot;
Income:Accommodation:Guests -200.00 EUR
sats-equivalent: &quot;225033&quot;
; Step 2: Lightning Payment Received
2025-11-12 * &quot;Lightning payment settlement from user 375ec158&quot;
#lightning-payment #net-settlement
user-id: &quot;375ec158&quot;
source: &quot;lightning_payment&quot;
payment-type: &quot;net-settlement&quot;
payment-hash: &quot;8d080ec4cc4301715535004156085dd50c159185...&quot;
Assets:Bitcoin:Lightning 225033 SATS @ 0.0008887585... EUR
payment-hash: &quot;8d080ec4cc4301715535004156085dd50c159185...&quot;
Assets:Receivable:User-375ec158 -200.00 EUR
sats-equivalent: &quot;225033&quot;
Liabilities:Payable:User-375ec158 0.00 EUR</code></pre>
<h3 id="code-implementation">Code Implementation</h3>
<p><strong>Location</strong>:
<code>beancount_format.py:739-760</code></p>
<div class="sourceCode" id="cb2"><pre
class="sourceCode python"><code class="sourceCode python"><span id="cb2-1"><a href="#cb2-1" aria-hidden="true" tabindex="-1"></a><span class="co"># Build postings for net settlement</span></span>
<span id="cb2-2"><a href="#cb2-2" aria-hidden="true" tabindex="-1"></a>postings <span class="op">=</span> [</span>
<span id="cb2-3"><a href="#cb2-3" aria-hidden="true" tabindex="-1"></a> {</span>
<span id="cb2-4"><a href="#cb2-4" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;account&quot;</span>: payment_account,</span>
<span id="cb2-5"><a href="#cb2-5" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;amount&quot;</span>: <span class="ss">f&quot;</span><span class="sc">{</span><span class="bu">abs</span>(amount_sats)<span class="sc">}</span><span class="ss"> SATS @@ </span><span class="sc">{</span><span class="bu">abs</span>(net_fiat_amount)<span class="sc">:.2f}</span><span class="ss"> </span><span class="sc">{</span>fiat_currency<span class="sc">}</span><span class="ss">&quot;</span>,</span>
<span id="cb2-6"><a href="#cb2-6" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;meta&quot;</span>: {<span class="st">&quot;payment-hash&quot;</span>: payment_hash} <span class="cf">if</span> payment_hash <span class="cf">else</span> {}</span>
<span id="cb2-7"><a href="#cb2-7" aria-hidden="true" tabindex="-1"></a> },</span>
<span id="cb2-8"><a href="#cb2-8" aria-hidden="true" tabindex="-1"></a> {</span>
<span id="cb2-9"><a href="#cb2-9" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;account&quot;</span>: receivable_account,</span>
<span id="cb2-10"><a href="#cb2-10" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;amount&quot;</span>: <span class="ss">f&quot;-</span><span class="sc">{</span><span class="bu">abs</span>(total_receivable_fiat)<span class="sc">:.2f}</span><span class="ss"> </span><span class="sc">{</span>fiat_currency<span class="sc">}</span><span class="ss">&quot;</span>,</span>
<span id="cb2-11"><a href="#cb2-11" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;meta&quot;</span>: {<span class="st">&quot;sats-equivalent&quot;</span>: <span class="bu">str</span>(<span class="bu">abs</span>(amount_sats))}</span>
<span id="cb2-12"><a href="#cb2-12" aria-hidden="true" tabindex="-1"></a> },</span>
<span id="cb2-13"><a href="#cb2-13" aria-hidden="true" tabindex="-1"></a> {</span>
<span id="cb2-14"><a href="#cb2-14" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;account&quot;</span>: payable_account,</span>
<span id="cb2-15"><a href="#cb2-15" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;amount&quot;</span>: <span class="ss">f&quot;</span><span class="sc">{</span><span class="bu">abs</span>(total_payable_fiat)<span class="sc">:.2f}</span><span class="ss"> </span><span class="sc">{</span>fiat_currency<span class="sc">}</span><span class="ss">&quot;</span>,</span>
<span id="cb2-16"><a href="#cb2-16" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;meta&quot;</span>: {}</span>
<span id="cb2-17"><a href="#cb2-17" aria-hidden="true" tabindex="-1"></a> }</span>
<span id="cb2-18"><a href="#cb2-18" aria-hidden="true" tabindex="-1"></a>]</span></code></pre></div>
<p><strong>Three-Posting Structure</strong>: 1. <strong>Lightning
Account</strong>: Records SATS received with <code>@@</code> total price
notation 2. <strong>Receivable Account</strong>: Clears EUR receivable
with sats-equivalent metadata 3. <strong>Payable Account</strong>:
Clears any outstanding EUR payables (often 0.00)</p>
<hr />
<h2 id="accounting-issues-identified">Accounting Issues Identified</h2>
<h3 id="issue-1-zero-amount-postings">Issue 1: Zero-Amount Postings</h3>
<p><strong>Problem</strong>: The third posting often records
<code>0.00 EUR</code> when no payable exists.</p>
<pre class="beancount"><code>Liabilities:Payable:User-375ec158 0.00 EUR</code></pre>
<p><strong>Why This Is Wrong</strong>: - Zero-amount postings have no
economic substance - Clutters the journal with non-events - Violates the
principle of materiality (GAAP Concept Statement 2) - Makes auditing
more difficult (reviewers must verify why zero amounts exist)</p>
<p><strong>Accounting Principle Violated</strong>: &gt; “Transactions
should only include postings that represent actual economic events or
changes in account balances.”</p>
<p><strong>Impact</strong>: Low severity, but unprofessional
presentation</p>
<p><strong>Recommendation</strong>:</p>
<div class="sourceCode" id="cb4"><pre
class="sourceCode python"><code class="sourceCode python"><span id="cb4-1"><a href="#cb4-1" aria-hidden="true" tabindex="-1"></a><span class="co"># Make payable posting conditional</span></span>
<span id="cb4-2"><a href="#cb4-2" aria-hidden="true" tabindex="-1"></a>postings <span class="op">=</span> [</span>
<span id="cb4-3"><a href="#cb4-3" aria-hidden="true" tabindex="-1"></a> {<span class="st">&quot;account&quot;</span>: payment_account, <span class="st">&quot;amount&quot;</span>: ...},</span>
<span id="cb4-4"><a href="#cb4-4" aria-hidden="true" tabindex="-1"></a> {<span class="st">&quot;account&quot;</span>: receivable_account, <span class="st">&quot;amount&quot;</span>: ...}</span>
<span id="cb4-5"><a href="#cb4-5" aria-hidden="true" tabindex="-1"></a>]</span>
<span id="cb4-6"><a href="#cb4-6" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb4-7"><a href="#cb4-7" aria-hidden="true" tabindex="-1"></a><span class="co"># Only add payable posting if there&#39;s actually a payable</span></span>
<span id="cb4-8"><a href="#cb4-8" aria-hidden="true" tabindex="-1"></a><span class="cf">if</span> total_payable_fiat <span class="op">&gt;</span> <span class="dv">0</span>:</span>
<span id="cb4-9"><a href="#cb4-9" aria-hidden="true" tabindex="-1"></a> postings.append({</span>
<span id="cb4-10"><a href="#cb4-10" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;account&quot;</span>: payable_account,</span>
<span id="cb4-11"><a href="#cb4-11" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;amount&quot;</span>: <span class="ss">f&quot;</span><span class="sc">{</span><span class="bu">abs</span>(total_payable_fiat)<span class="sc">:.2f}</span><span class="ss"> </span><span class="sc">{</span>fiat_currency<span class="sc">}</span><span class="ss">&quot;</span>,</span>
<span id="cb4-12"><a href="#cb4-12" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;meta&quot;</span>: {}</span>
<span id="cb4-13"><a href="#cb4-13" aria-hidden="true" tabindex="-1"></a> })</span></code></pre></div>
<hr />
<h3 id="issue-2-redundant-satoshi-tracking">Issue 2: Redundant Satoshi
Tracking</h3>
<p><strong>Problem</strong>: Satoshis are tracked in TWO places in the
same transaction:</p>
<ol type="1">
<li><p><strong>Position Amount</strong> (via <code>@@</code>
notation):</p>
<pre class="beancount"><code>Assets:Bitcoin:Lightning 225033 SATS @@ 200.00 EUR</code></pre></li>
<li><p><strong>Metadata</strong> (sats-equivalent):</p>
<pre class="beancount"><code>Assets:Receivable:User-375ec158 -200.00 EUR
sats-equivalent: &quot;225033&quot;</code></pre></li>
</ol>
<p><strong>Why This Is Problematic</strong>: - The <code>@@</code>
notation already records the exact satoshi amount - Beancounts price
database stores this relationship - Metadata becomes redundant for this
specific posting - Increases storage and potential for inconsistency</p>
<p><strong>Technical Detail</strong>:</p>
<p>The <code>@@</code> notation means “total price” and Beancount
converts it to per-unit price:</p>
<pre class="beancount"><code>; You write:
Assets:Bitcoin:Lightning 225033 SATS @@ 200.00 EUR
; Beancount stores:
Assets:Bitcoin:Lightning 225033 SATS @ 0.0008887585... EUR
; (where 200.00 / 225033 = 0.0008887585...)</code></pre>
<p>Beancount can query this:</p>
<div class="sourceCode" id="cb8"><pre
class="sourceCode sql"><code class="sourceCode sql"><span id="cb8-1"><a href="#cb8-1" aria-hidden="true" tabindex="-1"></a><span class="kw">SELECT</span> <span class="kw">account</span>, <span class="fu">sum</span>(<span class="fu">convert</span>(position, SATS))</span>
<span id="cb8-2"><a href="#cb8-2" aria-hidden="true" tabindex="-1"></a><span class="kw">WHERE</span> <span class="kw">account</span> <span class="op">=</span> <span class="st">&#39;Assets:Bitcoin:Lightning&#39;</span></span></code></pre></div>
<p><strong>Recommendation</strong>:</p>
<p>Choose ONE approach consistently:</p>
<p><strong>Option A - Use @ notation</strong> (Beancount standard):</p>
<pre class="beancount"><code>Assets:Bitcoin:Lightning 225033 SATS @@ 200.00 EUR
payment-hash: &quot;8d080ec4...&quot;
Assets:Receivable:User-375ec158 -200.00 EUR
; No sats-equivalent needed here</code></pre>
<p><strong>Option B - Use EUR positions with metadata</strong> (Libras
current approach):</p>
<pre class="beancount"><code>Assets:Bitcoin:Lightning 200.00 EUR
sats-received: &quot;225033&quot;
payment-hash: &quot;8d080ec4...&quot;
Assets:Receivable:User-375ec158 -200.00 EUR
sats-cleared: &quot;225033&quot;</code></pre>
<p><strong>Dont</strong>: Mix both in the same transaction (current
implementation)</p>
<hr />
<h3 id="issue-3-no-exchange-gainloss-recognition">Issue 3: No Exchange
Gain/Loss Recognition</h3>
<p><strong>Problem</strong>: When receivables are denominated in one
currency (EUR) and paid in another (SATS), exchange rate fluctuations
create gains or losses that should be recognized.</p>
<p><strong>Example Scenario</strong>:</p>
<pre><code>Day 1 - Receivable Created:
200 EUR = 225,033 SATS (rate: 1,125.165 sats/EUR)
Day 5 - Payment Received:
225,033 SATS = 199.50 EUR (rate: 1,127.682 sats/EUR)
Exchange rate moved unfavorably
Economic Reality: 0.50 EUR LOSS</code></pre>
<p><strong>Current Implementation</strong>: Forces balance by
calculating the <code>@</code> rate to make it exactly 200 EUR:</p>
<pre class="beancount"><code>Assets:Bitcoin:Lightning 225033 SATS @ 0.000888... EUR ; = exactly 200.00 EUR</code></pre>
<p>This <strong>hides the exchange variance</strong> by treating the
payment as if it was worth exactly the receivable amount.</p>
<p><strong>GAAP/IFRS Requirement</strong>:</p>
<p>Under both US GAAP (ASC 830) and IFRS (IAS 21), exchange gains and
losses on monetary items (like receivables) should be recognized in the
period they occur.</p>
<p><strong>Proper Accounting Treatment</strong>:</p>
<pre class="beancount"><code>2025-11-12 * &quot;Lightning payment with exchange loss&quot;
Assets:Bitcoin:Lightning 225033 SATS @ 0.000886... EUR
; Market rate at payment time = 199.50 EUR
Expenses:Foreign-Exchange-Loss 0.50 EUR
Assets:Receivable:User-375ec158 -200.00 EUR</code></pre>
<p><strong>Impact</strong>: Moderate severity - affects financial
statement accuracy</p>
<p><strong>Why This Matters</strong>: - Tax reporting may require
exchange gain/loss recognition - Financial statements misstate true
economic results - Auditors would flag this as a compliance issue -
Cannot accurately calculate ROI or performance metrics</p>
<hr />
<h3 id="issue-4-semantic-misuse-of-price-notation">Issue 4: Semantic
Misuse of Price Notation</h3>
<p><strong>Problem</strong>: The <code>@</code> notation in Beancount
represents <strong>acquisition cost</strong>, not <strong>settlement
value</strong>.</p>
<p><strong>Current Usage</strong>:</p>
<pre class="beancount"><code>Assets:Bitcoin:Lightning 225033 SATS @ 0.000888... EUR</code></pre>
<p><strong>What this notation means in accounting</strong>: “We
<strong>purchased</strong> 225,033 satoshis at a cost of 0.000888 EUR
per satoshi”</p>
<p><strong>What actually happened</strong>: “We
<strong>received</strong> 225,033 satoshis as payment for a debt”</p>
<p><strong>Economic Difference</strong>: - <strong>Purchase</strong>:
You exchange cash for an asset (buying Bitcoin) - <strong>Payment
Receipt</strong>: You receive an asset in settlement of a receivable</p>
<p><strong>Accounting Substance vs. Form</strong>: -
<strong>Form</strong>: The transaction looks like a Bitcoin purchase -
<strong>Substance</strong>: The transaction is actually a receivable
collection</p>
<p><strong>GAAP Principle (ASC 105-10-05)</strong>: &gt; “Accounting
should reflect the economic substance of transactions, not merely their
legal form.”</p>
<p><strong>Why This Creates Issues</strong>:</p>
<ol type="1">
<li><strong>Cost Basis Tracking</strong>: For tax purposes, the “cost”
of Bitcoin received as payment should be its fair market value at
receipt, not the receivable amount</li>
<li><strong>Price Database Pollution</strong>: Beancounts price
database now contains “prices” that arent real market prices</li>
<li><strong>Auditor Confusion</strong>: An auditor reviewing this would
question why purchase prices dont match market rates</li>
</ol>
<p><strong>Proper Accounting Approach</strong>:</p>
<pre class="beancount"><code>; Approach 1: Record at fair market value
Assets:Bitcoin:Lightning 225033 SATS @ 0.000886... EUR
; Using actual market price at time of receipt
acquisition-type: &quot;payment-received&quot;
Revenue:Exchange-Gain 0.50 EUR
Assets:Receivable:User-375ec158 -200.00 EUR
; Approach 2: Don&#39;t use @ notation at all
Assets:Bitcoin:Lightning 200.00 EUR
sats-received: &quot;225033&quot;
fmv-at-receipt: &quot;199.50 EUR&quot;
Assets:Receivable:User-375ec158 -200.00 EUR</code></pre>
<hr />
<h3 id="issue-5-misnamed-function-and-incorrect-usage">Issue 5: Misnamed
Function and Incorrect Usage</h3>
<p><strong>Problem</strong>: Function is called
<code>format_net_settlement_entry</code>, but its used for simple
payments that arent true net settlements.</p>
<p><strong>Example from Users Transaction</strong>: - Receivable:
200.00 EUR - Payable: 0.00 EUR - Net: 200.00 EUR (this is just a
<strong>payment</strong>, not a <strong>settlement</strong>)</p>
<p><strong>Accounting Terminology</strong>:</p>
<ul>
<li><strong>Payment</strong>: Settling a single obligation (receivable
OR payable)</li>
<li><strong>Net Settlement</strong>: Offsetting multiple obligations
(receivable AND payable)</li>
</ul>
<p><strong>When Net Settlement is Appropriate</strong>:</p>
<pre><code>User owes Libra: 555.00 EUR (receivable)
Libra owes User: 38.00 EUR (payable)
Net amount due: 517.00 EUR (true settlement)</code></pre>
<p>Proper three-posting entry:</p>
<pre class="beancount"><code>Assets:Bitcoin:Lightning 565251 SATS @@ 517.00 EUR
Assets:Receivable:User -555.00 EUR
Liabilities:Payable:User 38.00 EUR
; Net: 517.00 = -555.00 + 38.00 ✓</code></pre>
<p><strong>When Two Postings Suffice</strong>:</p>
<pre><code>User owes Libra: 200.00 EUR (receivable)
Libra owes User: 0.00 EUR (no payable)
Amount due: 200.00 EUR (simple payment)</code></pre>
<p>Simpler two-posting entry:</p>
<pre class="beancount"><code>Assets:Bitcoin:Lightning 225033 SATS @@ 200.00 EUR
Assets:Receivable:User -200.00 EUR</code></pre>
<p><strong>Best Practice</strong>: Use the simplest journal entry
structure that accurately represents the transaction.</p>
<p><strong>Recommendation</strong>: 1. Rename function to
<code>format_payment_entry</code> or
<code>format_receivable_payment_entry</code> 2. Create separate
<code>format_net_settlement_entry</code> for true netting scenarios 3.
Use conditional logic to choose 2-posting vs 3-posting based on whether
both receivables AND payables exist</p>
<hr />
<h2 id="traditional-accounting-approaches">Traditional Accounting
Approaches</h2>
<h3
id="approach-1-record-bitcoin-at-fair-market-value-tax-compliant">Approach
1: Record Bitcoin at Fair Market Value (Tax Compliant)</h3>
<pre class="beancount"><code>2025-11-12 * &quot;Bitcoin payment from user 375ec158&quot;
Assets:Bitcoin:Lightning 199.50 EUR
sats-received: &quot;225033&quot;
fmv-per-sat: &quot;0.000886 EUR&quot;
cost-basis: &quot;199.50 EUR&quot;
payment-hash: &quot;8d080ec4...&quot;
Revenue:Exchange-Gain 0.50 EUR
source: &quot;cryptocurrency-receipt&quot;
Assets:Receivable:User-375ec158 -200.00 EUR</code></pre>
<p><strong>Pros</strong>: - ✅ Tax compliant (establishes cost basis) -
✅ Recognizes exchange gain/loss - ✅ Uses actual market rates - ✅
Audit trail for cryptocurrency receipts</p>
<p><strong>Cons</strong>: - ❌ Requires real-time price feeds - ❌
Creates taxable events</p>
<hr />
<h3
id="approach-2-simplified-eur-only-ledger-no-sats-positions">Approach 2:
Simplified EUR-Only Ledger (No SATS Positions)</h3>
<pre class="beancount"><code>2025-11-12 * &quot;Bitcoin payment from user 375ec158&quot;
Assets:Bitcoin:Lightning 200.00 EUR
sats-received: &quot;225033&quot;
sats-rate: &quot;1125.165&quot;
payment-hash: &quot;8d080ec4...&quot;
Assets:Receivable:User-375ec158 -200.00 EUR</code></pre>
<p><strong>Pros</strong>: - ✅ Simple and clean - ✅ EUR positions match
accounting reality - ✅ SATS tracked in metadata for reference - ✅ No
artificial price notation</p>
<p><strong>Cons</strong>: - ❌ SATS not queryable via Beancount
positions - ❌ Requires metadata parsing for SATS balances</p>
<hr />
<h3
id="approach-3-true-net-settlement-when-both-obligations-exist">Approach
3: True Net Settlement (When Both Obligations Exist)</h3>
<pre class="beancount"><code>2025-11-12 * &quot;Net settlement via Lightning&quot;
; User owes 555 EUR, Libra owes 38 EUR, net: 517 EUR
Assets:Bitcoin:Lightning 517.00 EUR
sats-received: &quot;565251&quot;
Assets:Receivable:User-375ec158 -555.00 EUR
Liabilities:Payable:User-375ec158 38.00 EUR</code></pre>
<p><strong>When to Use</strong>: Only when <strong>both</strong>
receivables and payables exist and youre truly netting them.</p>
<hr />
<h2 id="recommendations">Recommendations</h2>
<h3 id="priority-1-immediate-fixes-easy-wins">Priority 1: Immediate
Fixes (Easy Wins)</h3>
<h4 id="remove-zero-amount-postings">1.1 Remove Zero-Amount
Postings</h4>
<p><strong>File</strong>: <code>beancount_format.py:739-760</code></p>
<p><strong>Current Code</strong>:</p>
<div class="sourceCode" id="cb23"><pre
class="sourceCode python"><code class="sourceCode python"><span id="cb23-1"><a href="#cb23-1" aria-hidden="true" tabindex="-1"></a>postings <span class="op">=</span> [</span>
<span id="cb23-2"><a href="#cb23-2" aria-hidden="true" tabindex="-1"></a> {...}, <span class="co"># Lightning</span></span>
<span id="cb23-3"><a href="#cb23-3" aria-hidden="true" tabindex="-1"></a> {...}, <span class="co"># Receivable</span></span>
<span id="cb23-4"><a href="#cb23-4" aria-hidden="true" tabindex="-1"></a> { <span class="co"># Payable (always included, even if 0.00)</span></span>
<span id="cb23-5"><a href="#cb23-5" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;account&quot;</span>: payable_account,</span>
<span id="cb23-6"><a href="#cb23-6" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;amount&quot;</span>: <span class="ss">f&quot;</span><span class="sc">{</span><span class="bu">abs</span>(total_payable_fiat)<span class="sc">:.2f}</span><span class="ss"> </span><span class="sc">{</span>fiat_currency<span class="sc">}</span><span class="ss">&quot;</span>,</span>
<span id="cb23-7"><a href="#cb23-7" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;meta&quot;</span>: {}</span>
<span id="cb23-8"><a href="#cb23-8" aria-hidden="true" tabindex="-1"></a> }</span>
<span id="cb23-9"><a href="#cb23-9" aria-hidden="true" tabindex="-1"></a>]</span></code></pre></div>
<p><strong>Fixed Code</strong>:</p>
<div class="sourceCode" id="cb24"><pre
class="sourceCode python"><code class="sourceCode python"><span id="cb24-1"><a href="#cb24-1" aria-hidden="true" tabindex="-1"></a>postings <span class="op">=</span> [</span>
<span id="cb24-2"><a href="#cb24-2" aria-hidden="true" tabindex="-1"></a> {</span>
<span id="cb24-3"><a href="#cb24-3" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;account&quot;</span>: payment_account,</span>
<span id="cb24-4"><a href="#cb24-4" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;amount&quot;</span>: <span class="ss">f&quot;</span><span class="sc">{</span><span class="bu">abs</span>(amount_sats)<span class="sc">}</span><span class="ss"> SATS @@ </span><span class="sc">{</span><span class="bu">abs</span>(net_fiat_amount)<span class="sc">:.2f}</span><span class="ss"> </span><span class="sc">{</span>fiat_currency<span class="sc">}</span><span class="ss">&quot;</span>,</span>
<span id="cb24-5"><a href="#cb24-5" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;meta&quot;</span>: {<span class="st">&quot;payment-hash&quot;</span>: payment_hash} <span class="cf">if</span> payment_hash <span class="cf">else</span> {}</span>
<span id="cb24-6"><a href="#cb24-6" aria-hidden="true" tabindex="-1"></a> },</span>
<span id="cb24-7"><a href="#cb24-7" aria-hidden="true" tabindex="-1"></a> {</span>
<span id="cb24-8"><a href="#cb24-8" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;account&quot;</span>: receivable_account,</span>
<span id="cb24-9"><a href="#cb24-9" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;amount&quot;</span>: <span class="ss">f&quot;-</span><span class="sc">{</span><span class="bu">abs</span>(total_receivable_fiat)<span class="sc">:.2f}</span><span class="ss"> </span><span class="sc">{</span>fiat_currency<span class="sc">}</span><span class="ss">&quot;</span>,</span>
<span id="cb24-10"><a href="#cb24-10" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;meta&quot;</span>: {<span class="st">&quot;sats-equivalent&quot;</span>: <span class="bu">str</span>(<span class="bu">abs</span>(amount_sats))}</span>
<span id="cb24-11"><a href="#cb24-11" aria-hidden="true" tabindex="-1"></a> }</span>
<span id="cb24-12"><a href="#cb24-12" aria-hidden="true" tabindex="-1"></a>]</span>
<span id="cb24-13"><a href="#cb24-13" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb24-14"><a href="#cb24-14" aria-hidden="true" tabindex="-1"></a><span class="co"># Only add payable posting if there&#39;s actually a payable to clear</span></span>
<span id="cb24-15"><a href="#cb24-15" aria-hidden="true" tabindex="-1"></a><span class="cf">if</span> total_payable_fiat <span class="op">&gt;</span> <span class="dv">0</span>:</span>
<span id="cb24-16"><a href="#cb24-16" aria-hidden="true" tabindex="-1"></a> postings.append({</span>
<span id="cb24-17"><a href="#cb24-17" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;account&quot;</span>: payable_account,</span>
<span id="cb24-18"><a href="#cb24-18" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;amount&quot;</span>: <span class="ss">f&quot;</span><span class="sc">{</span><span class="bu">abs</span>(total_payable_fiat)<span class="sc">:.2f}</span><span class="ss"> </span><span class="sc">{</span>fiat_currency<span class="sc">}</span><span class="ss">&quot;</span>,</span>
<span id="cb24-19"><a href="#cb24-19" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;meta&quot;</span>: {}</span>
<span id="cb24-20"><a href="#cb24-20" aria-hidden="true" tabindex="-1"></a> })</span></code></pre></div>
<p><strong>Impact</strong>: Cleaner journal, professional presentation,
easier auditing</p>
<hr />
<h4 id="choose-one-sats-tracking-method">1.2 Choose One SATS Tracking
Method</h4>
<p><strong>Decision Required</strong>: Select either position-based OR
metadata-based satoshi tracking.</p>
<p><strong>Option A - Keep Metadata Approach</strong> (recommended for
Libra):</p>
<div class="sourceCode" id="cb25"><pre
class="sourceCode python"><code class="sourceCode python"><span id="cb25-1"><a href="#cb25-1" aria-hidden="true" tabindex="-1"></a><span class="co"># In format_net_settlement_entry()</span></span>
<span id="cb25-2"><a href="#cb25-2" aria-hidden="true" tabindex="-1"></a>postings <span class="op">=</span> [</span>
<span id="cb25-3"><a href="#cb25-3" aria-hidden="true" tabindex="-1"></a> {</span>
<span id="cb25-4"><a href="#cb25-4" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;account&quot;</span>: payment_account,</span>
<span id="cb25-5"><a href="#cb25-5" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;amount&quot;</span>: <span class="ss">f&quot;</span><span class="sc">{</span><span class="bu">abs</span>(net_fiat_amount)<span class="sc">:.2f}</span><span class="ss"> </span><span class="sc">{</span>fiat_currency<span class="sc">}</span><span class="ss">&quot;</span>, <span class="co"># EUR only</span></span>
<span id="cb25-6"><a href="#cb25-6" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;meta&quot;</span>: {</span>
<span id="cb25-7"><a href="#cb25-7" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;sats-received&quot;</span>: <span class="bu">str</span>(<span class="bu">abs</span>(amount_sats)),</span>
<span id="cb25-8"><a href="#cb25-8" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;payment-hash&quot;</span>: payment_hash</span>
<span id="cb25-9"><a href="#cb25-9" aria-hidden="true" tabindex="-1"></a> }</span>
<span id="cb25-10"><a href="#cb25-10" aria-hidden="true" tabindex="-1"></a> },</span>
<span id="cb25-11"><a href="#cb25-11" aria-hidden="true" tabindex="-1"></a> {</span>
<span id="cb25-12"><a href="#cb25-12" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;account&quot;</span>: receivable_account,</span>
<span id="cb25-13"><a href="#cb25-13" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;amount&quot;</span>: <span class="ss">f&quot;-</span><span class="sc">{</span><span class="bu">abs</span>(total_receivable_fiat)<span class="sc">:.2f}</span><span class="ss"> </span><span class="sc">{</span>fiat_currency<span class="sc">}</span><span class="ss">&quot;</span>,</span>
<span id="cb25-14"><a href="#cb25-14" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;meta&quot;</span>: {<span class="st">&quot;sats-cleared&quot;</span>: <span class="bu">str</span>(<span class="bu">abs</span>(amount_sats))}</span>
<span id="cb25-15"><a href="#cb25-15" aria-hidden="true" tabindex="-1"></a> }</span>
<span id="cb25-16"><a href="#cb25-16" aria-hidden="true" tabindex="-1"></a>]</span></code></pre></div>
<p><strong>Option B - Use Position-Based Tracking</strong>:</p>
<div class="sourceCode" id="cb26"><pre
class="sourceCode python"><code class="sourceCode python"><span id="cb26-1"><a href="#cb26-1" aria-hidden="true" tabindex="-1"></a><span class="co"># Remove sats-equivalent metadata entirely</span></span>
<span id="cb26-2"><a href="#cb26-2" aria-hidden="true" tabindex="-1"></a>postings <span class="op">=</span> [</span>
<span id="cb26-3"><a href="#cb26-3" aria-hidden="true" tabindex="-1"></a> {</span>
<span id="cb26-4"><a href="#cb26-4" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;account&quot;</span>: payment_account,</span>
<span id="cb26-5"><a href="#cb26-5" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;amount&quot;</span>: <span class="ss">f&quot;</span><span class="sc">{</span><span class="bu">abs</span>(amount_sats)<span class="sc">}</span><span class="ss"> SATS @@ </span><span class="sc">{</span><span class="bu">abs</span>(net_fiat_amount)<span class="sc">:.2f}</span><span class="ss"> </span><span class="sc">{</span>fiat_currency<span class="sc">}</span><span class="ss">&quot;</span>,</span>
<span id="cb26-6"><a href="#cb26-6" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;meta&quot;</span>: {<span class="st">&quot;payment-hash&quot;</span>: payment_hash}</span>
<span id="cb26-7"><a href="#cb26-7" aria-hidden="true" tabindex="-1"></a> },</span>
<span id="cb26-8"><a href="#cb26-8" aria-hidden="true" tabindex="-1"></a> {</span>
<span id="cb26-9"><a href="#cb26-9" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;account&quot;</span>: receivable_account,</span>
<span id="cb26-10"><a href="#cb26-10" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;amount&quot;</span>: <span class="ss">f&quot;-</span><span class="sc">{</span><span class="bu">abs</span>(total_receivable_fiat)<span class="sc">:.2f}</span><span class="ss"> </span><span class="sc">{</span>fiat_currency<span class="sc">}</span><span class="ss">&quot;</span>,</span>
<span id="cb26-11"><a href="#cb26-11" aria-hidden="true" tabindex="-1"></a> <span class="co"># No sats-equivalent needed - queryable via price database</span></span>
<span id="cb26-12"><a href="#cb26-12" aria-hidden="true" tabindex="-1"></a> }</span>
<span id="cb26-13"><a href="#cb26-13" aria-hidden="true" tabindex="-1"></a>]</span></code></pre></div>
<p><strong>Recommendation</strong>: Choose Option A (metadata) for
consistency with Libras architecture.</p>
<hr />
<h4 id="rename-function-for-clarity">1.3 Rename Function for
Clarity</h4>
<p><strong>File</strong>: <code>beancount_format.py</code></p>
<p><strong>Current</strong>:
<code>format_net_settlement_entry()</code></p>
<p><strong>New</strong>: <code>format_receivable_payment_entry()</code>
or <code>format_payment_settlement_entry()</code></p>
<p><strong>Rationale</strong>: More accurately describes what the
function does (processes payments, not always net settlements)</p>
<hr />
<h3 id="priority-2-medium-term-improvements-compliance">Priority 2:
Medium-Term Improvements (Compliance)</h3>
<h4 id="add-exchange-gainloss-tracking">2.1 Add Exchange Gain/Loss
Tracking</h4>
<p><strong>File</strong>: <code>tasks.py:259-276</code> (get balance and
calculate settlement)</p>
<p><strong>New Logic</strong>:</p>
<div class="sourceCode" id="cb27"><pre
class="sourceCode python"><code class="sourceCode python"><span id="cb27-1"><a href="#cb27-1" aria-hidden="true" tabindex="-1"></a><span class="co"># Get user&#39;s current balance</span></span>
<span id="cb27-2"><a href="#cb27-2" aria-hidden="true" tabindex="-1"></a>balance <span class="op">=</span> <span class="cf">await</span> fava.get_user_balance(user_id)</span>
<span id="cb27-3"><a href="#cb27-3" aria-hidden="true" tabindex="-1"></a>fiat_balances <span class="op">=</span> balance.get(<span class="st">&quot;fiat_balances&quot;</span>, {})</span>
<span id="cb27-4"><a href="#cb27-4" aria-hidden="true" tabindex="-1"></a>total_fiat_balance <span class="op">=</span> fiat_balances.get(fiat_currency, Decimal(<span class="dv">0</span>))</span>
<span id="cb27-5"><a href="#cb27-5" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb27-6"><a href="#cb27-6" aria-hidden="true" tabindex="-1"></a><span class="co"># Calculate expected fiat value of SATS payment at current market rate</span></span>
<span id="cb27-7"><a href="#cb27-7" aria-hidden="true" tabindex="-1"></a>market_rate <span class="op">=</span> <span class="cf">await</span> get_current_sats_eur_rate() <span class="co"># New function needed</span></span>
<span id="cb27-8"><a href="#cb27-8" aria-hidden="true" tabindex="-1"></a>market_value <span class="op">=</span> Decimal(amount_sats) <span class="op">*</span> market_rate</span>
<span id="cb27-9"><a href="#cb27-9" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb27-10"><a href="#cb27-10" aria-hidden="true" tabindex="-1"></a><span class="co"># Calculate exchange variance</span></span>
<span id="cb27-11"><a href="#cb27-11" aria-hidden="true" tabindex="-1"></a>receivable_amount <span class="op">=</span> <span class="bu">abs</span>(total_fiat_balance) <span class="cf">if</span> total_fiat_balance <span class="op">&gt;</span> <span class="dv">0</span> <span class="cf">else</span> Decimal(<span class="dv">0</span>)</span>
<span id="cb27-12"><a href="#cb27-12" aria-hidden="true" tabindex="-1"></a>exchange_variance <span class="op">=</span> market_value <span class="op">-</span> receivable_amount</span>
<span id="cb27-13"><a href="#cb27-13" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb27-14"><a href="#cb27-14" aria-hidden="true" tabindex="-1"></a><span class="co"># If variance is material (&gt; 1 cent), create exchange gain/loss posting</span></span>
<span id="cb27-15"><a href="#cb27-15" aria-hidden="true" tabindex="-1"></a><span class="cf">if</span> <span class="bu">abs</span>(exchange_variance) <span class="op">&gt;</span> Decimal(<span class="st">&quot;0.01&quot;</span>):</span>
<span id="cb27-16"><a href="#cb27-16" aria-hidden="true" tabindex="-1"></a> <span class="co"># Add exchange gain/loss to postings</span></span>
<span id="cb27-17"><a href="#cb27-17" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> exchange_variance <span class="op">&gt;</span> <span class="dv">0</span>:</span>
<span id="cb27-18"><a href="#cb27-18" aria-hidden="true" tabindex="-1"></a> <span class="co"># Gain: payment worth more than receivable</span></span>
<span id="cb27-19"><a href="#cb27-19" aria-hidden="true" tabindex="-1"></a> exchange_account <span class="op">=</span> <span class="st">&quot;Revenue:Foreign-Exchange-Gain&quot;</span></span>
<span id="cb27-20"><a href="#cb27-20" aria-hidden="true" tabindex="-1"></a> <span class="cf">else</span>:</span>
<span id="cb27-21"><a href="#cb27-21" aria-hidden="true" tabindex="-1"></a> <span class="co"># Loss: payment worth less than receivable</span></span>
<span id="cb27-22"><a href="#cb27-22" aria-hidden="true" tabindex="-1"></a> exchange_account <span class="op">=</span> <span class="st">&quot;Expenses:Foreign-Exchange-Loss&quot;</span></span>
<span id="cb27-23"><a href="#cb27-23" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb27-24"><a href="#cb27-24" aria-hidden="true" tabindex="-1"></a> <span class="co"># Include in entry creation</span></span>
<span id="cb27-25"><a href="#cb27-25" aria-hidden="true" tabindex="-1"></a> exchange_posting <span class="op">=</span> {</span>
<span id="cb27-26"><a href="#cb27-26" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;account&quot;</span>: exchange_account,</span>
<span id="cb27-27"><a href="#cb27-27" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;amount&quot;</span>: <span class="ss">f&quot;</span><span class="sc">{</span><span class="bu">abs</span>(exchange_variance)<span class="sc">:.2f}</span><span class="ss"> </span><span class="sc">{</span>fiat_currency<span class="sc">}</span><span class="ss">&quot;</span>,</span>
<span id="cb27-28"><a href="#cb27-28" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;meta&quot;</span>: {</span>
<span id="cb27-29"><a href="#cb27-29" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;sats-amount&quot;</span>: <span class="bu">str</span>(amount_sats),</span>
<span id="cb27-30"><a href="#cb27-30" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;market-rate&quot;</span>: <span class="bu">str</span>(market_rate),</span>
<span id="cb27-31"><a href="#cb27-31" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;receivable-amount&quot;</span>: <span class="bu">str</span>(receivable_amount)</span>
<span id="cb27-32"><a href="#cb27-32" aria-hidden="true" tabindex="-1"></a> }</span>
<span id="cb27-33"><a href="#cb27-33" aria-hidden="true" tabindex="-1"></a> }</span></code></pre></div>
<p><strong>Benefits</strong>: - ✅ Tax compliance - ✅ Accurate
financial reporting - ✅ Audit trail for cryptocurrency gains/losses -
✅ Regulatory compliance (GAAP/IFRS)</p>
<hr />
<h4 id="implement-true-net-settlement-vs.-simple-payment-logic">2.2
Implement True Net Settlement vs. Simple Payment Logic</h4>
<p><strong>File</strong>: <code>tasks.py</code> or new
<code>payment_logic.py</code></p>
<div class="sourceCode" id="cb28"><pre
class="sourceCode python"><code class="sourceCode python"><span id="cb28-1"><a href="#cb28-1" aria-hidden="true" tabindex="-1"></a><span class="cf">async</span> <span class="kw">def</span> create_payment_entry(</span>
<span id="cb28-2"><a href="#cb28-2" aria-hidden="true" tabindex="-1"></a> user_id: <span class="bu">str</span>,</span>
<span id="cb28-3"><a href="#cb28-3" aria-hidden="true" tabindex="-1"></a> amount_sats: <span class="bu">int</span>,</span>
<span id="cb28-4"><a href="#cb28-4" aria-hidden="true" tabindex="-1"></a> fiat_amount: Decimal,</span>
<span id="cb28-5"><a href="#cb28-5" aria-hidden="true" tabindex="-1"></a> fiat_currency: <span class="bu">str</span>,</span>
<span id="cb28-6"><a href="#cb28-6" aria-hidden="true" tabindex="-1"></a> payment_hash: <span class="bu">str</span></span>
<span id="cb28-7"><a href="#cb28-7" aria-hidden="true" tabindex="-1"></a>):</span>
<span id="cb28-8"><a href="#cb28-8" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;</span></span>
<span id="cb28-9"><a href="#cb28-9" aria-hidden="true" tabindex="-1"></a><span class="co"> Create appropriate payment entry based on user&#39;s balance situation.</span></span>
<span id="cb28-10"><a href="#cb28-10" aria-hidden="true" tabindex="-1"></a><span class="co"> Uses 2-posting for simple payments, 3-posting for net settlements.</span></span>
<span id="cb28-11"><a href="#cb28-11" aria-hidden="true" tabindex="-1"></a><span class="co"> &quot;&quot;&quot;</span></span>
<span id="cb28-12"><a href="#cb28-12" aria-hidden="true" tabindex="-1"></a> <span class="co"># Get user balance</span></span>
<span id="cb28-13"><a href="#cb28-13" aria-hidden="true" tabindex="-1"></a> balance <span class="op">=</span> <span class="cf">await</span> fava.get_user_balance(user_id)</span>
<span id="cb28-14"><a href="#cb28-14" aria-hidden="true" tabindex="-1"></a> fiat_balances <span class="op">=</span> balance.get(<span class="st">&quot;fiat_balances&quot;</span>, {})</span>
<span id="cb28-15"><a href="#cb28-15" aria-hidden="true" tabindex="-1"></a> total_balance <span class="op">=</span> fiat_balances.get(fiat_currency, Decimal(<span class="dv">0</span>))</span>
<span id="cb28-16"><a href="#cb28-16" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb28-17"><a href="#cb28-17" aria-hidden="true" tabindex="-1"></a> receivable_amount <span class="op">=</span> Decimal(<span class="dv">0</span>)</span>
<span id="cb28-18"><a href="#cb28-18" aria-hidden="true" tabindex="-1"></a> payable_amount <span class="op">=</span> Decimal(<span class="dv">0</span>)</span>
<span id="cb28-19"><a href="#cb28-19" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb28-20"><a href="#cb28-20" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> total_balance <span class="op">&gt;</span> <span class="dv">0</span>:</span>
<span id="cb28-21"><a href="#cb28-21" aria-hidden="true" tabindex="-1"></a> receivable_amount <span class="op">=</span> total_balance</span>
<span id="cb28-22"><a href="#cb28-22" aria-hidden="true" tabindex="-1"></a> <span class="cf">elif</span> total_balance <span class="op">&lt;</span> <span class="dv">0</span>:</span>
<span id="cb28-23"><a href="#cb28-23" aria-hidden="true" tabindex="-1"></a> payable_amount <span class="op">=</span> <span class="bu">abs</span>(total_balance)</span>
<span id="cb28-24"><a href="#cb28-24" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb28-25"><a href="#cb28-25" aria-hidden="true" tabindex="-1"></a> <span class="co"># Determine entry type</span></span>
<span id="cb28-26"><a href="#cb28-26" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> receivable_amount <span class="op">&gt;</span> <span class="dv">0</span> <span class="kw">and</span> payable_amount <span class="op">&gt;</span> <span class="dv">0</span>:</span>
<span id="cb28-27"><a href="#cb28-27" aria-hidden="true" tabindex="-1"></a> <span class="co"># TRUE NET SETTLEMENT: Both obligations exist</span></span>
<span id="cb28-28"><a href="#cb28-28" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> <span class="cf">await</span> format_net_settlement_entry(</span>
<span id="cb28-29"><a href="#cb28-29" aria-hidden="true" tabindex="-1"></a> user_id<span class="op">=</span>user_id,</span>
<span id="cb28-30"><a href="#cb28-30" aria-hidden="true" tabindex="-1"></a> amount_sats<span class="op">=</span>amount_sats,</span>
<span id="cb28-31"><a href="#cb28-31" aria-hidden="true" tabindex="-1"></a> receivable_amount<span class="op">=</span>receivable_amount,</span>
<span id="cb28-32"><a href="#cb28-32" aria-hidden="true" tabindex="-1"></a> payable_amount<span class="op">=</span>payable_amount,</span>
<span id="cb28-33"><a href="#cb28-33" aria-hidden="true" tabindex="-1"></a> fiat_amount<span class="op">=</span>fiat_amount,</span>
<span id="cb28-34"><a href="#cb28-34" aria-hidden="true" tabindex="-1"></a> fiat_currency<span class="op">=</span>fiat_currency,</span>
<span id="cb28-35"><a href="#cb28-35" aria-hidden="true" tabindex="-1"></a> payment_hash<span class="op">=</span>payment_hash</span>
<span id="cb28-36"><a href="#cb28-36" aria-hidden="true" tabindex="-1"></a> )</span>
<span id="cb28-37"><a href="#cb28-37" aria-hidden="true" tabindex="-1"></a> <span class="cf">elif</span> receivable_amount <span class="op">&gt;</span> <span class="dv">0</span>:</span>
<span id="cb28-38"><a href="#cb28-38" aria-hidden="true" tabindex="-1"></a> <span class="co"># SIMPLE RECEIVABLE PAYMENT: Only receivable exists</span></span>
<span id="cb28-39"><a href="#cb28-39" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> <span class="cf">await</span> format_receivable_payment_entry(</span>
<span id="cb28-40"><a href="#cb28-40" aria-hidden="true" tabindex="-1"></a> user_id<span class="op">=</span>user_id,</span>
<span id="cb28-41"><a href="#cb28-41" aria-hidden="true" tabindex="-1"></a> amount_sats<span class="op">=</span>amount_sats,</span>
<span id="cb28-42"><a href="#cb28-42" aria-hidden="true" tabindex="-1"></a> receivable_amount<span class="op">=</span>receivable_amount,</span>
<span id="cb28-43"><a href="#cb28-43" aria-hidden="true" tabindex="-1"></a> fiat_amount<span class="op">=</span>fiat_amount,</span>
<span id="cb28-44"><a href="#cb28-44" aria-hidden="true" tabindex="-1"></a> fiat_currency<span class="op">=</span>fiat_currency,</span>
<span id="cb28-45"><a href="#cb28-45" aria-hidden="true" tabindex="-1"></a> payment_hash<span class="op">=</span>payment_hash</span>
<span id="cb28-46"><a href="#cb28-46" aria-hidden="true" tabindex="-1"></a> )</span>
<span id="cb28-47"><a href="#cb28-47" aria-hidden="true" tabindex="-1"></a> <span class="cf">else</span>:</span>
<span id="cb28-48"><a href="#cb28-48" aria-hidden="true" tabindex="-1"></a> <span class="co"># PAYABLE PAYMENT: Libra paying user (different flow)</span></span>
<span id="cb28-49"><a href="#cb28-49" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> <span class="cf">await</span> format_payable_payment_entry(...)</span></code></pre></div>
<hr />
<h3 id="priority-3-long-term-architectural-decisions">Priority 3:
Long-Term Architectural Decisions</h3>
<h4 id="establish-primary-currency-hierarchy">3.1 Establish Primary
Currency Hierarchy</h4>
<p><strong>Current Issue</strong>: Mixed approach (EUR positions with
SATS metadata, but also SATS positions with @ notation)</p>
<p><strong>Decision Required</strong>: Choose ONE of the following
architectures:</p>
<p><strong>Architecture A - EUR Primary, SATS Secondary</strong>
(recommended):</p>
<pre class="beancount"><code>; All positions in EUR, SATS in metadata
2025-11-12 * &quot;Payment&quot;
Assets:Bitcoin:Lightning 200.00 EUR
sats-received: &quot;225033&quot;
Assets:Receivable:User -200.00 EUR
sats-cleared: &quot;225033&quot;</code></pre>
<p><strong>Architecture B - SATS Primary, EUR Secondary</strong>:</p>
<pre class="beancount"><code>; All positions in SATS, EUR in metadata
2025-11-12 * &quot;Payment&quot;
Assets:Bitcoin:Lightning 225033 SATS
eur-value: &quot;200.00&quot;
Assets:Receivable:User -225033 SATS
eur-cleared: &quot;200.00&quot;</code></pre>
<p><strong>Recommendation</strong>: Architecture A (EUR primary)
because: 1. Most receivables created in EUR 2. Financial reporting
requirements typically in fiat 3. Tax obligations calculated in fiat 4.
Aligns with current Libra metadata approach</p>
<hr />
<h4 id="consider-separate-ledger-for-cryptocurrency-holdings">3.2
Consider Separate Ledger for Cryptocurrency Holdings</h4>
<p><strong>Advanced Approach</strong>: Separate cryptocurrency movements
from fiat accounting</p>
<p><strong>Main Ledger</strong> (EUR-denominated):</p>
<pre class="beancount"><code>2025-11-12 * &quot;Payment received from user&quot;
Assets:Bitcoin-Custody:User-375ec158 200.00 EUR
Assets:Receivable:User-375ec158 -200.00 EUR</code></pre>
<p><strong>Cryptocurrency Sub-Ledger</strong> (SATS-denominated):</p>
<pre class="beancount"><code>2025-11-12 * &quot;Lightning payment received&quot;
Assets:Bitcoin:Lightning:Libra 225033 SATS
Assets:Bitcoin:Custody:User-375ec 225033 SATS</code></pre>
<p><strong>Benefits</strong>: - ✅ Clean separation of concerns - ✅
Cryptocurrency movements tracked independently - ✅ Fiat accounting
unaffected by Bitcoin volatility - ✅ Can generate separate financial
statements</p>
<p><strong>Drawbacks</strong>: - ❌ Increased complexity - ❌
Reconciliation between ledgers required - ❌ Two sets of books to
maintain</p>
<hr />
<h2 id="code-files-requiring-changes">Code Files Requiring Changes</h2>
<h3 id="high-priority-immediate-fixes">High Priority (Immediate
Fixes)</h3>
<ol type="1">
<li><strong><code>beancount_format.py:739-760</code></strong>
<ul>
<li>Remove zero-amount postings</li>
<li>Make payable posting conditional</li>
</ul></li>
<li><strong><code>beancount_format.py:692</code></strong>
<ul>
<li>Rename function to <code>format_receivable_payment_entry</code></li>
</ul></li>
</ol>
<h3 id="medium-priority-compliance">Medium Priority (Compliance)</h3>
<ol start="3" type="1">
<li><strong><code>tasks.py:235-310</code></strong>
<ul>
<li>Add exchange gain/loss calculation</li>
<li>Implement payment vs. settlement logic</li>
</ul></li>
<li><strong>New file: <code>exchange_rates.py</code></strong>
<ul>
<li>Create <code>get_current_sats_eur_rate()</code> function</li>
<li>Implement price feed integration</li>
</ul></li>
<li><strong><code>beancount_format.py</code></strong>
<ul>
<li>Create new <code>format_net_settlement_entry()</code> for true
netting</li>
<li>Create <code>format_receivable_payment_entry()</code> for simple
payments</li>
</ul></li>
</ol>
<hr />
<h2 id="testing-requirements">Testing Requirements</h2>
<h3 id="test-case-1-simple-receivable-payment-no-payable">Test Case 1:
Simple Receivable Payment (No Payable)</h3>
<p><strong>Setup</strong>: - User has receivable: 200.00 EUR - User has
payable: 0.00 EUR - User pays: 225,033 SATS</p>
<p><strong>Expected Entry</strong> (after fixes):</p>
<pre class="beancount"><code>2025-11-12 * &quot;Lightning payment from user&quot;
Assets:Bitcoin:Lightning 200.00 EUR
sats-received: &quot;225033&quot;
payment-hash: &quot;8d080ec4...&quot;
Assets:Receivable:User -200.00 EUR
sats-cleared: &quot;225033&quot;</code></pre>
<p><strong>Verify</strong>: - ✅ Only 2 postings (no zero-amount
payable) - ✅ Entry balances - ✅ SATS tracked in metadata - ✅ User
balance becomes 0 (both EUR and SATS)</p>
<hr />
<h3 id="test-case-2-true-net-settlement">Test Case 2: True Net
Settlement</h3>
<p><strong>Setup</strong>: - User has receivable: 555.00 EUR - User has
payable: 38.00 EUR - Net owed: 517.00 EUR - User pays: 565,251 SATS
(worth 517.00 EUR)</p>
<p><strong>Expected Entry</strong>:</p>
<pre class="beancount"><code>2025-11-12 * &quot;Net settlement via Lightning&quot;
Assets:Bitcoin:Lightning 517.00 EUR
sats-received: &quot;565251&quot;
payment-hash: &quot;abc123...&quot;
Assets:Receivable:User -555.00 EUR
sats-portion: &quot;565251&quot;
Liabilities:Payable:User 38.00 EUR</code></pre>
<p><strong>Verify</strong>: - ✅ 3 postings (receivable + payable
cleared) - ✅ Net amount = receivable - payable - ✅ Both balances
become 0 - ✅ Mathematically balanced</p>
<hr />
<h3 id="test-case-3-exchange-gainloss-future">Test Case 3: Exchange
Gain/Loss (Future)</h3>
<p><strong>Setup</strong>: - User has receivable: 200.00 EUR (created at
1,125 sats/EUR) - User pays: 225,033 SATS (now worth 199.50 EUR at
market) - Exchange loss: 0.50 EUR</p>
<p><strong>Expected Entry</strong> (with exchange tracking):</p>
<pre class="beancount"><code>2025-11-12 * &quot;Lightning payment with exchange loss&quot;
Assets:Bitcoin:Lightning 199.50 EUR
sats-received: &quot;225033&quot;
market-rate: &quot;0.000886&quot;
Expenses:Foreign-Exchange-Loss 0.50 EUR
Assets:Receivable:User -200.00 EUR</code></pre>
<p><strong>Verify</strong>: - ✅ Bitcoin recorded at fair market value -
✅ Exchange loss recognized - ✅ Receivable cleared at book value - ✅
Entry balances</p>
<hr />
<h2 id="conclusion">Conclusion</h2>
<h3 id="summary-of-issues">Summary of Issues</h3>
<table>
<colgroup>
<col style="width: 12%" />
<col style="width: 18%" />
<col style="width: 34%" />
<col style="width: 34%" />
</colgroup>
<thead>
<tr>
<th>Issue</th>
<th>Severity</th>
<th>Accounting Impact</th>
<th>Recommended Action</th>
</tr>
</thead>
<tbody>
<tr>
<td>Zero-amount postings</td>
<td>Low</td>
<td>Presentation only</td>
<td>Remove immediately</td>
</tr>
<tr>
<td>Redundant SATS tracking</td>
<td>Low</td>
<td>Storage/efficiency</td>
<td>Choose one method</td>
</tr>
<tr>
<td>No exchange gain/loss</td>
<td><strong>High</strong></td>
<td>Financial accuracy</td>
<td>Implement for compliance</td>
</tr>
<tr>
<td>Semantic misuse of @</td>
<td>Medium</td>
<td>Audit clarity</td>
<td>Consider EUR-only positions</td>
</tr>
<tr>
<td>Misnamed function</td>
<td>Low</td>
<td>Code clarity</td>
<td>Rename function</td>
</tr>
</tbody>
</table>
<h3 id="professional-assessment">Professional Assessment</h3>
<p><strong>Is this “best practice” accounting?</strong>
<strong>No</strong>, this implementation deviates from traditional
accounting standards in several ways.</p>
<p><strong>Is it acceptable for Libras use case?</strong> <strong>Yes,
with modifications</strong>, its a reasonable pragmatic solution for a
novel problem (cryptocurrency payments of fiat debts).</p>
<p><strong>Critical improvements needed</strong>: 1. ✅ Remove
zero-amount postings (easy fix, professional presentation) 2. ✅
Implement exchange gain/loss tracking (required for compliance) 3. ✅
Separate payment vs. settlement logic (accuracy and clarity)</p>
<p><strong>The fundamental challenge</strong>: Traditional accounting
wasnt designed for this scenario. There is no established “standard”
for recording cryptocurrency payments of fiat-denominated receivables.
Libras approach is functional, but should be refined to align better
with accounting principles where possible.</p>
<h3 id="next-steps">Next Steps</h3>
<ol type="1">
<li><strong>Week 1</strong>: Implement Priority 1 fixes (remove zero
postings, rename function)</li>
<li><strong>Week 2-3</strong>: Design and implement exchange gain/loss
tracking</li>
<li><strong>Week 4</strong>: Add payment vs. settlement logic</li>
<li><strong>Ongoing</strong>: Monitor regulatory guidance on
cryptocurrency accounting</li>
</ol>
<hr />
<h2 id="references">References</h2>
<ul>
<li><strong>FASB ASC 830</strong>: Foreign Currency Matters</li>
<li><strong>IAS 21</strong>: The Effects of Changes in Foreign Exchange
Rates</li>
<li><strong>FASB Concept Statement No. 2</strong>: Qualitative
Characteristics of Accounting Information</li>
<li><strong>ASC 105-10-05</strong>: Substance Over Form</li>
<li><strong>Beancount Documentation</strong>:
http://furius.ca/beancount/doc/index</li>
<li><strong>Libra Extension</strong>:
<code>docs/SATS-EQUIVALENT-METADATA.md</code></li>
<li><strong>BQL Analysis</strong>:
<code>docs/BQL-BALANCE-QUERIES.md</code></li>
</ul>
<hr />
<p><strong>Document Version</strong>: 1.0 <strong>Last Updated</strong>:
2025-01-12 <strong>Next Review</strong>: After Priority 1 fixes
implemented</p>
<hr />
<p><em>This analysis was prepared for internal review and development
planning. It represents a professional accounting assessment of the
current implementation and should be used to guide improvements to
Libras payment recording system.</em></p>
</body>
</html>

260
docs/CODE-REVIEW-2026-06.md Normal file
View file

@ -0,0 +1,260 @@
# Code review — 2026-06-05
Findings from a deep review of the Libra LNbits extension (12k LOC,
14 files). Each finding has `file:line` references, a one-line fix
proposal, and a status tag:
- ✅ **fixed** — merged in commit listed
- ⏳ **outstanding** — still needs work
- 🚫 **downgraded** — initially flagged, verified not a bug on closer read
Triage order at the bottom prioritises blast radius over file location.
> **2026-07-12 refactor series:** findings #2#19 fixed across PRs
> #55#59 + the chore/hygiene branch (stacked; merge in order). LOW
> items fixed in chore/hygiene except `parse_legacy_account_name`
> fragility (documented assumption, internal input only) and the
> `is_active`/`is_virtual` filter inconsistency (still open).
---
## CRITICAL
### ✅ #1 — Mass `require_admin_key` mis-use → cross-user privilege escalation
**Status:** fixed in `1201557` (`aiolabs/libra` main, 2026-06-05) +
`4c704e5` (`aiolabs/webapp` dev).
27 endpoints documented "(admin only)" used `require_admin_key`, which
only checks the caller owns *some* wallet with its admin key — i.e.
any authenticated user. Cluster included `receivable`/`revenue`
creation, equity-eligibility grant/revoke, account-permission CRUD
(grant yourself MANAGE on any account → ledger god mode), role and
user-role CRUD, account-sync admin, cross-user reports.
Also deleted the duplicate `api_pay_user` at `views_api.py:1937` (the
correctly-gated `/api/v1/payables/pay` at L2144 replaces it).
Webapp side: deleted orphaned `PermissionManager.vue` +
`GrantPermissionDialog.vue` admin components that were never imported
or routed and whose backing API methods pointed at non-existent paths.
### ✅ #2`format_net_settlement_entry` ships unbalanced postings on partial payments
`beancount_format.py:761-777` emits three postings whose weights sum to
`net_fiat total_receivable + total_payable`. The docstring example
assumes `net_fiat == total_receivable total_payable`. But
`tasks.py:251-258` sets `total_receivable = total_prior_balance` and
`net_fiat = invoice_fiat_amount` — only equal when the user paid the
full balance. Any partial payment ships unbalanced postings; Beancount
will reject or apply tolerance silently.
**Fix:** add `assert abs(net_fiat (receivable payable)) <= 0.005`
inside the formatter and raise on violation. Then fix the caller in
`tasks.py:218-322` to settle only what the payment covers (likely a
two-posting `DR Lightning / CR Receivable`-for-payment-amount, not
net-settlement).
### ✅ #3 — Migrations not idempotent (violates fork-migrations contract)
`migrations.py:347, 377` (`ALTER TABLE accounts ADD COLUMN
is_active/is_virtual`) and `migrations.py:441, 472, 510` (`CREATE TABLE
roles/role_permissions/user_roles`) lack idempotency guards. Seed
`INSERT`s at `migrations.py:319-330, 400-412, 587-597` have no `ON
CONFLICT DO NOTHING`. Per CLAUDE.md, the cross-DB write between
`ext_libra` and core `dbversions` is non-atomic — a failed version-bump
leaves the migration to re-run on boot and crash with `duplicate
column` / `table exists` / UNIQUE violation. Bricks the extension until
manual `dbversions` surgery.
**Fix:** wrap ALTERs with `_alter_add_column_safe`, switch CREATEs to
`CREATE TABLE IF NOT EXISTS`, gate seed INSERTs with `INSERT ... ON
CONFLICT DO NOTHING`.
### ✅ #4 — Lightning payment recording has no local idempotency gate
`tasks.py:218-322` relies entirely on `fava.add_entry_idempotent` for
dedup, which itself does a read-then-write race on the Fava ledger. On
lnbits restart with a persisted invoice queue, the same `payment_hash`
can re-fire; the per-user lock is in-process only and doesn't survive
restart. Webhook + poller hitting concurrently both pass the
"not present" check and both insert.
**Fix:** add a `processed_payments(payment_hash TEXT PRIMARY KEY)`
table; `INSERT OR IGNORE` at the top of `on_invoice_paid`; only
proceed if `rowcount == 1`.
---
## HIGH
### ✅ #5 — Auth prefix-match in `can_access_user_data`
`auth.py:248-251` uses `caller.user_id[:8] == target.user_id[:8]`.
Eight hex chars = 32 bits; birthday collision at ~65k users.
**Fix:** require full UUID equality; never resolve by prefix in an
authorisation decision.
### ✅ #6`can_access_account` substring match
`auth.py:178-180` does `f"User-{short}" in account.name` — matches
`Expenses:Misc-User-deadbeef` too.
**Fix:** split on `:`, require segment equality.
### ✅ #7`ChecksumConflictError` never raised by update/delete
`fava_client.py:1392-1482`: Fava 409/412 propagates as raw
`HTTPStatusError`. The `ChecksumConflictError` type exists but isn't
raised by these methods; callers see stack-trace 500s instead of a
clean retry path.
**Fix:** `if status in (409, 412): raise ChecksumConflictError(...)`.
### ✅ #8`float()` arithmetic in fiat-rate metadata
`views_api.py:1061-1062, 1263-1264, 1364-1365, 1759-1760` compute
`fiat_rate` / `btc_rate` via `float()`, then persist into Beancount
metadata as the cost basis for that entry. Float drift cascades
through reporting.
**Fix:** keep `Decimal` end-to-end; only stringify at JSON-serialise
time.
### ✅ #9 — Background loop swallows `raise` in `on_invoice_paid`
`tasks.py:320-322` does `logger.error(...); raise`.
`wait_for_paid_invoices` at `tasks.py:178-180` has no surrounding
try/except, so one unhandled exception kills the listener for the
rest of the process lifetime — no further Lightning payments get
recorded, no alarm.
**Fix:** wrap the iteration body in
`try/except Exception: logger.exception(...)`; never `raise` from
`on_invoice_paid`.
### ✅ #10`record-payment` dedup is non-atomic AND exception-swallowing
`views_api.py:1841-1924` (per subagent report — needs verification)
catches all exceptions in the dedup window with a 5-second timeout
and treats Fava errors as "not duplicate", producing double-entries
on transient Fava blips.
**Fix:** fail closed on transport error; narrow the exception type
catch to `httpx.HTTPError` only.
### ✅ #11`validate_journal_entry` is stale
`core/validation.py:21-93` validates the pre-string-amount model —
sums one bag of integers, doesn't balance per currency. Doesn't match
production data shape post-Fava migration.
**Fix:** rewrite to parse `"X CCY"` strings and balance per currency,
or delete if Beancount-side validation is now considered sufficient.
---
## MEDIUM
### ✅ #12`m001_initial` seed `INSERT` into `accounts` non-idempotent
`migrations.py:319-330` — same shape as #3.
### ✅ #13`format_posting_at_average_cost` emits `{}` when `cost_currency=None`
`beancount_format.py:256``<sats> SATS {}` isn't valid Beancount
syntax; drop the braces when cost is unset.
### ✅ #14 — Per-call `httpx.AsyncClient` instantiation in fava_client
~30 sites build a new `httpx.AsyncClient` per call. TCP handshake every
time.
**Fix:** construct once on `FavaClient.__init__`, expose `aclose()`.
### ✅ #15 — BQL string interpolation without quoting
`fava_client.py:250, 621, 770-775, 853-858` interpolate
`account_name` / user-id-prefix raw into BQL `WHERE account = '{...}'`.
The 8-char hex prefix is safe in practice; arbitrary `account_name`
input is not.
**Fix:** validate against `^[A-Za-z0-9:_-]+$` before interpolation.
### ✅ #16`approve_manual_payment_request` not status-guarded
`crud.py:559-579` overwrites `status='approved'` regardless of current
state. Two concurrent admins → two journal entries.
**Fix:** `UPDATE ... WHERE id=:id AND status='pending'`, check
`rowcount == 1`.
### ✅ #17 — Account name not validated on receivable/revenue/expense
`views_api.py:1066-1074, 1224-1236, 1369-1376, 1477-1494` accept
free-string `data.expense_account` (etc.) with no Beancount-syntax
check before lookup.
**Fix:** enforce `^[A-Z][A-Za-z0-9:-]*$`.
### ✅ #18`_get_username_from_user_id` creates a fresh LNbits DB per call
`views_api.py:697-708` opens an LNbits DB inside a per-row hot path.
**Fix:** cache the Database instance at module load; batch-load
usernames once per request via single `IN (…)` query.
### ✅ #19`get_user_balance` regex rejects decimal SATS
`fava_client.py:346, 503` patterns require `(-?\d+)` SATS. Fava's
`@@→@` normalisation can emit decimal SATS.
**Fix:** `(-?[\d.]+)`.
### ⏳ #20`fava_url` default `http://localhost:3333` and sandboxed lnbits
Loopback breaks if the lnbits service unit gains `PrivateNetwork=true`.
Not a code bug — worth documenting in deploy assumptions.
---
## LOW
- ⏳ `tasks.py` mixes `print()` with `logger.*` (`:61, 65-69, 81, 89,
94, 162`).
- ⏳ Dead model imports in `crud.py:21-27` (`JournalEntry`,
`EntryLine`) after `entry_lines` table dropped.
- ⏳ `auto_assign_default_role` check-then-act race
(`crud.py:1609-1641`) — add UNIQUE constraint on
`user_roles(user_id, role_id)`.
- ⏳ Pydantic v1 `.dict()` calls (`crud.py:381, 400, 411, 450`) if
upstream is on v2.
- ⏳ `account_utils.parse_legacy_account_name` splits on ` - `
fragile if ever called on user input.
- ⏳ `Account.is_active` vs `is_virtual` default-filter inconsistency
hides virtual parents in permission-grant UI (`crud.py:146-167`).
---
## 🚫 Downgraded (initially flagged, verified not a bug)
### Subagent A — "inverted balance sign in tasks.py:251-258"
`fava_client.py:303` docstring says positive = user owes libra
(bookkeeper perspective). `tasks.py:249-250` matches. CLAUDE.md
describes the *user's* perspective (positive = libra owes user), which
is consistent at the UI layer. Both representations are internally
coherent — no bug, just a doc-vs-code perspective collision.
### Subagent A — "fava_ledger_slug default doesn't match deploy"
Verified empirically by user: Fava in single-ledger mode appears to
accept arbitrary slugs against the JSON API, and the deploy's seed
title `"Libra Ledger"` → slugify → `libra-ledger` matches the
extension default in `models.py:159` anyway. False alarm.
---
## Triage order (when picking the next item)
1. **#2 (unbalanced net settlement) + #4 (idempotency)** — silently
corrupts the ledger on every partial Lightning payment + every
restart with a persisted invoice queue. Real-money blast radius.
2. **#3 (migrations) + #12** — guaranteed boot crash on the documented
failure mode; bricks the extension.
3. **#8 (float in fiat metadata)** — every entry written today carries
float-drift cost basis into Beancount.
4. **#9 (silent listener death)** — operational; the kind of bug
discovered when nobody can pay for a week.
5. **#5, #6 (auth narrowing)** — residual privilege risk; smaller blast
than #1 (already fixed) but worth closing.
6. Everything else, in arbitrary order; mostly hygiene.
---
## Commits applied
| Commit | Repo / branch | What |
|---|---|---|
| `1201557` | `aiolabs/libra` `main` | Gate cross-user admin endpoints behind `require_super_user`; delete duplicate `api_pay_user` |
| `4c704e5` | `aiolabs/webapp` `dev` | Delete orphaned `PermissionManager.vue` + `GrantPermissionDialog.vue` + 3 API methods + 4 dead types |

View file

@ -1,200 +0,0 @@
# Phase 1 Implementation - Complete ✅
## Summary
We've successfully implemented the core improvements from Phase 1 of the Beancount patterns adoption:
## ✅ Completed
### 1. **Decimal Instead of Float for Fiat Amounts**
- **Files Changed:**
- `models.py`: Changed all fiat amount fields from `float` to `Decimal`
- `ExpenseEntry.amount`
- `ReceivableEntry.amount`
- `RevenueEntry.amount`
- `UserBalance.fiat_balances` dictionary values
- `crud.py`: Updated fiat balance calculations to use `Decimal`
- `views_api.py`: Store fiat amounts as strings with `str(amount.quantize(Decimal("0.001")))`
- **Benefits:**
- Prevents floating point rounding errors
- Exact decimal arithmetic
- Financial-grade precision
### 2. **Meta Field for Journal Entries**
- **Database Migration:** `m005_add_flag_and_meta`
- Added `meta TEXT DEFAULT '{}'` column to `journal_entries` table
- **Model Changes:**
- Added `meta: dict = {}` to `JournalEntry` and `CreateJournalEntry`
- Meta stores: source, created_via, user_id, payment_hash, etc.
- **CRUD Updates:**
- `create_journal_entry()` now stores meta as JSON
- `get_journal_entries_by_user()` parses meta from JSON
- **API Integration:**
- Expense entries: `{"source": "api", "created_via": "expense_entry", "user_id": "...", "is_equity": false}`
- Receivable entries: `{"source": "api", "created_via": "receivable_entry", "debtor_user_id": "..."}`
- Payment entries: `{"source": "lightning_payment", "created_via": "record_payment", "payment_hash": "...", "payer_user_id": "..."}`
- **Benefits:**
- Full audit trail for every transaction
- Source tracking (where did this entry come from?)
- Can add tags, links, notes in future
- Essential for compliance and debugging
### 3. **Flag Field for Transaction Status**
- **Database Migration:** `m005_add_flag_and_meta`
- Added `flag TEXT DEFAULT '*'` column to `journal_entries` table
- **Model Changes:**
- Created `JournalEntryFlag` enum:
- `*` = CLEARED (confirmed/reconciled)
- `!` = PENDING (awaiting confirmation)
- `#` = FLAGGED (needs review)
- `x` = VOID (cancelled)
- Added `flag: JournalEntryFlag` to `JournalEntry` and `CreateJournalEntry`
- **CRUD Updates:**
- `create_journal_entry()` stores flag as string value
- `get_journal_entries_by_user()` converts string to enum
- **API Logic:**
- Expense entries: Default to CLEARED (immediately confirmed)
- Receivable entries: Start as PENDING (unpaid debt)
- Payment entries: Mark as CLEARED (payment received)
- **Benefits:**
- Visual indication of transaction status in UI
- Filter transactions by status
- Supports reconciliation workflows
- Standard accounting practice (Beancount-style)
## 📊 Migration Details
**Migration `m005_add_flag_and_meta`:**
```sql
ALTER TABLE journal_entries ADD COLUMN flag TEXT DEFAULT '*';
ALTER TABLE journal_entries ADD COLUMN meta TEXT DEFAULT '{}';
```
**To Apply:**
1. Stop LNbits server (if running)
2. Restart LNbits - migration runs automatically
3. Check logs for "m005_add_flag_and_meta" success message
## 🔧 Technical Implementation Details
### Decimal Handling
```python
# Store as string for precision
metadata = {
"fiat_amount": str(data.amount.quantize(Decimal("0.001"))),
}
# Parse back to Decimal
fiat_decimal = Decimal(str(fiat_amount))
```
### Flag Handling
```python
# Set flag on creation
entry_data = CreateJournalEntry(
flag=JournalEntryFlag.PENDING, # or CLEARED
# ...
)
# Parse from database
flag = JournalEntryFlag(entry_data.get("flag", "*"))
```
### Meta Handling
```python
# Create with meta
entry_meta = {
"source": "api",
"created_via": "expense_entry",
"user_id": wallet.wallet.user,
}
entry_data = CreateJournalEntry(
meta=entry_meta,
# ...
)
# Parse from database
meta = json.loads(entry_data.get("meta", "{}")) if entry_data.get("meta") else {}
```
## 🎯 What's Next (Remaining Phase 1 Items)
### Hierarchical Account Naming (In Progress)
Implement Beancount-style account hierarchy:
- Current: `"Accounts Receivable - af983632"`
- Better: `"Assets:Receivable:User-af983632"`
### UI Updates for Flags
Display flag icons in transaction list:
- ✅ `*` = Green checkmark (cleared)
- ⚠️ `!` = Yellow/Orange badge (pending)
- 🚩 `#` = Red flag (needs review)
- ❌ `x` = Strikethrough (voided)
## 🧪 Testing Recommendations
1. **Test Decimal Precision:**
```python
# Create expense with fiat amount
POST /api/v1/entries/expense
{"amount": "36.93", "currency": "EUR", ...}
# Verify stored as exact string
SELECT metadata FROM entry_lines WHERE ...
# Should see: {"fiat_amount": "36.930", ...}
```
2. **Test Flag Workflow:**
```python
# Create receivable (should be PENDING)
POST /api/v1/entries/receivable
# Check: flag = '!'
# Pay receivable (creates CLEARED entry)
POST /api/v1/record-payment
# Check: payment entry flag = '*'
```
3. **Test Meta Audit Trail:**
```python
# Create any entry
# Check database:
SELECT meta FROM journal_entries WHERE ...
# Should see: {"source": "api", "created_via": "...", ...}
```
## 🎉 Success Metrics
- ✅ No more floating point errors in fiat calculations
- ✅ Every transaction has source tracking
- ✅ Transaction status is visible (pending vs cleared)
- ✅ Database migration successful
- ✅ All API endpoints updated
- ✅ CRUD operations handle new fields
## 📝 Notes
- **Backward Compatibility:** Old entries will have default values (`flag='*'`, `meta='{}'`)
- **Performance:** No impact - added columns have defaults and indexes not needed yet
- **Storage:** Minimal increase (meta typically < 200 bytes per entry)
## ✅ Phase 1 Complete!
All Phase 1 tasks have been completed:
1. ✅ Decimal instead of float for fiat amounts
2. ✅ Meta field for journal entries (audit trail)
3. ✅ Flag field for transaction status
4. ✅ Hierarchical account naming (Beancount-style)
5. ✅ UI updated to display flags and metadata
**Next:** Move to Phase 2 (Core logic refactoring) when ready.

View file

@ -1,273 +0,0 @@
# Phase 2: Reconciliation - COMPLETE ✅
## Summary
Phase 2 of the Beancount-inspired refactor focused on **reconciliation and automated balance checking**. This phase builds on Phase 1's foundation to provide robust reconciliation tools that ensure accounting accuracy and catch discrepancies early.
## Completed Features
### 1. Balance Assertions ✅
**Purpose**: Verify account balances match expected values at specific points in time (like Beancount's `balance` directive)
**Implementation**:
- **Models** (`models.py:184-219`):
- `AssertionStatus` enum (pending, passed, failed)
- `BalanceAssertion` model with sats and optional fiat checks
- `CreateBalanceAssertion` request model
- **Database** (`migrations.py:275-320`):
- `balance_assertions` table with expected/actual balance tracking
- Tolerance levels for flexible matching
- Status tracking and timestamps
- Indexes for performance
- **CRUD** (`crud.py:773-981`):
- `create_balance_assertion()` - Create and store assertion
- `get_balance_assertion()` - Fetch single assertion
- `get_balance_assertions()` - List with filters
- `check_balance_assertion()` - Compare expected vs actual
- `delete_balance_assertion()` - Remove assertion
- **API Endpoints** (`views_api.py:1067-1230`):
- `POST /api/v1/assertions` - Create and check assertion
- `GET /api/v1/assertions` - List assertions with filters
- `GET /api/v1/assertions/{id}` - Get specific assertion
- `POST /api/v1/assertions/{id}/check` - Re-check assertion
- `DELETE /api/v1/assertions/{id}` - Delete assertion
- **UI** (`templates/libra/index.html:254-378`):
- Balance Assertions card (super user only)
- Failed assertions prominently displayed with red banner
- Passed assertions in collapsible panel
- Create assertion dialog with validation
- Re-check and delete buttons
- **Frontend** (`static/js/index.js:70-79, 602-726`):
- Data properties and computed values
- CRUD methods for assertions
- Automatic loading on page load
### 2. Reconciliation API Endpoints ✅
**Purpose**: Provide comprehensive reconciliation tools and reporting
**Implementation**:
- **Summary Endpoint** (`views_api.py:1236-1287`):
- `GET /api/v1/reconciliation/summary`
- Returns counts of assertions by status
- Returns counts of journal entries by flag
- Total accounts count
- Last checked timestamp
- **Check All Endpoint** (`views_api.py:1290-1325`):
- `POST /api/v1/reconciliation/check-all`
- Re-checks all balance assertions
- Returns summary of results (passed/failed/errors)
- Useful for manual reconciliation runs
- **Discrepancies Endpoint** (`views_api.py:1328-1357`):
- `GET /api/v1/reconciliation/discrepancies`
- Returns all failed assertions
- Returns all flagged journal entries
- Returns all pending entries
- Total discrepancy count
### 3. Reconciliation UI Dashboard ✅
**Purpose**: Visual dashboard for reconciliation status and quick access to reconciliation tools
**Implementation** (`templates/libra/index.html:380-499`):
- **Summary Cards**:
- Balance Assertions stats (total, passed, failed, pending)
- Journal Entries stats (total, cleared, pending, flagged)
- Total Accounts count with last checked timestamp
- **Discrepancies Alert**:
- Warning banner when discrepancies found
- Shows count of failed assertions and flagged entries
- "View Details" button to expand discrepancy list
- **Discrepancy Details**:
- Failed assertions list with expected vs actual balances
- Flagged entries list
- Quick access to problematic transactions
- **Actions**:
- "Check All" button to run full reconciliation
- Loading states during checks
- Success message when all accounts reconciled
**Frontend** (`static/js/index.js:80-85, 727-779, 933-934`):
- Reconciliation data properties
- Methods to load summary and discrepancies
- `runFullReconciliation()` method with notifications
- Automatic loading on page load for super users
### 4. Automated Daily Balance Checks ✅
**Purpose**: Run balance checks automatically on a schedule to catch discrepancies early
**Implementation**:
- **Tasks Module** (`tasks.py`):
- `check_all_balance_assertions()` - Core checking logic
- `scheduled_daily_reconciliation()` - Scheduled wrapper
- Results logging and reporting
- Error handling
- **API Endpoint** (`views_api.py:1363-1390`):
- `POST /api/v1/tasks/daily-reconciliation`
- Can be triggered manually or via cron
- Returns detailed results
- Super user only
- **Documentation** (`DAILY_RECONCILIATION.md`):
- Comprehensive setup guide
- Multiple scheduling options (cron, systemd, k8s)
- Monitoring and troubleshooting
- Best practices
- Example scripts
## Benefits
### Accounting Accuracy
- ✅ Catch data entry errors early
- ✅ Verify balances at critical checkpoints
- ✅ Build confidence in accounting accuracy
- ✅ Required for external audits
### Operational Excellence
- ✅ Automated daily checks reduce manual work
- ✅ Dashboard provides at-a-glance reconciliation status
- ✅ Discrepancies are immediately visible
- ✅ Historical tracking of assertions
### Developer Experience
- ✅ Clean API for programmatic reconciliation
- ✅ Well-documented scheduling options
- ✅ Flexible tolerance levels
- ✅ Comprehensive error reporting
## File Changes
### New Files Created
1. `tasks.py` - Background tasks for automated reconciliation
2. `DAILY_RECONCILIATION.md` - Setup and scheduling documentation
3. `PHASE2_COMPLETE.md` - This file
### Modified Files
1. `models.py` - Added `BalanceAssertion`, `CreateBalanceAssertion`, `AssertionStatus`
2. `migrations.py` - Added `m007_balance_assertions` migration
3. `crud.py` - Added balance assertion CRUD operations
4. `views_api.py` - Added assertion, reconciliation, and task endpoints
5. `templates/libra/index.html` - Added assertions and reconciliation UI
6. `static/js/index.js` - Added assertion and reconciliation functionality
7. `BEANCOUNT_PATTERNS.md` - Updated roadmap to mark Phase 2 complete
## API Endpoints Summary
### Balance Assertions
- `POST /api/v1/assertions` - Create assertion
- `GET /api/v1/assertions` - List assertions
- `GET /api/v1/assertions/{id}` - Get assertion
- `POST /api/v1/assertions/{id}/check` - Re-check assertion
- `DELETE /api/v1/assertions/{id}` - Delete assertion
### Reconciliation
- `GET /api/v1/reconciliation/summary` - Get reconciliation summary
- `POST /api/v1/reconciliation/check-all` - Check all assertions
- `GET /api/v1/reconciliation/discrepancies` - Get discrepancies
### Automated Tasks
- `POST /api/v1/tasks/daily-reconciliation` - Run daily reconciliation check
## Usage Examples
### Create a Balance Assertion
```bash
curl -X POST http://localhost:5000/libra/api/v1/assertions \
-H "X-Api-Key: ADMIN_KEY" \
-H "Content-Type: application/json" \
-d '{
"account_id": "lightning",
"expected_balance_sats": 268548,
"tolerance_sats": 100
}'
```
### Get Reconciliation Summary
```bash
curl http://localhost:5000/libra/api/v1/reconciliation/summary \
-H "X-Api-Key: ADMIN_KEY"
```
### Run Full Reconciliation
```bash
curl -X POST http://localhost:5000/libra/api/v1/reconciliation/check-all \
-H "X-Api-Key: ADMIN_KEY"
```
### Schedule Daily Reconciliation (Cron)
```bash
# Add to crontab
0 2 * * * curl -X POST http://localhost:5000/libra/api/v1/tasks/daily-reconciliation -H "X-Api-Key: ADMIN_KEY"
```
## Testing Checklist
- [x] Create balance assertion (UI)
- [x] Create balance assertion (API)
- [x] Assertion passes when balance matches
- [x] Assertion fails when balance doesn't match
- [x] Tolerance levels work correctly
- [x] Fiat balance assertions work
- [x] Re-check assertion updates status
- [x] Delete assertion removes it
- [x] Reconciliation summary shows correct stats
- [x] Check all assertions endpoint works
- [x] Discrepancies endpoint returns correct data
- [x] Dashboard displays summary correctly
- [x] Discrepancy alert shows when issues exist
- [x] "Check All" button triggers reconciliation
- [x] Daily reconciliation task executes successfully
- [x] Failed assertions are logged
- [x] All endpoints require super user access
## Next Steps
**Phase 3: Core Logic Refactoring (Medium Priority)**
- Create `core/` module with pure accounting logic
- Implement `LibraInventory` for position tracking
- Move balance calculation to `core/balance.py`
- Add comprehensive validation in `core/validation.py`
**Phase 4: Validation Plugins (Medium Priority)**
- Create plugin system architecture
- Implement `check_balanced` plugin
- Implement `check_receivables` plugin
- Add plugin configuration UI
**Phase 5: Advanced Features (Low Priority)**
- Add tags and links to entries
- Implement query language
- Add lot tracking to inventory
- Support multi-currency in single entry
## Conclusion
Phase 2 successfully implements Beancount's reconciliation philosophy in the Libra extension. With balance assertions, comprehensive reconciliation APIs, a visual dashboard, and automated daily checks, users can:
- **Trust their data** with automated verification
- **Catch errors early** through regular reconciliation
- **Save time** with automated daily checks
- **Gain confidence** in their accounting accuracy
The implementation follows Beancount's best practices while adapting to LNbits' architecture and use case. All reconciliation features are admin-only, ensuring proper access control for sensitive accounting operations.
**Phase 2 Status**: ✅ COMPLETE
---
*Generated: 2025-10-23*
*Next: Phase 3 - Core Logic Refactoring*

View file

@ -1,365 +0,0 @@
# Phase 3: Core Logic Refactoring - COMPLETE ✅
## Summary
Phase 3 of the Beancount-inspired refactor focused on **separating business logic from database operations** and creating a clean, testable core module. This phase improves code quality, maintainability, and follows best practices from Beancount's architecture.
## Completed Features
### 1. Core Module Structure ✅
**Purpose**: Separate pure accounting logic from database and API concerns
**Implementation** (`core/__init__.py`):
- Created `core/` module package
- Exports main classes and functions
- Clean separation of concerns
**Benefits**:
- Testable without database
- Reusable across different storage backends
- Easier to audit and verify
- Clear architecture
### 2. LibraInventory for Position Tracking ✅
**Purpose**: Track balances across multiple currencies with cost basis information (following Beancount's Inventory pattern)
**Implementation** (`core/inventory.py`):
**LibraPosition** (Lines 11-84):
- Immutable dataclass representing a single position
- Tracks currency, amount, cost basis, and metadata
- Supports addition and negation operations
- Automatic Decimal conversion in `__post_init__`
```python
@dataclass(frozen=True)
class LibraPosition:
currency: str # "SATS", "EUR", "USD"
amount: Decimal
cost_currency: Optional[str] = None
cost_amount: Optional[Decimal] = None
date: Optional[datetime] = None
metadata: Dict[str, Any] = field(default_factory=dict)
```
**LibraInventory** (Lines 87-201):
- Container for multiple positions
- Positions keyed by `(currency, cost_currency)` tuple
- Methods for querying balances:
- `get_balance_sats()` - Total satoshis
- `get_balance_fiat(currency)` - Fiat balance for specific currency
- `get_all_fiat_balances()` - All fiat balances
- Utility methods:
- `is_empty()` - Check if no positions
- `is_zero()` - Check if all positions sum to zero
- `to_dict()` - Export to dictionary
### 3. BalanceCalculator ✅
**Purpose**: Pure logic for calculating balances from journal entries
**Implementation** (`core/balance.py`):
**AccountType Enum** (Lines 13-19):
```python
class AccountType(str, Enum):
ASSET = "asset"
LIABILITY = "liability"
EQUITY = "equity"
REVENUE = "revenue"
EXPENSE = "expense"
```
**BalanceCalculator Class** (Lines 22-217):
**Static Methods**:
1. **`calculate_account_balance()`** (Lines 29-54):
- Calculate balance based on account type
- Normal balances:
- Assets/Expenses: Debit balance (debit - credit)
- Liabilities/Equity/Revenue: Credit balance (credit - debit)
2. **`build_inventory_from_entry_lines()`** (Lines 56-117):
- Build LibraInventory from journal entry lines
- Handles both sats and fiat currency tracking
- Accounts for account type when determining sign
3. **`calculate_user_balance()`** (Lines 119-168):
- Calculate user's total balance across all accounts
- Returns both sats balance and fiat balances by currency
- Properly handles asset (receivable) vs liability (payable) accounts
4. **`check_balance_matches()`** (Lines 170-187):
- Verify balance assertion for sats
5. **`check_fiat_balance_matches()`** (Lines 189-202):
- Verify balance assertion for fiat currency
### 4. Comprehensive Validation ✅
**Purpose**: Validation rules for accounting operations
**Implementation** (`core/validation.py`):
**ValidationError Exception** (Lines 10-18):
- Custom exception for validation failures
- Includes detailed error information
**Validation Functions**:
1. **`validate_journal_entry()`** (Lines 21-124):
- Checks:
- At least 2 lines (double-entry requirement)
- Entry is balanced (debits = credits)
- Valid amounts (non-negative)
- No line has both debit and credit
- All lines have account_id
2. **`validate_balance()`** (Lines 127-177):
- Validates balance assertions
- Checks both sats and fiat within tolerance
3. **`validate_receivable_entry()`** (Lines 180-199):
- Validates receivable (user owes libra) entries
- Ensures positive amount
- Ensures revenue account type
4. **`validate_expense_entry()`** (Lines 202-227):
- Validates expense entries
- Ensures positive amount
- Checks account type (expense or equity)
5. **`validate_payment_entry()`** (Lines 230-245):
- Validates payment entries
- Ensures positive amount
6. **`validate_metadata()`** (Lines 248-284):
- Validates entry line metadata
- Checks for required keys
- Validates fiat currency/amount consistency
- Validates Decimal conversion
### 5. Refactored CRUD Operations ✅
**Purpose**: Use core logic in database operations
**Modified Files**: `crud.py`
**Changes**:
1. **Imports** (Lines 26-36):
- Import core accounting logic
- Import validation functions
2. **`get_account_balance()`** (Lines 347-377):
- Refactored to use `BalanceCalculator.calculate_account_balance()`
- Removed duplicate logic
3. **`get_user_balance()`** (Lines 380-435):
- Completely refactored to use:
- `BalanceCalculator.build_inventory_from_entry_lines()`
- `BalanceCalculator.calculate_user_balance()`
- Cleaner separation of database queries vs business logic
4. **`get_all_user_balances()`** (Lines 438-459):
- Simplified to call `get_user_balance()` for each user
- Eliminates code duplication
## Architecture
### Before Phase 3
```
views_api.py → crud.py (mixed DB + logic)
database
```
All accounting logic was embedded in crud.py alongside database operations.
### After Phase 3
```
views_api.py → crud.py → core/
↓ ↓
database Pure Logic
(testable)
```
**Separation of Concerns**:
- `core/` - Pure accounting logic (no DB dependencies)
- `crud.py` - Database operations + orchestration
- `views_api.py` - HTTP API layer
## Benefits
### Code Quality
- ✅ **Testability**: Core logic can be tested without database
- ✅ **Maintainability**: Clear separation makes code easier to understand
- ✅ **Reusability**: Core logic can be used in different contexts
- ✅ **Consistency**: Centralized accounting rules
### Developer Experience
- ✅ **Type Safety**: Immutable dataclasses with proper types
- ✅ **Documentation**: Well-documented core functions
- ✅ **Debugging**: Easier to trace accounting logic
- ✅ **Refactoring**: Safer to make changes
### Reliability
- ✅ **Validation**: Comprehensive validation rules
- ✅ **Correctness**: Pure functions easier to verify
- ✅ **Auditability**: Clear accounting rules
## File Structure
```
lnbits/extensions/libra/
├── core/
│ ├── __init__.py # Module exports
│ ├── inventory.py # LibraInventory, LibraPosition
│ ├── balance.py # BalanceCalculator
│ └── validation.py # Validation functions
├── crud.py # DB operations (refactored to use core/)
├── models.py # Pydantic models
├── views_api.py # API endpoints
└── PHASE3_COMPLETE.md # This file
```
## Usage Examples
### Using LibraInventory
```python
from decimal import Decimal
from libra.core.inventory import LibraInventory, LibraPosition
# Create inventory
inv = LibraInventory()
# Add positions
inv.add_position(LibraPosition(
currency="SATS",
amount=Decimal("100000")
))
inv.add_position(LibraPosition(
currency="SATS",
amount=Decimal("50000"),
cost_currency="EUR",
cost_amount=Decimal("25.00")
))
# Query balances
total_sats = inv.get_balance_sats() # Decimal("150000")
eur_balance = inv.get_balance_fiat("EUR") # Decimal("25.00")
# Export
data = inv.to_dict()
# {"sats": 150000, "fiat": {"EUR": 25.00}}
```
### Using BalanceCalculator
```python
from libra.core.balance import BalanceCalculator, AccountType
# Calculate account balance
balance = BalanceCalculator.calculate_account_balance(
total_debit=100000,
total_credit=50000,
account_type=AccountType.ASSET
)
# Returns: 50000 (debit balance for asset)
# Build inventory from entry lines
entry_lines = [
{"amount": 100000, "metadata": '{"fiat_currency": "EUR", "fiat_amount": "50.00"}'}, # Positive = debit
{"amount": -50000, "metadata": "{}"} # Negative = credit
]
inventory = BalanceCalculator.build_inventory_from_entry_lines(
entry_lines,
AccountType.ASSET
)
# Check balance matches
is_valid = BalanceCalculator.check_balance_matches(
actual_balance_sats=100000,
expected_balance_sats=99900,
tolerance_sats=100
)
# Returns: True (within tolerance)
```
### Using Validation
```python
from libra.core.validation import validate_journal_entry, ValidationError
entry = {
"id": "abc123",
"description": "Test entry",
"entry_date": datetime.now()
}
entry_lines = [
{"account_id": "acc1", "amount": 100000}, # Positive = debit
{"account_id": "acc2", "amount": -100000} # Negative = credit
]
try:
validate_journal_entry(entry, entry_lines)
print("Valid!")
except ValidationError as e:
print(f"Invalid: {e.message}")
print(f"Details: {e.details}")
```
## Testing Checklist
- [x] LibraInventory created and tested
- [x] LibraPosition addition works
- [x] Inventory balance calculations work
- [x] BalanceCalculator account balance calculation works
- [x] BalanceCalculator inventory building works
- [x] BalanceCalculator user balance calculation works
- [x] Validation functions work
- [x] crud.py refactored to use core logic
- [x] Existing balance calculations still work
- [ ] Unit tests for core module (future work)
## Next Steps
**Phase 4: Validation Plugins** (Medium Priority)
- Create plugin system architecture
- Implement `check_balanced` plugin
- Implement `check_receivables` plugin
- Add plugin configuration UI
**Future Enhancements**:
- Add unit tests for core/ module
- Add integration tests
- Add lot tracking to inventory
- Support multi-currency in single entry
- Add more validation plugins
## Conclusion
Phase 3 successfully refactors Libra's accounting logic into a clean, testable core module. By following Beancount's architecture patterns, we've created:
- **Pure accounting logic** separated from database concerns
- **LibraInventory** for position tracking across currencies
- **BalanceCalculator** for consistent balance calculations
- **Comprehensive validation** for data integrity
The refactoring improves code quality, maintainability, and sets the foundation for Phase 4's plugin system.
**Phase 3 Status**: ✅ COMPLETE
---
*Generated: 2025-10-23*
*Next: Phase 4 - Validation Plugins*