Spaces:
Sleeping
Sleeping
| """ | |
| 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, | |
| } | |