fix(ledger): keep legacy amount wire type and preserve dividend tax decimals

Co-authored-by: genichin <4282227+genichin@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-09-09 13:00:23 +00:00
committed by GitHub
co-authored by genichin
parent fde5b33f05
commit d3601d32dc
8 changed files with 131 additions and 19 deletions
@@ -292,6 +292,7 @@ def test_dividend_precision_and_reversal_round_trip_on_real_postgres(
assert dividend.price == 0.00000001
assert dividend.tax_amount == 0.00000001
assert str(dividend.tax_amount_decimal) == "0.00000001"
assert dividend.profit_buffer_after == 0.00000001
with repo._connect().cursor() as cur:
cur.execute(
@@ -329,6 +330,7 @@ def test_dividend_tax_omitted_persists_zero_on_real_postgres(
note="tax-omitted",
)
assert dividend.tax_amount == 0.0
assert str(dividend.tax_amount_decimal) == "0E-8"
with repo._connect().cursor() as cur:
cur.execute("SELECT tax_amount::text FROM portfolio_ledger WHERE ledger_entry_id = %s", (dividend.ledger_entry_id,))
assert cur.fetchone()[0] == "0.00000000"
@@ -429,6 +431,35 @@ def test_large_invest_and_large_dividend_tax_metadata_are_accepted_when_snapshot
note="large-tax-metadata",
)
assert dividend.tax_amount == 50000000.0
assert str(dividend.tax_amount_decimal) == "50000000.00000000"
repo.close()
def test_dividend_tax_alias_prone_decimals_round_trip_distinctly_on_real_postgres(
isolated_settings: Settings,
) -> None:
upgrade_to_head(isolated_settings)
repo = _repo(isolated_settings)
portfolio_id = _seed_portfolio(repo, "tax-alias-roundtrip")
first = repo.record_manual_ledger(
portfolio_id,
action="DIVIDEND",
amount=Decimal("1.00000000"),
tax=Decimal("90000000.00000001"),
executed_at=datetime(2026, 9, 1, 0, 0, 0, tzinfo=UTC),
note="alias-tax-1",
)
second = repo.record_manual_ledger(
portfolio_id,
action="DIVIDEND",
amount=Decimal("1.00000000"),
tax=Decimal("90000000.00000002"),
executed_at=datetime(2026, 9, 2, 0, 0, 0, tzinfo=UTC),
note="alias-tax-2",
)
assert str(first.tax_amount_decimal) == "90000000.00000001"
assert str(second.tax_amount_decimal) == "90000000.00000002"
repo.close()
+68 -3
View File
@@ -73,6 +73,7 @@ class _FakeRepo:
self.entries: list[LedgerEntry] = []
self._next_id = 1
self._dividend_amounts: dict[int, Decimal] = {}
self._dividend_tax_amounts: dict[int, Decimal] = {}
def close(self) -> None:
pass
@@ -95,6 +96,7 @@ class _FakeRepo:
price = float(kwargs.get("price") or 0.0)
fee = float(kwargs.get("fee") or 0.0)
tax = float(kwargs.get("tax") or 0.0)
tax_decimal = Decimal(str(kwargs.get("tax") if kwargs.get("tax") is not None else 0))
executed_at = kwargs.get("executed_at")
confirm_duplicate = bool(kwargs.get("confirm_duplicate") or False)
note = kwargs.get("note") or ""
@@ -202,6 +204,7 @@ class _FakeRepo:
price_decimal=amount_decimal if action == "DIVIDEND" else Decimal(str(price)),
fee_amount=fee,
tax_amount=tax,
tax_amount_decimal=tax_decimal if action == "DIVIDEND" else Decimal(str(tax)),
executed_at=executed_at,
recorded_at=datetime.now(UTC),
cash_balance_after=self.portfolio.cash_balance,
@@ -218,6 +221,7 @@ class _FakeRepo:
self.entries.append(entry)
if action == "DIVIDEND":
self._dividend_amounts[entry.ledger_entry_id] = amount_decimal
self._dividend_tax_amounts[entry.ledger_entry_id] = tax_decimal
return entry
def list_manual_ledger_entries(self, portfolio_id: str, limit: int = 50) -> list[LedgerEntry]:
@@ -507,7 +511,7 @@ def test_dividend_updates_only_profit_buffer_and_records_executed_at_and_tax() -
body = resp.json()
assert body["action"] == "DIVIDEND"
assert body["amount"] == "120"
assert body["tax"] == 10.0
assert body["tax"] == "10"
assert body["executed_at"] == "2026-09-01T00:00:00Z"
assert body["cash_balance_after"] == 1000.0
assert body["asset_quantity_after"] == 10.0
@@ -528,7 +532,7 @@ def test_dividend_tax_defaults_to_zero_when_omitted() -> None:
assert resp.status_code == 201
body = resp.json()
assert body["tax"] == 0.0
assert body["tax"] == "0"
assert body["cash_balance_after"] == 1000.0
assert body["asset_quantity_after"] == 10.0
assert body["profit_buffer_after"] == 620.0
@@ -726,6 +730,45 @@ def test_dividend_duplicate_check_does_not_collapse_decimals_that_can_alias_in_f
assert "90000000.00000002" in amounts
def test_dividend_tax_does_not_collapse_decimals_that_can_alias_in_float() -> None:
repo = _FakeRepo(_portfolio())
client = _make_client(repo)
first = client.post(
f"/v1/portfolios/{_PORTFOLIO_ID}/ledger/manual",
json={
"action": "DIVIDEND",
"amount": "1.00000000",
"tax": "90000000.00000001",
"executed_at": "2026-09-01",
"note": "Q3-1",
},
headers=_auth(),
)
assert first.status_code == 201
second = client.post(
f"/v1/portfolios/{_PORTFOLIO_ID}/ledger/manual",
json={
"action": "DIVIDEND",
"amount": "1.00000000",
"tax": "90000000.00000002",
"executed_at": "2026-09-02",
"note": "Q3-2",
},
headers=_auth(),
)
assert second.status_code == 201
assert first.json()["tax"] == "90000000.00000001"
assert second.json()["tax"] == "90000000.00000002"
history = client.get(f"/v1/portfolios/{_PORTFOLIO_ID}/ledger?source=manual", headers=_auth())
assert history.status_code == 200
taxes = [item["tax_amount"] for item in history.json()["items"] if item["action"] == "DIVIDEND"]
assert "90000000.00000001" in taxes
assert "90000000.00000002" in taxes
def test_reversed_dividend_is_excluded_from_future_duplicate_candidate_matching() -> None:
repo = _FakeRepo(_portfolio())
client = _make_client(repo)
@@ -762,6 +805,7 @@ def test_confirm_duplicate_false_is_accepted_for_non_dividend_actions() -> None:
headers=_auth(),
)
assert invest.status_code == 201
assert isinstance(invest.json()["amount"], float)
buy = client.post(
f"/v1/portfolios/{_PORTFOLIO_ID}/ledger/manual",
@@ -771,6 +815,25 @@ def test_confirm_duplicate_false_is_accepted_for_non_dividend_actions() -> None:
assert buy.status_code == 201
def test_unified_ledger_keeps_non_dividend_amount_as_number() -> None:
repo = _FakeRepo(_portfolio())
client = _make_client(repo)
created = client.post(
f"/v1/portfolios/{_PORTFOLIO_ID}/ledger/manual",
json={"action": "INVEST", "amount": 100},
headers=_auth(),
)
assert created.status_code == 201
assert created.json()["amount"] == 100.0
history = client.get(f"/v1/portfolios/{_PORTFOLIO_ID}/ledger?source=manual", headers=_auth())
assert history.status_code == 200
item = history.json()["items"][0]
assert item["action"] == "INVEST"
assert item["amount"] == 100.0
def test_confirm_duplicate_true_is_rejected_for_non_dividend_actions() -> None:
repo = _FakeRepo(_portfolio())
client = _make_client(repo)
@@ -906,7 +969,7 @@ def test_unified_ledger_includes_dividend_fields() -> None:
assert item["amount"] == "33"
assert item["price"] == 33.0
assert item["executed_at"] == "2026-09-01T00:00:00Z"
assert item["tax_amount"] == 3.0
assert item["tax_amount"] == "3"
assert item["note"] == "Q3"
assert item["profit_buffer_after"] == created.json()["profit_buffer_after"]
@@ -919,6 +982,8 @@ def test_unified_ledger_openapi_contains_nullable_amount() -> None:
assert response.status_code == 200
schemas = response.json()["components"]["schemas"]
assert "amount" in schemas["LedgerHistoryItem"]["properties"]
amount_schema = schemas["ManualLedgerResponse"]["properties"]["amount"]
assert any(option.get("type") == "number" for option in amount_schema.get("anyOf", []))
def test_reversal_rejects_fee_tax_and_missing_target() -> None: