from __future__ import annotations import logging import time from typing import List, Tuple from models import AnomalyFlag, NormalisedInvoice, DeliveryCountMap, AgentTraceEntry from tracer import make_trace_entry logger = logging.getLogger(__name__) AGENT_NAME = "Reconciliation_Agent" AGENT_VERSION = "1.0.0" class ReconciliationAgent: def run( self, invoice: NormalisedInvoice, delivery_count_map: DeliveryCountMap, audit_run_id: str, ) -> Tuple[List[AnomalyFlag], List[str], AgentTraceEntry]: t_start = time.monotonic() flags: List[AnomalyFlag] = [] invoiced_product_ids: set[str] = set() for item in invoice.items: if item.product_id is None: continue invoiced_product_ids.add(item.product_id) delivered = delivery_count_map.get(item.product_id, 0) if item.quantity > delivered: shortage = item.quantity - delivered flags.append(AnomalyFlag( flag_type="delivery_shortage", product_id=item.product_id, product_name=item.product_normalized or item.product_raw, amount_inr=shortage * item.unit_price, description=( f"{item.product_normalized or item.product_raw}: " f"invoiced {item.quantity}, delivered {delivered}, short {shortage}" ), action_item=( f"Request delivery of {shortage:.0f} missing " f"{item.product_normalized or item.product_raw} " f"or credit note for ₹{shortage * item.unit_price:.2f}" ), metadata={ "invoice_quantity": item.quantity, "delivered_quantity": delivered, "shortage_quantity": shortage, "unit_price": item.unit_price, }, )) # Products in delivery photos but NOT on invoice unexpected = [ pid for pid in delivery_count_map if pid not in invoiced_product_ids ] t_end = time.monotonic() n_flags = len(flags) n_unexpected = len(unexpected) trace = make_trace_entry( agent_name=AGENT_NAME, agent_version=AGENT_VERSION, audit_run_id=audit_run_id, t_start=t_start, t_end=t_end, input_summary=( f"{len(invoice.items)} invoice items; " f"{len(delivery_count_map)} delivery entries" ), output_summary=f"{n_flags} shortage flags; {n_unexpected} unexpected deliveries", ) return flags, unexpected, trace