#!/usr/bin/env python3 """ Data mapper for field transformations. Maps ACH parser output to database format. """ from datetime import datetime from decimal import Decimal from typing import Dict, Any from logging_config import get_logger from db.models import TransactionRecord logger = get_logger(__name__) class DataMapper: """Maps parsed ACH transactions to database records.""" @staticmethod def convert_date(date_str: str) -> str: """ Convert ACH date string to DDMMYYYY format. ACH format: DD/MM/YY (e.g., '19/01/26') Output format: DDMMYYYY (e.g., '19012026') Args: date_str: Date string in DD/MM/YY format Returns: Date string in DDMMYYYY format """ try: if not date_str or len(date_str) < 8: raise ValueError(f"Invalid date format: {date_str}") # Parse DD/MM/YY parsed_date = datetime.strptime(date_str, '%d/%m/%y') # Return in DDMMYYYY format return parsed_date.strftime('%d%m%Y') except Exception as e: logger.error(f"Error converting date '{date_str}': {e}") # Return today's date in DDMMYYYY format as fallback return datetime.now().strftime('%d%m%Y') @staticmethod def calculate_txnind(amount_str: str) -> str: """ Calculate transaction indicator from amount. Args: amount_str: Amount as string Returns: 'CR' for credit (>= 0), 'DR' for debit (< 0) """ try: amount = Decimal(amount_str.strip()) return 'DR' if amount < 0 else 'CR' except Exception as e: logger.error(f"Error calculating TXNIND for amount '{amount_str}': {e}") return 'CR' # Default to credit @staticmethod def convert_amount(amount_str: str) -> Decimal: """ Convert amount string to Decimal. Args: amount_str: Amount as string Returns: Decimal representation of amount """ try: if not amount_str: return Decimal('0') amount = Decimal(amount_str.strip()) return abs(amount) # Store absolute value, use TXNIND for sign except Exception as e: logger.error(f"Error converting amount '{amount_str}': {e}") return Decimal('0') @classmethod def map_transaction( cls, parsed_transaction: Dict[str, Any], bankcode: str ) -> TransactionRecord: """ Map parsed transaction to database record. Args: parsed_transaction: Transaction from ACHParser bankcode: Bank code for this transaction Returns: TransactionRecord ready for database insertion """ try: amount_str = parsed_transaction.get('amount', '0') amount = cls.convert_amount(amount_str) txnind = cls.calculate_txnind(amount_str) tran_date = cls.convert_date(parsed_transaction.get('date', '')) record = TransactionRecord( narration=parsed_transaction.get('remarks', '')[:500], # Limit to 500 chars status=parsed_transaction.get('sys', ''), bankcode=bankcode, jrnl_id=parsed_transaction.get('jrnl_no', ''), tran_date=tran_date, cbs_acct=parsed_transaction.get('cust_acct', ''), tran_amt=amount, txnind=txnind, ) return record except Exception as e: logger.error(f"Error mapping transaction: {e}", exc_info=True) raise @classmethod def map_transactions( cls, parsed_transactions: list, bankcode: str ) -> list: """ Map multiple transactions. Args: parsed_transactions: List of transactions from ACHParser bankcode: Bank code for these transactions Returns: List of TransactionRecord objects """ records = [] for txn in parsed_transactions: try: record = cls.map_transaction(txn, bankcode) records.append(record) except Exception as e: logger.warning(f"Skipping transaction due to error: {e}") continue logger.info(f"Mapped {len(records)} transactions for bank {bankcode}") return records