Spaces:
Sleeping
Sleeping
File size: 4,899 Bytes
10ec275 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 | """Step C: deterministic validation -- the trust-winning feature.
Running-balance reconciliation: for each consecutive pair of rows,
expected_balance[i] = balance[i-1] - debit[i] + credit[i]
If |expected - printed| > tolerance, both rows are flagged for review.
Also validates: dates parse + are monotonically non-decreasing; each row has
exactly one of debit/credit; a printed totals row (if any) matches the sum.
This converts "the AI might hallucinate" into "the math checks itself".
"""
TOLERANCE = 0.01
def detect_direction(transactions):
"""Detect the bank's balance sign convention from the first few rows.
Standard convention: balance[i] = balance[i-1] - debit + credit.
Some statements print the opposite. We test both on the first usable rows
and return +1 (standard) or -1 (inverted).
"""
standard_err = 0.0
inverted_err = 0.0
checked = 0
prev = None
for t in transactions:
bal = t.get("balance")
if bal is None:
prev = bal
continue
if prev is not None:
delta = (t.get("debit") or 0) - (t.get("credit") or 0)
standard_err += abs((prev - delta) - bal)
inverted_err += abs((prev + delta) - bal)
checked += 1
prev = bal
if checked >= 3:
break
return -1 if inverted_err < standard_err else 1
def reconcile(transactions):
"""Reconcile running balances. Mutates each txn with a 'flags' list.
Returns a dict:
{reconciled, total, direction, banner, all_flags}
where `total` counts rows that had a checkable printed balance.
"""
direction = detect_direction(transactions)
reconciled = 0
checkable = 0
prev_balance = None
for t in transactions:
t.setdefault("flags", [])
for i, t in enumerate(transactions):
bal = t.get("balance")
debit = t.get("debit") or 0
credit = t.get("credit") or 0
if prev_balance is not None and bal is not None:
checkable += 1
if direction == 1:
expected = prev_balance - debit + credit
else:
expected = prev_balance + debit - credit
if abs(expected - bal) > TOLERANCE:
msg = f"balance mismatch (expected {expected:.2f}, printed {bal:.2f})"
_flag(transactions, i, "balance", msg)
_flag(transactions, i - 1, "balance", "adjacent to balance mismatch")
else:
reconciled += 1
if bal is not None:
prev_balance = bal
# Structural checks (debit XOR credit; date present).
_structural_checks(transactions)
banner = _banner(reconciled, checkable)
all_flags = sum(1 for t in transactions if t.get("flags"))
return {
"reconciled": reconciled,
"total": checkable,
"direction": direction,
"banner": banner,
"flagged_rows": all_flags,
}
def _flag(transactions, idx, kind, msg):
if 0 <= idx < len(transactions):
flags = transactions[idx].setdefault("flags", [])
if msg not in flags:
flags.append(msg)
def _structural_checks(transactions):
prev_date = None
for i, t in enumerate(transactions):
debit = t.get("debit")
credit = t.get("credit")
has_debit = debit is not None and debit != 0
has_credit = credit is not None and credit != 0
if has_debit and has_credit:
_flag(transactions, i, "amount", "both debit and credit present")
if not has_debit and not has_credit:
_flag(transactions, i, "amount", "no debit or credit amount")
date = t.get("date")
if not date:
_flag(transactions, i, "date", "unparseable date")
elif prev_date and date < prev_date:
_flag(transactions, i, "date", f"date {date} precedes previous {prev_date}")
if date:
prev_date = date
def _banner(reconciled, total):
if total == 0:
return "ℹ️ No printed balances available to reconcile."
if reconciled == total:
return f"✅ {reconciled}/{total} rows reconciled against printed balances"
return (f"⚠️ {reconciled}/{total} rows reconciled — "
f"{total - reconciled} need review (flagged below)")
def flags_text(txn):
"""Compact ⚠ string for the review table."""
flags = txn.get("flags") or []
return ("⚠ " + "; ".join(flags)) if flags else ""
def summary_stats(transactions):
"""Totals for the summary chips."""
total_debit = round(sum(t.get("debit") or 0 for t in transactions), 2)
total_credit = round(sum(t.get("credit") or 0 for t in transactions), 2)
return {
"count": len(transactions),
"total_debit": total_debit,
"total_credit": total_credit,
"net": round(total_credit - total_debit, 2),
}
|