"""Tests for DP payment handlers.""" from http import HTTPStatus import json from unittest.mock import ANY, MagicMock, patch from collaborator.constants import error from collaborator.constants.features import ( ABACUS_COLLABORATORS, ABACUS_COLLABORATORS_PAYONEER_API_INTEGRATION, ) from collaborator.models.rds.dp_payment import PayoneerEventType, PayoneerPaymentStatus from collaborator.schemas.abacus_statement_period import ( GetDpPaymentsForAbacusStatementPeriodResponseSchema, ) from collaborator.schemas.dp_payment import DpPaymentSchema from tests.testutils import mock_auth from tests.testutils.seed.dp_payment_seed import dp_payment_seed_data @patch("collaborator.handlers.dp_payment.dp_payment") def test_get_dp_payments_all_periods( mock_dp_payment_logic, auth_client, mock_account, mocker, mock_features, ): """Test GET /dp-payments returns all payments.""" mock_auth(mocker, mock_account.id) mock_features({ABACUS_COLLABORATORS: True}) mock_payments = [DpPaymentSchema(**payment) for payment in dp_payment_seed_data] total_amount = sum(payment["amount"] for payment in dp_payment_seed_data) mock_dp_payment_logic.get_dp_payments.return_value = (mock_payments, total_amount) result = auth_client.get("/dp-payments") assert result.status_code == 200 assert ( result.json == GetDpPaymentsForAbacusStatementPeriodResponseSchema( currency_agnostic_total_amount=total_amount, total_count=len(dp_payment_seed_data), payments=mock_payments, ).dump() ) mock_dp_payment_logic.get_dp_payments.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="ASC", ) @patch("collaborator.handlers.dp_payment.dp_payment") def test_get_dp_payments_filtered( mock_dp_payment_logic, auth_client, mock_account, mocker, mock_features, ): """Test GET /dp-payments filters by dimensions.""" mock_auth(mocker, mock_account.id) mock_features({ABACUS_COLLABORATORS: True}) mock_payments = [DpPaymentSchema(**payment) for payment in dp_payment_seed_data] total_amount = sum(payment["amount"] for payment in dp_payment_seed_data) mock_dp_payment_logic.get_dp_payments.return_value = (mock_payments, total_amount) query_string = { "abacus_statement_period_id": 1, "collaborator_id": 2, "account_id": 3, "payoneer_program_id": "4", "payoneer_status": PayoneerPaymentStatus.RUNNING.value, "sort_key": "dp_payment_id", "sort_direction": "DESC", } result = auth_client.get("/dp-payments", query_string=query_string) assert result.status_code == 200 assert ( result.json == GetDpPaymentsForAbacusStatementPeriodResponseSchema( currency_agnostic_total_amount=total_amount, total_count=len(dp_payment_seed_data), payments=mock_payments, ).dump() ) mock_dp_payment_logic.get_dp_payments.assert_called_once_with(**query_string) def test_get_dp_payments_feature_disabled( auth_client, mock_account, mocker, mock_features, ): """Test GET /dp-payments returns 403 when feature flag is disabled.""" mock_auth(mocker, mock_account.id) result = auth_client.get("/dp-payments") assert result.status_code == 403 assert result.json["code"] == error.ERROR_CODE_AUTHORIZATION assert result.json["message"] == error.ERROR_MESSAGE_FORBIDDEN_USER @patch("collaborator.handlers.dp_payment.dp_payment") def test_post_payoneer_webhook_feature_disabled( mock_dp_payment_logic, auth_client, mock_account, mocker, mock_features, ): """Test POST /dp-payment/payoneer-webhook is a no-op when feature flag is disabled.""" mock_auth(mocker, mock_account.id) mock_payment = MagicMock() mock_payment.account_id = 24601 mock_dp_payment_logic.get_by_payoneer_payment_id.return_value = mock_payment result = auth_client.post( "/dp-payment/payoneer-webhook", content_type="application/json", data=json.dumps( { "client_reference_id": "split:1:1", "event_type": PayoneerEventType.PAYMENT_ACCEPTED.value, } ), ) assert result.status_code == HTTPStatus.NO_CONTENT mock_dp_payment_logic.update_payoneer_status.assert_not_called() @patch("collaborator.handlers.dp_payment.dp_payment") def test_post_payoneer_webhook_payment_cancelled_calls_handle_payment_returned( mock_dp_payment_logic, auth_client, mock_account, mocker, mock_features, ): """payment_cancelled event calls handle_payment_returned.""" mock_auth(mocker, mock_account.id) mock_features({ABACUS_COLLABORATORS_PAYONEER_API_INTEGRATION: True}) mock_payment = MagicMock() mock_payment.account_id = 24601 mock_dp_payment_logic.get_by_payoneer_payment_id.return_value = mock_payment result = auth_client.post( "/dp-payment/payoneer-webhook", content_type="application/json", data=json.dumps( { "client_reference_id": "split:1:1", "event_type": PayoneerEventType.PAYMENT_CANCELLED.value, "reason_description": "Cancelled by payer", } ), ) assert result.status_code == HTTPStatus.NO_CONTENT mock_dp_payment_logic.update_payoneer_status.assert_called_once_with( mock_payment.dp_payment_id, PayoneerEventType.PAYMENT_CANCELLED, reason="Cancelled by payer", user=ANY, ) @patch("collaborator.handlers.dp_payment.dp_payment") def test_post_payoneer_webhook_iach_failed_is_noop( mock_dp_payment_logic, auth_client, mock_account, mocker, mock_features, ): """iach_failed event updates status but triggers no transaction action.""" mock_auth(mocker, mock_account.id) mock_features({ABACUS_COLLABORATORS_PAYONEER_API_INTEGRATION: True}) mock_payment = MagicMock() mock_payment.account_id = 24601 mock_dp_payment_logic.get_by_payoneer_payment_id.return_value = mock_payment result = auth_client.post( "/dp-payment/payoneer-webhook", content_type="application/json", data=json.dumps( { "client_reference_id": "split:1:1", "event_type": PayoneerEventType.IACH_FAILED.value, } ), ) assert result.status_code == HTTPStatus.NO_CONTENT mock_dp_payment_logic.update_payoneer_status.assert_called_once_with( mock_payment.dp_payment_id, PayoneerEventType.IACH_FAILED, reason=None, user=ANY, ) @patch("collaborator.handlers.dp_payment.dp_payment") def test_post_payoneer_webhook_payment_accepted_calls_handle_payment_accepted( mock_dp_payment_logic, auth_client, mock_account, mocker, mock_features, ): """payment_accepted event calls handle_payment_accepted and updates status.""" mock_auth(mocker, mock_account.id) mock_features({ABACUS_COLLABORATORS_PAYONEER_API_INTEGRATION: True}) mock_payment = MagicMock() mock_payment.account_id = 24601 mock_dp_payment_logic.get_by_payoneer_payment_id.return_value = mock_payment result = auth_client.post( "/dp-payment/payoneer-webhook", content_type="application/json", data=json.dumps( { "client_reference_id": "split:1:1", "event_type": PayoneerEventType.PAYMENT_ACCEPTED.value, } ), ) assert result.status_code == HTTPStatus.NO_CONTENT mock_dp_payment_logic.update_payoneer_status.assert_called_once_with( mock_payment.dp_payment_id, PayoneerEventType.PAYMENT_ACCEPTED, reason=None, user=ANY, ) mock_dp_payment_logic.handle_payment_event.assert_called_once_with( mock_payment, ANY, PayoneerEventType.PAYMENT_ACCEPTED ) @patch("collaborator.handlers.dp_payment.dp_payment") def test_post_payoneer_webhook_payment_completed_is_noop( mock_dp_payment_logic, auth_client, mock_account, mocker, mock_features, ): """payment_completed event updates status but triggers no transaction action.""" mock_auth(mocker, mock_account.id) mock_features({ABACUS_COLLABORATORS_PAYONEER_API_INTEGRATION: True}) mock_payment = MagicMock() mock_payment.account_id = 24601 mock_dp_payment_logic.get_by_payoneer_payment_id.return_value = mock_payment result = auth_client.post( "/dp-payment/payoneer-webhook", content_type="application/json", data=json.dumps( { "client_reference_id": "split:1:1", "event_type": PayoneerEventType.PAYMENT_COMPLETED.value, } ), ) assert result.status_code == HTTPStatus.NO_CONTENT mock_dp_payment_logic.update_payoneer_status.assert_called_once_with( mock_payment.dp_payment_id, PayoneerEventType.PAYMENT_COMPLETED, reason=None, user=ANY, ) @patch("collaborator.handlers.dp_payment.dp_payment") def test_post_payoneer_webhook_payment_not_found( mock_dp_payment_logic, auth_client, mock_account, mocker, mock_features, ): """POST /dp-payment/payoneer-webhook returns 404 when payment is not found.""" mock_auth(mocker, mock_account.id) mock_dp_payment_logic.get_by_payoneer_payment_id.return_value = None result = auth_client.post( "/dp-payment/payoneer-webhook", content_type="application/json", data=json.dumps( { "client_reference_id": "split:999:999", "event_type": PayoneerEventType.PAYMENT_ACCEPTED.value, } ), ) assert result.status_code == 404 assert result.json["code"] == error.ERROR_CODE_DP_PAYMENT_NOT_FOUND assert result.json["message"] == error.ERROR_MESSAGE_DP_PAYMENT_NOT_FOUND mock_dp_payment_logic.update_payoneer_status.assert_not_called() @patch("collaborator.handlers.dp_payment.dp_payment") def test_post_payoneer_webhook_iach_failed_stores_reason( mock_dp_payment_logic, auth_client, mock_account, mocker, mock_features, ): """iach_failed event with a reason_description passes the reason to logic.""" mock_auth(mocker, mock_account.id) mock_features({ABACUS_COLLABORATORS_PAYONEER_API_INTEGRATION: True}) mock_payment = MagicMock() mock_payment.account_id = 24601 mock_dp_payment_logic.get_by_payoneer_payment_id.return_value = mock_payment result = auth_client.post( "/dp-payment/payoneer-webhook", content_type="application/json", data=json.dumps( { "client_reference_id": "split:1:1", "event_type": PayoneerEventType.IACH_FAILED.value, "reason_description": "Insufficient funds", } ), ) assert result.status_code == HTTPStatus.NO_CONTENT mock_dp_payment_logic.update_payoneer_status.assert_called_once_with( mock_payment.dp_payment_id, PayoneerEventType.IACH_FAILED, reason="Insufficient funds", user=ANY, ) @patch("collaborator.handlers.dp_payment.dp_payment") def test_post_payoneer_webhook_payment_cancelled_without_reason_passes_none( mock_dp_payment_logic, auth_client, mock_account, mocker, mock_features, ): """payment_cancelled event with no reason_description passes reason=None to logic.""" mock_auth(mocker, mock_account.id) mock_features({ABACUS_COLLABORATORS_PAYONEER_API_INTEGRATION: True}) mock_payment = MagicMock() mock_payment.account_id = 24601 mock_dp_payment_logic.get_by_payoneer_payment_id.return_value = mock_payment result = auth_client.post( "/dp-payment/payoneer-webhook", content_type="application/json", data=json.dumps( { "client_reference_id": "split:1:1", "event_type": PayoneerEventType.PAYMENT_CANCELLED.value, } ), ) assert result.status_code == HTTPStatus.NO_CONTENT mock_dp_payment_logic.update_payoneer_status.assert_called_once_with( mock_payment.dp_payment_id, PayoneerEventType.PAYMENT_CANCELLED, reason=None, user=ANY, )