Files
WealthySmart/backend/app/api/v1/endpoints/analytics.py
Carlos Escalante 9a80f2a997
All checks were successful
Deploy to VPS / deploy (push) Successful in 13s
Convert USD and EUR to CRC in analytics endpoints
All three analytics endpoints (by-category, monthly-trend, daily-spending)
now convert foreign currency amounts to CRC using current exchange rates.
EUR/CRC rate derived from ExchangeRate-API (USD-based cross rate).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 20:41:38 -06:00

220 lines
6.4 KiB
Python

from datetime import datetime
from typing import Optional
from fastapi import APIRouter, Depends
from pydantic import BaseModel
from sqlalchemy import case
from sqlmodel import Session, func, select
from app.auth import get_current_user
from app.db import get_session
from app.models.models import Category, Transaction
from app.services.budget_projection import get_cycle_range
from app.services.exchange_rate import get_current_rate, get_eur_crc_rate
router = APIRouter(prefix="/analytics", tags=["analytics"])
class CategorySpending(BaseModel):
category_id: Optional[int]
category_name: str
total: float
count: int
percentage: float
class MonthlyTrend(BaseModel):
year: int
month: int
label: str
total_crc: float
total_usd: float
count: int
class DailySpending(BaseModel):
date: str
total: float
count: int
def _get_crc_multipliers(session: Session) -> dict[str, float]:
"""Return multipliers to convert each currency to CRC."""
usd_rate = get_current_rate(session)
eur_rate = get_eur_crc_rate()
return {
"CRC": 1.0,
"USD": usd_rate.sell_rate if usd_rate else 0.0,
"EUR": eur_rate if eur_rate else 0.0,
}
@router.get("/by-category", response_model=list[CategorySpending])
def spending_by_category(
cycle_year: Optional[int] = None,
cycle_month: Optional[int] = None,
session: Session = Depends(get_session),
_user: str = Depends(get_current_user),
):
rates = _get_crc_multipliers(session)
query = (
select(
Transaction.category_id,
func.sum(
case(
(Transaction.currency == "USD", Transaction.amount * rates["USD"]),
(Transaction.currency == "EUR", Transaction.amount * rates["EUR"]),
else_=Transaction.amount,
)
).label("total"),
func.count().label("count"),
)
.where(Transaction.transaction_type == "COMPRA")
.group_by(Transaction.category_id)
)
if cycle_year and cycle_month:
start, end = get_cycle_range(cycle_year, cycle_month)
query = query.where(Transaction.date >= start, Transaction.date < end)
rows = session.exec(query).all()
grand_total = sum(r[1] for r in rows) or 1
results = []
for category_id, total, count in rows:
cat_name = "Uncategorized"
if category_id:
cat = session.get(Category, category_id)
if cat:
cat_name = cat.name
results.append(
CategorySpending(
category_id=category_id,
category_name=cat_name,
total=float(total),
count=count,
percentage=round(float(total) / grand_total * 100, 1),
)
)
return sorted(results, key=lambda x: x.total, reverse=True)
@router.get("/monthly-trend", response_model=list[MonthlyTrend])
def monthly_trend(
months: int = 6,
session: Session = Depends(get_session),
_user: str = Depends(get_current_user),
):
"""Monthly spending totals using billing cycle boundaries (18th-18th).
total_crc includes all currencies converted to CRC at current rates.
total_usd is the raw USD amount (unconverted) for display purposes.
"""
rates = _get_crc_multipliers(session)
now = datetime.now()
results = []
month_names = [
"", "Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
]
y, m = now.year, now.month
for _ in range(months):
start, end = get_cycle_range(y, m)
row = session.exec(
select(
func.count(),
func.coalesce(
func.sum(
case(
(Transaction.currency == "USD", Transaction.amount * rates["USD"]),
(Transaction.currency == "EUR", Transaction.amount * rates["EUR"]),
else_=Transaction.amount,
)
),
0,
),
func.coalesce(
func.sum(
case(
(Transaction.currency == "USD", Transaction.amount),
else_=0,
)
),
0,
),
)
.where(
Transaction.transaction_type == "COMPRA",
Transaction.date >= start,
Transaction.date < end,
)
).first()
count = row[0] if row else 0
total_crc = float(row[1]) if row else 0.0
total_usd = float(row[2]) if row else 0.0
end_month = m + 1 if m < 12 else 1
label = f"{month_names[m]} - {month_names[end_month]}"
results.append(
MonthlyTrend(
year=y,
month=m,
label=label,
total_crc=total_crc,
total_usd=total_usd,
count=count,
)
)
# Previous month
if m == 1:
y, m = y - 1, 12
else:
m -= 1
return list(reversed(results))
@router.get("/daily-spending", response_model=list[DailySpending])
def daily_spending(
cycle_year: Optional[int] = None,
cycle_month: Optional[int] = None,
session: Session = Depends(get_session),
_user: str = Depends(get_current_user),
):
rates = _get_crc_multipliers(session)
query = (
select(
func.date(Transaction.date).label("day"),
func.sum(
case(
(Transaction.currency == "USD", Transaction.amount * rates["USD"]),
(Transaction.currency == "EUR", Transaction.amount * rates["EUR"]),
else_=Transaction.amount,
)
).label("total"),
func.count().label("count"),
)
.where(Transaction.transaction_type == "COMPRA")
.group_by(func.date(Transaction.date))
.order_by(func.date(Transaction.date))
)
if cycle_year and cycle_month:
start, end = get_cycle_range(cycle_year, cycle_month)
query = query.where(Transaction.date >= start, Transaction.date < end)
rows = session.exec(query).all()
return [
DailySpending(date=str(day), total=float(total), count=count)
for day, total, count in rows
]