mirror of
https://github.com/elisiariocouto/leggen.git
synced 2025-12-14 00:22:16 +00:00
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>
This commit is contained in:
committed by
Elisiário Couto
parent
73d6bd32db
commit
91f53b35b1
238
leggend/api/routes/transactions.py
Normal file
238
leggend/api/routes/transactions.py
Normal file
@@ -0,0 +1,238 @@
|
||||
from typing import List, Optional
|
||||
from datetime import datetime, timedelta
|
||||
from fastapi import APIRouter, HTTPException, Query
|
||||
from loguru import logger
|
||||
|
||||
from leggend.api.models.common import APIResponse
|
||||
from leggend.api.models.accounts import 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("/transactions", response_model=APIResponse)
|
||||
async def get_all_transactions(
|
||||
limit: Optional[int] = Query(default=100, le=500),
|
||||
offset: Optional[int] = Query(default=0, ge=0),
|
||||
summary_only: bool = Query(default=True, description="Return transaction summaries only"),
|
||||
date_from: Optional[str] = Query(default=None, description="Filter from date (YYYY-MM-DD)"),
|
||||
date_to: Optional[str] = Query(default=None, description="Filter to date (YYYY-MM-DD)"),
|
||||
min_amount: Optional[float] = Query(default=None, description="Minimum transaction amount"),
|
||||
max_amount: Optional[float] = Query(default=None, description="Maximum transaction amount"),
|
||||
search: Optional[str] = Query(default=None, description="Search in transaction descriptions"),
|
||||
account_id: Optional[str] = Query(default=None, description="Filter by account ID")
|
||||
) -> APIResponse:
|
||||
"""Get all transactions across all accounts with filtering options"""
|
||||
try:
|
||||
# Get all requisitions and accounts
|
||||
requisitions_data = await gocardless_service.get_requisitions()
|
||||
all_accounts = set()
|
||||
|
||||
for req in requisitions_data.get("results", []):
|
||||
all_accounts.update(req.get("accounts", []))
|
||||
|
||||
# Filter by specific account if requested
|
||||
if account_id:
|
||||
if account_id not in all_accounts:
|
||||
raise HTTPException(status_code=404, detail="Account not found")
|
||||
all_accounts = {account_id}
|
||||
|
||||
all_transactions = []
|
||||
|
||||
# Collect transactions from all accounts
|
||||
for acc_id in all_accounts:
|
||||
try:
|
||||
account_details = await gocardless_service.get_account_details(acc_id)
|
||||
transactions_data = await gocardless_service.get_account_transactions(acc_id)
|
||||
|
||||
processed_transactions = database_service.process_transactions(
|
||||
acc_id, account_details, transactions_data
|
||||
)
|
||||
all_transactions.extend(processed_transactions)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get transactions for account {acc_id}: {e}")
|
||||
continue
|
||||
|
||||
# Apply filters
|
||||
filtered_transactions = all_transactions
|
||||
|
||||
# Date range filter
|
||||
if date_from:
|
||||
from_date = datetime.fromisoformat(date_from)
|
||||
filtered_transactions = [
|
||||
txn for txn in filtered_transactions
|
||||
if txn["transactionDate"] >= from_date
|
||||
]
|
||||
|
||||
if date_to:
|
||||
to_date = datetime.fromisoformat(date_to)
|
||||
filtered_transactions = [
|
||||
txn for txn in filtered_transactions
|
||||
if txn["transactionDate"] <= to_date
|
||||
]
|
||||
|
||||
# Amount filters
|
||||
if min_amount is not None:
|
||||
filtered_transactions = [
|
||||
txn for txn in filtered_transactions
|
||||
if txn["transactionValue"] >= min_amount
|
||||
]
|
||||
|
||||
if max_amount is not None:
|
||||
filtered_transactions = [
|
||||
txn for txn in filtered_transactions
|
||||
if txn["transactionValue"] <= max_amount
|
||||
]
|
||||
|
||||
# Search filter
|
||||
if search:
|
||||
search_lower = search.lower()
|
||||
filtered_transactions = [
|
||||
txn for txn in filtered_transactions
|
||||
if search_lower in txn["description"].lower()
|
||||
]
|
||||
|
||||
# Sort by date (newest first)
|
||||
filtered_transactions.sort(
|
||||
key=lambda x: x["transactionDate"],
|
||||
reverse=True
|
||||
)
|
||||
|
||||
# Apply pagination
|
||||
total_transactions = len(filtered_transactions)
|
||||
paginated_transactions = filtered_transactions[offset:offset + limit]
|
||||
|
||||
if summary_only:
|
||||
# Return simplified transaction summaries
|
||||
data = [
|
||||
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
|
||||
]
|
||||
else:
|
||||
# Return full transaction details
|
||||
data = [
|
||||
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
|
||||
]
|
||||
|
||||
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: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to get transactions: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/transactions/stats", response_model=APIResponse)
|
||||
async def get_transaction_stats(
|
||||
days: int = Query(default=30, description="Number of days to include in stats"),
|
||||
account_id: Optional[str] = Query(default=None, description="Filter by account ID")
|
||||
) -> APIResponse:
|
||||
"""Get transaction statistics for the last N days"""
|
||||
try:
|
||||
# Date range for stats
|
||||
end_date = datetime.now()
|
||||
start_date = end_date - timedelta(days=days)
|
||||
|
||||
# Get all transactions (reuse the existing endpoint logic)
|
||||
# This is a simplified implementation - in practice you might want to optimize this
|
||||
requisitions_data = await gocardless_service.get_requisitions()
|
||||
all_accounts = set()
|
||||
|
||||
for req in requisitions_data.get("results", []):
|
||||
all_accounts.update(req.get("accounts", []))
|
||||
|
||||
if account_id:
|
||||
if account_id not in all_accounts:
|
||||
raise HTTPException(status_code=404, detail="Account not found")
|
||||
all_accounts = {account_id}
|
||||
|
||||
all_transactions = []
|
||||
|
||||
for acc_id in all_accounts:
|
||||
try:
|
||||
account_details = await gocardless_service.get_account_details(acc_id)
|
||||
transactions_data = await gocardless_service.get_account_transactions(acc_id)
|
||||
|
||||
processed_transactions = database_service.process_transactions(
|
||||
acc_id, account_details, transactions_data
|
||||
)
|
||||
all_transactions.extend(processed_transactions)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get transactions for account {acc_id}: {e}")
|
||||
continue
|
||||
|
||||
# Filter transactions by date range
|
||||
recent_transactions = [
|
||||
txn for txn in all_transactions
|
||||
if start_date <= txn["transactionDate"] <= end_date
|
||||
]
|
||||
|
||||
# Calculate stats
|
||||
total_transactions = len(recent_transactions)
|
||||
total_income = sum(
|
||||
txn["transactionValue"]
|
||||
for txn in recent_transactions
|
||||
if txn["transactionValue"] > 0
|
||||
)
|
||||
total_expenses = sum(
|
||||
abs(txn["transactionValue"])
|
||||
for txn in recent_transactions
|
||||
if txn["transactionValue"] < 0
|
||||
)
|
||||
net_change = total_income - total_expenses
|
||||
|
||||
# Count by status
|
||||
booked_count = len([txn for txn in recent_transactions if txn["transactionStatus"] == "booked"])
|
||||
pending_count = len([txn for txn in recent_transactions if txn["transactionStatus"] == "pending"])
|
||||
|
||||
stats = {
|
||||
"period_days": days,
|
||||
"total_transactions": total_transactions,
|
||||
"booked_transactions": booked_count,
|
||||
"pending_transactions": pending_count,
|
||||
"total_income": round(total_income, 2),
|
||||
"total_expenses": round(total_expenses, 2),
|
||||
"net_change": round(net_change, 2),
|
||||
"average_transaction": round(
|
||||
sum(txn["transactionValue"] for txn in recent_transactions) / total_transactions, 2
|
||||
) if total_transactions > 0 else 0,
|
||||
"accounts_included": len(all_accounts)
|
||||
}
|
||||
|
||||
return APIResponse(
|
||||
success=True,
|
||||
data=stats,
|
||||
message=f"Transaction statistics for last {days} days"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get transaction stats: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to get transaction stats: {str(e)}")
|
||||
Reference in New Issue
Block a user