mirror of
https://github.com/escalante29/WealthySmart.git
synced 2026-05-19 08:28:48 +02:00
All checks were successful
Deploy to VPS / deploy (push) Successful in 13s
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>
220 lines
6.4 KiB
Python
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
|
|
]
|