from __future__ import annotations import re from pathlib import Path import pandas as pd import pdfplumber from .base import BaseParser, StatementMeta, amount, extract_text, finalize, norm from .common import date_iso, find, infer_mode class RBLBankParser(BaseParser): bank_name = "RBL Bank" parser_name = "RBLBankParser" @classmethod def detect(cls, text: str) -> float: upper = (text or "").upper() score = 0.0 if "RBL BANK" in upper or "RBLBANK" in upper: score += 0.58 if "RATN000" in upper or "IFSC/RTGS/NEFT" in upper and "RATN" in upper: score += 0.20 if "STATEMENT OF TRANSACTIONS IN SAVINGS ACCOUNT" in upper: score += 0.12 if all(token in upper for token in ("TRANSACTION DETAILS", "VALUE DATE", "WITHDRAWAL AMT", "DEPOSIT AMT", "BALANCE AMT")): score += 0.10 return min(score, 0.99) @staticmethod def _signed_balance(value) -> float | None: text = norm(value).upper() parsed = amount(text) if parsed is None: return None return -abs(parsed) if text.endswith("DR") else abs(parsed) @staticmethod def _reference_number(narration: str, cheque_no: str) -> str: if norm(cheque_no): return norm(cheque_no) for pattern in ( r"\bUPI/([0-9]{10,18})\b", r"\bIMPS\s+([0-9]{10,18})\b", r"\b(?:NEFT|RTGS)/([A-Z0-9]{10,30})\b", r"\b([0-9]{12,18})-IMPS\b", ): match = re.search(pattern, narration, re.I) if match: return match.group(1) return "" def parse(self, path, text=None): pdf_path = Path(path) text = text or extract_text(pdf_path) meta = StatementMeta( bank_name=self.bank_name, source_file=pdf_path.name, parser_name=self.parser_name, confidence="High", ) meta.customer_name = norm(find(r"Accountholder Name\s*:?\s*([^\n]+)", text, flags=re.I)) meta.customer_id = find(r"CIF ID\s*:?\s*([0-9]+)", text, flags=re.I) meta.account_number = find(r"Statement Of Transactions In Savings Account No\.\s*([0-9]+)", text, flags=re.I) meta.ifsc = find(r"(?:IFSC/RTGS/NEFT|IFSC)\s*:?\s*([A-Z0-9]+)", text, flags=re.I) period = re.search(r"Period\s*:\s*(\d{2}-\d{2}-\d{4})\s+to\s+(\d{2}-\d{2}-\d{4})", text, re.I) if period: meta.period_from = date_iso(period.group(1)) meta.period_to = date_iso(period.group(2)) rows: list[dict] = [] printed_opening: float | None = None with pdfplumber.open(str(pdf_path)) as pdf: for page_no, page in enumerate(pdf.pages, start=1): for table in page.extract_tables() or []: if not table: continue header = [norm(cell).upper() for cell in table[0]] if not all(token in header for token in ("DATE", "TRANSACTION DETAILS", "VALUE DATE", "WITHDRAWAL AMT", "DEPOSIT AMT", "BALANCE AMT")): continue for raw in table[1:]: cells = list(raw) + [None] * (7 - len(raw)) txn_date, description, cheque_no, value_date, withdrawal, deposit, balance = cells[:7] txn_date = norm(txn_date) balance_value = self._signed_balance(balance) if not txn_date and balance_value is not None and printed_opening is None: printed_opening = balance_value continue if not re.fullmatch(r"\d{2}-[A-Za-z]{3}-\d{4}", txn_date): continue debit_value = amount(withdrawal) credit_value = amount(deposit) if debit_value is None and credit_value is None: continue narration = norm(description) rows.append({ "transaction_date": txn_date, "value_date": norm(value_date) or txn_date, "narration": narration, "reference_no": self._reference_number(narration, norm(cheque_no)), "debit": debit_value, "credit": credit_value, "balance": balance_value, "source_page": page_no, "mode": infer_mode(narration), }) data = pd.DataFrame(rows) if not data.empty: first = data.iloc[0] first_balance = float(first["balance"]) first_debit = float(first["debit"]) if pd.notna(first["debit"]) else 0.0 first_credit = float(first["credit"]) if pd.notna(first["credit"]) else 0.0 meta.opening_balance = round(printed_opening if printed_opening is not None else first_balance + first_debit - first_credit, 2) meta.total_debit = round(float(pd.to_numeric(data["debit"], errors="coerce").fillna(0).sum()), 2) meta.total_credit = round(float(pd.to_numeric(data["credit"], errors="coerce").fillna(0).sum()), 2) meta.closing_balance = round(float(data.iloc[-1]["balance"]), 2) return meta, finalize(data, meta)