#!/usr/bin/env python3 """ Data mapper for NEFT SFTP feed. Maps parsed NEFT transactions (dicts) to RTGSInwardRecord for database insertion. - No padding of account numbers (kept as-is, trimmed). """ from datetime import datetime from decimal import Decimal from typing import Dict, Any, List from logging_config import get_logger from db.models import RTGSInwardRecord logger = get_logger(__name__) class RTGSDataMapper: """Maps parsed NEFT transactions to RTGSInwardRecord objects.""" # ------------------------- # Helpers # ------------------------- @staticmethod def convert_date(date_str: str) -> str: """ Convert NEFT date (YYYYMMDD) to DDMMYYYY. Input : '20260306' Output: '06032026' On error, returns today's date in DDMMYYYY format. """ try: if not date_str or len(date_str.strip()) != 8 or not date_str.isdigit(): raise ValueError(f"Invalid NEFT date format: {date_str!r}") dt = datetime.strptime(date_str, "%Y%m%d") return dt.strftime("%d%m%Y") except Exception as e: logger.error(f"Error converting date '{date_str}': {e}") return datetime.now().strftime("%d%m%Y") @staticmethod def process_status(status: str) -> str: try: if not status: return '' s = status.strip() if s == 'PROS': return 'PROCESSED' if s == 'SUSP': return 'SUSPENDED' if s == 'FAIL': return 'FAILED' if s == 'RVRS': return 'REVERSED' return s @staticmethod def convert_amount(amount_in: Any) -> Decimal: """ Convert amount to Decimal and return absolute value. Use TXNIND to capture the sign semantics. """ try: if isinstance(amount_in, Decimal): val = amount_in else: txt = (str(amount_in) or '').replace(',', '').strip() val = Decimal(txt) if txt else Decimal('0') return abs(val) except Exception as e: logger.error(f"Error converting amount '{amount_in}': {e}") return Decimal('0') # ------------------------- # Mapping # ------------------------- @classmethod def map_transaction(cls, parsed_txn: Dict[str, Any], bankcode: str) -> NEFTInwardRecord: """ Map a single parsed NEFT transaction (dict) to NEFTInwardRecord. Args: parsed_txn: Dict emitted by SFTPUtrParser bankcode : Bank code for this transaction (mapped to NEFTInwardRecord.bank_code) """ try: # Amount handling amount_in = parsed_txn.get('amount', '0') txn_amt = cls.convert_amount(amount_in) txnind = "CR" creditor_amt=parsed_txn.get('amount', '0') # Date handling txn_date_raw = parsed_txn.get('tran_date', '') or '' txn_date_ddmmyyyy = cls.convert_date(txn_date_raw) # Account numbers: NO padding, just trim sender_acct = (parsed_txn.get('remitter_acct_no') or '').lstrip('/').strip() recvr_acct = (parsed_txn.get('benef_acct_no') or '').strip() # Status normalization status_norm = cls.process_status(parsed_txn.get('status', '')) # Receiver account name: best available proxy is beneficiary_details recvr_acct_name = (parsed_txn.get('beneficiary_details') or '').strip() record = NEFTInwardRecord( bank_code=bankcode, txnind=txnind, jrnl_id=(parsed_txn.get('journal_no') or '').strip(), ref_no=(parsed_txn.get('utr') or '').strip(), txn_date=txn_date_ddmmyyyy, txn_amt=txn_amt, sender_ifsc=(parsed_txn.get('ifsc_sender') or '').strip(), reciever_ifsc=(parsed_txn.get('ifsc_recvr') or '').strip(), sender_acct_no=sender_acct, sender_acct_name=(parsed_txn.get('sender_acct_name') or '').strip(), remitter_detail=(parsed_txn.get('remitter_detail') or '').strip(), remitter_info=(parsed_txn.get('remmiter_info') or '').strip(), recvr_acct_no=recvr_acct, recvr_acct_name=recvr_acct_name, status=status_norm, reject_code=(parsed_txn.get('reject_code') or '').strip(), benef_address=(parsed_txn.get('benef_address') or '').strip(), msg_type=(parsed_txn.get('sub_msg_type') or '').strip(), creditor_amt=creditor_amt, ) return record except Exception as e: logger.error(f"Error mapping NEFT transaction: {e}", exc_info=True) raise @classmethod def map_transactions(cls, parsed_transactions: List[Dict[str, Any]], bankcode: str) -> List[NEFTInwardRecord]: """ Map a list of parsed NEFT transactions to RTGSInwardRecord objects. Args: parsed_transactions: List of dicts from SFTPUtrParser bankcode : Bank code to be applied to each record """ records: List[NEFTInwardRecord] = [] for txn in parsed_transactions: try: rec = cls.map_transaction(txn, bankcode) records.append(rec) except Exception as e: logger.warning(f"Skipping transaction due to error: {e}") logger.info(f"Mapped {len(records)} NEFT transactions for bank {bankcode}") return records