feat: complete advisor product suitability gates

This commit is contained in:
Windows
2026-09-11 14:09:23 +08:00
parent 281e76e4b1
commit 47a5ca498c
3 changed files with 137 additions and 11 deletions
@@ -80,6 +80,7 @@ async def test_authoritative_candidates_fail_closed_and_keep_r1_without_scale_li
],
[],
[],
[],
])
result = await AdvisorProductRepository(session).authoritative_tradable_products(
now, sales_institution="南方基金管理股份有限公司直销",
@@ -95,10 +96,52 @@ async def test_authoritative_candidates_fail_closed_and_keep_r1_without_scale_li
async def test_pending_governance_and_missing_evidence_are_excluded() -> None:
now = datetime(2026, 9, 11, 10, 0)
session = FakeSession([
[product(1)], [suitability(1)], [contract(1)], [], [1], [],
[product(1)], [suitability(1)], [contract(1)], [], [], [1], [],
])
result = await AdvisorProductRepository(session).authoritative_tradable_products(
now, sales_institution="南方基金管理股份有限公司直销",
)
assert result == []
assert "exchange_code IN" in str(session.statements[0])
def test_hard_suitability_filter_returns_exclusion_reasons() -> None:
from app.repository.advisor_product_repository import (
AdvisorProductRepository,
AuthoritativeProductCandidate,
ProductContractEvidence,
ProductSuitabilityEvidence,
)
def candidate(code: str, risk: str) -> AuthoritativeProductCandidate:
item = product(1)
item.product_code = code
return AuthoritativeProductCandidate(
product=item,
suitability=ProductSuitabilityEvidence(
risk_level=risk, sales_institution="南方基金管理股份有限公司直销",
source_url="https://example.test/risk.pdf", document_title="风险等级披露",
document_sha256="a" * 64, effective_from=date(2026, 1, 1),
),
contract=ProductContractEvidence(
fund_type="股票型ETF", investment_scope="场内基金", performance_benchmark=None,
risk_return_characteristics="净值波动", custodian_name=None,
management_fee_rate_pct=None, custodian_fee_rate_pct=None, inception_date=None,
source_url="https://example.test/contract.pdf", document_title="基金合同",
document_sha256="b" * 64, document_published_at=None,
),
)
selected, excluded = AdvisorProductRepository.hard_suitability_filter(
[candidate("R1", "R1"), candidate("R4", "R4")], 2
)
assert [item.product.product_code for item in selected] == ["R1"]
assert excluded[0]["reason_code"] == "RISK_LEVEL_MISMATCH"
def test_liquidity_requirement_is_failed_closed_without_metrics() -> None:
from app.repository.advisor_product_repository import AdvisorProductRepository
evidence = AdvisorProductRepository._liquidity_evidence(None, None, "within_7_days")
assert evidence is not None
assert evidence.status == "insufficient"