fix: add unified ledger amount and enforce manual decimal precision
Co-authored-by: genichin <4282227+genichin@users.noreply.github.com>
This commit is contained in:
co-authored by
genichin
parent
ccde81b7a0
commit
a71c91f6ef
@@ -10,6 +10,7 @@ from __future__ import annotations
|
||||
|
||||
import os
|
||||
from datetime import datetime, timezone
|
||||
from decimal import Decimal
|
||||
from urllib.parse import unquote, urlparse
|
||||
from uuid import uuid4
|
||||
|
||||
@@ -271,3 +272,43 @@ def test_broker_and_manual_ledger_writes_regress_after_real_upgrade(
|
||||
)
|
||||
assert cur.fetchone() == ("manual", "bat025-user", "synthetic-local-fixture")
|
||||
repo.close()
|
||||
|
||||
|
||||
def test_dividend_precision_and_reversal_round_trip_on_real_postgres(
|
||||
isolated_settings: Settings,
|
||||
) -> None:
|
||||
"""DIVIDEND min unit persists and REVERSAL removes the same amount exactly."""
|
||||
upgrade_to_head(isolated_settings)
|
||||
repo = _repo(isolated_settings)
|
||||
portfolio_id = _seed_portfolio(repo, "dividend-precision")
|
||||
|
||||
dividend = repo.record_manual_ledger(
|
||||
portfolio_id,
|
||||
action="DIVIDEND",
|
||||
amount=Decimal("0.00000001"),
|
||||
tax=Decimal("0.00000001"),
|
||||
executed_at=datetime(2026, 9, 1, 0, 0, 0, tzinfo=timezone.utc),
|
||||
note="precision-test",
|
||||
)
|
||||
|
||||
assert dividend.price == 0.00000001
|
||||
assert dividend.tax_amount == 0.00000001
|
||||
assert dividend.profit_buffer_after == 0.00000001
|
||||
with repo._connect().cursor() as cur:
|
||||
cur.execute(
|
||||
"SELECT price::text, tax_amount::text, profit_buffer_after::text FROM portfolio_ledger WHERE ledger_entry_id = %s",
|
||||
(dividend.ledger_entry_id,),
|
||||
)
|
||||
assert cur.fetchone() == ("0.00000001", "0.00000001", "0.00000001")
|
||||
|
||||
reversal = repo.record_manual_ledger(
|
||||
portfolio_id,
|
||||
action="REVERSAL",
|
||||
target_ledger_entry_id=dividend.ledger_entry_id,
|
||||
)
|
||||
assert reversal.profit_buffer_after == 0.0
|
||||
with repo._connect().cursor() as cur:
|
||||
cur.execute("SELECT profit_buffer::text FROM portfolios WHERE portfolio_id = %s", (portfolio_id,))
|
||||
assert cur.fetchone()[0] == "0"
|
||||
|
||||
repo.close()
|
||||
|
||||
@@ -500,6 +500,30 @@ def test_dividend_requires_executed_at_and_rejects_fee_field() -> None:
|
||||
assert invalid_fee.status_code == 422
|
||||
|
||||
|
||||
def test_dividend_rejects_sub_minimum_numeric_precision() -> None:
|
||||
repo = _FakeRepo(_portfolio())
|
||||
client = _make_client(repo)
|
||||
|
||||
too_precise = client.post(
|
||||
f"/v1/portfolios/{_PORTFOLIO_ID}/ledger/manual",
|
||||
json={"action": "DIVIDEND", "amount": "0.000000004", "executed_at": "2026-09-01"},
|
||||
headers=_auth(),
|
||||
)
|
||||
assert too_precise.status_code == 422
|
||||
|
||||
|
||||
def test_dividend_rejects_amount_exceeding_numeric_20_8() -> None:
|
||||
repo = _FakeRepo(_portfolio())
|
||||
client = _make_client(repo)
|
||||
|
||||
too_large = client.post(
|
||||
f"/v1/portfolios/{_PORTFOLIO_ID}/ledger/manual",
|
||||
json={"action": "DIVIDEND", "amount": "1000000000000.00000000", "executed_at": "2026-09-01"},
|
||||
headers=_auth(),
|
||||
)
|
||||
assert too_large.status_code == 422
|
||||
|
||||
|
||||
def test_dividend_duplicate_requires_confirmation() -> None:
|
||||
repo = _FakeRepo(_portfolio())
|
||||
client = _make_client(repo)
|
||||
@@ -571,12 +595,24 @@ def test_unified_ledger_includes_dividend_fields() -> None:
|
||||
item = resp.json()["items"][0]
|
||||
assert item["source"] == "manual"
|
||||
assert item["action"] == "DIVIDEND"
|
||||
assert item["amount"] == 33.0
|
||||
assert item["price"] == 33.0
|
||||
assert item["executed_at"] == "2026-09-01T00:00:00Z"
|
||||
assert item["tax_amount"] == 3.0
|
||||
assert item["note"] == "Q3"
|
||||
assert item["profit_buffer_after"] == created.json()["profit_buffer_after"]
|
||||
|
||||
|
||||
def test_unified_ledger_openapi_contains_nullable_amount() -> None:
|
||||
repo = _FakeRepo(_portfolio())
|
||||
client = _make_client(repo)
|
||||
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200
|
||||
schemas = response.json()["components"]["schemas"]
|
||||
assert "amount" in schemas["LedgerHistoryItem"]["properties"]
|
||||
|
||||
|
||||
def test_reversal_rejects_fee_tax_and_missing_target() -> None:
|
||||
repo = _FakeRepo(_portfolio())
|
||||
client = _make_client(repo)
|
||||
|
||||
Reference in New Issue
Block a user