Files
leggen/leggend/api/routes/accounts.py
Elisiário Couto 91f53b35b1 feat: Transform to web architecture with FastAPI backend
This major update transforms leggen from CLI-only to a web-ready
architecture while maintaining full CLI compatibility.

New Features:
- FastAPI backend service (leggend) with comprehensive REST API
- Background job scheduler with configurable cron (replaces Ofelia)
- All CLI commands refactored to use API endpoints
- Docker configuration updated for new services
- API client with health checks and error handling

API Endpoints:
- /api/v1/banks/* - Bank connections and institutions
- /api/v1/accounts/* - Account management and balances
- /api/v1/transactions/* - Transaction retrieval with filtering
- /api/v1/sync/* - Manual sync and scheduler configuration
- /api/v1/notifications/* - Notification settings management

CLI Enhancements:
- New --api-url option and LEGGEND_API_URL environment variable
- Enhanced sync command with --wait and --force options
- Improved transactions command with --full and --limit options
- Automatic fallback and health checking

Breaking Changes:
- compose.yml structure updated (leggend service added)
- Ofelia scheduler removed (internal scheduler used instead)

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-09 19:39:11 +01:00

200 lines
8.2 KiB
Python

from typing import List, Optional
from fastapi import APIRouter, HTTPException, Query
from loguru import logger
from leggend.api.models.common import APIResponse
from leggend.api.models.accounts import AccountDetails, AccountBalance, Transaction, TransactionSummary
from leggend.services.gocardless_service import GoCardlessService
from leggend.services.database_service import DatabaseService
router = APIRouter()
gocardless_service = GoCardlessService()
database_service = DatabaseService()
@router.get("/accounts", response_model=APIResponse)
async def get_all_accounts() -> APIResponse:
"""Get all connected accounts"""
try:
requisitions_data = await gocardless_service.get_requisitions()
all_accounts = set()
for req in requisitions_data.get("results", []):
all_accounts.update(req.get("accounts", []))
accounts = []
for account_id in all_accounts:
try:
account_details = await gocardless_service.get_account_details(account_id)
balances_data = await gocardless_service.get_account_balances(account_id)
# Process balances
balances = []
for balance in balances_data.get("balances", []):
balance_amount = balance["balanceAmount"]
balances.append(AccountBalance(
amount=float(balance_amount["amount"]),
currency=balance_amount["currency"],
balance_type=balance["balanceType"],
last_change_date=balance.get("lastChangeDateTime")
))
accounts.append(AccountDetails(
id=account_details["id"],
institution_id=account_details["institution_id"],
status=account_details["status"],
iban=account_details.get("iban"),
name=account_details.get("name"),
currency=account_details.get("currency"),
created=account_details["created"],
last_accessed=account_details.get("last_accessed"),
balances=balances
))
except Exception as e:
logger.error(f"Failed to get details for account {account_id}: {e}")
continue
return APIResponse(
success=True,
data=accounts,
message=f"Retrieved {len(accounts)} accounts"
)
except Exception as e:
logger.error(f"Failed to get accounts: {e}")
raise HTTPException(status_code=500, detail=f"Failed to get accounts: {str(e)}")
@router.get("/accounts/{account_id}", response_model=APIResponse)
async def get_account_details(account_id: str) -> APIResponse:
"""Get details for a specific account"""
try:
account_details = await gocardless_service.get_account_details(account_id)
balances_data = await gocardless_service.get_account_balances(account_id)
# Process balances
balances = []
for balance in balances_data.get("balances", []):
balance_amount = balance["balanceAmount"]
balances.append(AccountBalance(
amount=float(balance_amount["amount"]),
currency=balance_amount["currency"],
balance_type=balance["balanceType"],
last_change_date=balance.get("lastChangeDateTime")
))
account = AccountDetails(
id=account_details["id"],
institution_id=account_details["institution_id"],
status=account_details["status"],
iban=account_details.get("iban"),
name=account_details.get("name"),
currency=account_details.get("currency"),
created=account_details["created"],
last_accessed=account_details.get("last_accessed"),
balances=balances
)
return APIResponse(
success=True,
data=account,
message=f"Account details retrieved for {account_id}"
)
except Exception as e:
logger.error(f"Failed to get account details for {account_id}: {e}")
raise HTTPException(status_code=404, detail=f"Account not found: {str(e)}")
@router.get("/accounts/{account_id}/balances", response_model=APIResponse)
async def get_account_balances(account_id: str) -> APIResponse:
"""Get balances for a specific account"""
try:
balances_data = await gocardless_service.get_account_balances(account_id)
balances = []
for balance in balances_data.get("balances", []):
balance_amount = balance["balanceAmount"]
balances.append(AccountBalance(
amount=float(balance_amount["amount"]),
currency=balance_amount["currency"],
balance_type=balance["balanceType"],
last_change_date=balance.get("lastChangeDateTime")
))
return APIResponse(
success=True,
data=balances,
message=f"Retrieved {len(balances)} balances for account {account_id}"
)
except Exception as e:
logger.error(f"Failed to get balances for account {account_id}: {e}")
raise HTTPException(status_code=404, detail=f"Failed to get balances: {str(e)}")
@router.get("/accounts/{account_id}/transactions", response_model=APIResponse)
async def get_account_transactions(
account_id: str,
limit: Optional[int] = Query(default=100, le=500),
offset: Optional[int] = Query(default=0, ge=0),
summary_only: bool = Query(default=False, description="Return transaction summaries only")
) -> APIResponse:
"""Get transactions for a specific account"""
try:
account_details = await gocardless_service.get_account_details(account_id)
transactions_data = await gocardless_service.get_account_transactions(account_id)
# Process transactions
processed_transactions = database_service.process_transactions(
account_id, account_details, transactions_data
)
# Apply pagination
total_transactions = len(processed_transactions)
paginated_transactions = processed_transactions[offset:offset + limit]
if summary_only:
# Return simplified transaction summaries
summaries = [
TransactionSummary(
internal_transaction_id=txn["internalTransactionId"],
date=txn["transactionDate"],
description=txn["description"],
amount=txn["transactionValue"],
currency=txn["transactionCurrency"],
status=txn["transactionStatus"],
account_id=txn["accountId"]
)
for txn in paginated_transactions
]
data = summaries
else:
# Return full transaction details
transactions = [
Transaction(
internal_transaction_id=txn["internalTransactionId"],
institution_id=txn["institutionId"],
iban=txn["iban"],
account_id=txn["accountId"],
transaction_date=txn["transactionDate"],
description=txn["description"],
transaction_value=txn["transactionValue"],
transaction_currency=txn["transactionCurrency"],
transaction_status=txn["transactionStatus"],
raw_transaction=txn["rawTransaction"]
)
for txn in paginated_transactions
]
data = transactions
return APIResponse(
success=True,
data=data,
message=f"Retrieved {len(data)} transactions (showing {offset + 1}-{offset + len(data)} of {total_transactions})"
)
except Exception as e:
logger.error(f"Failed to get transactions for account {account_id}: {e}")
raise HTTPException(status_code=404, detail=f"Failed to get transactions: {str(e)}")