from decimal import Decimal from typing import Iterable from config import app_logger as logger from src.connectors.ows_payment import bulk_create_payment_accounts from src.constants import CREATE_PAYMENT_ACCOUNT_MSG, NO_ELIGIBLE_ACCOUNTS from src.models import ( AccountPayableContract, Event, PaymentAccount, PaymentAccountInstance, PaymentBatch, PaymentBatchItem, PaymentMethodMinimum, ) class BatchPaymentsProcessor: """ Processor to handle batches of worksheets and create payment accounts. Workflow: 1. Filters batch items whose payable amount meets both the payment term minimum and the base currency minimum defined in payment method minimums 2. Formats eligible items into PaymentAccount payloads 3. Submits the payloads in bulk to the ows-payment service and returns the resulting PaymentAccountInstance records """ _abacus_event: Event _payment_minimums: dict[str, PaymentMethodMinimum] _payment_accounts: list[PaymentAccount] def process(self, batch: PaymentBatch) -> list[PaymentAccountInstance]: """Process a payment batch and return the created payment account instances. This is the main entry point. It stores the event and payment minimums from the batch, filters and formats eligible payment accounts, and then triggers their creation via the ows-payment service. Args: batch: A PaymentBatch containing the Abacus event, payment method minimums, and the list of items to process. Returns: A list of PaymentAccountInstance objects created by the ows-payment service. """ self._abacus_event = batch.abacus_event self._payment_minimums = batch.payment_minimums self._setup_payment_accounts(batch.items) return self._create_payment_accounts() def _setup_payment_accounts(self, batch: Iterable[PaymentBatchItem]) -> None: """Build the list of PaymentAccount payloads from eligible batch items. Iterates over the batch, checks balances limits, formats payloads.`. """ self._payment_accounts = [ self._format_payment_account( batch_item, ) for batch_item in batch if self._check_balance_limit(batch_item) ] def _check_balance_limit( self, batch_item: PaymentBatchItem, ) -> bool: """Return True if the item's payable amount satisfies all payment minimums. Two thresholds are evaluated against the post-tax payable amount using base payment minimums and per account payments minimums. """ payment_term = batch_item.payment_term payment_method_minimum = self._payment_minimums.get(payment_term.currency_code) base_minimum = ( payment_method_minimum.check_amount if payment_method_minimum else Decimal(0) ) post_tax_amount = batch_item.aggregated_balance.payable_amount_post_tax return ( Decimal(payment_term.payment_minimum or 0) <= post_tax_amount and base_minimum <= post_tax_amount ) @staticmethod def _format_payment_account( batch_item: PaymentBatchItem, ) -> PaymentAccount: """Build a PaymentAccount payload from a single batch item. Constructs the list of AccountPayableContract entries from the item's payable worksheets and combines them with aggregated balance figures, last-payment details, and payee information into a PaymentAccount ready to be submitted to the ows-payment service. Args: batch_item: The batch item whose data will be mapped to a PaymentAccount. Returns: A fully populated PaymentAccount instance. """ account_balance = batch_item.aggregated_balance last_payment = batch_item.last_payment contracts_payable = [ AccountPayableContract( contract_id=worksheet.contract_id, currency_code=worksheet.currency_code, current_balance=worksheet.payable_amount_post_tax, ) for worksheet in batch_item.payable_worksheets ] return PaymentAccount( contracts_payable=contracts_payable, currency_code=batch_item.payment_term.currency_code, current_balance=account_balance.payable_amount_pre_tax, last_payment=last_payment.balance_after_tax or Decimal(0) if last_payment else Decimal(0), tax_withholding=account_balance.tax_withholding_amount, vat_amount=account_balance.vat_amount, balance_after_tax=account_balance.payable_amount_post_tax, account_id=batch_item.account_id, payoneer_program_id=batch_item.account_payee.payoneer_program_id or 0, last_statement_period_id=last_payment.current_statement_period_id if last_payment else None, ) def _create_payment_accounts(self) -> list[PaymentAccountInstance]: """Submit the formatted payment accounts to the ows-payment service in bulk. Skips the remote call and returns an empty list when there are no eligible payment accounts. Otherwise, returns the resulting PaymentAccountInstance list. """ if not self._payment_accounts: logger.info(NO_ELIGIBLE_ACCOUNTS) return [] logger.info(CREATE_PAYMENT_ACCOUNT_MSG.format(len(self._payment_accounts))) return ( bulk_create_payment_accounts( self._abacus_event.target_id, self._payment_accounts ) or [] )