Add manual pension data entry and fix chart to use real historical data
All checks were successful
Deploy to VPS / deploy (push) Successful in 23s

- Add paste-and-preview modal for entering pension fund balances from bank website
- Backend upsert logic so n8n PDF uploads overwrite manual entries
- Chart now shows actual snapshot data with dynamic month labels
- New POST /pensions/manual endpoint for JSON-based fund entry

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Carlos Escalante
2026-04-01 10:03:29 -06:00
parent e011a3adcc
commit 3c9656f416
5 changed files with 548 additions and 80 deletions

View File

@@ -1,3 +1,5 @@
from datetime import date
from fastapi import APIRouter, Depends, UploadFile
from pydantic import BaseModel
from sqlmodel import Session, select
@@ -12,11 +14,93 @@ router = APIRouter(prefix="/pensions", tags=["pensions"])
class PensionUploadResult(BaseModel):
imported: int
updated: int
duplicates: int
errors: list[str]
snapshots: list[PensionSnapshotRead]
class PensionManualEntry(BaseModel):
fund: str
period_start: date
period_end: date
saldo_anterior: float
aportes: float
rendimientos: float
retiros: float
traslados: float
comision: float
correccion: float = 0.0
bonificacion: float = 0.0
saldo_final: float
class PensionManualRequest(BaseModel):
entries: list[PensionManualEntry]
def _upsert_snapshot(
session: Session,
fund: str,
period_start: date,
period_end: date,
saldo_anterior: float,
aportes: float,
rendimientos: float,
retiros: float,
traslados: float,
comision: float,
correccion: float,
bonificacion: float,
saldo_final: float,
source_filename: str,
contract_number: str = "",
) -> tuple[PensionSnapshot, bool]:
"""Insert or update a pension snapshot. Returns (row, is_new)."""
existing = session.exec(
select(PensionSnapshot).where(
PensionSnapshot.fund == Bank(fund),
PensionSnapshot.period_start == period_start,
PensionSnapshot.period_end == period_end,
)
).first()
if existing:
existing.saldo_anterior = saldo_anterior
existing.aportes = aportes
existing.rendimientos = rendimientos
existing.retiros = retiros
existing.traslados = traslados
existing.comision = comision
existing.correccion = correccion
existing.bonificacion = bonificacion
existing.saldo_final = saldo_final
existing.source_filename = source_filename
if contract_number:
existing.contract_number = contract_number
session.add(existing)
return existing, False
row = PensionSnapshot(
fund=Bank(fund),
contract_number=contract_number,
period_start=period_start,
period_end=period_end,
saldo_anterior=saldo_anterior,
aportes=aportes,
rendimientos=rendimientos,
retiros=retiros,
traslados=traslados,
comision=comision,
correccion=correccion,
bonificacion=bonificacion,
saldo_final=saldo_final,
source_filename=source_filename,
)
session.add(row)
return row, True
@router.post("/upload", response_model=PensionUploadResult)
async def upload_pension_pdfs(
files: list[UploadFile],
@@ -24,9 +108,9 @@ async def upload_pension_pdfs(
_user: str = Depends(get_current_user),
):
imported = 0
duplicates = 0
updated = 0
errors: list[str] = []
created: list[PensionSnapshot] = []
results: list[PensionSnapshot] = []
for file in files:
filename = file.filename or "unknown.pdf"
@@ -41,20 +125,9 @@ async def upload_pension_pdfs(
continue
for snap in fund_snapshots:
existing = session.exec(
select(PensionSnapshot).where(
PensionSnapshot.fund == Bank(snap.fund),
PensionSnapshot.period_start == snap.period_start,
PensionSnapshot.period_end == snap.period_end,
)
).first()
if existing:
duplicates += 1
continue
row = PensionSnapshot(
fund=Bank(snap.fund),
contract_number=snap.contract_number,
row, is_new = _upsert_snapshot(
session,
fund=snap.fund,
period_start=snap.period_start,
period_end=snap.period_end,
saldo_anterior=snap.saldo_anterior,
@@ -67,21 +140,72 @@ async def upload_pension_pdfs(
bonificacion=snap.bonificacion,
saldo_final=snap.saldo_final,
source_filename=filename,
contract_number=snap.contract_number,
)
session.add(row)
created.append(row)
imported += 1
results.append(row)
if is_new:
imported += 1
else:
updated += 1
if imported > 0:
if imported > 0 or updated > 0:
session.commit()
for row in created:
for row in results:
session.refresh(row)
return PensionUploadResult(
imported=imported,
duplicates=duplicates,
updated=updated,
duplicates=0,
errors=errors,
snapshots=[PensionSnapshotRead.model_validate(r) for r in created],
snapshots=[PensionSnapshotRead.model_validate(r) for r in results],
)
@router.post("/manual", response_model=PensionUploadResult)
def submit_manual_entries(
body: PensionManualRequest,
session: Session = Depends(get_session),
_user: str = Depends(get_current_user),
):
imported = 0
updated = 0
results: list[PensionSnapshot] = []
for entry in body.entries:
row, is_new = _upsert_snapshot(
session,
fund=entry.fund,
period_start=entry.period_start,
period_end=entry.period_end,
saldo_anterior=entry.saldo_anterior,
aportes=entry.aportes,
rendimientos=entry.rendimientos,
retiros=entry.retiros,
traslados=entry.traslados,
comision=entry.comision,
correccion=entry.correccion,
bonificacion=entry.bonificacion,
saldo_final=entry.saldo_final,
source_filename="manual-entry",
)
results.append(row)
if is_new:
imported += 1
else:
updated += 1
if imported > 0 or updated > 0:
session.commit()
for row in results:
session.refresh(row)
return PensionUploadResult(
imported=imported,
updated=updated,
duplicates=0,
errors=[],
snapshots=[PensionSnapshotRead.model_validate(r) for r in results],
)