Completes core logic refactoring (Phase 3)
Refactors the accounting logic into a clean, testable core module, separating business logic from database operations. This improves code quality, maintainability, and testability by creating a dedicated `core/` module, implementing `CastleInventory` for position tracking, moving balance calculations to `core/balance.py`, and adding comprehensive validation in `core/validation.py`.
This commit is contained in:
parent
6d84479f7d
commit
9c0bdc58eb
7 changed files with 1204 additions and 123 deletions
168
crud.py
168
crud.py
|
|
@ -23,6 +23,18 @@ from .models import (
|
|||
UserWalletSettings,
|
||||
)
|
||||
|
||||
# Import core accounting logic
|
||||
from .core.balance import BalanceCalculator, AccountType as CoreAccountType
|
||||
from .core.inventory import CastleInventory, CastlePosition
|
||||
from .core.validation import (
|
||||
ValidationError,
|
||||
validate_journal_entry,
|
||||
validate_balance,
|
||||
validate_receivable_entry,
|
||||
validate_expense_entry,
|
||||
validate_payment_entry,
|
||||
)
|
||||
|
||||
db = Database("ext_castle")
|
||||
|
||||
|
||||
|
|
@ -358,13 +370,11 @@ async def get_account_balance(account_id: str) -> int:
|
|||
total_debit = result["total_debit"]
|
||||
total_credit = result["total_credit"]
|
||||
|
||||
# Normal balance for each account type:
|
||||
# Assets and Expenses: Debit balance (debit - credit)
|
||||
# Liabilities, Equity, and Revenue: Credit balance (credit - debit)
|
||||
if account.account_type in [AccountType.ASSET, AccountType.EXPENSE]:
|
||||
return total_debit - total_credit
|
||||
else:
|
||||
return total_credit - total_debit
|
||||
# Use core BalanceCalculator for consistent logic
|
||||
core_account_type = CoreAccountType(account.account_type.value)
|
||||
return BalanceCalculator.calculate_account_balance(
|
||||
total_debit, total_credit, core_account_type
|
||||
)
|
||||
|
||||
|
||||
async def get_user_balance(user_id: str) -> UserBalance:
|
||||
|
|
@ -376,13 +386,16 @@ async def get_user_balance(user_id: str) -> UserBalance:
|
|||
Account,
|
||||
)
|
||||
|
||||
total_balance = 0
|
||||
fiat_balances = {} # Track fiat balances by currency
|
||||
# Calculate balances for each account
|
||||
account_balances = {}
|
||||
account_inventories = {}
|
||||
|
||||
for account in user_accounts:
|
||||
# Get satoshi balance
|
||||
balance = await get_account_balance(account.id)
|
||||
account_balances[account.id] = balance
|
||||
|
||||
# Get all entry lines for this account to calculate fiat balances
|
||||
# Get all entry lines for this account to build inventory
|
||||
# Only include cleared entries (exclude pending/flagged/voided)
|
||||
entry_lines = await db.fetchall(
|
||||
"""
|
||||
|
|
@ -395,49 +408,30 @@ async def get_user_balance(user_id: str) -> UserBalance:
|
|||
{"account_id": account.id},
|
||||
)
|
||||
|
||||
for line in entry_lines:
|
||||
# Parse metadata to get fiat amounts
|
||||
metadata = json.loads(line["metadata"]) if line.get("metadata") else {}
|
||||
fiat_currency = metadata.get("fiat_currency")
|
||||
fiat_amount = metadata.get("fiat_amount")
|
||||
# Use BalanceCalculator to build inventory from entry lines
|
||||
core_account_type = CoreAccountType(account.account_type.value)
|
||||
inventory = BalanceCalculator.build_inventory_from_entry_lines(
|
||||
[dict(line) for line in entry_lines],
|
||||
core_account_type
|
||||
)
|
||||
account_inventories[account.id] = inventory
|
||||
|
||||
if fiat_currency and fiat_amount:
|
||||
from decimal import Decimal
|
||||
# Initialize currency if not exists
|
||||
if fiat_currency not in fiat_balances:
|
||||
fiat_balances[fiat_currency] = Decimal("0")
|
||||
|
||||
# Convert fiat_amount to Decimal
|
||||
fiat_decimal = Decimal(str(fiat_amount))
|
||||
|
||||
# Calculate fiat balance based on account type
|
||||
if account.account_type == AccountType.LIABILITY:
|
||||
# Liability: credit increases (castle owes more), debit decreases
|
||||
if line["credit"] > 0:
|
||||
fiat_balances[fiat_currency] += fiat_decimal
|
||||
elif line["debit"] > 0:
|
||||
fiat_balances[fiat_currency] -= fiat_decimal
|
||||
elif account.account_type == AccountType.ASSET:
|
||||
# Asset (receivable): debit increases (user owes more), credit decreases
|
||||
if line["debit"] > 0:
|
||||
fiat_balances[fiat_currency] -= fiat_decimal
|
||||
elif line["credit"] > 0:
|
||||
fiat_balances[fiat_currency] += fiat_decimal
|
||||
|
||||
# Calculate satoshi balance
|
||||
# If it's a liability account (castle owes user), it's positive
|
||||
# If it's an asset account (user owes castle), it's negative
|
||||
if account.account_type == AccountType.LIABILITY:
|
||||
total_balance += balance
|
||||
elif account.account_type == AccountType.ASSET:
|
||||
total_balance -= balance
|
||||
# Equity contributions are tracked but don't affect what castle owes
|
||||
# Use BalanceCalculator to calculate total user balance
|
||||
accounts_list = [
|
||||
{"id": acc.id, "account_type": acc.account_type.value}
|
||||
for acc in user_accounts
|
||||
]
|
||||
balance_result = BalanceCalculator.calculate_user_balance(
|
||||
accounts_list,
|
||||
account_balances,
|
||||
account_inventories
|
||||
)
|
||||
|
||||
return UserBalance(
|
||||
user_id=user_id,
|
||||
balance=total_balance,
|
||||
balance=balance_result["balance"],
|
||||
accounts=user_accounts,
|
||||
fiat_balances=fiat_balances,
|
||||
fiat_balances=balance_result["fiat_balances"],
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -450,79 +444,17 @@ async def get_all_user_balances() -> list[UserBalance]:
|
|||
Account,
|
||||
)
|
||||
|
||||
# Group by user_id
|
||||
users_dict = {}
|
||||
for account in all_accounts:
|
||||
if account.user_id not in users_dict:
|
||||
users_dict[account.user_id] = []
|
||||
users_dict[account.user_id].append(account)
|
||||
# Get unique user IDs
|
||||
user_ids = set(account.user_id for account in all_accounts if account.user_id)
|
||||
|
||||
# Calculate balance for each user
|
||||
# Calculate balance for each user using the refactored function
|
||||
user_balances = []
|
||||
for user_id, accounts in users_dict.items():
|
||||
total_balance = 0
|
||||
fiat_balances = {}
|
||||
for user_id in user_ids:
|
||||
balance = await get_user_balance(user_id)
|
||||
|
||||
for account in accounts:
|
||||
balance = await get_account_balance(account.id)
|
||||
|
||||
# Get all entry lines for this account to calculate fiat balances
|
||||
# Only include cleared entries (exclude pending/flagged/voided)
|
||||
entry_lines = await db.fetchall(
|
||||
"""
|
||||
SELECT el.*
|
||||
FROM entry_lines el
|
||||
JOIN journal_entries je ON el.journal_entry_id = je.id
|
||||
WHERE el.account_id = :account_id
|
||||
AND je.flag = '*'
|
||||
""",
|
||||
{"account_id": account.id},
|
||||
)
|
||||
|
||||
for line in entry_lines:
|
||||
# Parse metadata to get fiat amounts
|
||||
metadata = json.loads(line["metadata"]) if line.get("metadata") else {}
|
||||
fiat_currency = metadata.get("fiat_currency")
|
||||
fiat_amount = metadata.get("fiat_amount")
|
||||
|
||||
if fiat_currency and fiat_amount:
|
||||
from decimal import Decimal
|
||||
# Initialize currency if not exists
|
||||
if fiat_currency not in fiat_balances:
|
||||
fiat_balances[fiat_currency] = Decimal("0")
|
||||
|
||||
# Convert fiat_amount to Decimal
|
||||
fiat_decimal = Decimal(str(fiat_amount))
|
||||
|
||||
# Calculate fiat balance based on account type
|
||||
if account.account_type == AccountType.LIABILITY:
|
||||
# Liability: credit increases (castle owes more), debit decreases
|
||||
if line["credit"] > 0:
|
||||
fiat_balances[fiat_currency] += fiat_decimal
|
||||
elif line["debit"] > 0:
|
||||
fiat_balances[fiat_currency] -= fiat_decimal
|
||||
elif account.account_type == AccountType.ASSET:
|
||||
# Asset (receivable): debit increases (user owes more), credit decreases
|
||||
if line["debit"] > 0:
|
||||
fiat_balances[fiat_currency] -= fiat_decimal
|
||||
elif line["credit"] > 0:
|
||||
fiat_balances[fiat_currency] += fiat_decimal
|
||||
|
||||
# Calculate satoshi balance
|
||||
if account.account_type == AccountType.LIABILITY:
|
||||
total_balance += balance
|
||||
elif account.account_type == AccountType.ASSET:
|
||||
total_balance -= balance
|
||||
|
||||
if total_balance != 0 or fiat_balances: # Include users with non-zero balance or fiat balances
|
||||
user_balances.append(
|
||||
UserBalance(
|
||||
user_id=user_id,
|
||||
balance=total_balance,
|
||||
accounts=accounts,
|
||||
fiat_balances=fiat_balances,
|
||||
)
|
||||
)
|
||||
# Include users with non-zero balance or fiat balances
|
||||
if balance.balance != 0 or balance.fiat_balances:
|
||||
user_balances.append(balance)
|
||||
|
||||
return user_balances
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue