libra/views_api/_shared.py
Padreug a7d7740a3a refactor(api): split views_api.py into a package of domain modules
Pure move — no logic changes. The 4,100-line single file becomes
views_api/ with one module per domain (accounts, entries, payments,
settings_reports, reconciliation, permissions, admin), each
registering full literal paths on its own APIRouter; __init__ builds
the combined libra_api_router so libra/__init__.py is untouched.
Shared imports/helpers live in views_api/_shared.py;
_extract_entry_id and _SYSTEM_LINK_PREFIXES move to
beancount_format.py (they are pure entry-dict parsing).

Route behavior is pinned by tests/test_route_table.py: the 73-route
set is unchanged, and the two order-sensitive families (the shadowed
/accounts/hierarchy wart — deliberately preserved here, fixed in the
next commit — and the admin sync literal/param pair) keep their
relative order inside a single module each.

Addresses CODE-REVIEW-2026-06 structure findings (views_api monolith).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 16:09:58 +02:00

177 lines
5.1 KiB
Python

"""Shared imports and helpers for the views_api package.
Everything module-level that the pre-split views_api.py defined above
its first route lives here; endpoint modules pull it in with a
wildcard import plus explicit imports for underscore-prefixed names
(which `import *` does not export).
"""
from datetime import datetime
from decimal import Decimal
from http import HTTPStatus
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException
from loguru import logger
from lnbits.core.models import User, WalletTypeInfo
from lnbits.decorators import (
check_super_user,
check_user_exists,
require_invoice_key,
)
from lnbits.utils.exchange_rates import allowed_currencies, fiat_amount_as_satoshis
from ..account_utils import VALID_ACCOUNT_PREFIXES, validate_account_name
from ..beancount_format import (
_SYSTEM_LINK_PREFIXES,
_extract_entry_id,
fiat_rate_metadata,
)
from ..user_lookup import get_username
from ..crud import (
approve_manual_payment_request,
check_balance_assertion,
create_account,
create_account_permission,
create_balance_assertion,
create_manual_payment_request,
db,
delete_account_permission,
delete_balance_assertion,
get_account,
get_account_by_name,
get_account_permission,
get_account_permissions,
get_all_accounts,
get_all_manual_payment_requests,
get_all_user_wallet_settings,
get_balance_assertion,
get_balance_assertions,
get_manual_payment_request,
get_or_create_user_account,
get_user_manual_payment_requests,
get_user_permissions,
get_user_permissions_with_inheritance,
reject_manual_payment_request,
)
from ..models import (
Account,
AccountPermission,
AccountType,
AccountWithPermissions,
AssertionStatus,
AssignUserRole,
BalanceAssertion,
BulkGrantPermission,
BulkGrantResult,
LibraSettings,
CreateAccount,
CreateAccountPermission,
CreateChartAccount,
CreateBalanceAssertion,
CreateEntryLine,
CreateJournalEntry,
CreateManualPaymentRequest,
CreateRole,
CreateRolePermission,
CreateUserEquityStatus,
ExpenseEntry,
GeneratePaymentInvoice,
IncomeEntry,
JournalEntry,
JournalEntryFlag,
ManualPaymentRequest,
PayUser,
PermissionType,
ReceivableEntry,
RecordPayment,
RevenueEntry,
Role,
RolePermission,
RoleWithPermissions,
SettleReceivable,
UpdateRole,
UserBalance,
UserEquityStatus,
UserInfo,
UserRole,
UserWalletSettings,
UserWithRoles,
)
from ..services import get_settings, get_user_wallet, update_settings, update_user_wallet
from ..auth import (
AuthContext,
require_authenticated,
require_authenticated_write,
require_super_user,
require_account_access,
require_user_data_access,
)
# Synthetic Beancount flags marking auto-generated entries (summarization,
# padding, transfers, conversions, unrealized gains, returns, merging) that
# should not appear in user-facing transaction lists. Mirrors Fava's
# _EXCL_FLAGS in fava/core/file.py.
_SYNTHETIC_FLAGS = frozenset({"S", "T", "C", "P", "U", "R", "M"})
# ===== HELPER FUNCTIONS =====
async def check_libra_wallet_configured() -> str:
"""Ensure libra wallet is configured, return wallet_id"""
settings = await get_settings("admin")
if not settings or not settings.libra_wallet_id:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail="Libra wallet not configured. Please contact the super user to configure the Libra wallet in settings.",
)
return settings.libra_wallet_id
async def check_user_wallet_configured(user_id: str) -> str:
"""Ensure user has configured their wallet, return wallet_id"""
from lnbits.settings import settings as lnbits_settings
# If user is super user, use the libra wallet
if user_id == lnbits_settings.super_user:
libra_settings = await get_settings("admin")
if libra_settings and libra_settings.libra_wallet_id:
return libra_settings.libra_wallet_id
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail="Libra wallet not configured. Please configure the Libra wallet in settings.",
)
# For regular users, check their personal wallet
user_wallet = await get_user_wallet(user_id)
if not user_wallet or not user_wallet.user_wallet_id:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail="You must configure your wallet in settings before using this feature.",
)
return user_wallet.user_wallet_id
# ===== UTILITY ENDPOINTS =====
_VALID_ACCOUNT_PREFIXES = VALID_ACCOUNT_PREFIXES
def _validate_account_name(name: str) -> None:
"""Raise HTTP 400 if ``name`` is not a syntactically valid Beancount account.
Thin HTTP wrapper around account_utils.validate_account_name — the
single source of truth for account-name syntax (libra-#51).
"""
try:
validate_account_name(name)
except ValueError as e:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail=str(e),
)