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:
parent
c0d371036b
commit
ec6cac51f0
19 changed files with 454 additions and 2942 deletions
|
|
@ -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
|
||||
Libra’s 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 * "room (200.00 EUR)" #receivable-entry
|
||||
user-id: "375ec158"
|
||||
source: "libra-api"
|
||||
sats-amount: "225033"
|
||||
Assets:Receivable:User-375ec158 200.00 EUR
|
||||
sats-equivalent: "225033"
|
||||
Income:Accommodation:Guests -200.00 EUR
|
||||
sats-equivalent: "225033"
|
||||
|
||||
; Step 2: Lightning Payment Received
|
||||
2025-11-12 * "Lightning payment settlement from user 375ec158"
|
||||
#lightning-payment #net-settlement
|
||||
user-id: "375ec158"
|
||||
source: "lightning_payment"
|
||||
payment-type: "net-settlement"
|
||||
payment-hash: "8d080ec4cc4301715535004156085dd50c159185..."
|
||||
Assets:Bitcoin:Lightning 225033 SATS @ 0.0008887585... EUR
|
||||
payment-hash: "8d080ec4cc4301715535004156085dd50c159185..."
|
||||
Assets:Receivable:User-375ec158 -200.00 EUR
|
||||
sats-equivalent: "225033"
|
||||
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">"account"</span>: payment_account,</span>
|
||||
<span id="cb2-5"><a href="#cb2-5" aria-hidden="true" tabindex="-1"></a> <span class="st">"amount"</span>: <span class="ss">f"</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">"</span>,</span>
|
||||
<span id="cb2-6"><a href="#cb2-6" aria-hidden="true" tabindex="-1"></a> <span class="st">"meta"</span>: {<span class="st">"payment-hash"</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">"account"</span>: receivable_account,</span>
|
||||
<span id="cb2-10"><a href="#cb2-10" aria-hidden="true" tabindex="-1"></a> <span class="st">"amount"</span>: <span class="ss">f"-</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">"</span>,</span>
|
||||
<span id="cb2-11"><a href="#cb2-11" aria-hidden="true" tabindex="-1"></a> <span class="st">"meta"</span>: {<span class="st">"sats-equivalent"</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">"account"</span>: payable_account,</span>
|
||||
<span id="cb2-15"><a href="#cb2-15" aria-hidden="true" tabindex="-1"></a> <span class="st">"amount"</span>: <span class="ss">f"</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">"</span>,</span>
|
||||
<span id="cb2-16"><a href="#cb2-16" aria-hidden="true" tabindex="-1"></a> <span class="st">"meta"</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>: > “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">"account"</span>: payment_account, <span class="st">"amount"</span>: ...},</span>
|
||||
<span id="cb4-4"><a href="#cb4-4" aria-hidden="true" tabindex="-1"></a> {<span class="st">"account"</span>: receivable_account, <span class="st">"amount"</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'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">></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">"account"</span>: payable_account,</span>
|
||||
<span id="cb4-11"><a href="#cb4-11" aria-hidden="true" tabindex="-1"></a> <span class="st">"amount"</span>: <span class="ss">f"</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">"</span>,</span>
|
||||
<span id="cb4-12"><a href="#cb4-12" aria-hidden="true" tabindex="-1"></a> <span class="st">"meta"</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: "225033"</code></pre></li>
|
||||
</ol>
|
||||
<p><strong>Why This Is Problematic</strong>: - The <code>@@</code>
|
||||
notation already records the exact satoshi amount - Beancount’s 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">'Assets:Bitcoin:Lightning'</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: "8d080ec4..."
|
||||
Assets:Receivable:User-375ec158 -200.00 EUR
|
||||
; No sats-equivalent needed here</code></pre>
|
||||
<p><strong>Option B - Use EUR positions with metadata</strong> (Libra’s
|
||||
current approach):</p>
|
||||
<pre class="beancount"><code>Assets:Bitcoin:Lightning 200.00 EUR
|
||||
sats-received: "225033"
|
||||
payment-hash: "8d080ec4..."
|
||||
Assets:Receivable:User-375ec158 -200.00 EUR
|
||||
sats-cleared: "225033"</code></pre>
|
||||
<p><strong>Don’t</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 * "Lightning payment with exchange loss"
|
||||
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>: > “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>: Beancount’s price
|
||||
database now contains “prices” that aren’t real market prices</li>
|
||||
<li><strong>Auditor Confusion</strong>: An auditor reviewing this would
|
||||
question why purchase prices don’t 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: "payment-received"
|
||||
Revenue:Exchange-Gain 0.50 EUR
|
||||
Assets:Receivable:User-375ec158 -200.00 EUR
|
||||
|
||||
; Approach 2: Don't use @ notation at all
|
||||
Assets:Bitcoin:Lightning 200.00 EUR
|
||||
sats-received: "225033"
|
||||
fmv-at-receipt: "199.50 EUR"
|
||||
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 it’s used for simple
|
||||
payments that aren’t true net settlements.</p>
|
||||
<p><strong>Example from User’s 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 * "Bitcoin payment from user 375ec158"
|
||||
Assets:Bitcoin:Lightning 199.50 EUR
|
||||
sats-received: "225033"
|
||||
fmv-per-sat: "0.000886 EUR"
|
||||
cost-basis: "199.50 EUR"
|
||||
payment-hash: "8d080ec4..."
|
||||
Revenue:Exchange-Gain 0.50 EUR
|
||||
source: "cryptocurrency-receipt"
|
||||
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 * "Bitcoin payment from user 375ec158"
|
||||
Assets:Bitcoin:Lightning 200.00 EUR
|
||||
sats-received: "225033"
|
||||
sats-rate: "1125.165"
|
||||
payment-hash: "8d080ec4..."
|
||||
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 * "Net settlement via Lightning"
|
||||
; User owes 555 EUR, Libra owes 38 EUR, net: 517 EUR
|
||||
Assets:Bitcoin:Lightning 517.00 EUR
|
||||
sats-received: "565251"
|
||||
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 you’re 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">"account"</span>: payable_account,</span>
|
||||
<span id="cb23-6"><a href="#cb23-6" aria-hidden="true" tabindex="-1"></a> <span class="st">"amount"</span>: <span class="ss">f"</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">"</span>,</span>
|
||||
<span id="cb23-7"><a href="#cb23-7" aria-hidden="true" tabindex="-1"></a> <span class="st">"meta"</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">"account"</span>: payment_account,</span>
|
||||
<span id="cb24-4"><a href="#cb24-4" aria-hidden="true" tabindex="-1"></a> <span class="st">"amount"</span>: <span class="ss">f"</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">"</span>,</span>
|
||||
<span id="cb24-5"><a href="#cb24-5" aria-hidden="true" tabindex="-1"></a> <span class="st">"meta"</span>: {<span class="st">"payment-hash"</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">"account"</span>: receivable_account,</span>
|
||||
<span id="cb24-9"><a href="#cb24-9" aria-hidden="true" tabindex="-1"></a> <span class="st">"amount"</span>: <span class="ss">f"-</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">"</span>,</span>
|
||||
<span id="cb24-10"><a href="#cb24-10" aria-hidden="true" tabindex="-1"></a> <span class="st">"meta"</span>: {<span class="st">"sats-equivalent"</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'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">></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">"account"</span>: payable_account,</span>
|
||||
<span id="cb24-18"><a href="#cb24-18" aria-hidden="true" tabindex="-1"></a> <span class="st">"amount"</span>: <span class="ss">f"</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">"</span>,</span>
|
||||
<span id="cb24-19"><a href="#cb24-19" aria-hidden="true" tabindex="-1"></a> <span class="st">"meta"</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">"account"</span>: payment_account,</span>
|
||||
<span id="cb25-5"><a href="#cb25-5" aria-hidden="true" tabindex="-1"></a> <span class="st">"amount"</span>: <span class="ss">f"</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">"</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">"meta"</span>: {</span>
|
||||
<span id="cb25-7"><a href="#cb25-7" aria-hidden="true" tabindex="-1"></a> <span class="st">"sats-received"</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">"payment-hash"</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">"account"</span>: receivable_account,</span>
|
||||
<span id="cb25-13"><a href="#cb25-13" aria-hidden="true" tabindex="-1"></a> <span class="st">"amount"</span>: <span class="ss">f"-</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">"</span>,</span>
|
||||
<span id="cb25-14"><a href="#cb25-14" aria-hidden="true" tabindex="-1"></a> <span class="st">"meta"</span>: {<span class="st">"sats-cleared"</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">"account"</span>: payment_account,</span>
|
||||
<span id="cb26-5"><a href="#cb26-5" aria-hidden="true" tabindex="-1"></a> <span class="st">"amount"</span>: <span class="ss">f"</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">"</span>,</span>
|
||||
<span id="cb26-6"><a href="#cb26-6" aria-hidden="true" tabindex="-1"></a> <span class="st">"meta"</span>: {<span class="st">"payment-hash"</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">"account"</span>: receivable_account,</span>
|
||||
<span id="cb26-10"><a href="#cb26-10" aria-hidden="true" tabindex="-1"></a> <span class="st">"amount"</span>: <span class="ss">f"-</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">"</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 Libra’s 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'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">"fiat_balances"</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">></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 (> 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">></span> Decimal(<span class="st">"0.01"</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">></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">"Revenue:Foreign-Exchange-Gain"</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">"Expenses:Foreign-Exchange-Loss"</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">"account"</span>: exchange_account,</span>
|
||||
<span id="cb27-27"><a href="#cb27-27" aria-hidden="true" tabindex="-1"></a> <span class="st">"amount"</span>: <span class="ss">f"</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">"</span>,</span>
|
||||
<span id="cb27-28"><a href="#cb27-28" aria-hidden="true" tabindex="-1"></a> <span class="st">"meta"</span>: {</span>
|
||||
<span id="cb27-29"><a href="#cb27-29" aria-hidden="true" tabindex="-1"></a> <span class="st">"sats-amount"</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">"market-rate"</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">"receivable-amount"</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">"""</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'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"> """</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">"fiat_balances"</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">></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"><</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">></span> <span class="dv">0</span> <span class="kw">and</span> payable_amount <span class="op">></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">></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 * "Payment"
|
||||
Assets:Bitcoin:Lightning 200.00 EUR
|
||||
sats-received: "225033"
|
||||
Assets:Receivable:User -200.00 EUR
|
||||
sats-cleared: "225033"</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 * "Payment"
|
||||
Assets:Bitcoin:Lightning 225033 SATS
|
||||
eur-value: "200.00"
|
||||
Assets:Receivable:User -225033 SATS
|
||||
eur-cleared: "200.00"</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 * "Payment received from user"
|
||||
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 * "Lightning payment received"
|
||||
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 * "Lightning payment from user"
|
||||
Assets:Bitcoin:Lightning 200.00 EUR
|
||||
sats-received: "225033"
|
||||
payment-hash: "8d080ec4..."
|
||||
Assets:Receivable:User -200.00 EUR
|
||||
sats-cleared: "225033"</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 * "Net settlement via Lightning"
|
||||
Assets:Bitcoin:Lightning 517.00 EUR
|
||||
sats-received: "565251"
|
||||
payment-hash: "abc123..."
|
||||
Assets:Receivable:User -555.00 EUR
|
||||
sats-portion: "565251"
|
||||
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 * "Lightning payment with exchange loss"
|
||||
Assets:Bitcoin:Lightning 199.50 EUR
|
||||
sats-received: "225033"
|
||||
market-rate: "0.000886"
|
||||
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 Libra’s use case?</strong> <strong>Yes,
|
||||
with modifications</strong>, it’s 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
|
||||
wasn’t designed for this scenario. There is no established “standard”
|
||||
for recording cryptocurrency payments of fiat-denominated receivables.
|
||||
Libra’s 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
|
||||
Libra’s payment recording system.</em></p>
|
||||
</body>
|
||||
</html>
|
||||
260
docs/CODE-REVIEW-2026-06.md
Normal file
260
docs/CODE-REVIEW-2026-06.md
Normal 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 |
|
||||
|
|
@ -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.
|
||||
|
|
@ -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*
|
||||
|
|
@ -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*
|
||||
Loading…
Add table
Add a link
Reference in a new issue