statementsetu / test_pipeline.py
perceptron01's picture
Upload 16 files
10ec275 verified
Raw
History Blame Contribute Delete
5.66 kB
"""End-to-end pipeline tests (pytest).
Deterministic only -- no model required. Vision-extraction accuracy is checked
manually against the scan fixture (see README).
"""
import json
import os
import sys
import pytest
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, ROOT)
SAMPLES = os.path.join(ROOT, "samples")
from categorize import categorize_rules, enforce_closed_set # noqa: E402
from constants import CATEGORY_SET, SUSPENSE # noqa: E402
from excel_export import write_excel # noqa: E402
from extraction import extract_from_pdf, parse_date # noqa: E402
from tally_export import generate_tally_xml # noqa: E402
from validate import reconcile # noqa: E402
@pytest.fixture(scope="module")
def truth():
with open(os.path.join(SAMPLES, "sample_digital_truth.json"), encoding="utf-8") as f:
return json.load(f)
# 1. Extraction (digital) ---------------------------------------------------- #
def test_extraction_digital_exact(truth):
txns, meta = extract_from_pdf(os.path.join(SAMPLES, "sample_digital.pdf"))
assert meta["path"] == "text-layer"
assert meta["gpu_used"] is False
assert len(txns) == len(truth) == 40
for got, want in zip(txns, truth):
assert got["date"] == want["date"]
assert got["debit"] == want["debit"]
assert got["credit"] == want["credit"]
assert got["balance"] == want["balance"]
assert got["narration"].strip() == want["narration"].strip()
# 2. Balance reconciliation -------------------------------------------------- #
def test_reconcile_clean(truth):
txns = [dict(t) for t in truth]
result = reconcile(txns)
assert result["reconciled"] == result["total"]
assert result["total"] == 39 # 40 rows, 39 consecutive pairs
assert "✅" in result["banner"]
def test_reconcile_flags_corrupted_row(truth):
txns = [dict(t) for t in truth]
txns[10]["balance"] = txns[10]["balance"] + 999.0 # corrupt one balance
result = reconcile(txns)
assert txns[10]["flags"], "corrupted row must be flagged"
# the mismatch is detected on the corrupted row and its neighbour
assert result["reconciled"] < result["total"]
# 3. Date parsing ------------------------------------------------------------ #
@pytest.mark.parametrize("raw", ["01/04/2026", "01-04-26", "1 Apr 2026",
"2026-04-01", "1-April-2026", "01.04.2026"])
def test_date_parsing(raw):
assert parse_date(raw) == "2026-04-01"
# 4. Categorization rules (deterministic) ------------------------------------ #
CANNED = [
("SALARY APR 2026 - STAFF PAYROLL", None, 50000.0, "Salary & Wages"),
("UPI/DR/123/SHARMA RENT/office rent", 35000.0, None, "Office Rent"),
("GST PMT-CBIC-GSTR3B", 18900.0, None, "GST Payment"),
("TDS PMT-CPC Q4", 9400.0, None, "TDS Payment"),
("INCOME TAX-ADVANCE TAX CBDT", 35000.0, None, "Income Tax Payment"),
("ATM CSH WDL/AXIS", 10000.0, None, "Cash Withdrawal"),
("CSH DEP/BRANCH CASH DEPOSIT", None, 50000.0, "Cash Deposit"),
("BSNL BROADBAND INTERNET BILL", 1499.0, None, "Telephone & Internet"),
("ELECTRICITY KESCO BILL", 8920.0, None, "Electricity & Utilities"),
("BANK CHRG-SMS CHGS", 23.6, None, "Bank Charges"),
("LIC OF INDIA PREMIUM POLICY", 9800.0, None, "Insurance"),
("HDFC HOME LOAN EMI", 28750.0, None, "Loan EMI"),
("INT CREDIT-FIXED DEPOSIT INTEREST", None, 4521.0, "Interest Received"),
("IRCTC RAIL TICKET TRAVEL", 3450.0, None, "Travel & Conveyance"),
("DRAWINGS-PROPRIETOR PERSONAL", 30000.0, None, "Drawings"),
]
@pytest.mark.parametrize("narration,debit,credit,expected", CANNED)
def test_categorization_rules(narration, debit, credit, expected):
txn = {"narration": narration, "debit": debit, "credit": credit}
result = categorize_rules(txn)
assert result is not None, f"no rule matched: {narration}"
assert result[0] == expected
# 5. Closed-set guarantee ---------------------------------------------------- #
def test_closed_set_rejects_invented_category():
cat, conf = enforce_closed_set("Crypto Trading", 0.99)
assert cat == SUSPENSE
cat, conf = enforce_closed_set("Office Rent", 0.3) # below floor
assert cat == SUSPENSE
cat, conf = enforce_closed_set("Office Rent", 0.9)
assert cat == "Office Rent"
assert cat in CATEGORY_SET
# 6. Tally XML --------------------------------------------------------------- #
def test_tally_xml_wellformed_and_balanced(truth):
from lxml import etree
txns = [dict(t, category="Suspense / Unclassified",
voucher_type=("Payment" if t["debit"] else "Receipt"))
for t in truth]
xml = generate_tally_xml(txns)
root = etree.fromstring(xml.encode("utf-8"))
vouchers = root.findall(".//VOUCHER")
assert len(vouchers) == len(truth)
for v in vouchers:
amounts = [float(a.text) for a in v.findall(".//AMOUNT")]
assert len(amounts) == 2
assert abs(sum(amounts)) < 0.01, "debit/credit must net to zero per voucher"
# 7. Excel ------------------------------------------------------------------- #
def test_excel_export(truth, tmp_path):
from openpyxl import load_workbook
txns = [dict(t, category="Suspense / Unclassified") for t in truth]
out = os.path.join(tmp_path, "out.xlsx")
write_excel(txns, out)
wb = load_workbook(out)
ws = wb.active
# header + 40 rows + totals row
assert ws.max_row == 1 + len(truth) + 1
total_debit = sum(t["debit"] or 0 for t in truth)
# totals row, Debit column (4)
assert abs(ws.cell(row=ws.max_row, column=4).value - round(total_debit, 2)) < 0.01