DEV Community

Subhendu Das
Subhendu Das

Posted on

GSTR-2B Reconciliation with Configurable Tolerances in GSTBot

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:

  1. Ingestion — PDFs, photos, or Excel files land in a Celery queue. A FastAPI worker calls pdfplumber for text extraction, pytesseract for OCR on images, and openpyxl for 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.

  2. 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.

  3. 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)
Enter fullscreen mode Exit fullscreen mode

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).

  1. 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.

  2. 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)