"""赎回服务:校验持仓 → 减仓 + 余额入账(单事务原子)。""" from __future__ import annotations from decimal import ROUND_HALF_UP, Decimal from sqlalchemy.ext.asyncio import AsyncSession from model.sys_user import SysUser from repositories.fin_account import FinAccountRepo from repositories.fin_holdings import FinHoldingsRepo from repositories.fin_product import FinProductRepo from schemas.holdings import HoldingResp from schemas.redeem import RedeemResp from utils.exceptions import ForbiddenError, NotFoundError, ParamError _MONEY = Decimal("0.01") _SHARES = Decimal("0.0001") async def redeem( db: AsyncSession, user: SysUser, product_id: int, shares: Decimal ) -> RedeemResp: """赎回基金:校验通过后减仓并按净值入账,全程单事务。 - 仅客户可赎回; - 产品须存在且净值非空; - 持仓须存在且份额 > 0,赎回份额不得超过持仓份额; - 减仓(归 0 时置'已清仓')+ 入账要么都成、要么都回滚。 """ 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("产品暂无净值,无法赎回") account_repo = FinAccountRepo(db) holdings_repo = FinHoldingsRepo(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("赎回金额过低") try: if not await holdings_repo.redeem(user.id, product_id, shares): raise ParamError("可赎回份额不足") await account_repo.credit_balance(user.id, credited) await db.commit() except Exception: await db.rollback() raise account = await account_repo.get_by_customer_id(user.id) holding = await holdings_repo.get_by_customer_product(user.id, product_id) return RedeemResp( balance=account.balance, holding=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, ), )