"""Tests for BatchPaymentsProcessor.""" from decimal import Decimal from unittest import mock from unittest.mock import call, patch from src.models import ( AccountPayableContract, PaymentAccount, PaymentBatch, PaymentBatchItem, ) from src.processors.batch_payments_processor import BatchPaymentsProcessor from tests.unit.factories import ( AccountPayeeFactory, AccountPaymentDetailsFactory, AccountPaymentTermFactory, EventFactory, PayableBalanceAfterTaxFactory, PaymentAccountInstanceFactory, PaymentMethodMinimumFactory, ) @patch('src.processors.batch_payments_processor.bulk_create_payment_accounts') def test_process_eligible_items_creates_payment_accounts( mock_bulk_create: mock.MagicMock, ) -> None: """Process formats and creates accounts that pass the balance limit.""" worksheet = PayableBalanceAfterTaxFactory.build( account_id=1, currency_code='USD', payable_amount_pre_tax=Decimal('120'), payable_amount_post_tax=Decimal('100'), tax_withholding_amount=Decimal('20'), vat_amount=Decimal('0'), ) payment_term = AccountPaymentTermFactory.build( account_id=1, currency_code='USD', payment_minimum=Decimal('10') ) last_payment = AccountPaymentDetailsFactory.build( account_id=1, balance_after_tax=Decimal('50'), current_statement_period_id=99, ) account_payee = AccountPayeeFactory.build(account_id=1, payoneer_program_id=42) item = PaymentBatchItem( account_id=1, payable_worksheets=[worksheet], last_payment=last_payment, payment_term=payment_term, account_payee=account_payee, ) minimum = PaymentMethodMinimumFactory.build( currency_code='USD', check_amount=Decimal('20') ) event = EventFactory.build(target_id=999) instance = PaymentAccountInstanceFactory.build() mock_bulk_create.return_value = [instance] batch = PaymentBatch( abacus_event=event, payment_minimums={'USD': minimum}, items=[item], ) result = BatchPaymentsProcessor().process(batch) assert result == [instance] assert mock_bulk_create.call_args_list == [ call( 999, [ PaymentAccount( contracts_payable=[ AccountPayableContract( contract_id=worksheet.contract_id, currency_code='USD', current_balance=Decimal('100'), ) ], currency_code='USD', current_balance=Decimal('120'), last_payment=Decimal('50'), tax_withholding=Decimal('20'), vat_amount=Decimal('0'), balance_after_tax=Decimal('100'), account_id=1, payoneer_program_id=42, last_statement_period_id=99, ) ], ) ] @patch('src.processors.batch_payments_processor.bulk_create_payment_accounts') def test_process_filters_items_below_payment_minimum( mock_bulk_create: mock.MagicMock, ) -> None: """Items below the payment_term minimum are filtered out.""" eligible_worksheet = PayableBalanceAfterTaxFactory.build( account_id=1, currency_code='USD', payable_amount_pre_tax=Decimal('120'), payable_amount_post_tax=Decimal('100'), tax_withholding_amount=Decimal('20'), vat_amount=Decimal('0'), ) eligible = PaymentBatchItem( account_id=1, payable_worksheets=[eligible_worksheet], last_payment=AccountPaymentDetailsFactory.build( account_id=1, balance_after_tax=Decimal('50'), current_statement_period_id=99, ), payment_term=AccountPaymentTermFactory.build( account_id=1, currency_code='USD', payment_minimum=Decimal('10') ), account_payee=AccountPayeeFactory.build(account_id=1, payoneer_program_id=42), ) below_minimum = PaymentBatchItem( account_id=2, payable_worksheets=[ PayableBalanceAfterTaxFactory.build( account_id=2, currency_code='USD', payable_amount_pre_tax=Decimal('120'), payable_amount_post_tax=Decimal('100'), tax_withholding_amount=Decimal('20'), vat_amount=Decimal('0'), ) ], last_payment=AccountPaymentDetailsFactory.build(account_id=2), payment_term=AccountPaymentTermFactory.build( account_id=2, currency_code='USD', payment_minimum=Decimal('500') ), account_payee=AccountPayeeFactory.build(account_id=2), ) minimum = PaymentMethodMinimumFactory.build( currency_code='USD', check_amount=Decimal('20') ) event = EventFactory.build() mock_bulk_create.return_value = [] batch = PaymentBatch( abacus_event=event, payment_minimums={'USD': minimum}, items=[eligible, below_minimum], ) BatchPaymentsProcessor().process(batch) assert mock_bulk_create.call_args_list == [ call( event.target_id, [ PaymentAccount( contracts_payable=[ AccountPayableContract( contract_id=eligible_worksheet.contract_id, currency_code='USD', current_balance=Decimal('100'), ) ], currency_code='USD', current_balance=Decimal('120'), last_payment=Decimal('50'), tax_withholding=Decimal('20'), vat_amount=Decimal('0'), balance_after_tax=Decimal('100'), account_id=1, payoneer_program_id=42, last_statement_period_id=99, ) ], ) ] @patch('src.processors.batch_payments_processor.bulk_create_payment_accounts') def test_process_filters_items_below_check_amount( mock_bulk_create: mock.MagicMock, ) -> None: """Items below the payment_method_minimum.check_amount are filtered out.""" item = PaymentBatchItem( account_id=1, payable_worksheets=[ PayableBalanceAfterTaxFactory.build( account_id=1, currency_code='USD', payable_amount_pre_tax=Decimal('120'), payable_amount_post_tax=Decimal('5'), tax_withholding_amount=Decimal('20'), vat_amount=Decimal('0'), ) ], last_payment=AccountPaymentDetailsFactory.build(account_id=1), payment_term=AccountPaymentTermFactory.build( account_id=1, currency_code='USD', payment_minimum=Decimal('0') ), account_payee=AccountPayeeFactory.build(account_id=1), ) minimum = PaymentMethodMinimumFactory.build( currency_code='USD', check_amount=Decimal('100') ) event = EventFactory.build() batch = PaymentBatch( abacus_event=event, payment_minimums={'USD': minimum}, items=[item], ) result = BatchPaymentsProcessor().process(batch) assert result == [] assert not mock_bulk_create.called @patch('src.processors.batch_payments_processor.bulk_create_payment_accounts') def test_process_no_payment_minimum_for_currency_uses_zero_base( mock_bulk_create: mock.MagicMock, ) -> None: """When no payment_method_minimum exists for the currency, base is 0.""" worksheet = PayableBalanceAfterTaxFactory.build( account_id=1, currency_code='EUR', payable_amount_pre_tax=Decimal('120'), payable_amount_post_tax=Decimal('1'), tax_withholding_amount=Decimal('20'), vat_amount=Decimal('0'), ) item = PaymentBatchItem( account_id=1, payable_worksheets=[worksheet], last_payment=AccountPaymentDetailsFactory.build( account_id=1, balance_after_tax=Decimal('50'), current_statement_period_id=99, ), payment_term=AccountPaymentTermFactory.build( account_id=1, currency_code='EUR', payment_minimum=Decimal('0') ), account_payee=AccountPayeeFactory.build(account_id=1, payoneer_program_id=42), ) # Only USD minimum is provided. minimum = PaymentMethodMinimumFactory.build( currency_code='USD', check_amount=Decimal('100') ) event = EventFactory.build() mock_bulk_create.return_value = [] batch = PaymentBatch( abacus_event=event, payment_minimums={'USD': minimum}, items=[item], ) BatchPaymentsProcessor().process(batch) assert mock_bulk_create.call_args_list == [ call( event.target_id, [ PaymentAccount( contracts_payable=[ AccountPayableContract( contract_id=worksheet.contract_id, currency_code='EUR', current_balance=Decimal('1'), ) ], currency_code='EUR', current_balance=Decimal('120'), last_payment=Decimal('50'), tax_withholding=Decimal('20'), vat_amount=Decimal('0'), balance_after_tax=Decimal('1'), account_id=1, payoneer_program_id=42, last_statement_period_id=99, ) ], ) ] @patch('src.processors.batch_payments_processor.bulk_create_payment_accounts') def test_process_no_eligible_items_skips_bulk_create( mock_bulk_create: mock.MagicMock, ) -> None: """No bulk_create call when nothing passes the balance limit.""" item = PaymentBatchItem( account_id=1, payable_worksheets=[ PayableBalanceAfterTaxFactory.build( account_id=1, currency_code='USD', payable_amount_pre_tax=Decimal('120'), payable_amount_post_tax=Decimal('1'), tax_withholding_amount=Decimal('20'), vat_amount=Decimal('0'), ) ], last_payment=AccountPaymentDetailsFactory.build(account_id=1), payment_term=AccountPaymentTermFactory.build( account_id=1, currency_code='USD', payment_minimum=Decimal('1000') ), account_payee=AccountPayeeFactory.build(account_id=1), ) minimum = PaymentMethodMinimumFactory.build( currency_code='USD', check_amount=Decimal('1') ) event = EventFactory.build() batch = PaymentBatch( abacus_event=event, payment_minimums={'USD': minimum}, items=[item], ) result = BatchPaymentsProcessor().process(batch) assert result == [] assert not mock_bulk_create.called @patch('src.processors.batch_payments_processor.bulk_create_payment_accounts') def test_process_bulk_create_returns_none_yields_empty_list( mock_bulk_create: mock.MagicMock, ) -> None: """If the microservice returns None, the processor returns [].""" worksheet = PayableBalanceAfterTaxFactory.build( account_id=1, currency_code='USD', payable_amount_pre_tax=Decimal('120'), payable_amount_post_tax=Decimal('100'), tax_withholding_amount=Decimal('20'), vat_amount=Decimal('0'), ) item = PaymentBatchItem( account_id=1, payable_worksheets=[worksheet], last_payment=AccountPaymentDetailsFactory.build( account_id=1, balance_after_tax=Decimal('50'), current_statement_period_id=99, ), payment_term=AccountPaymentTermFactory.build( account_id=1, currency_code='USD', payment_minimum=Decimal('10') ), account_payee=AccountPayeeFactory.build(account_id=1, payoneer_program_id=42), ) minimum = PaymentMethodMinimumFactory.build( currency_code='USD', check_amount=Decimal('1') ) event = EventFactory.build() mock_bulk_create.return_value = None batch = PaymentBatch( abacus_event=event, payment_minimums={'USD': minimum}, items=[item], ) result = BatchPaymentsProcessor().process(batch) assert result == [] assert mock_bulk_create.call_args_list == [ call( event.target_id, [ PaymentAccount( contracts_payable=[ AccountPayableContract( contract_id=worksheet.contract_id, currency_code='USD', current_balance=Decimal('100'), ) ], currency_code='USD', current_balance=Decimal('120'), last_payment=Decimal('50'), tax_withholding=Decimal('20'), vat_amount=Decimal('0'), balance_after_tax=Decimal('100'), account_id=1, payoneer_program_id=42, last_statement_period_id=99, ) ], ) ] @patch('src.processors.batch_payments_processor.bulk_create_payment_accounts') def test_format_payment_account_without_last_payment( mock_bulk_create: mock.MagicMock, ) -> None: """When last_payment is None, last_payment=0 and last_statement_period_id=None.""" worksheet = PayableBalanceAfterTaxFactory.build( account_id=7, currency_code='USD', payable_amount_pre_tax=Decimal('120'), payable_amount_post_tax=Decimal('100'), tax_withholding_amount=Decimal('20'), vat_amount=Decimal('0'), ) item = PaymentBatchItem( account_id=7, payable_worksheets=[worksheet], last_payment=None, payment_term=AccountPaymentTermFactory.build( account_id=7, currency_code='USD', payment_minimum=Decimal('0') ), account_payee=AccountPayeeFactory.build(account_id=7, payoneer_program_id=None), ) minimum = PaymentMethodMinimumFactory.build( currency_code='USD', check_amount=Decimal('0') ) event = EventFactory.build() mock_bulk_create.return_value = [] batch = PaymentBatch( abacus_event=event, payment_minimums={'USD': minimum}, items=[item], ) BatchPaymentsProcessor().process(batch) assert mock_bulk_create.call_args_list == [ call( event.target_id, [ PaymentAccount( contracts_payable=[ AccountPayableContract( contract_id=worksheet.contract_id, currency_code='USD', current_balance=Decimal('100'), ) ], currency_code='USD', current_balance=Decimal('120'), last_payment=Decimal(0), tax_withholding=Decimal('20'), vat_amount=Decimal('0'), balance_after_tax=Decimal('100'), account_id=7, payoneer_program_id=0, last_statement_period_id=None, ) ], ) ] @patch('src.processors.batch_payments_processor.bulk_create_payment_accounts') def test_format_payment_account_last_payment_with_null_balance_after_tax( mock_bulk_create: mock.MagicMock, ) -> None: """A non-null last_payment with null balance_after_tax still resolves to 0.""" worksheet = PayableBalanceAfterTaxFactory.build( account_id=3, currency_code='USD', payable_amount_pre_tax=Decimal('120'), payable_amount_post_tax=Decimal('100'), tax_withholding_amount=Decimal('20'), vat_amount=Decimal('0'), ) item = PaymentBatchItem( account_id=3, payable_worksheets=[worksheet], last_payment=AccountPaymentDetailsFactory.build( account_id=3, balance_after_tax=None, current_statement_period_id=99, ), payment_term=AccountPaymentTermFactory.build( account_id=3, currency_code='USD', payment_minimum=Decimal('0') ), account_payee=AccountPayeeFactory.build(account_id=3, payoneer_program_id=42), ) minimum = PaymentMethodMinimumFactory.build( currency_code='USD', check_amount=Decimal('0') ) event = EventFactory.build() mock_bulk_create.return_value = [] batch = PaymentBatch( abacus_event=event, payment_minimums={'USD': minimum}, items=[item], ) BatchPaymentsProcessor().process(batch) assert mock_bulk_create.call_args_list == [ call( event.target_id, [ PaymentAccount( contracts_payable=[ AccountPayableContract( contract_id=worksheet.contract_id, currency_code='USD', current_balance=Decimal('100'), ) ], currency_code='USD', current_balance=Decimal('120'), last_payment=Decimal(0), tax_withholding=Decimal('20'), vat_amount=Decimal('0'), balance_after_tax=Decimal('100'), account_id=3, payoneer_program_id=42, last_statement_period_id=99, ) ], ) ] @patch('src.processors.batch_payments_processor.bulk_create_payment_accounts') def test_format_payment_account_contracts_payable_from_worksheets( mock_bulk_create: mock.MagicMock, ) -> None: """contracts_payable is built from the item's payable_worksheets.""" worksheet_1 = PayableBalanceAfterTaxFactory.build( account_id=1, contract_id=101, currency_code='USD', payable_amount_pre_tax=Decimal('70'), payable_amount_post_tax=Decimal('60'), tax_withholding_amount=Decimal('10'), vat_amount=None, ) worksheet_2 = PayableBalanceAfterTaxFactory.build( account_id=1, contract_id=102, currency_code='USD', payable_amount_pre_tax=Decimal('30'), payable_amount_post_tax=Decimal('25'), tax_withholding_amount=Decimal('5'), vat_amount=Decimal('3'), ) payment_term = AccountPaymentTermFactory.build( account_id=1, currency_code='USD', payment_minimum=Decimal(0) ) account_payee = AccountPayeeFactory.build(account_id=1, payoneer_program_id=1) item = PaymentBatchItem( account_id=1, payable_worksheets=[worksheet_1, worksheet_2], last_payment=None, payment_term=payment_term, account_payee=account_payee, ) minimum = PaymentMethodMinimumFactory.build( currency_code='USD', check_amount=Decimal('0') ) event = EventFactory.build() mock_bulk_create.return_value = [] batch = PaymentBatch( abacus_event=event, payment_minimums={'USD': minimum}, items=[item], ) BatchPaymentsProcessor().process(batch) assert mock_bulk_create.call_args_list == [ call( event.target_id, [ PaymentAccount( contracts_payable=[ AccountPayableContract( contract_id=101, currency_code='USD', current_balance=Decimal('60'), ), AccountPayableContract( contract_id=102, currency_code='USD', current_balance=Decimal('25'), ), ], currency_code='USD', current_balance=Decimal('100'), last_payment=Decimal(0), tax_withholding=Decimal('15'), vat_amount=Decimal('3'), balance_after_tax=Decimal('85'), account_id=1, payoneer_program_id=1, last_statement_period_id=None, ) ], ) ]