Files
group_fqcd_jr/app/repository/risk_questionnaire_repository.py
T
张胜宇 e239eb778b docs: 品牌全量口径统一为「南方基金」+ 作废文档清理
1) 客服 Agent 四份交付文档 + 构建脚手架:品牌由包装占位 XX科技 / 旧名 南方财富
   统一为南方基金(热线 400-889-8899 / 官网 nffund.com),系统名改为「智能服务系统」;
   同步追加 §0.4 修订记录行,工程记录行保留原占位字面以支撑硬编码扫描验收。
2) 开发文档:清理 28 份已作废/残留文档(14 份移出归档 + 14 份仓库副本),
   新增《文档规整方案与开发前待决事项-2026-09-17》。
3) 客服agent 四份交付文档首次纳入本分支。
2026-09-17 15:15:22 +08:00

172 lines
6.5 KiB
Python

"""Repository for formal risk assessments and internal profile projections."""
from datetime import datetime
from typing import cast
from sqlalchemy import func, select, update
from sqlalchemy.ext.asyncio import AsyncSession
from app.model.fund import FundRiskAssessment as RiskAssessment
from app.model.memory import MemorySyncOutbox
from app.model.profile_tag import AdvisorProfileDriftReview, AdvisorProfileTag
from app.model.risk_questionnaire import ProfileSnapshot
class RiskQuestionnaireRepository:
def __init__(self, session: AsyncSession) -> None:
self.session = session
async def has_valid_assessment(self, customer_id: int, now: datetime) -> bool:
return await self.session.scalar(
select(RiskAssessment.id)
.where(RiskAssessment.customer_id == customer_id, RiskAssessment.valid_until > now)
.limit(1)
) is not None
async def assessments_since(self, customer_id: int, since: datetime) -> int:
value = await self.session.scalar(
select(func.count())
.select_from(RiskAssessment)
.where(RiskAssessment.customer_id == customer_id, RiskAssessment.assessed_at >= since)
)
return int(value or 0)
async def next_profile_version(self, customer_id: int) -> int:
value = await self.session.scalar(
select(func.coalesce(func.max(ProfileSnapshot.version), 0)).where(
ProfileSnapshot.customer_id == customer_id
)
)
return int(value or 0) + 1
async def deactivate_current_profile(self, customer_id: int, now: datetime) -> None:
await self.session.execute(
update(ProfileSnapshot)
.where(ProfileSnapshot.customer_id == customer_id, ProfileSnapshot.is_current.is_(True))
.values(is_current=False, updated_at=now)
)
def add_assessment(self, assessment: RiskAssessment) -> None:
self.session.add(assessment)
def add_profile(self, profile: ProfileSnapshot) -> None:
self.session.add(profile)
def add_sync_event(self, event: MemorySyncOutbox) -> None:
self.session.add(event)
async def latest_assessment(self, customer_id: int) -> RiskAssessment | None:
return cast(RiskAssessment | None, await self.session.scalar(
select(RiskAssessment)
.where(RiskAssessment.customer_id == customer_id)
.order_by(RiskAssessment.assessed_at.desc(), RiskAssessment.id.desc())
.limit(1)
))
async def current_profile(self, customer_id: int) -> ProfileSnapshot | None:
return cast(ProfileSnapshot | None, await self.session.scalar(
select(ProfileSnapshot)
.where(
ProfileSnapshot.customer_id == customer_id,
ProfileSnapshot.is_current.is_(True),
)
.order_by(ProfileSnapshot.version.desc(), ProfileSnapshot.id.desc())
.limit(1)
))
async def active_tags(
self, customer_id: int, *, lock: bool = False
) -> list[AdvisorProfileTag]:
statement = (
select(AdvisorProfileTag)
.where(
AdvisorProfileTag.customer_id == customer_id,
AdvisorProfileTag.status == "active",
)
.order_by(AdvisorProfileTag.tag_key.asc(), AdvisorProfileTag.id.desc())
)
if lock:
statement = statement.with_for_update()
return list(await self.session.scalars(statement))
async def tags(self, customer_id: int, *, limit: int = 100) -> list[AdvisorProfileTag]:
return list(await self.session.scalars(
select(AdvisorProfileTag)
.where(AdvisorProfileTag.customer_id == customer_id)
.order_by(AdvisorProfileTag.tag_key.asc(), AdvisorProfileTag.id.desc())
.limit(limit)
))
async def pending_drift_review(
self, customer_id: int, *, lock: bool = False
) -> AdvisorProfileDriftReview | None:
statement = (
select(AdvisorProfileDriftReview)
.where(
AdvisorProfileDriftReview.customer_id == customer_id,
AdvisorProfileDriftReview.status == "pending_review",
)
.order_by(
AdvisorProfileDriftReview.created_at.desc(),
AdvisorProfileDriftReview.id.desc(),
)
.limit(1)
)
if lock:
statement = statement.with_for_update()
return cast(AdvisorProfileDriftReview | None, await self.session.scalar(statement))
async def drift_review(
self, review_id: int, *, lock: bool = False
) -> AdvisorProfileDriftReview | None:
statement = select(AdvisorProfileDriftReview).where(
AdvisorProfileDriftReview.id == review_id
)
if lock:
statement = statement.with_for_update()
return cast(AdvisorProfileDriftReview | None, await self.session.scalar(statement))
async def pending_reviews(self, *, limit: int) -> list[AdvisorProfileDriftReview]:
return list(await self.session.scalars(
select(AdvisorProfileDriftReview)
.where(AdvisorProfileDriftReview.status == "pending_review")
.order_by(
AdvisorProfileDriftReview.created_at.asc(),
AdvisorProfileDriftReview.id.asc(),
)
.limit(limit)
))
async def tags_for_review(
self, review_id: int, *, lock: bool = False
) -> list[AdvisorProfileTag]:
statement = (
select(AdvisorProfileTag)
.where(AdvisorProfileTag.drift_review_id == review_id)
.order_by(AdvisorProfileTag.tag_key.asc(), AdvisorProfileTag.id.asc())
)
if lock:
statement = statement.with_for_update()
return list(await self.session.scalars(statement))
async def supersede_active_tags(
self, customer_id: int, tag_keys: tuple[str, ...], now: datetime
) -> None:
if not tag_keys:
return
await self.session.execute(
update(AdvisorProfileTag)
.where(
AdvisorProfileTag.customer_id == customer_id,
AdvisorProfileTag.tag_key.in_(tag_keys),
AdvisorProfileTag.status == "active",
)
.values(status="superseded", active_customer_tag=None, updated_at=now)
)
def add_tag(self, tag: AdvisorProfileTag) -> None:
self.session.add(tag)
def add_drift_review(self, review: AdvisorProfileDriftReview) -> None:
self.session.add(review)