"""Generate banking details report processor.""" from typing import Dict, List import uuid from sentry_sdk import capture_message from config import app_logger as logger from src.connectors.exceptions import OwsPayeeException from src.connectors.ows_abacus_account import get_payees_by_accounts from src.connectors.ows_payee import ( get_bank_details_by_account_payee, get_bank_details_by_payee, get_payees_by_client_refs, ) from src.models import PayeeDetails from src.processors.base import ReportProcessor from src.processors.exceptions import ProcessingError from src.processors.reports.utils import flatten_model, map_bank_details_fields class GenerateReportBankingDetailsProcessor(ReportProcessor): """ Processor to generate report for payee's banking details information. Expects a CSV file with the following columns: - vendor_id/account_id (int or UUID) The first row is expected to be a header. """ _report_type: str = 'banking_details' _CSV_ACCOUNT_ID_FIELD: str = 'vendor_id' def __init__(self, bucket_name: str, file_path: str): super().__init__(bucket_name, file_path) self._account_ids: List[int] = [] self._account_id_to_account_payee_id: Dict[int, int] = {} self._client_reference_ids: List[str] = [] self._client_reference_id_to_payee_id: Dict[str, int] = {} self._raw_data: List[PayeeDetails] = [] def process(self) -> None: """ Run the report generation process. Executes the complete workflow: 1. Load account IDs from CSV 2. Map account IDs to payee IDs 3. Fetch bank details for each payee 4. Prepare and format the output data """ self._load_csv_accounts() self._load_payees_by_accounts() self._load_payees_by_client_refs() self._load_bank_details() self._prepare_data() @staticmethod def capture_error(message: str) -> None: """ Send error to sentry and log. Args: message: Error message to log and send to Sentry """ logger.error(message) capture_message(message, level='error') def _load_csv_accounts(self) -> None: """ Load account IDs from CSV file. Raises: ProcessingError: If file cannot be read or has invalid content """ logger.info('Loading data from CSV file') try: raw_data = list(self.csv_dict_reader) except Exception as e: raise ProcessingError(f'Unable to read the file: {e}') for item in raw_data: try: _item = item[self._CSV_ACCOUNT_ID_FIELD] except KeyError: raise ProcessingError( f'Invalid file content: missing field {self._CSV_ACCOUNT_ID_FIELD}' ) try: self._account_ids.append(int(_item)) continue except ValueError: pass try: uuid.UUID(str(_item)) self._client_reference_ids.append(_item) continue except ValueError: pass raise ProcessingError( f'Invalid account ID format: {_item}. Expected UUID or integer.' ) total_ids = len(self._account_ids) + len(self._client_reference_ids) if total_ids == 0: raise ProcessingError('No valid IDs found in the CSV file.') logger.info( f'Loaded {total_ids} IDs ({len(self._account_ids)} account IDs, ' f'{len(self._client_reference_ids)} client reference IDs).' ) def _load_payees_by_accounts(self) -> None: """ Get payee IDs corresponding to account IDs. Logs any accounts that could not be found. """ if not self._account_ids: logger.info('No account IDs provided, skipping this step') return self._account_id_to_account_payee_id = get_payees_by_accounts(self._account_ids) missing_account_ids = set(self._account_ids) - set( self._account_id_to_account_payee_id.keys() ) if missing_account_ids: self.capture_error(f'Accounts not found: {missing_account_ids}') logger.info( f'Loaded {len(self._account_id_to_account_payee_id)} payees from {len(self._account_ids)} account IDs' ) def _load_payees_by_client_refs(self) -> None: """ Get payee IDs corresponding to client reference IDs. Logs any client reference IDs that could not be found. """ if not self._client_reference_ids: logger.info('No client reference IDs provided, skipping this step') return self._client_reference_id_to_payee_id = get_payees_by_client_refs( self._client_reference_ids ) missing_reference_ids = set(self._client_reference_ids) - set( self._client_reference_id_to_payee_id.keys() ) if missing_reference_ids: self.capture_error( f'Client reference IDs not found: {missing_reference_ids}' ) logger.info( f'Loaded {len(self._client_reference_id_to_payee_id)} payees from {len(self._client_reference_ids)} client reference IDs' ) def _load_bank_details(self) -> None: """ Fetch banking details for each payee ID via the ows-payee service. Logs any errors encountered when fetching individual payee details. """ self._raw_data = [] account_payee_ids = list(self._account_id_to_account_payee_id.values()) client_payee_ids = list(self._client_reference_id_to_payee_id.values()) total_payee_ids = len(account_payee_ids) + len(client_payee_ids) if total_payee_ids == 0: logger.warning('No payee IDs found to fetch bank details') return logger.info(f'Fetching banking details for {total_payee_ids} payees') payee_sources = { get_bank_details_by_account_payee: account_payee_ids, get_bank_details_by_payee: client_payee_ids, } for func, owner_ids in payee_sources.items(): for owner_id in owner_ids: try: payee_details = func(owner_id) self._raw_data.append(payee_details) except OwsPayeeException as e: error_msg = f'Failed to fetch bank details for ID {owner_id}: {e}' logger.error(error_msg) capture_message(error_msg) logger.info( f'Successfully fetched details for {len(self._raw_data)} out of {total_payee_ids} payees' ) def _prepare_data(self) -> None: """ Process raw data into the required report format. 1. Flattens PayeeDetails models 2. Maps payee IDs back to account IDs 3. Applies field mapping for the final output """ if not self._raw_data: logger.warning('No raw data available to prepare') self._output_data = [] return logger.info('Preparing data for banking details report') account_payee_id_to_account_id = { v: k for k, v in self._account_id_to_account_payee_id.items() } self._output_data = [] for item in self._raw_data: entry = flatten_model(item) vendor_id = entry['account_payee_id'] if isinstance(vendor_id, str) and vendor_id.isdigit(): entry['account_id'] = account_payee_id_to_account_id.get(int(vendor_id)) else: entry['account_id'] = vendor_id self._output_data.append(map_bank_details_fields(entry)) logger.info(f'Prepared {len(self._output_data)} records for the report')