"""Tests for account_tax_info logic.""" import datetime from decimal import Decimal from unittest.mock import call, MagicMock, patch from marshmallow import ValidationError import pytest from abacus_account.constants import error from abacus_account.constants.constants import ( ACCOUNT_KAFKA_EVENT_NAMES, TAX_EMPLOYMENT_TYPES ) from abacus_account.logic import account_tax_info as logic from abacus_account.schemas.account_tax_info import AccountTaxInfoDetailSchema from tests.utils.factories import ( AccountFactory, AccountPaymentTermFactory, AccountTaxInfoFactory ) @patch('abacus_account.logic.account_tax_info._validate_account_id_unique') @patch('abacus_account.logic.account_tax_info.validate_country_code') @patch('abacus_account.logic.account_tax_info.models') def test_create_account_tax_info_success( mock_models, mock_validate_country_code, mock__validate_account_id_unique, ): """Test success response of create_account_tax_info method.""" account_id = 123 post_data = { 'account_id': account_id, 'country_of_tax_residence': 'USA', 'is_sba_signed': False, } new_account_tax_info = AccountTaxInfoFactory.build(**post_data) mock_models.AccountTaxInfo.create.return_value = new_account_tax_info mock__validate_account_id_unique.return_value = None response = logic.create_account_tax_info(**post_data) mock_models.AccountTaxInfo.create.assert_called_once_with( **post_data, is_vat_exempt=True, is_tax_treaty_claimed=False, tax_employment_type=None, certificate_of_residence_expiration_date=None, is_wht_applicable=True, is_resident_of_spanish_islands=None, wht_rate_override=None ) mock_validate_country_code.assert_called_once_with('USA') mock__validate_account_id_unique.assert_called_once_with(account_id) assert response.status == 201 assert response.message == AccountTaxInfoDetailSchema().dump(new_account_tax_info) @patch('abacus_account.logic.account_tax_info._validate_account_id_unique') @patch('abacus_account.logic.account_tax_info.validate_country_code') @patch('abacus_account.logic.account_tax_info.models') def test_create_account_tax_info_success_with_spanish_taxes_fields( mock_models, mock_validate_country_code, mock__validate_account_id_unique, ): """Test success response of create_account_tax_info method.""" account_id = 123 test_date = datetime.date(2025, 1, 1) post_data = { 'account_id': account_id, 'country_of_tax_residence': 'USA', 'is_sba_signed': False, 'tax_employment_type': TAX_EMPLOYMENT_TYPES.BUSINESS, 'certificate_of_residence_expiration_date': test_date, 'is_wht_applicable': True, 'is_resident_of_spanish_islands': False } new_account_tax_info = AccountTaxInfoFactory.build(**post_data) mock_models.AccountTaxInfo.create.return_value = new_account_tax_info mock__validate_account_id_unique.return_value = None response = logic.create_account_tax_info(**post_data) mock_models.AccountTaxInfo.create.assert_called_once_with( **post_data, is_vat_exempt=True, is_tax_treaty_claimed=False, wht_rate_override=None ) mock_validate_country_code.assert_called_once_with('USA') mock__validate_account_id_unique.assert_called_once_with(account_id) assert response.status == 201 assert response.message == AccountTaxInfoDetailSchema().dump(new_account_tax_info) @patch('abacus_account.logic.account_tax_info._validate_account_id_unique') @patch('abacus_account.logic.account_tax_info.validate_country_code') @patch('abacus_account.logic.account_tax_info.models') def test_create_account_tax_info_success_with_german_tax_fields( mock_models, mock_validate_country_code, mock__validate_account_id_unique, ): """Test success response of create_account_tax_info method.""" account_id = 123 post_data = { 'account_id': account_id, 'country_of_tax_residence': 'USA', 'is_sba_signed': False, 'tax_employment_type': TAX_EMPLOYMENT_TYPES.INDIVIDUAL, 'wht_rate_override': Decimal('10.01') } new_account_tax_info = AccountTaxInfoFactory.build(**post_data) mock_models.AccountTaxInfo.create.return_value = new_account_tax_info mock__validate_account_id_unique.return_value = None response = logic.create_account_tax_info(**post_data) mock_models.AccountTaxInfo.create.assert_called_once_with( **post_data, is_vat_exempt=True, is_tax_treaty_claimed=False, certificate_of_residence_expiration_date=None, is_wht_applicable=True, is_resident_of_spanish_islands=None ) mock_validate_country_code.assert_called_once_with('USA') mock__validate_account_id_unique.assert_called_once_with(account_id) assert response.status == 201 assert response.message == AccountTaxInfoDetailSchema().dump(new_account_tax_info) @patch('abacus_account.logic.account_tax_info._validate_account_id_unique') @patch('abacus_account.logic.account_tax_info.validate_country_code') @patch('abacus_account.logic.account_tax_info.models') def test_create_account_tax_info_with_vat_exempt_field( mock_models, mock_validate_country_code, mock__validate_account_id_unique, ): """Test create_account_tax_info method with additional field is_vat_exempt.""" account_id = 123 post_data = { 'account_id': account_id, 'country_of_tax_residence': 'USA', 'is_sba_signed': False, 'is_vat_exempt': False } new_account_tax_info = AccountTaxInfoFactory.build(**post_data) mock_models.AccountTaxInfo.create.return_value = new_account_tax_info mock__validate_account_id_unique.return_value = None response = logic.create_account_tax_info(**post_data) mock_models.AccountTaxInfo.create.assert_called_once_with( **post_data, is_tax_treaty_claimed=False, tax_employment_type=None, certificate_of_residence_expiration_date=None, is_wht_applicable=True, is_resident_of_spanish_islands=None, wht_rate_override=None ) mock_validate_country_code.assert_called_once_with('USA') mock__validate_account_id_unique.assert_called_once_with(account_id) assert response.status == 201 assert response.message == AccountTaxInfoDetailSchema().dump(new_account_tax_info) @patch('abacus_account.logic.account_tax_info._validate_account_id_unique') @patch('abacus_account.logic.account_tax_info.validate_country_code') @patch('abacus_account.logic.account_tax_info.models') def test_create_account_tax_info_with_is_tax_treaty_claimed( mock_models, mock_validate_country_code, mock__validate_account_id_unique, ): """Test create_account_tax_info method with additional field is_vat_exempt.""" account_id = 123 post_data = { 'account_id': account_id, 'country_of_tax_residence': 'USA', 'is_sba_signed': False, 'is_tax_treaty_claimed': True } new_account_tax_info = AccountTaxInfoFactory.build(**post_data) mock_models.AccountTaxInfo.create.return_value = new_account_tax_info mock__validate_account_id_unique.return_value = None response = logic.create_account_tax_info(**post_data) mock_models.AccountTaxInfo.create.assert_called_once_with( **post_data, is_vat_exempt=True, tax_employment_type=None, certificate_of_residence_expiration_date=None, is_wht_applicable=True, is_resident_of_spanish_islands=None, wht_rate_override=None ) mock_validate_country_code.assert_called_once_with('USA') mock__validate_account_id_unique.assert_called_once_with(account_id) assert response.status == 201 assert response.message == AccountTaxInfoDetailSchema().dump(new_account_tax_info) @pytest.mark.parametrize( 'country_of_tax_residence,country_of_tax_reporting,payment_entity_id', (('GBR', 'GBR', 1), (None, 'ESP', 6)) ) @patch('abacus_common_logic.models.base.CRUDMixin.update_attributes') @patch('abacus_account.logic.account_tax_info.validate_country_code') @patch('abacus_account.logic.account_tax_info.models') @patch('abacus_account.logic.account_tax_info.emit_account_tax_id_event', return_value=True) def test_update_account_tax_info_success( mock_emit, mock_models, mock_validate_country_code, mock_update_attributes, country_of_tax_residence, country_of_tax_reporting, payment_entity_id, monkeypatch, reference_payment_entity_fixture ): """Test success response of update_account_tax_info method.""" init_account_tax_info = AccountTaxInfoFactory.create() AccountPaymentTermFactory.create( account=init_account_tax_info.account, payment_entity_id=payment_entity_id ) mock_models.AccountTaxInfo.commit_changes.return_value = True mock_models.ReferencePaymentEntity.get_by_id.return_value = MagicMock( country_of_tax_reporting=country_of_tax_reporting ) post_data = { 'country_of_tax_residence': country_of_tax_residence, 'is_sba_signed': True, } updated_account_tax_info = AccountTaxInfoFactory.build(**post_data) mock_update_attributes.return_value = updated_account_tax_info mock_kafka_producers = { ACCOUNT_KAFKA_EVENT_NAMES.TAX_INFO_UPDATED: MagicMock( produce_event=MagicMock() ), } monkeypatch.setattr('abacus_account.config.Config.KAFKA_PRODUCERS_BY_ACTION_NAME', mock_kafka_producers) response = logic.update_account_tax_info(init_account_tax_info, **post_data) mock_update_attributes.assert_called_once_with(**post_data) if country_of_tax_residence: mock_validate_country_code.assert_called_once_with(country_of_tax_residence) expected_result = AccountTaxInfoDetailSchema().dump( updated_account_tax_info ) assert response.status == 201 assert response.message == expected_result mock_emit.assert_called_once_with( account_tax_info_id=updated_account_tax_info.account_tax_info_id, action_name=ACCOUNT_KAFKA_EVENT_NAMES.TAX_INFO_UPDATED) @patch('abacus_common_logic.models.base.CRUDMixin.update_attributes') @patch('abacus_account.logic.account_tax_info.validate_country_code') @patch('abacus_account.logic.account_tax_info.models') @patch('abacus_account.logic.account_tax_info.emit_account_tax_id_event', return_value=True) def test_update_account_tax_info_success_with_german_fields( mock_emit, mock_models, mock_validate_country_code, mock_update_attributes, reference_payment_entity_fixture, monkeypatch, ): """Test success response of update_account_tax_info method.""" init_account_tax_info = AccountTaxInfoFactory.create() AccountPaymentTermFactory.create( account=init_account_tax_info.account, payment_entity_id=8 ) mock_models.AccountTaxInfo.commit_changes.return_value = True mock_models.ReferencePaymentEntity.get_by_id.return_value = MagicMock( country_of_tax_reporting='DEU' ) post_data = { 'country_of_tax_residence': 'GBR', 'is_sba_signed': True, 'tax_employment_type': TAX_EMPLOYMENT_TYPES.INDIVIDUAL, 'wht_rate_override': Decimal('10.01') } updated_account_tax_info = AccountTaxInfoFactory.build(**post_data) mock_update_attributes.return_value = updated_account_tax_info mock_kafka_producers = { ACCOUNT_KAFKA_EVENT_NAMES.TAX_INFO_UPDATED: MagicMock( produce_event=MagicMock() ), } monkeypatch.setattr('abacus_account.config.Config.KAFKA_PRODUCERS_BY_ACTION_NAME', mock_kafka_producers) response = logic.update_account_tax_info(init_account_tax_info, **post_data) mock_update_attributes.assert_called_once_with(**post_data) mock_validate_country_code.assert_called_once_with('GBR') expected_result = AccountTaxInfoDetailSchema().dump( updated_account_tax_info ) assert response.status == 201 assert response.message == expected_result mock_emit.assert_called_once_with( account_tax_info_id=updated_account_tax_info.account_tax_info_id, action_name=ACCOUNT_KAFKA_EVENT_NAMES.TAX_INFO_UPDATED) @patch('abacus_common_logic.models.base.CRUDMixin.update_attributes') @patch('abacus_account.logic.account_tax_info.validate_country_code') @patch('abacus_account.logic.account_tax_info.models') @patch('abacus_account.logic.account_tax_info.emit_account_tax_id_event', return_value=True) def test_update_account_tax_info_failure_null_country( mock_emit, mock_models, mock_validate_country_code, mock_update_attributes, monkeypatch, reference_payment_entity_fixture, ): """Test failure response of update_account_tax_info method with null country.""" init_account_tax_info = AccountTaxInfoFactory.create() AccountPaymentTermFactory.create( account=init_account_tax_info.account, ) post_data = { 'country_of_tax_residence': None, } updated_account_tax_info = AccountTaxInfoFactory.build(**post_data) mock_update_attributes.return_value = updated_account_tax_info mock_kafka_producers = { ACCOUNT_KAFKA_EVENT_NAMES.TAX_INFO_UPDATED: MagicMock( produce_event=MagicMock() ), } monkeypatch.setattr( 'abacus_account.config.Config.KAFKA_PRODUCERS_BY_ACTION_NAME', mock_kafka_producers ) response = logic.update_account_tax_info(init_account_tax_info, **post_data) assert response.status == 400 assert response.errors == { 'code': 'error', 'message': error.COUNTRY_OF_TAX_RESIDENCE_IS_REQUIRED } assert not mock_update_attributes.called assert not mock_models.AccountTaxInfo.commit_changes.called assert not mock_emit.called @patch('abacus_common_logic.models.base.CRUDMixin.update_attributes') @patch('abacus_account.logic.account_tax_info.validate_country_code') @patch('abacus_account.logic.account_tax_info.models') @patch('abacus_account.logic.account_tax_info.emit_account_tax_id_event', return_value=True) def test_update_account_tax_info_without_country( mock_emit, mock_models, mock_validate_country_code, mock_update_attributes, monkeypatch ): """Test success response of update_account_tax_info method w/o country.""" init_account_tax_info = AccountTaxInfoFactory.build(country_of_tax_residence='GBR') mock_models.AccountTaxInfo.commit_changes.return_value = True post_data = { 'is_sba_signed': True, } updated_account_tax_info = AccountTaxInfoFactory.build(**post_data) mock_update_attributes.return_value = updated_account_tax_info mock_kafka_producers = { ACCOUNT_KAFKA_EVENT_NAMES.TAX_INFO_UPDATED: MagicMock( produce_event=MagicMock() ), } monkeypatch.setattr('abacus_account.config.Config.KAFKA_PRODUCERS_BY_ACTION_NAME', mock_kafka_producers) response = logic.update_account_tax_info(init_account_tax_info, **post_data) mock_update_attributes.assert_called_once_with(**post_data) mock_validate_country_code.assert_not_called() expected_result = AccountTaxInfoDetailSchema().dump( updated_account_tax_info ) assert response.status == 201 assert response.message == expected_result mock_emit.assert_called_once_with( account_tax_info_id=updated_account_tax_info.account_tax_info_id, action_name=ACCOUNT_KAFKA_EVENT_NAMES.TAX_INFO_UPDATED) @patch('abacus_common_logic.models.base.CRUDMixin.update_attributes') @patch('abacus_account.logic.account_tax_info.validate_country_code') @patch('abacus_account.logic.account_tax_info.models') @patch('abacus_account.logic.account_tax_info.emit_account_tax_id_event', return_value=True) def test_update_account_tax_info_is_vat_exempt_field( mock_emit, mock_models, mock_validate_country_code, mock_update_attributes, monkeypatch ): """Test update_account_tax_info method for is_vat_exempt field.""" init_account_tax_info = AccountTaxInfoFactory.build(is_vat_exempt=True) mock_models.AccountTaxInfo.commit_changes.return_value = True post_data = { 'is_vat_exempt': False, } updated_account_tax_info = AccountTaxInfoFactory.build(**post_data) mock_update_attributes.return_value = updated_account_tax_info mock_kafka_producers = { ACCOUNT_KAFKA_EVENT_NAMES.TAX_INFO_UPDATED: MagicMock( produce_event=MagicMock() ), } monkeypatch.setattr('abacus_account.config.Config.KAFKA_PRODUCERS_BY_ACTION_NAME', mock_kafka_producers) response = logic.update_account_tax_info(init_account_tax_info, **post_data) mock_update_attributes.assert_called_once_with(**post_data) mock_validate_country_code.assert_not_called() expected_result = AccountTaxInfoDetailSchema().dump( updated_account_tax_info ) assert response.status == 201 assert response.message == expected_result mock_emit.assert_called_once_with( account_tax_info_id=updated_account_tax_info.account_tax_info_id, action_name=ACCOUNT_KAFKA_EVENT_NAMES.TAX_INFO_UPDATED) @patch('abacus_common_logic.models.base.CRUDMixin.update_attributes') @patch('abacus_account.logic.account_tax_info.validate_country_code') @patch('abacus_account.logic.account_tax_info.models') @patch('abacus_account.logic.account_tax_info.emit_account_tax_id_event', return_value=True) def test_update_account_tax_info_is_tax_treaty_claimed_field( mock_emit, mock_models, mock_validate_country_code, mock_update_attributes, monkeypatch ): """Test update_account_tax_info method for is_vat_exempt field.""" init_account_tax_info = AccountTaxInfoFactory.build(is_vat_exempt=True) mock_models.AccountTaxInfo.commit_changes.return_value = True post_data = { 'is_tax_treaty_claimed': False, } updated_account_tax_info = AccountTaxInfoFactory.build(**post_data) mock_update_attributes.return_value = updated_account_tax_info mock_kafka_producers = { ACCOUNT_KAFKA_EVENT_NAMES.TAX_INFO_UPDATED: MagicMock( produce_event=MagicMock() ), } monkeypatch.setattr('abacus_account.config.Config.KAFKA_PRODUCERS_BY_ACTION_NAME', mock_kafka_producers) response = logic.update_account_tax_info(init_account_tax_info, **post_data) mock_update_attributes.assert_called_once_with(**post_data) mock_validate_country_code.assert_not_called() expected_result = AccountTaxInfoDetailSchema().dump( updated_account_tax_info ) assert response.status == 201 assert response.message == expected_result mock_emit.assert_called_once_with( account_tax_info_id=updated_account_tax_info.account_tax_info_id, action_name=ACCOUNT_KAFKA_EVENT_NAMES.TAX_INFO_UPDATED) @patch('abacus_account.logic.account_tax_info.models') def test_get_account_tax_info_by_account_id_success(mock_models): """Test success response of get_account_tax_info_by_account_id method.""" account_tax_info = AccountTaxInfoFactory.build() mock_models.Account.get_by_id_or_error.return_value = account_tax_info.account response = logic.get_account_tax_info_by_account_id(account_tax_info.account_id) mock_models.Account.get_by_id_or_error.assert_called_once_with( account_tax_info.account_id ) assert response.status == 200 assert response.message == AccountTaxInfoDetailSchema().dump(account_tax_info) @patch('abacus_account.logic.account_tax_info.models') def test_validate_account_id_unique_valid(mock_models): """Test valid response of _validate_account_id_unique method.""" account = AccountFactory.build(account_id=123) mock_models.Account.get_by_id_or_error.return_value = account response = logic._validate_account_id_unique(account.account_id) mock_models.Account.get_by_id_or_error.assert_called_once_with(account.account_id) assert response is None @patch('abacus_account.logic.account_tax_info.models') def test_validate_account_id_unique_invalid(mock_models): """Test invalid response of _validate_account_id_unique method.""" account_tax_info = AccountTaxInfoFactory.build() mock_models.Account.get_by_id_or_error.return_value = account_tax_info.account expected_error_message = error.ERROR_ACCOUNT_TAX_INFO_ALREADY_EXISTS.format( object_id=account_tax_info.account_id ) with pytest.raises(ValidationError, match=expected_error_message): logic._validate_account_id_unique(account_tax_info.account_id) mock_models.Account.get_by_id_or_error\ .assert_called_once_with(account_tax_info.account_id) @patch('abacus_account.logic.account_tax_info.models') def test_account_tax_info_export(mock_model, account_fixtures): """Test account_tax_info_export method.""" account_tax_info_items = [ AccountTaxInfoFactory.create( account_tax_info_id=11, account=account_fixtures[0]), AccountTaxInfoFactory.create( account_tax_info_id=22, account=account_fixtures[1]) ] mock_model.AccountTaxInfo.stream_all.return_value = iter(account_tax_info_items) result = '' for chunk in logic.account_tax_info_export(): result += chunk mock_model.AccountTaxInfo.stream_all.assert_called_once_with(None) assert result == 'account_tax_info_id\taccount_id\tcountry_of_tax_residence\t' \ 'is_sba_signed\tis_vat_exempt\tis_tax_treaty_claimed\ttax_employment_type\t' \ 'certificate_of_residence_expiration_date\tis_wht_applicable\t' \ 'is_resident_of_spanish_islands\twht_rate_override\n' \ '11\t1\tUSA\t0\t1\t0\t\t\t1\t\t\n' \ '22\t2\tUSA\t0\t1\t0\t\t\t1\t\t\n' @patch('abacus_account.logic.account_tax_info.models') def test_get_account_tax_info_list(mock_model, account_fixtures, faker): """Test get_account_tax_info_list function.""" limit = faker.pyint(1, 100) offset = faker.pyint(0, 10) certificate_of_residence_expiration_date_start = faker.past_date() certificate_of_residence_expiration_date_end = faker.future_date() account_tax_info_items = [ AccountTaxInfoFactory.create( account_tax_info_id=11, account=account_fixtures[0]), AccountTaxInfoFactory.create( account_tax_info_id=22, account=account_fixtures[1]) ] account_ids = [ati.account_id for ati in account_tax_info_items] mock_model.AccountTaxInfo.get_filtered_items.return_value = ( account_tax_info_items, len(account_tax_info_items) ) res = logic.get_account_tax_info_list( limit, offset, account_ids, certificate_of_residence_expiration_date_start, certificate_of_residence_expiration_date_end ) assert res.message == { 'items': AccountTaxInfoDetailSchema().dump(account_tax_info_items, many=True), 'total_count': len(account_tax_info_items) } assert res.status == 200 assert mock_model.AccountTaxInfo.get_filtered_items.call_args_list == [ call( limit=limit, offset=offset, account_ids=account_ids, certificate_of_residence_expiration_date_start=certificate_of_residence_expiration_date_start, # noqa: E501 certificate_of_residence_expiration_date_end=certificate_of_residence_expiration_date_end # noqa: E501 ) ]