The Problem: Exact Matching Breaks at Scale
Indian SMBs reconciling purchase registers against GSTR-2B face a fundamental mismatch: supplier-uploaded invoices rarely match buyer records character-for-character. A vendor's GSTIN might appear as 29AAACA1234A1Z5 in GSTR-2B but 29AAACA1234A2Z5 in the purchase register (one digit off). Invoice numbers differ by prefixes (INV-001 vs 001), dates shift by a day, and HSN codes get truncated. Traditional tools priced for enterprises solve this with rule engines; tools priced for SMBs often skip reconciliation entirely and jump straight to filing.
How GSTBot Handles It
GSTBot's reconciliation engine treats matching as a scoring problem, not a boolean one. The pipeline:
Ingestion — PDFs, photos, or Excel files land in a Celery queue. A FastAPI worker calls
pdfplumberfor text extraction,pytesseractfor OCR on images, andopenpyxlfor spreadsheets. Extracted fields (vendor name, GSTIN, invoice number, date, taxable value, tax rate, HSN) are normalized — GSTINs uppercased, whitespace stripped, common OCR artifacts (O→0,I→1) corrected.Candidate Generation — For each purchase-register row, the engine pulls potential GSTR-2B matches within a configurable window: ±3 days on invoice date, same GSTIN prefix (first 10 chars), and amount within a tolerance band. This reduces the cross-join from O(n×m) to a manageable candidate set.
Weighted Scoring — Each candidate pair receives a composite score:
# gstbot/reconciliation/scoring.py
def score_pair(purchase: PurchaseRow, gstr2b: GSTR2BRow, cfg: ToleranceConfig) -> float:
score = 0.0
# GSTIN exact match = 40 pts, prefix match = 20 pts
if purchase.gstin == gstr2b.gstin:
score += 40
elif purchase.gstin[:10] == gstr2b.gstin[:10]:
score += 20
# Invoice number: Levenshtein similarity × 15
inv_sim = 1 - (levenshtein(purchase.inv_no, gstr2b.inv_no) / max(len(purchase.inv_no), len(gstr2b.inv_no)))
score += inv_sim * 15
# Date within tolerance
day_diff = abs((purchase.date - gstr2b.date).days)
if day_diff <= cfg.date_tolerance_days:
score += 15 * (1 - day_diff / cfg.date_tolerance_days)
# Amount within tolerance %
amt_diff_pct = abs(purchase.taxable - gstr2b.taxable) / max(purchase.taxable, 1)
if amt_diff_pct <= cfg.amount_tolerance_pct:
score += 20 * (1 - amt_diff_pct / cfg.amount_tolerance_pct)
# HSN exact = 10 pts
if purchase.hsn == gstr2b.hsn:
score += 10
return round(score, 2)
Default tolerances: date_tolerance_days=3, amount_tolerance_pct=0.02 (2%). Users override per supplier or globally via the React settings panel (Vite + TanStack Query).
Classification & Actions — Pairs ≥ 85 auto-match. Scores 60–84 become "Review" exceptions with a suggested action: "Accept match", "Split invoice", "Mark vendor GSTIN error", or "Escalate to CA". Below 60 → "Unmatched" with a "Create missing invoice" shortcut. Each exception row surfaces the top 3 scoring candidates with side-by-side field comparison.
Reversal Tracking — Matched invoices feed Rule 37 (ITC reversal for non-payment within 180 days), Rule 42 (ISD reversal), and Rule 43 (capital goods reversal) calculators. The engine stores the match timestamp and score, so reversal reports show why an invoice was considered matched.
What It Looks Like to Use
The reconciliation dashboard (/reconcile) loads a virtualized table (TanStack Virtual) showing 5,000+ rows without pagination lag. Columns: Status (badge: ✅ Matched, ⚠️ Review, ❌ Unmatched), Score, Vendor, GSTIN, Invoice No, Date, Taxable, Tax, HSN, Actions. Clicking a Review row opens a drawer with the three best GSTR-2B candidates, each field highlighted green/amber/red per match quality. One click "Accept" writes the link to PostgreSQL (reconciliation_links table) and updates the purchase register's gstr2b_matched_at timestamp.
Bulk actions: checkbox-select → "Accept all ≥ 90" or "Export exceptions to Excel for CA". The export includes the scoring breakdown so the CA sees the evidence trail.
Why It Matters for ITC Reconciliation
Configurable tolerances let a 50-invoice/month retailer use tight settings (1 day, 0.5%) while a 5,000-invoice wholesaler loosens to 5 days, 3% — without code changes. The scoring breakdown also feeds the supplier health score: vendors consistently scoring < 70 get flagged in the "Supplier Filing Health" view, informing the decision to follow up or switch suppliers before the next GSTR-2B cycle.
Built on FastAPI + Celery + Redis for async throughput, PostgreSQL for ACID guarantees on the link table, and React/Vite for a responsive review UI. No enterprise license required.
Top comments (0)