Spaces:
Sleeping
Sleeping
| from decimal import Decimal, ROUND_HALF_UP | |
| from datetime import date, datetime | |
| def run_tax_amount_validation( | |
| *, | |
| document, | |
| extracted_fields, | |
| line_items, | |
| vendor, | |
| entity, | |
| tax_master_rows, | |
| platform_configs=None, | |
| ): | |
| """ | |
| Tax percentage and tax amount validation. | |
| Advisory checks: | |
| - tax rate exists in tax_master | |
| - tax amount matches taxable base | |
| - cross-country routing | |
| - US sales/use tax advisories | |
| - UK VAT advisories | |
| - India GST advisories | |
| - EU reverse charge | |
| - vendor withholding advisory | |
| - vendor tax exemption handling | |
| """ | |
| platform_configs = platform_configs or {} | |
| tax_rate_tolerance = Decimal( | |
| str(platform_configs.get("validation.tax_rate_match_tolerance", "0.0001")) | |
| ) | |
| currency_minor_units = platform_configs.get( | |
| "iso_currencies.minor_unit_value", | |
| { | |
| "USD": Decimal("0.01"), | |
| "GBP": Decimal("0.01"), | |
| "EUR": Decimal("0.01"), | |
| "INR": Decimal("0.01"), | |
| "JPY": Decimal("1"), | |
| "BHD": Decimal("0.001"), | |
| }, | |
| ) | |
| eu_member_states = set( | |
| platform_configs.get( | |
| "validation.eu_member_states", | |
| [ | |
| "AT", "BE", "BG", "HR", "CY", "CZ", "DK", "EE", "FI", "FR", | |
| "DE", "GR", "HU", "IE", "IT", "LV", "LT", "LU", "MT", "NL", | |
| "PL", "PT", "RO", "SK", "SI", "ES", "SE", | |
| ], | |
| ) | |
| ) | |
| def as_decimal(value): | |
| if value is None or value == "": | |
| return None | |
| try: | |
| return Decimal(str(value)) | |
| except Exception: | |
| return None | |
| def country(value): | |
| if not value: | |
| return None | |
| return str(value).upper() | |
| def is_zero(value): | |
| value = as_decimal(value) | |
| return value is not None and value == Decimal("0") | |
| def is_non_zero(value): | |
| value = as_decimal(value) | |
| return value is not None and value != Decimal("0") | |
| def invoice_date_value(): | |
| value = getattr(extracted_fields, "invoice_date", None) | |
| if isinstance(value, datetime): | |
| return value.date() | |
| if isinstance(value, date): | |
| return value | |
| return None # do not invent today's date | |
| def currency_tolerance(): | |
| invoice_currency = country(getattr(extracted_fields, "currency", None)) | |
| raw = currency_minor_units.get(invoice_currency) | |
| return Decimal(str(raw)) if raw is not None else None | |
| def add_flag(code, *, weight=None, detail=None): | |
| flag = {"code": code, "severity": "advisory"} | |
| if weight is not None: | |
| flag["weight"] = weight | |
| if detail is not None: | |
| flag["detail"] = detail | |
| risk_flags.append(flag) | |
| def add_advisory(code, *, detail=None): | |
| advisory = {"code": code, "severity": "advisory"} | |
| if detail is not None: | |
| advisory["detail"] = detail | |
| # avoid duplicates | |
| if not any(a["code"] == code for a in advisory_flags): | |
| advisory_flags.append(advisory) | |
| def add_skip(step_name, reason): | |
| skipped_steps.append({"step": step_name, "skip_reason": reason}) | |
| def effective_tax_master_rows(): | |
| invoice_date = invoice_date_value() | |
| rows = [] | |
| for row in tax_master_rows: | |
| if getattr(row, "tenant_id", None) != document.tenant_id: | |
| continue | |
| if not getattr(row, "is_active", True): | |
| continue | |
| if getattr(row, "deleted_at", None) is not None: | |
| continue | |
| # If invoice date is unavailable, do not invent one. | |
| # Keep only active rows, but skip date-sensitive matching later if needed. | |
| if invoice_date is not None: | |
| effective_from = getattr(row, "effective_from", None) | |
| effective_to = getattr(row, "effective_to", None) | |
| if effective_from is not None and effective_from > invoice_date: | |
| continue | |
| if effective_to is not None and invoice_date >= effective_to: | |
| continue | |
| rows.append(row) | |
| return rows | |
| def rate_matches(master_rate, invoice_rate): | |
| master_rate = as_decimal(master_rate) | |
| invoice_rate = as_decimal(invoice_rate) | |
| if master_rate is None or invoice_rate is None: | |
| return False | |
| return abs(master_rate - invoice_rate) <= tax_rate_tolerance | |
| def row_region_matches(row_region, hint_region): | |
| if row_region is None: | |
| return True | |
| return hint_region is not None and str(row_region).upper() == str(hint_region).upper() | |
| def find_tax_master_rate( | |
| *, | |
| rate, | |
| country_code, | |
| region_code=None, | |
| allowed_tax_types=None, | |
| reduced_rate_allowed=False, | |
| ): | |
| country_code = country(country_code) | |
| matches = [] | |
| for row in active_tax_master: | |
| if country(getattr(row, "country_code", None)) != country_code: | |
| continue | |
| if not row_region_matches(getattr(row, "region_code", None), region_code): | |
| continue | |
| if allowed_tax_types is not None and getattr(row, "tax_type", None) not in allowed_tax_types: | |
| continue | |
| if rate_matches(getattr(row, "tax_rate", None), rate): | |
| matches.append(row) | |
| continue | |
| elif reduced_rate_allowed: | |
| tax_name = str(getattr(row, "tax_name", "") or "").lower() | |
| if "reduced" in tax_name: | |
| matches.append(row) | |
| return matches | |
| def find_india_gst_aggregate_rate(rate, region_code, jurisdiction): | |
| """ | |
| Match invoice aggregate GST rate against the sum of applicable component rates: | |
| inter_state : IGST single-row match | |
| intra_state : CGST + SGST pair whose rates sum to the invoice rate | |
| None/unknown: try IGST, then CGST+SGST, then CGST+UTGST | |
| """ | |
| def in_rows(tax_type, rgn=None): | |
| return [ | |
| r for r in active_tax_master | |
| if country(getattr(r, "country_code", None)) == "IN" | |
| and getattr(r, "tax_type", None) == tax_type | |
| and row_region_matches(getattr(r, "region_code", None), rgn) | |
| ] | |
| def sum_pair(type_a, type_b, rgn=None): | |
| for a in in_rows(type_a, rgn): | |
| a_rate = as_decimal(getattr(a, "tax_rate", None)) | |
| if a_rate is None: | |
| continue | |
| for b in in_rows(type_b, rgn): | |
| b_rate = as_decimal(getattr(b, "tax_rate", None)) | |
| if b_rate is None: | |
| continue | |
| if abs((a_rate + b_rate) - rate) <= tax_rate_tolerance: | |
| return [a, b] | |
| return [] | |
| if jurisdiction == "inter_state": | |
| for r in in_rows("IGST", region_code): | |
| if rate_matches(getattr(r, "tax_rate", None), rate): | |
| return [r] | |
| return [] | |
| if jurisdiction == "intra_state": | |
| return sum_pair("CGST", "SGST", region_code) | |
| # Unknown jurisdiction: try all applicable component combinations. | |
| for r in in_rows("IGST"): | |
| if rate_matches(getattr(r, "tax_rate", None), rate): | |
| return [r] | |
| result = sum_pair("CGST", "SGST") | |
| if result: | |
| return result | |
| return sum_pair("CGST", "UTGST") | |
| def all_line_rates_zero(): | |
| for line in line_items: | |
| rate = as_decimal(getattr(line, "tax_rate_per_item", None)) | |
| if rate is None: | |
| continue | |
| if rate != Decimal("0"): | |
| return False | |
| return True | |
| def sender_tax_family(): | |
| return getattr(extracted_fields, "sender_tax_id_inferred_family", None) | |
| def sender_tax_valid_for_country(expected_family): | |
| return ( | |
| getattr(extracted_fields, "sender_tax_id", None) is not None | |
| and sender_tax_family() == expected_family | |
| and bool(getattr(extracted_fields, "sender_tax_id_format_valid", True)) | |
| and bool(getattr(extracted_fields, "sender_tax_id_checksum_valid", True)) | |
| ) | |
| def supplier_and_recipient_are_eu(): | |
| return supplier_country in eu_member_states and recipient_country in eu_member_states | |
| def eu_reverse_charge_conditions_met(): | |
| if not supplier_and_recipient_are_eu(): | |
| return False | |
| if supplier_country == recipient_country: | |
| return False | |
| if vendor_exemption_status == "reverse_charge": | |
| return True | |
| supplier_family = f"VAT-{supplier_country}" | |
| supplier_vat_valid = sender_tax_valid_for_country(supplier_family) | |
| recipient_vat_present = bool(getattr(entity, "vat_id", None)) | |
| return ( | |
| supplier_vat_valid | |
| and recipient_vat_present | |
| and all_line_rates_zero() | |
| and is_zero(getattr(extracted_fields, "tax_amount", None)) | |
| ) | |
| def india_jurisdiction(): | |
| supplier_state = getattr(vendor, "billing_state", None) | |
| recipient_state = getattr(entity, "region_code", None) | |
| if not supplier_state or not recipient_state: | |
| return None | |
| if str(supplier_state).upper() == str(recipient_state).upper(): | |
| return "intra_state" | |
| return "inter_state" | |
| risk_flags = [] | |
| advisory_flags = [] | |
| skipped_steps = [] | |
| ops_alerts = [] | |
| matched_tax_master_rows = {} | |
| active_tax_master = effective_tax_master_rows() | |
| supplier_country = country(getattr(vendor, "billing_country", None)) | |
| recipient_country = country(getattr(entity, "country_code", None)) | |
| invoice_currency = country(getattr(extracted_fields, "currency", None)) | |
| vendor_exemption_status = getattr(vendor, "exemption_status", None) | |
| invoice_date = invoice_date_value() | |
| if not active_tax_master: | |
| ops_alerts.append( | |
| { | |
| "code": "alert.tax_master_unconfigured", | |
| "tenant_id": document.tenant_id, | |
| } | |
| ) | |
| return { | |
| "risk_flags": risk_flags, | |
| "advisory_flags": advisory_flags, | |
| "skipped_steps": [ | |
| {"step": "all_tax_checks", "skip_reason": "tax_master_unconfigured"} | |
| ], | |
| "ops_alerts": ops_alerts, | |
| "matched_tax_master_rows": matched_tax_master_rows, | |
| } | |
| # Check 1 and Check 2 both skip if supplier country is missing. | |
| if not supplier_country: | |
| add_skip("rate_exists_in_master", "supplier_country_missing") | |
| add_skip("tax_amount_matches_taxable_base", "supplier_country_missing") | |
| else: | |
| unique_rates = set() | |
| # Gather unique (rate, region) pairs — deduplicated by value per SDD §8.1. | |
| for line in line_items: | |
| rate = as_decimal(getattr(line, "tax_rate_per_item", None)) | |
| if rate is not None and rate != Decimal("0"): | |
| unique_rates.add( | |
| ( | |
| rate, | |
| getattr(line, "region_code_hint", None), | |
| ) | |
| ) | |
| # Summary tax rate is checked separately | |
| summary_rate = as_decimal(getattr(extracted_fields, "tax_rate", None)) | |
| if summary_rate is not None and summary_rate != Decimal("0"): | |
| unique_rates.add((summary_rate, None)) | |
| for rate, region_code in unique_rates: | |
| # India aggregate rate matching is handled entirely in Check 6. | |
| if supplier_country == "IN": | |
| continue | |
| # US state tax needs a region hint | |
| if supplier_country == "US" and not region_code: | |
| add_advisory("us_region_unresolved") | |
| continue | |
| allowed_tax_types = None | |
| if supplier_country == "GB": | |
| allowed_tax_types = {"VAT"} | |
| reduced_rate_allowed = vendor_exemption_status == "reduced_rate" | |
| matches = find_tax_master_rate( | |
| rate=rate, | |
| country_code=supplier_country, | |
| region_code=region_code, | |
| allowed_tax_types=allowed_tax_types, | |
| reduced_rate_allowed=reduced_rate_allowed, | |
| ) | |
| if matches: | |
| matched_tax_master_rows[str(rate)] = [getattr(row, "id", None) for row in matches] | |
| else: | |
| add_flag( | |
| "tax_rate_not_in_master", | |
| weight=Decimal("0.35"), | |
| detail={ | |
| "rate": str(rate), | |
| "country": supplier_country, | |
| "region_code": region_code, | |
| }, | |
| ) | |
| # Check 2: Amount matches taxable base. | |
| if supplier_country: | |
| tolerance = currency_tolerance() | |
| if tolerance is None: | |
| add_skip("tax_amount_matches_taxable_base", "currency_minor_unit_unknown") | |
| else: | |
| for line in line_items: | |
| rate = as_decimal(getattr(line, "tax_rate_per_item", None)) | |
| amount = as_decimal(getattr(line, "amount", None)) | |
| actual_tax = as_decimal(getattr(line, "tax_amount_per_item", None)) | |
| if rate is None or amount is None or actual_tax is None: | |
| continue | |
| discount = as_decimal(getattr(line, "discount_amount_per_item", None)) or Decimal("0") | |
| taxable_base = amount - discount | |
| expected_tax = taxable_base * (rate / Decimal("100")) | |
| if abs(expected_tax - actual_tax) > tolerance: | |
| add_flag( | |
| "tax_amount_mismatch", | |
| weight=Decimal("0.35"), | |
| detail={ | |
| "line_number": getattr(line, "line_number", None), | |
| "expected_tax": str(expected_tax.quantize(tolerance, rounding=ROUND_HALF_UP)), | |
| "actual_tax": str(actual_tax), | |
| "currency": invoice_currency, | |
| }, | |
| ) | |
| # §8.9: vendor_tax_exempt advisory fires when all extracted line rates are zero. | |
| if vendor_exemption_status == "exempt" and all_line_rates_zero(): | |
| add_advisory("vendor_tax_exempt") | |
| # Check 3: Cross-country routing. | |
| summary_tax_amount = as_decimal(getattr(extracted_fields, "tax_amount", None)) | |
| if supplier_country is None or summary_tax_amount is None or summary_tax_amount == Decimal("0"): | |
| add_skip("cross_country_routing", "supplier_country_or_tax_amount_missing_or_zero") | |
| elif recipient_country is None: | |
| add_skip("cross_country_routing", "recipient_country_missing") | |
| elif supplier_country != recipient_country: | |
| add_flag( | |
| "cross_country_tax", | |
| weight=Decimal("0.30"), | |
| detail={ | |
| "supplier_country": supplier_country, | |
| "recipient_country": recipient_country, | |
| }, | |
| ) | |
| # Check 4: United States. | |
| if supplier_country == "US" or recipient_country == "US": | |
| if supplier_country == "US" and summary_tax_amount is not None and summary_tax_amount != Decimal("0"): | |
| add_flag( | |
| "us_sales_tax", | |
| weight=Decimal("0.20"), | |
| detail={"tax_amount": str(summary_tax_amount)}, | |
| ) | |
| if ( | |
| recipient_country == "US" | |
| and supplier_country != recipient_country | |
| and all_line_rates_zero() | |
| ): | |
| add_flag( | |
| "us_use_tax_possibly_owed", | |
| weight=Decimal("0.15"), | |
| ) | |
| # Check 5: United Kingdom. | |
| if supplier_country == "GB" or recipient_country == "GB": | |
| for line in line_items: | |
| rate = as_decimal(getattr(line, "tax_rate_per_item", None)) | |
| tax_amount = as_decimal(getattr(line, "tax_amount_per_item", None)) | |
| if rate == Decimal("0"): | |
| gb_matches = find_tax_master_rate( | |
| rate=rate, | |
| country_code="GB", | |
| region_code=getattr(line, "region_code_hint", None), | |
| allowed_tax_types={"zero_rated", "exempt", "VAT"}, | |
| reduced_rate_allowed=False, | |
| ) | |
| if any(getattr(row, "tax_type", None) == "zero_rated" for row in gb_matches): | |
| add_advisory("uk_zero_rated", detail={"line_number": getattr(line, "line_number", None)}) | |
| elif tax_amount == Decimal("0") and any( | |
| getattr(row, "tax_type", None) == "exempt" for row in gb_matches | |
| ): | |
| add_advisory("uk_exempt", detail={"line_number": getattr(line, "line_number", None)}) | |
| elif not ( | |
| vendor_exemption_status == "reverse_charge" | |
| and supplier_and_recipient_are_eu() | |
| and supplier_country != recipient_country | |
| ) and not ( | |
| vendor_exemption_status == "exempt" | |
| and all_line_rates_zero() | |
| ): | |
| add_flag( | |
| "tax_rate_not_in_master", | |
| weight=Decimal("0.35"), | |
| detail={ | |
| "rate": "0", | |
| "country": "GB", | |
| "source": getattr(line, "line_number", None), | |
| }, | |
| ) | |
| if ( | |
| supplier_country == "GB" | |
| and recipient_country == "GB" | |
| and summary_tax_amount is not None | |
| and summary_tax_amount != Decimal("0") | |
| and not sender_tax_valid_for_country("VAT-GB") | |
| ): | |
| add_flag( | |
| "missing_tax_id_for_local_tax", | |
| weight=Decimal("0.25"), | |
| detail={"expected_family": "VAT-GB"}, | |
| ) | |
| # Check 6: India GST. | |
| if supplier_country == "IN" or recipient_country == "IN": | |
| jurisdiction = india_jurisdiction() | |
| if jurisdiction is None: | |
| add_advisory("in_gst_jurisdiction_unresolved") | |
| else: | |
| add_advisory("in_gst_jurisdiction_inferred", detail={"jurisdiction": jurisdiction}) | |
| # Sender tax ID must look like GSTIN-IN or PAN-IN | |
| if sender_tax_family() not in {"GSTIN-IN", "PAN-IN"}: | |
| add_flag( | |
| "missing_tax_id_for_local_tax", | |
| weight=Decimal("0.25"), | |
| detail={"expected_family": "GSTIN-IN or PAN-IN"}, | |
| ) | |
| for line in line_items: | |
| rate = as_decimal(getattr(line, "tax_rate_per_item", None)) | |
| if rate is None or rate == Decimal("0"): | |
| continue | |
| region_code = None | |
| if jurisdiction == "intra_state": | |
| region_code = getattr(vendor, "billing_state", None) or getattr(entity, "region_code", None) | |
| matches = find_india_gst_aggregate_rate(rate, region_code, jurisdiction) | |
| if matches: | |
| matched_tax_master_rows[getattr(line, "line_number", None)] = [ | |
| getattr(r, "id", None) for r in matches | |
| ] | |
| else: | |
| add_flag( | |
| "tax_rate_not_in_master", | |
| weight=Decimal("0.35"), | |
| detail={ | |
| "rate": str(rate), | |
| "country": "IN", | |
| "region_code": region_code, | |
| "source": getattr(line, "line_number", None), | |
| }, | |
| ) | |
| # Check 7: EU reverse charge. | |
| # If conditions are met, add advisory and retract cross_country_tax raised by Check 3. | |
| if supplier_and_recipient_are_eu() and supplier_country != recipient_country: | |
| if eu_reverse_charge_conditions_met(): | |
| add_advisory("eu_reverse_charge") | |
| risk_flags[:] = [f for f in risk_flags if f["code"] != "cross_country_tax"] | |
| # Check 8: Withholding tax advisory. | |
| if vendor_exemption_status == "withholding_applicable": | |
| add_advisory("vendor_withholding_applicable") | |
| for line in line_items: | |
| tax_master_id = getattr(line, "tax_master_id", None) | |
| if not tax_master_id: | |
| continue | |
| for row in active_tax_master: | |
| if getattr(row, "id", None) == tax_master_id and getattr(row, "tax_type", None) == "withholding": | |
| add_advisory("vendor_withholding_applicable") | |
| break | |
| return { | |
| "risk_flags": risk_flags, | |
| "advisory_flags": advisory_flags, | |
| "skipped_steps": skipped_steps, | |
| "ops_alerts": ops_alerts, | |
| "matched_tax_master_rows": matched_tax_master_rows, | |
| } |