"""赎回服务:校验持仓 → 建单(待确认) → 风控检测 → 落账/挂起(单事务原子)。""" from __future__ import annotations from datetime import datetime from decimal import ROUND_HALF_UP, Decimal from sqlalchemy.ext.asyncio import AsyncSession from model.fin_risk_alert import FinRiskAlert from model.fin_transaction import FinTransaction from model.sys_user import SysUser from model.trade_order import TradeOrder from repositories.fin_account import FinAccountRepo from repositories.fin_holdings import FinHoldingsRepo from repositories.fin_product import FinProductRepo from repositories.trade_order import TradeOrderRepo from schemas.holdings import HoldingResp from schemas.redeem import RedeemResp from service.risk.engine import RiskEngine, summarize from service.risk.settle import settle from utils.exceptions import ForbiddenError, NotFoundError, ParamError from utils.order_no import gen_order_no _MONEY = Decimal("0.01") _SHARES = Decimal("0.0001") def _holding_resp(holding) -> HoldingResp: return HoldingResp( id=holding.id, customer_id=holding.customer_id, product_id=holding.product_id, shares=holding.shares, cost_amount=holding.cost_amount, current_value=holding.current_value, profit_loss=holding.profit_loss, profit_ratio=holding.profit_ratio, status=holding.status, ) async def redeem( db: AsyncSession, user: SysUser, product_id: int, shares: Decimal ) -> RedeemResp: """赎回基金:校验持仓 → 建单 → 风控检测 → 落账/挂起,全程单事务。 - 仅客户可赎回; - 产品须存在且净值非空,持仓须足额; - 未命中规则:减仓 + 入账 + 写流水,订单「已确认」; - 命中规则:生成预警,订单「风控挂起」,账户与持仓不变。 """ if user.user_type != "CUSTOMER": raise ForbiddenError("仅客户账号可赎回") shares = shares.quantize(_SHARES, rounding=ROUND_HALF_UP) if shares <= 0: raise ParamError("赎回份额须大于 0") product = await FinProductRepo(db).get(product_id) if product is None: raise NotFoundError("产品不存在") if product.nav is None: raise ParamError("产品暂无净值,无法赎回") holdings_repo = FinHoldingsRepo(db) account_repo = FinAccountRepo(db) holding = await holdings_repo.get_by_customer_product(user.id, product_id) if holding is None or holding.shares <= 0: raise ParamError("无可赎回份额") if holding.shares < shares: raise ParamError("可赎回份额不足") credited = (shares * product.nav).quantize(_MONEY, rounding=ROUND_HALF_UP) if credited <= 0: raise ParamError("赎回金额过低") now = datetime.now() order = TradeOrder( order_no=gen_order_no("PO"), customer_id=user.id, product_id=product_id, advisor_id=None, order_type="赎回", amount=credited, shares=shares, nav=product.nav, fee=Decimal("0"), status="待确认", create_time=now, ) db.add(order) await db.flush() # 生成 order.id,供流水/预警关联 hits = await RiskEngine(db).detect(order, user) try: if not hits: # 未命中:减仓 + 入账 + 写流水 + 订单确认 await settle(db, order) db.add( FinTransaction( transaction_no=gen_order_no("TR"), order_id=order.id, customer_id=user.id, product_id=product_id, operator_id=None, transaction_type="赎回", amount=credited, shares=shares, nav=product.nav, fee=Decimal("0"), status="已确认", create_time=now, ) ) await TradeOrderRepo(db).update_status( order.id, status="已确认", confirm_time=now ) await db.commit() account = await account_repo.get_by_customer_id(user.id) current_holding = await holdings_repo.get_by_customer_product( user.id, product_id ) return RedeemResp( order_no=order.order_no, status="已确认", balance=account.balance, holding=_holding_resp(current_holding), ) # 命中:生成预警 + 订单挂起 summary = summarize(hits) alert = FinRiskAlert( customer_id=user.id, order_id=order.id, alert_type=summary.alert_type, alert_level=summary.alert_level, trigger_detail=summary.trigger_detail, transaction_ids=summary.transaction_ids or None, confidence=summary.confidence, status="未处理", create_time=now, ) db.add(alert) await db.flush() # 生成 alert.id,供订单回填 risk_alert_id await TradeOrderRepo(db).update_status( order.id, status="风控挂起", risk_alert_id=alert.id ) await db.commit() return RedeemResp( order_no=order.order_no, status="风控挂起", alert_id=alert.id ) except Exception: await db.rollback() raise