mirror of
https://github.com/Zproger/Aurum.git
synced 2026-10-13 11:06:40 +07:00
feat: add a Crypto tab with live CoinGecko prices synced into Net Worth
Track individual coins (identity + quantity) in a dedicated tab, backed by CoinGecko's Demo API (AURUM_COINGECKO_API_KEY). A holding is just another Asset under the hood (asset_class=crypto) with its value written as a regular AssetValuation — the existing Net Worth aggregation needs zero changes to pick it up. Prices never refresh in the background (no scheduler in this stack, same reasoning as recurring transactions): a "Refresh prices" button forces a sync, and GET /crypto/holdings lazily re-syncs at most once every 24h. CoinGecko outages leave existing values untouched (error_key: "unreachable") instead of failing the request. Backup/restore now carries CryptoHolding rows too, so a restore doesn't silently strip a holding's coin identity. Backend: 75/75 tests passing (9 new), including a real identity-map staleness bug the tests caught in the raw-Core-upsert + ORM re-read pattern, fixed via populate_existing=True. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01855PiW5Wd8n4aRW4n9kByY
This commit is contained in:
43
backend/app/api/routes/crypto.py
Normal file
43
backend/app/api/routes/crypto.py
Normal file
@@ -0,0 +1,43 @@
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.api.deps import get_session
|
||||
from app.schemas.crypto import CryptoHoldingCreate, CryptoHoldingRead, CryptoHoldingUpdate, CryptoSearchResult, CryptoSyncResult
|
||||
from app.services.crypto_service import create_holding, refresh_prices, search_coins, update_holding_quantity
|
||||
|
||||
router = APIRouter(prefix="/crypto", tags=["crypto"])
|
||||
|
||||
|
||||
@router.get("/holdings", response_model=CryptoSyncResult)
|
||||
async def read_holdings(session: AsyncSession = Depends(get_session)) -> CryptoSyncResult:
|
||||
"""Opening the Crypto tab lands here — this is also where the lazy
|
||||
once-a-day auto-refresh happens (see services/crypto_service.py):
|
||||
prices only actually get re-fetched from CoinGecko if 24h have passed
|
||||
since the last sync, otherwise this just reads the current cache."""
|
||||
return await refresh_prices(session, force=False)
|
||||
|
||||
|
||||
@router.post("/refresh", response_model=CryptoSyncResult)
|
||||
async def refresh_holdings(session: AsyncSession = Depends(get_session)) -> CryptoSyncResult:
|
||||
"""The "Refresh prices" button — always hits CoinGecko regardless of
|
||||
the once-a-day window."""
|
||||
return await refresh_prices(session, force=True)
|
||||
|
||||
|
||||
@router.post("/holdings", response_model=CryptoHoldingRead, status_code=201)
|
||||
async def create_holding_route(
|
||||
payload: CryptoHoldingCreate, session: AsyncSession = Depends(get_session)
|
||||
) -> CryptoHoldingRead:
|
||||
return await create_holding(session, payload)
|
||||
|
||||
|
||||
@router.patch("/holdings/{asset_id}", response_model=CryptoHoldingRead)
|
||||
async def update_holding_route(
|
||||
asset_id: int, payload: CryptoHoldingUpdate, session: AsyncSession = Depends(get_session)
|
||||
) -> CryptoHoldingRead:
|
||||
return await update_holding_quantity(session, asset_id, payload.quantity)
|
||||
|
||||
|
||||
@router.get("/search", response_model=list[CryptoSearchResult])
|
||||
async def search_coins_route(q: str = Query(min_length=1, max_length=100)) -> list[CryptoSearchResult]:
|
||||
return await search_coins(q)
|
||||
Reference in New Issue
Block a user