fix: qualify corporate-action ledger reads (#36)

This commit is contained in:
2026-09-13 22:02:43 +09:00
committed by GitHub
parent c7d8235bde
commit 8841bdd441
12 changed files with 290 additions and 22 deletions
@@ -165,6 +165,33 @@ def _record(repo: PostgresPortfolioRepository, asset_id: str, request: Corporate
)
def test_joined_ledger_reads_qualify_base_columns(repository):
"""Mixed trade/corporate-action rows must be readable through both joins."""
asset_id = _seed_daekyo(repository, quantity=Decimal(1452))
_record(repository, asset_id, _request(), "ca-joined-read")
pnl_entries = repository.get_all_trades_for_pnl(_PORTFOLIO_ID)
ledger_entries = repository.list_ledger_entries(_PORTFOLIO_ID, limit=50)
assert len(pnl_entries) == 2
assert len(ledger_entries) == 2
assert any(entry.corporate_action for entry in pnl_entries)
assert any(entry.corporate_action for entry in ledger_entries)
assert ledger_entries[0].recorded_at >= ledger_entries[1].recorded_at
corporate_entries = repository.list_ledger_entries(_PORTFOLIO_ID, source="corporate_action", limit=50)
assert len(corporate_entries) == 1
assert corporate_entries[0].corporate_action is not None
next_page = repository.list_ledger_entries(
_PORTFOLIO_ID,
before_recorded_at=ledger_entries[0].recorded_at,
before_ledger_entry_id=ledger_entries[0].ledger_entry_id,
limit=50,
)
assert [entry.source for entry in next_page] == ["broker"]
def _state(repo: PostgresPortfolioRepository) -> tuple:
with repo._connect().cursor() as cur:
cur.execute(
+1 -1
View File
@@ -64,7 +64,7 @@ def test_default_resume_flag_preserves_pre_recovery_idempotency_hash():
def test_runtime_schema_has_one_corporate_action_ddl_and_replay_mapping_starts_at_18():
source = Path("app/repositories/postgres.py").read_text()
assert source.count("CREATE TABLE IF NOT EXISTS portfolio_corporate_actions") == 1
assert "strategy_set_id, note, source," in source
assert "ledger.strategy_set_id, ledger.note, ledger.source," in source
assert "ca.corporate_action_id, ca.action_type" in source
assert 'row[18] is not None' in source
+35 -1
View File
@@ -7,7 +7,7 @@ Covers AC-1~11 from REQ-043.
from __future__ import annotations
from contextlib import ExitStack
from datetime import datetime, timezone
from datetime import UTC, datetime, timezone
from unittest.mock import MagicMock, patch
import pytest
@@ -286,6 +286,40 @@ class TestAC2PartialSellWacUnchanged:
assert resp.json()["total_cost_basis"] == pytest.approx(500_000.0)
def test_corporate_action_row_keeps_holdings_cost_basis_http_200() -> None:
buy = _make_entry(
ledger_entry_id=1,
side="buy",
quantity=20,
price=100,
executed_at=datetime(2026, 9, 1, tzinfo=UTC),
)
corporate_action = LedgerEntry(
ledger_entry_id=2,
portfolio_id=_PORTFOLIO_ID,
source="corporate_action",
side="stock_consolidation",
quantity=10,
price=200,
recorded_at=datetime(2026, 9, 2, tzinfo=UTC),
corporate_action={"action_type": "stock_consolidation"},
)
repo = _make_service_repo(
portfolio=_make_portfolio(asset_quantity=10),
trades=[buy, corporate_action],
)
with _with_repo(repo):
response = _make_client(_make_settings()).get(
_PATH,
headers={"Authorization": "Bearer tok"},
)
assert response.status_code == 200
assert response.json()["cost_basis_per_unit"] == pytest.approx(100)
assert response.json()["total_cost_basis"] == pytest.approx(1000)
# ---------------------------------------------------------------------------
# AC-3: user read_token (owner) → 200 + normal response
# ---------------------------------------------------------------------------
+57 -1
View File
@@ -5,7 +5,7 @@ from __future__ import annotations
import os
import time
from contextlib import ExitStack
from datetime import UTC, datetime
from datetime import UTC, date, datetime
from decimal import Decimal
from typing import Any
from unittest.mock import patch
@@ -977,6 +977,62 @@ def test_unified_ledger_includes_dividend_fields() -> None:
assert item["profit_buffer_after"] == created.json()["profit_buffer_after"]
def test_unified_ledger_includes_corporate_action_fields() -> None:
repo = _FakeRepo(_portfolio())
repo.entries.append(
LedgerEntry(
ledger_entry_id=7,
portfolio_id=_PORTFOLIO_ID,
source="corporate_action",
side="stock_consolidation",
quantity=10,
price=200,
recorded_at=datetime(2026, 9, 2, tzinfo=UTC),
corporate_action={
"corporate_action_id": "ca-7",
"action_type": "stock_consolidation",
"effective_date": date(2026, 9, 2),
"quantity_before": Decimal(20),
"quantity_after": Decimal(10),
"average_price_before": Decimal(100),
"average_price_after": Decimal(200),
"total_cost_basis": Decimal(2000),
"cash_balance": Decimal(1000),
"profit_buffer": Decimal(500),
"evidence_ref": "notice://ca-7",
"actor_user_id": _USER_ID,
},
)
)
client = _make_client(repo)
response = client.get(
f"/v1/portfolios/{_PORTFOLIO_ID}/ledger?source=corporate_action",
headers=_auth(),
)
assert response.status_code == 200
item = response.json()["items"][0]
assert item == {
"ledger_entry_id": 7,
"portfolio_id": _PORTFOLIO_ID,
"source": "corporate_action",
"event_id": "ca-7",
"event_type": "stock_consolidation",
"effective_date": "2026-09-02",
"quantity_before": "20",
"quantity_after": "10",
"average_price_before": "100",
"average_price_after": "200",
"total_cost_basis": "2000",
"cash_balance": "1000",
"profit_buffer": "500",
"evidence_ref": "notice://ca-7",
"actor_user_id": _USER_ID,
"recorded_at": "2026-09-02T00:00:00Z",
}
def test_unified_ledger_openapi_contains_nullable_amount() -> None:
repo = _FakeRepo(_portfolio())
client = _make_client(repo)