"""Tests for BatchFlowthroughProcessor.""" from unittest import mock from unittest.mock import call, patch from src.connectors.ows_payment import get_bulk_payable_details from src.constants import ( ACCOUNT_BATCH_SIZE, FLOWTHROUGH_PAYABLE_DETAIL_TYPE_ID, PaymentAllocationLedgerStatuses, PaymentAllocationStatuses, ) from src.models import PaymentAllocationFlowthroughUpdate from src.processors.batch_flowthrough_processor import BatchFlowthroughProcessor from tests.unit.factories import ( PayableDetailsFactory, PaymentAllocationFlowthroughFactory, ) @patch( 'src.processors.batch_flowthrough_processor.bulk_update_payment_allocations_flowthrough' ) @patch( 'src.processors.batch_flowthrough_processor.get_bulk_payment_allocations_flowthrough' ) @patch('src.processors.batch_flowthrough_processor.get_bulk_payable_details') def test_process_empty_payment_accounts_ids_skips_everything( mock_get_payable_details: mock.MagicMock, mock_get_allocations: mock.MagicMock, mock_bulk_update: mock.MagicMock, ) -> None: """Empty input short-circuits before any connector call.""" BatchFlowthroughProcessor().process([]) assert not mock_get_payable_details.called assert not mock_get_allocations.called assert not mock_bulk_update.called @patch( 'src.processors.batch_flowthrough_processor.bulk_update_payment_allocations_flowthrough' ) @patch('src.processors.batch_flowthrough_processor.fetch_all') def test_process_no_payable_details_skips_update( mock_fetch_all: mock.MagicMock, mock_bulk_update: mock.MagicMock, ) -> None: """No payable details -> allocations left empty, no update.""" mock_fetch_all.return_value = iter([]) BatchFlowthroughProcessor().process([1, 2, 3]) assert mock_fetch_all.call_args_list == [ call( get_bulk_payable_details, [1, 2, 3], [FLOWTHROUGH_PAYABLE_DETAIL_TYPE_ID], ) ] assert not mock_bulk_update.called @patch( 'src.processors.batch_flowthrough_processor.bulk_update_payment_allocations_flowthrough' ) @patch( 'src.processors.batch_flowthrough_processor.get_bulk_payment_allocations_flowthrough' ) @patch('src.processors.batch_flowthrough_processor.get_bulk_payable_details') def test_process_no_allocations_skips_update( mock_get_payable_details: mock.MagicMock, mock_get_allocations: mock.MagicMock, mock_bulk_update: mock.MagicMock, ) -> None: """Payable details exist but no allocations -> no update.""" payable_details_page = mock.MagicMock( items=[PayableDetailsFactory.build(contract_id=10)], total_count=1 ) mock_get_payable_details.return_value = payable_details_page mock_get_allocations.return_value = mock.MagicMock(items=[], total_count=0) BatchFlowthroughProcessor().process([1]) assert mock_get_payable_details.call_args_list == [ call( [1], [FLOWTHROUGH_PAYABLE_DETAIL_TYPE_ID], limit=ACCOUNT_BATCH_SIZE, offset=0, ) ] assert mock_get_allocations.call_args_list == [ call( contract_ids=[10], payment_statuses=[ PaymentAllocationStatuses.INIT, PaymentAllocationStatuses.RETURNED, ], limit=ACCOUNT_BATCH_SIZE, offset=0, ) ] assert not mock_bulk_update.called @patch( 'src.processors.batch_flowthrough_processor.bulk_update_payment_allocations_flowthrough' ) @patch( 'src.processors.batch_flowthrough_processor.get_bulk_payment_allocations_flowthrough' ) @patch('src.processors.batch_flowthrough_processor.get_bulk_payable_details') def test_process_full_flow_updates_allocations( mock_get_payable_details: mock.MagicMock, mock_get_allocations: mock.MagicMock, mock_bulk_update: mock.MagicMock, ) -> None: """Happy path: build update payloads with ATTACHED_TO_PAYMENT statuses.""" detail_a = PayableDetailsFactory.build(contract_id=10) detail_b = PayableDetailsFactory.build(contract_id=20) detail_c = PayableDetailsFactory.build(contract_id=10) # duplicate contract mock_get_payable_details.return_value = mock.MagicMock( items=[detail_a, detail_b, detail_c], total_count=3 ) allocation_1 = PaymentAllocationFlowthroughFactory.build(payment_allocation_id=111) allocation_2 = PaymentAllocationFlowthroughFactory.build(payment_allocation_id=222) mock_get_allocations.return_value = mock.MagicMock( items=[allocation_1, allocation_2], total_count=2 ) BatchFlowthroughProcessor().process([1, 2]) assert mock_get_payable_details.call_args_list == [ call( [1, 2], [FLOWTHROUGH_PAYABLE_DETAIL_TYPE_ID], limit=ACCOUNT_BATCH_SIZE, offset=0, ) ] # contract_ids comes from a set comprehension, so order is non-deterministic. assert len(mock_get_allocations.call_args_list) == 1 actual_call = mock_get_allocations.call_args_list[0] assert actual_call.args == () assert sorted(actual_call.kwargs['contract_ids']) == [10, 20] assert actual_call.kwargs['payment_statuses'] == [ PaymentAllocationStatuses.INIT, PaymentAllocationStatuses.RETURNED, ] assert actual_call.kwargs['limit'] == ACCOUNT_BATCH_SIZE assert actual_call.kwargs['offset'] == 0 assert mock_bulk_update.call_args_list == [ call( [ PaymentAllocationFlowthroughUpdate( payment_allocation_id=111, payment_status=PaymentAllocationStatuses.ATTACHED_TO_PAYMENT, ledger_status=PaymentAllocationLedgerStatuses.ATTACHED_TO_PAYMENT, ), PaymentAllocationFlowthroughUpdate( payment_allocation_id=222, payment_status=PaymentAllocationStatuses.ATTACHED_TO_PAYMENT, ledger_status=PaymentAllocationLedgerStatuses.ATTACHED_TO_PAYMENT, ), ] ) ]