mirror of
https://github.com/Zproger/Aurum.git
synced 2026-10-13 02:56:39 +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:
60
backend/app/schemas/crypto.py
Normal file
60
backend/app/schemas/crypto.py
Normal file
@@ -0,0 +1,60 @@
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
|
||||
class CryptoHoldingCreate(BaseModel):
|
||||
coingecko_id: str = Field(min_length=1, max_length=100)
|
||||
symbol: str = Field(min_length=1, max_length=20)
|
||||
name: str = Field(min_length=1, max_length=150)
|
||||
thumb_url: str | None = Field(default=None, max_length=500)
|
||||
# 18 decimal places covers wei-level token amounts; still gt=0 since a
|
||||
# zero/negative holding isn't a holding.
|
||||
quantity: Decimal = Field(gt=0, max_digits=38, decimal_places=18)
|
||||
|
||||
|
||||
class CryptoHoldingUpdate(BaseModel):
|
||||
quantity: Decimal = Field(gt=0, max_digits=38, decimal_places=18)
|
||||
|
||||
|
||||
class CryptoHoldingRead(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
asset_id: int
|
||||
coingecko_id: str
|
||||
symbol: str
|
||||
name: str
|
||||
thumb_url: str | None
|
||||
quantity: Decimal
|
||||
# Both derived from the linked Asset's latest AssetValuation — null
|
||||
# value/unit_price means "added but never priced yet" (CoinGecko was
|
||||
# unreachable at creation time), not zero.
|
||||
value: Decimal | None
|
||||
unit_price: Decimal | None
|
||||
as_of_date: str | None
|
||||
|
||||
|
||||
class CryptoSyncResult(BaseModel):
|
||||
# False when the lazy daily check ran but the 24h window hadn't
|
||||
# elapsed yet (nothing called CoinGecko this time, error_key is None),
|
||||
# OR when a refresh was attempted but CoinGecko couldn't be reached
|
||||
# (error_key is set) — check error_key to tell the two apart. Either
|
||||
# way `holdings` still carries the best data on hand, never blocked by
|
||||
# an external outage.
|
||||
#
|
||||
# A key, not a message string: the frontend translates it (RU/EN), same
|
||||
# key+params convention as FinancialAlert/AdviceItem — a raw English
|
||||
# sentence here would silently break the app's bilingual UI.
|
||||
synced: bool
|
||||
last_synced_at: datetime | None
|
||||
error_key: Literal["unreachable"] | None = None
|
||||
holdings: list[CryptoHoldingRead]
|
||||
|
||||
|
||||
class CryptoSearchResult(BaseModel):
|
||||
coingecko_id: str
|
||||
symbol: str
|
||||
name: str
|
||||
thumb_url: str | None
|
||||
Reference in New Issue
Block a user