mirror of
https://github.com/escalante29/WealthySmart.git
synced 2026-05-19 11:28:49 +02:00
Add cumulative balance tracking with editable overrides
All checks were successful
Deploy to VPS / deploy (push) Successful in 21s
All checks were successful
Deploy to VPS / deploy (push) Successful in 21s
- New BalanceOverride table for manual balance adjustments per month
- Cumulative balance computation with cross-year carryover
- Three new columns: Acum. Anterior, Neto Mes, Balance Acum.
- Inline editing on Balance Acum. cell (pencil icon for overrides)
- Year navigation clamped to 2026–2030, fresh start at March 2026
- PUT/DELETE /budget/balance-override/{year}/{month} endpoints
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Path, Query
|
||||
@@ -7,13 +8,23 @@ from sqlmodel import Session, select
|
||||
from app.auth import get_current_user
|
||||
from app.db import get_session
|
||||
from app.models.models import (
|
||||
BalanceOverride,
|
||||
BalanceOverrideCreate,
|
||||
BalanceOverrideRead,
|
||||
RecurringItem,
|
||||
RecurringItemCreate,
|
||||
RecurringItemRead,
|
||||
RecurringItemType,
|
||||
RecurringItemUpdate,
|
||||
)
|
||||
from app.services.budget_projection import compute_monthly_projection
|
||||
from app.services.budget_projection import (
|
||||
FRESH_START_MONTH,
|
||||
FRESH_START_YEAR,
|
||||
MAX_YEAR,
|
||||
MIN_YEAR,
|
||||
compute_monthly_projection,
|
||||
compute_yearly_projection_with_cumulative,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/budget", tags=["budget"])
|
||||
|
||||
@@ -97,6 +108,9 @@ class MonthlyProjectionResponse(BaseModel):
|
||||
uncovered_actual: float
|
||||
gran_total_egresos: float
|
||||
net_balance: float
|
||||
carryover_balance: float = 0.0
|
||||
cumulative_balance: float = 0.0
|
||||
balance_overridden: bool = False
|
||||
|
||||
|
||||
class YearlyProjectionResponse(BaseModel):
|
||||
@@ -114,14 +128,20 @@ def get_yearly_projection(
|
||||
session: Session = Depends(get_session),
|
||||
_user: str = Depends(get_current_user),
|
||||
):
|
||||
if year < MIN_YEAR or year > MAX_YEAR:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Year must be between {MIN_YEAR} and {MAX_YEAR}",
|
||||
)
|
||||
|
||||
months_data = compute_yearly_projection_with_cumulative(session, year)
|
||||
months = []
|
||||
annual_income = 0.0
|
||||
annual_expenses = 0.0
|
||||
annual_savings = 0.0
|
||||
annual_net = 0.0
|
||||
|
||||
for m in range(1, 13):
|
||||
data = compute_monthly_projection(session, year, m)
|
||||
for data in months_data:
|
||||
monthly = MonthlyProjectionResponse(
|
||||
month=data["month"],
|
||||
year=data["year"],
|
||||
@@ -134,6 +154,9 @@ def get_yearly_projection(
|
||||
uncovered_actual=data["uncovered_actual"],
|
||||
gran_total_egresos=data["gran_total_egresos"],
|
||||
net_balance=data["net_balance"],
|
||||
carryover_balance=data["carryover_balance"],
|
||||
cumulative_balance=data["cumulative_balance"],
|
||||
balance_overridden=data["balance_overridden"],
|
||||
)
|
||||
months.append(monthly)
|
||||
annual_income += data["projected_income"]
|
||||
@@ -208,3 +231,63 @@ def get_monthly_detail(
|
||||
gran_total_egresos=data["gran_total_egresos"],
|
||||
net_balance=data["net_balance"],
|
||||
)
|
||||
|
||||
|
||||
# --- Balance Override CRUD ---
|
||||
|
||||
|
||||
@router.put(
|
||||
"/balance-override/{year}/{month}",
|
||||
response_model=BalanceOverrideRead,
|
||||
)
|
||||
def upsert_balance_override(
|
||||
year: int,
|
||||
month: int = Path(ge=1, le=12),
|
||||
data: BalanceOverrideCreate = ...,
|
||||
session: Session = Depends(get_session),
|
||||
_user: str = Depends(get_current_user),
|
||||
):
|
||||
if year < MIN_YEAR or year > MAX_YEAR:
|
||||
raise HTTPException(400, f"Year must be between {MIN_YEAR} and {MAX_YEAR}")
|
||||
if year == FRESH_START_YEAR and month < FRESH_START_MONTH:
|
||||
raise HTTPException(400, f"Cannot override before {FRESH_START_YEAR}-{FRESH_START_MONTH:02d}")
|
||||
|
||||
existing = session.exec(
|
||||
select(BalanceOverride).where(
|
||||
BalanceOverride.year == year, BalanceOverride.month == month
|
||||
)
|
||||
).first()
|
||||
|
||||
if existing:
|
||||
existing.override_balance = data.override_balance
|
||||
existing.updated_at = datetime.utcnow()
|
||||
session.add(existing)
|
||||
session.commit()
|
||||
session.refresh(existing)
|
||||
return existing
|
||||
|
||||
override = BalanceOverride(
|
||||
year=year, month=month, override_balance=data.override_balance
|
||||
)
|
||||
session.add(override)
|
||||
session.commit()
|
||||
session.refresh(override)
|
||||
return override
|
||||
|
||||
|
||||
@router.delete("/balance-override/{year}/{month}", status_code=204)
|
||||
def delete_balance_override(
|
||||
year: int,
|
||||
month: int = Path(ge=1, le=12),
|
||||
session: Session = Depends(get_session),
|
||||
_user: str = Depends(get_current_user),
|
||||
):
|
||||
existing = session.exec(
|
||||
select(BalanceOverride).where(
|
||||
BalanceOverride.year == year, BalanceOverride.month == month
|
||||
)
|
||||
).first()
|
||||
if not existing:
|
||||
raise HTTPException(404, "No override found for this month")
|
||||
session.delete(existing)
|
||||
session.commit()
|
||||
|
||||
Reference in New Issue
Block a user