Files
ach_ui_dbtl_file_based/processors/data_mapper.py

204 lines
6.2 KiB
Python

#!/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 process_status(status: str) -> str:
"""
Process status field.
If status contains 'processed' (case-insensitive), return 'Processed'.
Otherwise return the original status.
Args:
status: Original status text
Returns:
'Processed' if status contains 'processed', else original status
"""
try:
if not status:
return ''
# Check if 'processed' is in status (case-insensitive)
if 'processed' in status.lower():
return 'Processed'
# Otherwise return original status
return status
except Exception as e:
logger.error(f"Error processing status: {e}")
return status
@staticmethod
def pad_account_number(account_number: str) -> str:
"""
Pad account number with leading zeroes to make it 17 digits.
Args:
account_number: Account number string
Returns:
Account number padded to 17 digits with leading zeroes
"""
try:
if not account_number:
return '0' * 17
# Remove any existing spaces and pad to 17 digits
clean_account = account_number.strip()
return clean_account.zfill(17)
except Exception as e:
logger.error(f"Error padding account number '{account_number}': {e}")
return '0' * 17
@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', ''))
# Pad account number to 17 digits
padded_account = cls.pad_account_number(parsed_transaction.get('cust_acct', ''))
# Process status (check for 'processed' keyword)
status = cls.process_status(parsed_transaction.get('sys', ''))
record = TransactionRecord(
narration=parsed_transaction.get('remarks', '')[:500], # Keep original remarks
status=status, # 'Processed' if status contains 'processed', else original
bankcode=bankcode,
jrnl_id=parsed_transaction.get('jrnl_no', ''),
tran_date=tran_date,
cbs_acct=padded_account, # Padded to 17 digits
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