145 lines
4.8 KiB
Python
145 lines
4.8 KiB
Python
from __future__ import annotations
|
|
|
|
from datetime import UTC, datetime
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
from mousehold_domain import ImportConnectionStatus, ImportConnectionType
|
|
from sqlalchemy import select
|
|
from sqlalchemy.orm import Session
|
|
|
|
from .. import models
|
|
from ..db import get_session
|
|
from ..schemas import (
|
|
ImportConnectionCreate,
|
|
ImportConnectionRead,
|
|
ImportConnectionUpdate,
|
|
LocalAgentHeartbeat,
|
|
LocalAgentRegister,
|
|
)
|
|
|
|
router = APIRouter(tags=["connections"])
|
|
|
|
|
|
@router.get("/connections/supported")
|
|
def supported_connections() -> dict[str, object]:
|
|
return {
|
|
"connectors": [
|
|
{
|
|
"type": ImportConnectionType.FINTS,
|
|
"runs_locally": True,
|
|
"status": "local-agent-ready",
|
|
"notes": "PIN/TAN auth runs in the local agent; normalized observations can be synced to the API.",
|
|
},
|
|
{
|
|
"type": ImportConnectionType.PAYPAL_API,
|
|
"runs_locally": True,
|
|
"status": "experimental-off-ui",
|
|
"notes": "PayPal CSV statements and email evidence are the active PoC path.",
|
|
},
|
|
{
|
|
"type": ImportConnectionType.IMAP,
|
|
"runs_locally": True,
|
|
"status": "scaffold",
|
|
"notes": "Local agent can filter Klarna, PayPal, invoices, and order confirmations via IMAP.",
|
|
},
|
|
{
|
|
"type": ImportConnectionType.CSV_FOLDER,
|
|
"runs_locally": True,
|
|
"status": "ready-shape",
|
|
"notes": "CSV rows can be normalized into /observations/sync payloads.",
|
|
},
|
|
]
|
|
}
|
|
|
|
|
|
@router.post("/connections", response_model=ImportConnectionRead)
|
|
def create_connection(
|
|
payload: ImportConnectionCreate,
|
|
session: Session = Depends(get_session),
|
|
) -> models.ImportConnection:
|
|
connection = models.ImportConnection(**payload.model_dump())
|
|
session.add(connection)
|
|
session.commit()
|
|
session.refresh(connection)
|
|
return connection
|
|
|
|
|
|
@router.get("/connections", response_model=list[ImportConnectionRead])
|
|
def list_connections(
|
|
household_id: str,
|
|
session: Session = Depends(get_session),
|
|
) -> list[models.ImportConnection]:
|
|
return list(
|
|
session.scalars(
|
|
select(models.ImportConnection)
|
|
.where(models.ImportConnection.household_id == household_id)
|
|
.order_by(models.ImportConnection.created_at.desc())
|
|
).all()
|
|
)
|
|
|
|
|
|
@router.patch("/connections/{connection_id}", response_model=ImportConnectionRead)
|
|
def update_connection(
|
|
connection_id: str,
|
|
payload: ImportConnectionUpdate,
|
|
session: Session = Depends(get_session),
|
|
) -> models.ImportConnection:
|
|
connection = session.get(models.ImportConnection, connection_id)
|
|
if not connection:
|
|
raise HTTPException(status_code=404, detail="Connection not found")
|
|
for field, value in payload.model_dump(exclude_unset=True).items():
|
|
setattr(connection, field, value)
|
|
session.add(connection)
|
|
session.commit()
|
|
session.refresh(connection)
|
|
return connection
|
|
|
|
|
|
@router.delete("/connections/{connection_id}")
|
|
def delete_connection(connection_id: str, session: Session = Depends(get_session)) -> dict[str, bool]:
|
|
connection = session.get(models.ImportConnection, connection_id)
|
|
if not connection:
|
|
raise HTTPException(status_code=404, detail="Connection not found")
|
|
session.delete(connection)
|
|
session.commit()
|
|
return {"deleted": True}
|
|
|
|
|
|
@router.post("/local-agent/register", response_model=ImportConnectionRead)
|
|
def register_local_agent(
|
|
payload: LocalAgentRegister,
|
|
session: Session = Depends(get_session),
|
|
) -> models.ImportConnection:
|
|
connection = models.ImportConnection(
|
|
household_id=payload.household_id,
|
|
owner_user_id=payload.owner_user_id,
|
|
connection_type=ImportConnectionType.LOCAL_AGENT,
|
|
display_name=payload.display_name,
|
|
status=ImportConnectionStatus.ACTIVE,
|
|
runs_locally=True,
|
|
connector_config_json={
|
|
"capabilities": ["bank_csv", "paypal_csv", "imap", "fints_placeholder"]
|
|
},
|
|
)
|
|
session.add(connection)
|
|
session.commit()
|
|
session.refresh(connection)
|
|
return connection
|
|
|
|
|
|
@router.post("/local-agent/heartbeat", response_model=ImportConnectionRead)
|
|
def local_agent_heartbeat(
|
|
payload: LocalAgentHeartbeat,
|
|
session: Session = Depends(get_session),
|
|
) -> models.ImportConnection:
|
|
connection = session.get(models.ImportConnection, payload.connection_id)
|
|
if not connection:
|
|
raise HTTPException(status_code=404, detail="Connection not found")
|
|
connection.status = payload.status
|
|
connection.last_error = payload.last_error
|
|
connection.last_sync_at = datetime.now(UTC)
|
|
session.add(connection)
|
|
session.commit()
|
|
session.refresh(connection)
|
|
return connection
|