Add Docs
This commit is contained in:
parent
1d2eb05c36
commit
862fe0bfad
4 changed files with 2729 additions and 0 deletions
953
docs/ACCOUNTING-ANALYSIS-NET-SETTLEMENT.html
Normal file
953
docs/ACCOUNTING-ANALYSIS-NET-SETTLEMENT.html
Normal file
|
|
@ -0,0 +1,953 @@
|
||||||
|
<!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>: Castle 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
|
||||||
|
Castle’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>Castle 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
|
||||||
|
Castle) and payables (Castle 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: "castle-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> (Castle’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 Castle: 555.00 EUR (receivable)
|
||||||
|
Castle 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 Castle: 200.00 EUR (receivable)
|
||||||
|
Castle 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, Castle 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
|
||||||
|
Castle):</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 Castle’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: Castle 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 Castle 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:Castle 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 Castle’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.
|
||||||
|
Castle’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>Castle 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
|
||||||
|
Castle’s payment recording system.</em></p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
861
docs/ACCOUNTING-ANALYSIS-NET-SETTLEMENT.md
Normal file
861
docs/ACCOUNTING-ANALYSIS-NET-SETTLEMENT.md
Normal file
|
|
@ -0,0 +1,861 @@
|
||||||
|
# Accounting Analysis: Net Settlement Entry Pattern
|
||||||
|
|
||||||
|
**Date**: 2025-01-12
|
||||||
|
**Prepared By**: Senior Accounting Review
|
||||||
|
**Subject**: Castle Extension - Lightning Payment Settlement Entries
|
||||||
|
**Status**: Technical Review
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Executive Summary
|
||||||
|
|
||||||
|
This document provides a professional accounting assessment of Castle'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.
|
||||||
|
|
||||||
|
**Key Findings**:
|
||||||
|
- ✅ 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
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Background: The Technical Challenge
|
||||||
|
|
||||||
|
Castle operates as a Lightning Network-integrated accounting system for collectives (co-living spaces, makerspaces). It faces a unique accounting challenge:
|
||||||
|
|
||||||
|
**Scenario**: User creates a receivable in EUR (e.g., €200 for room rent), then pays via Lightning Network in satoshis (225,033 sats).
|
||||||
|
|
||||||
|
**Challenge**: 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 Castle) and payables (Castle owes them)
|
||||||
|
4. Maintaining Beancount double-entry balance
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Current Implementation
|
||||||
|
|
||||||
|
### Transaction Example
|
||||||
|
|
||||||
|
```beancount
|
||||||
|
; Step 1: Receivable Created
|
||||||
|
2025-11-12 * "room (200.00 EUR)" #receivable-entry
|
||||||
|
user-id: "375ec158"
|
||||||
|
source: "castle-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 Implementation
|
||||||
|
|
||||||
|
**Location**: `beancount_format.py:739-760`
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Build postings for net settlement
|
||||||
|
postings = [
|
||||||
|
{
|
||||||
|
"account": payment_account,
|
||||||
|
"amount": f"{abs(amount_sats)} SATS @@ {abs(net_fiat_amount):.2f} {fiat_currency}",
|
||||||
|
"meta": {"payment-hash": payment_hash} if payment_hash else {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"account": receivable_account,
|
||||||
|
"amount": f"-{abs(total_receivable_fiat):.2f} {fiat_currency}",
|
||||||
|
"meta": {"sats-equivalent": str(abs(amount_sats))}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"account": payable_account,
|
||||||
|
"amount": f"{abs(total_payable_fiat):.2f} {fiat_currency}",
|
||||||
|
"meta": {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Three-Posting Structure**:
|
||||||
|
1. **Lightning Account**: Records SATS received with `@@` total price notation
|
||||||
|
2. **Receivable Account**: Clears EUR receivable with sats-equivalent metadata
|
||||||
|
3. **Payable Account**: Clears any outstanding EUR payables (often 0.00)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Accounting Issues Identified
|
||||||
|
|
||||||
|
### Issue 1: Zero-Amount Postings
|
||||||
|
|
||||||
|
**Problem**: The third posting often records `0.00 EUR` when no payable exists.
|
||||||
|
|
||||||
|
```beancount
|
||||||
|
Liabilities:Payable:User-375ec158 0.00 EUR
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why This Is Wrong**:
|
||||||
|
- 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)
|
||||||
|
|
||||||
|
**Accounting Principle Violated**:
|
||||||
|
> "Transactions should only include postings that represent actual economic events or changes in account balances."
|
||||||
|
|
||||||
|
**Impact**: Low severity, but unprofessional presentation
|
||||||
|
|
||||||
|
**Recommendation**:
|
||||||
|
```python
|
||||||
|
# Make payable posting conditional
|
||||||
|
postings = [
|
||||||
|
{"account": payment_account, "amount": ...},
|
||||||
|
{"account": receivable_account, "amount": ...}
|
||||||
|
]
|
||||||
|
|
||||||
|
# Only add payable posting if there's actually a payable
|
||||||
|
if total_payable_fiat > 0:
|
||||||
|
postings.append({
|
||||||
|
"account": payable_account,
|
||||||
|
"amount": f"{abs(total_payable_fiat):.2f} {fiat_currency}",
|
||||||
|
"meta": {}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Issue 2: Redundant Satoshi Tracking
|
||||||
|
|
||||||
|
**Problem**: Satoshis are tracked in TWO places in the same transaction:
|
||||||
|
|
||||||
|
1. **Position Amount** (via `@@` notation):
|
||||||
|
```beancount
|
||||||
|
Assets:Bitcoin:Lightning 225033 SATS @@ 200.00 EUR
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Metadata** (sats-equivalent):
|
||||||
|
```beancount
|
||||||
|
Assets:Receivable:User-375ec158 -200.00 EUR
|
||||||
|
sats-equivalent: "225033"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why This Is Problematic**:
|
||||||
|
- The `@@` 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
|
||||||
|
|
||||||
|
**Technical Detail**:
|
||||||
|
|
||||||
|
The `@@` notation means "total price" and Beancount converts it to per-unit price:
|
||||||
|
```beancount
|
||||||
|
; 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...)
|
||||||
|
```
|
||||||
|
|
||||||
|
Beancount can query this:
|
||||||
|
```sql
|
||||||
|
SELECT account, sum(convert(position, SATS))
|
||||||
|
WHERE account = 'Assets:Bitcoin:Lightning'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Recommendation**:
|
||||||
|
|
||||||
|
Choose ONE approach consistently:
|
||||||
|
|
||||||
|
**Option A - Use @ notation** (Beancount standard):
|
||||||
|
```beancount
|
||||||
|
Assets:Bitcoin:Lightning 225033 SATS @@ 200.00 EUR
|
||||||
|
payment-hash: "8d080ec4..."
|
||||||
|
Assets:Receivable:User-375ec158 -200.00 EUR
|
||||||
|
; No sats-equivalent needed here
|
||||||
|
```
|
||||||
|
|
||||||
|
**Option B - Use EUR positions with metadata** (Castle's current approach):
|
||||||
|
```beancount
|
||||||
|
Assets:Bitcoin:Lightning 200.00 EUR
|
||||||
|
sats-received: "225033"
|
||||||
|
payment-hash: "8d080ec4..."
|
||||||
|
Assets:Receivable:User-375ec158 -200.00 EUR
|
||||||
|
sats-cleared: "225033"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Don't**: Mix both in the same transaction (current implementation)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Issue 3: No Exchange Gain/Loss Recognition
|
||||||
|
|
||||||
|
**Problem**: When receivables are denominated in one currency (EUR) and paid in another (SATS), exchange rate fluctuations create gains or losses that should be recognized.
|
||||||
|
|
||||||
|
**Example Scenario**:
|
||||||
|
|
||||||
|
```
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
**Current Implementation**: Forces balance by calculating the `@` rate to make it exactly 200 EUR:
|
||||||
|
```beancount
|
||||||
|
Assets:Bitcoin:Lightning 225033 SATS @ 0.000888... EUR ; = exactly 200.00 EUR
|
||||||
|
```
|
||||||
|
|
||||||
|
This **hides the exchange variance** by treating the payment as if it was worth exactly the receivable amount.
|
||||||
|
|
||||||
|
**GAAP/IFRS Requirement**:
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
**Proper Accounting Treatment**:
|
||||||
|
|
||||||
|
```beancount
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
**Impact**: Moderate severity - affects financial statement accuracy
|
||||||
|
|
||||||
|
**Why This Matters**:
|
||||||
|
- 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
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Issue 4: Semantic Misuse of Price Notation
|
||||||
|
|
||||||
|
**Problem**: The `@` notation in Beancount represents **acquisition cost**, not **settlement value**.
|
||||||
|
|
||||||
|
**Current Usage**:
|
||||||
|
```beancount
|
||||||
|
Assets:Bitcoin:Lightning 225033 SATS @ 0.000888... EUR
|
||||||
|
```
|
||||||
|
|
||||||
|
**What this notation means in accounting**: "We **purchased** 225,033 satoshis at a cost of 0.000888 EUR per satoshi"
|
||||||
|
|
||||||
|
**What actually happened**: "We **received** 225,033 satoshis as payment for a debt"
|
||||||
|
|
||||||
|
**Economic Difference**:
|
||||||
|
- **Purchase**: You exchange cash for an asset (buying Bitcoin)
|
||||||
|
- **Payment Receipt**: You receive an asset in settlement of a receivable
|
||||||
|
|
||||||
|
**Accounting Substance vs. Form**:
|
||||||
|
- **Form**: The transaction looks like a Bitcoin purchase
|
||||||
|
- **Substance**: The transaction is actually a receivable collection
|
||||||
|
|
||||||
|
**GAAP Principle (ASC 105-10-05)**:
|
||||||
|
> "Accounting should reflect the economic substance of transactions, not merely their legal form."
|
||||||
|
|
||||||
|
**Why This Creates Issues**:
|
||||||
|
|
||||||
|
1. **Cost Basis Tracking**: For tax purposes, the "cost" of Bitcoin received as payment should be its fair market value at receipt, not the receivable amount
|
||||||
|
2. **Price Database Pollution**: Beancount's price database now contains "prices" that aren't real market prices
|
||||||
|
3. **Auditor Confusion**: An auditor reviewing this would question why purchase prices don't match market rates
|
||||||
|
|
||||||
|
**Proper Accounting Approach**:
|
||||||
|
|
||||||
|
```beancount
|
||||||
|
; 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
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Issue 5: Misnamed Function and Incorrect Usage
|
||||||
|
|
||||||
|
**Problem**: Function is called `format_net_settlement_entry`, but it's used for simple payments that aren't true net settlements.
|
||||||
|
|
||||||
|
**Example from User's Transaction**:
|
||||||
|
- Receivable: 200.00 EUR
|
||||||
|
- Payable: 0.00 EUR
|
||||||
|
- Net: 200.00 EUR (this is just a **payment**, not a **settlement**)
|
||||||
|
|
||||||
|
**Accounting Terminology**:
|
||||||
|
|
||||||
|
- **Payment**: Settling a single obligation (receivable OR payable)
|
||||||
|
- **Net Settlement**: Offsetting multiple obligations (receivable AND payable)
|
||||||
|
|
||||||
|
**When Net Settlement is Appropriate**:
|
||||||
|
|
||||||
|
```
|
||||||
|
User owes Castle: 555.00 EUR (receivable)
|
||||||
|
Castle owes User: 38.00 EUR (payable)
|
||||||
|
Net amount due: 517.00 EUR (true settlement)
|
||||||
|
```
|
||||||
|
|
||||||
|
Proper three-posting entry:
|
||||||
|
```beancount
|
||||||
|
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 ✓
|
||||||
|
```
|
||||||
|
|
||||||
|
**When Two Postings Suffice**:
|
||||||
|
|
||||||
|
```
|
||||||
|
User owes Castle: 200.00 EUR (receivable)
|
||||||
|
Castle owes User: 0.00 EUR (no payable)
|
||||||
|
Amount due: 200.00 EUR (simple payment)
|
||||||
|
```
|
||||||
|
|
||||||
|
Simpler two-posting entry:
|
||||||
|
```beancount
|
||||||
|
Assets:Bitcoin:Lightning 225033 SATS @@ 200.00 EUR
|
||||||
|
Assets:Receivable:User -200.00 EUR
|
||||||
|
```
|
||||||
|
|
||||||
|
**Best Practice**: Use the simplest journal entry structure that accurately represents the transaction.
|
||||||
|
|
||||||
|
**Recommendation**:
|
||||||
|
1. Rename function to `format_payment_entry` or `format_receivable_payment_entry`
|
||||||
|
2. Create separate `format_net_settlement_entry` for true netting scenarios
|
||||||
|
3. Use conditional logic to choose 2-posting vs 3-posting based on whether both receivables AND payables exist
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Traditional Accounting Approaches
|
||||||
|
|
||||||
|
### Approach 1: Record Bitcoin at Fair Market Value (Tax Compliant)
|
||||||
|
|
||||||
|
```beancount
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
**Pros**:
|
||||||
|
- ✅ Tax compliant (establishes cost basis)
|
||||||
|
- ✅ Recognizes exchange gain/loss
|
||||||
|
- ✅ Uses actual market rates
|
||||||
|
- ✅ Audit trail for cryptocurrency receipts
|
||||||
|
|
||||||
|
**Cons**:
|
||||||
|
- ❌ Requires real-time price feeds
|
||||||
|
- ❌ Creates taxable events
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Approach 2: Simplified EUR-Only Ledger (No SATS Positions)
|
||||||
|
|
||||||
|
```beancount
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
**Pros**:
|
||||||
|
- ✅ Simple and clean
|
||||||
|
- ✅ EUR positions match accounting reality
|
||||||
|
- ✅ SATS tracked in metadata for reference
|
||||||
|
- ✅ No artificial price notation
|
||||||
|
|
||||||
|
**Cons**:
|
||||||
|
- ❌ SATS not queryable via Beancount positions
|
||||||
|
- ❌ Requires metadata parsing for SATS balances
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Approach 3: True Net Settlement (When Both Obligations Exist)
|
||||||
|
|
||||||
|
```beancount
|
||||||
|
2025-11-12 * "Net settlement via Lightning"
|
||||||
|
; User owes 555 EUR, Castle 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
|
||||||
|
```
|
||||||
|
|
||||||
|
**When to Use**: Only when **both** receivables and payables exist and you're truly netting them.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Recommendations
|
||||||
|
|
||||||
|
### Priority 1: Immediate Fixes (Easy Wins)
|
||||||
|
|
||||||
|
#### 1.1 Remove Zero-Amount Postings
|
||||||
|
|
||||||
|
**File**: `beancount_format.py:739-760`
|
||||||
|
|
||||||
|
**Current Code**:
|
||||||
|
```python
|
||||||
|
postings = [
|
||||||
|
{...}, # Lightning
|
||||||
|
{...}, # Receivable
|
||||||
|
{ # Payable (always included, even if 0.00)
|
||||||
|
"account": payable_account,
|
||||||
|
"amount": f"{abs(total_payable_fiat):.2f} {fiat_currency}",
|
||||||
|
"meta": {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fixed Code**:
|
||||||
|
```python
|
||||||
|
postings = [
|
||||||
|
{
|
||||||
|
"account": payment_account,
|
||||||
|
"amount": f"{abs(amount_sats)} SATS @@ {abs(net_fiat_amount):.2f} {fiat_currency}",
|
||||||
|
"meta": {"payment-hash": payment_hash} if payment_hash else {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"account": receivable_account,
|
||||||
|
"amount": f"-{abs(total_receivable_fiat):.2f} {fiat_currency}",
|
||||||
|
"meta": {"sats-equivalent": str(abs(amount_sats))}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
# Only add payable posting if there's actually a payable to clear
|
||||||
|
if total_payable_fiat > 0:
|
||||||
|
postings.append({
|
||||||
|
"account": payable_account,
|
||||||
|
"amount": f"{abs(total_payable_fiat):.2f} {fiat_currency}",
|
||||||
|
"meta": {}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
**Impact**: Cleaner journal, professional presentation, easier auditing
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 1.2 Choose One SATS Tracking Method
|
||||||
|
|
||||||
|
**Decision Required**: Select either position-based OR metadata-based satoshi tracking.
|
||||||
|
|
||||||
|
**Option A - Keep Metadata Approach** (recommended for Castle):
|
||||||
|
```python
|
||||||
|
# In format_net_settlement_entry()
|
||||||
|
postings = [
|
||||||
|
{
|
||||||
|
"account": payment_account,
|
||||||
|
"amount": f"{abs(net_fiat_amount):.2f} {fiat_currency}", # EUR only
|
||||||
|
"meta": {
|
||||||
|
"sats-received": str(abs(amount_sats)),
|
||||||
|
"payment-hash": payment_hash
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"account": receivable_account,
|
||||||
|
"amount": f"-{abs(total_receivable_fiat):.2f} {fiat_currency}",
|
||||||
|
"meta": {"sats-cleared": str(abs(amount_sats))}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Option B - Use Position-Based Tracking**:
|
||||||
|
```python
|
||||||
|
# Remove sats-equivalent metadata entirely
|
||||||
|
postings = [
|
||||||
|
{
|
||||||
|
"account": payment_account,
|
||||||
|
"amount": f"{abs(amount_sats)} SATS @@ {abs(net_fiat_amount):.2f} {fiat_currency}",
|
||||||
|
"meta": {"payment-hash": payment_hash}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"account": receivable_account,
|
||||||
|
"amount": f"-{abs(total_receivable_fiat):.2f} {fiat_currency}",
|
||||||
|
# No sats-equivalent needed - queryable via price database
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Recommendation**: Choose Option A (metadata) for consistency with Castle's architecture.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 1.3 Rename Function for Clarity
|
||||||
|
|
||||||
|
**File**: `beancount_format.py`
|
||||||
|
|
||||||
|
**Current**: `format_net_settlement_entry()`
|
||||||
|
|
||||||
|
**New**: `format_receivable_payment_entry()` or `format_payment_settlement_entry()`
|
||||||
|
|
||||||
|
**Rationale**: More accurately describes what the function does (processes payments, not always net settlements)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Priority 2: Medium-Term Improvements (Compliance)
|
||||||
|
|
||||||
|
#### 2.1 Add Exchange Gain/Loss Tracking
|
||||||
|
|
||||||
|
**File**: `tasks.py:259-276` (get balance and calculate settlement)
|
||||||
|
|
||||||
|
**New Logic**:
|
||||||
|
```python
|
||||||
|
# Get user's current balance
|
||||||
|
balance = await fava.get_user_balance(user_id)
|
||||||
|
fiat_balances = balance.get("fiat_balances", {})
|
||||||
|
total_fiat_balance = fiat_balances.get(fiat_currency, Decimal(0))
|
||||||
|
|
||||||
|
# Calculate expected fiat value of SATS payment at current market rate
|
||||||
|
market_rate = await get_current_sats_eur_rate() # New function needed
|
||||||
|
market_value = Decimal(amount_sats) * market_rate
|
||||||
|
|
||||||
|
# Calculate exchange variance
|
||||||
|
receivable_amount = abs(total_fiat_balance) if total_fiat_balance > 0 else Decimal(0)
|
||||||
|
exchange_variance = market_value - receivable_amount
|
||||||
|
|
||||||
|
# If variance is material (> 1 cent), create exchange gain/loss posting
|
||||||
|
if abs(exchange_variance) > Decimal("0.01"):
|
||||||
|
# Add exchange gain/loss to postings
|
||||||
|
if exchange_variance > 0:
|
||||||
|
# Gain: payment worth more than receivable
|
||||||
|
exchange_account = "Revenue:Foreign-Exchange-Gain"
|
||||||
|
else:
|
||||||
|
# Loss: payment worth less than receivable
|
||||||
|
exchange_account = "Expenses:Foreign-Exchange-Loss"
|
||||||
|
|
||||||
|
# Include in entry creation
|
||||||
|
exchange_posting = {
|
||||||
|
"account": exchange_account,
|
||||||
|
"amount": f"{abs(exchange_variance):.2f} {fiat_currency}",
|
||||||
|
"meta": {
|
||||||
|
"sats-amount": str(amount_sats),
|
||||||
|
"market-rate": str(market_rate),
|
||||||
|
"receivable-amount": str(receivable_amount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Benefits**:
|
||||||
|
- ✅ Tax compliance
|
||||||
|
- ✅ Accurate financial reporting
|
||||||
|
- ✅ Audit trail for cryptocurrency gains/losses
|
||||||
|
- ✅ Regulatory compliance (GAAP/IFRS)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 2.2 Implement True Net Settlement vs. Simple Payment Logic
|
||||||
|
|
||||||
|
**File**: `tasks.py` or new `payment_logic.py`
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def create_payment_entry(
|
||||||
|
user_id: str,
|
||||||
|
amount_sats: int,
|
||||||
|
fiat_amount: Decimal,
|
||||||
|
fiat_currency: str,
|
||||||
|
payment_hash: str
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Create appropriate payment entry based on user's balance situation.
|
||||||
|
Uses 2-posting for simple payments, 3-posting for net settlements.
|
||||||
|
"""
|
||||||
|
# Get user balance
|
||||||
|
balance = await fava.get_user_balance(user_id)
|
||||||
|
fiat_balances = balance.get("fiat_balances", {})
|
||||||
|
total_balance = fiat_balances.get(fiat_currency, Decimal(0))
|
||||||
|
|
||||||
|
receivable_amount = Decimal(0)
|
||||||
|
payable_amount = Decimal(0)
|
||||||
|
|
||||||
|
if total_balance > 0:
|
||||||
|
receivable_amount = total_balance
|
||||||
|
elif total_balance < 0:
|
||||||
|
payable_amount = abs(total_balance)
|
||||||
|
|
||||||
|
# Determine entry type
|
||||||
|
if receivable_amount > 0 and payable_amount > 0:
|
||||||
|
# TRUE NET SETTLEMENT: Both obligations exist
|
||||||
|
return await format_net_settlement_entry(
|
||||||
|
user_id=user_id,
|
||||||
|
amount_sats=amount_sats,
|
||||||
|
receivable_amount=receivable_amount,
|
||||||
|
payable_amount=payable_amount,
|
||||||
|
fiat_amount=fiat_amount,
|
||||||
|
fiat_currency=fiat_currency,
|
||||||
|
payment_hash=payment_hash
|
||||||
|
)
|
||||||
|
elif receivable_amount > 0:
|
||||||
|
# SIMPLE RECEIVABLE PAYMENT: Only receivable exists
|
||||||
|
return await format_receivable_payment_entry(
|
||||||
|
user_id=user_id,
|
||||||
|
amount_sats=amount_sats,
|
||||||
|
receivable_amount=receivable_amount,
|
||||||
|
fiat_amount=fiat_amount,
|
||||||
|
fiat_currency=fiat_currency,
|
||||||
|
payment_hash=payment_hash
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# PAYABLE PAYMENT: Castle paying user (different flow)
|
||||||
|
return await format_payable_payment_entry(...)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Priority 3: Long-Term Architectural Decisions
|
||||||
|
|
||||||
|
#### 3.1 Establish Primary Currency Hierarchy
|
||||||
|
|
||||||
|
**Current Issue**: Mixed approach (EUR positions with SATS metadata, but also SATS positions with @ notation)
|
||||||
|
|
||||||
|
**Decision Required**: Choose ONE of the following architectures:
|
||||||
|
|
||||||
|
**Architecture A - EUR Primary, SATS Secondary** (recommended):
|
||||||
|
```beancount
|
||||||
|
; 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"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Architecture B - SATS Primary, EUR Secondary**:
|
||||||
|
```beancount
|
||||||
|
; 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"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Recommendation**: 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 Castle metadata approach
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 3.2 Consider Separate Ledger for Cryptocurrency Holdings
|
||||||
|
|
||||||
|
**Advanced Approach**: Separate cryptocurrency movements from fiat accounting
|
||||||
|
|
||||||
|
**Main Ledger** (EUR-denominated):
|
||||||
|
```beancount
|
||||||
|
2025-11-12 * "Payment received from user"
|
||||||
|
Assets:Bitcoin-Custody:User-375ec158 200.00 EUR
|
||||||
|
Assets:Receivable:User-375ec158 -200.00 EUR
|
||||||
|
```
|
||||||
|
|
||||||
|
**Cryptocurrency Sub-Ledger** (SATS-denominated):
|
||||||
|
```beancount
|
||||||
|
2025-11-12 * "Lightning payment received"
|
||||||
|
Assets:Bitcoin:Lightning:Castle 225033 SATS
|
||||||
|
Assets:Bitcoin:Custody:User-375ec 225033 SATS
|
||||||
|
```
|
||||||
|
|
||||||
|
**Benefits**:
|
||||||
|
- ✅ Clean separation of concerns
|
||||||
|
- ✅ Cryptocurrency movements tracked independently
|
||||||
|
- ✅ Fiat accounting unaffected by Bitcoin volatility
|
||||||
|
- ✅ Can generate separate financial statements
|
||||||
|
|
||||||
|
**Drawbacks**:
|
||||||
|
- ❌ Increased complexity
|
||||||
|
- ❌ Reconciliation between ledgers required
|
||||||
|
- ❌ Two sets of books to maintain
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Code Files Requiring Changes
|
||||||
|
|
||||||
|
### High Priority (Immediate Fixes)
|
||||||
|
|
||||||
|
1. **`beancount_format.py:739-760`**
|
||||||
|
- Remove zero-amount postings
|
||||||
|
- Make payable posting conditional
|
||||||
|
|
||||||
|
2. **`beancount_format.py:692`**
|
||||||
|
- Rename function to `format_receivable_payment_entry`
|
||||||
|
|
||||||
|
### Medium Priority (Compliance)
|
||||||
|
|
||||||
|
3. **`tasks.py:235-310`**
|
||||||
|
- Add exchange gain/loss calculation
|
||||||
|
- Implement payment vs. settlement logic
|
||||||
|
|
||||||
|
4. **New file: `exchange_rates.py`**
|
||||||
|
- Create `get_current_sats_eur_rate()` function
|
||||||
|
- Implement price feed integration
|
||||||
|
|
||||||
|
5. **`beancount_format.py`**
|
||||||
|
- Create new `format_net_settlement_entry()` for true netting
|
||||||
|
- Create `format_receivable_payment_entry()` for simple payments
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing Requirements
|
||||||
|
|
||||||
|
### Test Case 1: Simple Receivable Payment (No Payable)
|
||||||
|
|
||||||
|
**Setup**:
|
||||||
|
- User has receivable: 200.00 EUR
|
||||||
|
- User has payable: 0.00 EUR
|
||||||
|
- User pays: 225,033 SATS
|
||||||
|
|
||||||
|
**Expected Entry** (after fixes):
|
||||||
|
```beancount
|
||||||
|
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"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Verify**:
|
||||||
|
- ✅ Only 2 postings (no zero-amount payable)
|
||||||
|
- ✅ Entry balances
|
||||||
|
- ✅ SATS tracked in metadata
|
||||||
|
- ✅ User balance becomes 0 (both EUR and SATS)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Test Case 2: True Net Settlement
|
||||||
|
|
||||||
|
**Setup**:
|
||||||
|
- 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)
|
||||||
|
|
||||||
|
**Expected Entry**:
|
||||||
|
```beancount
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
**Verify**:
|
||||||
|
- ✅ 3 postings (receivable + payable cleared)
|
||||||
|
- ✅ Net amount = receivable - payable
|
||||||
|
- ✅ Both balances become 0
|
||||||
|
- ✅ Mathematically balanced
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Test Case 3: Exchange Gain/Loss (Future)
|
||||||
|
|
||||||
|
**Setup**:
|
||||||
|
- 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
|
||||||
|
|
||||||
|
**Expected Entry** (with exchange tracking):
|
||||||
|
```beancount
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
**Verify**:
|
||||||
|
- ✅ Bitcoin recorded at fair market value
|
||||||
|
- ✅ Exchange loss recognized
|
||||||
|
- ✅ Receivable cleared at book value
|
||||||
|
- ✅ Entry balances
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
### Summary of Issues
|
||||||
|
|
||||||
|
| Issue | Severity | Accounting Impact | Recommended Action |
|
||||||
|
|-------|----------|-------------------|-------------------|
|
||||||
|
| Zero-amount postings | Low | Presentation only | Remove immediately |
|
||||||
|
| Redundant SATS tracking | Low | Storage/efficiency | Choose one method |
|
||||||
|
| No exchange gain/loss | **High** | Financial accuracy | Implement for compliance |
|
||||||
|
| Semantic misuse of @ | Medium | Audit clarity | Consider EUR-only positions |
|
||||||
|
| Misnamed function | Low | Code clarity | Rename function |
|
||||||
|
|
||||||
|
### Professional Assessment
|
||||||
|
|
||||||
|
**Is this "best practice" accounting?**
|
||||||
|
**No**, this implementation deviates from traditional accounting standards in several ways.
|
||||||
|
|
||||||
|
**Is it acceptable for Castle's use case?**
|
||||||
|
**Yes, with modifications**, it's a reasonable pragmatic solution for a novel problem (cryptocurrency payments of fiat debts).
|
||||||
|
|
||||||
|
**Critical improvements needed**:
|
||||||
|
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)
|
||||||
|
|
||||||
|
**The fundamental challenge**: Traditional accounting wasn't designed for this scenario. There is no established "standard" for recording cryptocurrency payments of fiat-denominated receivables. Castle's approach is functional, but should be refined to align better with accounting principles where possible.
|
||||||
|
|
||||||
|
### Next Steps
|
||||||
|
|
||||||
|
1. **Week 1**: Implement Priority 1 fixes (remove zero postings, rename function)
|
||||||
|
2. **Week 2-3**: Design and implement exchange gain/loss tracking
|
||||||
|
3. **Week 4**: Add payment vs. settlement logic
|
||||||
|
4. **Ongoing**: Monitor regulatory guidance on cryptocurrency accounting
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- **FASB ASC 830**: Foreign Currency Matters
|
||||||
|
- **IAS 21**: The Effects of Changes in Foreign Exchange Rates
|
||||||
|
- **FASB Concept Statement No. 2**: Qualitative Characteristics of Accounting Information
|
||||||
|
- **ASC 105-10-05**: Substance Over Form
|
||||||
|
- **Beancount Documentation**: http://furius.ca/beancount/doc/index
|
||||||
|
- **Castle Extension**: `docs/SATS-EQUIVALENT-METADATA.md`
|
||||||
|
- **BQL Analysis**: `docs/BQL-BALANCE-QUERIES.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Document Version**: 1.0
|
||||||
|
**Last Updated**: 2025-01-12
|
||||||
|
**Next Review**: After Priority 1 fixes implemented
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*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 Castle's payment recording system.*
|
||||||
529
docs/BQL-PRICE-NOTATION-SOLUTION.md
Normal file
529
docs/BQL-PRICE-NOTATION-SOLUTION.md
Normal file
|
|
@ -0,0 +1,529 @@
|
||||||
|
# BQL Price Notation Solution for SATS Tracking
|
||||||
|
|
||||||
|
**Date**: 2025-01-12
|
||||||
|
**Status**: Testing
|
||||||
|
**Context**: Explore price notation as alternative to metadata for SATS tracking
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Problem Recap
|
||||||
|
|
||||||
|
Current approach stores SATS in metadata:
|
||||||
|
```beancount
|
||||||
|
2025-11-10 * "Groceries"
|
||||||
|
Expenses:Food -360.00 EUR
|
||||||
|
sats-equivalent: 337096
|
||||||
|
Liabilities:Payable:User-abc 360.00 EUR
|
||||||
|
sats-equivalent: 337096
|
||||||
|
```
|
||||||
|
|
||||||
|
**Issue**: BQL cannot access metadata, so balance queries require manual aggregation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Solution: Use Price Notation
|
||||||
|
|
||||||
|
### Proposed Format
|
||||||
|
|
||||||
|
Post in actual transaction currency (EUR) with SATS as price:
|
||||||
|
|
||||||
|
```beancount
|
||||||
|
2025-11-10 * "Groceries"
|
||||||
|
Expenses:Food -360.00 EUR @@ 337096 SATS
|
||||||
|
Liabilities:Payable:User-abc 360.00 EUR @@ 337096 SATS
|
||||||
|
```
|
||||||
|
|
||||||
|
**What this means**:
|
||||||
|
- Primary amount: `-360.00 EUR` (the actual transaction currency)
|
||||||
|
- Total price: `337096 SATS` (the bitcoin equivalent value)
|
||||||
|
- Transaction integrity preserved (posted in EUR as it occurred)
|
||||||
|
- SATS tracked as price (queryable by BQL)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Price Notation Options
|
||||||
|
|
||||||
|
### Option 1: Per-Unit Price (`@`)
|
||||||
|
|
||||||
|
```beancount
|
||||||
|
Expenses:Food -360.00 EUR @ 936.38 SATS
|
||||||
|
```
|
||||||
|
|
||||||
|
**What it means**: Each EUR is worth 936.38 SATS
|
||||||
|
**Total calculation**: 360 × 936.38 = 337,096.8 SATS
|
||||||
|
**Precision**: May introduce rounding (336,696.8 vs 337,096)
|
||||||
|
|
||||||
|
### Option 2: Total Price (`@@`) ✅ RECOMMENDED
|
||||||
|
|
||||||
|
```beancount
|
||||||
|
Expenses:Food -360.00 EUR @@ 337096 SATS
|
||||||
|
```
|
||||||
|
|
||||||
|
**What it means**: Total transaction value is 337,096 SATS
|
||||||
|
**Total calculation**: Exact 337,096 SATS (no rounding)
|
||||||
|
**Precision**: Preserves exact SATS amount from original calculation
|
||||||
|
|
||||||
|
**Why `@@` is better for Castle:**
|
||||||
|
- ✅ Preserves exact SATS amount (no rounding errors)
|
||||||
|
- ✅ Matches current metadata storage exactly
|
||||||
|
- ✅ Clearer intent: "this transaction equals X SATS total"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## How BQL Handles Prices
|
||||||
|
|
||||||
|
### Available Price Columns
|
||||||
|
|
||||||
|
From BQL schema:
|
||||||
|
- `price_number` - The numeric price amount (Decimal)
|
||||||
|
- `price_currency` - The currency of the price (str)
|
||||||
|
- `position` - Full posting (includes price)
|
||||||
|
- `WEIGHT(position)` - Function that returns balance weight
|
||||||
|
|
||||||
|
### BQL Query Capabilities
|
||||||
|
|
||||||
|
**Test Query 1: Access price directly**
|
||||||
|
```sql
|
||||||
|
SELECT account, number, currency, price_number, price_currency
|
||||||
|
WHERE account ~ 'User-375ec158'
|
||||||
|
AND price_currency = 'SATS';
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected Result** (if price notation works):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"rows": [
|
||||||
|
["Liabilities:Payable:User-abc", "360.00", "EUR", "337096", "SATS"]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Test Query 2: Aggregate SATS from prices**
|
||||||
|
```sql
|
||||||
|
SELECT account,
|
||||||
|
SUM(price_number) as total_sats
|
||||||
|
WHERE account ~ 'User-'
|
||||||
|
AND price_currency = 'SATS'
|
||||||
|
AND flag != '!'
|
||||||
|
GROUP BY account;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected Result**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"rows": [
|
||||||
|
["Liabilities:Payable:User-abc", "337096"]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing Plan
|
||||||
|
|
||||||
|
### Step 1: Run Metadata Test
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/padreug/projects/castle-beancounter
|
||||||
|
./test_metadata_simple.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
**What to look for**:
|
||||||
|
- Does `meta` column exist in response?
|
||||||
|
- Is `sats-equivalent` accessible in the data?
|
||||||
|
|
||||||
|
**If YES**: Metadata IS accessible, simpler solution available
|
||||||
|
**If NO**: Proceed with price notation approach
|
||||||
|
|
||||||
|
### Step 2: Test Current Data Structure
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./test_bql_metadata.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
This runs 6 tests:
|
||||||
|
1. Check metadata column
|
||||||
|
2. Check price columns
|
||||||
|
3. Basic position query
|
||||||
|
4. Test WEIGHT function
|
||||||
|
5. Aggregate positions
|
||||||
|
6. Aggregate weights
|
||||||
|
|
||||||
|
**What to look for**:
|
||||||
|
- Which columns are available?
|
||||||
|
- What does `position` return for entries with prices?
|
||||||
|
- Can we access `price_number` and `price_currency`?
|
||||||
|
|
||||||
|
### Step 3: Create Test Ledger Entry
|
||||||
|
|
||||||
|
Add one test entry to your ledger:
|
||||||
|
|
||||||
|
```beancount
|
||||||
|
2025-01-12 * "TEST: Price notation test"
|
||||||
|
Expenses:Test:PriceNotation -100.00 EUR @@ 93600 SATS
|
||||||
|
Liabilities:Payable:User-TEST 100.00 EUR @@ 93600 SATS
|
||||||
|
```
|
||||||
|
|
||||||
|
Then query:
|
||||||
|
```bash
|
||||||
|
curl -s "http://localhost:3333/castle-ledger/api/query" \
|
||||||
|
-G \
|
||||||
|
--data-urlencode "query_string=SELECT account, position, price_number, price_currency WHERE account ~ 'TEST'" \
|
||||||
|
| jq '.'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected if working**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"rows": [
|
||||||
|
["Expenses:Test:PriceNotation", "-100.00 EUR @@ 93600 SATS", "93600", "SATS"],
|
||||||
|
["Liabilities:Payable:User-TEST", "100.00 EUR @@ 93600 SATS", "93600", "SATS"]
|
||||||
|
],
|
||||||
|
"types": [
|
||||||
|
{"name": "account", "type": "str"},
|
||||||
|
{"name": "position", "type": "Position"},
|
||||||
|
{"name": "price_number", "type": "Decimal"},
|
||||||
|
{"name": "price_currency", "type": "str"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Migration Strategy (If Price Notation Works)
|
||||||
|
|
||||||
|
### Phase 1: Test on Sample Data
|
||||||
|
|
||||||
|
1. Create test ledger with mix of formats
|
||||||
|
2. Verify BQL can query price_number
|
||||||
|
3. Verify aggregation accuracy
|
||||||
|
4. Compare with manual method results
|
||||||
|
|
||||||
|
### Phase 2: Write Migration Script
|
||||||
|
|
||||||
|
```python
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Migrate metadata sats-equivalent to price notation.
|
||||||
|
|
||||||
|
Converts:
|
||||||
|
Expenses:Food -360.00 EUR
|
||||||
|
sats-equivalent: 337096
|
||||||
|
|
||||||
|
To:
|
||||||
|
Expenses:Food -360.00 EUR @@ 337096 SATS
|
||||||
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
def migrate_entry(entry_lines):
|
||||||
|
"""Migrate a single transaction entry."""
|
||||||
|
result = []
|
||||||
|
current_posting = None
|
||||||
|
sats_value = None
|
||||||
|
|
||||||
|
for line in entry_lines:
|
||||||
|
# Check if this is a posting line
|
||||||
|
if re.match(r'^\s{2,}\w+:', line):
|
||||||
|
# If we have pending sats from previous posting, add it
|
||||||
|
if current_posting and sats_value:
|
||||||
|
# Add @@ notation to posting
|
||||||
|
posting = current_posting.rstrip()
|
||||||
|
posting += f" @@ {sats_value} SATS\n"
|
||||||
|
result.append(posting)
|
||||||
|
current_posting = None
|
||||||
|
sats_value = None
|
||||||
|
else:
|
||||||
|
if current_posting:
|
||||||
|
result.append(current_posting)
|
||||||
|
current_posting = line
|
||||||
|
|
||||||
|
# Check if this is sats-equivalent metadata
|
||||||
|
elif 'sats-equivalent:' in line:
|
||||||
|
match = re.search(r'sats-equivalent:\s*(-?\d+)', line)
|
||||||
|
if match:
|
||||||
|
sats_value = match.group(1)
|
||||||
|
# Don't include metadata line in result
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Other lines (date, narration, other metadata)
|
||||||
|
if current_posting and sats_value:
|
||||||
|
posting = current_posting.rstrip()
|
||||||
|
posting += f" @@ {sats_value} SATS\n"
|
||||||
|
result.append(posting)
|
||||||
|
current_posting = None
|
||||||
|
sats_value = None
|
||||||
|
elif current_posting:
|
||||||
|
result.append(current_posting)
|
||||||
|
current_posting = None
|
||||||
|
|
||||||
|
result.append(line)
|
||||||
|
|
||||||
|
# Handle last posting
|
||||||
|
if current_posting and sats_value:
|
||||||
|
posting = current_posting.rstrip()
|
||||||
|
posting += f" @@ {sats_value} SATS\n"
|
||||||
|
result.append(posting)
|
||||||
|
elif current_posting:
|
||||||
|
result.append(current_posting)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def migrate_ledger(input_file, output_file):
|
||||||
|
"""Migrate entire ledger file."""
|
||||||
|
with open(input_file, 'r') as f:
|
||||||
|
lines = f.readlines()
|
||||||
|
|
||||||
|
result = []
|
||||||
|
current_entry = []
|
||||||
|
in_transaction = False
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
# Transaction start
|
||||||
|
if re.match(r'^\d{4}-\d{2}-\d{2}\s+[*!]', line):
|
||||||
|
in_transaction = True
|
||||||
|
current_entry = [line]
|
||||||
|
|
||||||
|
# Empty line ends transaction
|
||||||
|
elif in_transaction and line.strip() == '':
|
||||||
|
current_entry.append(line)
|
||||||
|
migrated = migrate_entry(current_entry)
|
||||||
|
result.extend(migrated)
|
||||||
|
current_entry = []
|
||||||
|
in_transaction = False
|
||||||
|
|
||||||
|
# Inside transaction
|
||||||
|
elif in_transaction:
|
||||||
|
current_entry.append(line)
|
||||||
|
|
||||||
|
# Outside transaction
|
||||||
|
else:
|
||||||
|
result.append(line)
|
||||||
|
|
||||||
|
# Handle last entry if file doesn't end with blank line
|
||||||
|
if current_entry:
|
||||||
|
migrated = migrate_entry(current_entry)
|
||||||
|
result.extend(migrated)
|
||||||
|
|
||||||
|
with open(output_file, 'w') as f:
|
||||||
|
f.writelines(result)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import sys
|
||||||
|
if len(sys.argv) != 3:
|
||||||
|
print("Usage: migrate_ledger.py <input.beancount> <output.beancount>")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
migrate_ledger(sys.argv[1], sys.argv[2])
|
||||||
|
print(f"Migrated {sys.argv[1]} -> {sys.argv[2]}")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 3: Update Balance Query Methods
|
||||||
|
|
||||||
|
Replace `get_user_balance_bql()` with price-based version:
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def get_user_balance_bql(self, user_id: str) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Get user balance using price notation (SATS stored as @@ price).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
{
|
||||||
|
"balance": int (sats from price_number),
|
||||||
|
"fiat_balances": {"EUR": Decimal("100.50")},
|
||||||
|
"accounts": [{"account": "...", "sats": 150000}]
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
user_id_prefix = user_id[:8]
|
||||||
|
|
||||||
|
# Query: Get EUR positions with SATS prices
|
||||||
|
query = f"""
|
||||||
|
SELECT
|
||||||
|
account,
|
||||||
|
number as eur_amount,
|
||||||
|
price_number as sats_amount
|
||||||
|
WHERE account ~ ':User-{user_id_prefix}'
|
||||||
|
AND (account ~ 'Payable' OR account ~ 'Receivable')
|
||||||
|
AND flag != '!'
|
||||||
|
AND price_currency = 'SATS'
|
||||||
|
"""
|
||||||
|
|
||||||
|
result = await self.query_bql(query)
|
||||||
|
|
||||||
|
total_sats = 0
|
||||||
|
fiat_balances = {}
|
||||||
|
accounts_map = {}
|
||||||
|
|
||||||
|
for row in result["rows"]:
|
||||||
|
account_name, eur_amount, sats_amount = row
|
||||||
|
|
||||||
|
# Parse amounts
|
||||||
|
sats = int(Decimal(sats_amount)) if sats_amount else 0
|
||||||
|
eur = Decimal(eur_amount) if eur_amount else Decimal(0)
|
||||||
|
|
||||||
|
total_sats += sats
|
||||||
|
|
||||||
|
# Aggregate fiat
|
||||||
|
if eur != 0:
|
||||||
|
if "EUR" not in fiat_balances:
|
||||||
|
fiat_balances["EUR"] = Decimal(0)
|
||||||
|
fiat_balances["EUR"] += eur
|
||||||
|
|
||||||
|
# Track per account
|
||||||
|
if account_name not in accounts_map:
|
||||||
|
accounts_map[account_name] = {"account": account_name, "sats": 0}
|
||||||
|
accounts_map[account_name]["sats"] += sats
|
||||||
|
|
||||||
|
return {
|
||||||
|
"balance": total_sats,
|
||||||
|
"fiat_balances": fiat_balances,
|
||||||
|
"accounts": list(accounts_map.values())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 4: Validation
|
||||||
|
|
||||||
|
1. Run both methods in parallel
|
||||||
|
2. Compare results for all users
|
||||||
|
3. Log any discrepancies
|
||||||
|
4. Investigate and fix differences
|
||||||
|
5. Once validated, switch to BQL method
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Advantages of Price Notation Approach
|
||||||
|
|
||||||
|
### 1. BQL Compatibility ✅
|
||||||
|
- `price_number` is a standard BQL column
|
||||||
|
- Can aggregate: `SUM(price_number)`
|
||||||
|
- Can filter: `WHERE price_currency = 'SATS'`
|
||||||
|
|
||||||
|
### 2. Transaction Integrity ✅
|
||||||
|
- Post in actual transaction currency (EUR)
|
||||||
|
- SATS as secondary value (price)
|
||||||
|
- Proper accounting: source currency preserved
|
||||||
|
|
||||||
|
### 3. Beancount Features ✅
|
||||||
|
- Price database automatically updated
|
||||||
|
- Can query historical EUR/SATS rates
|
||||||
|
- Reports can show both EUR and SATS values
|
||||||
|
|
||||||
|
### 4. Performance ✅
|
||||||
|
- BQL filters at source (no fetching all entries)
|
||||||
|
- Direct column access (no metadata parsing)
|
||||||
|
- Efficient aggregation (database-level)
|
||||||
|
|
||||||
|
### 5. Reporting Flexibility ✅
|
||||||
|
- Show EUR amounts in reports
|
||||||
|
- Show SATS equivalents alongside
|
||||||
|
- Filter by either currency
|
||||||
|
- Calculate gains/losses if SATS price changes
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Potential Issues and Solutions
|
||||||
|
|
||||||
|
### Issue 1: Price vs Cost Confusion
|
||||||
|
|
||||||
|
**Problem**: Beancount distinguishes between `@` price and `{}` cost
|
||||||
|
**Solution**: Always use price (`@` or `@@`), never cost (`{}`)
|
||||||
|
|
||||||
|
**Why**:
|
||||||
|
- Cost is for tracking cost basis (investments, capital gains)
|
||||||
|
- Price is for conversion rates (what we need)
|
||||||
|
|
||||||
|
### Issue 2: Precision Loss with `@`
|
||||||
|
|
||||||
|
**Problem**: Per-unit price may have rounding
|
||||||
|
```beancount
|
||||||
|
360.00 EUR @ 936.38 SATS = 336,696.8 SATS (not 337,096)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solution**: Always use `@@` total price
|
||||||
|
```beancount
|
||||||
|
360.00 EUR @@ 337096 SATS = 337,096 SATS (exact)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Issue 3: Negative Numbers
|
||||||
|
|
||||||
|
**Problem**: How to handle negative EUR with positive SATS?
|
||||||
|
```beancount
|
||||||
|
-360.00 EUR @@ ??? SATS
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solution**: Price is always positive (it's a rate, not an amount)
|
||||||
|
```beancount
|
||||||
|
-360.00 EUR @@ 337096 SATS ✅ Correct
|
||||||
|
```
|
||||||
|
|
||||||
|
The sign applies to the position, price is the conversion factor.
|
||||||
|
|
||||||
|
### Issue 4: Historical Data
|
||||||
|
|
||||||
|
**Problem**: Existing entries have metadata, not prices
|
||||||
|
|
||||||
|
**Solution**: Migration script (see Phase 2)
|
||||||
|
- One-time conversion
|
||||||
|
- Validate with checksums
|
||||||
|
- Keep backup of original
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing Checklist
|
||||||
|
|
||||||
|
- [ ] Run `test_metadata_simple.sh` - Check if metadata is accessible
|
||||||
|
- [ ] Run `test_bql_metadata.sh` - Full BQL capabilities test
|
||||||
|
- [ ] Add test entry with `@@` notation to ledger
|
||||||
|
- [ ] Query test entry with BQL to verify price_number access
|
||||||
|
- [ ] Compare aggregation: metadata vs price notation
|
||||||
|
- [ ] Test negative amounts with prices
|
||||||
|
- [ ] Test zero amounts
|
||||||
|
- [ ] Test multi-currency scenarios (EUR, USD with SATS prices)
|
||||||
|
- [ ] Verify price database is populated correctly
|
||||||
|
- [ ] Check that WEIGHT() function returns SATS value
|
||||||
|
- [ ] Validate balances match current manual method
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Decision Matrix
|
||||||
|
|
||||||
|
| Criteria | Metadata | Price Notation | Winner |
|
||||||
|
|----------|----------|----------------|--------|
|
||||||
|
| BQL Queryable | ❌ No | ✅ Yes | Price |
|
||||||
|
| Transaction Integrity | ✅ EUR first | ✅ EUR first | Tie |
|
||||||
|
| SATS Precision | ✅ Exact int | ✅ Exact (with @@) | Tie |
|
||||||
|
| Migration Effort | ✅ None | ⚠️ Script needed | Metadata |
|
||||||
|
| Performance | ❌ Manual loop | ✅ BQL optimized | Price |
|
||||||
|
| Beancount Standard | ⚠️ Non-standard | ✅ Standard feature | Price |
|
||||||
|
| Reporting Flexibility | ⚠️ Limited | ✅ Both currencies | Price |
|
||||||
|
| Future Proof | ⚠️ Custom | ✅ Standard | Price |
|
||||||
|
|
||||||
|
**Recommendation**: **Price Notation** if tests confirm BQL can access `price_number`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
1. **Run tests** (test_metadata_simple.sh and test_bql_metadata.sh)
|
||||||
|
2. **Review results** - Can BQL access price_number?
|
||||||
|
3. **Add test entry** with @@ notation
|
||||||
|
4. **Query test entry** - Verify aggregation works
|
||||||
|
5. **If successful**:
|
||||||
|
- Write full migration script
|
||||||
|
- Test on copy of production ledger
|
||||||
|
- Validate balances match
|
||||||
|
- Schedule migration (maintenance window)
|
||||||
|
- Update balance query methods
|
||||||
|
- Deploy and monitor
|
||||||
|
6. **If unsuccessful**:
|
||||||
|
- Document why price notation doesn't work
|
||||||
|
- Consider Beancount plugin approach
|
||||||
|
- Or accept manual aggregation with caching
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Document Status**: Awaiting test results
|
||||||
|
**Next Action**: Run test scripts and report findings
|
||||||
386
docs/SATS-EQUIVALENT-METADATA.md
Normal file
386
docs/SATS-EQUIVALENT-METADATA.md
Normal file
|
|
@ -0,0 +1,386 @@
|
||||||
|
# SATS-Equivalent Metadata Field
|
||||||
|
|
||||||
|
**Date**: 2025-01-12
|
||||||
|
**Status**: Current Architecture
|
||||||
|
**Location**: Beancount posting metadata
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The `sats-equivalent` metadata field is Castle's solution for **dual-currency tracking** in a fiat-denominated ledger. It preserves Bitcoin (satoshi) amounts alongside fiat currency amounts without violating accounting principles or creating multi-currency complexity in position balances.
|
||||||
|
|
||||||
|
### Quick Summary
|
||||||
|
|
||||||
|
- **Purpose**: Track Bitcoin/Lightning amounts in a EUR-denominated ledger
|
||||||
|
- **Location**: Beancount posting metadata (not position amounts)
|
||||||
|
- **Format**: String containing absolute satoshi amount (e.g., `"337096"`)
|
||||||
|
- **Primary Use**: Calculate user balances in satoshis (Castle's primary currency)
|
||||||
|
- **Key Principle**: Satoshis are for reference; EUR is the actual transaction currency
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The Problem: Dual-Currency Tracking
|
||||||
|
|
||||||
|
Castle needs to track both:
|
||||||
|
1. **Fiat amounts** (EUR, USD) - The actual transaction currency
|
||||||
|
2. **Bitcoin amounts** (satoshis) - The Lightning Network settlement currency
|
||||||
|
|
||||||
|
### Why Not Just Use SATS as Position Amounts?
|
||||||
|
|
||||||
|
**Accounting Reality**: When a user pays €36.93 cash for groceries, the transaction is denominated in EUR, not Bitcoin. Recording it as Bitcoin would:
|
||||||
|
- ❌ Misrepresent the actual transaction
|
||||||
|
- ❌ Create exchange rate volatility issues
|
||||||
|
- ❌ Complicate traditional accounting reconciliation
|
||||||
|
- ❌ Make fiat-based reporting difficult
|
||||||
|
|
||||||
|
**Castle's Philosophy**: Record transactions in their **actual currency**, with Bitcoin as supplementary data.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architecture: EUR-Primary Format
|
||||||
|
|
||||||
|
### Current Ledger Format
|
||||||
|
|
||||||
|
```beancount
|
||||||
|
2025-11-10 * "Groceries (36.93 EUR)" #expense-entry
|
||||||
|
Expenses:Food:Supplies 36.93 EUR
|
||||||
|
sats-equivalent: "39669"
|
||||||
|
reference: "cash-payment-abc123"
|
||||||
|
Liabilities:Payable:User-5987ae95 -36.93 EUR
|
||||||
|
sats-equivalent: "39669"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key Components:**
|
||||||
|
- **Position Amount**: `36.93 EUR` - The actual transaction amount
|
||||||
|
- **Metadata**: `sats-equivalent: "39669"` - The Bitcoin equivalent at time of transaction
|
||||||
|
- **Sign**: The sign (debit/credit) is on the EUR amount; sats-equivalent is always absolute value
|
||||||
|
|
||||||
|
### How It's Created
|
||||||
|
|
||||||
|
In `views_api.py:839`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# If fiat currency is provided, use EUR-based format
|
||||||
|
if fiat_currency and fiat_amount:
|
||||||
|
# EUR-based posting (current architecture)
|
||||||
|
posting_metadata["sats-equivalent"] = str(abs(line.amount))
|
||||||
|
|
||||||
|
# Apply the sign from line.amount to fiat_amount
|
||||||
|
signed_fiat_amount = fiat_amount if line.amount >= 0 else -fiat_amount
|
||||||
|
|
||||||
|
posting = {
|
||||||
|
"account": account.name,
|
||||||
|
"amount": f"{signed_fiat_amount:.2f} {fiat_currency}",
|
||||||
|
"meta": posting_metadata if posting_metadata else None
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Critical Details:**
|
||||||
|
- `line.amount` is always in satoshis internally
|
||||||
|
- The sign (debit/credit) transfers to the fiat amount
|
||||||
|
- `sats-equivalent` stores the **absolute value** of the satoshi amount
|
||||||
|
- Sign interpretation depends on account type (Asset/Liability/etc.)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Usage: Balance Calculation
|
||||||
|
|
||||||
|
### Primary Use Case: User Balances
|
||||||
|
|
||||||
|
Castle's core function is tracking **who owes whom** in satoshis. The `sats-equivalent` metadata enables this.
|
||||||
|
|
||||||
|
**Flow** (`fava_client.py:220-248`):
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Parse posting amount (EUR/USD)
|
||||||
|
fiat_match = re.match(r'^(-?[\d.]+)\s+([A-Z]{3})$', amount_str)
|
||||||
|
if fiat_match and fiat_match.group(2) in ('EUR', 'USD', 'GBP'):
|
||||||
|
fiat_amount = Decimal(fiat_match.group(1))
|
||||||
|
fiat_currency = fiat_match.group(2)
|
||||||
|
|
||||||
|
# Track fiat balance
|
||||||
|
fiat_balances[fiat_currency] += fiat_amount
|
||||||
|
|
||||||
|
# Extract SATS equivalent from metadata
|
||||||
|
posting_meta = posting.get("meta", {})
|
||||||
|
sats_equiv = posting_meta.get("sats-equivalent")
|
||||||
|
if sats_equiv:
|
||||||
|
# Apply the sign from fiat_amount to sats_equiv
|
||||||
|
sats_amount = int(sats_equiv) if fiat_amount > 0 else -int(sats_equiv)
|
||||||
|
total_sats += sats_amount
|
||||||
|
```
|
||||||
|
|
||||||
|
**Sign Interpretation:**
|
||||||
|
- EUR amount is `36.93` (positive/debit) → sats is `+39669`
|
||||||
|
- EUR amount is `-36.93` (negative/credit) → sats is `-39669`
|
||||||
|
|
||||||
|
### Secondary Use: Journal Entry Display
|
||||||
|
|
||||||
|
When displaying transactions to users (`views_api.py:747-751`):
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Extract sats equivalent from metadata
|
||||||
|
posting_meta = first_posting.get("meta", {})
|
||||||
|
sats_equiv = posting_meta.get("sats-equivalent")
|
||||||
|
if sats_equiv:
|
||||||
|
amount_sats = abs(int(sats_equiv))
|
||||||
|
```
|
||||||
|
|
||||||
|
This allows the UI to show both EUR and SATS amounts for each transaction.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Why Metadata Instead of Positions?
|
||||||
|
|
||||||
|
### The BQL Limitation
|
||||||
|
|
||||||
|
Beancount Query Language (BQL) **cannot access metadata**. This means:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- ✅ This works (queries position amounts):
|
||||||
|
SELECT account, sum(position) WHERE account ~ 'User-5987ae95'
|
||||||
|
-- Returns: EUR positions (not useful for satoshi balances)
|
||||||
|
|
||||||
|
-- ❌ This is NOT possible:
|
||||||
|
SELECT account, sum(meta["sats-equivalent"]) WHERE account ~ 'User-5987ae95'
|
||||||
|
-- Error: BQL cannot access metadata
|
||||||
|
```
|
||||||
|
|
||||||
|
### Why Castle Accepts This Trade-off
|
||||||
|
|
||||||
|
**Performance Analysis** (see `docs/BQL-BALANCE-QUERIES.md`):
|
||||||
|
1. **Caching solves the bottleneck**: 60-80% performance improvement from caching account/permission lookups
|
||||||
|
2. **Iteration is necessary anyway**: Even with BQL, we'd need to iterate postings to access metadata
|
||||||
|
3. **Manual aggregation is fast**: The actual summation is not the bottleneck
|
||||||
|
4. **Database queries are the bottleneck**: Solved by Phase 1 caching, not BQL
|
||||||
|
|
||||||
|
**Architectural Correctness > Query Performance**:
|
||||||
|
- ✅ Transactions recorded in their actual currency
|
||||||
|
- ✅ No artificial multi-currency positions
|
||||||
|
- ✅ Clean accounting reconciliation
|
||||||
|
- ✅ Exchange rate changes don't affect historical records
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Alternative Considered: Price Notation
|
||||||
|
|
||||||
|
### Price Notation Format (Not Implemented)
|
||||||
|
|
||||||
|
```beancount
|
||||||
|
2025-11-10 * "Groceries"
|
||||||
|
Expenses:Food -360.00 EUR @@ 337096 SATS
|
||||||
|
Liabilities:Payable:User-abc 360.00 EUR @@ 337096 SATS
|
||||||
|
```
|
||||||
|
|
||||||
|
**Pros:**
|
||||||
|
- ✅ BQL can query prices (enables BQL aggregation)
|
||||||
|
- ✅ Standard Beancount syntax
|
||||||
|
- ✅ SATS trackable via price database
|
||||||
|
|
||||||
|
**Cons:**
|
||||||
|
- ❌ Semantically incorrect: `@@` means "total price paid", not "equivalent value"
|
||||||
|
- ❌ Implies currency conversion happened (it didn't)
|
||||||
|
- ❌ Confuses future readers about transaction nature
|
||||||
|
- ❌ Complicates Beancount's price database
|
||||||
|
|
||||||
|
**Decision**: Metadata is more semantically correct for "reference value" than price notation.
|
||||||
|
|
||||||
|
See `docs/BQL-PRICE-NOTATION-SOLUTION.md` for full analysis.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Data Flow Example
|
||||||
|
|
||||||
|
### User Adds Expense
|
||||||
|
|
||||||
|
**User Action**: "I paid €36.93 cash for groceries"
|
||||||
|
|
||||||
|
**Castle's Internal Representation**:
|
||||||
|
```python
|
||||||
|
# User provides or Castle calculates:
|
||||||
|
fiat_amount = Decimal("36.93") # EUR
|
||||||
|
fiat_currency = "EUR"
|
||||||
|
amount_sats = 39669 # Calculated from exchange rate
|
||||||
|
|
||||||
|
# Create journal entry line:
|
||||||
|
line = CreateEntryLine(
|
||||||
|
account_id=expense_account.id,
|
||||||
|
amount=amount_sats, # Internal: always satoshis
|
||||||
|
metadata={
|
||||||
|
"fiat_currency": "EUR",
|
||||||
|
"fiat_amount": "36.93"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Beancount Entry Created** (`views_api.py:835-849`):
|
||||||
|
```beancount
|
||||||
|
2025-11-10 * "Groceries (36.93 EUR)" #expense-entry
|
||||||
|
Expenses:Food:Supplies 36.93 EUR
|
||||||
|
sats-equivalent: "39669"
|
||||||
|
Liabilities:Payable:User-5987ae95 -36.93 EUR
|
||||||
|
sats-equivalent: "39669"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Balance Calculation** (`fava_client.py:get_user_balance`):
|
||||||
|
```python
|
||||||
|
# Iterate all postings for user accounts
|
||||||
|
# For each posting:
|
||||||
|
# - Parse EUR amount: -36.93 EUR (credit to liability)
|
||||||
|
# - Extract sats-equivalent: "39669"
|
||||||
|
# - Apply sign: -36.93 is negative → sats = -39669
|
||||||
|
# - Accumulate: user_balance_sats += -39669
|
||||||
|
|
||||||
|
# Result: negative balance = Castle owes user
|
||||||
|
```
|
||||||
|
|
||||||
|
**User Balance Response**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"user_id": "5987ae95",
|
||||||
|
"balance": -39669, // Castle owes user 39,669 sats
|
||||||
|
"fiat_balances": {
|
||||||
|
"EUR": "-36.93" // Castle owes user €36.93
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Details
|
||||||
|
|
||||||
|
### Where It's Set
|
||||||
|
|
||||||
|
**Primary Location**: `views_api.py:835-849` (Creating journal entries)
|
||||||
|
|
||||||
|
All EUR-based postings get `sats-equivalent` metadata:
|
||||||
|
- Expense entries (user adds liability)
|
||||||
|
- Receivable entries (admin records what user owes)
|
||||||
|
- Revenue entries (direct income)
|
||||||
|
- Payment entries (settling balances)
|
||||||
|
|
||||||
|
### Where It's Read
|
||||||
|
|
||||||
|
**Primary Location**: `fava_client.py:239-247` (Balance calculation)
|
||||||
|
|
||||||
|
Used in:
|
||||||
|
1. `get_user_balance()` - Calculate individual user balance
|
||||||
|
2. `get_all_user_balances()` - Calculate all user balances
|
||||||
|
3. `get_journal_entries()` - Display transaction amounts
|
||||||
|
|
||||||
|
### Data Type and Format
|
||||||
|
|
||||||
|
- **Type**: String (Beancount metadata values must be strings or numbers)
|
||||||
|
- **Format**: Absolute value, no sign, no decimal point
|
||||||
|
- **Examples**:
|
||||||
|
- ✅ `"39669"` (correct)
|
||||||
|
- ✅ `"1000000"` (1M sats)
|
||||||
|
- ❌ `"-39669"` (incorrect: sign goes on EUR amount)
|
||||||
|
- ❌ `"396.69"` (incorrect: satoshis are integers)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Principles
|
||||||
|
|
||||||
|
### 1. Record in Transaction Currency
|
||||||
|
|
||||||
|
```beancount
|
||||||
|
# ✅ CORRECT: User paid EUR, record in EUR
|
||||||
|
Expenses:Food 36.93 EUR
|
||||||
|
sats-equivalent: "39669"
|
||||||
|
|
||||||
|
# ❌ WRONG: Recording Bitcoin when user paid cash
|
||||||
|
Expenses:Food 39669 SATS {36.93 EUR}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Preserve Historical Values
|
||||||
|
|
||||||
|
The `sats-equivalent` is the **exact satoshi amount at transaction time**. It does NOT change when exchange rates change.
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
- 2025-11-10: User pays €36.93 → 39,669 sats (rate: 1074.19 sats/EUR)
|
||||||
|
- 2025-11-15: Exchange rate changes to 1100 sats/EUR
|
||||||
|
- **Metadata stays**: `sats-equivalent: "39669"` ✅
|
||||||
|
- **If we used current rate**: Would become 40,623 sats ❌
|
||||||
|
|
||||||
|
### 3. Separate Fiat and Sats Balances
|
||||||
|
|
||||||
|
Castle tracks TWO independent balances:
|
||||||
|
- **Satoshi balance**: Sum of `sats-equivalent` metadata (primary)
|
||||||
|
- **Fiat balances**: Sum of EUR/USD position amounts (secondary)
|
||||||
|
|
||||||
|
These are calculated independently and don't cross-convert.
|
||||||
|
|
||||||
|
### 4. Absolute Values in Metadata
|
||||||
|
|
||||||
|
The sign (debit/credit) lives on the position amount, NOT the metadata.
|
||||||
|
|
||||||
|
```beancount
|
||||||
|
# Debit (expense increases):
|
||||||
|
Expenses:Food 36.93 EUR # Positive
|
||||||
|
sats-equivalent: "39669" # Absolute value
|
||||||
|
|
||||||
|
# Credit (liability increases):
|
||||||
|
Liabilities:Payable -36.93 EUR # Negative
|
||||||
|
sats-equivalent: "39669" # Same absolute value
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Migration Path
|
||||||
|
|
||||||
|
### Future: If We Change to SATS-Primary Format
|
||||||
|
|
||||||
|
**Hypothetical future format:**
|
||||||
|
```beancount
|
||||||
|
; SATS as position, EUR as cost:
|
||||||
|
2025-11-10 * "Groceries"
|
||||||
|
Expenses:Food 39669 SATS {36.93 EUR}
|
||||||
|
Liabilities:Payable:User-abc -39669 SATS {36.93 EUR}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Benefits:**
|
||||||
|
- ✅ BQL can query SATS directly
|
||||||
|
- ✅ No metadata parsing needed
|
||||||
|
- ✅ Standard Beancount cost accounting
|
||||||
|
|
||||||
|
**Migration Script** (conceptual):
|
||||||
|
```python
|
||||||
|
# Read all postings with sats-equivalent metadata
|
||||||
|
# For each posting:
|
||||||
|
# - Extract sats from metadata
|
||||||
|
# - Extract EUR from position
|
||||||
|
# - Rewrite as: "<sats> SATS {<eur> EUR}"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Decision**: Not implementing now because:
|
||||||
|
1. Current architecture is semantically correct
|
||||||
|
2. Performance is acceptable with caching
|
||||||
|
3. Migration would break existing tooling
|
||||||
|
4. EUR-primary aligns with accounting reality
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Related Documentation
|
||||||
|
|
||||||
|
- `docs/BQL-BALANCE-QUERIES.md` - Why BQL can't query metadata and performance analysis
|
||||||
|
- `docs/BQL-PRICE-NOTATION-SOLUTION.md` - Alternative using price notation (not implemented)
|
||||||
|
- `beancount_format.py` - Functions that create entries with sats-equivalent metadata
|
||||||
|
- `fava_client.py:get_user_balance()` - How metadata is parsed for balance calculation
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Technical Summary
|
||||||
|
|
||||||
|
**Field**: `sats-equivalent`
|
||||||
|
**Type**: Metadata (string)
|
||||||
|
**Location**: Beancount posting metadata
|
||||||
|
**Format**: Absolute satoshi amount as string (e.g., `"39669"`)
|
||||||
|
**Purpose**: Track Bitcoin equivalent of fiat transactions
|
||||||
|
**Primary Use**: Calculate user satoshi balances
|
||||||
|
**Sign Handling**: Inherits from position amount (EUR/USD)
|
||||||
|
**Queryable via BQL**: ❌ No (BQL cannot access metadata)
|
||||||
|
**Performance**: ✅ Acceptable with caching (60-80% improvement)
|
||||||
|
**Architectural Status**: ✅ Current production format
|
||||||
|
**Future Migration**: Possible to SATS-primary if needed
|
||||||
Loading…
Add table
Add a link
Reference in a new issue