81 lines
2.8 KiB
Python
81 lines
2.8 KiB
Python
"""赎回服务:校验持仓 → 减仓 + 余额入账(单事务原子)。"""
|
|
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,
|
|
),
|
|
)
|