DEV Community

Subhendu Das
Subhendu Das

Posted on

GSTR-2B Reconciliation with Configurable Tolerances in GSTBot

The Problem: Exact Matching Breaks on Real Invoices

Indian SMBs reconciling purchase registers against GSTR-2B quickly discover that exact string matching fails on vendor names, GSTINs, and invoice numbers. A vendor appears as "ABC Traders" in your ERP but "A.B.C. Traders Pvt Ltd" on the portal. GSTINs carry trailing spaces. Invoice numbers differ by a prefix or a single digit. Traditional tools priced for enterprises handle this; tools priced for SMBs mostly skip reconciliation and jump straight to filing.

GSTBot's reconciliation engine addresses this with configurable tolerance matching instead of binary exact/non-exact logic.

How It Works: Tolerance-Based Matching Pipeline

The pipeline runs as a Celery task chain triggered after bulk invoice ingestion (PDF, photo, or Excel) and AI extraction via a layout-aware parser. Each extracted invoice becomes a PurchaseRecord row in PostgreSQL with fields: vendor_name, gstin, invoice_number, invoice_date, taxable_value, tax_rate, hsn_code, igst, cgst, sgst.

GSTR-2B data is fetched via the GSTN API (or uploaded as JSON) and normalized into a GSTR2BRecord table with the same schema.

Matching Algorithm

Instead of a single WHERE clause, the engine scores candidate pairs across five dimensions:

# services/reconciliation/matcher.py
from rapidfuzz import fuzz, process
from decimal import Decimal

DIMENSIONS = [
    ("gstin", 0.35, lambda a, b: 100 if a == b else 0),
    ("invoice_number", 0.25, lambda a, b: fuzz.ratio(a, b)),
    ("vendor_name", 0.20, lambda a, b: fuzz.token_set_ratio(a, b)),
    ("invoice_date", 0.10, lambda a, b: 100 if a == b else (50 if abs((a - b).days) <= 1 else 0)),
    ("taxable_value", 0.10, lambda a, b: 100 if abs(Decimal(a) - Decimal(b)) <= Decimal("0.50") else 0),
]

def score_pair(purchase: PurchaseRecord, gstr2b: GSTR2BRecord) -> float:
    total = 0.0
    for field, weight, scorer in DIMENSIONS:
        total += weight * scorer(getattr(purchase, field), getattr(gstr2b, field))
    return round(total, 2)
Enter fullscreen mode Exit fullscreen mode

Weights and thresholds are stored in a ReconciliationConfig model so finance teams can adjust them per client or per month without code changes:

# models/config.py
class ReconciliationConfig(Base):
    __tablename__ = "reconciliation_configs"
    id = Column(Integer, primary_key=True)
    client_id = Column(Integer, ForeignKey("clients.id"))
    gstin_weight = Column(Float, default=0.35)
    invoice_number_weight = Column(Float, default=0.25)
    vendor_name_weight = Column(Float, default=0.20)
    date_weight = Column(Float, default=0.10)
    value_weight = Column(Float, default=0.10)
    auto_match_threshold = Column(Float, default=85.0)   # auto-accept
    review_threshold = Column(Float, default=60.0)       # flag for review
    # below review_threshold → exception
Enter fullscreen mode Exit fullscreen mode

The Celery task reconcile_batch processes in chunks of 500 records, writes MatchResult rows (purchase_id, gstr2b_id, score, status), and publishes a Redis stream event for the React frontend to poll.

Exception Classification

Every non-matched or low-score record gets an Exception row with a classification enum and a recommended_action:

Classification Condition Recommended Action
GSTIN_MISMATCH GSTIN score < 100 but other fields high Verify GSTIN with vendor; update master
INVOICE_NUMBER_VARIANCE Invoice number fuzzy score 60–90 Accept if date + value match
VENDOR_NAME_DRIFT Token-set ratio 50–80 Map to existing vendor master
MISSING_IN_GSTR2B No candidate above review_threshold Follow up with supplier for upload
EXTRA_IN_GSTR2B GSTR-2B record unmatched Check if purchase omitted
RULE_37_REVERSAL Matched but supplier not filed GSTR-1 Flag for ITC reversal
RULE_42_43_REVERSAL Capital goods / input service mismatch Compute reversal per rule

Rule 37, 42, and 43 reversals are tracked in a separate ITCReversal table linked to the exception, with a computed reversal_amount and reversal_period.

What It Looks Like to Use

  1. Upload — Drag a folder of PDFs or an Excel template into the React/Vite dashboard. The FastAPI endpoint /api/v1/ingest/bulk streams files to Redis, OCR workers (Tesseract + layoutlm) extract fields, and the UI shows a progress bar per file.
  2. Review Extraction — A table renders each invoice with confidence badges (green ≥ 95 %, amber 80–94 %, red < 80 %). Editable cells let you correct vendor name, GSTIN, or HSN before reconciliation.
  3. Run Reconciliation — Click "Reconcile against GSTR-2B". The backend pulls the latest GSTR-2B JSON (cached for 24 h), spins the Celery chain, and the dashboard switches to a live-updating grid via Server-Sent Events.
  4. Tune Thresholds — A side drawer exposes the ReconciliationConfig sliders. Moving "Invoice Number Weight" from 0.25 to 0.30 instantly re-scores the current batch without re-fetching GSTR-2B.
  5. Resolve Exceptions — Each exception row shows the top 3 candidate matches with scores, a diff view (highlighted via diff-match-patch), and the recommended action button: "Accept Match", "Map Vendor", "Request Supplier Upload", or "Mark for Reversal".
  6. Export — One click generates the GSTR-1 B2B/B2CS/HSN sheets and GSTR-3B summary JSON, validated against the GSTN schema before download.

Why It Matters for ITC Reconciliation

Configurable tolerance matching turns a month-end fire drill into a repeatable, auditable process. Finance teams no longer maintain fragile VLOOKUP sheets; CAs get a documented trail of every accepted, mapped, or reversed invoice — ready for scrutiny under Section 16(2)(aa) and Rule 37/42/43.

Top comments (0)