"""Lambda test module.""" from decimal import Decimal from typing import Any, Dict, List from unittest import mock from unittest.mock import call, patch import pytest from src import constants, main from src.models import ( Account, AccountPaymentDetails, AggregatedBalancesAfterTax, ContractCloseBalance, PaginatedContractCloseBalances, PayableBalanceAfterTax, ) from tests.unit.factories import ( AbacusStateFactory, AccountFactory, AccountPayableContractFactory, AccountPaymentDetailsFactory, AccountPaymentHoldFactory, ContractCloseBalanceFactory, EventFactory, PayableBalanceAfterTaxFactory, PayableDetailsFactory, PaymentAccountFactory, PaymentAccountInstanceFactory, PaymentAllocationFlowthroughFactory, PaymentEntityFactory, PaymentGroupFactory, PaymentGroupPaymentFactory, PaymentMethodMinimumFactory, ) @patch('src.main._update_payment_allocations_flowthrough') @patch('src.main.get_payment_group') @patch('src.main._fetch_all_payable_balance_after_tax_entries') @patch('src.main._update_state_status') @patch('src.main._get_generate_payment_state') @patch('src.main._create_payment_accounts_from_agg_balances') @patch('src.main._aggregate_worksheets_by_account') @patch('src.main._get_payment_minimums') @patch('src.main._batch_accounts') @patch('src.main.get_eligible_accounts') @patch('src.main._payment_group_payment') def test_old_handler_success( mock_payment_group_payment: mock.MagicMock, mock_get_eligible_accounts: mock.MagicMock, mock_batch_accounts: mock.MagicMock, mock_get_payment_minimums: mock.MagicMock, mock_aggregate_worksheets_by_account: mock.MagicMock, mock_create_payment_accounts: mock.MagicMock, mock_get_generate_payment_state: mock.MagicMock, mock_update_state_status: mock.MagicMock, mock_fetch_all_payable_balance_after_tax_entries: mock.MagicMock, mock_get_payment_group: mock.MagicMock, mock_update_payment_allocations_flowthrough: mock.MagicMock, ) -> None: """Test main.old_handler. separate calculation""" payment_group = PaymentGroupFactory.build(group_criteria={}) payment_group_payment = PaymentGroupPaymentFactory.build( payment_group_id=payment_group.payment_group_id ) account_1 = AccountFactory.build(account_id=1) account_2 = AccountFactory.build(account_id=2) payment_group_payment_account = PaymentAccountInstanceFactory.build() eligible_accounts = [account_1, account_2] entry1 = PayableBalanceAfterTaxFactory.build(account_id=account_1.account_id) entry2 = PayableBalanceAfterTaxFactory.build(account_id=account_2.account_id) payment_minimums = [PaymentMethodMinimumFactory.build()] event = EventFactory.build() state = AbacusStateFactory.build() account_balances1 = { account_1.account_id: AggregatedBalancesAfterTax( payable_amount_pre_tax=123.12, tax_withholding_amount=10.1, payable_amount_post_tax=113.02, ) } account_balances2 = { account_2.account_id: AggregatedBalancesAfterTax( payable_amount_pre_tax=1110.32, tax_withholding_amount=100.1, payable_amount_post_tax=1010.22, ) } mock_get_payment_group.return_value = payment_group mock_payment_group_payment.return_value = payment_group_payment mock_get_eligible_accounts.return_value = eligible_accounts mock_fetch_all_payable_balance_after_tax_entries.return_value = [entry1, entry2] mock_get_payment_minimums.return_value = payment_minimums mock_aggregate_worksheets_by_account.side_effect = [ account_balances1, account_balances2, ] mock_create_payment_accounts.return_value = None mock_get_generate_payment_state.return_value = state mock_update_state_status.return_value = None mock_batch_accounts.return_value = [eligible_accounts] mock_create_payment_accounts.return_value = [payment_group_payment_account] result = main.old_handler(event.model_dump(), None) mock_get_payment_group.assert_called_once_with( payment_group_payment.payment_group_id ) mock_payment_group_payment.assert_called_once_with(event) mock_get_payment_minimums.assert_called_once() mock_get_eligible_accounts.assert_called_once_with( payment_group_payment.payment_group_id ) mock_fetch_all_payable_balance_after_tax_entries.assert_called_once_with(event) assert mock_aggregate_worksheets_by_account.call_args_list == [ call([entry1, entry2]), ] assert mock_create_payment_accounts.call_args_list == [ call( event, [account_1, account_2], payment_minimums, account_balances1, [entry1, entry2], ) ] mock_get_generate_payment_state.assert_called_once_with(event) mock_update_state_status.assert_has_calls( [ call(state, constants.ACTION_STATUS_RUNNING), call(state, constants.ACTION_STATUS_COMPLETE), ] ) assert mock_update_payment_allocations_flowthrough.call_args_list == [ call([payment_group_payment_account.payment_group_payment_account_id]) ] assert result['statusCode'] == 200 assert result['statusDescription'] == 'Success: generating payments finished' @patch('src.main._update_payment_allocations_flowthrough', return_value=True) @patch('src.main.get_payment_group') @patch('src.main._fetch_all_payable_balance_after_tax_entries') @patch('src.main._update_state_status') @patch('src.main._get_generate_payment_state') @patch('src.main._create_payment_accounts_from_agg_balances') @patch('src.main._aggregate_worksheets_by_account') @patch('src.main._get_payment_minimums') @patch('src.main._batch_accounts') @patch('src.main.get_eligible_accounts') @patch('src.main._payment_group_payment') def test_old_handler_error_allocations_update( mock_payment_group_payment: mock.MagicMock, mock_get_eligible_accounts: mock.MagicMock, mock_batch_accounts: mock.MagicMock, mock_get_payment_minimums: mock.MagicMock, mock_aggregate_worksheets_by_account: mock.MagicMock, mock_create_payment_accounts: mock.MagicMock, mock_get_generate_payment_state: mock.MagicMock, mock_update_state_status: mock.MagicMock, mock_fetch_all_payable_balance_after_tax_entries: mock.MagicMock, mock_get_payment_group: mock.MagicMock, mock_update_payment_allocations_flowthrough: mock.MagicMock, ) -> None: """Test main.old_handler failure on allocation update""" payment_group = PaymentGroupFactory.build(group_criteria={}) payment_group_payment = PaymentGroupPaymentFactory.build( payment_group_id=payment_group.payment_group_id ) account_1 = AccountFactory.build(account_id=1) account_2 = AccountFactory.build(account_id=2) payment_group_payment_account = PaymentAccountInstanceFactory.build() eligible_accounts = [account_1, account_2] entry1 = PayableBalanceAfterTaxFactory.build(account_id=account_1.account_id) entry2 = PayableBalanceAfterTaxFactory.build(account_id=account_2.account_id) payment_minimums = [PaymentMethodMinimumFactory.build()] event = EventFactory.build() state = AbacusStateFactory.build() account_balances1 = { account_1.account_id: AggregatedBalancesAfterTax( payable_amount_pre_tax=123.12, tax_withholding_amount=10.1, payable_amount_post_tax=113.02, ) } account_balances2 = { account_2.account_id: AggregatedBalancesAfterTax( payable_amount_pre_tax=1110.32, tax_withholding_amount=100.1, payable_amount_post_tax=1010.22, ) } mock_get_payment_group.return_value = payment_group mock_payment_group_payment.return_value = payment_group_payment mock_get_eligible_accounts.return_value = eligible_accounts mock_fetch_all_payable_balance_after_tax_entries.return_value = [entry1, entry2] mock_get_payment_minimums.return_value = payment_minimums mock_aggregate_worksheets_by_account.side_effect = [ account_balances1, account_balances2, ] mock_create_payment_accounts.return_value = None mock_get_generate_payment_state.return_value = state mock_update_state_status.return_value = None mock_batch_accounts.return_value = [eligible_accounts] mock_create_payment_accounts.return_value = [payment_group_payment_account] mock_update_payment_allocations_flowthrough.side_effect = Exception('test') result = main.old_handler(event.model_dump(), None) mock_get_payment_group.assert_called_once_with( payment_group_payment.payment_group_id ) mock_payment_group_payment.assert_called_once_with(event) mock_get_payment_minimums.assert_called_once() mock_get_eligible_accounts.assert_called_once_with( payment_group_payment.payment_group_id ) mock_fetch_all_payable_balance_after_tax_entries.assert_called_once_with(event) assert mock_aggregate_worksheets_by_account.call_args_list == [ call([entry1, entry2]), ] assert mock_create_payment_accounts.call_args_list == [ call( event, [account_1, account_2], payment_minimums, account_balances1, [entry1, entry2], ) ] mock_get_generate_payment_state.assert_called_once_with(event) mock_update_state_status.assert_has_calls( [ call(state, constants.ACTION_STATUS_RUNNING), call(state, constants.ACTION_STATUS_ERROR), ] ) assert mock_update_payment_allocations_flowthrough.call_args_list == [ call([payment_group_payment_account.payment_group_payment_account_id]) ] assert result['statusCode'] == 500 assert ( result['statusDescription'] == 'Failed to update flowthrough allocations: test' ) @pytest.mark.parametrize('agreement_type_ids', [None, [], [1, 2]]) @patch('src.main._update_payment_allocations_flowthrough') @patch('src.main.get_account_payment_hold') @patch('src.main.get_payment_group') @patch('src.main._fetch_all_payable_balance_after_tax_entries') @patch('src.main._update_state_status') @patch('src.main._get_generate_payment_state') @patch('src.main._create_payment_accounts_from_agg_balances') @patch('src.main._aggregate_worksheets_by_account') @patch('src.main._get_payment_minimums') @patch('src.main._batch_accounts') @patch('src.main.fetch_all_accounts') @patch('src.main._payment_group_payment') def test_old_handler_managed_group_success( mock_payment_group_payment: mock.MagicMock, mock_fetch_all_accounts: mock.MagicMock, mock_batch_accounts: mock.MagicMock, mock_get_payment_minimums: mock.MagicMock, mock_aggregate_worksheets_by_account: mock.MagicMock, mock_create_payment_accounts: mock.MagicMock, mock_get_generate_payment_state: mock.MagicMock, mock_update_state_status: mock.MagicMock, mock_fetch_all_payable_balance_after_tax_entries: mock.MagicMock, mock_get_payment_group: mock.MagicMock, mock_get_account_payment_hold: mock.MagicMock, mock_update_payment_allocations_flowthrough: mock.MagicMock, agreement_type_ids: List[int] | None, ) -> None: """Test main.old_handler. separate calculation""" reference_payment_type_id = 10 group_criteria: dict[str, Any] = { 'reference_payment_type_id': reference_payment_type_id, } if agreement_type_ids is not None: group_criteria['reference_agreement_types'] = agreement_type_ids payment_group = PaymentGroupFactory.build(group_criteria=group_criteria) payment_group_payment = PaymentGroupPaymentFactory.build( payment_group_id=payment_group.payment_group_id ) account_1 = AccountFactory.build(account_id=1) account_2 = AccountFactory.build(account_id=2) account_3 = AccountFactory.build(account_id=3) payment_group_payment_account = PaymentAccountInstanceFactory.build() all_accounts = [account_1, account_2, account_3] payment_holds = [ None, AccountPaymentHoldFactory.build(is_on_hold=False), AccountPaymentHoldFactory.build(is_on_hold=True), ] eligible_accounts = [account_1, account_2] entry1 = PayableBalanceAfterTaxFactory.build(account_id=account_1.account_id) entry2 = PayableBalanceAfterTaxFactory.build(account_id=account_2.account_id) payment_minimums = [PaymentMethodMinimumFactory.build()] event = EventFactory.build() state = AbacusStateFactory.build() account_balances1 = { account_1.account_id: AggregatedBalancesAfterTax( payable_amount_pre_tax=123.12, tax_withholding_amount=10.1, payable_amount_post_tax=113.02, ) } account_balances2 = { account_2.account_id: AggregatedBalancesAfterTax( payable_amount_pre_tax=1110.32, tax_withholding_amount=100.1, payable_amount_post_tax=1010.22, ) } mock_get_payment_group.return_value = payment_group mock_payment_group_payment.return_value = payment_group_payment mock_fetch_all_accounts.return_value = all_accounts mock_get_account_payment_hold.side_effect = payment_holds mock_fetch_all_payable_balance_after_tax_entries.return_value = [entry1, entry2] mock_get_payment_minimums.return_value = payment_minimums mock_aggregate_worksheets_by_account.side_effect = [ account_balances1, account_balances2, ] mock_create_payment_accounts.return_value = None mock_get_generate_payment_state.return_value = state mock_update_state_status.return_value = None mock_batch_accounts.return_value = [eligible_accounts] mock_create_payment_accounts.return_value = [payment_group_payment_account] result = main.old_handler(event.model_dump(), None) mock_get_payment_group.assert_called_once_with( payment_group_payment.payment_group_id ) mock_payment_group_payment.assert_called_once_with(event) mock_get_payment_minimums.assert_called_once() mock_fetch_all_accounts.assert_called_once_with( reference_payment_type_id=reference_payment_type_id, agreement_type_ids=agreement_type_ids, ) mock_fetch_all_payable_balance_after_tax_entries.assert_called_once_with(event) assert mock_aggregate_worksheets_by_account.call_args_list == [ call([entry1, entry2]), ] assert mock_create_payment_accounts.call_args_list == [ call( event, [account_1, account_2], payment_minimums, account_balances1, [entry1, entry2], ) ] mock_get_generate_payment_state.assert_called_once_with(event) mock_update_state_status.assert_has_calls( [ call(state, constants.ACTION_STATUS_RUNNING), call(state, constants.ACTION_STATUS_COMPLETE), ] ) assert mock_update_payment_allocations_flowthrough.call_args_list == [ call([payment_group_payment_account.payment_group_payment_account_id]) ] assert mock_get_account_payment_hold.call_args_list == [ call(account_1.account_id), call(account_2.account_id), call(account_3.account_id), ] assert result['statusCode'] == 200 assert result['statusDescription'] == 'Success: generating payments finished' @patch('src.main._update_payment_allocations_flowthrough') @patch('src.main._update_state_status') @patch('src.main._get_generate_payment_state') @patch('src.main._create_payment_accounts') @patch('src.main._calculate_account_balances_after_tax') @patch('src.main._get_payment_minimums') @patch('src.main._create_balance_entries_after_tax') @patch('src.main._get_closing_balance_entries') @patch('src.main._batch_accounts') @patch('src.main.get_eligible_accounts') @patch('src.main._payment_group_payment') def test_old_handler_not_valid_event( mock_payment_group_payment: mock.MagicMock, mock_get_eligible_accounts: mock.MagicMock, mock_batch_accounts: mock.MagicMock, mock_get_closing_balance_entries: mock.MagicMock, mock_create_balance_entries_after_tax: mock.MagicMock, mock_get_payment_minimums: mock.MagicMock, mock_calculate_account_balances_after_tax: mock.MagicMock, mock_create_payment_accounts: mock.MagicMock, mock_get_generate_payment_state: mock.MagicMock, mock_update_state_status: mock.MagicMock, mock_update_payment_allocations_flowthrough: mock.MagicMock, ) -> None: event_dict = {'not_existing_field': 'some_value'} response = main.old_handler(event_dict, None) mock_payment_group_payment.assert_not_called() mock_get_eligible_accounts.assert_not_called() mock_batch_accounts.assert_not_called() mock_get_closing_balance_entries.assert_not_called() mock_create_balance_entries_after_tax.assert_not_called() mock_get_payment_minimums.assert_not_called() mock_calculate_account_balances_after_tax.assert_not_called() mock_create_payment_accounts.assert_not_called() mock_get_generate_payment_state.assert_not_called() mock_update_state_status.assert_not_called() mock_update_payment_allocations_flowthrough.assert_not_called() mock_update_payment_allocations_flowthrough.assert_not_called() assert response['statusCode'] == 400 @patch('src.main._update_payment_allocations_flowthrough') @patch('src.main.get_payment_group') @patch('src.main._update_state_status') @patch('src.main._get_generate_payment_state') @patch('src.main._create_payment_accounts') @patch('src.main._calculate_account_balances_after_tax') @patch('src.main._get_payment_minimums') @patch('src.main._create_balance_entries_after_tax') @patch('src.main._get_closing_balance_entries') @patch('src.main._batch_accounts') @patch('src.main.get_eligible_accounts') @patch('src.main._payment_group_payment') def test_old_handler_success_no_eligible_accounts( mock_payment_group_payment: mock.MagicMock, get_get_eligible_accounts: mock.MagicMock, mock_batch_accounts: mock.MagicMock, mock_get_closing_balance_entries: mock.MagicMock, mock_create_balance_entries_after_tax: mock.MagicMock, mock_get_payment_minimums: mock.MagicMock, mock_calculate_account_balances_after_tax: mock.MagicMock, mock_create_payment_accounts: mock.MagicMock, mock_get_generate_payment_state: mock.MagicMock, mock_update_state_status: mock.MagicMock, mock_get_payment_group: mock.MagicMock, mock_update_payment_allocations_flowthrough: mock.MagicMock, ) -> None: """Test main.old_handler.""" payment_group = PaymentGroupFactory.build(group_criteria={}) payment_group_payment = PaymentGroupPaymentFactory.build( payment_group_id=payment_group.payment_group_id ) eligible_accounts: List[Account] = [] event = EventFactory.build() state = AbacusStateFactory.build() mock_get_payment_group.return_value = payment_group mock_get_generate_payment_state.return_value = state mock_update_state_status.return_value = None mock_payment_group_payment.return_value = payment_group_payment get_get_eligible_accounts.return_value = eligible_accounts result = main.old_handler(event.model_dump(), None) mock_get_payment_group.assert_called_once_with( payment_group_payment.payment_group_id ) mock_payment_group_payment.assert_called_once_with(event) get_get_eligible_accounts.assert_called_once_with( payment_group_payment.payment_group_id ) mock_batch_accounts.assert_not_called() mock_get_closing_balance_entries.assert_not_called() mock_create_balance_entries_after_tax.assert_not_called() mock_get_payment_minimums.assert_not_called() mock_calculate_account_balances_after_tax.assert_not_called() mock_create_payment_accounts.assert_not_called() mock_get_generate_payment_state.assert_called_once_with(event) mock_update_state_status.assert_has_calls( [ call(state, constants.ACTION_STATUS_RUNNING), call(state, constants.ACTION_STATUS_COMPLETE), ] ) mock_update_payment_allocations_flowthrough.assert_not_called() assert result['statusCode'] == 200 assert ( result['statusDescription'] == f'No accounts for payment_group_payment {payment_group_payment.payment_group_id}' ) @patch('src.main.Processor') @patch('src.main._update_state_status') @patch('src.main._get_generate_payment_state') @patch('src.main._payment_group_payment') def test_new_handler_success( mock_payment_group_payment: mock.MagicMock, mock_get_generate_payment_state: mock.MagicMock, mock_update_state_status: mock.MagicMock, mock_processor_cls: mock.MagicMock, ) -> None: """Test main.new_handler delegates to Processor and reports success.""" payment_group_payment = PaymentGroupPaymentFactory.build() event = EventFactory.build() state = AbacusStateFactory.build() mock_payment_group_payment.return_value = payment_group_payment mock_get_generate_payment_state.return_value = state result = main.new_handler(event.model_dump(), None) assert mock_payment_group_payment.call_args_list == [call(event)] assert mock_get_generate_payment_state.call_args_list == [call(event)] assert mock_processor_cls.call_args_list == [call()] assert mock_processor_cls.return_value.process.call_args_list == [call(event)] assert mock_update_state_status.call_args_list == [ call(state, constants.ACTION_STATUS_RUNNING), call(state, constants.ACTION_STATUS_COMPLETE), ] assert result['statusCode'] == 200 assert result['statusDescription'] == constants.SUCCESS_MSG @patch('src.main.Processor') @patch('src.main._update_state_status') @patch('src.main._get_generate_payment_state') @patch('src.main._payment_group_payment') def test_new_handler_not_valid_event( mock_payment_group_payment: mock.MagicMock, mock_get_generate_payment_state: mock.MagicMock, mock_update_state_status: mock.MagicMock, mock_processor_cls: mock.MagicMock, ) -> None: """Test main.new_handler returns 400 on invalid event.""" response = main.new_handler({'not_existing_field': 'some_value'}, None) assert not mock_payment_group_payment.called assert not mock_get_generate_payment_state.called assert not mock_update_state_status.called assert not mock_processor_cls.called assert response['statusCode'] == 400 assert response['statusDescription'] == 'INVALID_EVENT' @patch('src.main.Processor') @patch('src.main._update_state_status') @patch('src.main._get_generate_payment_state') @patch('src.main._payment_group_payment') def test_new_handler_no_payment_group_payment( mock_payment_group_payment: mock.MagicMock, mock_get_generate_payment_state: mock.MagicMock, mock_update_state_status: mock.MagicMock, mock_processor_cls: mock.MagicMock, ) -> None: """Test main.new_handler returns 400 when no payment_group_payment found.""" event = EventFactory.build() mock_payment_group_payment.return_value = None result = main.new_handler(event.model_dump(), None) assert mock_payment_group_payment.call_args_list == [call(event)] assert not mock_get_generate_payment_state.called assert not mock_update_state_status.called assert not mock_processor_cls.called assert result['statusCode'] == 400 assert result[ 'statusDescription' ] == constants.NO_PAYMENT_GROUP_PAYMENT_FOUND.format(event.target_id) @patch('src.main.Processor') @patch('src.main._update_state_status') @patch('src.main._get_generate_payment_state') @patch('src.main._payment_group_payment') def test_new_handler_processor_exception( mock_payment_group_payment: mock.MagicMock, mock_get_generate_payment_state: mock.MagicMock, mock_update_state_status: mock.MagicMock, mock_processor_cls: mock.MagicMock, ) -> None: """Test main.new_handler returns 500 and marks state as ERROR on Processor failure.""" payment_group_payment = PaymentGroupPaymentFactory.build() event = EventFactory.build() state = AbacusStateFactory.build() mock_payment_group_payment.return_value = payment_group_payment mock_get_generate_payment_state.return_value = state mock_processor_cls.return_value.process.side_effect = Exception('boom') result = main.new_handler(event.model_dump(), None) assert mock_processor_cls.return_value.process.call_args_list == [call(event)] assert mock_update_state_status.call_args_list == [ call(state, constants.ACTION_STATUS_RUNNING), call(state, constants.ACTION_STATUS_ERROR), ] assert result['statusCode'] == 500 assert result['statusDescription'] == 'Failed to process event: boom' @pytest.mark.parametrize( 'refactoring_enabled, expected_handler, other_handler', [ (True, 'new_handler', 'old_handler'), (False, 'old_handler', 'new_handler'), ], ) @patch('src.main.old_handler') @patch('src.main.new_handler') @patch('src.main.is_refactoring_enabled') def test_handler_dispatches_by_feature_flag( mock_is_refactoring_enabled: mock.MagicMock, mock_new_handler: mock.MagicMock, mock_old_handler: mock.MagicMock, refactoring_enabled: bool, expected_handler: str, other_handler: str, ) -> None: """Test main.handler dispatches to new/old handler based on the feature flag.""" mock_is_refactoring_enabled.return_value = refactoring_enabled event = {'foo': 'bar'} context = object() handlers = {'new_handler': mock_new_handler, 'old_handler': mock_old_handler} result = main.handler(event, context) assert handlers[expected_handler].call_args_list == [call(event, context)] assert not handlers[other_handler].called assert result is handlers[expected_handler].return_value @patch('src.main.logger') @patch('src.main.constants') def test_batch_account_ids( mock_constants: mock.MagicMock, mock_logger: mock.MagicMock ) -> None: """Test processor batches list of account_ids into smaller chunks.""" mock_constants.ACCOUNT_BATCH_SIZE = 2 account_ids = list(range(9)) accounts = [ AccountFactory.build(account_id=account_id) for account_id in account_ids ] # noqa: E501 res = main._batch_accounts(accounts) assert len(res) == 5 assert mock_logger.info.call_count == 1 mock_logger.info.assert_called_with(mock_constants.ACCOUNTS_BATCHES_MSG.format(2)) @patch('src.main.get_payment_group_payment') def test_payment_group_payment(mock_get_payment: mock.MagicMock) -> None: """Test main _payment_group_payment method.""" target_id = 1 event = EventFactory.build(target_id=target_id) expected_res = PaymentGroupPaymentFactory.build() mock_get_payment.return_value = expected_res res = main._payment_group_payment(event) mock_get_payment.assert_called_once_with(target_id) assert res == expected_res @patch('src.main._fetch_all_contract_closing_balance_entries') def test_get_closing_balance_entries( mock_get_contract_closing_balance_entries: mock.MagicMock, ) -> None: """Test main _get_closing_balance_entries method.""" statement_period_id = 1 event = EventFactory.build(statement_period_id=statement_period_id) account_1 = AccountFactory.build(account_id=1) eligible_accounts = [account_1] contract_close_balance = ContractCloseBalanceFactory.build(account_id=1) expected_res: List[ContractCloseBalance] = [contract_close_balance] mock_get_contract_closing_balance_entries.return_value = expected_res res = main._get_closing_balance_entries(event, eligible_accounts) mock_get_contract_closing_balance_entries.assert_called_once_with( statement_period_id, eligible_accounts ) # noqa: E501 assert res == expected_res @patch('src.main.get_contract_closing_balance_entries') def test_fetch_all_contract_closing_balance_entries( mock_get_contract_closing_balance_entries: mock.MagicMock, ) -> None: """Test main _fetch_all_contract_closing_balance_entries method to fetch several batches and returns them in one list.""" statement_period_id = 100 account_1 = AccountFactory.build(account_id=1) account_2 = AccountFactory.build(account_id=2) eligible_accounts = [account_1, account_2] contract_close_balance_1 = ContractCloseBalanceFactory.build(account_id=1) contract_close_balance_2 = ContractCloseBalanceFactory.build(account_id=2) first_batch: PaginatedContractCloseBalances = PaginatedContractCloseBalances( items=[contract_close_balance_1], total_count=constants.ACCOUNT_BATCH_SIZE + 1 ) second_batch: PaginatedContractCloseBalances = PaginatedContractCloseBalances( items=[contract_close_balance_2], total_count=1 ) mock_get_contract_closing_balance_entries.side_effect = [first_batch, second_batch] res = main._fetch_all_contract_closing_balance_entries( statement_period_id, eligible_accounts ) assert mock_get_contract_closing_balance_entries.call_args_list == [ call( statement_period_id, eligible_accounts, limit=constants.ACCOUNT_BATCH_SIZE, offset=0, ), call( statement_period_id, eligible_accounts, limit=constants.ACCOUNT_BATCH_SIZE, offset=constants.ACCOUNT_BATCH_SIZE, ), ] assert res == [contract_close_balance_1, contract_close_balance_2] @patch('src.main._fetch_all_contract_closing_balance_entries') def test_get_closing_balance_entries_with_prev_period( mock_get_contract_closing_balance_entries: mock.MagicMock, ) -> None: """Test main _get_closing_balance_entries method when entries exist for current period and for previous.""" statement_period_id = 100 event = EventFactory.build(statement_period_id=statement_period_id) account_1 = AccountFactory.build(account_id=1) account_2 = AccountFactory.build(account_id=2) eligible_accounts = [account_1, account_2] contract_close_balance_1 = ContractCloseBalanceFactory.build(account_id=1) contract_close_balance_2 = ContractCloseBalanceFactory.build(account_id=2) current_cb_entries: List[ContractCloseBalance] = [contract_close_balance_1] previous_cb_entries: List[ContractCloseBalance] = [contract_close_balance_2] mock_get_contract_closing_balance_entries.side_effect = [ current_cb_entries, previous_cb_entries, ] res = main._get_closing_balance_entries(event, eligible_accounts) assert mock_get_contract_closing_balance_entries.call_args_list == [ call(statement_period_id, eligible_accounts), call(statement_period_id - 1, [account_2]), ] assert res == [contract_close_balance_1, contract_close_balance_2] @patch('src.main._fetch_all_contract_closing_balance_entries') def test_get_closing_balance_entries_no_entries_prev_period( mock_get_contract_closing_balance_entries: mock.MagicMock, ) -> None: """Test main _get_closing_balance_entries method when entries exist for current period, but not for previous.""" statement_period_id = 100 event = EventFactory.build(statement_period_id=statement_period_id) account_1 = AccountFactory.build(account_id=1) account_2 = AccountFactory.build(account_id=2) eligible_accounts = [account_1, account_2] contract_close_balance_1 = ContractCloseBalanceFactory.build(account_id=1) current_cb_entries: List[ContractCloseBalance] = [contract_close_balance_1] previous_cb_entries: List[ContractCloseBalance] = [] mock_get_contract_closing_balance_entries.side_effect = [ current_cb_entries, previous_cb_entries, ] res = main._get_closing_balance_entries(event, eligible_accounts) assert mock_get_contract_closing_balance_entries.call_args_list == [ call(statement_period_id, eligible_accounts), call(statement_period_id - 1, [account_2]), ] assert res == current_cb_entries @patch('src.main._fetch_all_contract_closing_balance_entries') def test_get_closing_balance_entries_only_entries_prev_period( mock_get_contract_closing_balance_entries: mock.MagicMock, ) -> None: """Test main _get_closing_balance_entries method when entries don't exist for current period, but exist for previous one.""" statement_period_id = 100 event = EventFactory.build(statement_period_id=statement_period_id) account_1 = AccountFactory.build(account_id=100) account_2 = AccountFactory.build(account_id=2) eligible_accounts = [account_1, account_2] contract_close_balance_2 = ContractCloseBalanceFactory.build(account_id=2) current_cb_entries: List[ContractCloseBalance] = [] previous_cb_entries: List[ContractCloseBalance] = [contract_close_balance_2] mock_get_contract_closing_balance_entries.side_effect = [ current_cb_entries, previous_cb_entries, ] res = main._get_closing_balance_entries(event, eligible_accounts) assert mock_get_contract_closing_balance_entries.call_args_list == [ call(statement_period_id, eligible_accounts), call(statement_period_id - 1, eligible_accounts), ] assert res == [contract_close_balance_2] @patch('src.main._format_payable_balance_after_tax_entry') @patch('src.main.bulk_create_worksheet_contract_balance_after_tax') def test_create_balance_entries_after_tax( mock_bulk_create_worksheet_contract_balance_after_tax: mock.MagicMock, mock_format_payable_balance_after_tax_entry: mock.MagicMock, ) -> None: """Test main _create_balance_entries_after_tax method.""" statement_period_id = 1 event_id = 2 event = EventFactory.build( statement_period_id=statement_period_id, abacus_event_id=event_id ) mapping: Dict[int, str] = dict() account_1 = AccountFactory.build(account_id=1) account_2 = AccountFactory.build(account_id=2) eligible_accounts = [account_1, account_2] close_balance_entries = [ ContractCloseBalanceFactory.build(account_id=account_1.account_id), ContractCloseBalanceFactory.build(account_id=account_2.account_id), ] expected_res = 'some obj' balance_after_tax_entry = {'formatted_entry': 'ok'} mock_bulk_create_worksheet_contract_balance_after_tax.return_value = expected_res mock_format_payable_balance_after_tax_entry.return_value = balance_after_tax_entry main._create_balance_entries_after_tax( event, mapping, eligible_accounts, close_balance_entries ) # noqa: E501 mock_bulk_create_worksheet_contract_balance_after_tax.assert_called_once_with( event_id, statement_period_id, [balance_after_tax_entry, balance_after_tax_entry], ) mock_bulk_create_worksheet_contract_balance_after_tax.assert_called() @patch('src.main._get_country_of_tax_policy') @patch('src.main._get_country_of_tax_residence') def test_format_payable_balance_after_tax_entry( mock_get_country_of_tax_residence: mock.MagicMock, mock_get_country_of_tax_policy: mock.MagicMock, ) -> None: """Test main _format_payable_balance_after_tax_entry method.""" mapping: Dict[int, str] = dict() worksheet_account_contract_closing_balance_id = 123 contract_id = 100 amount = '100' currency_code = 'USD' account_1 = AccountFactory.build(account_id=1) account_2 = AccountFactory.build(account_id=2) eligible_accounts = [account_1, account_2] close_balance_entry = ContractCloseBalanceFactory.build( account_id=account_1.account_id, contract_id=contract_id, amount=amount, currency_code=currency_code, worksheet_account_contract_closing_balance_id=worksheet_account_contract_closing_balance_id, # noqa: E501 ) country_of_tax_policy = 'USA' country_of_tax_residence = 'GBR' mock_get_country_of_tax_policy.return_value = country_of_tax_policy mock_get_country_of_tax_residence.return_value = country_of_tax_residence expected_res = PayableBalanceAfterTax( account_id=1, contract_id=contract_id, payable_amount_pre_tax=amount, payable_amount_post_tax=amount, currency_code=currency_code, worksheet_account_contract_closing_balance_id=worksheet_account_contract_closing_balance_id, # noqa: E501 country_of_tax_policy=country_of_tax_policy, country_of_tax_residence=country_of_tax_residence, tax_withholding_amount=None, vat_amount=None, ) res = main._format_payable_balance_after_tax_entry( mapping, close_balance_entry, eligible_accounts ) # noqa: E501 mock_get_country_of_tax_policy.assert_called_once_with( close_balance_entry.reference_payment_entity_id, mapping ) mock_get_country_of_tax_residence.assert_called_once_with( close_balance_entry, eligible_accounts ) assert res == expected_res @patch('src.main._get_country_of_tax_policy') @patch('src.main._get_country_of_tax_residence') def test_format_payable_balance_after_tax_entry_negative_balance( mock_get_country_of_tax_residence: mock.MagicMock, mock_get_country_of_tax_policy: mock.MagicMock, ) -> None: """Test main _format_payable_balance_after_tax_entry method with negative balance.""" # noqa mapping: Dict[int, str] = dict() worksheet_account_contract_closing_balance_id = 123 contract_id = 100 amount = '-100' currency_code = 'USD' account_1 = AccountFactory.build(account_id=1) account_2 = AccountFactory.build(account_id=2) eligible_accounts = [account_1, account_2] close_balance_entry = ContractCloseBalanceFactory.build( account_id=account_1.account_id, contract_id=contract_id, amount=amount, currency_code=currency_code, worksheet_account_contract_closing_balance_id=worksheet_account_contract_closing_balance_id, # noqa: E501 ) country_of_tax_policy = 'USA' country_of_tax_residence = 'UK' mock_get_country_of_tax_policy.return_value = country_of_tax_policy mock_get_country_of_tax_residence.return_value = country_of_tax_residence expected_res = PayableBalanceAfterTax( account_id=1, contract_id=contract_id, payable_amount_pre_tax='0', payable_amount_post_tax='0', currency_code=currency_code, worksheet_account_contract_closing_balance_id=worksheet_account_contract_closing_balance_id, # noqa: E501 country_of_tax_policy=country_of_tax_policy, country_of_tax_residence=country_of_tax_residence, tax_withholding_amount=None, vat_amount=None, ) res = main._format_payable_balance_after_tax_entry( mapping, close_balance_entry, eligible_accounts ) # noqa: E501 mock_get_country_of_tax_policy.assert_called_once_with( close_balance_entry.reference_payment_entity_id, mapping ) mock_get_country_of_tax_residence.assert_called_once_with( close_balance_entry, eligible_accounts ) assert res == expected_res def test_get_country_of_tax_residence() -> None: """Test main _get_country_of_tax_residence method.""" account_1 = AccountFactory.build(account_id=1, country_of_tax_residence='USA') account_2 = AccountFactory.build(account_id=2, country_of_tax_residence='UK') eligible_accounts = [account_1, account_2] close_balance_entry = ContractCloseBalanceFactory.build( account_id=account_2.account_id ) expected_res = 'UK' res = main._get_country_of_tax_residence(close_balance_entry, eligible_accounts) assert res == expected_res def test_get_country_of_tax_policy_if_ref_payment_entity_exists() -> None: """Test main _get_country_of_tax_policy method for the case when needed payment_entity exists.""" # noqa: E501 reference_payment_entity_id = 10 payment_entity_policy_country_mapping = {25: 'UK', 10: 'USA'} expected_res = 'USA' res = main._get_country_of_tax_policy( reference_payment_entity_id, payment_entity_policy_country_mapping ) # noqa: E501 assert res == expected_res @patch('src.main.get_payment_entity') def test_get_country_of_tax_policy_if_ref_payment_entity_does_not_exist( mock_get_payment_entity: mock.MagicMock, ) -> None: """Test main _get_country_of_tax_policy method for the case when payment_entity fetched first time.""" # noqa: E501 reference_payment_entity_id = 10 payment_entity_policy_country_mapping = {25: 'UK'} payment_entity = PaymentEntityFactory.build( reference_payment_entity_id=reference_payment_entity_id, payment_entity_name='AWAL-US', ) mock_get_payment_entity.return_value = payment_entity expected_res = 'USA' res = main._get_country_of_tax_policy( reference_payment_entity_id, payment_entity_policy_country_mapping ) # noqa: E501 mock_get_payment_entity.assert_called_once_with(10) assert res == expected_res @patch('src.main.get_payment_minimums') def test_get_payment_minimums(mock_get_payment_minimums: mock.MagicMock) -> None: """Test main _get_payment_minimums method""" payment_minimum_1 = PaymentMethodMinimumFactory.build() payment_minimum_2 = PaymentMethodMinimumFactory.build() payment_minimums = [payment_minimum_1, payment_minimum_2] mock_get_payment_minimums.return_value = payment_minimums res = main._get_payment_minimums() mock_get_payment_minimums.assert_called_once() assert res == payment_minimums def test_calculate_account_balances_after_tax() -> None: """Test main _calculate_account_balances_after_tax to calculate sum balances correctly""" # noqa: E501 account_id_1 = 1 account_id_2 = 2 list_balances = [ PayableBalanceAfterTaxFactory.build( account_id=account_id_1, payable_amount_post_tax=Decimal(12.2) ), PayableBalanceAfterTaxFactory.build( account_id=account_id_1, payable_amount_post_tax=Decimal(23.1) ), PayableBalanceAfterTaxFactory.build( account_id=account_id_2, payable_amount_post_tax=Decimal(100) ), ] res = main._calculate_account_balances_after_tax(list_balances) assert res[account_id_1] == Decimal(12.2) + Decimal(23.1) assert res[account_id_2] == Decimal(100.0) @patch('src.main.get_abacus_states') def test_get_payments_generate_state(mock_get_abacus_states: mock.MagicMock) -> None: """Test getting payments_generate states by payment_group_payment_id(target_id).""" event = EventFactory.build(target_type='target_type_1', target_id=2) states = [ AbacusStateFactory.build(action_name='other_name', abacus_state_id=1), AbacusStateFactory.build(action_name='generate_payments', abacus_state_id=2), ] mock_get_abacus_states.return_value = states res = main._get_generate_payment_state(event) mock_get_abacus_states.assert_called_once_with('payment_group_payment', 2) assert res.abacus_state_id == 2 assert res.action_name == 'generate_payments' @patch('src.main.get_abacus_states') def test_get_payments_generate_states( mock_get_abacus_states: mock.MagicMock, ) -> None: """Test raising exception in case no state with payments_generate action_name.""" # noqa: E501 event = EventFactory.build(target_type='target_type_1', target_id=2) states = [AbacusStateFactory.build(action_name='other_name', abacus_state_id=1)] mock_get_abacus_states.return_value = states with pytest.raises(ValueError): main._get_generate_payment_state(event) mock_get_abacus_states.assert_called_once_with('payment_group_payment', 2) @patch('src.main.update_abacus_state_by_id') def test_update_state_status(mock_update_abacus_state_by_id: mock.MagicMock) -> None: """Test updating payments_generate state with new status by state_id.""" mock_update_abacus_state_by_id.return_value = None abacus_state_id = 1 status = 'some_status' state = AbacusStateFactory.build(abacus_state_id=abacus_state_id) main._update_state_status(state, status) mock_update_abacus_state_by_id.assert_called_once_with( abacus_state_id, {'action_status': status} ) @patch('src.main.get_bulk_last_payments') def test_get_last_payments_map_with_single_account( mock_get_bulk_last_payments: mock.MagicMock, ) -> None: """Test _get_last_payments_map returns correct mapping for single account.""" account_id = 1 accounts = [AccountFactory.build(account_id=account_id)] account_payment_details = AccountPaymentDetailsFactory.build(account_id=account_id) mock_get_bulk_last_payments.return_value = [account_payment_details] result = main._get_last_payments_map(accounts) mock_get_bulk_last_payments.assert_called_once_with( [account_id], limit=constants.ACCOUNT_BATCH_SIZE ) assert result == {account_id: account_payment_details} assert len(result) == 1 @patch('src.main.get_bulk_last_payments') def test_get_last_payments_map_with_multiple_accounts( mock_get_bulk_last_payments: mock.MagicMock, ) -> None: """Test _get_last_payments_map returns correct mapping for multiple accounts.""" account_id_1 = 1 account_id_2 = 2 account_id_3 = 3 accounts = [ AccountFactory.build(account_id=account_id_1), AccountFactory.build(account_id=account_id_2), AccountFactory.build(account_id=account_id_3), ] payment_details_1 = AccountPaymentDetailsFactory.build(account_id=account_id_1) payment_details_2 = AccountPaymentDetailsFactory.build(account_id=account_id_2) payment_details_3 = AccountPaymentDetailsFactory.build(account_id=account_id_3) mock_get_bulk_last_payments.return_value = [ payment_details_1, payment_details_2, payment_details_3, ] result = main._get_last_payments_map(accounts) mock_get_bulk_last_payments.assert_called_once_with( [ account_id_1, account_id_2, account_id_3, ], limit=constants.ACCOUNT_BATCH_SIZE, ) assert result == { account_id_1: payment_details_1, account_id_2: payment_details_2, account_id_3: payment_details_3, } assert len(result) == 3 @patch('src.main.get_bulk_last_payments') def test_get_last_payments_map_with_empty_accounts( mock_get_bulk_last_payments: mock.MagicMock, ) -> None: """Test _get_last_payments_map handles empty accounts list.""" accounts: List[Account] = [] mock_get_bulk_last_payments.return_value = [] result = main._get_last_payments_map(accounts) mock_get_bulk_last_payments.assert_called_once_with( [], limit=constants.ACCOUNT_BATCH_SIZE ) assert result == {} assert len(result) == 0 @patch('src.main.get_bulk_last_payments') def test_get_last_payments_map_filters_null_account_ids( mock_get_bulk_last_payments: mock.MagicMock, ) -> None: """Test _get_last_payments_map filters out payments with null account_id.""" account_id_1 = 1 account_id_2 = 2 accounts = [ AccountFactory.build(account_id=account_id_1), AccountFactory.build(account_id=account_id_2), ] payment_details_1 = AccountPaymentDetailsFactory.build(account_id=account_id_1) payment_details_null = AccountPaymentDetailsFactory.build(account_id=None) payment_details_2 = AccountPaymentDetailsFactory.build(account_id=account_id_2) mock_get_bulk_last_payments.return_value = [ payment_details_1, payment_details_null, payment_details_2, ] result = main._get_last_payments_map(accounts) mock_get_bulk_last_payments.assert_called_once_with( [account_id_1, account_id_2], limit=constants.ACCOUNT_BATCH_SIZE ) assert result == { account_id_1: payment_details_1, account_id_2: payment_details_2, } assert len(result) == 2 assert None not in result @patch('src.main.get_bulk_last_payments') def test_get_last_payments_map_returns_correct_type( mock_get_bulk_last_payments: mock.MagicMock, ) -> None: """Test _get_last_payments_map returns Dict[int, AccountPaymentDetails].""" account_id = 123 accounts = [AccountFactory.build(account_id=account_id)] account_payment_details = AccountPaymentDetailsFactory.build(account_id=account_id) mock_get_bulk_last_payments.return_value = [account_payment_details] result = main._get_last_payments_map(accounts) assert isinstance(result, dict) assert all(isinstance(k, int) for k in result.keys()) assert all(isinstance(v, AccountPaymentDetails) for v in result.values()) @patch('src.main.bulk_create_payment_accounts') @patch('src.main._check_balance_limit') @patch('src.main._format_payment_account') @patch('src.main.get_bulk_last_payments') def test_create_payment_accounts_with_valid_balance( mock_get_bulk_last_payments: mock.MagicMock, mock_format_payment_account: mock.MagicMock, mock_check_balance_limit: mock.MagicMock, mock_bulk_create_payment_accounts: mock.MagicMock, ) -> None: event = EventFactory.build(target_type='target_type_1', target_id=2) account_id = 1 accounts = [AccountFactory.build(account_id=account_id)] account_balances = {account_id: Decimal(22.2)} payment_method_minimums = [PaymentMethodMinimumFactory.build()] payment_account = PaymentAccountFactory.build() payable_balance_after_tax_entries = [PayableBalanceAfterTaxFactory.build()] account_payment_details = AccountPaymentDetailsFactory.build(account_id=account_id) mock_get_bulk_last_payments.return_value = [account_payment_details] mock_format_payment_account.return_value = payment_account mock_check_balance_limit.return_value = True main._create_payment_accounts( event, accounts, payment_method_minimums, account_balances, payable_balance_after_tax_entries, ) mock_get_bulk_last_payments.assert_called_once_with( [account_id], limit=constants.ACCOUNT_BATCH_SIZE ) mock_format_payment_account.assert_called_once_with( accounts[0], account_balances, payable_balance_after_tax_entries, {account_id: account_payment_details}, ) mock_bulk_create_payment_accounts.assert_called_once_with( event.target_id, [payment_account] ) mock_bulk_create_payment_accounts.assert_called_once_with(2, [payment_account]) @patch('src.main.bulk_create_payment_accounts') @patch('src.main._check_balance_limit') @patch('src.main._format_payment_account') @patch('src.main.get_bulk_last_payments') def test_create_payment_accounts_with_invalid_entities( mock_get_bulk_last_payments: mock.MagicMock, mock_format_payment_account: mock.MagicMock, mock_check_balance_limit: mock.MagicMock, mock_bulk_create_payment_accounts: mock.MagicMock, ) -> None: event = EventFactory.build(target_type='target_type_1', target_id=2) account_id = 1 accounts = [AccountFactory.build(account_id=account_id)] account_balances = {account_id: Decimal(22.2)} payment_method_minimums = [PaymentMethodMinimumFactory.build()] payment_account = PaymentAccountFactory.build() payable_balance_after_tax_entries = [PayableBalanceAfterTaxFactory.build()] account_payment_details = AccountPaymentDetailsFactory.build(account_id=account_id) mock_get_bulk_last_payments.return_value = [account_payment_details] mock_format_payment_account.return_value = payment_account mock_check_balance_limit.return_value = False main._create_payment_accounts( event, accounts, payment_method_minimums, account_balances, payable_balance_after_tax_entries, ) mock_get_bulk_last_payments.assert_called_once_with( [account_id], limit=constants.ACCOUNT_BATCH_SIZE ) mock_format_payment_account.assert_not_called() mock_bulk_create_payment_accounts.assert_not_called() @patch('src.main.bulk_create_payment_accounts') @patch('src.main._check_agg_balance_limit') @patch('src.main._format_payment_account_from_agg_balances') @patch('src.main.get_bulk_last_payments') def test_create_payment_accounts_from_agg_balances_with_valid_entities( mock_get_bulk_last_payments: mock.MagicMock, mock_format_payment_account_from_agg_balances: mock.MagicMock, mock_check_agg_balance_limit: mock.MagicMock, mock_bulk_create_payment_accounts: mock.MagicMock, ) -> None: event = EventFactory.build(target_type='target_type_1', target_id=2) account_id = 1 accounts = [AccountFactory.build(account_id=account_id)] account_balances = { account_id: AggregatedBalancesAfterTax( payable_amount_pre_tax=123.12, tax_withholding_amount=10.1, payable_amount_post_tax=113.02, ) } payment_method_minimums = [PaymentMethodMinimumFactory.build()] payment_account = PaymentAccountFactory.build() payable_balance_after_tax_entries = [PayableBalanceAfterTaxFactory.build()] account_payment_details = AccountPaymentDetailsFactory.build(account_id=account_id) mock_get_bulk_last_payments.return_value = [account_payment_details] mock_format_payment_account_from_agg_balances.return_value = payment_account mock_check_agg_balance_limit.return_value = True main._create_payment_accounts_from_agg_balances( event, accounts, payment_method_minimums, account_balances, payable_balance_after_tax_entries, ) mock_get_bulk_last_payments.assert_called_once_with( [account_id], limit=constants.ACCOUNT_BATCH_SIZE ) mock_format_payment_account_from_agg_balances.assert_called_once_with( accounts[0], account_balances, payable_balance_after_tax_entries, {account_id: account_payment_details}, ) mock_bulk_create_payment_accounts.assert_called_once_with( event.target_id, [payment_account] ) mock_bulk_create_payment_accounts.assert_called_once_with(2, [payment_account]) @patch('src.main.bulk_create_payment_accounts') @patch('src.main._check_agg_balance_limit') @patch('src.main._format_payment_account_from_agg_balances') @patch('src.main.get_bulk_last_payments') def test_create_payment_accounts_from_agg_balances_with_invalid_entities( mock_get_bulk_last_payments: mock.MagicMock, mock_format_payment_account_from_agg_balances: mock.MagicMock, mock_check_agg_balance_limit: mock.MagicMock, mock_bulk_create_payment_accounts: mock.MagicMock, ) -> None: event = EventFactory.build(target_type='target_type_1', target_id=2) account_id = 1 accounts = [AccountFactory.build(account_id=account_id)] account_balances = { account_id: AggregatedBalancesAfterTax( payable_amount_pre_tax=123.12, tax_withholding_amount=10.1, payable_amount_post_tax=113.02, ) } payment_method_minimums = [PaymentMethodMinimumFactory.build()] payment_account = PaymentAccountFactory.build() payable_balance_after_tax_entries = [PayableBalanceAfterTaxFactory.build()] account_payment_details = AccountPaymentDetailsFactory.build(account_id=account_id) mock_get_bulk_last_payments.return_value = [account_payment_details] mock_format_payment_account_from_agg_balances.return_value = payment_account mock_check_agg_balance_limit.return_value = False main._create_payment_accounts_from_agg_balances( event, accounts, payment_method_minimums, account_balances, payable_balance_after_tax_entries, ) mock_get_bulk_last_payments.assert_called_once_with( [account_id], limit=constants.ACCOUNT_BATCH_SIZE ) mock_format_payment_account_from_agg_balances.assert_not_called() mock_bulk_create_payment_accounts.assert_not_called() def test_check_balance_limit_success() -> None: account = AccountFactory.build( account_id=1, currency_code='USD', payment_minimum='10' ) account_balances = {1: Decimal(22.2)} payment_method_minimums = [ PaymentMethodMinimumFactory.build( currency_code='USD', wire_transfer_amount='10', check_amount='20' ), PaymentMethodMinimumFactory.build( currency_code='EUR', wire_transfer_amount='10', check_amount='20' ), ] is_valid = main._check_balance_limit( account, payment_method_minimums, account_balances ) # noqa: E501 assert is_valid def test_check_balance_limit_success_default_payment_method() -> None: account = AccountFactory.build( account_id=1, currency_code='USD', payment_minimum='10' ) account_balances = {1: Decimal(22.2)} payment_method_minimums = [ PaymentMethodMinimumFactory.build( currency_code='USD', wire_transfer_amount='10', check_amount='5' ), PaymentMethodMinimumFactory.build( currency_code='EUR', wire_transfer_amount='10', check_amount='20' ), ] is_valid = main._check_balance_limit( account, payment_method_minimums, account_balances ) # noqa: E501 assert is_valid def test_check_balance_limit_not_exist_in_account_balances() -> None: account = AccountFactory.build( account_id=2, currency_code='USD', payment_minimum='10' ) account_balances = {1: Decimal(22.2)} payment_method_minimums = [ PaymentMethodMinimumFactory.build(currency_code='USD', check_amount='20'), PaymentMethodMinimumFactory.build(currency_code='EUR', check_amount='20'), ] is_valid = main._check_balance_limit( account, payment_method_minimums, account_balances ) # noqa: E501 assert not is_valid def test_check_balance_limit_less_account_payment_minimum() -> None: account = AccountFactory.build( account_id=1, currency_code='USD', payment_minimum='100' ) account_balances = {1: Decimal(22.2)} payment_method_minimums = [ PaymentMethodMinimumFactory.build(currency_code='USD', check_amount='20'), PaymentMethodMinimumFactory.build(currency_code='EUR', check_amount='20'), ] is_valid = main._check_balance_limit( account, payment_method_minimums, account_balances ) # noqa: E501 assert not is_valid def test_check_balance_limit_less_payment_method_min() -> None: account = AccountFactory.build( account_id=1, currency_code='USD', payment_minimum='10' ) account_balances = {1: Decimal(22.2)} payment_method_minimums = [ PaymentMethodMinimumFactory.build(currency_code='USD', check_amount='100'), PaymentMethodMinimumFactory.build(currency_code='EUR', check_amount='100'), ] is_valid = main._check_balance_limit( account, payment_method_minimums, account_balances ) # noqa: E501 assert not is_valid def test_check_balance_limit_less_check_amount_if_no_payment_method() -> None: account = AccountFactory.build( account_id=1, currency_code='USD', payment_minimum='10' ) account_balances = {1: Decimal(22.2)} payment_method_minimums = [ PaymentMethodMinimumFactory.build( currency_code='USD', wire_transfer_amount='10', check_amount='200' ), PaymentMethodMinimumFactory.build( currency_code='EUR', wire_transfer_amount='10', check_amount='20' ), ] is_valid = main._check_balance_limit( account, payment_method_minimums, account_balances ) # noqa: E501 assert not is_valid def test_check_agg_balance_limit_success() -> None: account = AccountFactory.build( account_id=1, currency_code='USD', payment_minimum='10' ) account_balances = { 1: AggregatedBalancesAfterTax( payable_amount_pre_tax=22.2, tax_withholding_amount=2.1, payable_amount_post_tax=20.1, ) } payment_method_minimums = [ PaymentMethodMinimumFactory.build( currency_code='USD', wire_transfer_amount='10', check_amount='20' ), PaymentMethodMinimumFactory.build( currency_code='EUR', wire_transfer_amount='10', check_amount='20' ), ] is_valid = main._check_agg_balance_limit( account, payment_method_minimums, account_balances ) # noqa: E501 assert is_valid def test_check_agg_balance_limit_success_default_payment_method() -> None: account = AccountFactory.build( account_id=1, currency_code='USD', payment_minimum='10' ) account_balances = { 1: AggregatedBalancesAfterTax( payable_amount_pre_tax=22.2, tax_withholding_amount=2.1, payable_amount_post_tax=20.1, ) } payment_method_minimums = [ PaymentMethodMinimumFactory.build( currency_code='USD', wire_transfer_amount='10', check_amount='5' ), PaymentMethodMinimumFactory.build( currency_code='EUR', wire_transfer_amount='10', check_amount='20' ), ] is_valid = main._check_agg_balance_limit( account, payment_method_minimums, account_balances ) # noqa: E501 assert is_valid def test_check_agg_balance_limit_not_exist_in_account_balances() -> None: account = AccountFactory.build( account_id=2, currency_code='USD', payment_minimum='10' ) account_balances = { 1: AggregatedBalancesAfterTax( payable_amount_pre_tax=22.2, tax_withholding_amount=2.1, payable_amount_post_tax=20.1, ) } payment_method_minimums = [ PaymentMethodMinimumFactory.build(currency_code='USD', check_amount='20'), PaymentMethodMinimumFactory.build(currency_code='EUR', check_amount='20'), ] is_valid = main._check_agg_balance_limit( account, payment_method_minimums, account_balances ) # noqa: E501 assert not is_valid def test_check_agg_balance_limit_less_account_payment_minimum() -> None: account = AccountFactory.build( account_id=1, currency_code='USD', payment_minimum='100' ) account_balances = { 1: AggregatedBalancesAfterTax( payable_amount_pre_tax=22.2, tax_withholding_amount=2.1, payable_amount_post_tax=20.1, ) } payment_method_minimums = [ PaymentMethodMinimumFactory.build(currency_code='USD', check_amount='20'), PaymentMethodMinimumFactory.build(currency_code='EUR', check_amount='20'), ] is_valid = main._check_agg_balance_limit( account, payment_method_minimums, account_balances ) # noqa: E501 assert not is_valid def test_check_agg_balance_limit_less_payment_method_min() -> None: account = AccountFactory.build( account_id=1, currency_code='USD', payment_minimum='10' ) account_balances = { 1: AggregatedBalancesAfterTax( payable_amount_pre_tax=22.2, tax_withholding_amount=2.1, payable_amount_post_tax=20.1, ) } payment_method_minimums = [ PaymentMethodMinimumFactory.build(currency_code='USD', check_amount='100'), PaymentMethodMinimumFactory.build(currency_code='EUR', check_amount='100'), ] is_valid = main._check_agg_balance_limit( account, payment_method_minimums, account_balances ) # noqa: E501 assert not is_valid def test_check_agg_balance_limit_less_check_amount_if_no_payment_method() -> None: account = AccountFactory.build( account_id=1, currency_code='USD', payment_minimum='10' ) account_balances = { 1: AggregatedBalancesAfterTax( payable_amount_pre_tax=22.2, tax_withholding_amount=2.1, payable_amount_post_tax=20.1, ) } payment_method_minimums = [ PaymentMethodMinimumFactory.build( currency_code='USD', wire_transfer_amount='10', check_amount='200' ), PaymentMethodMinimumFactory.build( currency_code='EUR', wire_transfer_amount='10', check_amount='20' ), ] is_valid = main._check_agg_balance_limit( account, payment_method_minimums, account_balances ) # noqa: E501 assert not is_valid @patch('src.main._get_account_contracts_payable') def test_format_payment_account( mock_get_account_contracts_payable: mock.MagicMock, ) -> None: account_id = 1 account_balances = {account_id: Decimal(22.2)} payment_details = AccountPaymentDetailsFactory.build( account_id=account_id, balance_after_tax=Decimal(2.22) ) last_payments_map = {account_id: payment_details} account = AccountFactory.build(account_id=account_id) payable_balance_after_tax_entries = [PayableBalanceAfterTaxFactory.build()] account_contracts = [AccountPayableContractFactory.build()] mock_get_account_contracts_payable.return_value = account_contracts res = main._format_payment_account( account, account_balances, payable_balance_after_tax_entries, last_payments_map ) assert res.contracts_payable == account_contracts assert res.currency_code == account.currency_code assert res.current_balance == res.balance_after_tax == Decimal(22.2) assert res.account_id == account.account_id assert res.payoneer_program_id == (account.payoneer_program_id or 0) assert res.last_payment == payment_details.balance_after_tax assert res.last_statement_period_id == payment_details.current_statement_period_id assert res.tax_withholding is None @patch('src.main._get_account_contracts_payable') def test_format_payment_account_from_agg_balances( mock_get_account_contracts_payable: mock.MagicMock, ) -> None: account_id = 1 account_balances = { account_id: AggregatedBalancesAfterTax( payable_amount_pre_tax=Decimal(22.2), tax_withholding_amount=Decimal(2.1), payable_amount_post_tax=Decimal(20.1), ) } payment_details = AccountPaymentDetailsFactory.build( account_id=account_id, balance_after_tax=Decimal(2.22) ) last_payments_map = {account_id: payment_details} account = AccountFactory.build(account_id=account_id) payable_balance_after_tax_entries = [PayableBalanceAfterTaxFactory.build()] account_contracts = [AccountPayableContractFactory.build()] mock_get_account_contracts_payable.return_value = account_contracts res = main._format_payment_account_from_agg_balances( account, account_balances, payable_balance_after_tax_entries, last_payments_map ) assert res.contracts_payable == account_contracts assert res.currency_code == account.currency_code assert res.current_balance == Decimal(22.2) assert res.balance_after_tax == Decimal(20.1) assert res.account_id == account.account_id assert res.payoneer_program_id == (account.payoneer_program_id or 0) assert res.last_payment == payment_details.balance_after_tax assert res.last_statement_period_id == payment_details.current_statement_period_id assert res.tax_withholding == Decimal(2.1) @patch('src.main._get_account_contracts_payable') def test_format_payment_account_missing_last_payment( mock_get_account_contracts_payable: mock.MagicMock, ) -> None: """Test that _format_payment_account creates default payment details when account_id not in last_payments_map.""" account_id = 1 account_balances = {account_id: Decimal(22.2)} last_payments_map: Dict[ int, AccountPaymentDetails ] = {} # Empty map, account_id not present account = AccountFactory.build(account_id=account_id) payable_balance_after_tax_entries = [PayableBalanceAfterTaxFactory.build()] account_contracts = [AccountPayableContractFactory.build()] mock_get_account_contracts_payable.return_value = account_contracts res = main._format_payment_account( account, account_balances, payable_balance_after_tax_entries, last_payments_map, ) # Verify default payment details are used assert res.last_payment == Decimal(0.0) assert res.last_statement_period_id is None assert res.account_id == account_id assert res.contracts_payable == account_contracts @patch('src.main._get_account_contracts_payable') def test_format_payment_account_from_agg_balances_missing_last_payment( mock_get_account_contracts_payable: mock.MagicMock, ) -> None: """Test that _format_payment_account_from_agg_balances creates default payment details when account_id not in last_payments_map.""" account_id = 1 account_balances = { account_id: AggregatedBalancesAfterTax( payable_amount_pre_tax=Decimal(22.2), tax_withholding_amount=Decimal(2.1), payable_amount_post_tax=Decimal(20.1), ) } last_payments_map: Dict[ int, AccountPaymentDetails ] = {} # Empty map, account_id not present account = AccountFactory.build(account_id=account_id) payable_balance_after_tax_entries = [PayableBalanceAfterTaxFactory.build()] account_contracts = [AccountPayableContractFactory.build()] mock_get_account_contracts_payable.return_value = account_contracts res = main._format_payment_account_from_agg_balances( account, account_balances, payable_balance_after_tax_entries, last_payments_map, ) # Verify default payment details are used assert res.last_payment == Decimal(0.0) assert res.last_statement_period_id is None assert res.account_id == account_id assert res.contracts_payable == account_contracts def test_get_account_contracts_payable() -> None: account_id = 1 account = AccountFactory.build(account_id=account_id) payable_balance_exp = PayableBalanceAfterTaxFactory.build( account_id=account_id, contract_id=1, payable_amount_post_tax=Decimal(300.00) ) payable_balance_after_tax_entries = [ payable_balance_exp, PayableBalanceAfterTaxFactory.build( account_id=account_id, contract_id=2, payable_amount_post_tax=Decimal(0.0) ), PayableBalanceAfterTaxFactory.build( account_id=2, contract_id=3, payable_amount_post_tax=Decimal(300.00) ), ] res = main._get_account_contracts_payable( account, payable_balance_after_tax_entries ) assert len(res) == 1 assert res[0].contract_id == payable_balance_exp.contract_id assert res[0].currency_code == payable_balance_exp.currency_code assert res[0].current_balance == payable_balance_exp.payable_amount_post_tax def test_get_account_contracts_payable_not_found() -> None: account_id = 1 account = AccountFactory.build(account_id=account_id) payable_balance_after_tax_entries = [ PayableBalanceAfterTaxFactory.build(account_id=2) ] res = main._get_account_contracts_payable( account, payable_balance_after_tax_entries ) assert len(res) == 0 @patch('src.main.get_events_by_target_type') def test_get_related_calculate_payments_event(mock_get_events: mock.MagicMock) -> None: calculate_payments_event = EventFactory.build( event_name=constants.CALCULATE_PAYMENTS_ACTION_NAME, ) generate_payments_event = EventFactory.build( event_name=constants.GENERATE_PAYMENTS_ACTION_NAME, ) mock_get_events.return_value = [calculate_payments_event] res = main._get_related_calculate_payments_event(generate_payments_event) assert mock_get_events.call_args_list == [ call(generate_payments_event.target_type, generate_payments_event.target_id) ] assert res == calculate_payments_event def test_aggregate_worksheets_by_account_none_taxes() -> None: """Test null wht and vat aggregation.""" account_id = 1 balance_after_tax_entries = [ PayableBalanceAfterTaxFactory.build( account_id=account_id, payable_amount_pre_tax=Decimal(100.00), tax_withholding_amount=None, vat_amount=None, payable_amount_post_tax=Decimal(100.00), ), PayableBalanceAfterTaxFactory.build( account_id=account_id, payable_amount_pre_tax=Decimal(100.00), tax_withholding_amount=None, vat_amount=None, payable_amount_post_tax=Decimal(100.00), ), ] res = main._aggregate_worksheets_by_account(balance_after_tax_entries) assert res[1].payable_amount_pre_tax == Decimal(200.00) assert res[1].payable_amount_post_tax == Decimal(200.00) assert res[1].tax_withholding_amount is None assert res[1].vat_amount is None def test_aggregate_worksheets_by_account_success() -> None: """Test null wht and vat aggregation.""" account_id = 1 account_id_alt = 2 balance_after_tax_entries = [ PayableBalanceAfterTaxFactory.build( account_id=account_id, payable_amount_pre_tax=Decimal(100.00), tax_withholding_amount=None, vat_amount=None, payable_amount_post_tax=Decimal(100.00), ), PayableBalanceAfterTaxFactory.build( account_id=account_id, payable_amount_pre_tax=Decimal(100.00), tax_withholding_amount=Decimal(-10.00), vat_amount=Decimal(0.0), payable_amount_post_tax=Decimal(90.00), ), PayableBalanceAfterTaxFactory.build( account_id=account_id_alt, payable_amount_pre_tax=Decimal(100.00), tax_withholding_amount=Decimal(-10.00), vat_amount=Decimal(10.0), payable_amount_post_tax=Decimal(100.00), ), PayableBalanceAfterTaxFactory.build( account_id=account_id_alt, payable_amount_pre_tax=Decimal(100.00), tax_withholding_amount=Decimal(-10.00), vat_amount=Decimal(10.0), payable_amount_post_tax=Decimal(100.00), ), ] res = main._aggregate_worksheets_by_account(balance_after_tax_entries) mock_account_res = res[account_id] assert mock_account_res.payable_amount_pre_tax == Decimal(200.00) assert mock_account_res.payable_amount_post_tax == Decimal(190.00) assert mock_account_res.tax_withholding_amount == Decimal(-10.00) assert mock_account_res.vat_amount == Decimal(0.0) mock_account_alt_res = res[account_id_alt] assert mock_account_alt_res.payable_amount_pre_tax == Decimal(200.00) assert mock_account_alt_res.payable_amount_post_tax == Decimal(200.00) assert mock_account_alt_res.tax_withholding_amount == Decimal(-20.00) assert mock_account_alt_res.vat_amount == Decimal(20.0) @patch('src.main.bulk_update_payment_allocations_flowthrough') @patch('src.main._fetch_all_allocations_flowthrough_entries') @patch('src.main._fetch_all_payable_details_entries') def test_update_payment_allocations_flowthrough_batches_updates( mock_fetch_payable_details: mock.MagicMock, mock_fetch_allocations: mock.MagicMock, mock_bulk_update: mock.MagicMock, ) -> None: """Test _update_payment_allocations_flowthrough.""" batch_size = constants.ACCOUNT_BATCH_SIZE payment_accounts_ids = list(range(1, batch_size + 2)) payable_details = PayableDetailsFactory.batch(batch_size + 1) mock_fetch_payable_details.return_value = payable_details allocations = PaymentAllocationFlowthroughFactory.batch(batch_size + 1) mock_fetch_allocations.return_value = allocations main._update_payment_allocations_flowthrough(payment_accounts_ids) mock_fetch_payable_details.assert_called_once_with(payment_accounts_ids) mock_fetch_allocations.assert_called_once_with( contract_ids=list({d.contract_id for d in payable_details}), payment_statuses=[ constants.PaymentAllocationStatuses.INIT, constants.PaymentAllocationStatuses.RETURNED, ], ) assert mock_bulk_update.call_count == 2 all_payloads = [] for call_args in mock_bulk_update.call_args_list: batch_payloads = call_args.args[0] all_payloads.extend(batch_payloads) assert {p.payment_allocation_id for p in all_payloads} == { a.payment_allocation_id for a in allocations } assert all( p.payment_status == constants.PaymentAllocationStatuses.ATTACHED_TO_PAYMENT for p in all_payloads ) assert all( p.ledger_status == constants.PaymentAllocationLedgerStatuses.ATTACHED_TO_PAYMENT for p in all_payloads ) @patch('src.main.bulk_update_payment_allocations_flowthrough') @patch('src.main._fetch_all_allocations_flowthrough_entries') @patch('src.main._fetch_all_payable_details_entries') def test_update_payment_allocations_flowthrough_no_accounts( mock_fetch_payable_details: mock.MagicMock, mock_fetch_allocations: mock.MagicMock, mock_bulk_update: mock.MagicMock, ) -> None: """Test _update_payment_allocations_flowthrough with no accounts.""" mock_fetch_payable_details.return_value = [] mock_fetch_allocations.return_value = [] main._update_payment_allocations_flowthrough([]) mock_fetch_payable_details.assert_not_called() mock_fetch_allocations.assert_not_called() mock_bulk_update.assert_not_called() @patch('src.main.bulk_update_payment_allocations_flowthrough') @patch('src.main._fetch_all_allocations_flowthrough_entries') @patch('src.main._fetch_all_payable_details_entries') def test_update_payment_allocations_flowthrough_no_details( mock_fetch_payable_details: mock.MagicMock, mock_fetch_allocations: mock.MagicMock, mock_bulk_update: mock.MagicMock, ) -> None: """Test _update_payment_allocations_flowthrough with no details.""" payment_accounts_ids = [10] mock_fetch_payable_details.return_value = [] mock_fetch_allocations.return_value = [] main._update_payment_allocations_flowthrough(payment_accounts_ids) mock_fetch_payable_details.assert_called_once_with(payment_accounts_ids) mock_fetch_allocations.assert_not_called() mock_bulk_update.assert_not_called() @patch('src.main.bulk_update_payment_allocations_flowthrough') @patch('src.main._fetch_all_allocations_flowthrough_entries') @patch('src.main._fetch_all_payable_details_entries') def test_update_payment_allocations_flowthrough_no_allocations( mock_fetch_payable_details: mock.MagicMock, mock_fetch_allocations: mock.MagicMock, mock_bulk_update: mock.MagicMock, ) -> None: """Test _update_payment_allocations_flowthrough with no allocations.""" payment_accounts_ids = [10] payable_details = [PayableDetailsFactory.build(contract_id=100)] mock_fetch_payable_details.return_value = payable_details mock_fetch_allocations.return_value = [] main._update_payment_allocations_flowthrough(payment_accounts_ids) mock_fetch_payable_details.assert_called_once_with(payment_accounts_ids) mock_fetch_allocations.assert_called_once_with( contract_ids=[100], payment_statuses=[ constants.PaymentAllocationStatuses.INIT, constants.PaymentAllocationStatuses.RETURNED, ], ) mock_bulk_update.assert_not_called() @patch('src.main.bulk_update_payment_allocations_flowthrough') @patch('src.main._fetch_all_allocations_flowthrough_entries') @patch('src.main._fetch_all_payable_details_entries') def test_update_payment_allocations_flowthrough_deduplicates_contract_ids( mock_fetch_payable_details: mock.MagicMock, mock_fetch_allocations: mock.MagicMock, mock_bulk_update: mock.MagicMock, ) -> None: """Test _update_payment_allocations_flowthrough deduplicates contract_ids before sending.""" payment_accounts_ids = [10, 11] # Two payable_details with the same contract_id (> 1 allocation per account) payable_details = [ PayableDetailsFactory.build(contract_id=100), PayableDetailsFactory.build(contract_id=100), ] mock_fetch_payable_details.return_value = payable_details mock_fetch_allocations.return_value = [] main._update_payment_allocations_flowthrough(payment_accounts_ids) mock_fetch_payable_details.assert_called_once_with(payment_accounts_ids) mock_fetch_allocations.assert_called_once_with( contract_ids=[100], payment_statuses=[ constants.PaymentAllocationStatuses.INIT, constants.PaymentAllocationStatuses.RETURNED, ], ) mock_bulk_update.assert_not_called()