Bhuvi13's picture
Upload 20 files
6f0c329 verified
Raw
History Blame Contribute Delete
11.9 kB
"""
Invoice validation orchestrator.
Loads platform_configs.json once, runs all seven validators in SDD order,
wires outputs to downstream inputs, and returns a single consolidated result.
No business logic lives here β€” all decisions remain inside the validator modules.
"""
import importlib.util
import json
import os
# ---------------------------------------------------------------------------
# Module loader β€” handles filenames that contain spaces or hyphens
# ---------------------------------------------------------------------------
def _load_fn(filename, fn_name):
"""Import a function from a .py file by path."""
here = os.path.dirname(os.path.abspath(__file__))
path = os.path.join(here, filename)
spec = importlib.util.spec_from_file_location(filename, path)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return getattr(mod, fn_name)
_run_vendor_validation = _load_fn("Vendor Validation.py", "run_vendor_validation")
_run_tax_id_validation = _load_fn("Tax - ID validation.py", "run_tax_id_validation")
_run_bank_validation = _load_fn("Bank validation.py", "run_bank_validation")
_run_date_sanity_checks = _load_fn("date_sanity.py", "run_date_sanity_checks")
_run_payment_terms_validation = _load_fn("payment terms valdiation.py", "run_payment_terms_validation")
_run_tax_amount_validation = _load_fn("tax validation.py", "run_tax_amount_validation")
_run_completeness_validation = _load_fn("completeness.py", "run_invoice_validation")
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
class _FieldProxy:
"""
Overlays computed attributes onto an existing extracted_fields object so
downstream validators can read Tax-ID results via the normal
getattr(extracted_fields, ...) pattern without mutating the original object.
Only non-None values are overlaid; None values fall through to the base
object (and its getattr default), which preserves the lenient defaults each
validator defines for fields it cannot always receive.
"""
__slots__ = ("_base", "_overlay")
def __init__(self, base, **overlay):
object.__setattr__(self, "_base", base)
object.__setattr__(self, "_overlay", {k: v for k, v in overlay.items() if v is not None})
def __getattr__(self, name):
overlay = object.__getattribute__(self, "_overlay")
if name in overlay:
return overlay[name]
return getattr(object.__getattribute__(self, "_base"), name)
def _load_platform_configs(path=None):
if path is None:
here = os.path.dirname(os.path.abspath(__file__))
path = os.path.join(here, "platform_configs.json")
with open(path, encoding="utf-8") as fh:
return json.load(fh)
def _collect(result, key):
"""Safely pull a list from a module result dict (returns [] if absent)."""
return result.get(key) or []
# ---------------------------------------------------------------------------
# Pipeline
# ---------------------------------------------------------------------------
def run_invoice_validation_pipeline(
*,
document,
extracted_fields,
vendors,
vendor_bank_accounts,
vendor_tax_ids,
entity,
line_items,
purchase_orders,
contracts,
tax_master_rows,
currency_seed_map,
platform_configs=None,
route_contexts=None,
actor_id=None,
):
"""
Run all seven validation modules in SDD order and return a consolidated result.
Parameters
----------
document : Invoice document object (needs .id, .tenant_id, .entity_id).
extracted_fields : VLM-extracted fields object.
vendors : Iterable of vendor records used for vendor matching.
vendor_bank_accounts : Iterable of vendor bank account records.
vendor_tax_ids : Iterable of vendor tax-ID records.
entity : The routing entity object (needs .country_code, .vat_id, .region_code).
line_items : Iterable of line-item records.
purchase_orders : Iterable of purchase-order records.
contracts : Iterable of contract records.
tax_master_rows : Iterable of tax-master records.
currency_seed_map : Dict mapping ISO currency code β†’ row with minor_unit_value.
platform_configs : Pre-loaded config dict; loaded from platform_configs.json when None.
route_contexts : List of route-context strings forwarded to Vendor Validation.
actor_id : Actor identifier forwarded to Vendor Validation for audit.
Returns
-------
{
"risk_flags": [...], # aggregated, in execution order
"advisory_flags": [...], # aggregated, in execution order
"skipped_steps": [...], # aggregated, in execution order
"ops_alerts": [...], # operational alerts (e.g. tax_master_unconfigured)
"resolved_vendor_id": ...,
"module_results": {...}, # raw return dict from each module, keyed by name
}
"""
if platform_configs is None:
platform_configs = _load_platform_configs()
risk_flags = []
advisory_flags = []
skipped_steps = []
ops_alerts = []
module_results = {}
# ------------------------------------------------------------------
# Step 1 β€” Vendor Validation
# Runs first; produces resolved_vendor_id used by all downstream steps.
# ------------------------------------------------------------------
vendor_result = _run_vendor_validation(
document=document,
extracted_fields=extracted_fields,
vendors=vendors,
platform_configs=platform_configs,
route_contexts=route_contexts,
actor_id=actor_id,
)
module_results["vendor_validation"] = vendor_result
risk_flags.extend(_collect(vendor_result, "risk_flags"))
advisory_flags.extend(_collect(vendor_result, "advisory_flags"))
skipped_steps.extend(_collect(vendor_result, "skipped_steps"))
resolved_vendor_id = vendor_result.get("resolved_vendor_id")
# Resolve the vendor object so downstream modules receive it directly.
resolved_vendor = next(
(v for v in vendors if getattr(v, "id", None) == resolved_vendor_id),
None,
) if resolved_vendor_id else None
# ------------------------------------------------------------------
# Step 2 β€” Tax ID Validation
# Depends on resolved_vendor_id and the resolved vendor object.
# ------------------------------------------------------------------
tax_id_result = _run_tax_id_validation(
document=document,
extracted_fields=extracted_fields,
vendor=resolved_vendor,
vendor_tax_ids=vendor_tax_ids,
resolved_vendor_id=resolved_vendor_id,
platform_configs=platform_configs,
)
module_results["tax_id_validation"] = tax_id_result
risk_flags.extend(_collect(tax_id_result, "risk_flags"))
advisory_flags.extend(_collect(tax_id_result, "advisory_flags"))
skipped_steps.extend(_collect(tax_id_result, "skipped_steps"))
# Enrich extracted_fields with Tax-ID results so Tax Validation can read
# sender_tax_id_inferred_family / _format_valid / _checksum_valid via its
# normal getattr(extracted_fields, ...) pattern.
enriched_fields = _FieldProxy(
extracted_fields,
sender_tax_id_inferred_family=tax_id_result.get("invoice_tax_id_inferred_type"),
sender_tax_id_format_valid=tax_id_result.get("invoice_tax_id_format_valid"),
sender_tax_id_checksum_valid=tax_id_result.get("invoice_tax_id_checksum_valid"),
)
# ------------------------------------------------------------------
# Step 3 β€” Bank Validation
# Depends on resolved_vendor_id.
# ------------------------------------------------------------------
bank_result = _run_bank_validation(
document=document,
extracted_fields=extracted_fields,
vendor_bank_accounts=vendor_bank_accounts,
resolved_vendor_id=resolved_vendor_id,
platform_configs=platform_configs,
)
module_results["bank_validation"] = bank_result
risk_flags.extend(_collect(bank_result, "risk_flags"))
advisory_flags.extend(_collect(bank_result, "advisory_flags"))
skipped_steps.extend(_collect(bank_result, "skipped_steps"))
# ------------------------------------------------------------------
# Step 4 β€” Date Sanity Checks
# Independent of vendor resolution.
# ------------------------------------------------------------------
date_result = _run_date_sanity_checks(
document=document,
extracted_fields=extracted_fields,
platform_configs=platform_configs,
)
module_results["date_sanity"] = date_result
risk_flags.extend(_collect(date_result, "risk_flags"))
advisory_flags.extend(_collect(date_result, "advisory_flags"))
skipped_steps.extend(_collect(date_result, "skipped_steps"))
# ------------------------------------------------------------------
# Step 5 β€” Payment Terms Validation
# Depends on resolved_vendor_id and the resolved vendor object.
# ------------------------------------------------------------------
payment_terms_result = _run_payment_terms_validation(
document=document,
extracted_fields=extracted_fields,
vendor=resolved_vendor,
purchase_orders=purchase_orders,
contracts=contracts,
resolved_vendor_id=resolved_vendor_id,
platform_configs=platform_configs,
)
module_results["payment_terms_validation"] = payment_terms_result
risk_flags.extend(_collect(payment_terms_result, "risk_flags"))
advisory_flags.extend(_collect(payment_terms_result, "advisory_flags"))
skipped_steps.extend(_collect(payment_terms_result, "skipped_steps"))
# ------------------------------------------------------------------
# Step 6 β€” Tax Amount Validation
# Depends on resolved vendor object and enriched_fields from Step 2.
# ------------------------------------------------------------------
tax_result = _run_tax_amount_validation(
document=document,
extracted_fields=enriched_fields,
line_items=line_items,
vendor=resolved_vendor,
entity=entity,
tax_master_rows=tax_master_rows,
platform_configs=platform_configs,
)
module_results["tax_validation"] = tax_result
risk_flags.extend(_collect(tax_result, "risk_flags"))
advisory_flags.extend(_collect(tax_result, "advisory_flags"))
skipped_steps.extend(_collect(tax_result, "skipped_steps"))
ops_alerts.extend(_collect(tax_result, "ops_alerts"))
# ------------------------------------------------------------------
# Step 7 β€” Completeness / Math Cross-check
# Independent of vendor resolution.
# ------------------------------------------------------------------
completeness_result = _run_completeness_validation(
document=document,
extracted_fields=extracted_fields,
line_items=line_items,
currency_seed_map=currency_seed_map,
platform_configs=platform_configs,
)
module_results["completeness"] = completeness_result
risk_flags.extend(_collect(completeness_result, "risk_flags"))
advisory_flags.extend(_collect(completeness_result, "advisory_flags"))
skipped_steps.extend(_collect(completeness_result, "skipped_steps"))
return {
"risk_flags": risk_flags,
"advisory_flags": advisory_flags,
"skipped_steps": skipped_steps,
"ops_alerts": ops_alerts,
"resolved_vendor_id": resolved_vendor_id,
"module_results": module_results,
}