"""POST /v1/assets/{asset_id}/ledger/trades — idempotent ledger trade recording.""" from __future__ import annotations from decimal import Decimal from fastapi import APIRouter, Depends, HTTPException, Response, status from app.core.auth import AuthContext, require_trade_write from app.repositories.factory import get_portfolio_repository_from_settings from app.schemas.v1 import LedgerTradeRequest, LedgerTradeResponse router = APIRouter(prefix="/v1", tags=["v1"]) def _require_service_token(auth: AuthContext = Depends(require_trade_write)) -> AuthContext: if auth.token_source != "db" or auth.token_type != "service": raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Service token required", ) return auth def _payloads_match(existing, body: LedgerTradeRequest) -> bool: """Return True when the existing trade's payload matches the request body.""" return ( existing.side == body.side and abs(existing.quantity - float(body.quantity)) < 1e-9 and abs(existing.price - float(body.price)) < 1e-9 and abs(existing.fee_amount - float(body.fee_amount)) < 1e-9 and abs(existing.tax_amount - float(body.tax_amount)) < 1e-9 ) @router.post( "/assets/{asset_id}/ledger/trades", response_model=LedgerTradeResponse, status_code=status.HTTP_201_CREATED, ) def record_ledger_trade( asset_id: str, body: LedgerTradeRequest, response: Response, auth: AuthContext = Depends(_require_service_token), ) -> LedgerTradeResponse: from app.core.config import get_settings settings = get_settings() repo = get_portfolio_repository_from_settings(settings) try: # B-9: Validate asset_id (portfolio) existence try: repo.get(asset_id) except Exception: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Asset not found: {asset_id}", ) trade, created = repo.insert_ledger_trade( asset_id=asset_id, order_id=body.order_id, execution_id=body.execution_id, side=body.side, quantity=float(body.quantity), price=float(body.price), executed_at=body.executed_at, fee_amount=float(body.fee_amount), tax_amount=float(body.tax_amount), ) finally: if callable(getattr(type(repo), "close", None)): repo.close() if not created: # B-8: Check payload conflict → 409 if not _payloads_match(trade, body): raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail="Duplicate key with different payload", ) # B-7: Same payload duplicate → 200 + status="duplicate" response.status_code = status.HTTP_200_OK trade_status = "duplicate" else: trade_status = "new" return LedgerTradeResponse( ledger_entry_id=trade.ledger_entry_id, asset_id=trade.asset_id, order_id=trade.order_id, execution_id=trade.execution_id, side=trade.side, quantity=trade.quantity, price=trade.price, fee_amount=trade.fee_amount, tax_amount=trade.tax_amount, executed_at=trade.executed_at, recorded_at=trade.recorded_at, status=trade_status, )