"""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, )