mirror of
https://github.com/escalante29/WealthySmart.git
synced 2026-05-19 11:28:49 +02:00
Add pension PDF upload, parsing, and fund summary API
All checks were successful
Deploy to VPS / deploy (push) Successful in 48s
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>
This commit is contained in:
121
backend/app/api/v1/endpoints/pensions.py
Normal file
121
backend/app/api/v1/endpoints/pensions.py
Normal file
@@ -0,0 +1,121 @@
|
||||
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
|
||||
Reference in New Issue
Block a user