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:
@@ -14,6 +14,8 @@ from app.models import ( # noqa: F401 — registers metadata
|
||||
AssetValuation,
|
||||
Budget,
|
||||
Category,
|
||||
CryptoHolding,
|
||||
CryptoSyncState,
|
||||
Goal,
|
||||
GoalContribution,
|
||||
RecurringTransaction,
|
||||
|
||||
45
backend/alembic/versions/3e3503b61b20_add_crypto_holdings.py
Normal file
45
backend/alembic/versions/3e3503b61b20_add_crypto_holdings.py
Normal file
@@ -0,0 +1,45 @@
|
||||
"""add crypto holdings and sync state
|
||||
|
||||
Revision ID: 3e3503b61b20
|
||||
Revises: f6a4d8b2e017
|
||||
Create Date: 2026-08-30 12:00:00.000000
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '3e3503b61b20'
|
||||
down_revision: Union[str, None] = 'f6a4d8b2e017'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.create_table(
|
||||
'crypto_holdings',
|
||||
sa.Column('asset_id', sa.Integer(), nullable=False),
|
||||
sa.Column('coingecko_id', sa.String(length=100), nullable=False),
|
||||
sa.Column('symbol', sa.String(length=20), nullable=False),
|
||||
sa.Column('name', sa.String(length=150), nullable=False),
|
||||
sa.Column('thumb_url', sa.String(length=500), nullable=True),
|
||||
sa.Column('quantity', sa.Numeric(precision=38, scale=18), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
||||
sa.ForeignKeyConstraint(['asset_id'], ['assets.id'], ondelete='CASCADE'),
|
||||
sa.PrimaryKeyConstraint('asset_id'),
|
||||
)
|
||||
op.create_table(
|
||||
'crypto_sync_state',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('last_synced_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_table('crypto_sync_state')
|
||||
op.drop_table('crypto_holdings')
|
||||
Reference in New Issue
Block a user