| import asyncio |
| from datetime import datetime, timedelta |
| import logging |
| import os |
| from typing import Any, Dict, List, Optional |
| from fastapi import APIRouter, Depends, HTTPException, Query |
| from pydantic import BaseModel |
|
|
| from integrations.microsoft365_service import microsoft365_service |
| try: |
| from integrations.stripe_service import stripe_service |
| HAS_STRIPE = True |
| except ImportError: |
| |
| stripe_service = None |
| HAS_STRIPE = False |
|
|
| from integrations.xero_service import XeroService |
| from integrations.zoho_books_service import ZohoBooksService |
|
|
| router = APIRouter(prefix="/api/atom/finance/live", tags=["finance-live"]) |
| logger = logging.getLogger(__name__) |
|
|
| |
|
|
| class UnifiedTransaction(BaseModel): |
| id: str |
| description: str |
| amount: float |
| currency: str |
| date: str |
| status: str |
| platform: str |
| customer_name: Optional[str] = None |
| url: Optional[str] = None |
|
|
| class FinanceStats(BaseModel): |
| total_revenue: float |
| pending_revenue: float |
| transaction_count: int |
| platform_breakdown: Dict[str, float] |
|
|
| class LiveFinanceResponse(BaseModel): |
| ok: bool = True |
| stats: FinanceStats |
| transactions: List[UnifiedTransaction] |
| providers: Dict[str, bool] |
|
|
| |
|
|
| def map_stripe_payment(payment: Dict[str, Any]) -> UnifiedTransaction: |
| amount = float(payment.get("amount", 0)) / 100.0 |
| return UnifiedTransaction( |
| id=payment.get("id"), |
| description=payment.get("description") or "Stripe Payment", |
| amount=amount, |
| currency=payment.get("currency", "usd"), |
| date=datetime.fromtimestamp(payment.get("created", 0)).isoformat(), |
| status=payment.get("status", "unknown"), |
| platform="stripe", |
| customer_name=None, |
| url=f"https://dashboard.stripe.com/payments/{payment.get('id')}" |
| ) |
|
|
| def map_xero_invoice(invoice: Dict[str, Any]) -> UnifiedTransaction: |
| return UnifiedTransaction( |
| id=invoice.get("InvoiceID"), |
| description=f"Invoice #{invoice.get('InvoiceNumber')}", |
| amount=float(invoice.get("Total", 0.0)), |
| currency=invoice.get("CurrencyCode", "USD"), |
| date=invoice.get("DateString", "") or datetime.now().isoformat(), |
| status=invoice.get("Status", "unknown"), |
| platform="xero", |
| customer_name=invoice.get("Contact", {}).get("Name"), |
| url=None |
| ) |
|
|
| def map_zoho_invoice(invoice: Dict[str, Any]) -> UnifiedTransaction: |
| return UnifiedTransaction( |
| id=invoice.get("invoice_id"), |
| description=f"Invoice {invoice.get('invoice_number')}", |
| amount=float(invoice.get("total", 0.0)), |
| currency=invoice.get("currency_code", "USD"), |
| date=invoice.get("date"), |
| status=invoice.get("status"), |
| customer_name=invoice.get("customer_name") |
| ) |
|
|
| def map_dynamics_invoice(invoice: Dict[str, Any]) -> UnifiedTransaction: |
| |
| return UnifiedTransaction( |
| id=invoice.get("id", "dynamics_invoice"), |
| description=invoice.get("resourceVisualization", {}).get("title") or "Dynamics Invoice", |
| amount=0.0, |
| currency="USD", |
| date=datetime.now().isoformat(), |
| status="active", |
| platform="dynamics", |
| url=invoice.get("resourceReference", {}).get("webUrl") |
| ) |
|
|
| |
|
|
| @router.get("/overview", response_model=LiveFinanceResponse) |
| async def get_live_financial_overview( |
| limit: int = 50, |
| ): |
| """ |
| Fetch live financial data from connected providers (Stripe, Xero) |
| and aggregate into a unified view. |
| """ |
| transactions = [] |
| providers_status = {"stripe": False, "xero": False, "zoho": False, "dynamics": False} |
|
|
| |
| try: |
| |
| stripe_token = os.getenv("STRIPE_SECRET_KEY") |
| if stripe_token: |
| raw_payments = stripe_service.list_payments(stripe_token, limit=limit) |
| charges = raw_payments.get("data", []) |
| transactions.extend([map_stripe_payment(p) for p in charges]) |
| providers_status["stripe"] = True |
| except Exception as e: |
| logger.warning(f"Failed to fetch live Stripe data: {e}") |
|
|
| |
| |
| try: |
| xero_token = os.getenv("XERO_ACCESS_TOKEN") |
| if not xero_token: |
| logger.warning("XERO_ACCESS_TOKEN not configured, skipping Xero fetch") |
| else: |
| |
| from integrations.xero_service import xero_service |
| xero_invoices = xero_service.get_invoices(access_token=xero_token, limit=limit) |
| transactions.extend([map_xero_invoice(i) for i in xero_invoices]) |
| providers_status["xero"] = True |
| except Exception as e: |
| logger.warning(f"Failed to fetch live Xero data: {e}") |
|
|
| |
| try: |
| zoho_token = os.getenv("ZOHO_CRM_ACCESS_TOKEN") |
| org_id = os.getenv("ZOHO_BOOKS_ORG_ID") |
| |
| if zoho_token and org_id: |
| zoho = ZohoBooksService() |
| |
| headers = zoho._get_headers(zoho_token, org_id) |
| url = f"{zoho.base_url}/invoices" |
| async with httpx.AsyncClient() as client: |
| res = await client.get(url, headers=headers, params={"organization_id": org_id}) |
| if res.status_code == 200: |
| raw_invoices = res.json().get("invoices", []) |
| transactions.extend([map_zoho_invoice(i) for i in raw_invoices]) |
| providers_status["zoho"] = True |
| except Exception as e: |
| logger.warning(f"Failed to fetch live Zoho Books data: {e}") |
|
|
| |
| try: |
| ms_token = os.getenv("MICROSOFT_365_ACCESS_TOKEN") |
| if ms_token: |
| res = await microsoft365_service.get_dynamics_invoices(access_token=ms_token, top=limit) |
| if res.get("status") == "success": |
| raw_invoices = res.get("data", {}).get("value", []) |
| transactions.extend([map_dynamics_invoice(i) for i in raw_invoices]) |
| providers_status["dynamics"] = True |
| except Exception as e: |
| logger.warning(f"Failed to fetch live Dynamics 365 data: {e}") |
|
|
| |
| total_rev = sum(t.amount for t in transactions if t.status in ['succeeded', 'paid', 'paid']) |
| pending_rev = sum(t.amount for t in transactions if t.status in ['pending', 'open']) |
|
|
| breakdown = { |
| "stripe": sum(t.amount for t in transactions if t.platform == 'stripe'), |
| "xero": sum(t.amount for t in transactions if t.platform == 'xero'), |
| "zoho": sum(t.amount for t in transactions if t.platform == 'zoho'), |
| "dynamics": sum(t.amount for t in transactions if t.platform == 'dynamics') |
| } |
|
|
| return LiveFinanceResponse( |
| ok=True, |
| stats=FinanceStats( |
| total_revenue=total_rev, |
| pending_revenue=pending_rev, |
| transaction_count=len(transactions), |
| platform_breakdown=breakdown |
| ), |
| transactions=sorted(transactions, key=lambda x: x.date, reverse=True)[:limit], |
| providers=providers_status |
| ) |
|
|