"""申购服务:风险匹配校验 + 余额扣减 + 持仓加仓(单事务原子)。""" 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_customer_profile import FinCustomerProfileRepo from repositories.fin_holdings import FinHoldingsRepo from repositories.fin_product import FinProductRepo from schemas.holdings import HoldingResp from schemas.purchase import PurchaseResp from utils.exceptions import ( ForbiddenError, NotFoundError, NotSuitableError, ParamError, ) _MONEY = Decimal("0.01") _SHARES = Decimal("0.0001") # 风险等级 → 序号。兼容两套口径:R1~R5 与 保守~激进(同一映射)。 _RISK_RANK = { "R1": 1, "R2": 2, "R3": 3, "R4": 4, "R5": 5, "保守": 1, "稳健": 2, "平衡": 3, "进取": 4, "激进": 5, } def _risk_rank(level: str | None) -> int | None: """客户画像 / 产品的风险等级统一转序号;未知返回 None。""" if not level: return None return _RISK_RANK.get(level.strip()) async def purchase( db: AsyncSession, user: SysUser, product_id: int, amount: Decimal ) -> PurchaseResp: """申购基金:校验通过后扣减余额并加仓,全程单事务。 - 仅客户可申购; - 产品须在售且净值非空; - 客户风险等级序号 >= 产品风险等级序号,否则 1005 拦截; - 余额不足拦截;扣款 + 加仓要么都成、要么都回滚。 """ if user.user_type != "CUSTOMER": raise ForbiddenError("仅客户账号可申购") amount = amount.quantize(_MONEY, rounding=ROUND_HALF_UP) product = await FinProductRepo(db).get(product_id) if product is None: raise NotFoundError("产品不存在") if product.status != "在售": raise ParamError("产品不在售") if product.nav is None: raise ParamError("产品暂无净值,无法申购") profile = await FinCustomerProfileRepo(db).get_by_customer_id(user.id) customer_rank = _risk_rank(profile.risk_level if profile else None) if customer_rank is None: raise NotSuitableError("客户无风险等级,无法申购") product_rank = _risk_rank(product.risk_level) if product_rank is None or customer_rank < product_rank: raise NotSuitableError() shares = (amount / product.nav).quantize(_SHARES, rounding=ROUND_HALF_UP) account_repo = FinAccountRepo(db) holdings_repo = FinHoldingsRepo(db) try: if not await account_repo.deduct_balance(user.id, amount): raise ParamError("可用余额不足") await holdings_repo.upsert(user.id, product_id, shares, amount) 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 PurchaseResp( 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, ), )