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:
co-authored by
genichin
parent
fde5b33f05
commit
d3601d32dc
@@ -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()
|
||||
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user