Preserve ledger venue provenance (#39)

* feat: preserve ledger venue provenance

* fix: satisfy ledger test lint ratchet

* fix: advance image migration inventory
This commit is contained in:
2026-09-19 19:30:32 +09:00
committed by GitHub
parent 567d662913
commit 996127f215
23 changed files with 768 additions and 35 deletions
+23 -2
View File
@@ -27,6 +27,7 @@ from app.domain.models import CorporateAction, Portfolio, PortfolioComment
from app.domain.pnl import calculate_current_wac, calculate_realized_pnl
from app.domain.services import PORTFOLIO_NOT_FOUND_MESSAGE
from app.repositories.factory import get_portfolio_repository_from_settings
from app.repositories.portfolio_repository import LedgerTradeConflictError
from app.schemas.corporate_actions import (
CorporateActionRequest,
CorporateActionResponse,
@@ -604,7 +605,13 @@ def _require_service_token(auth: AuthContext = Depends(require_trade_write)) ->
return auth
def _payloads_match(existing, body: LedgerTradeRequest) -> bool:
def _economic_payloads_match(existing, body: LedgerTradeRequest) -> bool:
"""Defence in depth for non-Postgres repository implementations.
The Postgres repository performs this comparison under the portfolio and
ledger row locks; this check preserves the public contract for test or
alternate repository implementations.
"""
return (
existing.side == body.side
and abs(existing.quantity - float(body.quantity)) < 1e-9
@@ -659,14 +666,21 @@ def record_ledger_trade_v1(
executed_at=body.executed_at,
fee_amount=float(body.fee_amount),
tax_amount=float(body.tax_amount),
execution_venue=body.execution_venue,
order_venue=body.order_venue,
)
except LedgerTradeConflictError:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="Duplicate key with different payload",
) from None
except HTTPException:
raise
finally:
_close_repo(repo)
if not created:
if not _payloads_match(trade, body):
if not _economic_payloads_match(trade, body):
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="Duplicate key with different payload",
@@ -687,6 +701,8 @@ def record_ledger_trade_v1(
fee_amount=trade.fee_amount,
tax_amount=trade.tax_amount,
executed_at=trade.executed_at,
execution_venue=trade.execution_venue,
order_venue=trade.order_venue,
recorded_at=trade.recorded_at,
cash_balance_after=trade.cash_balance_after,
asset_quantity_after=trade.asset_quantity_after,
@@ -827,6 +843,7 @@ def _ledger_entry_to_history_item(entry) -> LedgerHistoryItem | DividendLedgerHi
total_cost_basis=event["total_cost_basis"], cash_balance=event["cash_balance"],
profit_buffer=event["profit_buffer"], evidence_ref=event["evidence_ref"],
actor_user_id=event.get("actor_user_id"), recorded_at=entry.recorded_at,
execution_venue=entry.execution_venue, order_venue=entry.order_venue,
)
action = (entry.action or entry.side or "").upper()
if action == "DIVIDEND":
@@ -853,6 +870,8 @@ def _ledger_entry_to_history_item(entry) -> LedgerHistoryItem | DividendLedgerHi
total_value_after=entry.total_value_after,
target_ledger_entry_id=entry.target_ledger_entry_id,
confirmation_evidence=entry.confirmation_evidence,
execution_venue=entry.execution_venue,
order_venue=entry.order_venue,
)
return LedgerHistoryItem(
ledger_entry_id=entry.ledger_entry_id,
@@ -875,6 +894,8 @@ def _ledger_entry_to_history_item(entry) -> LedgerHistoryItem | DividendLedgerHi
total_value_after=entry.total_value_after,
target_ledger_entry_id=entry.target_ledger_entry_id,
confirmation_evidence=entry.confirmation_evidence,
execution_venue=entry.execution_venue,
order_venue=entry.order_venue,
)