from datetime import date, datetime from http import HTTPStatus from unittest.mock import Mock, patch from flask.testing import FlaskClient import pytest from payee.config import Config from payee.constants.constants import TAX_FORM_TYPES from payee.constants.error import ( ERROR_DONT_HAVE_PERMISSIONS, ERROR_NO_VALID_IDENTITY_IN_CONTEXT, ) from payee.logic import tax_form as logic from payee.logic.exceptions import LogicError from payee.logic.secure_document import ( create_secure_document_details, get_secure_document_manager, ) from payee.models.tax_form import TaxFormInfo from payee.schemas.tax_form import TaxFormInfoOutputSchema from payee.utils.models import object_as_dict from tests.conftest import mock_abacus_headers, mock_abacus_readonly_headers from tests.constants import MOCK_AUDIT_FIELDS, MOCK_TAX_FORM_DETAILS from tests.utils.factories import TaxFormInfoFactory @patch('payee.utils.validations._get_vendor_ids', return_value=[1, 2, 3, 4, 5]) def test_get_tax_form_with_empty_body( mock_vendor_ids, account_fixtures, payoneer_program_fixtures, account_payee_fixtures, account_payee_tax_form_info, tax_form_info_secure_documents, fixture_client, mock_bypass_headers, ): for url in ( '/account-payee/tax-form-info/bulk', '/account-payee/tax-form-info/details/bulk', ): result = fixture_client.post(url, headers=mock_bypass_headers) assert result.status_code == 401 assert result.json == { 'error': f'{ERROR_NO_VALID_IDENTITY_IN_CONTEXT}, {ERROR_DONT_HAVE_PERMISSIONS}' } @patch('payee.utils.validations._get_vendor_ids', return_value=[1, 2, 3, 4, 5]) def test_get_tax_form_with_valid_body( mock_vendor_ids, account_fixtures, payoneer_program_fixtures, account_payee_fixtures, account_payee_tax_form_info, tax_form_info_secure_documents, fixture_client, mock_jwt_auth, ): mock_jwt_auth.identity = Config.TAX_FORM_SERVICE_IDENTITIES[0] for url in ( '/account-payee/tax-form-info/bulk', '/account-payee/tax-form-info/details/bulk', ): result = fixture_client.post( url, headers=mock_jwt_auth.headers, json={'account_payee_ids': [1, 2, 3, 4, 5]}, ) assert result.status_code == 200 assert result.json['total_count'] == 5 assert len(result.json['items']) == 5 @patch('payee.utils.validations._get_vendor_ids', return_value=[1, 2, 3, 4, 5]) def test_get_tax_form_with_partial_body( mock_vendor_ids, account_fixtures, payoneer_program_fixtures, account_payee_fixtures, account_payee_tax_form_info, tax_form_info_secure_documents, fixture_client, mock_bypass_headers, ): for url in ( '/account-payee/tax-form-info/bulk', '/account-payee/tax-form-info/details/bulk', ): result = fixture_client.post( url, headers=mock_bypass_headers, json={'account_payee_ids': [1, 2, 3, 4]}, ) assert result.status_code == 200 assert result.json['total_count'] == 4 assert len(result.json['items']) == 4 @patch('payee.utils.validations._get_vendor_ids', return_value=[1, 2]) def test_get_tax_form_with_invalid_body( mock_vendor_ids, account_fixtures, payoneer_program_fixtures, account_payee_fixtures, account_payee_tax_form_info, tax_form_info_secure_documents, fixture_client, mock_bypass_headers, ): for url in ( '/account-payee/tax-form-info/bulk', '/account-payee/tax-form-info/details/bulk', ): result = fixture_client.post( url, headers=mock_bypass_headers, json={'account_payee_ids': [1, 2, 3]}, ) assert result.status_code == 401 assert result.json == { 'error': f'{ERROR_NO_VALID_IDENTITY_IN_CONTEXT}, {ERROR_DONT_HAVE_PERMISSIONS}' } @patch('payee.utils.permissions.authorization_backend.is_authorized', return_value=True) def test_get_tax_form_info_empty_db( mock_perm, fresh_db, account_fixtures, payoneer_program_fixtures, account_payee_fixtures, fixture_client, mock_headers, ): result = fixture_client.post( '/account-payee/tax-form-info/bulk', headers=mock_headers ) assert result.status_code == 200 assert result.json == TaxFormInfoOutputSchema().dump( {'items': [], 'total_count': 0} ) @patch('payee.utils.permissions.authorization_backend.is_authorized', return_value=True) def test_get_tax_form_info( fresh_db, account_fixtures, payoneer_program_fixtures, account_payee_fixtures, account_payee_tax_form_info, fixture_client, mock_headers, ): result = fixture_client.post( '/account-payee/tax-form-info/bulk', headers=mock_headers ) assert result.status_code == 200 assert result.json['total_count'] == 5 item1, item2, item3, item4, item5 = result.json['items'] result = fixture_client.post( '/account-payee/tax-form-info/bulk?limit=2&offset=1', headers=mock_headers ) assert result.status_code == 200 assert len(result.json['items']) == 2 assert result.json['total_count'] == 5 assert result.json['items'][0] == item2 assert result.json['items'][1] == item3 result = fixture_client.post( '/account-payee/tax-form-info/bulk?limit=2&offset=4', headers=mock_headers ) assert result.status_code == 200 assert len(result.json['items']) == 1 assert result.json['total_count'] == 5 assert result.json['items'][0] == item5 result = fixture_client.post( '/account-payee/tax-form-info/bulk?limit=2&offset=5', headers=mock_headers ) assert result.status_code == 200 assert len(result.json['items']) == 0 assert result.json['total_count'] == 5 assert result.json['items'] == [] result = fixture_client.post( '/account-payee/tax-form-info/bulk?limit=305&offset=-1', headers=mock_headers, ) assert result.status_code == 400 assert result.json == { 'code': 'error', 'message': { 'query': { 'limit': [ 'Must be greater than or equal to 1 and less than or equal to 300.' ], 'offset': ['Must be greater than or equal to 0.'], } }, } result = fixture_client.post( '/account-payee/tax-form-info/bulk?limit=10&offset=0', headers=mock_headers, json={'account_payee_ids': [1, 2]}, ) assert result.status_code == 200 assert result.json['total_count'] == 2 assert result.json['items'][0] == item1 assert result.json['items'][1] == item2 result = fixture_client.post( '/account-payee/tax-form-info/bulk?limit=10&offset=0', headers=mock_headers, json={'is_active': True}, ) assert result.status_code == 200 assert result.json['total_count'] == 4 assert result.json['items'] == [item1, item3, item4, item5] result = fixture_client.post( '/account-payee/tax-form-info/bulk?limit=10&offset=0', headers=mock_headers, json={'expiration_date_start': '2024-11-30'}, ) assert result.status_code == 200 assert result.json['total_count'] == 3 assert result.json['items'] == [item3, item4, item5] result = fixture_client.post( '/account-payee/tax-form-info/bulk?limit=10&offset=0', headers=mock_headers, json={'expiration_date_end': '2025-11-30'}, ) assert result.status_code == 200 assert result.json['total_count'] == 1 assert result.json['items'] == [item2] result = fixture_client.post( '/account-payee/tax-form-info/bulk?limit=10&offset=0', headers=mock_headers, json={ 'expiration_date_start': '2024-11-30', 'expiration_date_end': '2045-11-30', }, ) assert result.status_code == 200 assert result.json['total_count'] == 3 assert result.json['items'] == [item3, item4, item5] result = fixture_client.post( '/account-payee/tax-form-info/bulk?limit=10&offset=0', headers=mock_headers, json={ 'expiration_date_start': '2024-11-30', 'expiration_date_end': '2025-11-30', 'is_active': True, }, ) assert result.status_code == 200 assert result.json['total_count'] == 0 assert result.json['items'] == [] result = fixture_client.post( '/account-payee/tax-form-info/bulk?limit=10&offset=0', headers=mock_headers, json={ 'expiration_date_start': '2014-11-30', 'expiration_date_end': '2045-11-30', 'is_active': True, 'account_payee_ids': [1, 2, 3, 4, 5], }, ) assert result.status_code == 200 assert result.json['total_count'] == 3 assert result.json['items'] == [item3, item4, item5] result = fixture_client.post( '/account-payee/tax-form-info/bulk?limit=10&offset=0', headers=mock_headers, json={ 'account_payee_ids': [1, 2, 'a'], }, ) assert result.status_code == 400 assert result.json == { 'code': 'error', 'message': { 'json': { 'account_payee_ids': { '2': ['Must be an integer greater or equal to 0.'] } } }, } result = fixture_client.post( '/account-payee/tax-form-info/bulk?limit=10&offset=0', headers=mock_headers, json={ 'expiration_date_start': '2027-11-30', 'expiration_date_end': '2025-11-30', }, ) assert result.status_code == 400 assert result.json == { 'code': 'error', 'message': { 'json': { '_schema': ['expiration_date_start must be before expiration_date_end'] } }, } @patch('payee.utils.permissions.authorization_backend.is_authorized', return_value=True) def test_get_tax_form_info_details( fresh_db, account_fixtures, payoneer_program_fixtures, account_payee_fixtures, account_payee_tax_form_info, tax_form_info_secure_documents, fixture_client, mock_headers, ): result = fixture_client.post( '/account-payee/tax-form-info/details/bulk', headers=mock_headers ) assert result.status_code == 200 assert result.json['total_count'] == 5 item1, item2, item3, item4, item5 = result.json['items'] result = fixture_client.post( '/account-payee/tax-form-info/details/bulk?limit=2&offset=1', headers=mock_headers, ) assert result.status_code == 200 assert len(result.json['items']) == 2 assert result.json['total_count'] == 5 assert result.json['items'][0] == item2 assert ( result.json['items'][0].items() >= MOCK_TAX_FORM_DETAILS[item2['tax_form_type']].items() ), result.json['items'][0] assert result.json['items'][1] == item3 assert ( result.json['items'][1].items() >= MOCK_TAX_FORM_DETAILS[item3['tax_form_type']].items() ), result.json['items'][1] result = fixture_client.post( '/account-payee/tax-form-info/details/bulk?limit=2&offset=4', headers=mock_headers, ) assert result.status_code == 200 assert len(result.json['items']) == 1 assert result.json['total_count'] == 5 assert result.json['items'][0] == item5 result = fixture_client.post( '/account-payee/tax-form-info/details/bulk?limit=2&offset=5', headers=mock_headers, ) assert result.status_code == 200 assert len(result.json['items']) == 0 assert result.json['total_count'] == 5 assert result.json['items'] == [] result = fixture_client.post( '/account-payee/tax-form-info/details/bulk?limit=305&offset=-1', headers=mock_headers, ) assert result.status_code == 400 assert result.json == { 'code': 'error', 'message': { 'query': { 'limit': [ 'Must be greater than or equal to 1 and less than or equal to 300.' ], 'offset': ['Must be greater than or equal to 0.'], } }, } result = fixture_client.post( '/account-payee/tax-form-info/details/bulk?limit=10&offset=0', headers=mock_headers, json={'account_payee_ids': [1, 2]}, ) assert result.status_code == 200 assert result.json['total_count'] == 2 assert result.json['items'][0] == item1 assert result.json['items'][1] == item2 result = fixture_client.post( '/account-payee/tax-form-info/details/bulk?limit=10&offset=0', headers=mock_headers, json={'is_active': True}, ) assert result.status_code == 200 assert result.json['total_count'] == 4 assert result.json['items'] == [item1, item3, item4, item5] result = fixture_client.post( '/account-payee/tax-form-info/details/bulk?limit=10&offset=0', headers=mock_headers, json={'expiration_date_start': '2024-11-30'}, ) assert result.status_code == 200 assert result.json['total_count'] == 3 assert result.json['items'] == [item3, item4, item5] result = fixture_client.post( '/account-payee/tax-form-info/details/bulk?limit=10&offset=0', headers=mock_headers, json={'expiration_date_end': '2025-11-30'}, ) assert result.status_code == 200 assert result.json['total_count'] == 1 assert result.json['items'] == [item2] result = fixture_client.post( '/account-payee/tax-form-info/details/bulk?limit=10&offset=0', headers=mock_headers, json={ 'expiration_date_start': '2024-11-30', 'expiration_date_end': '2045-11-30', }, ) assert result.status_code == 200 assert result.json['total_count'] == 3 assert result.json['items'] == [item3, item4, item5] result = fixture_client.post( '/account-payee/tax-form-info/details/bulk?limit=10&offset=0', headers=mock_headers, json={ 'expiration_date_start': '2024-11-30', 'expiration_date_end': '2025-11-30', 'is_active': True, }, ) assert result.status_code == 200 assert result.json['total_count'] == 0 assert result.json['items'] == [] result = fixture_client.post( '/account-payee/tax-form-info/details/bulk?limit=10&offset=0', headers=mock_headers, json={ 'expiration_date_start': '2014-11-30', 'expiration_date_end': '2045-11-30', 'is_active': True, 'account_payee_ids': [1, 2, 3, 4, 5], }, ) assert result.status_code == 200 assert result.json['total_count'] == 3 assert result.json['items'] == [item3, item4, item5] result = fixture_client.post( '/account-payee/tax-form-info/details/bulk?limit=10&offset=0', headers=mock_headers, json={ 'account_payee_ids': [1, 2, 'a'], }, ) assert result.status_code == 400 assert result.json == { 'code': 'error', 'message': { 'json': { 'account_payee_ids': { '2': ['Must be an integer greater or equal to 0.'] } } }, } result = fixture_client.post( '/account-payee/tax-form-info/details/bulk?limit=10&offset=0', headers=mock_headers, json={ 'expiration_date_start': '2027-11-30', 'expiration_date_end': '2025-11-30', }, ) assert result.status_code == 400 assert result.json == { 'code': 'error', 'message': { 'json': { '_schema': ['expiration_date_start must be before expiration_date_end'] } }, } @patch( 'payee.utils.permissions.authorization_backend.is_authorized', return_value=False ) @pytest.mark.parametrize( 'headers', (mock_abacus_headers(), mock_abacus_readonly_headers()) ) def test_get_tax_form_info_details_not_finance_user( fresh_db, account_fixtures, payoneer_program_fixtures, account_payee_fixtures, account_payee_tax_form_info, tax_form_info_secure_documents, fixture_client, headers, ): result = fixture_client.post( '/account-payee/tax-form-info/details/bulk', headers=headers ) assert result.status_code == 200 assert result.json['total_count'] == 5 item1, item2, item3, item4, item5 = result.json['items'] result = fixture_client.post( '/account-payee/tax-form-info/details/bulk?limit=2&offset=1', headers=headers, ) assert result.status_code == 200 assert len(result.json['items']) == 2 assert result.json['total_count'] == 5 assert result.json['items'][0] == item2 assert set( (k, v) for k, v in MOCK_TAX_FORM_DETAILS[item2['tax_form_type']].items() if k != 'tin' ) <= set(result.json['items'][0].items()), result.json['items'][0] assert result.json['items'][1] == item3 assert set( (k, v) for k, v in MOCK_TAX_FORM_DETAILS[item3['tax_form_type']].items() if k != 'tin' ) <= set(result.json['items'][0].items()), result.json['items'][0] result = fixture_client.post( '/account-payee/tax-form-info/details/bulk?limit=2&offset=4', headers=headers, ) assert result.status_code == 200 assert len(result.json['items']) == 1 assert result.json['total_count'] == 5 assert result.json['items'][0] == item5 result = fixture_client.post( '/account-payee/tax-form-info/details/bulk?limit=2&offset=5', headers=headers, ) assert result.status_code == 200 assert len(result.json['items']) == 0 assert result.json['total_count'] == 5 assert result.json['items'] == [] result = fixture_client.post( '/account-payee/tax-form-info/details/bulk?limit=305&offset=-1', headers=headers, ) assert result.status_code == 400 assert result.json == { 'code': 'error', 'message': { 'query': { 'limit': [ 'Must be greater than or equal to 1 and less than or equal to 300.' ], 'offset': ['Must be greater than or equal to 0.'], } }, } result = fixture_client.post( '/account-payee/tax-form-info/details/bulk?limit=10&offset=0', headers=headers, json={'account_payee_ids': [1, 2]}, ) assert result.status_code == 200 assert result.json['total_count'] == 2 assert result.json['items'][0] == item1 assert result.json['items'][1] == item2 result = fixture_client.post( '/account-payee/tax-form-info/details/bulk?limit=10&offset=0', headers=headers, json={'is_active': True}, ) assert result.status_code == 200 assert result.json['total_count'] == 4 assert result.json['items'] == [item1, item3, item4, item5] result = fixture_client.post( '/account-payee/tax-form-info/details/bulk?limit=10&offset=0', headers=headers, json={'expiration_date_start': '2024-11-30'}, ) assert result.status_code == 200 assert result.json['total_count'] == 3 assert result.json['items'] == [item3, item4, item5] result = fixture_client.post( '/account-payee/tax-form-info/details/bulk?limit=10&offset=0', headers=headers, json={'expiration_date_end': '2025-11-30'}, ) assert result.status_code == 200 assert result.json['total_count'] == 1 assert result.json['items'] == [item2] result = fixture_client.post( '/account-payee/tax-form-info/details/bulk?limit=10&offset=0', headers=headers, json={ 'expiration_date_start': '2024-11-30', 'expiration_date_end': '2045-11-30', }, ) assert result.status_code == 200 assert result.json['total_count'] == 3 assert result.json['items'] == [item3, item4, item5] result = fixture_client.post( '/account-payee/tax-form-info/details/bulk?limit=10&offset=0', headers=headers, json={ 'expiration_date_start': '2024-11-30', 'expiration_date_end': '2025-11-30', 'is_active': True, }, ) assert result.status_code == 200 assert result.json['total_count'] == 0 assert result.json['items'] == [] result = fixture_client.post( '/account-payee/tax-form-info/details/bulk?limit=10&offset=0', headers=headers, json={ 'expiration_date_start': '2014-11-30', 'expiration_date_end': '2045-11-30', 'is_active': True, 'account_payee_ids': [1, 2, 3, 4, 5], }, ) assert result.status_code == 200 assert result.json['total_count'] == 3 assert result.json['items'] == [item3, item4, item5] result = fixture_client.post( '/account-payee/tax-form-info/details/bulk?limit=10&offset=0', headers=headers, json={ 'account_payee_ids': [1, 2, 'a'], }, ) assert result.status_code == 400 assert result.json == { 'code': 'error', 'message': { 'json': { 'account_payee_ids': { '2': ['Must be an integer greater or equal to 0.'] } } }, } result = fixture_client.post( '/account-payee/tax-form-info/details/bulk?limit=10&offset=0', headers=headers, json={ 'expiration_date_start': '2027-11-30', 'expiration_date_end': '2025-11-30', }, ) assert result.status_code == 400 assert result.json == { 'code': 'error', 'message': { 'json': { '_schema': ['expiration_date_start must be before expiration_date_end'] } }, } @pytest.mark.parametrize( 'deleted_tax_form_info_ids,requested_account_payee_ids,expected_tax_form_info_ids', ( ([2], [2], []), ([1, 2], [2, 3], [3]), ([2, 3], [1, 2, 3, 4], [1, 4]), ([], [1, 2, 3, 4], [1, 2, 3, 4]), ), ) @patch('payee.utils.validations._get_vendor_ids', return_value=[1, 2, 3, 4, 5]) def test_get_tax_form_info_soft_deleted( mock_vendor_ids: Mock, account_fixtures: None, payoneer_program_fixtures: None, account_payee_fixtures: None, account_payee_tax_form_info: None, tax_form_info_secure_documents: None, fixture_client: FlaskClient, mock_bypass_headers: dict[str, str], deleted_tax_form_info_ids: list[int], requested_account_payee_ids: list[int], expected_tax_form_info_ids: list[int], ): """Test getting tax form info soft deleted records.""" for index, account_payee_tax_form_info_id in enumerate( deleted_tax_form_info_ids, 1 ): TaxFormInfo.delete_with_revision( TaxFormInfo.get_by_id_or_error(account_payee_tax_form_info_id), f'rev{index}', ) for url in ( '/account-payee/tax-form-info/bulk', '/account-payee/tax-form-info/details/bulk', ): result = fixture_client.post( url, headers=mock_bypass_headers, json={'account_payee_ids': requested_account_payee_ids}, ) assert result.status_code == HTTPStatus.OK, result.text data = result.json assert data['total_count'] == len(expected_tax_form_info_ids) assert ( list(sorted(i['account_payee_tax_form_info_id'] for i in data['items'])) == expected_tax_form_info_ids ) @pytest.mark.parametrize( 'tax_form_type, details_fixture', MOCK_TAX_FORM_DETAILS.items() ) @patch('payee.utils.permissions.authorization_backend.is_authorized', return_value=True) def test_post_tax_form_create_success( fresh_db, fixture_client, tax_form_type, details_fixture, account_fixtures, payoneer_program_fixtures, account_payee_fixtures, tax_form_info_secure_documents, mock_headers, faker, ): """Test successfully posting the tax form.""" signed_date = faker.past_date() details = { **details_fixture, 'tax_form_type': tax_form_type, 'signed_date': str(signed_date), } res = fixture_client.post( '/account-payee/1/tax-form-info', json=details, headers=mock_headers ) assert res.status_code == 200, res.text assert res.json.items() >= {**details, 'account_payee_id': 1}.items() @patch('payee.utils.permissions.authorization_backend.is_authorized', return_value=True) def test_post_tax_form_update_success( fresh_db, fixture_client, account_fixtures, payoneer_program_fixtures, account_payee_fixtures, mock_headers, ): """Test successfully posting the tax form.""" updated_details = { 'tax_form_type': 'W9', 'tax_id_country': 'AFG', 'tin_type': 'updated test tin type', 'tin': 'updated test tin', 'tax_name': 'updated test tax name', 'tax_classification': 'updated test classification', 'signed_date': '2000-01-01', } res = fixture_client.post( '/account-payee/1/tax-form-info', json=updated_details, headers=mock_headers ) assert res.status_code == 200, res.text assert res.json.items() >= {**updated_details, 'account_payee_id': 1}.items() @pytest.mark.parametrize( 'tax_form_type, details_fixture', MOCK_TAX_FORM_DETAILS.items() ) @patch('payee.utils.permissions.authorization_backend.is_authorized', return_value=True) def test_post_tax_form_failure_payee( fresh_db, fixture_client, tax_form_type, details_fixture, account_fixtures, payoneer_program_fixtures, account_payee_fixtures, tax_form_info_secure_documents, mock_headers, faker, ): """Test successfully posting the tax form.""" signed_date = faker.past_date() details = { **details_fixture, 'tax_form_type': tax_form_type, 'signed_date': str(signed_date), } res = fixture_client.post( '/account-payee/999999999/tax-form-info', json=details, headers=mock_headers ) assert res.status_code == HTTPStatus.INTERNAL_SERVER_ERROR, res.text @pytest.mark.parametrize( 'tax_form_type, details_fixture', MOCK_TAX_FORM_DETAILS.items() ) @patch('payee.utils.permissions.authorization_backend.is_authorized', return_value=True) def test_post_tax_form_failure_validation( fresh_db, fixture_client, tax_form_type, details_fixture, account_fixtures, payoneer_program_fixtures, account_payee_fixtures, tax_form_info_secure_documents, mock_headers, ): """Test successfully posting the tax form.""" res = fixture_client.post( '/account-payee/1/tax-form-info', json={}, headers=mock_headers ) assert res.status_code == 400, res.text assert res.json['code'] == 'error' assert 'json' in res.json['message'] @patch('payee.utils.permissions.authorization_backend.is_authorized', return_value=True) def test_post_tax_form_update_type_failure( fresh_db: None, fixture_client: FlaskClient, account_fixtures: None, payoneer_program_fixtures: None, account_payee_fixtures: None, account_payee_tax_form_info: None, mock_headers: dict[str, str], ) -> None: """Test updating tax form info type validation error.""" updated_details = MOCK_TAX_FORM_DETAILS[TAX_FORM_TYPES.W9] | { 'tax_form_type': TAX_FORM_TYPES.W9 } res = fixture_client.post( '/account-payee/3/tax-form-info', json=updated_details, headers=mock_headers ) assert res.status_code == HTTPStatus.BAD_REQUEST, res.text assert ( res.text == '{"code": "error", "message": "Tax form type field is not editable."}' ) @patch('abacus_common_logic.models.base.datetime') @patch('payee.utils.permissions.authorization_backend.is_authorized', return_value=True) def test_post_tax_form_update_change_type_soft_deleted( _, mock_datetime: Mock, fresh_db: None, fixture_client: FlaskClient, account_fixtures: None, payoneer_program_fixtures: None, account_payee_fixtures: None, account_payee_tax_form_info: None, mock_headers: dict[str, str], ) -> None: """Test updating tax form info change type for soft deleted records.""" ts = datetime(2024, 3, 20, 10, 50, 44) account_payee_tax_form_info_id = 3 mock_datetime.now.return_value = ts TaxFormInfo.delete_with_revision( TaxFormInfo.get_by_id_or_error(account_payee_tax_form_info_id), 'rev1' ) # please see account_payee_tax_form_info, payee #3 tax form type is W8BEN updated_details = MOCK_TAX_FORM_DETAILS[TAX_FORM_TYPES.W9] | { 'tax_form_type': TAX_FORM_TYPES.W9 } res = fixture_client.post( f'/account-payee/{account_payee_tax_form_info_id}/tax-form-info', json=updated_details, headers=mock_headers, ) updated_details.update( { 'account_payee_id': 3, 'account_payee_tax_form_info_id': 6, 'expiration_date': None, 'last_modified': ts.isoformat(timespec='seconds'), 'signed_date': None, } ) assert res.status_code == HTTPStatus.OK, res.text assert res.json == updated_details assert object_as_dict( TaxFormInfo.query.get(account_payee_tax_form_info_id), ignore_fields=('created_at', 'created_by', 'last_modified_by', 'last_modified'), ) == { 'account_payee_id': 3, 'account_payee_tax_form_info_id': account_payee_tax_form_info_id, 'deleted_at': ts, 'deleted_by': 'default_user_id', 'expiration_date': date(2035, 12, 31), 'revision_id': 'rev1', 'signed_date': date(2022, 1, 1), 'tax_form_type': TAX_FORM_TYPES.W8BEN, } assert object_as_dict( TaxFormInfo.get_by_id_or_error(6), ignore_fields=('created_at', 'created_by', 'last_modified_by'), ) == { 'account_payee_id': 3, 'account_payee_tax_form_info_id': 6, 'deleted_at': None, 'deleted_by': None, 'expiration_date': None, 'last_modified': ts, 'revision_id': None, 'signed_date': None, 'tax_form_type': TAX_FORM_TYPES.W9, } @patch('payee.logic.secure_document.get_audit_fields') @patch('payee.connectors.secure_data.manager.uuid4') @patch( 'payee.connectors.secure_data.manager.current_timestamp', return_value=datetime.now(), ) @patch('payee.utils.permissions.authorization_backend.is_authorized', return_value=True) def test_delete_tax_form_success( mock_authorized: Mock, mock_timestamp: Mock, mock_uuid4: Mock, mock_get_audit_fields: Mock, fresh_db: None, account_fixtures: None, payoneer_program_fixtures: None, account_payee_fixtures: None, mock_config: Config, fixture_client: FlaskClient, mock_headers: dict[str, str], ) -> None: """Test successfully soft deleting tax form details.""" versions = ('rev0', 'rev1') tax_form_type = 'W9' mock_uuid4.side_effect = versions mock_get_audit_fields.return_value = MOCK_AUDIT_FIELDS w9_tax_form_info = TaxFormInfoFactory.create(tax_form_type=tax_form_type) document_class = w9_tax_form_info.tax_form_document_class account_payee_id = w9_tax_form_info.account_payee_id account_payee_tax_form_info_id = w9_tax_form_info.account_payee_tax_form_info_id test_secure_data = MOCK_TAX_FORM_DETAILS[TAX_FORM_TYPES.W9] create_secure_document_details( mock_config.SDM_CONFIG, document_class, account_payee_id=account_payee_id, **test_secure_data, ) res = fixture_client.delete( f'/account-payee/tax-form-info/{account_payee_tax_form_info_id}', headers=mock_headers, ) assert res.status_code == HTTPStatus.NO_CONTENT, res.text sql_item = TaxFormInfo.query.get(account_payee_tax_form_info_id) exprected_sql_item = { 'account_payee_id': account_payee_id, 'revision_id': versions[1], 'tax_form_type': tax_form_type, } assert { key: getattr(sql_item, key) for key in exprected_sql_item } == exprected_sql_item assert sql_item.deleted_by and sql_item.deleted_at sdm = get_secure_document_manager() items = sdm._get_items(document_class.__name__, account_payee_id, document_class) current_ts = sdm._get_current_timestamp() expected_items = [ test_secure_data | MOCK_AUDIT_FIELDS | { 'owner': f'{document_class.__name__}#{account_payee_id}', 'created_at': current_ts, 'revision': f'{tax_form_type}#{version}', } for version in versions ] assert sorted(items, key=lambda i: i['revision']) == expected_items @patch('payee.utils.permissions.authorization_backend.is_authorized', return_value=True) def test_delete_tax_form_sql_not_exists( _, fixture_client: FlaskClient, mock_headers: dict[str, str] ) -> None: """Test soft deleting tax form details failure on SQL record not found.""" res = fixture_client.delete('/account-payee/tax-form-info/1', headers=mock_headers) assert res.status_code == HTTPStatus.BAD_REQUEST, res.text @patch('payee.utils.permissions.authorization_backend.is_authorized', return_value=True) def test_delete_tax_form_sql_deleted( _, fresh_db: None, account_fixtures: None, payoneer_program_fixtures: None, account_payee_fixtures: None, fixture_client: FlaskClient, mock_headers: dict[str, str], ) -> None: """Test soft deleting tax form details failure on SQL record already removed.""" tax_form_type = 'W9' revision_id = 'rev1' w9_tax_form_info = TaxFormInfoFactory.create( tax_form_type=tax_form_type, deleted_at=datetime.now(), deleted_by='someone', revision_id=revision_id, ) account_payee_tax_form_info_id = w9_tax_form_info.account_payee_tax_form_info_id res = fixture_client.delete( f'/account-payee/tax-form-info/{account_payee_tax_form_info_id}', headers=mock_headers, ) assert res.status_code == HTTPStatus.BAD_REQUEST, res.text sql_item = TaxFormInfo.query.get(account_payee_tax_form_info_id) exprected_sql_item = { 'account_payee_id': 1, 'revision_id': revision_id, 'tax_form_type': tax_form_type, } assert { key: getattr(sql_item, key) for key in exprected_sql_item } == exprected_sql_item assert sql_item.deleted_by and sql_item.deleted_at @patch('payee.utils.permissions.authorization_backend.is_authorized', return_value=True) def test_delete_tax_form_dynamo_not_exists( _, fresh_db: None, account_fixtures: None, payoneer_program_fixtures: None, account_payee_fixtures: None, mock_config: Config, fixture_client: FlaskClient, mock_headers: dict[str, str], ) -> None: """Test soft deleting tax form details failure on DynamoDB record not found.""" tax_form_type = 'W9' w9_tax_form_info = TaxFormInfoFactory.create(tax_form_type=tax_form_type) document_class = w9_tax_form_info.tax_form_document_class account_payee_id = w9_tax_form_info.account_payee_id account_payee_tax_form_info_id = w9_tax_form_info.account_payee_tax_form_info_id res = fixture_client.delete( f'/account-payee/tax-form-info/{account_payee_tax_form_info_id}', headers=mock_headers, ) assert res.status_code == HTTPStatus.BAD_REQUEST, res.text sql_item = TaxFormInfo.query.get(account_payee_tax_form_info_id) exprected_sql_item = { 'account_payee_id': account_payee_id, 'revision_id': None, 'tax_form_type': tax_form_type, } assert { key: getattr(sql_item, key) for key in exprected_sql_item } == exprected_sql_item assert not sql_item.deleted_by and not sql_item.deleted_at sdm = get_secure_document_manager() items = sdm._get_items(document_class.__name__, account_payee_id, document_class) assert not items @patch.object( logic.TaxFormInfo, 'delete_with_revision', side_effect=LogicError('issues') ) @patch('payee.logic.secure_document.get_audit_fields') @patch('payee.connectors.secure_data.manager.uuid4') @patch( 'payee.connectors.secure_data.manager.current_timestamp', return_value=datetime.now(), ) @patch('payee.utils.permissions.authorization_backend.is_authorized', return_value=True) def test_delete_tax_form_sql_delete_error( mock_authorized: Mock, mock_timestamp: Mock, mock_uuid4: Mock, mock_get_audit_fields: Mock, mock_delete_with_revision: Mock, fresh_db: None, account_fixtures: None, payoneer_program_fixtures: None, account_payee_fixtures: None, mock_config: Config, fixture_client: FlaskClient, mock_headers: dict[str, str], ) -> None: """Test soft deleting tax form details failure on SQL delete.""" versions = ['rev0', 'rev1', 'rev2'] tax_form_type = 'W9' mock_uuid4.side_effect = versions mock_get_audit_fields.return_value = MOCK_AUDIT_FIELDS w9_tax_form_info = TaxFormInfoFactory.create(tax_form_type=tax_form_type) document_class = w9_tax_form_info.tax_form_document_class account_payee_id = w9_tax_form_info.account_payee_id account_payee_tax_form_info_id = w9_tax_form_info.account_payee_tax_form_info_id test_secure_data = MOCK_TAX_FORM_DETAILS[TAX_FORM_TYPES.W9] create_secure_document_details( mock_config.SDM_CONFIG, document_class, account_payee_id=account_payee_id, **test_secure_data, ) res = fixture_client.delete( f'/account-payee/tax-form-info/{account_payee_tax_form_info_id}', headers=mock_headers, ) assert res.status_code == HTTPStatus.BAD_REQUEST, res.text sql_item = TaxFormInfo.query.get(account_payee_tax_form_info_id) exprected_sql_item = { 'account_payee_id': account_payee_id, 'revision_id': None, 'tax_form_type': tax_form_type, } assert { key: getattr(sql_item, key) for key in exprected_sql_item } == exprected_sql_item assert not sql_item.deleted_by and not sql_item.deleted_at sdm = get_secure_document_manager() items = sdm._get_items(document_class.__name__, account_payee_id, document_class) versions.insert(0, '0') current_ts = sdm._get_current_timestamp() expected_items = [ test_secure_data | MOCK_AUDIT_FIELDS | { 'owner': f'{document_class.__name__}#{account_payee_id}', 'created_at': current_ts, 'revision': f'{tax_form_type}#{version}', } for version in versions ] assert sorted(items, key=lambda i: i['revision']) == expected_items