861 lines
26 KiB
Markdown
861 lines
26 KiB
Markdown
# 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.*
|