mirror of
https://github.com/escalante29/WealthySmart.git
synced 2026-05-19 15:28:48 +02:00
All checks were successful
Deploy to VPS / deploy (push) Successful in 45s
- Expand Account model with account_type (pension, savings, liability, crypto), new banks/currencies (BTC, XMR, FCL, ROP, VOL, MEMP, MPAT, MORTGAGE), and next_payment field - Add exchange rate endpoint (BCCR integration), analytics endpoint, paste-import for transactions, and API token management - Add PWA manifest, service worker, and app icons - Redesign dashboard, transactions, transfers, and login pages with theme support - Add billing cycle selector, confirm dialog, and paste import modal components - One-time DB reset in deploy workflow for schema migration Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
185 lines
5.1 KiB
Python
185 lines
5.1 KiB
Python
from datetime import datetime
|
|
from typing import Optional
|
|
|
|
from fastapi import APIRouter, Depends
|
|
from pydantic import BaseModel
|
|
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.api.v1.endpoints.transactions import get_cycle_range
|
|
|
|
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
|
|
|
|
|
|
@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),
|
|
):
|
|
query = (
|
|
select(
|
|
Transaction.category_id,
|
|
func.sum(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)."""
|
|
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(
|
|
func.case(
|
|
(Transaction.currency == "CRC", Transaction.amount),
|
|
else_=0,
|
|
)
|
|
),
|
|
0,
|
|
),
|
|
func.coalesce(
|
|
func.sum(
|
|
func.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),
|
|
):
|
|
query = (
|
|
select(
|
|
func.date(Transaction.date).label("day"),
|
|
func.sum(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
|
|
]
|