feat(V1-API): implement TREX-Basic consumer API contract with encryption and persistence
- Add V1 API endpoints: assets, ledger, user-info - Implement Fernet-based credential payload encryption (req-010) - Extend health response schema with runtime info (req-004) - Add service token runtime ID issuance (req-005) - Implement database schema migration for V1 API (req-009) - Add comprehensive test coverage for V1 endpoints - Update SRS with 10 new requirements (REQ-004 through REQ-011) - Create batch planning, design, and verification for BAT-001 - Integrate review-discovery StagePilot skill - Update documentation indices and baseline specs
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
"""Add v1 API schema: portfolios.market, auth_tokens.runtime_id, broker_credentials, ledger_trades
|
||||
|
||||
Revision ID: 20260502_0014
|
||||
Revises: 20260423_0013
|
||||
Create Date: 2026-05-02 00:00:00
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
from sqlalchemy import inspect
|
||||
|
||||
|
||||
revision = "20260502_0014"
|
||||
down_revision = "20260423_0013"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def _table_exists(inspector: sa.engine.Inspector, table_name: str) -> bool:
|
||||
return table_name in inspector.get_table_names()
|
||||
|
||||
|
||||
def _column_exists(inspector: sa.engine.Inspector, table_name: str, column_name: str) -> bool:
|
||||
if not _table_exists(inspector, table_name):
|
||||
return False
|
||||
return any(c["name"] == column_name for c in inspector.get_columns(table_name))
|
||||
|
||||
|
||||
def _index_exists(inspector: sa.engine.Inspector, table_name: str, index_name: str) -> bool:
|
||||
if not _table_exists(inspector, table_name):
|
||||
return False
|
||||
return any(idx["name"] == index_name for idx in inspector.get_indexes(table_name))
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
bind = op.get_bind()
|
||||
inspector = inspect(bind)
|
||||
|
||||
# 1. portfolios.market TEXT NULLABLE
|
||||
if not _column_exists(inspector, "portfolios", "market"):
|
||||
op.add_column("portfolios", sa.Column("market", sa.Text(), nullable=True))
|
||||
|
||||
# 2. auth_tokens.runtime_id TEXT UNIQUE NULLABLE
|
||||
if not _column_exists(inspector, "auth_tokens", "runtime_id"):
|
||||
op.add_column("auth_tokens", sa.Column("runtime_id", sa.Text(), nullable=True))
|
||||
op.create_index(
|
||||
"idx_auth_tokens_runtime_id_unique",
|
||||
"auth_tokens",
|
||||
["runtime_id"],
|
||||
unique=True,
|
||||
postgresql_where=sa.text("runtime_id IS NOT NULL"),
|
||||
)
|
||||
|
||||
# 3. broker_credentials 테이블 신규
|
||||
if not _table_exists(inspector, "broker_credentials"):
|
||||
op.create_table(
|
||||
"broker_credentials",
|
||||
sa.Column("user_id", sa.Integer(), nullable=False),
|
||||
sa.Column("broker", sa.Text(), nullable=False),
|
||||
sa.Column("account_ref", sa.Text(), nullable=False),
|
||||
sa.Column("encrypted_payload", sa.Text(), nullable=False),
|
||||
sa.Column("credential_version", sa.Integer(), nullable=False, server_default="1"),
|
||||
sa.Column(
|
||||
"last_rotated_at",
|
||||
sa.TIMESTAMP(timezone=True),
|
||||
nullable=False,
|
||||
server_default=sa.text("NOW()"),
|
||||
),
|
||||
sa.PrimaryKeyConstraint("user_id", "broker"),
|
||||
)
|
||||
|
||||
# 4. ledger_trades 테이블 신규
|
||||
if not _table_exists(inspector, "ledger_trades"):
|
||||
op.create_table(
|
||||
"ledger_trades",
|
||||
sa.Column(
|
||||
"ledger_entry_id",
|
||||
sa.BigInteger(),
|
||||
sa.Identity(always=False),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column("asset_id", sa.Text(), nullable=False),
|
||||
sa.Column("order_id", sa.Text(), nullable=False),
|
||||
sa.Column("execution_id", sa.Text(), nullable=False),
|
||||
sa.Column("side", sa.Text(), nullable=False),
|
||||
sa.Column("quantity", sa.Numeric(20, 8), nullable=False),
|
||||
sa.Column("price", sa.Numeric(20, 8), nullable=False),
|
||||
sa.Column("fee_amount", sa.Numeric(20, 8), nullable=False, server_default="0"),
|
||||
sa.Column("tax_amount", sa.Numeric(20, 8), nullable=False, server_default="0"),
|
||||
sa.Column("executed_at", sa.TIMESTAMP(timezone=True), nullable=False),
|
||||
sa.Column(
|
||||
"recorded_at",
|
||||
sa.TIMESTAMP(timezone=True),
|
||||
nullable=False,
|
||||
server_default=sa.text("NOW()"),
|
||||
),
|
||||
sa.PrimaryKeyConstraint("ledger_entry_id"),
|
||||
sa.UniqueConstraint("asset_id", "order_id", "execution_id", name="uq_ledger_trades_idempotency"),
|
||||
)
|
||||
op.create_index(
|
||||
"idx_ledger_trades_asset_executed",
|
||||
"ledger_trades",
|
||||
["asset_id", "executed_at"],
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
bind = op.get_bind()
|
||||
inspector = inspect(bind)
|
||||
|
||||
if _table_exists(inspector, "ledger_trades"):
|
||||
op.drop_table("ledger_trades")
|
||||
if _table_exists(inspector, "broker_credentials"):
|
||||
op.drop_table("broker_credentials")
|
||||
if _column_exists(inspector, "auth_tokens", "runtime_id"):
|
||||
if _index_exists(inspector, "auth_tokens", "idx_auth_tokens_runtime_id_unique"):
|
||||
op.drop_index("idx_auth_tokens_runtime_id_unique", table_name="auth_tokens")
|
||||
op.drop_column("auth_tokens", "runtime_id")
|
||||
if _column_exists(inspector, "portfolios", "market"):
|
||||
op.drop_column("portfolios", "market")
|
||||
Reference in New Issue
Block a user