Full-stack self-hosted finance app with FastAPI backend and React frontend. Features: - Accounts, transactions, budgets, investments with GBP base currency - CSV import with auto-detection for 10 UK bank formats - ML predictions: spending forecast, net worth projection, Monte Carlo - 7 selectable themes (Obsidian, Arctic, Midnight, Vault, Terminal, Synthwave, Ledger) - Receipt/document attachments on transactions (JPEG, PNG, WebP, PDF) - AES-256-GCM field encryption, RS256 JWT, TOTP 2FA, RLS, audit log - Encrypted nightly backups + key rotation script - Mobile-responsive layout with bottom nav Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
74 lines
2.4 KiB
Python
74 lines
2.4 KiB
Python
import structlog
|
|
|
|
logger = structlog.get_logger()
|
|
|
|
PAIRS = [
|
|
("GBP", "USD"), ("GBP", "EUR"), ("GBP", "JPY"), ("GBP", "CAD"),
|
|
("GBP", "AUD"), ("GBP", "CHF"), ("USD", "GBP"), ("EUR", "GBP"),
|
|
]
|
|
|
|
|
|
async def fx_sync_job() -> None:
|
|
from app.dependencies import get_session_factory
|
|
session_factory = get_session_factory()
|
|
if not session_factory:
|
|
return
|
|
|
|
try:
|
|
import requests
|
|
r = requests.get(
|
|
"https://api.exchangerate-api.com/v4/latest/GBP",
|
|
timeout=10,
|
|
)
|
|
r.raise_for_status()
|
|
data = r.json()
|
|
rates = data.get("rates", {})
|
|
except Exception as exc:
|
|
logger.error("fx_fetch_failed", error=str(exc))
|
|
return
|
|
|
|
from datetime import datetime, timezone
|
|
from decimal import Decimal
|
|
import uuid as _uuid
|
|
from sqlalchemy import select
|
|
from app.db.models.currency import ExchangeRate # type: ignore[attr-defined]
|
|
|
|
async with session_factory() as db:
|
|
try:
|
|
now = datetime.now(timezone.utc)
|
|
for base, quote in PAIRS:
|
|
if base == "GBP":
|
|
rate_val = rates.get(quote)
|
|
else:
|
|
gbp_to_base = rates.get(base)
|
|
if not gbp_to_base or gbp_to_base == 0:
|
|
continue
|
|
rate_val = 1 / gbp_to_base
|
|
|
|
if not rate_val:
|
|
continue
|
|
|
|
result = await db.execute(
|
|
select(ExchangeRate).where(
|
|
ExchangeRate.base_currency == base,
|
|
ExchangeRate.quote_currency == quote,
|
|
)
|
|
)
|
|
existing = result.scalar_one_or_none()
|
|
if existing:
|
|
existing.rate = Decimal(str(round(rate_val, 8)))
|
|
existing.fetched_at = now
|
|
else:
|
|
db.add(ExchangeRate(
|
|
id=_uuid.uuid4(),
|
|
base_currency=base,
|
|
quote_currency=quote,
|
|
rate=Decimal(str(round(rate_val, 8))),
|
|
source="exchangerate-api",
|
|
fetched_at=now,
|
|
))
|
|
await db.commit()
|
|
logger.info("fx_sync_done", pairs=len(PAIRS))
|
|
except Exception as exc:
|
|
await db.rollback()
|
|
logger.error("fx_sync_db_failed", error=str(exc))
|