Files
WealthySmart/backend/app/api/v1/endpoints/savings_accrual.py
Carlos Escalante d929ed6573
All checks were successful
Deploy to VPS / deploy (push) Successful in 23s
Remove Ahorro from budget UI, add SALARY type and savings auto-accrual
Ahorro was already deducted from gross salary so displaying it in
budget projections was misleading. This removes the Ahorro card,
summary line, Proyecciones column, and Ahorro Anual card from the UI,
and strips all savings fields from budget API responses.

Adds SALARY TransactionType so salary deposits can be distinguished
from generic DEPOSITO transfers. When a SALARY transaction arrives,
the system auto-increments MEMP and MPAT savings account balances
(+200K CRC each) once per month via an idempotent accrual log.

New CRUD endpoints at /api/v1/savings-accrual/ allow manual correction
of the accrual history. Feb+Mar 2026 are seeded as historical baseline.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 19:13:29 -06:00

84 lines
2.5 KiB
Python

from datetime import datetime
from fastapi import APIRouter, Depends, HTTPException, Path
from sqlmodel import Session, col, select
from app.auth import get_current_user
from app.db import get_session
from app.models.models import (
SavingsAccrual,
SavingsAccrualCreate,
SavingsAccrualRead,
SavingsAccrualUpdate,
)
router = APIRouter(prefix="/savings-accrual", tags=["savings-accrual"])
@router.get("/", response_model=list[SavingsAccrualRead])
def list_accruals(
session: Session = Depends(get_session),
_user: str = Depends(get_current_user),
):
query = select(SavingsAccrual).order_by(
col(SavingsAccrual.year).desc(), col(SavingsAccrual.month).desc()
)
return session.exec(query).all()
@router.post("/", response_model=SavingsAccrualRead, status_code=201)
def create_accrual(
data: SavingsAccrualCreate,
session: Session = Depends(get_session),
_user: str = Depends(get_current_user),
):
existing = session.exec(
select(SavingsAccrual).where(
SavingsAccrual.year == data.year,
SavingsAccrual.month == data.month,
)
).first()
if existing:
raise HTTPException(
status_code=409,
detail=f"Accrual for {data.year}-{data.month:02d} already exists (id={existing.id})",
)
accrual = SavingsAccrual.model_validate(data)
accrual.applied_at = datetime.utcnow()
session.add(accrual)
session.commit()
session.refresh(accrual)
return accrual
@router.patch("/{accrual_id}", response_model=SavingsAccrualRead)
def update_accrual(
accrual_id: int,
data: SavingsAccrualUpdate,
session: Session = Depends(get_session),
_user: str = Depends(get_current_user),
):
accrual = session.get(SavingsAccrual, accrual_id)
if not accrual:
raise HTTPException(status_code=404, detail="Accrual not found")
update_data = data.model_dump(exclude_unset=True)
for key, value in update_data.items():
setattr(accrual, key, value)
session.add(accrual)
session.commit()
session.refresh(accrual)
return accrual
@router.delete("/{accrual_id}", status_code=204)
def delete_accrual(
accrual_id: int = Path(...),
session: Session = Depends(get_session),
_user: str = Depends(get_current_user),
):
accrual = session.get(SavingsAccrual, accrual_id)
if not accrual:
raise HTTPException(status_code=404, detail="Accrual not found")
session.delete(accrual)
session.commit()