"""Tests for Payee logic.""" import copy from http import HTTPStatus from unittest import mock from unittest.mock import call, MagicMock, patch from owsresponse import response import pytest from payee.config import Config from payee.constants.constants import ( DOCUMENTS_AWAL_APP_BRAND, PAYONEER_GET_ACCOUNT_ID_ENDPOINT, PAYONEER_GET_PAYEE_REGISTER_FORMAT_ENDPOINT, PAYONEER_SERVICE_STATUSES, PAYONEER_STATUS_CODE, PAYONEER_STATUS_ENDPOINT, PREEXISTING_PAYONEER_ACCOUNT_PROGRAM_IDS, ) from payee.constants.error import ( ERROR_ACCOUNT_ID_NOT_FOUND, ERROR_PAYEE_DETAILS_NOT_FOUND, ) from payee.constants.queries import COMPANY_BRAND_BY_VENDOR_ID from payee.logic import payee_account_state, payoneer as logic from payee.logic.exceptions import LogicError from payee.logic.payoneer import ( build_payoneer_redirect_uri, get_payee_corp_entity, get_payee_register_format, ) from payee.models.account_payee import AccountPayee from payee.utils.exception import ( BrandFetchException, CorpEntityFetchException, CreatePayoneerRegistrationLinkException, GetPayeeDetailsException, GetRegisterPayeeFormatException, HttpMassPayoutsException, NoPayoneerCredentialsException, PayoneerMassPayoutsException, PayoneerPayeeDetailsException, RegisterPayeeException, ReleasePayeeException, UpdatePayeeException, ) from payee.utils.format_data import get_now_timestamp from tests.utils.factories import ( AccountPayeeFactory, BankDetailsIndividualTypeNestedFactory, PayeeRegistrationFormatFactory, PayeeRegistrationFormatGetFactory, ) redirect_uri_value = 'AWAL' account_id = 1 account_payee_id = 1 ts_now = get_now_timestamp() @mock.patch( 'payee.logic.payoneer.build_payoneer_redirect_uri', return_value=redirect_uri_value ) @mock.patch( 'payee.logic.payoneer._build_payoneer_headers', return_value={'mock': 'header'} ) @mock.patch('payee.logic.payoneer.post') def test_create_registration_link_payoneer_failure( mock_post_request, mock_build_payoneer_headers, mock_build_redirect_uri, payoneer_registration_link_post_failure, monkeypatch, mock_config, ): """Test create_registration_link method when the Payoneer request fails.""" mock_post_request.return_value.json.return_value = ( payoneer_registration_link_post_failure ) monkeypatch.setattr( 'payee.config.Config.PAYONEER_CLIENT_ID', mock_config.PAYONEER_CLIENT_ID ) monkeypatch.setattr( 'payee.config.Config.PAYONEER_REDIRECT_URL', mock_config.PAYONEER_REDIRECT_URL ) test_input = { 'account_payee_id': 123, 'payoneer_program_id': PREEXISTING_PAYONEER_ACCOUNT_PROGRAM_IDS[0], } with pytest.raises(CreatePayoneerRegistrationLinkException) as excinfo: logic.create_registration_link(test_input) assert excinfo.value.status_code == 404 @mock.patch( 'payee.logic.payoneer.build_payoneer_redirect_uri', return_value=redirect_uri_value ) @mock.patch( 'payee.logic.payoneer._build_payoneer_headers', return_value={'mock': 'header'} ) @mock.patch('payee.logic.payoneer.post') def test_create_registration_link_success( mock_post_request, mock_build_payoneer_headers, mock_build_redirect_uri, mock_config, monkeypatch, payoneer_registration_link_post_success, ): """Test create_registration_link method.""" mock_post_request.return_value.json.return_value = ( payoneer_registration_link_post_success ) monkeypatch.setattr( 'payee.config.Config.PAYONEER_CLIENT_ID', mock_config.PAYONEER_CLIENT_ID ) monkeypatch.setattr( 'payee.config.Config.PAYONEER_REDIRECT_URL', mock_config.PAYONEER_REDIRECT_URL ) test_input = { 'account_payee_id': 123, 'payoneer_program_id': PREEXISTING_PAYONEER_ACCOUNT_PROGRAM_IDS[0], } expected_output = { 'registration_link': 'https://payouts.sandbox.payoneer.com/partners/lp.aspx?token=7293f06a1d524e6f80a4f0f414b0f0962010B7745A', # noqa: E501 'account_payee_id': 123, } res = logic.create_registration_link(test_input) assert res == expected_output call_kwargs = mock_post_request.call_args[1] assert call_kwargs['json']['already_have_an_account'] is True @mock.patch( 'payee.logic.payoneer.build_payoneer_redirect_uri', return_value=redirect_uri_value ) @mock.patch( 'payee.logic.payoneer._build_payoneer_headers', return_value={'mock': 'header'} ) @mock.patch('payee.logic.payoneer.post') def test_create_registration_link_success_str_account_payee_id( mock_post_request, mock_build_payoneer_headers, mock_build_redirect_uri, mock_config, monkeypatch, payoneer_registration_link_post_success, ): """Test create_registration_link method preserves a string account_payee_id.""" mock_post_request.return_value.json.return_value = ( payoneer_registration_link_post_success ) monkeypatch.setattr( 'payee.config.Config.PAYONEER_CLIENT_ID', mock_config.PAYONEER_CLIENT_ID ) monkeypatch.setattr( 'payee.config.Config.PAYONEER_REDIRECT_URL', mock_config.PAYONEER_REDIRECT_URL ) test_input = { 'account_payee_id': '123', 'payoneer_program_id': PREEXISTING_PAYONEER_ACCOUNT_PROGRAM_IDS[0], } expected_output = { 'registration_link': 'https://payouts.sandbox.payoneer.com/partners/lp.aspx?token=7293f06a1d524e6f80a4f0f414b0f0962010B7745A', # noqa: E501 'account_payee_id': '123', } res = logic.create_registration_link(test_input) assert res == expected_output call_kwargs = mock_post_request.call_args[1] assert call_kwargs['json']['already_have_an_account'] is True @mock.patch( 'payee.logic.payoneer.build_payoneer_redirect_uri', return_value=redirect_uri_value ) @mock.patch( 'payee.logic.payoneer._build_payoneer_headers', return_value={'mock': 'header'} ) @mock.patch('payee.logic.payoneer.post') def test_create_registration_link_failure_wrong_program( mock_post_request, mock_build_payoneer_headers, mock_build_redirect_uri, mock_config, monkeypatch, payoneer_registration_link_post_success, ): """Test that a non-preexisting Payoneer program ID is forbidden.""" mock_post_request.return_value.json.return_value = ( payoneer_registration_link_post_success ) monkeypatch.setattr( 'payee.config.Config.PAYONEER_CLIENT_ID', mock_config.PAYONEER_CLIENT_ID ) monkeypatch.setattr( 'payee.config.Config.PAYONEER_REDIRECT_URL', mock_config.PAYONEER_REDIRECT_URL ) monkeypatch.setattr('payee.config.Config.PAYONEER_REDIRECT_TIME', 10) test_input = { 'account_payee_id': 123, 'payoneer_program_id': '12345', } with pytest.raises(LogicError, match='Forbidden for selected Payoneer program'): logic.create_registration_link(test_input) mock_post_request.assert_not_called() @mock.patch('payee.logic.payoneer._build_payoneer_headers', return_value=None) def test_create_mass_payouts_missing_credentials( mock_build_payoneer_headers, mock_payoneer_mass_payouts ): """Test that missing Payoneer credentials raises PayoneerMassPayoutsException.""" with pytest.raises(PayoneerMassPayoutsException) as excinfo: logic.create_mass_payouts(1, mock_payoneer_mass_payouts) assert str(excinfo.value) == 'Payoneer credentials not found' assert isinstance(excinfo.value, PayoneerMassPayoutsException) @mock.patch('payee.logic.payoneer._build_payoneer_headers', return_value={}) def test_create_mass_payouts_empty_credentials( mock_build_payoneer_headers, mock_payoneer_mass_payouts ): """Test that empty Payoneer credentials raises PayoneerMassPayoutsException.""" with pytest.raises(PayoneerMassPayoutsException) as excinfo: logic.create_mass_payouts(1, mock_payoneer_mass_payouts) assert str(excinfo.value) == 'Payoneer credentials not found' assert isinstance(excinfo.value, PayoneerMassPayoutsException) @mock.patch( 'payee.logic.payoneer._build_payoneer_headers', return_value={'mock': 'header'} ) @mock.patch('payee.logic.payoneer.post') def test_create_mass_payouts_empty_payments_list( mock_request_post, mock_build_payoneer_headers ): """Test that empty payments list is handled correctly.""" expected_result = {'result': 'Payments Created'} mock_request_post.return_value = MagicMock( status_code=200, json=MagicMock(return_value=expected_result) ) result = logic.create_mass_payouts(1, []) assert result.status == 201 assert result.message == expected_result mock_request_post.assert_called_with( 'https://api.sandbox.payoneer.com/v4/programs/1/masspayouts', headers={'mock': 'header'}, json={'Payments': []}, ) @mock.patch( 'payee.logic.payoneer._build_payoneer_headers', return_value={'mock': 'header'} ) @mock.patch('payee.logic.payoneer.post') def test_create_mass_payouts_success( mock_request_post, mock_build_payoneer_headers, mock_payoneer_mass_payouts ): """Test for successful mass payouts logic layer.""" expected_result = {'result': 'Payments Created'} mock_request_post.return_value = MagicMock( status_code=200, json=MagicMock(return_value=expected_result) ) result = logic.create_mass_payouts(1, mock_payoneer_mass_payouts) assert result.status == 201 assert result.message == expected_result mock_request_post.assert_called_with( 'https://api.sandbox.payoneer.com/v4/programs/1/masspayouts', headers={'mock': 'header'}, json={ 'Payments': [ { 'client_reference_id': 'test1', 'payee_id': '56732', 'description': 'Mass Payouts 1', 'currency': 'USD', 'amount': 222, }, { 'client_reference_id': 'test2', 'payee_id': '56733', 'description': 'Mass Payouts 2', 'currency': 'USD', 'amount': 555, }, ] }, ) @mock.patch( 'payee.logic.payoneer._build_payoneer_headers', return_value={'mock': 'header'} ) @mock.patch('payee.logic.payoneer.post') def test_create_mass_payouts_success_with_special_characters_in_description( mock_request_post, mock_build_payoneer_headers, mock_payoneer_mass_payouts ): """Test for successful mass payouts logic layer.""" modified_mock_payoneer_mass_payouts = copy.deepcopy(mock_payoneer_mass_payouts) modified_mock_payoneer_mass_payouts[0]['description'] = ( 'description with special characters: \!@#$%^&*()_+{}|:"<>?/.,;\'[]=-`~' ) modified_mock_payoneer_mass_payouts[1]['description'] = ( "description more than 250 characters: Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum" ) expected_result = {'result': 'Payments Created'} mock_request_post.return_value = MagicMock( status_code=200, json=MagicMock(return_value=expected_result) ) result = logic.create_mass_payouts(1, modified_mock_payoneer_mass_payouts) assert result.status == 201 assert result.message == expected_result mock_request_post.assert_called_with( 'https://api.sandbox.payoneer.com/v4/programs/1/masspayouts', headers={'mock': 'header'}, json={ 'Payments': [ { 'client_reference_id': 'test1', 'payee_id': '56732', 'description': 'description with special characters: !@$%&()_+:">/.,\'-', 'currency': 'USD', 'amount': 222, }, { 'client_reference_id': 'test2', 'payee_id': '56733', 'description': "description more than 250 characters: Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled", 'currency': 'USD', 'amount': 555, }, ] }, ) @mock.patch( 'payee.logic.payoneer._build_payoneer_headers', return_value={'mock': 'header'} ) @mock.patch('payee.logic.payoneer.post') def test_create_mass_payouts_failure_description_error( mock_request_post, mock_build_payoneer_headers, mock_payoneer_mass_payouts ): """Test that description with only special characters raises ValueError.""" modified_mock_payoneer_mass_payouts = copy.deepcopy(mock_payoneer_mass_payouts) modified_mock_payoneer_mass_payouts[0]['description'] = '?#' with pytest.raises(ValueError) as exc_info: logic.create_mass_payouts(1, modified_mock_payoneer_mass_payouts) assert str( exc_info.value ) == 'Description is empty after sanitization, original description: %s' % str( modified_mock_payoneer_mass_payouts[0]['description'] ) @mock.patch( 'payee.logic.payoneer._build_payoneer_headers', return_value={'mock': 'header'} ) @mock.patch('payee.logic.payoneer.post') def test_create_mass_payouts_only_special_characters_description( mock_request_post, mock_build_payoneer_headers ): """Test that description containing only invalid special characters raises ValueError.""" payouts_with_invalid_desc = [ { 'client_reference_id': 'test1', 'account_payee_id': '56732', 'description': '###^^^~~~```', 'currency': 'USD', 'amount': 222, } ] with pytest.raises(ValueError) as exc_info: logic.create_mass_payouts(1, payouts_with_invalid_desc) assert 'Description is empty after sanitization' in str(exc_info.value) @mock.patch( 'payee.logic.payoneer._build_payoneer_headers', return_value={'mock': 'header'} ) @mock.patch('payee.logic.payoneer.post') def test_create_mass_payouts_with_account_payee_description_override( mock_request_post, mock_build_payoneer_headers, mock_payoneer_mass_payouts, ): """Test that account_payee.payment_description overrides provided description.""" from tests.utils.factories import AccountPayeeFactory # Create account_payees with payment_description AccountPayeeFactory.create( account_payee_id=56732, payment_description='Account Payment Description 1' ) AccountPayeeFactory.create( account_payee_id=56733, payment_description='Account Payment Description 2' ) expected_result = {'result': 'Payments Created'} mock_request_post.return_value = MagicMock( status_code=200, json=MagicMock(return_value=expected_result) ) result = logic.create_mass_payouts(1, mock_payoneer_mass_payouts) assert result.status == 201 assert result.message == expected_result # Verify that account_payee descriptions were used instead of provided descriptions mock_request_post.assert_called_with( 'https://api.sandbox.payoneer.com/v4/programs/1/masspayouts', headers={'mock': 'header'}, json={ 'Payments': [ { 'client_reference_id': 'test1', 'payee_id': '56732', 'description': 'Account Payment Description 1', 'currency': 'USD', 'amount': 222, }, { 'client_reference_id': 'test2', 'payee_id': '56733', 'description': 'Account Payment Description 2', 'currency': 'USD', 'amount': 555, }, ] }, ) @mock.patch( 'payee.logic.payoneer._build_payoneer_headers', return_value={'mock': 'header'} ) @mock.patch('payee.logic.payoneer.post') def test_create_mass_payouts_partial_account_payee_description_override( mock_request_post, mock_build_payoneer_headers, mock_payoneer_mass_payouts, ): """Test when only some account_payees have payment_description.""" from tests.utils.factories import AccountPayeeFactory # Only first account_payee has payment_description AccountPayeeFactory.create( account_payee_id=56732, payment_description='Account Payment Description 1' ) AccountPayeeFactory.create(account_payee_id=56733, payment_description='') expected_result = {'result': 'Payments Created'} mock_request_post.return_value = MagicMock( status_code=200, json=MagicMock(return_value=expected_result) ) result = logic.create_mass_payouts(1, mock_payoneer_mass_payouts) assert result.status == 201 # Verify mixed descriptions mock_request_post.assert_called_with( 'https://api.sandbox.payoneer.com/v4/programs/1/masspayouts', headers={'mock': 'header'}, json={ 'Payments': [ { 'client_reference_id': 'test1', 'payee_id': '56732', 'description': 'Account Payment Description 1', # Overridden 'currency': 'USD', 'amount': 222, }, { 'client_reference_id': 'test2', 'payee_id': '56733', 'description': 'Mass Payouts 2', # Original 'currency': 'USD', 'amount': 555, }, ] }, ) @mock.patch( 'payee.logic.payoneer._build_payoneer_headers', return_value={'mock': 'header'} ) @mock.patch('payee.logic.payoneer.post') def test_create_mass_payouts_without_description_field( mock_request_post, mock_build_payoneer_headers ): """Test mass payouts when description is not provided but account_payee has one.""" from tests.utils.factories import AccountPayeeFactory # Create account_payee with payment_description AccountPayeeFactory.create( account_payee_id=56732, payment_description='Account Payment Description' ) payouts_without_description = [ { 'client_reference_id': 'test1', 'account_payee_id': '56732', 'currency': 'USD', 'amount': 222, } ] expected_result = {'result': 'Payments Created'} mock_request_post.return_value = MagicMock( status_code=200, json=MagicMock(return_value=expected_result) ) result = logic.create_mass_payouts(1, payouts_without_description) assert result.status == 201 # Verify account_payee description was used mock_request_post.assert_called_with( 'https://api.sandbox.payoneer.com/v4/programs/1/masspayouts', headers={'mock': 'header'}, json={ 'Payments': [ { 'client_reference_id': 'test1', 'payee_id': '56732', 'description': 'Account Payment Description', 'currency': 'USD', 'amount': 222, } ] }, ) @mock.patch( 'payee.logic.payoneer._build_payoneer_headers', return_value={'mock': 'header'} ) @mock.patch('payee.logic.payoneer.post') def test_create_mass_payouts_without_description_and_no_account_payee( mock_request_post, mock_build_payoneer_headers ): """Test that missing description with an empty account_payee description raises ValueError.""" from tests.utils.factories import AccountPayeeFactory # Create account_payee without payment_description AccountPayeeFactory.create(account_payee_id=56732, payment_description='') payouts_without_description = [ { 'client_reference_id': 'test1', 'account_payee_id': '56732', 'currency': 'USD', 'amount': 222, } ] with pytest.raises(ValueError) as exc_info: logic.create_mass_payouts(1, payouts_without_description) assert 'Description is empty after sanitization' in str(exc_info.value) @mock.patch( 'payee.logic.payoneer._build_payoneer_headers', return_value={'mock': 'header'} ) @mock.patch('payee.logic.payoneer.post') def test_create_mass_payouts_single_payment( mock_request_post, mock_build_payoneer_headers ): """Test mass payouts with a single payment.""" single_payment = [ { 'client_reference_id': 'single_test', 'account_payee_id': '99999', 'description': 'Single Payment Test', 'currency': 'EUR', 'amount': 1000, } ] expected_result = {'result': 'Payments Created'} mock_request_post.return_value = MagicMock( status_code=200, json=MagicMock(return_value=expected_result) ) result = logic.create_mass_payouts(1, single_payment) assert result.status == 201 assert result.message == expected_result mock_request_post.assert_called_with( 'https://api.sandbox.payoneer.com/v4/programs/1/masspayouts', headers={'mock': 'header'}, json={ 'Payments': [ { 'client_reference_id': 'single_test', 'payee_id': '99999', 'description': 'Single Payment Test', 'currency': 'EUR', 'amount': 1000, } ] }, ) @mock.patch( 'payee.logic.payoneer._build_payoneer_headers', return_value={'mock': 'header'} ) @mock.patch('payee.logic.payoneer.post') def test_create_mass_payouts_description_exactly_250_chars( mock_request_post, mock_build_payoneer_headers ): """Test that description of exactly 250 characters is accepted.""" description_250 = 'A' * 250 payouts_with_max_desc = [ { 'client_reference_id': 'test1', 'account_payee_id': '56732', 'description': description_250, 'currency': 'USD', 'amount': 222, } ] expected_result = {'result': 'Payments Created'} mock_request_post.return_value = MagicMock( status_code=200, json=MagicMock(return_value=expected_result) ) result = logic.create_mass_payouts(1, payouts_with_max_desc) assert result.status == 201 # Verify the description is exactly 250 characters call_args = mock_request_post.call_args assert len(call_args[1]['json']['Payments'][0]['description']) == 250 @mock.patch( 'payee.logic.payoneer._build_payoneer_headers', return_value={'mock': 'header'} ) @mock.patch('payee.logic.payoneer.post') def test_create_mass_payouts_numeric_account_payee_id( mock_request_post, mock_build_payoneer_headers ): """Test that numeric account_payee_id is handled correctly.""" payouts_with_numeric_id = [ { 'client_reference_id': 'test1', 'account_payee_id': 56732, # Numeric instead of string 'description': 'Payment with numeric ID', 'currency': 'USD', 'amount': 100, } ] expected_result = {'result': 'Payments Created'} mock_request_post.return_value = MagicMock( status_code=200, json=MagicMock(return_value=expected_result) ) result = logic.create_mass_payouts(1, payouts_with_numeric_id) assert result.status == 201 # Verify account_payee_id is converted to string in payee_id call_args = mock_request_post.call_args assert call_args[1]['json']['Payments'][0]['payee_id'] == '56732' @mock.patch( 'payee.logic.payoneer._build_payoneer_headers', return_value={'mock': 'header'} ) @mock.patch('payee.logic.payoneer.post') def test_create_mass_payouts_allowed_special_characters( mock_request_post, mock_build_payoneer_headers ): """Test that allowed special characters are preserved in description.""" payouts_with_allowed_chars = [ { 'client_reference_id': 'test1', 'account_payee_id': '56732', 'description': 'Payment (100%) for items: A, B & C - $50.00 each @ 10% discount! "Great deal" + more', 'currency': 'USD', 'amount': 450, } ] expected_result = {'result': 'Payments Created'} mock_request_post.return_value = MagicMock( status_code=200, json=MagicMock(return_value=expected_result) ) result = logic.create_mass_payouts(1, payouts_with_allowed_chars) assert result.status == 201 # Verify allowed characters are preserved call_args = mock_request_post.call_args actual_description = call_args[1]['json']['Payments'][0]['description'] assert '(' in actual_description assert ')' in actual_description assert ',' in actual_description assert '.' in actual_description assert '-' in actual_description assert '@' in actual_description assert '%' in actual_description assert '!' in actual_description assert '$' in actual_description assert '"' in actual_description assert '+' in actual_description assert '&' in actual_description @mock.patch( 'payee.logic.payoneer._build_payoneer_headers', return_value={'mock': 'header'} ) @mock.patch('payee.logic.payoneer.post') def test_create_mass_payouts_description_with_colon_and_underscore( mock_request_post, mock_build_payoneer_headers ): """Test that colons and underscores are preserved in description.""" payouts = [ { 'client_reference_id': 'test1', 'account_payee_id': '56732', 'description': 'Invoice_2024: Payment for Q1_deliverables', 'currency': 'USD', 'amount': 1000, } ] expected_result = {'result': 'Payments Created'} mock_request_post.return_value = MagicMock( status_code=200, json=MagicMock(return_value=expected_result) ) result = logic.create_mass_payouts(1, payouts) assert result.status == 201 call_args = mock_request_post.call_args actual_description = call_args[1]['json']['Payments'][0]['description'] assert actual_description == 'Invoice_2024: Payment for Q1_deliverables' @mock.patch( 'payee.logic.payoneer._build_payoneer_headers', return_value={'mock': 'header'} ) @mock.patch('payee.logic.payoneer.post') def test_create_mass_payouts_multiple_payments_different_currencies( mock_request_post, mock_build_payoneer_headers ): """Test mass payouts with multiple payments in different currencies.""" multi_currency_payments = [ { 'client_reference_id': 'usd_payment', 'account_payee_id': '56732', 'description': 'USD Payment', 'currency': 'USD', 'amount': 100, }, { 'client_reference_id': 'eur_payment', 'account_payee_id': '56733', 'description': 'EUR Payment', 'currency': 'EUR', 'amount': 200, }, { 'client_reference_id': 'gbp_payment', 'account_payee_id': '56734', 'description': 'GBP Payment', 'currency': 'GBP', 'amount': 300, }, ] expected_result = {'result': 'Payments Created'} mock_request_post.return_value = MagicMock( status_code=200, json=MagicMock(return_value=expected_result) ) result = logic.create_mass_payouts(1, multi_currency_payments) assert result.status == 201 assert result.message == expected_result call_args = mock_request_post.call_args payments = call_args[1]['json']['Payments'] assert len(payments) == 3 assert payments[0]['currency'] == 'USD' assert payments[1]['currency'] == 'EUR' assert payments[2]['currency'] == 'GBP' @mock.patch( 'payee.logic.payoneer._build_payoneer_headers', return_value={'mock': 'header'} ) @mock.patch('payee.logic.payoneer.post') def test_create_mass_payouts_with_account_payee_description_sanitization( mock_request_post, mock_build_payoneer_headers ): """Test that account_payee description is also sanitized.""" from tests.utils.factories import AccountPayeeFactory # Create account_payee with payment_description containing special characters AccountPayeeFactory.create( account_payee_id=56732, payment_description='Payment with invalid chars: ###^^^~~~ but also valid text', ) payouts = [ { 'client_reference_id': 'test1', 'account_payee_id': '56732', 'description': 'This will be overridden', 'currency': 'USD', 'amount': 222, } ] expected_result = {'result': 'Payments Created'} mock_request_post.return_value = MagicMock( status_code=200, json=MagicMock(return_value=expected_result) ) result = logic.create_mass_payouts(1, payouts) assert result.status == 201 call_args = mock_request_post.call_args actual_description = call_args[1]['json']['Payments'][0]['description'] # Verify sanitization removed invalid characters but kept valid text assert '###' not in actual_description assert '^^^' not in actual_description assert '~~~' not in actual_description assert 'Payment with invalid chars:' in actual_description assert 'but also valid text' in actual_description @mock.patch( 'payee.logic.payoneer._build_payoneer_headers', return_value={'mock': 'header'} ) @mock.patch('payee.utils.backoff_requests.requests') def test_create_mass_payouts_http_failure( mock_requests, mock_build_payoneer_headers, mock_payoneer_mass_payouts ): """Test for mass payouts logic layer on http failure response.""" mock_requests.post.return_value.json.return_value = None mock_requests.post.return_value.status_code = 500 with pytest.raises(HttpMassPayoutsException) as excinfo: logic.create_mass_payouts(1, mock_payoneer_mass_payouts) assert str(excinfo.value) == str('Server error. Status code: 500') assert isinstance(excinfo.value, HttpMassPayoutsException) assert mock_requests.post.call_count == 3 @mock.patch( 'payee.logic.payoneer._build_payoneer_headers', return_value={'mock': 'header'} ) @mock.patch('payee.logic.payoneer.post') def test_create_mass_payouts_failure( mock_post_requests, mock_build_payoneer_headers, mock_payoneer_mass_payouts, mock_payoneer_mass_payouts_bad_response, ): """Test for mass payouts logic layer on failure response.""" expected_result = mock_payoneer_mass_payouts_bad_response mock_post_requests.return_value.json.return_value = expected_result mock_post_requests.return_value.status_code = 400 with pytest.raises(PayoneerMassPayoutsException) as excinfo: logic.create_mass_payouts(1, mock_payoneer_mass_payouts) assert str(excinfo.value) == str(expected_result['error']) assert isinstance(excinfo.value, PayoneerMassPayoutsException) assert mock_post_requests.called @mock.patch( 'payee.logic.payoneer._build_payoneer_headers', return_value={'mock': 'header'} ) @mock.patch('payee.logic.payoneer.post') def test_create_mass_payouts_collaborator_payout( mock_post, mock_build_payoneer_headers, mock_payoneer_mass_payouts_collaborator ): """Test collaborator payout entry is routed correctly with no DB lookup.""" expected_result = {'result': 'Payments Created'} mock_post.return_value = MagicMock( status_code=200, json=MagicMock(return_value=expected_result) ) result = logic.create_mass_payouts(1, mock_payoneer_mass_payouts_collaborator) assert result.status == 201 call_args = mock_post.call_args payments = call_args[1]['json']['Payments'] assert len(payments) == 1 assert payments[0]['payee_id'] == '99001' assert payments[0]['description'] == 'Collaborator Payout 1' @mock.patch( 'payee.logic.payoneer._build_payoneer_headers', return_value={'mock': 'header'} ) @mock.patch('payee.logic.payoneer.post') def test_create_mass_payouts_collaborator_payout_description_sanitized( mock_post, mock_build_payoneer_headers ): """Test collaborator payout description is sanitized before sending to Payoneer.""" expected_result = {'result': 'Payments Created'} mock_post.return_value = MagicMock( status_code=200, json=MagicMock(return_value=expected_result) ) payouts = [ { 'client_reference_id': 'split:20240101:1001', 'payee_id': 99001, 'description': 'Payment with invalid chars: ###^^^~~~ but also valid text', 'currency': 'USD', 'amount': 100, } ] result = logic.create_mass_payouts(1, payouts) assert result.status == 201 actual_description = mock_post.call_args[1]['json']['Payments'][0]['description'] assert '###' not in actual_description assert '^^^' not in actual_description assert 'but also valid text' in actual_description @mock.patch( 'payee.logic.payoneer._build_payoneer_headers', return_value={'mock': 'header'} ) @mock.patch('payee.logic.payoneer.post') def test_create_mass_payouts_collaborator_payout_empty_description_after_sanitization( mock_post, mock_build_payoneer_headers ): """Test collaborator payout raises ValueError when description is empty after sanitization.""" payouts = [ { 'client_reference_id': 'split:20240101:1001', 'payee_id': 99001, 'description': '###^^^~~~', 'currency': 'USD', 'amount': 100, } ] with pytest.raises(ValueError) as excinfo: logic.create_mass_payouts(1, payouts) assert 'Description is empty after sanitization' in str(excinfo.value) @mock.patch( 'payee.logic.payoneer._build_payoneer_headers', return_value={'mock': 'header'} ) @mock.patch('payee.logic.payoneer.post') def test_create_mass_payouts_mixed_batch( mock_post, mock_build_payoneer_headers, mock_payoneer_mass_payouts_mixed ): """Test a mixed batch of account_payee and collaborator entries is processed correctly.""" expected_result = {'result': 'Payments Created'} mock_post.return_value = MagicMock( status_code=200, json=MagicMock(return_value=expected_result) ) result = logic.create_mass_payouts(1, mock_payoneer_mass_payouts_mixed) assert result.status == 201 payments = mock_post.call_args[1]['json']['Payments'] assert len(payments) == 2 assert payments[0]['payee_id'] == '56732' assert payments[0]['description'] == 'Account Payout Mixed' assert payments[1]['payee_id'] == '99002' assert payments[1]['description'] == 'Collaborator Payout Mixed' @mock.patch( 'payee.logic.payoneer._build_payoneer_headers', return_value={'mock': 'header'} ) @mock.patch('payee.logic.payoneer.post') def test_create_mass_payouts_mixed_batch_account_payee_gets_override( mock_post, mock_build_payoneer_headers, mock_payoneer_mass_payouts_mixed, ): """Test description override applies only to account_payee entry, not collaborator.""" from tests.utils.factories import AccountPayeeFactory AccountPayeeFactory.create( account_payee_id=56732, payment_description='Overridden Description', ) expected_result = {'result': 'Payments Created'} mock_post.return_value = MagicMock( status_code=200, json=MagicMock(return_value=expected_result) ) result = logic.create_mass_payouts(1, mock_payoneer_mass_payouts_mixed) assert result.status == 201 payments = mock_post.call_args[1]['json']['Payments'] assert payments[0]['payee_id'] == '56732' assert payments[0]['description'] == 'Overridden Description' assert payments[1]['payee_id'] == '99002' assert payments[1]['description'] == 'Collaborator Payout Mixed' @patch('payee.logic.payoneer.get_account_id_by_account_payee_id', return_value=None) def test_build_payoneer_redirect_uri_account_id_error(mock_config, monkeypatch): """Test getting brand name on null input values.""" monkeypatch.setattr( 'payee.config.Config.PAYONEER_REDIRECT_URL', mock_config.PAYONEER_REDIRECT_URL ) with pytest.raises(BrandFetchException) as excinfo: build_payoneer_redirect_uri(account_payee_id) assert str(excinfo.value) == ERROR_ACCOUNT_ID_NOT_FOUND.format( account_payee_id=account_payee_id ) assert isinstance(excinfo.value, BrandFetchException) @patch( 'payee.connectors.graphql.GraphQLConnector.execute', return_value={'data': {'orchardLabel': {'companyBrand': {'name': 'awal'}}}}, ) @patch( 'payee.logic.payoneer.get_account_id_by_account_payee_id', return_value=account_id ) def test_build_payoneer_redirect_uri_success_with_account_payee_id( mock_get_account_id, mock_get_graphql_client, mock_config, monkeypatch ): """Test getting brand name success.""" monkeypatch.setattr( 'payee.config.Config.PAYONEER_REDIRECT_URL', mock_config.PAYONEER_REDIRECT_URL ) result = build_payoneer_redirect_uri(account_payee_id) expected_result = DOCUMENTS_AWAL_APP_BRAND mock_get_account_id.assert_called_once_with( account_payee_id=account_payee_id, headers={'Orchard-Roles': 'None'} ) assert result == expected_result mock_get_graphql_client.assert_called_once_with( COMPANY_BRAND_BY_VENDOR_ID, {'vendorId': account_id} ) @patch('payee.logic.payoneer.get_payment_entity_name_by_account_id', return_value=None) @patch( 'payee.logic.payoneer.get_account_id_by_account_payee_id', return_value=account_id ) def test_get_payee_corp_entity_error_corp_entity_empty( mock_account, mock_signing_entity ): """Error retrieving country code from payment entity.""" res = get_payee_corp_entity(account_payee_id) assert res == '' @patch('payee.logic.payoneer.get_account_id_by_account_payee_id', return_value=None) def test_get_payee_corp_entity_error_account_not_found(mock_account): """Error retrieving account_id by account_payee_id from graphql.""" with pytest.raises(CorpEntityFetchException) as excinfo: get_payee_corp_entity(account_payee_id) assert str(excinfo.value) == ERROR_ACCOUNT_ID_NOT_FOUND.format( account_payee_id=account_payee_id ) assert isinstance(excinfo.value, CorpEntityFetchException) @pytest.mark.parametrize('payoneer_program_id', (None, 200100)) @mock.patch('payee.logic.payoneer._get_account_payee_payoneer_program_id') @mock.patch('payee.logic.payoneer._build_payoneer_headers') @mock.patch('payee.logic.payoneer.get') def test_get_payoneer_account_success( mock_get: MagicMock, mock_build_payoneer_headers: MagicMock, mock_get_payee_payoneer_program_id: MagicMock, payoneer_program_id: int | None, ): """Test get_payoneer_account.""" db_program_id = 'PROGRAM_ID_1' mock_get_payee_payoneer_program_id.return_value = db_program_id headers = {'mock': 'header'} mock_build_payoneer_headers.return_value = headers expected_result = {'test': 123} mock_result = MagicMock() mock_result.status_code = 200 mock_result.json.return_value = expected_result mock_get.return_value = mock_result result = logic.get_payoneer_account( account_payee_id, **({'payoneer_program_id': payoneer_program_id} if payoneer_program_id else {}), ) assert result.message == expected_result account_uri = PAYONEER_GET_ACCOUNT_ID_ENDPOINT.format( program_id=payoneer_program_id or db_program_id, payee_id=account_payee_id, ) mock_get.assert_called_once_with( f'{Config.PAYONEER_API_URL}{account_uri}', headers=headers ) if payoneer_program_id: mock_get_payee_payoneer_program_id.assert_not_called() else: mock_get_payee_payoneer_program_id.assert_called_once_with(account_payee_id) @mock.patch( 'payee.logic.payoneer._get_account_payee_payoneer_program_id', return_value='PROGRAM_ID_1', ) @mock.patch( 'payee.logic.payoneer._build_payoneer_headers', return_value={'mock': 'header'} ) @mock.patch('payee.logic.payoneer.get') def test_get_payoneer_account_failure( mock_get_requests, mock_build_payoneer_headers, mock_payoneer_program, mock_payoneer_mass_payouts_bad_response, ): """Test for mass payouts logic layer on failure response.""" expected_result = mock_payoneer_mass_payouts_bad_response mock_get_requests.return_value.json.return_value = expected_result with pytest.raises(PayoneerPayeeDetailsException) as excinfo: logic.get_payoneer_account(account_payee_id) assert str(excinfo.value) == str(expected_result) assert isinstance(excinfo.value, PayoneerPayeeDetailsException) assert mock_get_requests.called @pytest.mark.parametrize('payoneer_program_id', (None, 200100)) @patch('payee.logic.payoneer.release_payee') @patch('payee.logic.payoneer._get_account_payee_payoneer_program_id') def test_release_payee_from_payoneer_success( mock_get_payee_payoneer_program_id: MagicMock, mock_delete_request: MagicMock, payoneer_program_id: int | None, ): """Test release payee from payoneer.""" db_program_id = '123' mock_get_payee_payoneer_program_id.return_value = db_program_id payoneer_release_response = { 'result': {'payee_id': 'testyyy', 'release_date': '2023-03-31T15:54:02-04:00'} } mock_delete_request.return_value = payoneer_release_response res = logic.payoneer_release_payee( account_payee_id, **({'payoneer_program_id': payoneer_program_id} if payoneer_program_id else {}), ) assert res.status == 200 assert res.message == { 'account_payee_id': account_payee_id, 'payoneer_client_reference_id': str(account_payee_id), } if payoneer_program_id: mock_get_payee_payoneer_program_id.assert_not_called() else: mock_get_payee_payoneer_program_id.assert_called_once_with(account_payee_id) mock_delete_request.assert_called_once_with( int(payoneer_program_id or db_program_id), account_payee_id ) @patch('payee.logic.payoneer.release_payee') @patch( 'payee.logic.payoneer._get_account_payee_payoneer_program_id', return_value='123' ) def test_release_payee_from_payoneer_failure( mock_payoneer_details, mock_delete_request ): """Test release payee from payoneer failure.""" error_msg = 'some error' mock_delete_request.side_effect = ReleasePayeeException(error_msg) res = logic.payoneer_release_payee(account_payee_id) assert res.status == 400 assert res.errors['message'] == error_msg mock_payoneer_details.assert_called_once_with(account_payee_id) @patch('payee.logic.payoneer.release_payee') @patch('payee.logic.payoneer._get_account_payee_payoneer_program_id', return_value=None) def test_release_payee_from_payoneer_error_program_id( mock_payoneer_details, mock_delete_request ): """Test release payee from payoneer.""" res = logic.payoneer_release_payee(account_payee_id) assert res.status == 404 assert res.message == { 'error': ERROR_PAYEE_DETAILS_NOT_FOUND.format(id=account_payee_id) } mock_payoneer_details.assert_called_once_with(account_payee_id) mock_delete_request.assert_not_called() @patch('payee.logic.payee_account_state.payoneer.get_payoneer_account') def test_validate_payoneer_active_status_active( mock_payoneer_account, mock_payoneer_response_status_active ): """Test release payee from payoneer.""" mock_payoneer_account.return_value = response.Response( message=mock_payoneer_response_status_active, status=200 ) res = payee_account_state.validate_payoneer_active_status(account_payee_id) assert res mock_payoneer_account.assert_called_once_with(account_payee_id) @patch('payee.logic.payee_account_state.payoneer.get_payoneer_account') def test_validate_payoneer_active_status_error( mock_payoneer_account, mock_payoneer_mass_payouts_bad_response ): """Test release payee from payoneer.""" mock_payoneer_account.return_value = response.Response( message=mock_payoneer_mass_payouts_bad_response, status=404 ) res = payee_account_state.validate_payoneer_active_status(account_payee_id) assert not res mock_payoneer_account.assert_called_once_with(account_payee_id) @pytest.mark.parametrize( 'graphql_output', [ {'data': {'abacusAccountPayee': {'payoneerProgram': None}}}, {'data': {'abacusAccountPayee': None}}, { 'data': { 'abacusAccountPayee': {'payoneerProgram': {'payoneerProgramId': None}} } }, ], ) @patch('payee.logic.payoneer.graphql_client.execute') def test_get_payee_payoneer_program_id_none(mock_graphql, graphql_output): """Test getting payoneer program id from graphql.""" mock_graphql.return_value = graphql_output res = logic._get_account_payee_payoneer_program_id(account_payee_id) assert not res @patch('payee.logic.payoneer.graphql_client.execute') def test_get_payee_payoneer_program_id_success(mock_graphql): """Test getting payoneer program id from graphql.""" mock_graphql.return_value = { 'data': { 'abacusAccountPayee': {'payoneerProgram': {'payoneerProgramId': 1234567}} } } res = logic._get_account_payee_payoneer_program_id(account_payee_id) assert res == str(1234567) @patch( 'payee.logic.payoneer.get_payment_entity_name_by_account_id', return_value='AWAL-UK', ) @patch( 'payee.logic.payoneer.get_account_id_by_account_payee_id', return_value=account_id ) def test_get_payee_corp_entity(mock_account, mock_payment_entity_name): """Test retrieving corp entity from graphql.""" res = get_payee_corp_entity(account_payee_id) assert res == 'AWAL-UK' @patch('payee.logic.payoneer.post') @patch('payee.logic.payoneer._build_payoneer_headers', return_value={'mock': 'header'}) def test_move_payoneer_program(mock_headers, mock_post): """Test move_payoneer_program function.""" account_payee_id = (1,) existing_payoneer_program_id = 2 new_payoneer_program_id = 3 mock_payoneer_data = { 'result': { 'payee_id': existing_payoneer_program_id, 'adopted_to_partner_id': new_payoneer_program_id, 'adopted_payee_id': account_payee_id, 'date_adopted': '2021-03-24T14:45:43-04:00', } } mock_post.return_value = MagicMock( status_code=200, json=MagicMock(return_value=mock_payoneer_data) ) res = logic.move_payoneer_program( account_payee_id, existing_payoneer_program_id, new_payoneer_program_id ) assert mock_post.call_args_list == [ call( f'https://api.sandbox.payoneer.com/v4/programs/' f'{new_payoneer_program_id}/payees/adopt_payee', json={ 'partner_id_to_adopt': existing_payoneer_program_id, 'payee_id_to_adopt': account_payee_id, 'calling_partner_payee_id': account_payee_id, }, headers=mock_headers.return_value, ) ] assert res.status == 200 assert res.message == mock_payoneer_data @patch('payee.logic.payoneer.get') @patch('payee.logic.payoneer._build_payoneer_headers', return_value={'mock': 'header'}) def test_get_payee_status(mock_headers, mock_post): """Test get_payee_status function.""" account_payee_id = (1,) payoneer_program_id = 2 mock_payoneer_data = { 'result': { 'account_id': '3676945', 'status': {'type': 1, 'description': 'Active'}, 'registration_date': '2021-05-05', 'payout_method': {'type': 'BANK', 'currency': 'USD'}, } } mock_post.return_value = MagicMock( status_code=200, json=MagicMock(return_value=mock_payoneer_data) ) res = logic.get_payee_status( account_payee_id, payoneer_program_id, ) assert mock_post.call_args_list == [ call( f'https://api.sandbox.payoneer.com/v4/programs/' f'{payoneer_program_id}/payees/{account_payee_id}/status', headers=mock_headers.return_value, ) ] assert res.status == 200 assert res.message == mock_payoneer_data @patch('payee.logic.payoneer.get') @patch( 'payee.logic.payoneer.PayoneerAuthHelper.build_headers', return_value={'mock': 'header'}, ) @patch('payee.logic.payoneer.AccountPayee') def test_get_payee_register_format_success( mock_account_payee, mock_build_payoneer_headers, mock_get_requests ): """Test get_payee_register_format function success.""" request_data = PayeeRegistrationFormatGetFactory.build() data = PayeeRegistrationFormatFactory.build() payoneer_result = {'result': {'payout_method': data}} payee = AccountPayeeFactory.build() mock_account_payee.get_by_id_or_error.return_value = payee mock_get_requests.return_value.json.return_value = payoneer_result result = get_payee_register_format(payee, **request_data) url = PAYONEER_GET_PAYEE_REGISTER_FORMAT_ENDPOINT.format( program_id=payee.payoneer_program_id, payee_type=request_data['bank_account_type'], country=request_data['bank_country'], currency=request_data['bank_currency'], ) assert result == data mock_get_requests.assert_called_once_with( f'{Config.PAYONEER_API_URL}{url}', headers={'mock': 'header'} ) mock_build_payoneer_headers.assert_called_once() @pytest.mark.parametrize( 'input_country,expected_country', [ ('GB', 'UK'), ('XK', 'KV'), ], ) @patch('payee.logic.payoneer.get') @patch( 'payee.logic.payoneer.PayoneerAuthHelper.build_headers', return_value={'mock': 'header'}, ) @patch('payee.logic.payoneer.AccountPayee') def test_get_payee_register_format_success_countries( mock_account_payee, mock_build_payoneer_headers, mock_get_requests, input_country, expected_country, ): """Test get_payee_register_format function success for different countries.""" request_data = PayeeRegistrationFormatGetFactory.build(bank_country=input_country) data = PayeeRegistrationFormatFactory.build(bank_country=input_country) payoneer_result = {'result': {'payout_method': data}} payee = AccountPayeeFactory.build() mock_account_payee.get_by_id_or_error.return_value = payee mock_get_requests.return_value.json.return_value = payoneer_result result = get_payee_register_format(payee, **request_data) url = PAYONEER_GET_PAYEE_REGISTER_FORMAT_ENDPOINT.format( program_id=payee.payoneer_program_id, payee_type=request_data['bank_account_type'], country=expected_country, currency=request_data['bank_currency'], ) assert result == data mock_get_requests.assert_called_once_with( f'{Config.PAYONEER_API_URL}{url}', headers={'mock': 'header'} ) mock_build_payoneer_headers.assert_called_once() @patch('payee.logic.payoneer.get') @patch( 'payee.logic.payoneer.PayoneerAuthHelper.build_headers', side_effect=NoPayoneerCredentialsException, ) @patch('payee.logic.payoneer.AccountPayee') def test_get_payee_register_format_headers_failure( mock_account_payee, mock_build_payoneer_headers, mock_get_requests ): """Test get_payee_register_format function headers failure.""" with pytest.raises(NoPayoneerCredentialsException): get_payee_register_format( AccountPayeeFactory.build(account_payee_id=1), '', '', '' ) assert not mock_get_requests.called @patch('payee.logic.payoneer.get') @patch( 'payee.logic.payoneer.PayoneerAuthHelper.build_headers', return_value={'mock': 'header'}, ) @patch('payee.logic.payoneer.AccountPayee') def test_get_payee_register_format_payoneer_request_failure( mock_account_payee, mock_build_payoneer_headers, mock_get_requests ): """Test get_payee_register_format function payoneer request failure.""" request_data = PayeeRegistrationFormatGetFactory.build() payee = AccountPayeeFactory.build() mock_account_payee.get_by_id_or_error.return_value = payee mock_get_requests.side_effect = Exception('Error') with pytest.raises(Exception, match='Error'): get_payee_register_format(payee, **request_data) url = PAYONEER_GET_PAYEE_REGISTER_FORMAT_ENDPOINT.format( program_id=payee.payoneer_program_id, payee_type=request_data['bank_account_type'], country=request_data['bank_country'], currency=request_data['bank_currency'], ) mock_get_requests.assert_called_once_with( f'{Config.PAYONEER_API_URL}{url}', headers={'mock': 'header'} ) mock_build_payoneer_headers.assert_called_once() @patch('payee.logic.payoneer.get') @patch( 'payee.logic.payoneer.PayoneerAuthHelper.build_headers', return_value={'mock': 'header'}, ) @patch('payee.logic.payoneer.AccountPayee') def test_get_payee_register_format_payoneer_request_error( mock_account_payee, mock_build_payoneer_headers, mock_get_requests ): """Test get_payee_register_format function payoneer request failure.""" request_data = PayeeRegistrationFormatGetFactory.build() message = {'error': 'test code'} payee = AccountPayeeFactory.build() mock_account_payee.get_by_id_or_error.return_value = payee mock_get_requests.return_value.status_code = HTTPStatus.GATEWAY_TIMEOUT mock_get_requests.return_value.json.return_value = message with pytest.raises( GetRegisterPayeeFormatException, match='Error getting payee registration format' ): get_payee_register_format(payee, **request_data) url = PAYONEER_GET_PAYEE_REGISTER_FORMAT_ENDPOINT.format( program_id=payee.payoneer_program_id, payee_type=request_data['bank_account_type'], country=request_data['bank_country'], currency=request_data['bank_currency'], ) mock_get_requests.assert_called_once_with( f'{Config.PAYONEER_API_URL}{url}', headers={'mock': 'header'} ) mock_build_payoneer_headers.assert_called_once() @mock.patch( 'payee.logic.payoneer.PayoneerAuthHelper.build_headers', return_value={'mock': 'header'}, ) @mock.patch('payee.logic.payoneer.post') def test_register_payee_success( mock_request_post, mock_build_payoneer_headers, mock_register_payee_individual_type_params, ): """Test for successful register payee logic layer.""" payee = AccountPayee(account_payee_id=account_payee_id, payoneer_program_id=1) expected_result = {'result': 'Success'} mock_request_post.return_value = MagicMock( status_code=200, json=MagicMock(return_value=expected_result) ) result = logic.register_payee(payee, mock_register_payee_individual_type_params) assert result == expected_result mock_request_post.assert_called_with( 'https://api.sandbox.payoneer.com/v4/programs/1/payees/register-payee', headers={'mock': 'header'}, json={ 'payee': { 'address': { 'address_line_1': 'asdf', 'city': 'odesa', 'country': 'DE', 'zip_code': '65000', }, 'contact': { 'date_of_birth': '2000-10-10', 'first_name': 'firstName', 'last_name': 'lastName', 'email': 'test@example.com', 'phone': None, 'phone_country': None, }, 'type': 'INDIVIDUAL', }, 'payee_id': account_payee_id, 'payout_method': { 'bank_account_type': 'PERSONAL', 'bank_field_details': [ {'name': 'AccountNumber', 'value': '345234552345'}, {'name': 'AccountName', 'value': 'John Smith'}, {'name': 'BankName', 'value': 'Bank of Hope'}, {'name': 'RoutingNumber', 'value': '122105155'}, {'name': 'AccountType', 'value': 'S'}, ], 'country': 'US', 'currency': 'USD', 'type': 'BANK', }, }, ) @mock.patch( 'payee.logic.payoneer.PayoneerAuthHelper.build_headers', return_value={'mock': 'header'}, ) @mock.patch('payee.logic.payoneer.post') def test_register_payee_success_GB( mock_request_post, mock_build_payoneer_headers, mock_register_payee_individual_type_params_GB, ): """Test for successful register payee logic layer.""" payee = AccountPayee(account_payee_id=account_payee_id, payoneer_program_id=1) expected_result = {'result': 'Success'} mock_request_post.return_value = MagicMock( status_code=200, json=MagicMock(return_value=expected_result) ) result = logic.register_payee(payee, mock_register_payee_individual_type_params_GB) assert result == expected_result mock_request_post.assert_called_with( 'https://api.sandbox.payoneer.com/v4/programs/1/payees/register-payee', headers={'mock': 'header'}, json={ 'payee': { 'address': { 'address_line_1': 'asdf', 'city': 'odesa', 'country': 'UK', 'zip_code': '65000', }, 'contact': { 'date_of_birth': '2000-10-10', 'first_name': 'firstName', 'last_name': 'lastName', 'email': 'test@example.com', 'phone': None, 'phone_country': None, }, 'type': 'INDIVIDUAL', }, 'payee_id': account_payee_id, 'payout_method': { 'bank_account_type': 'PERSONAL', 'bank_field_details': [ {'name': 'AccountNumber', 'value': '345234552345'}, {'name': 'AccountName', 'value': 'John Smith'}, {'name': 'BankName', 'value': 'Bank of Hope'}, {'name': 'RoutingNumber', 'value': '122105155'}, {'name': 'AccountType', 'value': 'S'}, ], 'country': 'UK', 'currency': 'USD', 'type': 'BANK', }, }, ) @mock.patch( 'payee.logic.payoneer.PayoneerAuthHelper.build_headers', return_value={'mock': 'header'}, ) @mock.patch('payee.logic.payoneer.post') def test_register_payee_success_US( mock_request_post, mock_build_payoneer_headers, mock_register_payee_individual_type_params_US, ): """Test for successful register payee logic layer.""" payee = AccountPayee(account_payee_id=account_payee_id, payoneer_program_id=1) expected_result = {'result': 'Success'} mock_request_post.return_value = MagicMock( status_code=200, json=MagicMock(return_value=expected_result) ) result = logic.register_payee(payee, mock_register_payee_individual_type_params_US) assert result == expected_result mock_request_post.assert_called_with( 'https://api.sandbox.payoneer.com/v4/programs/1/payees/register-payee', headers={'mock': 'header'}, json={ 'payee': { 'address': { 'address_line_1': 'asdf', 'address_line_2': 'dfgh', 'city': 'odesa', 'country': 'US', 'state': 'AZ', 'zip_code': '65000', }, 'contact': { 'date_of_birth': '2000-10-10', 'first_name': 'firstName', 'last_name': 'lastName', 'email': 'test@example.com', 'phone': None, 'phone_country': None, }, 'type': 'INDIVIDUAL', }, 'payee_id': account_payee_id, 'payout_method': { 'bank_account_type': 'PERSONAL', 'bank_field_details': [ {'name': 'AccountNumber', 'value': '345234552345'}, {'name': 'AccountName', 'value': 'John Smith'}, {'name': 'BankName', 'value': 'Bank of Hope'}, {'name': 'RoutingNumber', 'value': '122105155'}, {'name': 'AccountType', 'value': 'S'}, ], 'country': 'US', 'currency': 'USD', 'type': 'BANK', }, }, ) @mock.patch( 'payee.logic.payoneer.PayoneerAuthHelper.build_headers', return_value={'mock': 'header'}, ) @mock.patch('payee.utils.backoff_requests.requests') def test_register_payee_failure( mock_requests, mock_build_payoneer_headers, mock_register_payee_individual_type_params, mock_payoneer_register_payee_bad_response, ): """Test for register payee logic layer on failure response.""" payee = AccountPayee(account_payee_id=account_payee_id, payoneer_program_id=1) expected_result = mock_payoneer_register_payee_bad_response mock_requests.post.return_value.json.return_value = expected_result mock_requests.post.return_value.status_code = 400 with pytest.raises(RegisterPayeeException) as excinfo: logic.register_payee(payee, mock_register_payee_individual_type_params) assert str(excinfo.value) == str(expected_result) assert mock_requests.post.called @mock.patch( 'payee.logic.payoneer.PayoneerAuthHelper.build_headers', return_value={'mock': 'header'}, ) @mock.patch('payee.logic.payoneer.post') def test_update_payee_success( mock_request_post, mock_build_payoneer_headers, mock_register_payee_individual_type_params, ): """Test for successful update payee logic layer.""" expected_result = {'result': 'Success'} mock_request_post.return_value = MagicMock( status_code=200, json=MagicMock(return_value=expected_result) ) result = logic.update_payout_methods( 1, mock_register_payee_individual_type_params['account_payee_id'], mock_register_payee_individual_type_params, ) assert result == expected_result mock_request_post.assert_called_with( 'https://api.sandbox.payoneer.com/v4/programs/1/payees/123/payout-methods', headers={'mock': 'header'}, json={ 'bank_account_type': 'PERSONAL', 'bank_field_details': mock_register_payee_individual_type_params[ 'bank_field_details' ], 'country': 'US', 'currency': 'USD', 'type': 'BANK', }, ) @mock.patch( 'payee.logic.payoneer.PayoneerAuthHelper.build_headers', return_value={'mock': 'header'}, ) @mock.patch('payee.utils.backoff_requests.requests') def test_update_payee_failure( mock_requests, mock_build_payoneer_headers, mock_register_payee_individual_type_params, mock_payoneer_register_payee_bad_response, ): """Test for update payee logic layer on failure response.""" expected_result = mock_payoneer_register_payee_bad_response mock_requests.post.return_value.json.return_value = expected_result mock_requests.post.return_value.status_code = 400 with pytest.raises(UpdatePayeeException) as excinfo: logic.update_payout_methods(1, 2, mock_register_payee_individual_type_params) assert str(excinfo.value) == str(expected_result) assert mock_requests.post.called @mock.patch( 'payee.logic.payoneer.PayoneerAuthHelper.build_headers', return_value={'mock': 'header'}, ) @mock.patch('payee.logic.payoneer.get') def test_get_payee_details_success(mock_get, mock_build_payoneer_headers): """Test for get_payee_details.""" mock_details = BankDetailsIndividualTypeNestedFactory.build() mock_get.return_value = MagicMock( status_code=200, json=MagicMock(return_value={'result': mock_details}) ) res = logic.get_payee_details(1, 2) assert res == mock_details mock_get.assert_called_with( 'https://api.sandbox.payoneer.com/v4/programs/1/payees/2/details', headers={'mock': 'header'}, ) @mock.patch( 'payee.logic.payoneer.PayoneerAuthHelper.build_headers', return_value={'mock': 'header'}, ) @mock.patch('payee.logic.payoneer.get') def test_get_payee_details_not_found(mock_get, mock_build_payoneer_headers): """Test for get_payee_details.""" mock_get.return_value = MagicMock(status_code=404) res = logic.get_payee_details(1, 2) assert res == None mock_get.assert_called_with( 'https://api.sandbox.payoneer.com/v4/programs/1/payees/2/details', headers={'mock': 'header'}, ) @mock.patch( 'payee.logic.payoneer.PayoneerAuthHelper.build_headers', return_value={'mock': 'header'}, ) @mock.patch('payee.logic.payoneer.get') def test_get_payee_details_no_payee_success(mock_get, mock_build_payoneer_headers): """Test for get_payee_details.""" mock_get.return_value = MagicMock(status_code=400) with pytest.raises(GetPayeeDetailsException): logic.get_payee_details(1, 2) mock_get.assert_called_with( 'https://api.sandbox.payoneer.com/v4/programs/1/payees/2/details', headers={'mock': 'header'}, ) # Tests for get_payoneer_service_status function @pytest.mark.parametrize( 'indicator,expected_status', [ ('none', PAYONEER_SERVICE_STATUSES.OPERATIONAL), ('major', PAYONEER_SERVICE_STATUSES.OUTAGE), ('minor', PAYONEER_SERVICE_STATUSES.OUTAGE), ('critical', PAYONEER_SERVICE_STATUSES.OUTAGE), ], ) @patch('payee.logic.payoneer.get') def test_get_payoneer_service_status_with_indicators( mock_get, indicator, expected_status ): """Test get_payoneer_service_status returns correct status codes for various indicators.""" mock_response = MagicMock() mock_response.json.return_value = {'status': {'indicator': indicator}} mock_get.return_value = mock_response result = logic.get_payoneer_service_status() expected_status_code = PAYONEER_STATUS_CODE[expected_status] assert result == expected_status_code mock_get.assert_called_once_with( f'{Config.PAYONEER_STATUS_URL}{PAYONEER_STATUS_ENDPOINT}' ) @pytest.mark.parametrize( 'response_data', [ {'status': {}}, # No indicator key {'status': {'indicator': ''}}, # Empty indicator {'status': {'indicator': None}}, # Null indicator {}, # No status key ], ) @patch('payee.logic.payoneer.get') def test_get_payoneer_service_status_unknown_cases(mock_get, response_data): """Test get_payoneer_service_status returns unknown status for edge cases.""" mock_response = MagicMock() mock_response.json.return_value = response_data mock_get.return_value = mock_response result = logic.get_payoneer_service_status() expected_status_code = PAYONEER_STATUS_CODE[PAYONEER_SERVICE_STATUSES.UNKNOWN] assert result == expected_status_code # Should be 98 mock_get.assert_called_once_with( f'{Config.PAYONEER_STATUS_URL}{PAYONEER_STATUS_ENDPOINT}' ) @pytest.mark.parametrize( 'error_source,side_effect', [ ('http', Exception('HTTP error')), ('json', ValueError('Invalid JSON')), ], ) @patch('payee.logic.payoneer.logger') @patch('payee.logic.payoneer.get') def test_get_payoneer_service_status_error_handling( mock_get, mock_logger, error_source, side_effect ): """Test get_payoneer_service_status handles errors gracefully.""" if error_source == 'http': mock_get.side_effect = side_effect else: # json error mock_response = MagicMock() mock_response.json.side_effect = side_effect mock_get.return_value = mock_response result = logic.get_payoneer_service_status() expected_status_code = PAYONEER_STATUS_CODE[PAYONEER_SERVICE_STATUSES.UNKNOWN] assert result == expected_status_code # Should be 98 mock_get.assert_called_once_with( f'{Config.PAYONEER_STATUS_URL}{PAYONEER_STATUS_ENDPOINT}' ) mock_logger.error.assert_called_once()