fix: enforce snapshot-consistent precision checks for manual ledger

Co-authored-by: genichin <4282227+genichin@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-09-09 03:08:46 +00:00
committed by GitHub
co-authored by genichin
parent aa40c7fdff
commit b74284e626
4 changed files with 147 additions and 63 deletions
@@ -9,7 +9,7 @@ or production application database.
from __future__ import annotations
import os
from datetime import datetime, timezone
from datetime import UTC, datetime
from decimal import Decimal
from urllib.parse import unquote, urlparse
from uuid import uuid4
@@ -106,10 +106,9 @@ def _current_revision(settings: Settings) -> str:
user=settings.active_db_user,
password=settings.active_db_password,
sslmode=settings.active_db_sslmode,
) as conn:
with conn.cursor() as cur:
cur.execute("SELECT version_num FROM alembic_version")
return str(cur.fetchone()[0])
) as conn, conn.cursor() as cur:
cur.execute("SELECT version_num FROM alembic_version")
return str(cur.fetchone()[0])
def _metadata_columns(settings: Settings) -> dict[str, tuple[str, str, str | None]]:
@@ -120,18 +119,17 @@ def _metadata_columns(settings: Settings) -> dict[str, tuple[str, str, str | Non
user=settings.active_db_user,
password=settings.active_db_password,
sslmode=settings.active_db_sslmode,
) as conn:
with conn.cursor() as cur:
cur.execute(
"""
) as conn, conn.cursor() as cur:
cur.execute(
"""
SELECT column_name, data_type, is_nullable, column_default
FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'portfolio_ledger'
AND column_name IN ('source', 'target_ledger_entry_id',
'confirmation_evidence', 'actor_user_id')
"""
)
return {row[0]: (row[1], row[2], row[3]) for row in cur.fetchall()}
)
return {row[0]: (row[1], row[2], row[3]) for row in cur.fetchall()}
def _seed_portfolio(repo: PostgresPortfolioRepository, suffix: str) -> str:
@@ -169,7 +167,7 @@ def test_upgrade_from_pre_0028_preserves_business_data_and_backfills_source(
command.upgrade(get_alembic_config(isolated_settings), _PRE_BAT025)
repo = _repo(isolated_settings)
portfolio_id = _seed_portfolio(repo, "preserve")
recorded_at = datetime(2026, 7, 15, tzinfo=timezone.utc)
recorded_at = datetime(2026, 7, 15, tzinfo=UTC)
with repo._connect().cursor() as cur:
cur.execute(
"""
@@ -249,7 +247,7 @@ def test_broker_and_manual_ledger_writes_regress_after_real_upgrade(
side="buy",
quantity=2,
price=100,
executed_at=datetime.now(timezone.utc),
executed_at=datetime.now(UTC),
)
manual = repo.record_manual_ledger(
portfolio_id,
@@ -287,7 +285,7 @@ def test_dividend_precision_and_reversal_round_trip_on_real_postgres(
action="DIVIDEND",
amount=Decimal("0.00000001"),
tax=Decimal("0.00000001"),
executed_at=datetime(2026, 9, 1, 0, 0, 0, tzinfo=timezone.utc),
executed_at=datetime(2026, 9, 1, 0, 0, 0, tzinfo=UTC),
note="precision-test",
)
@@ -352,7 +350,60 @@ def test_dividend_large_integer_with_8dp_is_rejected_to_protect_double_snapshot(
action="DIVIDEND",
amount=Decimal("1000000000.00000001"),
tax=Decimal("0.00000001"),
executed_at=datetime(2026, 9, 1, 0, 0, 0, tzinfo=timezone.utc),
executed_at=datetime(2026, 9, 1, 0, 0, 0, tzinfo=UTC),
note="too-large-for-double",
)
repo.close()
def test_large_existing_buffer_plus_min_dividend_is_rejected_before_snapshot_divergence(
isolated_settings: Settings,
) -> None:
"""Reject writes that would diverge between NUMERIC ledger and DOUBLE snapshot."""
upgrade_to_head(isolated_settings)
repo = _repo(isolated_settings)
portfolio_id = _seed_portfolio(repo, "large-buffer-min-dividend")
with repo._connect().cursor() as cur:
cur.execute("UPDATE portfolios SET profit_buffer = %s WHERE portfolio_id = %s", (1_000_000_000.0, portfolio_id))
with pytest.raises(ValueError, match="DOUBLE PRECISION snapshot"):
repo.record_manual_ledger(
portfolio_id,
action="DIVIDEND",
amount=Decimal("0.00000001"),
executed_at=datetime(2026, 9, 1, 0, 0, 0, tzinfo=UTC),
note="should-reject",
)
with repo._connect().cursor() as cur:
cur.execute("SELECT COUNT(*) FROM portfolio_ledger WHERE portfolio_id = %s AND side = 'dividend'", (portfolio_id,))
assert cur.fetchone()[0] == 0
repo.close()
def test_large_invest_and_large_dividend_tax_metadata_are_accepted_when_snapshot_is_safe(
isolated_settings: Settings,
) -> None:
"""No hidden per-field cap for values that do not cause snapshot divergence."""
upgrade_to_head(isolated_settings)
repo = _repo(isolated_settings)
portfolio_id = _seed_portfolio(repo, "large-invest-large-tax")
invest = repo.record_manual_ledger(
portfolio_id,
action="INVEST",
amount=Decimal("50000000.00000000"),
note="large-invest",
)
assert invest.price == 50000000.0
dividend = repo.record_manual_ledger(
portfolio_id,
action="DIVIDEND",
amount=Decimal("1.00000000"),
tax=Decimal("50000000.00000000"),
executed_at=datetime(2026, 9, 1, 0, 0, 0, tzinfo=UTC),
note="large-tax-metadata",
)
assert dividend.tax_amount == 50000000.0
repo.close()
+3 -3
View File
@@ -3,7 +3,7 @@
from __future__ import annotations
from contextlib import ExitStack
from datetime import datetime, timezone
from datetime import UTC, datetime
from typing import Any
from unittest.mock import patch
@@ -195,7 +195,7 @@ class _FakeRepo:
fee_amount=fee,
tax_amount=tax,
executed_at=executed_at,
recorded_at=datetime.now(timezone.utc),
recorded_at=datetime.now(UTC),
cash_balance_after=self.portfolio.cash_balance,
asset_quantity_after=self.portfolio.asset_quantity,
profit_buffer_after=self.portfolio.profit_buffer,
@@ -674,7 +674,7 @@ def test_unified_ledger_paginates_mixed_sources_and_hides_side() -> None:
repo.entries.append(
LedgerEntry(
ledger_entry_id=99, portfolio_id=_PORTFOLIO_ID, side="buy", quantity=2, price=10, source="broker",
recorded_at=datetime.now(timezone.utc), cash_balance_after=900, asset_quantity_after=12,
recorded_at=datetime.now(UTC), cash_balance_after=900, asset_quantity_after=12,
)
)
first = client.get(f"/v1/portfolios/{_PORTFOLIO_ID}/ledger?limit=1", headers=_auth())