"""Test suite for TransferWise logic tier.""" import copy from datetime import datetime from types import SimpleNamespace from unittest.mock import ANY, patch from urllib.parse import urlencode from oto import response, status import pytest from collaborator import config from collaborator.constants import ( email, error, transferwise_batch, transferwise_transfer, ) from collaborator.constants.transferwise_profile import TransferwiseProfileStatus from collaborator.logic import collaborator, transferwise from collaborator.models.rds.collaborator_persister import CollaboratorPersister from collaborator.models.rds.recipient_persister import RecipientPersister from collaborator.models.rds.statement_period_persister import StatementPeriodPersister from collaborator.models.rds.transaction_persister import TransactionPersister from collaborator.models.rds.transferwise_batch_persister import ( TransferwiseBatchPersister, ) from collaborator.models.rds.transferwise_profile_persister import ( TransferwiseProfilePersister, ) from collaborator.models.rds.transferwise_transaction_persister import ( TransferwiseTransactionPersister, ) from collaborator.models.transferwise import Transferwise from collaborator.utils import logging from collaborator.utils.error import OwsError from collaborator.utils.typing import Account, User from tests.testutils import ( assert_response, transferwise_batch_fixtures, user_fixtures, vendor_fixtures, ) from tests.testutils import transferwise_fixtures as fixtures @pytest.mark.parametrize( "original, expected", [ ( {"test": "lol", "whatever": "③"}, {"test": "lol", "whatever": "③"}, ), ( { "id": 123, "access_token": "yes", "refresh_token": "no", "refresh_token_created_at": "no", }, {"id": 123}, ), ], ) def test_filter_profile_data(original, expected): """Test filtering profile responses.""" result = transferwise._filter_profile_data(original) assert result == expected @patch.object(Transferwise, "refresh_account_requirements") def test_refresh_account_requirements(refresh_account_requirements_mock): """Test refresh account requirements.""" refresh_account_requirements_mock.return_value = ( fixtures.ACCOUNT_REQUIREMENTS_RESPONSE ) source = "USD" target = "USD" payload = fixtures.REFRESH_ACCOUNT_REQUIREMENTS result = transferwise.refresh_account_requirements("USD", "USD", payload) assert result.status == 200 assert result.message == fixtures.ACCOUNT_REQUIREMENTS_RESPONSE refresh_account_requirements_mock.assert_called_with(source, target, payload) @patch.object(Transferwise, "get_account_requirements") def test_get_account_requirements(get_account_requirements_mock): """Test fetching account requirements.""" get_account_requirements_mock.return_value = fixtures.ACCOUNT_REQUIREMENTS_RESPONSE source = "USD" target = "USD" result = transferwise.get_account_requirements("USD", "USD") assert result.status == 200 assert result.message == fixtures.ACCOUNT_REQUIREMENTS_RESPONSE get_account_requirements_mock.assert_called_with(source, target) @patch.object(Transferwise, "get_field_validation") def test_get_field_validation(get_field_validation_mock): """Test fetching account requirements.""" get_field_validation_mock.return_value = fixtures.FIELD_VALIDATION_RESPONSE validator = "sort-code" query_string_params = "sortCode=231470" result = transferwise.get_field_validation(validator, query_string_params) assert result.status == 200 assert result.message == fixtures.FIELD_VALIDATION_RESPONSE get_field_validation_mock.assert_called_with(validator, query_string_params) @pytest.mark.parametrize( ( "profile_id", "code", "account", "subaccount_id", "company_brand", "expected_redirect_url", ), [ ( 76, "tempcode123", Account("vendor", "24601"), None, "whatever", config.TRANSFERWISE_POST_AUTHORIZATION_REDIRECT_URLS["theorchard"], ), ( 42, "letmein", Account("vendor", "1234"), 4321, "awal", config.TRANSFERWISE_POST_AUTHORIZATION_REDIRECT_URLS["awal"], ), ], ) @patch.object(transferwise, "ows_abacus_account") @patch.object(transferwise, "ows_account") @patch.object(transferwise, "g") @patch.object(logging, "log_event") @patch.object(transferwise, "Transferwise") @patch.object(transferwise, "TransferwiseProfilePersister") def test_create_or_update_profile_create_success( TransferwiseProfilePersister_mock, Transferwise_mock, log_event_mock, g_mock, ows_account_mock, ows_abacus_account_mock, profile_id, code, account, subaccount_id, company_brand, expected_redirect_url, mock_user, ): """Test successfully creating a profile.""" currency = "USD" access_token = "abc" refresh_token = "jfd-gd234" profile = {"id": 99, "profile_id": 76, "vendor_id": 24601, "subaccount_id": None} verification_status = "VERIFIED" Transferwise_mock().create_oauth_token.return_value = { "access_token": access_token, "refresh_token": refresh_token, } Transferwise_mock().get_profile_verification_status.return_value = ( verification_status ) ows_abacus_account_mock.get_abacus_account_metadata.return_value = { "currency_code": currency } ows_account_mock.get_account_metadata.return_value = { "company_brand": company_brand } TransferwiseProfilePersister_mock.get_active_profile.side_effect = ( OwsError.not_found("test") ) TransferwiseProfilePersister_mock.create_profile.return_value = profile result = transferwise.create_or_update_profile( account, subaccount_id, mock_user, profile_id, code ) assert result.status == status.CREATED assert result.message == profile Transferwise_mock().create_oauth_token.assert_called_with( code, expected_redirect_url ) Transferwise_mock().get_profile_verification_status.assert_called_with( profile_id, currency, access_token ) TransferwiseProfilePersister_mock.create_profile.assert_called_with( account, subaccount_id, profile_id, access_token, refresh_token, verification_status, ) log_event_mock.assert_called_with( logging.LOG_EVENT_CREATE, "transferwise_profile", 99, None, profile, mock_user ) @pytest.mark.parametrize( ( "profile_id", "code", "account", "subaccount_id", "old_profile", "company_brand", "expected_redirect_url", ), [ ( 76, "tempcode123", Account("vendor", "24601"), None, {"id": 12}, "theorchard", config.TRANSFERWISE_POST_AUTHORIZATION_REDIRECT_URLS["theorchard"], ), ( 42, "letmein", Account("vendor", "1234"), 4321, {"id": 34}, "awal", config.TRANSFERWISE_POST_AUTHORIZATION_REDIRECT_URLS["awal"], ), ], ) @patch.object(transferwise, "ows_abacus_account") @patch.object(transferwise, "ows_account") @patch.object(transferwise, "g") @patch.object(transferwise, "Transferwise") @patch.object(transferwise, "TransferwiseProfilePersister") @patch.object(logging, "log_event") def test_create_or_update_profile_update_success( log_event_mock, TransferwiseProfilePersister_mock, Transferwise_mock, g_mock, ows_account_mock, ows_abacus_account_mock, profile_id, code, account, subaccount_id, old_profile, company_brand, expected_redirect_url, mock_user, ): """Test successfully updating a profile.""" currency = "LVL" access_token = "abc" refresh_token = "jfd-gd234" profile = {"id": 99, "profile_id": 76, "vendor_id": 24601, "subaccount_id": None} verification_status = "VERIFIED" Transferwise_mock().create_oauth_token.return_value = { "access_token": access_token, "refresh_token": refresh_token, } Transferwise_mock().get_profile_verification_status.return_value = ( verification_status ) ows_abacus_account_mock.get_abacus_account_metadata.return_value = { "currency_code": currency } ows_account_mock.get_account_metadata.return_value = { "company_brand": company_brand } TransferwiseProfilePersister_mock.get_active_profile.return_value = old_profile TransferwiseProfilePersister_mock.update_active_profile.return_value = profile result = transferwise.create_or_update_profile( account, subaccount_id, mock_user, profile_id, code ) assert result.status == status.OK assert result.message == profile Transferwise_mock().create_oauth_token.assert_called_with( code, expected_redirect_url ) Transferwise_mock().get_profile_verification_status.assert_called_with( profile_id, currency, access_token ) TransferwiseProfilePersister_mock.update_active_profile.assert_called_with( account, subaccount_id, profile_id, access_token, refresh_token, verification_status, ) log_event_mock.assert_called_with( logging.LOG_EVENT_UPDATE, "transferwise_profile", 99, old_profile, profile, mock_user, ) @pytest.mark.parametrize( ("company_brand", "expected_post_auth_redirect_url"), [ ( "theorchard", config.TRANSFERWISE_POST_AUTHORIZATION_REDIRECT_URLS["theorchard"], ), ( "awal", config.TRANSFERWISE_POST_AUTHORIZATION_REDIRECT_URLS["awal"], ), ( "unknown_brand", config.TRANSFERWISE_POST_AUTHORIZATION_REDIRECT_URLS["theorchard"], ), ], ) @patch.object(transferwise, "ows_account") def test_get_authorization_redirect_url( ows_account_mock, company_brand, expected_post_auth_redirect_url ): """Test getting the authorization flow redirect URL.""" account = Account("vendor", 24601) ows_account_mock.get_account_metadata.return_value = { "company_brand": company_brand } result = transferwise.get_authorization_redirect_url(account) assert result == "{}?{}".format( config.TRANSFERWISE_AUTHORIZATION_REDIRECT_URL, urlencode( { "client_id": config.TRANSFERWISE_CLIENT_ID, "redirect_uri": expected_post_auth_redirect_url, } ), ) @patch.object(collaborator, "update_collaborator") @patch.object(RecipientPersister, "create_recipient") @patch.object(Transferwise, "create_recipient") @patch.object(transferwise, "send_email_to_master_contact") def test_create_recipient( send_email_to_master_contact_mock, tw_create_recipient_mock, recipient_create_recipient_mock, update_collaborator_mock, ): """Test create a recipient.""" tw_create_recipient_mock.return_value = { "id": 1, "currency": "USD", "accountHolderName": "Shady", } recipient_create_recipient_mock.return_value = { "id": 1, "currency": "USD", "name": "Shady", "created_by": "some:1", "created_date": "2020-07-15 16:09:27", } update_collaborator_mock.return_value = dict( collaborator_id=1, recipient_id=1, name="Frank Zappa" ) account = Account(type="vendor", id=1) user = User(type="some", id=1) collaborator_id = 1 result = transferwise.create_recipient(account, {}, collaborator_id, user) assert result == dict(collaborator_id=1, recipient_id=1, name="Frank Zappa") recipient_create_recipient_mock.assert_called_with(1, "Shady", "USD", user) update_collaborator_mock.assert_called_with(1, {"recipient_id": 1}, account, user) send_email_to_master_contact_mock.assert_called_with( account, user, "RECIPIENT", {"collaborator_name": "Frank Zappa"} ) @patch.object(transferwise, "collaborator") @patch.object(RecipientPersister, "delete_recipient") @patch.object(transferwise, "send_email_to_master_contact") def test_delete_recipient( send_email_to_master_contact_mock, recipient_delete_recipient_mock, collaborator_mock, ): """Test delete a recipient.""" collaborator_mock.get_by_id_and_account.return_value = dict( collaborator_id=1, recipient_id=1, recipient_currency="USD", recipient_name="Someone going bye bye", ) collaborator_mock.update_collaborator.return_value = dict( collaborator_id=1, name="Frank Zappa", recipient_id=None, recipient_currency=None, recipient_name=None, ) account = Account(type="vendor", id=1) user = User(type="some", id=1) collaborator_id = 1 recipient_id = 1 result = transferwise.delete_recipient(account, collaborator_id, recipient_id, user) assert result == dict( collaborator_id=1, name="Frank Zappa", recipient_id=None, recipient_currency=None, recipient_name=None, ) collaborator_mock.get_by_id_and_account.assert_called_with(collaborator_id, account) recipient_delete_recipient_mock.assert_called_with(1, user) collaborator_mock.update_collaborator.assert_called_with( collaborator_id, {"recipient_id": None}, account, user ) send_email_to_master_contact_mock.assert_called_with( account, user, "RECIPIENT_DELETED", {"collaborator_name": "Frank Zappa"} ) @patch.object(TransferwiseProfilePersister, "get_active_profile") def test_get_for_account(get_profile_mock): """Test getting a profile for an account.""" account = Account("vendor", 24601) transferwise.get_vendor_profile(account) get_profile_mock.assert_called_with(account, None) @patch.object(transferwise, "TransferwiseProfilePersister") def test_get_vendor_profile(persister_mock, mock_account): """Test getting a vendor profile.""" persister_mock.get_active_profile.return_value = { "id": 123, "access_token": "hideme", "refresh_token": "token", "created": datetime(2012, 1, 2, 3, 4, 5), } result = transferwise.get_vendor_profile(mock_account) assert isinstance(result, response.Response) assert result.status == status.OK assert result.message == {"id": 123, "created": "2012-01-02T03:04:05"} persister_mock.get_active_profile.assert_called_with(mock_account, None) @pytest.mark.parametrize( "profile_id, input_status, expected_status", [ (1, "verified", TransferwiseProfileStatus.VERIFIED), (2, "not_verified", TransferwiseProfileStatus.UNVERIFIED), (3, "verified", TransferwiseProfileStatus.VERIFIED), ], ) @patch.object(transferwise, "TransferwiseProfilePersister") @patch.object(transferwise, "g") def test_update_verification_status( g_mock, persister_mock, profile_id, input_status, expected_status ): """Test updating verification status.""" persister_mock.update_profile_status.return_value = [{"id": 1}, {"id": 2}] result = transferwise.update_verification_status(profile_id, input_status) assert result.status == status.OK persister_mock.update_profile_status.assert_called_with(profile_id, expected_status) g_mock.ows.log.warning.assert_any_call("TW_PROFILE Updated: [1, 2]") @pytest.mark.parametrize( "profile_id, profile_status", [ (1, "fail"), (2, "unverified"), (3, "test"), ], ) @patch.object(transferwise, "TransferwiseProfilePersister") @patch.object(transferwise, "g") def test_update_verification_status_unhandled_status( g_mock, persister_mock, profile_id, profile_status ): """Test updating verification status with an unhandled status.""" with pytest.raises(Exception) as err: transferwise.update_verification_status(profile_id, profile_status) assert str(err.value) == f'Unhandled profile status "{profile_status}"' persister_mock.update_profile_status.assert_not_called() @pytest.mark.parametrize( "name, trigger_on", [ ("TEST webhook", "profile_verification"), ("State change", "transfers_state"), ], ) @patch.object(transferwise, "Transferwise") def test_subscribe_application_to_event(Transferwise_mock, name, trigger_on): """Test subscribe to application event.""" Transferwise_mock().get_application_subscriptions.return_value = [] Transferwise_mock().subscribe_application_to_event.return_value = ( fixtures.WEBHOOK_SUBSCRIPTION_RESPONSE ) result = transferwise.subscribe_application_to_event(name, trigger_on) assert result.status == status.OK assert result.message == fixtures.WEBHOOK_SUBSCRIPTION_RESPONSE Transferwise_mock().subscribe_application_to_event.assert_called_with( name, trigger_on ) @pytest.mark.parametrize( "name, trigger_on", [ ("TEST webhook", "test"), ("State change", "derp"), ], ) @patch.object(transferwise, "Transferwise") def test_subscribe_application_to_event_invalid(Transferwise_mock, name, trigger_on): """Test subscribing to an application event which doesn't exist.""" Transferwise_mock().get_application_subscriptions.return_value = [] Transferwise_mock().subscribe_application_to_event.return_value = ( fixtures.WEBHOOK_SUBSCRIPTION_RESPONSE ) with pytest.raises(OwsError) as error_info: transferwise.subscribe_application_to_event(name, trigger_on) assert error_info.value.status == status.NOT_FOUND Transferwise_mock().subscribe_application_to_event.assert_not_called() @patch.object(transferwise, "Transferwise") def test_subscribe_application_to_event_exists(Transferwise_mock): """Test subscribing to an application event which already exists.""" name = "TEST webhook" trigger_on = "profile_verification" Transferwise_mock().get_application_subscriptions.return_value = [ "profiles#verification-state-change" ] Transferwise_mock().subscribe_application_to_event.return_value = ( fixtures.WEBHOOK_SUBSCRIPTION_RESPONSE ) with pytest.raises(OwsError) as error_info: transferwise.subscribe_application_to_event(name, trigger_on) assert error_info.value.status == status.BAD_REQUEST Transferwise_mock().subscribe_application_to_event.assert_not_called() @patch.object(Transferwise, "ping_api") @patch.object(Transferwise, "create_quote") def test_create_quote(create_quote_mock, ping_api_mock): """Test creating a quote.""" ping_api_mock.return_value = "ACTIVATED" create_quote_mock.return_value = fixtures.QUOTE_RESPONSE account = Account(type="vendor", id=1) payload = {"quotes": [fixtures.QUOTE_REQUESTED_PARAMS]} result = transferwise.create_quote(account, payload) assert result.status == 200 assert result.message == {"quotes": [fixtures.QUOTE_RESPONSE]} ping_api_mock.assert_called_with("123fffa33") @patch.object(Transferwise, "ping_api") @patch.object(Transferwise, "create_quote") def test_create_quote_partial_error(create_quote_mock, ping_api_mock): """Test creating a quote with one request resulting in an error.""" ping_api_mock.return_value = "ACTIVATED" raised_error = OwsError( message=( '{"errors":[{"code":"error.greaterThanZero",' '"message":"Please type in a number that\'s ' 'larger than 0.","path":"targetAmount","arguments":[]}]}' ), status=400, code="transferwise_post_request_error", ) create_quote_mock.side_effect = [ fixtures.QUOTE_RESPONSE, raised_error, fixtures.QUOTE_RESPONSE, ] account = Account(type="vendor", id=1) payload = { "quotes": [ fixtures.QUOTE_REQUESTED_PARAMS, fixtures.QUOTE_REQUESTED_PARAMS, fixtures.QUOTE_REQUESTED_PARAMS, ] } with pytest.raises(OwsError) as err: transferwise.create_quote(account, payload) assert err.value == raised_error ping_api_mock.assert_called_with("123fffa33") @patch.object(transferwise, "Transferwise") @patch.object(transferwise, "TransferwiseTransactionPersister") @patch.object(transferwise, "send_email_to_master_contact") @patch.object(transferwise, "TransferwiseBatchPersister") def test_create_batch_payment( TransferwiseBatchPersister_mock, send_email_mock, TransferwiseTransactionPersister_mock, Transferwise_mock, mock_account, mock_user, ): """Test create payment.""" Transferwise_mock().get_quote.return_value = fixtures.QUOTE_RESPONSE Transferwise_mock().create_batch.return_value = fixtures.BATCH_RESPONSE Transferwise_mock().create_batch_transfer.return_value = ( fixtures.BATCH_TRANSFER_RESPONSE ) Transferwise_mock().get_batch_state.return_value = fixtures.BATCH_RESPONSE_INFO Transferwise_mock().modify_batch_state.return_value = ( fixtures.BATCH_COMPLETED_RESPONSE ) ( TransferwiseTransactionPersister_mock.get_by_quote_ids_for_vendor_profile.return_value ) = [] TransferwiseBatchPersister_mock.create_batch.return_value = ( transferwise_batch_fixtures.TRANSFERWISE_BATCH ) TransferwiseTransactionPersister_mock.save_transactions.return_value = ( fixtures.SAVE_TRANSACTIONS_RESPONSE ) TransferwiseBatchPersister_mock.update_batch.return_value = ( transferwise_batch_fixtures.TRANSFERWISE_BATCH ) result = transferwise.create_batch_payment( mock_account, mock_user, copy.deepcopy(fixtures.CREATE_PAYMENT_PAYLOAD) ) assert result.status == 200 assert result.message == transferwise_batch_fixtures.TRANSFERWISE_BATCH TransferwiseTransactionPersister_mock.save_transactions.assert_called_with( fixtures.EXPECTED_SAVE_TRANSFERWISE_TRANSACTIONS_PAYLOAD ) send_email_mock.assert_called_with(mock_account, mock_user, "PAYMENT") TransferwiseBatchPersister_mock.create_batch.assert_called_with( mock_user, { "account_number": "77643994", "account_type": None, "amount": 123.45, "bank_code": "231470", "batch_id": "54a6bc09-cef9-49a8-9041-f1f0c654cd88", "batch_name": "my-batch-group", "batch_type": "bank_transfer", "bban": None, "currency": "USD", "iban": None, "payin_name": "TransferWise", "profile_id": 4497, "reference": "XYZ123", "status": "COMPLETED", "vendor_id": 24601, "version": 1, "bank_address_name": None, "bank_address_branch_name": None, "bank_address_first_line": None, "bank_address_post_code": None, "bank_address_city": None, "bank_address_state_code": None, "bank_address_country": None, "tw_address_name": None, "tw_address_first_line": None, "tw_address_post_code": None, "tw_address_city": None, "tw_address_state_code": None, "tw_address_country": None, }, ) TransferwiseBatchPersister_mock.update_batch.assert_called_with( 6, {"wire_fee": 0.1, "transferwise_fee": 0.92}, mock_user ) @patch.object(transferwise, "Transferwise") @patch.object(transferwise, "TransferwiseTransactionPersister") @patch.object(transferwise, "TransferwiseBatchPersister") def test_create_batch_payment_returns_existing_batch_for_same_quotes( TransferwiseBatchPersister_mock, TransferwiseTransactionPersister_mock, Transferwise_mock, mock_account, mock_user, ): """Test retrying a completed quote set returns the existing batch.""" existing_batch = transferwise_batch_fixtures.TRANSFERWISE_BATCH payload = copy.deepcopy(fixtures.CREATE_PAYMENT_PAYLOAD) quote_id = payload["transfers"][0]["quoteUuid"] TransferwiseTransactionPersister_mock.get_by_quote_ids_for_vendor_profile.return_value = [ { "quote_id": quote_id, "transferwise_batch_id": existing_batch["id"], } ] TransferwiseBatchPersister_mock.get_by_id.return_value = existing_batch result = transferwise.create_batch_payment(mock_account, mock_user, payload) assert result.status == 200 assert result.message == existing_batch Transferwise_mock().create_batch.assert_not_called() TransferwiseBatchPersister_mock.create_batch.assert_not_called() TransferwiseBatchPersister_mock.get_by_id.assert_called_with(existing_batch["id"]) @patch.object(transferwise, "Transferwise") @patch.object(transferwise, "TransferwiseTransactionPersister") def test_create_batch_payment_rejects_duplicate_quotes_in_request( TransferwiseTransactionPersister_mock, Transferwise_mock, mock_account, mock_user, ): """Test duplicate quote IDs in one request are rejected before Wise calls.""" payload = copy.deepcopy(fixtures.CREATE_PAYMENT_PAYLOAD) payload["transfers"].append(copy.deepcopy(payload["transfers"][0])) with pytest.raises(OwsError) as err: transferwise.create_batch_payment(mock_account, mock_user, payload) assert err.value.status == status.BAD_REQUEST assert err.value.code == error.ERROR_CODE_BAD_PARAMS TransferwiseTransactionPersister_mock.get_by_quote_ids_for_vendor_profile.assert_not_called() Transferwise_mock().create_batch.assert_not_called() @patch.object(transferwise, "Transferwise") @patch.object(transferwise, "TransferwiseTransactionPersister") @patch.object(transferwise, "send_email_to_master_contact") @patch.object(transferwise, "TransferwiseBatchPersister") def test_create_batch_jpy_payment( TransferwiseBatchPersister_mock, send_email_mock, TransferwiseTransactionPersister_mock, Transferwise_mock, mock_account, mock_user, ): """Test create payment.""" Transferwise_mock().get_quote.return_value = fixtures.QUOTE_RESPONSE Transferwise_mock().create_batch.return_value = fixtures.BATCH_RESPONSE_JPY Transferwise_mock().create_batch_transfer.return_value = ( fixtures.BATCH_TRANSFER_RESPONSE ) Transferwise_mock().get_batch_state.return_value = fixtures.BATCH_RESPONSE_INFO_JPY Transferwise_mock().modify_batch_state.return_value = ( fixtures.BATCH_COMPLETED_RESPONSE_JPY ) ( TransferwiseTransactionPersister_mock.get_by_quote_ids_for_vendor_profile.return_value ) = [] TransferwiseBatchPersister_mock.create_batch.return_value = ( transferwise_batch_fixtures.TRANSFERWISE_BATCH_JPY ) TransferwiseTransactionPersister_mock.save_transactions.return_value = ( fixtures.SAVE_TRANSACTIONS_RESPONSE ) TransferwiseBatchPersister_mock.update_batch.return_value = ( transferwise_batch_fixtures.TRANSFERWISE_BATCH_JPY ) result = transferwise.create_batch_payment( mock_account, mock_user, fixtures.CREATE_PAYMENT_PAYLOAD_JPY ) assert result.status == 200 assert result.message == transferwise_batch_fixtures.TRANSFERWISE_BATCH_JPY TransferwiseTransactionPersister_mock.save_transactions.assert_called_with( fixtures.EXPECTED_SAVE_TRANSFERWISE_TRANSACTIONS_PAYLOAD ) send_email_mock.assert_called_with(mock_account, mock_user, "PAYMENT") TransferwiseBatchPersister_mock.create_batch.assert_called_with( mock_user, { "account_number": "0785985", "account_type": "普通 / Futsuu / フツウ", "amount": 123.45, "bank_code": "恵比寿支店 / EBISU / エビス", "batch_id": "54a6bc09-cef9-49a8-9041-f1f0c654cd88", "batch_name": "my-batch-group", "batch_type": "bank_transfer", "bban": None, "currency": "JPY", "iban": None, "payin_name": "ワイズ ペイメンツ ジヤパン(カ (translation: WISE " "PAYMENTS JAPAN KABUSHIKIGAISHA) 三菱UFJ銀行" " / MUFG Bank, Ltd. / ミツビシユーエフジエイ", "profile_id": 4497, "reference": "XYZ123", "status": "COMPLETED", "vendor_id": 24601, "version": 1, "bank_address_name": None, "bank_address_branch_name": None, "bank_address_first_line": None, "bank_address_post_code": None, "bank_address_city": None, "bank_address_state_code": None, "bank_address_country": None, "tw_address_name": None, "tw_address_first_line": None, "tw_address_post_code": None, "tw_address_city": None, "tw_address_state_code": None, "tw_address_country": None, }, ) TransferwiseBatchPersister_mock.update_batch.assert_called_with( 6, {"wire_fee": 0.1, "transferwise_fee": 0.92}, mock_user ) @patch("collaborator.models.ows.ows_account.get_account_metadata") @patch("collaborator.models.ows.ows_users.get_identity_metadata") @patch("collaborator.models.ows.ows_users.get_master_contact") @patch("collaborator.utils.ses.send_email") def test_send_email_to_master_contact( ses_mock, master_contact_mock, user_metadata_mock, account_metadata_mock, mock_account, mock_user, ): """Test send email to master contact.""" master_contact_mock.return_value = user_fixtures.MASTER_CONTACT_RESPONSE user_metadata_mock.return_value = user_fixtures.IDENTITY_RESPONSE account_metadata_mock.return_value = vendor_fixtures.VENDOR_RESPONSE transferwise.send_email_to_master_contact(mock_account, mock_user, "PAYMENT") email_body = email.BODY_PAYMENT.format( master_user="first name last name", user_name="Zoran Tosic", vendor_name="Pencilbrains, LLC", ) ses_mock.assert_called_with( "email", "donotreply@dev.theorchard.io", "Payment created", email_body ) @patch("collaborator.models.ows.ows_account.get_account_metadata") @patch("collaborator.models.ows.ows_users.get_identity_metadata") @patch("collaborator.models.ows.ows_users.get_master_contact") @patch("collaborator.utils.ses.send_email") def test_send_email_to_master_contact_recipient( ses_mock, master_contact_mock, user_metadata_mock, account_metadata_mock, mock_account, mock_user, ): """Test send email to master contact.""" master_contact_mock.return_value = user_fixtures.MASTER_CONTACT_RESPONSE user_metadata_mock.return_value = user_fixtures.IDENTITY_RESPONSE account_metadata_mock.return_value = vendor_fixtures.VENDOR_RESPONSE params = {"collaborator_name": "Frank Zappa"} transferwise.send_email_to_master_contact( mock_account, mock_user, "RECIPIENT", params ) email_body = email.BODY_RECIPIENT.format( master_user="first name last name", user_name="Zoran Tosic", collaborator_name="Frank Zappa", vendor_name="Pencilbrains, LLC", ) ses_mock.assert_called_with( "email", "donotreply@dev.theorchard.io", "Recipient created", email_body ) @patch("collaborator.models.ows.ows_account.get_account_metadata") @patch("collaborator.models.ows.ows_users.get_identity_metadata") @patch("collaborator.models.ows.ows_users.get_master_contact") @patch("collaborator.utils.ses.send_email") def test_send_email_to_master_contact_recipient_deleted( ses_mock, master_contact_mock, user_metadata_mock, account_metadata_mock, mock_account, mock_user, ): """Test send email to master contact.""" master_contact_mock.return_value = user_fixtures.MASTER_CONTACT_RESPONSE user_metadata_mock.return_value = user_fixtures.IDENTITY_RESPONSE account_metadata_mock.return_value = vendor_fixtures.VENDOR_RESPONSE params = {"collaborator_name": "Frank Zappa"} transferwise.send_email_to_master_contact( mock_account, mock_user, "RECIPIENT_DELETED", params ) email_body = email.BODY_RECIPIENT_DELETED.format( master_user="first name last name", user_name="Zoran Tosic", collaborator_name="Frank Zappa", vendor_name="Pencilbrains, LLC", ) ses_mock.assert_called_with( "email", "donotreply@dev.theorchard.io", "Recipient Deleted", email_body ) @patch("collaborator.models.ows.ows_users.get_identity_metadata") @patch("collaborator.models.ows.ows_users.get_master_contact") @patch("collaborator.logic.transferwise.capture_exception") def test_send_email_to_master_contact_error( capture_exception_mock, master_contact_mock, user_metadata_mock, mock_account, mock_user, ): """Test send email to master contact.""" master_contact_mock.return_value = response.Response( user_fixtures.MASTER_CONTACT_RESPONSE ) error_response = response.create_error_response( message=("error"), status=422, code="no_user_info" ) user_metadata_mock.return_value = error_response transferwise.send_email_to_master_contact(mock_account, mock_user, "PAYMENT") arg = capture_exception_mock.call_args[0][0] assert isinstance(arg, Exception) assert str(arg) == "Unable to get user metadata for user oa:24601" @patch.object(transferwise, "Transferwise") @patch.object(transferwise, "TransferwiseTransactionPersister") def test_create_batch_payment_batch_transfer_error( TransferwiseTransactionPersister_mock, Transferwise_mock, mock_account, mock_user ): """Test create payment error.""" payload = fixtures.CREATE_PAYMENT_PAYLOAD called_with_params = { "details": { "reference": "13926297", "sourceOfFunds": "Salary", "transferPurpose": "Other", }, "customerTransactionId": "81b5e5b0-e7d8-4112-aad9-b9189d53dcb2", "quoteUuid": "81b5e5b0-e7d8-4112-aad9-b9189d53dcb2", "targetAccount": 13926297, } error_message = { "called_with_params": called_with_params, "code": "quote.duplicate", "message": "Cannot use one quote to create 2 transfers", } expected_error = OwsError( message=error_message, status=422, code="transferwise_post_request_error" ) TransferwiseTransactionPersister_mock.get_by_quote_ids_for_vendor_profile.return_value = [] Transferwise_mock().get_quote.return_value = fixtures.QUOTE_RESPONSE Transferwise_mock().create_batch.return_value = fixtures.BATCH_RESPONSE Transferwise_mock().create_batch_transfer.side_effect = expected_error with pytest.raises(OwsError) as err: transferwise.create_batch_payment(mock_account, mock_user, payload) assert err.value == expected_error @patch.object(TransferwiseBatchPersister, "get_by_id") @patch.object(TransferwiseTransactionPersister, "get_for_transferwise_batch_id") def test_get_batch(get_for_transferwise_batch_id_mock, get_by_id_mock, mock_account): """Test getting a TW batch.""" mock_batch = {"id": 123, "vendor_id": mock_account.id} mock_txn = { "id": 123, "transferwise_batch_id": mock_batch["id"], "status": transferwise_transfer.STATUS_OUTGOING_PAYMENT_SENT, } get_by_id_mock.return_value = mock_batch get_for_transferwise_batch_id_mock.return_value = [mock_txn] result = transferwise.get_batch(int(mock_account.id), 123) mock_batch_with_imputed_status = { **mock_batch, "imputed_status": "COMPLETED", } assert_response(result, response.Response(mock_batch_with_imputed_status)) @patch.object(TransferwiseBatchPersister, "get_by_id") def test_get_batch_wrong_vendor(get_by_id_mock, mock_account): """Test getting a TW batch that belongs to a different account.""" get_by_id_mock.return_value = {"id": 123, "vendor_id": 1234} with pytest.raises(OwsError) as err: transferwise.get_batch(int(mock_account.id), 123) assert err.value.status == status.FORBIDDEN assert err.value.code == error.ERROR_CODE_AUTHORIZATION @patch.object(TransferwiseBatchPersister, "get_by_vendor_id") @patch.object(TransferwiseTransactionPersister, "bulk_get_for_transferwise_batch_ids") def test_get_batches(get_txns_mock, get_batches_mock, mock_account): """Test getting a TW batch.""" mock_batch = {"id": 123, "vendor_id": mock_account.id} mock_txn = { "id": 123, "transferwise_batch_id": mock_batch["id"], "status": transferwise_transfer.STATUS_OUTGOING_PAYMENT_SENT, } get_batches_mock.return_value = [mock_batch] get_txns_mock.return_value = [mock_txn] result = transferwise.get_batches(mock_account) assert_response( result, response.Response( [ { **mock_batch, "imputed_status": transferwise_batch.IMPUTED_STATUS_COMPLETED, } ] ), ) @patch.object(Transferwise, "get_batch_group") def test_get_batch_group(get_batch_group_mock): """Test fetching batch group details.""" get_batch_group_mock.return_value = fixtures.BATCH_COMPLETED_RESPONSE account = Account(type="vendor", id=1) profile_id = 4497 batch_group_id = "8faefe" result = transferwise.get_batch_group(account, profile_id, batch_group_id) assert result.status == 200 assert result.message == fixtures.BATCH_COMPLETED_RESPONSE get_batch_group_mock.assert_called_with(profile_id, batch_group_id) @patch.object(transferwise, "TransferwiseBatchPersister") @patch.object(transferwise, "TransferwiseTransactionPersister") @patch.object(transferwise, "TransferwiseProfilePersister") @patch.object(Transferwise, "get_quote") @pytest.mark.parametrize( "batch_id, transfers", [ ( 123, [ {"id": 123, "quote_id": 1}, {"id": 321, "quote_id": 2}, {"id": 191, "quote_id": 3}, ], ), (999, [{"id": 1, "quote_id": 1}]), (545, []), ], ) def test_get_transactions_for_batch( get_quote_mock, tpp_mock, ttp_mock, tbp_mock, batch_id, transfers, mock_account ): """Test getting the transactions for a batch.""" tpp_mock.get_active_profile.return_value = {"profile_id": 1} tbp_mock.get_by_id.return_value = {"id": 1, "vendor_id": mock_account.id} ttp_mock.get_for_transferwise_batch_id.return_value = transfers get_quote_mock.reurn_value = { "paymentOptions": [{"payIn": "BANK_TRANSFER", "price": {}}] } result = transferwise.get_transactions_for_batch(batch_id, mock_account) assert result.status == status.OK assert result.message == transfers ttp_mock.get_for_transferwise_batch_id.assert_called_with(batch_id) @patch.object(transferwise, "TransferwiseBatchPersister") @patch.object(transferwise, "TransferwiseTransactionPersister") @patch.object(transferwise, "TransferwiseProfilePersister") @patch.object(Transferwise, "get_quote") @pytest.mark.parametrize("vendor_id", [123, 9999]) def test_get_transactions_for_batch_incorrect_vendor( get_quote_mock, tpp_mock, ttp_mock, tbp_mock, mock_account, vendor_id ): """Test getting the transactions for a batch with the wrong account.""" tbp_mock.get_by_id.return_value = {"id": 1, "vendor_id": vendor_id} get_quote_mock.reurn_value = { "paymentOptions": [{"payIn": "BANK_TRANSFER", "price": {}}] } with pytest.raises(OwsError) as err: transferwise.get_transactions_for_batch(123, mock_account) assert err.value.status == status.FORBIDDEN @patch.object(TransferwiseTransactionPersister, "get_by_transfer_id") @patch.object(TransferwiseBatchPersister, "get_by_id") @patch.object(Transferwise, "simulate_transfer_processing") def test_simulate_transfer_processing( simulate_transfer_processing_mock, get_batch_mock, get_transaction_mock ): """Test simulating_transfer_processing.""" transfer_id = 468956 batch_id = 1337 vendor_id = 25153 transfer_status = "processing" get_transaction_mock.return_value = { "transferwise_batch_id": batch_id, } get_batch_mock.return_value = { "vendor_id": vendor_id, } simulate_transfer_processing_mock.return_value = fixtures.BATCH_TRANSFER_RESPONSE result = transferwise.simulate_transfer_processing(transfer_id, transfer_status) assert result.status == 200 assert result.message == fixtures.BATCH_TRANSFER_RESPONSE get_transaction_mock.assert_called_with(transfer_id) get_batch_mock.assert_called_with(batch_id) simulate_transfer_processing_mock.assert_called_with(transfer_id, transfer_status) @pytest.mark.parametrize( "transfer_id, input_status, expected_status, occurred_at", [ ( 1, "incoming_payment_waiting", transferwise_transfer.STATUS_INCOMING_PAYMENT_WAITING, datetime(2019, 1, 1, 0, 0, 0, 0), ), ( 2, "processing", transferwise_transfer.STATUS_PROCESSING, datetime(2019, 1, 1, 0, 0, 0, 0), ), ], ) @patch.object(transferwise, "TransferwiseTransactionPersister") def test_update_payment_status( persister_mock, transfer_id, input_status, expected_status, occurred_at ): """Test updating payment status.""" result = transferwise.update_payment_status(transfer_id, input_status, occurred_at) assert result.status == status.OK persister_mock.update_transaction_status.assert_called_with( expected_status, transfer_id, occurred_at, session=ANY ) @pytest.mark.parametrize( "transfer_id, transfer_status, occurred_at", [ (1, "random", datetime(2019, 1, 1, 0, 0, 0, 0)), (2, "test", datetime(2019, 1, 1, 0, 0, 0, 0)), ], ) @patch.object(transferwise, "TransferwiseTransactionPersister") def test_update_payment_status_unhandled_status( persister_mock, transfer_id, transfer_status, occurred_at ): """Test updating payment status with an unhandled status.""" with pytest.raises(Exception) as error: transferwise.update_payment_status(transfer_id, transfer_status, occurred_at) assert str(error.value) == f'Unhandled transfer status "{transfer_status}"' persister_mock.update_profile_status.assert_not_called() @pytest.mark.parametrize( "transfer_id, input_status, transaction_exists, is_credited", [ (1, "cancelled", False, True), (1, "charged_back", True, True), (1, "funds_refunded", True, False), ], ) @patch.object(TransferwiseTransactionPersister, "get_by_transfer_id") @patch.object(TransferwiseTransactionPersister, "update_transaction_status") @patch.object(TransactionPersister, "get_transaction_by_transferwise_transaction_id") @patch.object(CollaboratorPersister, "get_by_id") @patch.object(StatementPeriodPersister, "get_open_statement_period") @patch.object(TransactionPersister, "is_transaction_credited") @patch.object(TransactionPersister, "create_transaction") def test_update_payment_status_on_failed_transaction( create_transaction_mock, is_transaction_credited_mock, get_open_statement_period_mock, get_collaborator_by_id_mock, get_transaction_by_tw_transaction_id_mock, update_transaction_status_mock, get_tw_txn_by_transfer_id_mock, transfer_id, input_status, transaction_exists, is_credited, ): """Test updating payment status.""" occurred_at = datetime(2019, 1, 1, 0, 0, 0, 0) update_transaction_status_mock.return_value = None get_collaborator_by_id_mock.return_value = {"vendor_id": 1} get_open_statement_period_mock.return_value = SimpleNamespace(statement_period_id=1) is_transaction_credited_mock.return_value = is_credited get_transaction_by_tw_transaction_id_mock.return_value = ( {"id": 1, "description": "description"} if transaction_exists else None ) get_tw_txn_by_transfer_id_mock.return_value = { "id": 1, "status": transferwise_transfer.STATUS_INCOMING_PAYMENT_WAITING, "collaborator_id": 1, "source_currency": "USD", "source_amount": 123, } transferwise.update_payment_status(transfer_id, input_status, occurred_at) TransferwiseTransactionPersister.get_by_transfer_id.assert_called_with( transfer_id, session=ANY ) TransferwiseTransactionPersister.update_transaction_status.assert_called_with( input_status, transfer_id, occurred_at, session=ANY ) if not is_credited: if transaction_exists: create_transaction_mock.assert_called_with( collaborator_id=1, transaction_date=ANY, transferwise_transaction_id=1, currency="USD", statement_period_id=1, report_id=None, voided_transaction_id=None, transaction_type="CREDIT", description="Returned Payment: description", original_amount=123, collaborator_share=None, chargeable_amount=123, credited_payment_id=1, session=ANY, ) else: create_transaction_mock.assert_not_called() else: create_transaction_mock.assert_not_called() @patch.object(TransferwiseTransactionPersister, "get_by_transfer_id") @patch.object(TransferwiseTransactionPersister, "update_transaction_status") @patch.object(TransactionPersister, "create_transaction") @patch.object(TransactionPersister, "get_transaction_by_transferwise_transaction_id") @patch.object(collaborator, "get_collaborator_by_id") @patch.object(StatementPeriodPersister, "get_open_statement_period") @patch.object(transferwise, "get_batch") def test_update_payment_status_does_not_create_transaction_if_exists( tbp_mock, get_open_statement_period_mock, collaborator_mock, get_transaction_by_transferwise_transaction_id_mock, create_transaction_mock, update_transaction_status_mock, get_transaction_id_by_transfer_id_mock, ): """Test we don't create txn when payment is successful and txn already exists.""" get_transaction_by_transferwise_transaction_id_mock.return_value = {"id": 1} tbp_mock.return_value = response.Response( message=transferwise_batch_fixtures.TRANSFERWISE_BATCH ) get_open_statement_period_mock.return_value = SimpleNamespace(statement_period_id=2) get_transaction_id_by_transfer_id_mock.return_value = { "id": 1, "collaborator_id": 1, "status": transferwise_transfer.STATUS_INCOMING_PAYMENT_WAITING, "source_currency": "USD", "source_amount": 12.34, "transferwise_batch_id": 3, } update_transaction_status_mock.return_value = None collaborator_mock.return_value = {"id": 1, "vendor_id": 123, "name": "Collab 1"} transferwise.update_payment_status( 1, transferwise_transfer.STATUS_OUTGOING_PAYMENT_SENT, 0 ) create_transaction_mock.assert_not_called() def test__map_batch_payload(): """Test _map_batch_payload.""" payload = fixtures.BATCH_COMPLETED_RESPONSE payload.update({"vendor_id": 24601}) payload.update({"profile_id": 4497}) expected_result = { "account_number": "77643994", "account_type": None, "amount": 123.45, "bank_code": "231470", "batch_id": "54a6bc09-cef9-49a8-9041-f1f0c654cd88", "batch_name": "my-batch-group", "batch_type": "bank_transfer", "bban": None, "currency": "USD", "iban": None, "payin_name": "TransferWise", "profile_id": 4497, "reference": "XYZ123", "status": "COMPLETED", "vendor_id": 24601, "version": 1, "bank_address_name": None, "bank_address_branch_name": None, "bank_address_first_line": None, "bank_address_post_code": None, "bank_address_city": None, "bank_address_state_code": None, "bank_address_country": None, "tw_address_name": None, "tw_address_first_line": None, "tw_address_post_code": None, "tw_address_city": None, "tw_address_state_code": None, "tw_address_country": None, } result = transferwise._map_batch_payload(payload) assert result == expected_result @patch.object(logging, "log_event") @patch.object(TransferwiseBatchPersister, "get_by_id") @patch.object(TransferwiseBatchPersister, "update_batch") @patch.object(transferwise, "Transferwise") def test_cancel_batch_payment( Transferwise_mock, TransferwiseBatchPersisterUpdate_mock, TransferwiseBatchPersisterGet_mock, log_event_mock, mock_account, mock_user, ): """Test cancel batch payment.""" batch_id = 1 profile_id = transferwise_batch_fixtures.TRANSFERWISE_CANCELLED["profile_id"] batch_group_id = transferwise_batch_fixtures.TRANSFERWISE_CANCELLED["batch_id"] Transferwise_mock().get_batch_group.return_value = fixtures.BATCH_COMPLETED_RESPONSE Transferwise_mock().modify_batch_state.return_value = ( fixtures.BATCH_CANCELLED_RESPONSE ) TransferwiseBatchPersisterGet_mock.return_value = ( transferwise_batch_fixtures.TRANSFERWISE_BATCH ) TransferwiseBatchPersisterUpdate_mock.return_value = ( transferwise_batch_fixtures.TRANSFERWISE_CANCELLED ) result = transferwise.cancel_batch_payment(mock_account, mock_user, batch_id) assert result.status == 200 assert result.message == transferwise_batch_fixtures.TRANSFERWISE_CANCELLED Transferwise_mock().get_batch_group.assert_called_with(profile_id, batch_group_id) Transferwise_mock().modify_batch_state.assert_called_with( profile_id, batch_group_id, fixtures.BATCH_COMPLETED_RESPONSE["version"], complete_batch=False, ) TransferwiseBatchPersisterGet_mock.assert_called_with(batch_id) TransferwiseBatchPersisterUpdate_mock.assert_called_with( batch_id, {"status": "CANCELLED", "version": 12345}, mock_user ) @patch.object(logging, "log_event") @patch.object(TransferwiseBatchPersister, "get_by_id") @patch.object(transferwise, "Transferwise") def test_cancel_batch_payment_error( Transferwise_mock, TransferwiseBatchPersisterGet_mock, log_event_mock, mock_account, mock_user, ): """Test cancel batch payment.""" batch_id = 1 TransferwiseBatchPersisterGet_mock.return_value = ( transferwise_batch_fixtures.TRANSFERWISE_BATCH_RANDOM_VENDOR ) with pytest.raises(OwsError) as error_info: transferwise.cancel_batch_payment(mock_account, mock_user, batch_id) assert error_info.value.status == status.FORBIDDEN Transferwise_mock().modify_batch_state.assert_not_called() @pytest.mark.parametrize( "txns, expected_status", [ ( [ {"status": transferwise_transfer.STATUS_CANCELLED}, {"status": transferwise_transfer.STATUS_OUTGOING_PAYMENT_SENT}, ], transferwise_batch.IMPUTED_STATUS_CANCELLED, ), ( [ {"status": transferwise_transfer.STATUS_OUTGOING_PAYMENT_SENT}, {"status": transferwise_transfer.STATUS_OUTGOING_PAYMENT_SENT}, ], transferwise_batch.IMPUTED_STATUS_COMPLETED, ), ( [ {"status": transferwise_transfer.STATUS_INCOMING_PAYMENT_WAITING}, {"status": transferwise_transfer.STATUS_INCOMING_PAYMENT_WAITING}, ], transferwise_batch.IMPUTED_STATUS_AWAITING_FUNDS, ), ( [ {"status": transferwise_transfer.STATUS_OUTGOING_PAYMENT_SENT}, {"status": transferwise_transfer.STATUS_INCOMING_PAYMENT_WAITING}, ], transferwise_batch.IMPUTED_STATUS_IN_PROGRESS, ), ( [ {"status": transferwise_transfer.STATUS_OUTGOING_PAYMENT_SENT}, {"status": transferwise_transfer.STATUS_BOUNCED_BACK}, ], transferwise_batch.IMPUTED_STATUS_BOUNCED_BACK, ), ], ) def test__get_batch_imputed_status(txns, expected_status): """Test getting imputed status.""" status = transferwise._get_batch_imputed_status(txns) assert status == expected_status @patch.object(Transferwise, "get_transfer_requirements") @patch.object(RecipientPersister, "get_vendor_id_by_transferwise_id") @pytest.mark.parametrize( "currency, expected", [ ("USD", fixtures.TRANSFER_REQUIREMENTS_RESPONSE[0]["fields"]), ("BRL", transferwise_transfer.BRL_TRANSFER_REQUIREMENTS), ], ) def test_get_transfer_requirements( get_vendor_id_by_transferwise_id_mock, get_transfer_requirements_mock, currency, expected, ): """Test fetching transfer requirements.""" get_vendor_id_by_transferwise_id_mock.return_value = 1234 get_transfer_requirements_mock.return_value = { "currency": currency, "requirements": fixtures.TRANSFER_REQUIREMENTS_RESPONSE[0]["fields"], } TRANSFER = {"targetAccount": "1234"} result = transferwise.get_transfer_requirements( [{"currency": currency, "transfer": TRANSFER}] ) if currency != transferwise_transfer.CURRENCY_BRL: get_transfer_requirements_mock.assert_called_with( {"currency": currency, "transfer": TRANSFER} ) else: get_transfer_requirements_mock.assert_not_called() assert result.status == 200 assert result.message == [ { "currency": currency, "requirements": expected, } ]