mirror of
https://github.com/Zproger/Aurum.git
synced 2026-10-13 02:56:39 +07:00
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
44 lines
2.0 KiB
Python
44 lines
2.0 KiB
Python
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)
|