Files
portfolio-hub/tests/test_issue34_runtime_blockers.py
T
genichin 2b470c735f
Publish verified PortfolioHub image / verify (push) Canceled after 0s
Publish verified PortfolioHub image / publish (push) Canceled after 0s
fix(portfolio): tolerate consolidation WAC residue
2026-09-13 18:36:00 +09:00

123 lines
4.3 KiB
Python

from datetime import UTC, date, datetime
from decimal import Decimal
from unittest.mock import MagicMock
from uuid import UUID
import pytest
from fastapi.testclient import TestClient
from app.api import v1_portfolios
from app.domain.models import CorporateAction
from app.main import create_app
from app.repositories.postgres import PostgresPortfolioRepository
class MissingPortfolioRepo:
def get(self, portfolio_id):
raise ValueError("포트폴리오를 찾을 수 없습니다")
def close(self):
pass
def test_create_corporate_action_maps_postgres_missing_portfolio_to_404(monkeypatch):
app = create_app()
repo = MissingPortfolioRepo()
monkeypatch.setattr(v1_portfolios, "_get_repo", lambda: repo)
app.dependency_overrides[v1_portfolios.require_trade_write] = lambda: type("Auth", (), {"user_id": "u1", "permission": "trade_write"})()
client = TestClient(app)
response = client.post(
"/v1/portfolios/missing/corporate-actions",
headers={"Idempotency-Key": "idem-1"},
json={"effective_date": "2026-01-01", "quantity_before": "10", "quantity_after": "5", "evidence_ref": "ev"},
)
assert response.status_code == 404
assert response.json()["detail"] == "Portfolio not found"
def _action(asset_id="asset-1"):
return CorporateAction("ca-1", "pf-1", asset_id, "stock_consolidation", date(2026, 1, 1), Decimal(2), Decimal(1), Decimal(0), Decimal(0), Decimal(0), Decimal(10), Decimal(0), "ev", "idem", "hash", 0)
def _repo_for_row(asset_id):
repo = object.__new__(PostgresPortfolioRepository)
repo.ensure_schema = MagicMock()
conn = MagicMock()
cur = MagicMock()
conn.__enter__.return_value = conn
conn.cursor.return_value.__enter__.return_value = cur
repo._connect = MagicMock(return_value=conn)
cur.fetchone.side_effect = [(Decimal(2), Decimal(10), Decimal(0), asset_id, "enabled"), None, None, None, (7, date(2026, 1, 1))]
cur.fetchall.return_value = [("buy", Decimal(2), Decimal(0), Decimal(0), Decimal(0), datetime(2025, 12, 31, tzinfo=UTC))]
return repo
def test_record_corporate_action_accepts_uuid_row_against_string_domain_id():
asset = UUID("12345678-1234-5678-1234-567812345678")
action = _action(str(asset))
saved = _repo_for_row(asset).record_corporate_action("pf-1", action=action, idempotency_key="idem", payload_hash="hash")
assert saved.ledger_entry_id == 7
def test_record_corporate_action_rejects_different_asset_id():
repo = _repo_for_row(UUID("12345678-1234-5678-1234-567812345678"))
with pytest.raises(ValueError, match="CORPORATE_ACTION_ASSET_MISMATCH"):
repo.record_corporate_action("pf-1", action=_action("87654321-4321-8765-4321-876543218765"), idempotency_key="idem", payload_hash="hash")
def test_record_corporate_action_accepts_daekyo_rounding_residue():
repo = object.__new__(PostgresPortfolioRepository)
repo.ensure_schema = MagicMock()
conn = MagicMock()
cur = MagicMock()
conn.__enter__.return_value = conn
conn.cursor.return_value.__enter__.return_value = cur
repo._connect = MagicMock(return_value=conn)
cur.fetchone.side_effect = [
(Decimal(1452), Decimal(10), Decimal(0), "asset-1", "enabled"),
None,
None,
None,
(7, datetime(2026, 9, 13, tzinfo=UTC)),
]
cur.fetchall.return_value = [
(
"buy",
Decimal(1452),
Decimal("1086.234145936639256198347107438016528926"),
Decimal(0),
Decimal(0),
datetime(2026, 9, 9, tzinfo=UTC),
)
]
action = CorporateAction(
"ca-1",
"pf-1",
"asset-1",
"stock_consolidation",
date(2026, 9, 10),
Decimal(1452),
Decimal(726),
Decimal(0),
Decimal(0),
Decimal(0),
Decimal(10),
Decimal(0),
"KIS 019685 2026-09-10",
"idem-daekyo",
"hash-daekyo",
0,
)
saved = repo.record_corporate_action(
"pf-1",
action=action,
idempotency_key="idem-daekyo",
payload_hash="hash-daekyo",
)
assert saved.quantity_after == Decimal(726)
assert saved.average_price_after == Decimal("2172.46829187")
assert saved.total_cost_basis == Decimal("1577211.97990000")
assert saved.ledger_entry_id == 7