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)
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
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
-
Upload — Drag a folder of PDFs or an Excel template into the React/Vite dashboard. The FastAPI endpoint
/api/v1/ingest/bulkstreams files to Redis, OCR workers (Tesseract + layoutlm) extract fields, and the UI shows a progress bar per file. - 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.
- 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.
-
Tune Thresholds — A side drawer exposes the
ReconciliationConfigsliders. Moving "Invoice Number Weight" from 0.25 to 0.30 instantly re-scores the current batch without re-fetching GSTR-2B. -
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". - 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)