2025-08-05 22:25:51 +01:00
|
|
|
from dataclasses import dataclass
|
2025-08-07 09:06:05 +01:00
|
|
|
from typing import Any, Dict, List
|
|
|
|
|
|
2025-08-05 22:25:51 +01:00
|
|
|
from models import Receipt, Transaction
|
|
|
|
|
from tax_rules_engine import TaxRulesEngine
|
|
|
|
|
|
2025-08-07 09:06:05 +01:00
|
|
|
|
2025-08-05 22:25:51 +01:00
|
|
|
@dataclass
|
|
|
|
|
class AIRule:
|
|
|
|
|
name: str
|
|
|
|
|
condition: str
|
|
|
|
|
action: str
|
|
|
|
|
source: str
|
|
|
|
|
status: str = "active"
|
|
|
|
|
|
2025-08-07 09:06:05 +01:00
|
|
|
|
2025-08-05 22:25:51 +01:00
|
|
|
class AIRulesEngine:
|
|
|
|
|
def __init__(self):
|
|
|
|
|
self.rules: List[AIRule] = []
|
|
|
|
|
self.tax_rules_engine = TaxRulesEngine()
|
|
|
|
|
self._load_default_rules()
|
2025-08-07 09:06:05 +01:00
|
|
|
|
2025-08-05 22:25:51 +01:00
|
|
|
def _load_default_rules(self):
|
|
|
|
|
self.rules = [
|
2025-08-07 09:06:05 +01:00
|
|
|
AIRule(
|
|
|
|
|
"exact_amount_match", "amount_diff <= 0.01", "auto_approve", "system"
|
|
|
|
|
),
|
|
|
|
|
AIRule(
|
|
|
|
|
"same_vendor_same_date",
|
|
|
|
|
"vendor_match and date_diff <= 1",
|
|
|
|
|
"high_confidence",
|
|
|
|
|
"system",
|
|
|
|
|
),
|
|
|
|
|
AIRule(
|
|
|
|
|
"gas_station_pattern",
|
|
|
|
|
"vendor_contains_gas_or_fuel",
|
|
|
|
|
"categorize_transport",
|
|
|
|
|
"system",
|
|
|
|
|
),
|
2025-08-05 22:25:51 +01:00
|
|
|
# Tax-related rules
|
2025-08-07 09:06:05 +01:00
|
|
|
AIRule(
|
|
|
|
|
"fx_currency_mismatch",
|
|
|
|
|
"currency_mismatch",
|
|
|
|
|
"flag_fx_review",
|
|
|
|
|
"tax_system",
|
|
|
|
|
),
|
|
|
|
|
AIRule(
|
|
|
|
|
"meals_entertainment",
|
|
|
|
|
"is_meals_entertainment",
|
|
|
|
|
"apply_me_tax_rule",
|
|
|
|
|
"tax_system",
|
|
|
|
|
),
|
|
|
|
|
AIRule(
|
|
|
|
|
"provincial_tax_calculation",
|
|
|
|
|
"has_address_info",
|
|
|
|
|
"calculate_provincial_tax",
|
|
|
|
|
"tax_system",
|
|
|
|
|
),
|
2025-08-05 22:25:51 +01:00
|
|
|
]
|
2025-08-07 09:06:05 +01:00
|
|
|
|
2025-08-05 22:25:51 +01:00
|
|
|
def apply_rules(self, receipt: Receipt, transaction: Transaction) -> Dict[str, Any]:
|
2025-08-07 09:06:05 +01:00
|
|
|
results = {
|
|
|
|
|
"auto_approve": False,
|
|
|
|
|
"confidence_boost": 0,
|
|
|
|
|
"category": None,
|
|
|
|
|
"tax_analysis": {},
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-05 22:25:51 +01:00
|
|
|
for rule in self.rules:
|
|
|
|
|
if rule.status != "active":
|
|
|
|
|
continue
|
2025-08-07 09:06:05 +01:00
|
|
|
|
2025-08-05 22:25:51 +01:00
|
|
|
if self._evaluate_condition(rule.condition, receipt, transaction):
|
|
|
|
|
self._execute_action(rule.action, results, receipt, transaction)
|
2025-08-07 09:06:05 +01:00
|
|
|
|
2025-08-05 22:25:51 +01:00
|
|
|
return results
|
2025-08-07 09:06:05 +01:00
|
|
|
|
|
|
|
|
def _evaluate_condition(
|
|
|
|
|
self, condition: str, receipt: Receipt, transaction: Transaction
|
|
|
|
|
) -> bool:
|
2025-08-05 22:25:51 +01:00
|
|
|
"""Safely evaluate rule conditions without using eval()"""
|
|
|
|
|
amount_diff = abs(receipt.amount - abs(transaction.amount))
|
|
|
|
|
date_diff = abs((receipt.receipt_date - transaction.transaction_date).days)
|
2025-08-07 09:06:05 +01:00
|
|
|
vendor_match = (
|
|
|
|
|
receipt.vendor.lower() in transaction.vendor.lower()
|
|
|
|
|
or transaction.vendor.lower() in receipt.vendor.lower()
|
|
|
|
|
)
|
2025-08-05 22:25:51 +01:00
|
|
|
vendor_lower = receipt.vendor.lower()
|
2025-08-07 09:06:05 +01:00
|
|
|
vendor_contains_gas_or_fuel = "gas" in vendor_lower or "fuel" in vendor_lower
|
|
|
|
|
|
2025-08-05 22:25:51 +01:00
|
|
|
# Tax-related conditions
|
|
|
|
|
currency_mismatch = receipt.currency != transaction.currency
|
|
|
|
|
is_meals_entertainment = receipt.is_meals_entertainment
|
2025-08-07 09:06:05 +01:00
|
|
|
has_address_info = (
|
|
|
|
|
receipt.billing_address is not None or receipt.shipping_address is not None
|
|
|
|
|
)
|
|
|
|
|
|
2025-08-05 22:25:51 +01:00
|
|
|
# Handle specific condition types safely
|
|
|
|
|
if condition == "amount_diff <= 0.01":
|
|
|
|
|
return amount_diff <= 0.01
|
|
|
|
|
elif condition == "vendor_match and date_diff <= 1":
|
|
|
|
|
return vendor_match and date_diff <= 1
|
|
|
|
|
elif condition == "vendor_contains_gas_or_fuel":
|
|
|
|
|
return vendor_contains_gas_or_fuel
|
|
|
|
|
elif condition == "currency_mismatch":
|
|
|
|
|
return currency_mismatch
|
|
|
|
|
elif condition == "is_meals_entertainment":
|
|
|
|
|
return is_meals_entertainment
|
|
|
|
|
elif condition == "has_address_info":
|
|
|
|
|
return has_address_info
|
|
|
|
|
else:
|
|
|
|
|
# For any other conditions, try to evaluate them safely
|
|
|
|
|
try:
|
|
|
|
|
# Only allow safe operations
|
|
|
|
|
safe_globals = {
|
|
|
|
|
"amount_diff": amount_diff,
|
|
|
|
|
"date_diff": date_diff,
|
|
|
|
|
"vendor_match": vendor_match,
|
|
|
|
|
"vendor_contains_gas_or_fuel": vendor_contains_gas_or_fuel,
|
|
|
|
|
"currency_mismatch": currency_mismatch,
|
|
|
|
|
"is_meals_entertainment": is_meals_entertainment,
|
|
|
|
|
"has_address_info": has_address_info,
|
|
|
|
|
"receipt": receipt,
|
|
|
|
|
"transaction": transaction,
|
|
|
|
|
"abs": abs,
|
|
|
|
|
"len": len,
|
|
|
|
|
"min": min,
|
|
|
|
|
"max": max,
|
|
|
|
|
"sum": sum,
|
2025-08-07 09:06:05 +01:00
|
|
|
"round": round,
|
2025-08-05 22:25:51 +01:00
|
|
|
}
|
|
|
|
|
return eval(condition, safe_globals, {})
|
|
|
|
|
except (SyntaxError, NameError, TypeError) as e:
|
|
|
|
|
print(f"Warning: Invalid condition '{condition}': {e}")
|
|
|
|
|
return False
|
2025-08-07 09:06:05 +01:00
|
|
|
|
|
|
|
|
def _execute_action(
|
|
|
|
|
self,
|
|
|
|
|
action: str,
|
|
|
|
|
results: Dict[str, Any],
|
|
|
|
|
receipt: Receipt,
|
|
|
|
|
transaction: Transaction,
|
|
|
|
|
):
|
2025-08-05 22:25:51 +01:00
|
|
|
if action == "auto_approve":
|
|
|
|
|
results["auto_approve"] = True
|
|
|
|
|
elif action == "high_confidence":
|
|
|
|
|
results["confidence_boost"] += 0.2
|
|
|
|
|
elif action == "categorize_transport":
|
|
|
|
|
results["category"] = "Transportation"
|
|
|
|
|
elif action == "flag_fx_review":
|
|
|
|
|
# Apply FX rule and flag for review
|
|
|
|
|
fx_result = self.tax_rules_engine.apply_fx_rule(receipt, transaction)
|
|
|
|
|
results["tax_analysis"]["fx"] = fx_result
|
|
|
|
|
if fx_result.get("requires_manual_review", False):
|
|
|
|
|
results["confidence_boost"] -= 0.1 # Reduce confidence for FX issues
|
|
|
|
|
elif action == "apply_me_tax_rule":
|
|
|
|
|
# Apply meals & entertainment rule
|
|
|
|
|
me_result = self.tax_rules_engine.apply_meals_entertainment_rule(receipt)
|
|
|
|
|
results["tax_analysis"]["meals_entertainment"] = me_result
|
|
|
|
|
elif action == "calculate_provincial_tax":
|
|
|
|
|
# Calculate provincial tax
|
|
|
|
|
tax_result = self.tax_rules_engine.apply_sales_tax_rule(receipt)
|
|
|
|
|
results["tax_analysis"]["sales_tax"] = tax_result
|
2025-08-07 09:06:05 +01:00
|
|
|
|
2025-08-05 22:25:51 +01:00
|
|
|
def add_rule(self, rule: AIRule):
|
|
|
|
|
self.rules.append(rule)
|
2025-08-07 09:06:05 +01:00
|
|
|
|
2025-08-05 22:25:51 +01:00
|
|
|
def remove_rule(self, rule_name: str):
|
|
|
|
|
self.rules = [r for r in self.rules if r.name != rule_name]
|
2025-08-07 09:06:05 +01:00
|
|
|
|
|
|
|
|
def apply_tax_rules(
|
|
|
|
|
self, receipt: Receipt, transaction: Transaction = None
|
|
|
|
|
) -> Dict[str, Any]:
|
2025-08-05 22:25:51 +01:00
|
|
|
"""Apply all tax rules to a receipt/transaction pair"""
|
2025-08-07 09:06:05 +01:00
|
|
|
return self.tax_rules_engine.apply_all_tax_rules(receipt, transaction)
|