mirror of
https://github.com/escalante29/WealthySmart.git
synced 2026-05-19 10:28:48 +02:00
All checks were successful
Deploy to VPS / deploy (push) Successful in 48s
Backend: parse BAC pension statement PDFs (VOL, ROP, FCL) via pdftotext, store snapshots with duplicate detection, reject credit card statements. Endpoints: POST /upload, GET /snapshots, GET /fund-summary. Frontend: wire up drag-and-drop upload, load real balances and rendimientos from API, show upload results with error/duplicate feedback. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
122 lines
3.7 KiB
Python
122 lines
3.7 KiB
Python
from fastapi import APIRouter, Depends, UploadFile
|
|
from pydantic import BaseModel
|
|
from sqlmodel import Session, select
|
|
|
|
from app.auth import get_current_user
|
|
from app.db import get_session
|
|
from app.models.models import Bank, PensionSnapshot, PensionSnapshotRead
|
|
from app.services.pension_pdf import parse_pension_pdf
|
|
|
|
router = APIRouter(prefix="/pensions", tags=["pensions"])
|
|
|
|
|
|
class PensionUploadResult(BaseModel):
|
|
imported: int
|
|
duplicates: int
|
|
errors: list[str]
|
|
snapshots: list[PensionSnapshotRead]
|
|
|
|
|
|
@router.post("/upload", response_model=PensionUploadResult)
|
|
async def upload_pension_pdfs(
|
|
files: list[UploadFile],
|
|
session: Session = Depends(get_session),
|
|
_user: str = Depends(get_current_user),
|
|
):
|
|
imported = 0
|
|
duplicates = 0
|
|
errors: list[str] = []
|
|
created: list[PensionSnapshot] = []
|
|
|
|
for file in files:
|
|
filename = file.filename or "unknown.pdf"
|
|
try:
|
|
pdf_bytes = await file.read()
|
|
fund_snapshots = parse_pension_pdf(pdf_bytes, filename)
|
|
except ValueError as e:
|
|
errors.append(str(e))
|
|
continue
|
|
except Exception as e:
|
|
errors.append(f"{filename}: {e}")
|
|
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,
|
|
period_start=snap.period_start,
|
|
period_end=snap.period_end,
|
|
saldo_anterior=snap.saldo_anterior,
|
|
aportes=snap.aportes,
|
|
rendimientos=snap.rendimientos,
|
|
retiros=snap.retiros,
|
|
traslados=snap.traslados,
|
|
comision=snap.comision,
|
|
correccion=snap.correccion,
|
|
bonificacion=snap.bonificacion,
|
|
saldo_final=snap.saldo_final,
|
|
source_filename=filename,
|
|
)
|
|
session.add(row)
|
|
created.append(row)
|
|
imported += 1
|
|
|
|
if imported > 0:
|
|
session.commit()
|
|
for row in created:
|
|
session.refresh(row)
|
|
|
|
return PensionUploadResult(
|
|
imported=imported,
|
|
duplicates=duplicates,
|
|
errors=errors,
|
|
snapshots=[PensionSnapshotRead.model_validate(r) for r in created],
|
|
)
|
|
|
|
|
|
@router.get("/snapshots", response_model=list[PensionSnapshotRead])
|
|
def get_snapshots(
|
|
session: Session = Depends(get_session),
|
|
_user: str = Depends(get_current_user),
|
|
):
|
|
rows = session.exec(
|
|
select(PensionSnapshot).order_by(
|
|
PensionSnapshot.period_end.desc(), # type: ignore[union-attr]
|
|
PensionSnapshot.fund,
|
|
)
|
|
).all()
|
|
return rows
|
|
|
|
|
|
@router.get("/fund-summary", response_model=list[PensionSnapshotRead])
|
|
def get_fund_summary(
|
|
session: Session = Depends(get_session),
|
|
_user: str = Depends(get_current_user),
|
|
):
|
|
"""Return the latest snapshot per fund (by most recent period_end)."""
|
|
all_rows = session.exec(
|
|
select(PensionSnapshot).order_by(
|
|
PensionSnapshot.period_end.desc(), # type: ignore[union-attr]
|
|
)
|
|
).all()
|
|
|
|
seen: set[str] = set()
|
|
latest: list[PensionSnapshot] = []
|
|
for row in all_rows:
|
|
if row.fund.value not in seen:
|
|
seen.add(row.fund.value)
|
|
latest.append(row)
|
|
|
|
return latest
|