fix corporate action runtime blockers
This commit is contained in:
@@ -24,6 +24,7 @@ from app.core.auth import AuthContext, Permission, require_read, require_trade_w
|
||||
from app.core.config import get_settings
|
||||
from app.domain.errors import DaemonInactiveError, DaemonNotFoundError
|
||||
from app.domain.models import CorporateAction, Portfolio, PortfolioComment
|
||||
from app.domain.services import PORTFOLIO_NOT_FOUND_MESSAGE
|
||||
from app.domain.pnl import calculate_current_wac, calculate_realized_pnl
|
||||
from app.repositories.factory import get_portfolio_repository_from_settings
|
||||
from app.schemas.v1 import (
|
||||
@@ -1332,7 +1333,7 @@ def create_corporate_action(portfolio_id: str, payload: CorporateActionRequest,
|
||||
return CorporateActionResponse(**saved.__dict__)
|
||||
except ValueError as exc:
|
||||
code = str(exc)
|
||||
if code == "portfolio not found":
|
||||
if code == PORTFOLIO_NOT_FOUND_MESSAGE:
|
||||
raise HTTPException(status_code=404, detail="Portfolio not found") from exc
|
||||
raise HTTPException(status_code=409 if code.startswith(("IDEMPOTENCY", "CORPORATE_ACTION_")) else 400, detail=code) from exc
|
||||
finally:
|
||||
|
||||
@@ -7,7 +7,7 @@ from typing import Callable, Protocol
|
||||
|
||||
from app.domain.models import LedgerEntry, LiveAssetSource, Portfolio, StrategySet
|
||||
|
||||
_NOT_FOUND_MSG = "포트폴리오를 찾을 수 없습니다"
|
||||
PORTFOLIO_NOT_FOUND_MESSAGE = "포트폴리오를 찾을 수 없습니다"
|
||||
_INVEST_ACTION_MAP = {
|
||||
"deposit": "INVEST",
|
||||
"profit": "PROFIT",
|
||||
@@ -250,7 +250,7 @@ def apply_reset(
|
||||
portfolio = repo.reset(portfolio_id)
|
||||
return portfolio, False
|
||||
except ValueError as exc:
|
||||
if str(exc) != _NOT_FOUND_MSG:
|
||||
if str(exc) != PORTFOLIO_NOT_FOUND_MESSAGE:
|
||||
raise
|
||||
symbol = infer_symbol(portfolio_id)
|
||||
portfolio = repo.get_or_create(portfolio_id, symbol, initial_cash=0.0)
|
||||
|
||||
@@ -1654,7 +1654,7 @@ class PostgresPortfolioRepository:
|
||||
return action
|
||||
if row[4] != "enabled":
|
||||
raise ValueError("CORPORATE_ACTION_PORTFOLIO_DISABLED")
|
||||
if row[3] != action.asset_id:
|
||||
if str(row[3]) != str(action.asset_id):
|
||||
raise ValueError("CORPORATE_ACTION_ASSET_MISMATCH")
|
||||
if Decimal(str(row[1])) != action.cash_balance or Decimal(str(row[2])) != action.profit_buffer:
|
||||
raise ValueError("CORPORATE_ACTION_INVARIANT_MISMATCH")
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
from datetime import date
|
||||
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, Portfolio
|
||||
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("0"), 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("0"), Decimal("10"), Decimal("0"), asset_id, "enabled"), None, None, None, (7, date(2026, 1, 1))]
|
||||
cur.fetchall.return_value = []
|
||||
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")
|
||||
Reference in New Issue
Block a user