import hashlib import json from pathlib import Path from types import SimpleNamespace def _record(tmp_path: Path, *, status="parsed", parsed=None): source = tmp_path / "plan.pdf" source.write_bytes(b"approved-test-pdf") digest = hashlib.sha256(source.read_bytes()).hexdigest() parsed = parsed or {"meta": {"status": status}} return SimpleNamespace( parse_status=status, parsed_data=json.dumps(parsed, ensure_ascii=False), confirmed_data=None, file_hash=digest, source_file_url=str(source), ) def _valid_data(): return { "plan_type": "savings", "product_name": "测试储蓄计划", "age": 35, "gender": "男", "currency": "HKD", "annual_premium": 0, "premium_term": 5, "surrender_value_10": 0, "benefit_table": [{"policy_year": 10, "total_surrender_value": 0, "source_page": 8}], "meta": {"provenance": {"annual_premium": {"source": "pdf_table", "page": 2}}}, } def test_confirmation_rejects_empty_data_and_wrong_status(tmp_path): from insurance.plan_data.validators import validate_case_confirmation record = _record(tmp_path, status="failed") result = validate_case_confirmation(record, {}) assert not result.valid assert {issue.code for issue in result.issues} >= {"PARSE_STATUS_INVALID", "PLAN_DATA_EMPTY"} def test_service_confirmation_cannot_be_bypassed_by_direct_request(tmp_path, monkeypatch): from insurance.poster import service as service_module record = _record(tmp_path) record.id = 7 record.user_id = "user-1" record.product_snapshot_json = "{}" fake_model = SimpleNamespace(query=SimpleNamespace(get=lambda _record_id: record)) monkeypatch.setattr(service_module, "PosterCaseUpload", fake_model) result = service_module.PosterService().confirm_case_upload( 7, "user-1", {"confirmedData": {}}, ) assert result["code"] == 4201 assert result["data"]["errorCode"] == "PLAN_DATA_EMPTY" def test_confirmation_preserves_zero_but_requires_positive_premium(tmp_path): from insurance.plan_data.validators import parse_snapshot_hash, validate_case_confirmation record = _record(tmp_path) data = _valid_data() result = validate_case_confirmation(record, { "confirmedData": data, "fileHash": record.file_hash, "parseSnapshotHash": parse_snapshot_hash(record), }) assert not result.valid assert any(issue.path == "annual_premium" and issue.code == "NUMBER_INVALID" for issue in result.issues) assert not any(issue.path == "surrender_value_10" and issue.code == "AMOUNT_INVALID" for issue in result.issues) def test_confirmation_and_generation_bind_file_parse_and_payload_hashes(tmp_path): from insurance.plan_data.validators import ( parse_snapshot_hash, validate_case_confirmation, validate_confirmed_case, ) record = _record(tmp_path) data = _valid_data() data["annual_premium"] = 10000 request = { "confirmedData": data, "fileHash": record.file_hash, "parseSnapshotHash": parse_snapshot_hash(record), } confirmed = validate_case_confirmation(record, request, {"productName": "测试储蓄计划"}) assert confirmed.valid record.confirmed_data = json.dumps(confirmed.plan_data, ensure_ascii=False) assert validate_confirmed_case(record, {"productName": "测试储蓄计划"}).valid tampered = json.loads(record.confirmed_data) tampered["annual_premium"] = 1 record.confirmed_data = json.dumps(tampered, ensure_ascii=False) result = validate_confirmed_case(record, {"productName": "测试储蓄计划"}) assert not result.valid assert any(issue.code == "CONFIRMED_DATA_CHANGED" for issue in result.issues) def test_product_mismatch_and_unresolved_conflict_fail_closed(tmp_path): from insurance.plan_data.validators import parse_snapshot_hash, validate_case_confirmation record = _record(tmp_path) data = _valid_data() data["annual_premium"] = 10000 data["meta"]["conflicts"] = [{"field": "surrender_value_10", "status": "unresolved"}] result = validate_case_confirmation(record, { "confirmedData": data, "fileHash": record.file_hash, "parseSnapshotHash": parse_snapshot_hash(record), }, {"productName": "另一产品"}) codes = {issue.code for issue in result.issues} assert "UNRESOLVED_CONFLICT" in codes assert "PRODUCT_MISMATCH" in codes def test_milestone_and_benefit_table_disagreement_cannot_be_confirmed(tmp_path): from insurance.plan_data.validators import parse_snapshot_hash, validate_case_confirmation record = _record(tmp_path) data = _valid_data() data["annual_premium"] = 10000 data["surrender_value_10"] = 120000 data["benefit_table"][0]["total_surrender_value"] = 100000 result = validate_case_confirmation(record, { "confirmedData": data, "fileHash": record.file_hash, "parseSnapshotHash": parse_snapshot_hash(record), }) assert not result.valid assert any( issue.code == "UNRESOLVED_CONFLICT" and issue.path == "surrender_value_10" for issue in result.issues ) def test_normalizer_keeps_unknown_distinct_from_zero_and_removes_wrong_fallbacks(): from insurance.ppt.normalizer import normalize_ci_plan, normalize_iul_plan, normalize_savings_plan savings = normalize_savings_plan({ "insured": {"age": 30}, "policy": {"currency": "HKD", "annual_premium": 0, "premium_payment_period": 5}, "benefit_illustration": [{"policy_year": 10, "total_surrender_value": 0}], }) assert savings["policy"]["annualPremium"] == 0 assert savings["policy"]["contractualTotalPremium"] is None assert savings["benefitRows"][0]["deathBenefit"] is None ci = normalize_ci_plan({ "insured": {"age": 30}, "policy": {"sum_insured": 500000}, "benefit_illustration": [{"policy_year": 1, "death_benefit": 500000}], }) assert ci["benefitRows"][0]["deathBenefit"] == 500000 assert ci["benefitRows"][0]["totalSurrenderValue"] is None iul = normalize_iul_plan({ "insured": {"age": 30}, "policy": {}, "benefit_illustration": [{"policy_year": 1, "account_value": 1000, "cash_value": 900}], }) assert iul["benefitRows"][0]["nonGuaranteedAccountValue"] is None assert iul["benefitRows"][0]["totalSurrenderValue"] is None def test_golden_manifest_is_explicitly_blocked_without_approved_samples(): import runpy root = Path(__file__).resolve().parents[1] script = root / "scripts/tools/evaluate_plan_goldens.py" evaluate = runpy.run_path(str(script))["evaluate"] report = evaluate( root / "tests/fixtures/plan_goldens/manifest.json", root / "tests/fixtures/plan_goldens/predictions", ) assert report.status == "blocked" assert report.sample_count == 0 assert report.minimum_samples == 10 def test_template_scope_requires_every_input_product_and_company(): from insurance.ppt.template_selection import template_scope_compatible template = { "applicableCompanyIds": ["company-a", "company-b"], "applicableProductIds": ["product-a", "product-b"], } assert template_scope_compatible( template, ["company-a", "company-b"], ["product-a", "product-b"], ) assert not template_scope_compatible( template, ["company-a", "company-c"], ["product-a", "product-b"], ) assert not template_scope_compatible( template, ["company-a"], ["product-a", "product-c"], )