"""Tests for DP payment logic.""" from types import SimpleNamespace from unittest.mock import MagicMock, patch import pytest from collaborator.constants import error from collaborator.logic import dp_payment from collaborator.models.rds.dp_payment import ( DpPayment, PayoneerEventType, PayoneerPaymentStatus, ) from collaborator.schemas.dp_payment import DpPaymentSchema from collaborator.utils.error import OwsError from tests.testutils.seed.dp_payment_seed import dp_payment_seed_data @patch("collaborator.models.rds.dp_payment_persister.DpPaymentPersister.get_by_filters") def test_get_dp_payments(mock_get): """Test that DP payments are fetched correctly for a statement period.""" abacus_statement_period_id = 1 mock_payment = DpPayment(**dp_payment_seed_data[0]) mock_get.return_value = [(mock_payment, 1234)] payments, total_amount = dp_payment.get_dp_payments( abacus_statement_period_id=abacus_statement_period_id, ) assert total_amount == 1234 assert [payment.dump() for payment in payments] == [ DpPaymentSchema.parse(mock_payment).dump() ] @patch("collaborator.models.rds.dp_payment_persister.DpPaymentPersister.get_by_filters") def test_get_dp_payments_no_period_filter(mock_get): """Test that DP payments across all periods are fetched when no period is given.""" mock_payments = [DpPayment(**payment) for payment in dp_payment_seed_data] total_amount = sum(payment["amount"] for payment in dp_payment_seed_data) mock_get.return_value = [(payment, total_amount) for payment in mock_payments] payments, result_total = dp_payment.get_dp_payments() mock_get.assert_called_once_with( abacus_statement_period_id=None, collaborator_id=None, account_id=None, payoneer_program_id=None, payoneer_status=None, sort_key=None, sort_direction=None, ) assert result_total == total_amount assert [payment.dump() for payment in payments] == [ DpPaymentSchema.parse(p).dump() for p in mock_payments ] @patch("collaborator.logic.dp_payment.DpPaymentPersister.create_and_link_transaction") @patch( "collaborator.logic.dp_payment.StatementPeriodPersister.get_open_statement_period" ) def test_handle_payment_accepted_creates_transaction( mock_get_period, mock_create_and_link ): """handle_payment_accepted creates a transaction and links it to the dp_payment.""" mock_statement_period = MagicMock(statement_period_id=2) mock_get_period.return_value = mock_statement_period payment = DpPayment( **{**dp_payment_seed_data[0], "collaborator_transaction_id": None} ) user = SimpleNamespace(id="test-user") dp_payment._handle_payment_accepted(payment, user) mock_get_period.assert_called_once_with(payment.account_id) mock_create_and_link.assert_called_once_with( dp_payment_id=payment.dp_payment_id, collaborator_id=payment.collaborator_id, transaction_type="DIRECT_PAYMENT", transaction_date=mock_create_and_link.call_args.kwargs["transaction_date"], description=f"{payment.abacus_statement_period_name} Balance Payment", original_amount=-payment.amount, chargeable_amount=-payment.amount, statement_period_id=2, currency=payment.currency, user=user, ) @patch("collaborator.logic.dp_payment.DpPaymentPersister.create_and_link_transaction") @patch( "collaborator.logic.dp_payment.StatementPeriodPersister.get_open_statement_period" ) def test_handle_payment_accepted_skips_if_transaction_already_set( mock_get_period, mock_create_and_link ): """handle_payment_accepted is a no-op when collaborator_transaction_id is already set.""" payment = DpPayment(**dp_payment_seed_data[2]) # has collaborator_transaction_id=17 user = SimpleNamespace(id="test-user") dp_payment._handle_payment_accepted(payment, user) mock_get_period.assert_not_called() mock_create_and_link.assert_not_called() @patch("collaborator.logic.dp_payment.DpPaymentPersister.create_and_link_transaction") @patch( "collaborator.logic.dp_payment.StatementPeriodPersister.get_open_statement_period" ) def test_handle_payment_accepted_raises_if_no_open_period( mock_get_period, mock_create_and_link ): """handle_payment_accepted raises OwsError when there is no open statement period.""" mock_get_period.return_value = None payment = DpPayment( **{**dp_payment_seed_data[0], "collaborator_transaction_id": None} ) user = SimpleNamespace(id="test-user") with pytest.raises(OwsError) as exc_info: dp_payment._handle_payment_accepted(payment, user) assert exc_info.value.code == error.ERROR_CODE_STATEMENT_PERIOD_NOT_FOUND mock_create_and_link.assert_not_called() @patch("collaborator.logic.dp_payment.logging.log_event") @patch("collaborator.logic.dp_payment.TransactionPersister.create_transaction") @patch( "collaborator.logic.dp_payment.StatementPeriodPersister.get_open_statement_period" ) @patch("collaborator.logic.dp_payment.TransactionPersister.get_by_id") @patch( "collaborator.logic.dp_payment.TransactionPersister.is_credited_by_credited_payment_id" ) def test_handle_payment_returned_creates_credit_transaction( mock_is_credited, mock_get_by_id, mock_get_period, mock_create_transaction, mock_log_event, ): """handle_payment_returned creates a TYPE_CREDIT transaction.""" mock_is_credited.return_value = False mock_original = { "id": 17, "description": "Q1 Balance Payment", "chargeable_amount": -500.0, } mock_get_by_id.return_value = mock_original mock_statement_period = MagicMock(statement_period_id=3) mock_get_period.return_value = mock_statement_period mock_created = {"id": 99} mock_create_transaction.return_value = mock_created payment = DpPayment(**dp_payment_seed_data[2]) # has collaborator_transaction_id=17 dp_payment._handle_payment_returned(payment) mock_is_credited.assert_called_once_with(17) mock_get_by_id.assert_called_once_with(17) mock_get_period.assert_called_once_with(payment.account_id) mock_create_transaction.assert_called_once_with( collaborator_id=payment.collaborator_id, transaction_type="CREDIT", transaction_date=mock_create_transaction.call_args.kwargs["transaction_date"], description="Returned Payment: Q1 Balance Payment", original_amount=-mock_original["chargeable_amount"], collaborator_share=None, chargeable_amount=-mock_original["chargeable_amount"], statement_period_id=3, transferwise_transaction_id=None, report_id=None, voided_transaction_id=None, currency=payment.currency, credited_payment_id=17, ) mock_log_event.assert_called_once() @patch("collaborator.logic.dp_payment.TransactionPersister.create_transaction") @patch( "collaborator.logic.dp_payment.TransactionPersister.is_credited_by_credited_payment_id" ) def test_handle_payment_returned_skips_if_no_transaction( mock_is_credited, mock_create_transaction ): """handle_payment_returned is a no-op when collaborator_transaction_id is None.""" payment = DpPayment( **{**dp_payment_seed_data[0], "collaborator_transaction_id": None} ) dp_payment._handle_payment_returned(payment) mock_is_credited.assert_not_called() mock_create_transaction.assert_not_called() @patch("collaborator.logic.dp_payment.TransactionPersister.create_transaction") @patch( "collaborator.logic.dp_payment.StatementPeriodPersister.get_open_statement_period" ) @patch("collaborator.logic.dp_payment.TransactionPersister.get_by_id") @patch( "collaborator.logic.dp_payment.TransactionPersister.is_credited_by_credited_payment_id" ) def test_handle_payment_returned_idempotent( mock_is_credited, mock_get_by_id, mock_get_period, mock_create_transaction ): """handle_payment_returned is a no-op when a credit transaction already exists.""" mock_is_credited.return_value = True payment = DpPayment(**dp_payment_seed_data[2]) # has collaborator_transaction_id=17 dp_payment._handle_payment_returned(payment) mock_get_by_id.assert_not_called() mock_get_period.assert_not_called() mock_create_transaction.assert_not_called() @patch("collaborator.logic.dp_payment.TransactionPersister.create_transaction") @patch( "collaborator.logic.dp_payment.StatementPeriodPersister.get_open_statement_period" ) @patch("collaborator.logic.dp_payment.TransactionPersister.get_by_id") @patch( "collaborator.logic.dp_payment.TransactionPersister.is_credited_by_credited_payment_id" ) def test_handle_payment_returned_raises_if_no_open_period( mock_is_credited, mock_get_by_id, mock_get_period, mock_create_transaction ): """handle_payment_returned raises OwsError when there is no open statement period.""" mock_is_credited.return_value = False mock_get_by_id.return_value = {"id": 17, "description": "Q1 Balance Payment"} mock_get_period.return_value = None payment = DpPayment(**dp_payment_seed_data[2]) # has collaborator_transaction_id=17 with pytest.raises(OwsError) as exc_info: dp_payment._handle_payment_returned(payment) assert exc_info.value.code == error.ERROR_CODE_STATEMENT_PERIOD_NOT_FOUND mock_create_transaction.assert_not_called() @patch("collaborator.logic.dp_payment._handle_payment_returned") @patch("collaborator.logic.dp_payment._handle_payment_accepted") def test_handle_payment_event_accepted(mock_accepted, mock_returned): """handle_payment_event dispatches PAYMENT_ACCEPTED to _handle_payment_accepted.""" payment = DpPayment(**dp_payment_seed_data[0]) user = SimpleNamespace(id="test-user") dp_payment.handle_payment_event(payment, user, PayoneerEventType.PAYMENT_ACCEPTED) mock_accepted.assert_called_once_with(payment, user) mock_returned.assert_not_called() @patch("collaborator.logic.dp_payment._handle_payment_returned") @patch("collaborator.logic.dp_payment._handle_payment_accepted") def test_handle_payment_event_cancelled(mock_accepted, mock_returned): """handle_payment_event dispatches PAYMENT_CANCELLED to _handle_payment_returned.""" payment = DpPayment(**dp_payment_seed_data[0]) user = SimpleNamespace(id="test-user") dp_payment.handle_payment_event(payment, user, PayoneerEventType.PAYMENT_CANCELLED) mock_returned.assert_called_once_with(payment) mock_accepted.assert_not_called() @pytest.mark.parametrize( "event_type", [PayoneerEventType.PAYMENT_COMPLETED, PayoneerEventType.IACH_FAILED], ) @patch("collaborator.logic.dp_payment._handle_payment_returned") @patch("collaborator.logic.dp_payment._handle_payment_accepted") def test_handle_payment_event_no_transaction_action( mock_accepted, mock_returned, event_type ): """handle_payment_event is a no-op for PAYMENT_COMPLETED and IACH_FAILED.""" payment = DpPayment(**dp_payment_seed_data[0]) user = SimpleNamespace(id="test-user") dp_payment.handle_payment_event(payment, user, event_type) mock_accepted.assert_not_called() mock_returned.assert_not_called() @pytest.mark.parametrize( "event_type, expected_status", [ (PayoneerEventType.PAYMENT_ACCEPTED, PayoneerPaymentStatus.RUNNING), (PayoneerEventType.PAYMENT_COMPLETED, PayoneerPaymentStatus.COMPLETE), (PayoneerEventType.PAYMENT_CANCELLED, PayoneerPaymentStatus.REJECTED), (PayoneerEventType.IACH_FAILED, PayoneerPaymentStatus.ERROR), ], ) def test_get_status_by_event_type(event_type, expected_status): """_get_status_by_event_type maps each event type to the correct payment status.""" assert dp_payment._get_status_by_event_type(event_type) == expected_status