204 lines
6.2 KiB
Python
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
|