chore: Implement code review suggestions and format code.

This commit is contained in:
Elisiário Couto
2025-09-03 21:11:19 +01:00
committed by Elisiário Couto
parent 47164e8546
commit de3da84dff
42 changed files with 1144 additions and 966 deletions

View File

@@ -3,7 +3,12 @@ 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.api.models.accounts import (
AccountDetails,
AccountBalance,
Transaction,
TransactionSummary,
)
from leggend.services.gocardless_service import GoCardlessService
from leggend.services.database_service import DatabaseService
@@ -17,50 +22,56 @@ 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)
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
))
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"
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)}")
@@ -72,18 +83,20 @@ async def get_account_details(account_id: str) -> APIResponse:
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")
))
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"],
@@ -93,15 +106,15 @@ async def get_account_details(account_id: str) -> APIResponse:
currency=account_details.get("currency"),
created=account_details["created"],
last_accessed=account_details.get("last_accessed"),
balances=balances
balances=balances,
)
return APIResponse(
success=True,
data=account,
message=f"Account details retrieved for {account_id}"
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)}")
@@ -112,23 +125,25 @@ 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")
))
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}"
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)}")
@@ -139,22 +154,26 @@ 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")
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)
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]
paginated_transactions = processed_transactions[offset : offset + limit]
if summary_only:
# Return simplified transaction summaries
summaries = [
@@ -165,7 +184,7 @@ async def get_account_transactions(
amount=txn["transactionValue"],
currency=txn["transactionCurrency"],
status=txn["transactionStatus"],
account_id=txn["accountId"]
account_id=txn["accountId"],
)
for txn in paginated_transactions
]
@@ -183,18 +202,20 @@ async def get_account_transactions(
transaction_value=txn["transactionValue"],
transaction_currency=txn["transactionCurrency"],
transaction_status=txn["transactionStatus"],
raw_transaction=txn["rawTransaction"]
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})"
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)}")
raise HTTPException(
status_code=404, detail=f"Failed to get transactions: {str(e)}"
)

View File

@@ -4,10 +4,10 @@ from loguru import logger
from leggend.api.models.common import APIResponse, ErrorResponse
from leggend.api.models.banks import (
BankInstitution,
BankConnectionRequest,
BankInstitution,
BankConnectionRequest,
BankRequisition,
BankConnectionStatus
BankConnectionStatus,
)
from leggend.services.gocardless_service import GoCardlessService
from leggend.utils.gocardless import REQUISITION_STATUS
@@ -18,12 +18,12 @@ gocardless_service = GoCardlessService()
@router.get("/banks/institutions", response_model=APIResponse)
async def get_bank_institutions(
country: str = Query(default="PT", description="Country code (e.g., PT, ES, FR)")
country: str = Query(default="PT", description="Country code (e.g., PT, ES, FR)"),
) -> APIResponse:
"""Get available bank institutions for a country"""
try:
institutions_data = await gocardless_service.get_institutions(country)
institutions = [
BankInstitution(
id=inst["id"],
@@ -31,20 +31,22 @@ async def get_bank_institutions(
bic=inst.get("bic"),
transaction_total_days=inst["transaction_total_days"],
countries=inst["countries"],
logo=inst.get("logo")
logo=inst.get("logo"),
)
for inst in institutions_data
]
return APIResponse(
success=True,
data=institutions,
message=f"Found {len(institutions)} institutions for {country}"
message=f"Found {len(institutions)} institutions for {country}",
)
except Exception as e:
logger.error(f"Failed to get institutions for {country}: {e}")
raise HTTPException(status_code=500, detail=f"Failed to get institutions: {str(e)}")
raise HTTPException(
status_code=500, detail=f"Failed to get institutions: {str(e)}"
)
@router.post("/banks/connect", response_model=APIResponse)
@@ -52,28 +54,29 @@ async def connect_to_bank(request: BankConnectionRequest) -> APIResponse:
"""Create a connection to a bank (requisition)"""
try:
requisition_data = await gocardless_service.create_requisition(
request.institution_id,
request.redirect_url
request.institution_id, request.redirect_url
)
requisition = BankRequisition(
id=requisition_data["id"],
institution_id=requisition_data["institution_id"],
status=requisition_data["status"],
created=requisition_data["created"],
link=requisition_data["link"],
accounts=requisition_data.get("accounts", [])
accounts=requisition_data.get("accounts", []),
)
return APIResponse(
success=True,
data=requisition,
message=f"Bank connection created. Please visit the link to authorize."
message=f"Bank connection created. Please visit the link to authorize.",
)
except Exception as e:
logger.error(f"Failed to connect to bank {request.institution_id}: {e}")
raise HTTPException(status_code=500, detail=f"Failed to connect to bank: {str(e)}")
raise HTTPException(
status_code=500, detail=f"Failed to connect to bank: {str(e)}"
)
@router.get("/banks/status", response_model=APIResponse)
@@ -81,31 +84,37 @@ async def get_bank_connections_status() -> APIResponse:
"""Get status of all bank connections"""
try:
requisitions_data = await gocardless_service.get_requisitions()
connections = []
for req in requisitions_data.get("results", []):
status = req["status"]
status_display = REQUISITION_STATUS.get(status, "UNKNOWN")
connections.append(BankConnectionStatus(
bank_id=req["institution_id"],
bank_name=req["institution_id"], # Could be enhanced with actual bank names
status=status,
status_display=status_display,
created_at=req["created"],
requisition_id=req["id"],
accounts_count=len(req.get("accounts", []))
))
connections.append(
BankConnectionStatus(
bank_id=req["institution_id"],
bank_name=req[
"institution_id"
], # Could be enhanced with actual bank names
status=status,
status_display=status_display,
created_at=req["created"],
requisition_id=req["id"],
accounts_count=len(req.get("accounts", [])),
)
)
return APIResponse(
success=True,
data=connections,
message=f"Found {len(connections)} bank connections"
message=f"Found {len(connections)} bank connections",
)
except Exception as e:
logger.error(f"Failed to get bank connection status: {e}")
raise HTTPException(status_code=500, detail=f"Failed to get bank status: {str(e)}")
raise HTTPException(
status_code=500, detail=f"Failed to get bank status: {str(e)}"
)
@router.delete("/banks/connections/{requisition_id}", response_model=APIResponse)
@@ -116,12 +125,14 @@ async def delete_bank_connection(requisition_id: str) -> APIResponse:
# For now, return success
return APIResponse(
success=True,
message=f"Bank connection {requisition_id} deleted successfully"
message=f"Bank connection {requisition_id} deleted successfully",
)
except Exception as e:
logger.error(f"Failed to delete bank connection {requisition_id}: {e}")
raise HTTPException(status_code=500, detail=f"Failed to delete connection: {str(e)}")
raise HTTPException(
status_code=500, detail=f"Failed to delete connection: {str(e)}"
)
@router.get("/banks/countries", response_model=APIResponse)
@@ -160,9 +171,9 @@ async def get_supported_countries() -> APIResponse:
{"code": "SE", "name": "Sweden"},
{"code": "GB", "name": "United Kingdom"},
]
return APIResponse(
success=True,
data=countries,
message="Supported countries retrieved successfully"
)
message="Supported countries retrieved successfully",
)

View File

@@ -4,11 +4,11 @@ from loguru import logger
from leggend.api.models.common import APIResponse
from leggend.api.models.notifications import (
NotificationSettings,
NotificationTest,
NotificationSettings,
NotificationTest,
DiscordConfig,
TelegramConfig,
NotificationFilters
NotificationFilters,
)
from leggend.services.notification_service import NotificationService
from leggend.config import config
@@ -23,38 +23,44 @@ async def get_notification_settings() -> APIResponse:
try:
notifications_config = config.notifications_config
filters_config = config.filters_config
# Build response safely without exposing secrets
discord_config = notifications_config.get("discord", {})
telegram_config = notifications_config.get("telegram", {})
settings = NotificationSettings(
discord=DiscordConfig(
webhook="***" if discord_config.get("webhook") else "",
enabled=discord_config.get("enabled", True)
) if discord_config.get("webhook") else None,
enabled=discord_config.get("enabled", True),
)
if discord_config.get("webhook")
else None,
telegram=TelegramConfig(
token="***" if telegram_config.get("token") else "",
chat_id=telegram_config.get("chat_id", 0),
enabled=telegram_config.get("enabled", True)
) if telegram_config.get("token") else None,
enabled=telegram_config.get("enabled", True),
)
if telegram_config.get("token")
else None,
filters=NotificationFilters(
case_insensitive=filters_config.get("case-insensitive", {}),
case_sensitive=filters_config.get("case-sensitive"),
amount_threshold=filters_config.get("amount_threshold"),
keywords=filters_config.get("keywords", [])
)
keywords=filters_config.get("keywords", []),
),
)
return APIResponse(
success=True,
data=settings,
message="Notification settings retrieved successfully"
message="Notification settings retrieved successfully",
)
except Exception as e:
logger.error(f"Failed to get notification settings: {e}")
raise HTTPException(status_code=500, detail=f"Failed to get notification settings: {str(e)}")
raise HTTPException(
status_code=500, detail=f"Failed to get notification settings: {str(e)}"
)
@router.put("/notifications/settings", response_model=APIResponse)
@@ -63,20 +69,20 @@ async def update_notification_settings(settings: NotificationSettings) -> APIRes
try:
# Update notifications config
notifications_config = {}
if settings.discord:
notifications_config["discord"] = {
"webhook": settings.discord.webhook,
"enabled": settings.discord.enabled
"enabled": settings.discord.enabled,
}
if settings.telegram:
notifications_config["telegram"] = {
"token": settings.telegram.token,
"chat_id": settings.telegram.chat_id,
"enabled": settings.telegram.enabled
"enabled": settings.telegram.enabled,
}
# Update filters config
filters_config = {}
if settings.filters.case_insensitive:
@@ -87,22 +93,24 @@ async def update_notification_settings(settings: NotificationSettings) -> APIRes
filters_config["amount_threshold"] = settings.filters.amount_threshold
if settings.filters.keywords:
filters_config["keywords"] = settings.filters.keywords
# Save to config
if notifications_config:
config.update_section("notifications", notifications_config)
if filters_config:
config.update_section("filters", filters_config)
return APIResponse(
success=True,
data={"updated": True},
message="Notification settings updated successfully"
message="Notification settings updated successfully",
)
except Exception as e:
logger.error(f"Failed to update notification settings: {e}")
raise HTTPException(status_code=500, detail=f"Failed to update notification settings: {str(e)}")
raise HTTPException(
status_code=500, detail=f"Failed to update notification settings: {str(e)}"
)
@router.post("/notifications/test", response_model=APIResponse)
@@ -110,25 +118,26 @@ async def test_notification(test_request: NotificationTest) -> APIResponse:
"""Send a test notification"""
try:
success = await notification_service.send_test_notification(
test_request.service,
test_request.message
test_request.service, test_request.message
)
if success:
return APIResponse(
success=True,
data={"sent": True},
message=f"Test notification sent to {test_request.service} successfully"
message=f"Test notification sent to {test_request.service} successfully",
)
else:
return APIResponse(
success=False,
message=f"Failed to send test notification to {test_request.service}"
message=f"Failed to send test notification to {test_request.service}",
)
except Exception as e:
logger.error(f"Failed to send test notification: {e}")
raise HTTPException(status_code=500, detail=f"Failed to send test notification: {str(e)}")
raise HTTPException(
status_code=500, detail=f"Failed to send test notification: {str(e)}"
)
@router.get("/notifications/services", response_model=APIResponse)
@@ -136,37 +145,41 @@ async def get_notification_services() -> APIResponse:
"""Get available notification services and their status"""
try:
notifications_config = config.notifications_config
services = {
"discord": {
"name": "Discord",
"enabled": bool(notifications_config.get("discord", {}).get("webhook")),
"configured": bool(notifications_config.get("discord", {}).get("webhook")),
"active": notifications_config.get("discord", {}).get("enabled", True)
"configured": bool(
notifications_config.get("discord", {}).get("webhook")
),
"active": notifications_config.get("discord", {}).get("enabled", True),
},
"telegram": {
"name": "Telegram",
"name": "Telegram",
"enabled": bool(
notifications_config.get("telegram", {}).get("token") and
notifications_config.get("telegram", {}).get("chat_id")
notifications_config.get("telegram", {}).get("token")
and notifications_config.get("telegram", {}).get("chat_id")
),
"configured": bool(
notifications_config.get("telegram", {}).get("token") and
notifications_config.get("telegram", {}).get("chat_id")
notifications_config.get("telegram", {}).get("token")
and notifications_config.get("telegram", {}).get("chat_id")
),
"active": notifications_config.get("telegram", {}).get("enabled", True)
}
"active": notifications_config.get("telegram", {}).get("enabled", True),
},
}
return APIResponse(
success=True,
data=services,
message="Notification services status retrieved successfully"
message="Notification services status retrieved successfully",
)
except Exception as e:
logger.error(f"Failed to get notification services: {e}")
raise HTTPException(status_code=500, detail=f"Failed to get notification services: {str(e)}")
raise HTTPException(
status_code=500, detail=f"Failed to get notification services: {str(e)}"
)
@router.delete("/notifications/settings/{service}", response_model=APIResponse)
@@ -174,19 +187,23 @@ async def delete_notification_service(service: str) -> APIResponse:
"""Delete/disable a notification service"""
try:
if service not in ["discord", "telegram"]:
raise HTTPException(status_code=400, detail="Service must be 'discord' or 'telegram'")
raise HTTPException(
status_code=400, detail="Service must be 'discord' or 'telegram'"
)
notifications_config = config.notifications_config.copy()
if service in notifications_config:
del notifications_config[service]
config.update_section("notifications", notifications_config)
return APIResponse(
success=True,
data={"deleted": service},
message=f"{service.capitalize()} notification service deleted successfully"
message=f"{service.capitalize()} notification service deleted successfully",
)
except Exception as e:
logger.error(f"Failed to delete notification service {service}: {e}")
raise HTTPException(status_code=500, detail=f"Failed to delete notification service: {str(e)}")
raise HTTPException(
status_code=500, detail=f"Failed to delete notification service: {str(e)}"
)

View File

@@ -17,27 +17,26 @@ async def get_sync_status() -> APIResponse:
"""Get current sync status"""
try:
status = await sync_service.get_sync_status()
# Add scheduler information
next_sync_time = scheduler.get_next_sync_time()
if next_sync_time:
status.next_sync = next_sync_time
return APIResponse(
success=True,
data=status,
message="Sync status retrieved successfully"
success=True, data=status, message="Sync status retrieved successfully"
)
except Exception as e:
logger.error(f"Failed to get sync status: {e}")
raise HTTPException(status_code=500, detail=f"Failed to get sync status: {str(e)}")
raise HTTPException(
status_code=500, detail=f"Failed to get sync status: {str(e)}"
)
@router.post("/sync", response_model=APIResponse)
async def trigger_sync(
background_tasks: BackgroundTasks,
sync_request: Optional[SyncRequest] = None
background_tasks: BackgroundTasks, sync_request: Optional[SyncRequest] = None
) -> APIResponse:
"""Trigger a manual sync operation"""
try:
@@ -46,32 +45,37 @@ async def trigger_sync(
if status.is_running and not (sync_request and sync_request.force):
return APIResponse(
success=False,
message="Sync is already running. Use 'force: true' to override."
message="Sync is already running. Use 'force: true' to override.",
)
# Determine what to sync
if sync_request and sync_request.account_ids:
# Sync specific accounts in background
background_tasks.add_task(
sync_service.sync_specific_accounts,
sync_request.account_ids,
sync_request.force if sync_request else False
sync_service.sync_specific_accounts,
sync_request.account_ids,
sync_request.force if sync_request else False,
)
message = (
f"Started sync for {len(sync_request.account_ids)} specific accounts"
)
message = f"Started sync for {len(sync_request.account_ids)} specific accounts"
else:
# Sync all accounts in background
background_tasks.add_task(
sync_service.sync_all_accounts,
sync_request.force if sync_request else False
sync_service.sync_all_accounts,
sync_request.force if sync_request else False,
)
message = "Started sync for all accounts"
return APIResponse(
success=True,
data={"sync_started": True, "force": sync_request.force if sync_request else False},
message=message
data={
"sync_started": True,
"force": sync_request.force if sync_request else False,
},
message=message,
)
except Exception as e:
logger.error(f"Failed to trigger sync: {e}")
raise HTTPException(status_code=500, detail=f"Failed to trigger sync: {str(e)}")
@@ -83,20 +87,21 @@ async def sync_now(sync_request: Optional[SyncRequest] = None) -> APIResponse:
try:
if sync_request and sync_request.account_ids:
result = await sync_service.sync_specific_accounts(
sync_request.account_ids,
sync_request.force
sync_request.account_ids, sync_request.force
)
else:
result = await sync_service.sync_all_accounts(
sync_request.force if sync_request else False
)
return APIResponse(
success=result.success,
data=result,
message="Sync completed" if result.success else f"Sync failed with {len(result.errors)} errors"
message="Sync completed"
if result.success
else f"Sync failed with {len(result.errors)} errors",
)
except Exception as e:
logger.error(f"Failed to run sync: {e}")
raise HTTPException(status_code=500, detail=f"Failed to run sync: {str(e)}")
@@ -108,22 +113,28 @@ async def get_scheduler_config() -> APIResponse:
try:
scheduler_config = config.scheduler_config
next_sync_time = scheduler.get_next_sync_time()
response_data = {
**scheduler_config,
"next_scheduled_sync": next_sync_time.isoformat() if next_sync_time else None,
"is_running": scheduler.scheduler.running if hasattr(scheduler, 'scheduler') else False
"next_scheduled_sync": next_sync_time.isoformat()
if next_sync_time
else None,
"is_running": scheduler.scheduler.running
if hasattr(scheduler, "scheduler")
else False,
}
return APIResponse(
success=True,
data=response_data,
message="Scheduler configuration retrieved successfully"
message="Scheduler configuration retrieved successfully",
)
except Exception as e:
logger.error(f"Failed to get scheduler config: {e}")
raise HTTPException(status_code=500, detail=f"Failed to get scheduler config: {str(e)}")
raise HTTPException(
status_code=500, detail=f"Failed to get scheduler config: {str(e)}"
)
@router.put("/sync/scheduler", response_model=APIResponse)
@@ -135,26 +146,32 @@ async def update_scheduler_config(scheduler_config: SchedulerConfig) -> APIRespo
try:
cron_parts = scheduler_config.cron.split()
if len(cron_parts) != 5:
raise ValueError("Cron expression must have 5 parts: minute hour day month day_of_week")
raise ValueError(
"Cron expression must have 5 parts: minute hour day month day_of_week"
)
except Exception as e:
raise HTTPException(status_code=400, detail=f"Invalid cron expression: {str(e)}")
raise HTTPException(
status_code=400, detail=f"Invalid cron expression: {str(e)}"
)
# Update configuration
schedule_data = scheduler_config.dict(exclude_none=True)
config.update_section("scheduler", {"sync": schedule_data})
# Reschedule the job
scheduler.reschedule_sync(schedule_data)
return APIResponse(
success=True,
data=schedule_data,
message="Scheduler configuration updated successfully"
message="Scheduler configuration updated successfully",
)
except Exception as e:
logger.error(f"Failed to update scheduler config: {e}")
raise HTTPException(status_code=500, detail=f"Failed to update scheduler config: {str(e)}")
raise HTTPException(
status_code=500, detail=f"Failed to update scheduler config: {str(e)}"
)
@router.post("/sync/scheduler/start", response_model=APIResponse)
@@ -163,37 +180,29 @@ async def start_scheduler() -> APIResponse:
try:
if not scheduler.scheduler.running:
scheduler.start()
return APIResponse(
success=True,
message="Scheduler started successfully"
)
return APIResponse(success=True, message="Scheduler started successfully")
else:
return APIResponse(
success=True,
message="Scheduler is already running"
)
return APIResponse(success=True, message="Scheduler is already running")
except Exception as e:
logger.error(f"Failed to start scheduler: {e}")
raise HTTPException(status_code=500, detail=f"Failed to start scheduler: {str(e)}")
raise HTTPException(
status_code=500, detail=f"Failed to start scheduler: {str(e)}"
)
@router.post("/sync/scheduler/stop", response_model=APIResponse)
@router.post("/sync/scheduler/stop", response_model=APIResponse)
async def stop_scheduler() -> APIResponse:
"""Stop the background scheduler"""
try:
if scheduler.scheduler.running:
scheduler.shutdown()
return APIResponse(
success=True,
message="Scheduler stopped successfully"
)
return APIResponse(success=True, message="Scheduler stopped successfully")
else:
return APIResponse(
success=True,
message="Scheduler is already stopped"
)
return APIResponse(success=True, message="Scheduler is already stopped")
except Exception as e:
logger.error(f"Failed to stop scheduler: {e}")
raise HTTPException(status_code=500, detail=f"Failed to stop scheduler: {str(e)}")
raise HTTPException(
status_code=500, detail=f"Failed to stop scheduler: {str(e)}"
)

View File

@@ -17,95 +17,111 @@ database_service = DatabaseService()
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")
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)
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
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
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
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
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
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
)
filtered_transactions.sort(key=lambda x: x["transactionDate"], reverse=True)
# Apply pagination
total_transactions = len(filtered_transactions)
paginated_transactions = filtered_transactions[offset:offset + limit]
paginated_transactions = filtered_transactions[offset : offset + limit]
if summary_only:
# Return simplified transaction summaries
data = [
@@ -116,7 +132,7 @@ async def get_all_transactions(
amount=txn["transactionValue"],
currency=txn["transactionCurrency"],
status=txn["transactionStatus"],
account_id=txn["accountId"]
account_id=txn["accountId"],
)
for txn in paginated_transactions
]
@@ -133,86 +149,99 @@ async def get_all_transactions(
transaction_value=txn["transactionValue"],
transaction_currency=txn["transactionCurrency"],
transaction_status=txn["transactionStatus"],
raw_transaction=txn["rawTransaction"]
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})"
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)}")
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")
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)
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
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
txn["transactionValue"]
for txn in recent_transactions
if txn["transactionValue"] > 0
)
total_expenses = sum(
abs(txn["transactionValue"])
for txn in recent_transactions
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"])
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,
@@ -222,17 +251,23 @@ async def get_transaction_stats(
"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)
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"
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)}")
raise HTTPException(
status_code=500, detail=f"Failed to get transaction stats: {str(e)}"
)