Spaces:
Running
Running
| from __future__ import annotations | |
| from datetime import date | |
| from decimal import Decimal | |
| import pytest | |
| from doc_inspector.rules import ( | |
| aggregate_review, | |
| inspect_extraction, | |
| parse_amount, | |
| parse_document_date, | |
| run_amount_rules, | |
| run_date_rules, | |
| run_identity_rules, | |
| run_required_rules, | |
| validate_taiwan_citizen_id, | |
| ) | |
| from doc_inspector.schemas import ( | |
| ApplicationLineItem, | |
| DocumentPerson, | |
| LocatedIdType, | |
| LocatedValue, | |
| Receipt, | |
| ReceiptLineItem, | |
| RuleResult, | |
| SubsidyApplication, | |
| ) | |
| def located(value: str | None) -> LocatedValue: | |
| return LocatedValue(value=value, page_number=1 if value is not None else None) | |
| def valid_person(*, id_type: str = "citizen_id", id_number: str = "A123456789") -> DocumentPerson: | |
| return DocumentPerson( | |
| role=located("申請人"), | |
| name=located("測試者"), | |
| id_type=LocatedIdType(value=id_type, page_number=1), # type: ignore[arg-type] | |
| id_number=located(id_number), | |
| birth_date=located("民國80年1月2日"), | |
| ) | |
| def valid_subsidy() -> SubsidyApplication: | |
| return SubsidyApplication( | |
| program_name=located("測試補助"), | |
| application_date=located("2026-07-23"), | |
| applicants=[valid_person()], | |
| requested_amount=located("NT$3,000"), | |
| line_items=[ | |
| ApplicationLineItem(description=located("項目一"), amount=located("1,000")), | |
| ApplicationLineItem(description=located("項目二"), amount=located("2,000")), | |
| ], | |
| declared_total=located("3000.00"), | |
| ) | |
| def valid_receipt() -> Receipt: | |
| return Receipt( | |
| merchant_name=located("測試商店"), | |
| receipt_date=located("2026/07/23"), | |
| line_items=[ | |
| ReceiptLineItem( | |
| description=located("文件夾"), | |
| quantity=located("2"), | |
| unit_price=located("50"), | |
| line_total=located("100"), | |
| ) | |
| ], | |
| subtotal=located("100"), | |
| tax=located("5"), | |
| discount=located("0"), | |
| fees=located("0"), | |
| total=located("105"), | |
| ) | |
| def test_parse_document_date_accepts_supported_formats(raw: str, expected: date) -> None: | |
| assert parse_document_date(raw) == expected | |
| def test_parse_document_date_rejects_invalid_or_ambiguous_values(raw: str) -> None: | |
| with pytest.raises(ValueError): | |
| parse_document_date(raw) | |
| def test_birth_date_after_application_is_red() -> None: | |
| extraction = valid_subsidy() | |
| extraction.applicants[0].birth_date = located("2027-01-01") | |
| checks = run_date_rules(extraction) | |
| assert any(check.rule_id == "date.birth_before_application" and check.level == "red" for check in checks) | |
| def test_missing_date_dependency_is_yellow() -> None: | |
| extraction = valid_subsidy() | |
| extraction.application_date = LocatedValue() | |
| checks = run_date_rules(extraction) | |
| assert any(check.rule_id == "date.birth_before_application" and check.level == "yellow" for check in checks) | |
| def test_taiwan_citizen_id_normalizes_width_case_but_not_punctuation() -> None: | |
| assert validate_taiwan_citizen_id("a123456789") is True | |
| assert validate_taiwan_citizen_id("A123-456-789") is False | |
| assert validate_taiwan_citizen_id("A123456788") is False | |
| def test_declared_non_citizen_document_requires_manual_review() -> None: | |
| extraction = valid_subsidy() | |
| extraction.applicants[0] = valid_person(id_type="passport", id_number="P1234567") | |
| checks = run_identity_rules(extraction) | |
| assert checks[0].level == "yellow" | |
| assert checks[0].rule_id == "identity.manual_review" | |
| def test_unknown_citizen_shaped_id_uses_checksum() -> None: | |
| extraction = valid_subsidy() | |
| extraction.applicants[0] = valid_person(id_type="unknown", id_number="A123456789") | |
| assert run_identity_rules(extraction)[0].level == "green" | |
| def test_parse_amount_accepts_common_currency_formats(raw: str, expected: Decimal) -> None: | |
| assert parse_amount(raw) == expected | |
| def test_parse_amount_rejects_ambiguous_values(raw: str) -> None: | |
| with pytest.raises(ValueError): | |
| parse_amount(raw) | |
| def test_subsidy_amounts_allow_one_cent_tolerance() -> None: | |
| extraction = valid_subsidy() | |
| extraction.declared_total = located("3000.01") | |
| extraction.requested_amount = located("3000") | |
| checks = run_amount_rules(extraction) | |
| assert next(check for check in checks if check.rule_id == "amount.subsidy_line_total").level == "green" | |
| assert next(check for check in checks if check.rule_id == "amount.requested_vs_declared").level == "green" | |
| def test_receipt_inconsistent_total_is_red() -> None: | |
| extraction = valid_receipt() | |
| extraction.total = located("999") | |
| checks = run_amount_rules(extraction) | |
| assert next(check for check in checks if check.rule_id == "amount.receipt_total").level == "red" | |
| def test_receipt_missing_calculation_dependency_is_yellow() -> None: | |
| extraction = valid_receipt() | |
| extraction.subtotal = LocatedValue() | |
| checks = run_amount_rules(extraction) | |
| assert next(check for check in checks if check.rule_id == "amount.receipt_subtotal").level == "yellow" | |
| assert next(check for check in checks if check.rule_id == "amount.receipt_total").level == "yellow" | |
| def test_required_rules_detect_whitespace_and_empty_lists() -> None: | |
| extraction = SubsidyApplication(program_name=located(" ")) | |
| checks = run_required_rules(extraction) | |
| assert any(check.field_paths == ["program_name"] and check.level == "red" for check in checks) | |
| assert any(check.field_paths == ["applicants"] and check.level == "red" for check in checks) | |
| assert any(check.field_paths == ["line_items"] and check.level == "red" for check in checks) | |
| def test_aggregate_review_uses_strictest_level() -> None: | |
| checks = [ | |
| RuleResult(rule_id="g", level="green", message="green"), | |
| RuleResult(rule_id="y", level="yellow", message="yellow"), | |
| RuleResult(rule_id="r", level="red", message="red"), | |
| ] | |
| report = aggregate_review(checks) | |
| assert report.overall_level == "red" | |
| assert report.message == "檢核完成:紅 1、黃 1、綠 1。" | |
| def test_complete_valid_presets_are_green() -> None: | |
| assert inspect_extraction(valid_subsidy()).overall_level == "green" | |
| assert inspect_extraction(valid_receipt()).overall_level == "green" | |