File size: 3,703 Bytes
af24ae8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
"""Business rule validation for extracted invoice fields."""

from __future__ import annotations

import re

from docflow.models import ExtractedInvoice, ValidationIssue, ValidationReport, ValidationSeverity


def _issue(field: str, severity: ValidationSeverity, message: str, rule_id: str) -> ValidationIssue:
    return ValidationIssue(field=field, severity=severity, message=message, rule_id=rule_id)


def validate_invoice(invoice: ExtractedInvoice) -> ValidationReport:
    issues: list[ValidationIssue] = []

    if not invoice.vendor_name:
        issues.append(_issue("vendor_name", ValidationSeverity.ERROR, "Vendor name is required.", "BR-001"))
    elif len(invoice.vendor_name) < 3:
        issues.append(_issue("vendor_name", ValidationSeverity.WARNING, "Vendor name seems too short.", "BR-001-W"))

    if not invoice.invoice_number:
        issues.append(_issue("invoice_number", ValidationSeverity.ERROR, "Invoice number is missing.", "BR-002"))

    if not invoice.total_amount or invoice.total_amount <= 0:
        issues.append(_issue("total_amount", ValidationSeverity.ERROR, "Total amount must be positive.", "BR-003"))

    if invoice.vendor_tax_id:
        if not re.fullmatch(r"\d{10,14}", invoice.vendor_tax_id):
            issues.append(
                _issue(
                    "vendor_tax_id",
                    ValidationSeverity.WARNING,
                    "Tax ID format may be invalid (expected 10–14 digits).",
                    "BR-004",
                )
            )
    else:
        issues.append(
            _issue(
                "vendor_tax_id",
                ValidationSeverity.WARNING,
                "Tax ID not detected — manual verification recommended.",
                "BR-004-W",
            )
        )

    if invoice.invoice_date_jalali:
        if not re.fullmatch(r"\d{4}/\d{1,2}/\d{1,2}", invoice.invoice_date_jalali):
            issues.append(
                _issue(
                    "invoice_date_jalali",
                    ValidationSeverity.WARNING,
                    "Jalali date format should be YYYY/MM/DD.",
                    "BR-005",
                )
            )
    else:
        issues.append(
            _issue(
                "invoice_date_jalali",
                ValidationSeverity.WARNING,
                "Invoice date not detected.",
                "BR-005-W",
            )
        )

    if invoice.subtotal and invoice.tax_amount and invoice.total_amount:
        expected = invoice.subtotal + invoice.tax_amount
        tolerance = max(invoice.total_amount * 0.02, 1000)
        if abs(expected - invoice.total_amount) > tolerance:
            issues.append(
                _issue(
                    "total_amount",
                    ValidationSeverity.WARNING,
                    f"Total ({invoice.total_amount:,.0f}) does not match subtotal + tax ({expected:,.0f}).",
                    "BR-006",
                )
            )

    if invoice.confidence < 0.6:
        issues.append(
            _issue(
                "confidence",
                ValidationSeverity.INFO,
                f"Low extraction confidence ({invoice.confidence:.0%}) — accountant review recommended.",
                "BR-007",
            )
        )

    errors = sum(1 for i in issues if i.severity == ValidationSeverity.ERROR)
    warnings = sum(1 for i in issues if i.severity == ValidationSeverity.WARNING)
    score = max(0.0, 1.0 - (errors * 0.25) - (warnings * 0.08))

    return ValidationReport(
        is_valid=errors == 0,
        score=round(score, 3),
        issues=issues,
    )