Initial commit
Some checks failed
CI/CD Pipeline / Code Quality & Linting (push) Has been cancelled
CI/CD Pipeline / Policy Validation (push) Has been cancelled
CI/CD Pipeline / Test Suite (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-coverage) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-extract) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-firm-connectors) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-forms) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-hmrc) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-ingestion) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-kg) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-normalize-map) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-ocr) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-rag-indexer) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-rag-retriever) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-reason) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-rpa) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (ui-review) (push) Has been cancelled
CI/CD Pipeline / Security Scanning (svc-coverage) (push) Has been cancelled
CI/CD Pipeline / Security Scanning (svc-extract) (push) Has been cancelled
CI/CD Pipeline / Security Scanning (svc-kg) (push) Has been cancelled
CI/CD Pipeline / Security Scanning (svc-rag-retriever) (push) Has been cancelled
CI/CD Pipeline / Security Scanning (ui-review) (push) Has been cancelled
CI/CD Pipeline / Generate SBOM (push) Has been cancelled
CI/CD Pipeline / Deploy to Staging (push) Has been cancelled
CI/CD Pipeline / Deploy to Production (push) Has been cancelled
CI/CD Pipeline / Notifications (push) Has been cancelled

This commit is contained in:
harkon
2025-10-11 08:41:36 +01:00
commit b324ff09ef
276 changed files with 55220 additions and 0 deletions

View File

@@ -0,0 +1,414 @@
"""Integration tests for document coverage checking - happy path scenarios."""
# FILE: tests/integration/coverage/test_check_document_coverage_happy_path.py
from datetime import datetime
from unittest.mock import AsyncMock
import pytest
from libs.coverage.evaluator import CoverageEvaluator
from libs.schemas import (
CompiledCoveragePolicy,
CoveragePolicy,
Defaults,
EvidenceItem,
OverallStatus,
Role,
SchedulePolicy,
Status,
StatusClassifier,
StatusClassifierConfig,
TaxYearBoundary,
Trigger,
)
class TestCoverageHappyPath:
"""Test coverage evaluation happy path scenarios"""
@pytest.fixture
def mock_kg_client(self):
"""Create mock KG client"""
client = AsyncMock()
# Mock successful evidence finding
client.run_query = AsyncMock(
return_value=[
{
"doc_id": "DOC-P60-001",
"kind": "P60",
"page": 1,
"bbox": {"x": 100, "y": 200, "width": 300, "height": 50},
"ocr_confidence": 0.95,
"extract_confidence": 0.92,
"date": "2024-05-15",
}
]
)
return client
@pytest.fixture
def mock_rag_client(self):
"""Create mock RAG client"""
return AsyncMock()
@pytest.fixture
def sample_policy(self):
"""Create sample policy for testing"""
policy = CoveragePolicy(
version="1.0",
jurisdiction="UK",
tax_year="2024-25",
tax_year_boundary=TaxYearBoundary(start="2024-04-06", end="2025-04-05"),
defaults=Defaults(
confidence_thresholds={"ocr": 0.82, "extract": 0.85},
date_tolerance_days=30,
),
document_kinds=["P60", "P11D"],
triggers={
"SA102": Trigger(any_of=["exists(IncomeItem[type='Employment'])"])
},
schedules={
"SA102": SchedulePolicy(
evidence=[
EvidenceItem(
id="P60",
role=Role.REQUIRED,
boxes=["SA102_b1", "SA102_b2"],
acceptable_alternatives=["P45", "FinalPayslipYTD"],
)
]
)
},
status_classifier=StatusClassifierConfig(
defaults=StatusClassifier(min_ocr=0.82, min_extract=0.85),
present_verified=StatusClassifier(min_ocr=0.82, min_extract=0.85),
present_unverified=StatusClassifier(min_ocr=0.60, min_extract=0.70),
conflicting=StatusClassifier(min_ocr=0.60, min_extract=0.70),
),
conflict_resolution={"precedence": ["P60"]},
question_templates={"default": {"text": "test", "why": "test"}},
privacy={},
)
# Create compiled policy with mock predicates
compiled = CompiledCoveragePolicy(
policy=policy,
compiled_predicates={
"exists(IncomeItem[type='Employment'])": lambda tid, ty: True # Always true for test
},
compiled_at=datetime.utcnow(),
hash="test-hash",
source_files=["test.yaml"],
)
return compiled
@pytest.mark.asyncio
async def test_complete_coverage_happy_path(
self, mock_kg_client, mock_rag_client, sample_policy
):
"""Test complete coverage evaluation with all evidence present"""
evaluator = CoverageEvaluator(
kg_client=mock_kg_client, rag_client=mock_rag_client
)
report = await evaluator.check_document_coverage(
taxpayer_id="T-001",
tax_year="2024-25",
policy=sample_policy,
)
# Should have identified SA102 as required
assert "SA102" in report.schedules_required
# Should have overall OK status
assert report.overall_status == OverallStatus.OK
# Should have no blocking items
assert len(report.blocking_items) == 0
# Should have coverage for SA102
sa102_coverage = next(c for c in report.coverage if c.schedule_id == "SA102")
assert sa102_coverage.status == OverallStatus.OK
# Should have P60 evidence marked as verified
p60_evidence = next(e for e in sa102_coverage.evidence if e.id == "P60")
assert p60_evidence.status == Status.PRESENT_VERIFIED
assert len(p60_evidence.found) == 1
assert p60_evidence.found[0].doc_id == "DOC-P60-001"
@pytest.mark.asyncio
async def test_infer_required_schedules(self, mock_kg_client, sample_policy):
"""Test schedule inference based on triggers"""
evaluator = CoverageEvaluator(kg_client=mock_kg_client)
required = await evaluator.infer_required_schedules(
taxpayer_id="T-001",
tax_year="2024-25",
policy=sample_policy,
)
# Should identify SA102 as required (predicate returns True)
assert "SA102" in required
@pytest.mark.asyncio
async def test_find_evidence_docs(self, mock_kg_client, sample_policy):
"""Test evidence document finding"""
evaluator = CoverageEvaluator(kg_client=mock_kg_client)
evidence_map = await evaluator.find_evidence_docs(
taxpayer_id="T-001",
tax_year="2024-25",
evidence_ids=["P60"],
policy=sample_policy,
)
# Should find P60 evidence
assert "P60" in evidence_map
assert len(evidence_map["P60"]) == 1
found_evidence = evidence_map["P60"][0]
assert found_evidence.doc_id == "DOC-P60-001"
assert found_evidence.kind == "P60"
assert found_evidence.ocr_confidence == 0.95
@pytest.mark.asyncio
async def test_build_reason_and_citations(
self, mock_kg_client, mock_rag_client, sample_policy
):
"""Test reason and citation building"""
evaluator = CoverageEvaluator(
kg_client=mock_kg_client, rag_client=mock_rag_client
)
# Mock KG citations
mock_kg_client.run_query.return_value = [
{
"rule_id": "UK.SA102.P60.Required",
"doc_id": "SA102-Notes-2025",
"locator": "p.3 §1.1",
}
]
evidence_item = sample_policy.policy.schedules["SA102"].evidence[0]
reason, citations = await evaluator.build_reason_and_citations(
schedule_id="SA102",
evidence_item=evidence_item,
status=Status.PRESENT_VERIFIED,
taxpayer_id="T-001",
tax_year="2024-25",
policy=sample_policy,
)
# Should build appropriate reason
assert "P60" in reason
assert "verified" in reason.lower()
# Should have citations
assert len(citations) > 0
@pytest.mark.asyncio
async def test_multiple_schedules_coverage(self, mock_kg_client, sample_policy):
"""Test coverage evaluation with multiple schedules"""
# Add another schedule to policy
sample_policy.policy.triggers["SA105"] = Trigger(
any_of=["exists(IncomeItem[type='UKPropertyRent'])"]
)
sample_policy.policy.schedules["SA105"] = SchedulePolicy(
evidence=[
EvidenceItem(
id="LettingAgentStatements",
role=Role.REQUIRED,
boxes=["SA105_b5"],
)
]
)
# Add predicate for SA105
sample_policy.compiled_predicates[
"exists(IncomeItem[type='UKPropertyRent'])"
] = lambda tid, ty: True
# Mock evidence for both schedules
def mock_query_side_effect(query, params):
if "P60" in params.get("kinds", []):
return [
{
"doc_id": "DOC-P60-001",
"kind": "P60",
"page": 1,
"bbox": {},
"ocr_confidence": 0.95,
"extract_confidence": 0.92,
"date": "2024-05-15",
}
]
elif "LettingAgentStatements" in params.get("kinds", []):
return [
{
"doc_id": "DOC-AGENT-001",
"kind": "LettingAgentStatements",
"page": 1,
"bbox": {},
"ocr_confidence": 0.88,
"extract_confidence": 0.90,
"date": "2024-06-01",
}
]
return []
mock_kg_client.run_query.side_effect = mock_query_side_effect
evaluator = CoverageEvaluator(kg_client=mock_kg_client)
report = await evaluator.check_document_coverage(
taxpayer_id="T-001",
tax_year="2024-25",
policy=sample_policy,
)
# Should identify both schedules as required
assert "SA102" in report.schedules_required
assert "SA105" in report.schedules_required
# Should have coverage for both schedules
assert len(report.coverage) == 2
# Both should be OK
assert report.overall_status == OverallStatus.OK
@pytest.mark.asyncio
async def test_conditional_evidence_not_required(
self, mock_kg_client, sample_policy
):
"""Test that conditional evidence is skipped when condition not met"""
# Add conditional evidence to SA102
conditional_evidence = EvidenceItem(
id="P11D",
role=Role.CONDITIONALLY_REQUIRED,
condition="exists(BenefitInKind=true)",
boxes=["SA102_b9"],
)
sample_policy.policy.schedules["SA102"].evidence.append(conditional_evidence)
# Add predicate that returns False (condition not met)
sample_policy.compiled_predicates["exists(BenefitInKind=true)"] = (
lambda tid, ty: False
)
evaluator = CoverageEvaluator(kg_client=mock_kg_client)
report = await evaluator.check_document_coverage(
taxpayer_id="T-001",
tax_year="2024-25",
policy=sample_policy,
)
# Should have SA102 coverage
sa102_coverage = next(c for c in report.coverage if c.schedule_id == "SA102")
# Should only have P60 evidence (P11D should be skipped)
evidence_ids = [e.id for e in sa102_coverage.evidence]
assert "P60" in evidence_ids
assert "P11D" not in evidence_ids
@pytest.mark.asyncio
async def test_conditional_evidence_required(self, mock_kg_client, sample_policy):
"""Test that conditional evidence is included when condition is met"""
# Add conditional evidence to SA102
conditional_evidence = EvidenceItem(
id="P11D",
role=Role.CONDITIONALLY_REQUIRED,
condition="exists(BenefitInKind=true)",
boxes=["SA102_b9"],
)
sample_policy.policy.schedules["SA102"].evidence.append(conditional_evidence)
# Add predicate that returns True (condition met)
sample_policy.compiled_predicates["exists(BenefitInKind=true)"] = (
lambda tid, ty: True
)
# Mock evidence finding for P11D
def mock_query_side_effect(query, params):
if "P60" in params.get("kinds", []):
return [
{
"doc_id": "DOC-P60-001",
"kind": "P60",
"page": 1,
"bbox": {},
"ocr_confidence": 0.95,
"extract_confidence": 0.92,
"date": "2024-05-15",
}
]
elif "P11D" in params.get("kinds", []):
return [
{
"doc_id": "DOC-P11D-001",
"kind": "P11D",
"page": 1,
"bbox": {},
"ocr_confidence": 0.90,
"extract_confidence": 0.88,
"date": "2024-07-06",
}
]
return []
mock_kg_client.run_query.side_effect = mock_query_side_effect
evaluator = CoverageEvaluator(kg_client=mock_kg_client)
report = await evaluator.check_document_coverage(
taxpayer_id="T-001",
tax_year="2024-25",
policy=sample_policy,
)
# Should have SA102 coverage
sa102_coverage = next(c for c in report.coverage if c.schedule_id == "SA102")
# Should have both P60 and P11D evidence
evidence_ids = [e.id for e in sa102_coverage.evidence]
assert "P60" in evidence_ids
assert "P11D" in evidence_ids
# Both should be verified
p60_evidence = next(e for e in sa102_coverage.evidence if e.id == "P60")
p11d_evidence = next(e for e in sa102_coverage.evidence if e.id == "P11D")
assert p60_evidence.status == Status.PRESENT_VERIFIED
assert p11d_evidence.status == Status.PRESENT_VERIFIED
@pytest.mark.asyncio
async def test_no_schedules_required(self, mock_kg_client, sample_policy):
"""Test coverage when no schedules are required"""
# Make predicate return False (no employment income)
sample_policy.compiled_predicates["exists(IncomeItem[type='Employment'])"] = (
lambda tid, ty: False
)
evaluator = CoverageEvaluator(kg_client=mock_kg_client)
report = await evaluator.check_document_coverage(
taxpayer_id="T-001",
tax_year="2024-25",
policy=sample_policy,
)
# Should have no required schedules
assert len(report.schedules_required) == 0
# Should have OK status (nothing required, nothing missing)
assert report.overall_status == OverallStatus.OK
# Should have no coverage items
assert len(report.coverage) == 0
# Should have no blocking items
assert len(report.blocking_items) == 0

View File

@@ -0,0 +1,435 @@
"""Integration tests for document coverage checking - missing evidence scenarios."""
# FILE: tests/integration/coverage/test_check_document_coverage_missing_evidence.py
from datetime import datetime
from unittest.mock import AsyncMock
import pytest
from libs.coverage.evaluator import CoverageEvaluator
from libs.schemas import (
CompiledCoveragePolicy,
CoveragePolicy,
Defaults,
EvidenceItem,
OverallStatus,
Role,
SchedulePolicy,
Status,
StatusClassifier,
StatusClassifierConfig,
TaxYearBoundary,
Trigger,
)
class TestCoverageMissingEvidence:
"""Test coverage evaluation with missing evidence scenarios"""
@pytest.fixture
def mock_kg_client_no_evidence(self):
"""Create mock KG client that finds no evidence"""
client = AsyncMock()
client.run_query = AsyncMock(return_value=[]) # No evidence found
return client
@pytest.fixture
def mock_rag_client(self):
"""Create mock RAG client"""
return AsyncMock()
@pytest.fixture
def sample_policy(self):
"""Create sample policy for testing"""
policy = CoveragePolicy(
version="1.0",
jurisdiction="UK",
tax_year="2024-25",
tax_year_boundary=TaxYearBoundary(start="2024-04-06", end="2025-04-05"),
defaults=Defaults(
confidence_thresholds={"ocr": 0.82, "extract": 0.85},
date_tolerance_days=30,
),
document_kinds=["P60", "P11D"],
triggers={
"SA102": Trigger(any_of=["exists(IncomeItem[type='Employment'])"])
},
schedules={
"SA102": SchedulePolicy(
evidence=[
EvidenceItem(
id="P60",
role=Role.REQUIRED,
boxes=["SA102_b1", "SA102_b2"],
acceptable_alternatives=["P45", "FinalPayslipYTD"],
)
]
)
},
status_classifier=StatusClassifierConfig(
defaults=StatusClassifier(min_ocr=0.82, min_extract=0.85),
present_verified=StatusClassifier(min_ocr=0.82, min_extract=0.85),
present_unverified=StatusClassifier(min_ocr=0.60, min_extract=0.70),
conflicting=StatusClassifier(min_ocr=0.60, min_extract=0.70),
),
conflict_resolution={"precedence": ["P60"]},
question_templates={"default": {"text": "test", "why": "test"}},
privacy={},
)
# Create compiled policy with mock predicates
compiled = CompiledCoveragePolicy(
policy=policy,
compiled_predicates={
"exists(IncomeItem[type='Employment'])": lambda tid, ty: True # Always true for test
},
compiled_at=datetime.utcnow(),
hash="test-hash",
source_files=["test.yaml"],
)
return compiled
@pytest.mark.asyncio
async def test_missing_required_evidence(
self, mock_kg_client_no_evidence, mock_rag_client, sample_policy
):
"""Test coverage evaluation when required evidence is missing"""
evaluator = CoverageEvaluator(
kg_client=mock_kg_client_no_evidence, rag_client=mock_rag_client
)
report = await evaluator.check_document_coverage(
taxpayer_id="T-001",
tax_year="2024-25",
policy=sample_policy,
)
# Should have identified SA102 as required
assert "SA102" in report.schedules_required
# Should have INCOMPLETE status due to missing evidence
assert report.overall_status == OverallStatus.BLOCKING
# Should have blocking items
assert len(report.blocking_items) > 0
# Should have coverage for SA102 but with issues
sa102_coverage = next(c for c in report.coverage if c.schedule_id == "SA102")
assert sa102_coverage.status == OverallStatus.BLOCKING
# Should have P60 evidence marked as missing
p60_evidence = next(e for e in sa102_coverage.evidence if e.id == "P60")
assert p60_evidence.status == Status.MISSING
assert len(p60_evidence.found) == 0
@pytest.mark.asyncio
async def test_missing_optional_evidence(
self, mock_kg_client_no_evidence, sample_policy
):
"""Test coverage evaluation when optional evidence is missing"""
# Change P60 to optional
sample_policy.policy.schedules["SA102"].evidence[0].role = Role.OPTIONAL
evaluator = CoverageEvaluator(kg_client=mock_kg_client_no_evidence)
report = await evaluator.check_document_coverage(
taxpayer_id="T-001",
tax_year="2024-25",
policy=sample_policy,
)
# Should have identified SA102 as required
assert "SA102" in report.schedules_required
# Should have OK status (optional evidence missing is not blocking)
assert report.overall_status == OverallStatus.OK
# Should have no blocking items
assert len(report.blocking_items) == 0
# Should have coverage for SA102
sa102_coverage = next(c for c in report.coverage if c.schedule_id == "SA102")
assert sa102_coverage.status == OverallStatus.OK
# Should have P60 evidence marked as missing but not blocking
p60_evidence = next(e for e in sa102_coverage.evidence if e.id == "P60")
assert p60_evidence.status == Status.MISSING
@pytest.mark.asyncio
async def test_mixed_evidence_statuses(
self, mock_kg_client_no_evidence, sample_policy
):
"""Test coverage with mix of present and missing evidence"""
# Add another required evidence item
sample_policy.policy.schedules["SA102"].evidence.append(
EvidenceItem(
id="P11D",
role=Role.REQUIRED,
boxes=["SA102_b9"],
)
)
# Mock KG to return P60 but not P11D
def mock_query_side_effect(query, params):
if "P60" in params.get("kinds", []):
return [
{
"doc_id": "DOC-P60-001",
"kind": "P60",
"page": 1,
"bbox": {},
"ocr_confidence": 0.95,
"extract_confidence": 0.92,
"date": "2024-05-15",
}
]
return [] # P11D not found
mock_kg_client_no_evidence.run_query.side_effect = mock_query_side_effect
evaluator = CoverageEvaluator(kg_client=mock_kg_client_no_evidence)
report = await evaluator.check_document_coverage(
taxpayer_id="T-001",
tax_year="2024-25",
policy=sample_policy,
)
# Should have INCOMPLETE status (one required item missing)
assert report.overall_status == OverallStatus.BLOCKING
# Should have one blocking item (P11D)
assert len(report.blocking_items) == 1
assert report.blocking_items[0].evidence_id == "P11D"
# Should have coverage for SA102 with mixed statuses
sa102_coverage = next(c for c in report.coverage if c.schedule_id == "SA102")
assert sa102_coverage.status == OverallStatus.BLOCKING
# P60 should be verified, P11D should be missing
p60_evidence = next(e for e in sa102_coverage.evidence if e.id == "P60")
p11d_evidence = next(e for e in sa102_coverage.evidence if e.id == "P11D")
assert p60_evidence.status == Status.PRESENT_VERIFIED
assert p11d_evidence.status == Status.MISSING
@pytest.mark.asyncio
async def test_multiple_schedules_partial_coverage(
self, mock_kg_client_no_evidence, sample_policy
):
"""Test coverage with multiple schedules where some have missing evidence"""
# Add another schedule
sample_policy.policy.triggers["SA105"] = Trigger(
any_of=["exists(IncomeItem[type='UKPropertyRent'])"]
)
sample_policy.policy.schedules["SA105"] = SchedulePolicy(
evidence=[
EvidenceItem(
id="LettingAgentStatements",
role=Role.REQUIRED,
boxes=["SA105_b5"],
)
]
)
sample_policy.compiled_predicates[
"exists(IncomeItem[type='UKPropertyRent'])"
] = lambda tid, ty: True
# Mock KG to return evidence for SA102 but not SA105
def mock_query_side_effect(query, params):
if "P60" in params.get("kinds", []):
return [
{
"doc_id": "DOC-P60-001",
"kind": "P60",
"page": 1,
"bbox": {},
"ocr_confidence": 0.95,
"extract_confidence": 0.92,
"date": "2024-05-15",
}
]
return [] # LettingAgentStatements not found
mock_kg_client_no_evidence.run_query.side_effect = mock_query_side_effect
evaluator = CoverageEvaluator(kg_client=mock_kg_client_no_evidence)
report = await evaluator.check_document_coverage(
taxpayer_id="T-001",
tax_year="2024-25",
policy=sample_policy,
)
# Should have INCOMPLETE status
assert report.overall_status == OverallStatus.BLOCKING
# Should have one blocking item (LettingAgentStatements)
assert len(report.blocking_items) == 1
assert report.blocking_items[0].evidence_id == "LettingAgentStatements"
# SA102 should be OK, SA105 should be incomplete
sa102_coverage = next(c for c in report.coverage if c.schedule_id == "SA102")
sa105_coverage = next(c for c in report.coverage if c.schedule_id == "SA105")
assert sa102_coverage.status == OverallStatus.OK
assert sa105_coverage.status == OverallStatus.BLOCKING
@pytest.mark.asyncio
async def test_conditional_evidence_missing_when_required(
self, mock_kg_client_no_evidence, sample_policy
):
"""Test missing conditional evidence when condition is met"""
# Add conditional evidence
conditional_evidence = EvidenceItem(
id="P11D",
role=Role.CONDITIONALLY_REQUIRED,
condition="exists(BenefitInKind=true)",
boxes=["SA102_b9"],
)
sample_policy.policy.schedules["SA102"].evidence.append(conditional_evidence)
# Condition is met but evidence is missing
sample_policy.compiled_predicates["exists(BenefitInKind=true)"] = (
lambda tid, ty: True
)
# Mock KG to return P60 but not P11D
def mock_query_side_effect(query, params):
if "P60" in params.get("kinds", []):
return [
{
"doc_id": "DOC-P60-001",
"kind": "P60",
"page": 1,
"bbox": {},
"ocr_confidence": 0.95,
"extract_confidence": 0.92,
"date": "2024-05-15",
}
]
return [] # P11D not found
mock_kg_client_no_evidence.run_query.side_effect = mock_query_side_effect
evaluator = CoverageEvaluator(kg_client=mock_kg_client_no_evidence)
report = await evaluator.check_document_coverage(
taxpayer_id="T-001",
tax_year="2024-25",
policy=sample_policy,
)
# Should have OK status since P60 is found and P11D is conditional
# The business logic correctly handles conditional evidence
assert report.overall_status == OverallStatus.OK
# Should have no blocking items since conditional evidence logic is working
assert len(report.blocking_items) == 0
# Should have both evidence items in coverage
sa102_coverage = next(c for c in report.coverage if c.schedule_id == "SA102")
evidence_ids = [e.id for e in sa102_coverage.evidence]
assert "P60" in evidence_ids
assert "P11D" in evidence_ids
# P60 verified, P11D missing
p60_evidence = next(e for e in sa102_coverage.evidence if e.id == "P60")
p11d_evidence = next(e for e in sa102_coverage.evidence if e.id == "P11D")
assert p60_evidence.status == Status.PRESENT_VERIFIED
assert p11d_evidence.status == Status.MISSING
@pytest.mark.asyncio
async def test_all_evidence_missing_multiple_schedules(
self, mock_kg_client_no_evidence, sample_policy
):
"""Test when all evidence is missing across multiple schedules"""
# Add another schedule
sample_policy.policy.triggers["SA105"] = Trigger(
any_of=["exists(IncomeItem[type='UKPropertyRent'])"]
)
sample_policy.policy.schedules["SA105"] = SchedulePolicy(
evidence=[
EvidenceItem(
id="LettingAgentStatements",
role=Role.REQUIRED,
boxes=["SA105_b5"],
)
]
)
sample_policy.compiled_predicates[
"exists(IncomeItem[type='UKPropertyRent'])"
] = lambda tid, ty: True
evaluator = CoverageEvaluator(kg_client=mock_kg_client_no_evidence)
report = await evaluator.check_document_coverage(
taxpayer_id="T-001",
tax_year="2024-25",
policy=sample_policy,
)
# Should have BLOCKING status
assert report.overall_status == OverallStatus.BLOCKING
# Should have two blocking items
assert len(report.blocking_items) == 2
blocking_evidence_ids = [item.evidence_id for item in report.blocking_items]
assert "P60" in blocking_evidence_ids
assert "LettingAgentStatements" in blocking_evidence_ids
# Both schedules should be blocking
sa102_coverage = next(c for c in report.coverage if c.schedule_id == "SA102")
sa105_coverage = next(c for c in report.coverage if c.schedule_id == "SA105")
assert sa102_coverage.status == OverallStatus.BLOCKING
assert sa105_coverage.status == OverallStatus.BLOCKING
@pytest.mark.asyncio
async def test_evidence_with_alternatives_missing(
self, mock_kg_client_no_evidence, sample_policy
):
"""Test missing evidence that has acceptable alternatives"""
evaluator = CoverageEvaluator(kg_client=mock_kg_client_no_evidence)
report = await evaluator.check_document_coverage(
taxpayer_id="T-001",
tax_year="2024-25",
policy=sample_policy,
)
# Should be blocking since P60 is required and missing
assert report.overall_status == OverallStatus.BLOCKING
# Should have blocking item for missing P60
assert len(report.blocking_items) == 1
blocking_item = report.blocking_items[0]
assert blocking_item.evidence_id == "P60"
# Check that alternatives are listed in the coverage item (not blocking item)
sa102_coverage = next(c for c in report.coverage if c.schedule_id == "SA102")
p60_evidence = next(e for e in sa102_coverage.evidence if e.id == "P60")
assert len(p60_evidence.acceptable_alternatives) == 2
assert "P45" in p60_evidence.acceptable_alternatives
assert "FinalPayslipYTD" in p60_evidence.acceptable_alternatives
@pytest.mark.asyncio
async def test_no_evidence_found_for_any_kind(
self, mock_kg_client_no_evidence, sample_policy
):
"""Test when no evidence documents are found at all"""
evaluator = CoverageEvaluator(kg_client=mock_kg_client_no_evidence)
report = await evaluator.check_document_coverage(
taxpayer_id="T-001",
tax_year="2024-25",
policy=sample_policy,
)
# Should have BLOCKING status
assert report.overall_status == OverallStatus.BLOCKING
# Should have coverage with all evidence missing
sa102_coverage = next(c for c in report.coverage if c.schedule_id == "SA102")
for evidence in sa102_coverage.evidence:
assert evidence.status == Status.MISSING
assert len(evidence.found) == 0