Files
Mutual_Fund/service/purchase.py
T

100 lines
3.4 KiB
Python
Raw Normal View History

2026-09-10 23:21:30 +08:00
"""申购服务:风险匹配校验 + 余额扣减 + 持仓加仓(单事务原子)。"""
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,
),
)