"""Functional tests for account endpoints.""" import datetime from unittest.mock import MagicMock, patch from abacus_common_logic.utils.dates import safe_format_datetime from flask.testing import FlaskClient from abacus_account.constants import constants from abacus_account.constants.error import ERROR_ACCOUNT_ALREADY_EXISTS from abacus_account.constants.error import ERROR_PAYMENT_TERM_TEMPLATE_DOES_NOT_EXISTS from abacus_account.constants.error import ERROR_UNKNOWN_COUNTRY from abacus_account.models.account import Account from tests.utils.factories import AccountFactory from tests.utils.factories import AccountPayeeFactory from tests.utils.factories import AccountPaymentTermFactory from tests.utils.factories import AccountPaymentTermTemplateFactory from tests.utils.factories import PaymentEntityPayoneerProgramFactory from tests.utils.factories import PaymentHoldFactory from tests.utils.factories import ReferencePaymentTypeFactory from tests.utils.factories import ReferencePayoneerProgramFactory def test_create_an_account(fixture_client): """Test new account creation.""" account_post_data = { 'account_name': 'Test Account', 'account_id': 654321, 'created_by': 'sax_ingestion' } response = fixture_client.post('/account', json=account_post_data) assert response.status_code == 201 accounts = Account.query.all() assert len(accounts) == 1 created = accounts[0] assert created.account_id == account_post_data['account_id'] assert created.created_by == account_post_data['created_by'] assert created.account_tax_info.country_of_tax_residence is None def test_create_account_with_payment_terms_and_tax_info( reference_payment_entity_fixture, fixture_client ): """Test creation also creates account_payee, payment_terms and tax_info.""" template = AccountPaymentTermTemplateFactory.create( payment_terms={ 'payment_minimum': '35.00', 'payment_schedule': constants.PAYMENT_SCHEDULE.SCHEDULE_45_DAYS_MONTH, 'agreement_type_id': 1, 'payment_entity_id': 5 } ) reference_payment_type = \ ReferencePaymentTypeFactory.create(reference_payment_type_id=8) payment_entity_id = template.payment_terms.get('payment_entity_id') ReferencePayoneerProgramFactory.create( payoneer_program_id=9999, reference_payment_type_id=reference_payment_type.reference_payment_type_id ) PaymentEntityPayoneerProgramFactory.create( reference_payment_entity_id=payment_entity_id, payoneer_program_id=9999, payment_currency=template.currency_code, ) post_data = { 'account_id': 11111, 'account_name': 'A Count', 'currency_code': template.currency_code, 'country_of_tax_residence': constants.VAT_APPLICABLE_COUNTRIES.GBR, 'created_by': 'someone', } res = fixture_client.post('/account', json=post_data) assert res.status_code == 201 account = Account.query.first() assert account.account_payee assert account.account_payee.payoneer_program_id == 9999 assert account.account_payment_term assert account.account_tax_info assert res.json == { 'account_id': post_data.get('account_id'), 'account_name': post_data.get('account_name'), 'account_payee_id': account.account_payee.account_payee_id, 'account_payment_term_id': account.account_payment_term.account_payment_term_id, 'created_by': post_data.get('created_by'), 'sap_created_at': None } assert account.account_tax_info.country_of_tax_residence == \ post_data.get('country_of_tax_residence') def test_create_account_skip_payment_term_creation_from_template( fixture_client ): """Test creation skipping payment term creation.""" template = AccountPaymentTermTemplateFactory.create( payment_terms={ 'payment_minimum': '35.00', 'payment_schedule': constants.PAYMENT_SCHEDULE.SCHEDULE_45_DAYS_MONTH, 'agreement_type_id': 1, 'payment_entity_id': 5 } ) reference_payment_type = ReferencePaymentTypeFactory.create( reference_payment_type_id=8 ) ReferencePayoneerProgramFactory.create( payoneer_program_id=9999, reference_payment_type_id=reference_payment_type.reference_payment_type_id ) post_data = { 'account_id': 11111, 'account_name': 'A Count', 'currency_code': template.currency_code, 'country_of_tax_residence': constants.VAT_APPLICABLE_COUNTRIES.GBR, 'created_by': 'someone', 'creation_source': constants.SKIP_PAYMENT_TERM_TEMPLATE_CREATION_SOURCES[0] } res = fixture_client.post('/account', json=post_data) assert res.status_code == 201 account = Account.query.first() assert account.account_payee assert not account.account_payee.payoneer_program_id assert account.account_payment_term.currency_code == post_data['currency_code'] assert not account.account_payment_term.payment_minimum assert not account.account_payment_term.payment_entity_id assert not account.account_payment_term.agreement_type_id assert not account.account_payment_term.payment_schedule assert account.account_tax_info assert res.json == { 'account_id': post_data.get('account_id'), 'account_name': post_data.get('account_name'), 'account_payee_id': account.account_payee.account_payee_id, 'account_payment_term_id': account.account_payment_term.account_payment_term_id, 'created_by': post_data.get('created_by'), 'sap_created_at': None } assert account.account_tax_info.country_of_tax_residence == \ post_data.get('country_of_tax_residence') def test_create_account_error_account_already_exists(fixture_client): """Test new account creation returns error if account already exists.""" account = AccountFactory.create() post_data = { 'account_id': account.account_id, 'account_name': account.account_name } res = fixture_client.post('/account', json=post_data) assert res.status_code == 409 assert res.json.get('message') == \ ERROR_ACCOUNT_ALREADY_EXISTS.format(account_id=account.account_id) def test_create_account_invalid_template(fixture_client): """Test account creation returns error if template doesn't exist for specified currency_code.""" # noqa: E501 currency_code = 'AOA' post_data = { 'account_id': 11111, 'account_name': 'A Count', 'currency_code': currency_code, 'country_of_tax_residence': constants.VAT_APPLICABLE_COUNTRIES.GBR } res = fixture_client.post('/account', json=post_data) error_msg = ERROR_PAYMENT_TERM_TEMPLATE_DOES_NOT_EXISTS.format( currency_code=currency_code ) assert res.status_code == 400 assert res.json.get('message') == error_msg assert not Account.query.all() def test_create_account_invalid_currency_code(fixture_client): """Test account creation returns error if an alpha-2 currency_code is used.""" currency_code = 'CA' post_data = { 'account_id': 11111, 'account_name': 'A Count', 'currency_code': currency_code, 'country_of_tax_residence': constants.VAT_APPLICABLE_COUNTRIES.GBR } res = fixture_client.post('/account', json=post_data) error_msg = 'Length must be 3.' assert res.status_code == 400 assert res.json.get('message')['currency_code'][0] == error_msg assert not Account.query.all() def test_create_account_invalid_country_code(fixture_client): """Test account creation returns error if invalid country code is used.""" template = AccountPaymentTermTemplateFactory.create() country_code = 'BUG' post_data = { 'account_id': 11111, 'account_name': 'A Count', 'currency_code': template.currency_code, 'country_of_tax_residence': country_code } res = fixture_client.post('/account', json=post_data) assert res.status_code == 400 assert res.json.get('message') == ERROR_UNKNOWN_COUNTRY.format(code=country_code) assert not Account.query.all() def test_get_account_by_id(fixture_client): """Get /account/:id.""" new_account = AccountFactory.create( account_name='Test Account', account_id=123456, created_by='sax_ingestion' ) res = fixture_client.get(f'/account/{new_account.account_id}') assert res.status_code == 200 assert res.json == { 'account_id': new_account.account_id, 'account_name': new_account.account_name, 'account_payee_id': None, 'account_payment_term_id': None, 'created_by': new_account.created_by, 'sap_created_at': None } @patch('abacus_account.blueprints.account.authorize_many_accounts') def test_get_account_by_id_with_invalid_profile( mock_authorize_many_accounts: MagicMock, fixture_client: FlaskClient, ) -> None: """Test the GET /account/:id endpoint with an invalid profile type.""" new_account = AccountFactory.create( account_name='Test Account', account_id=123456, created_by='sax_ingestion' ) mock_authorize_many_accounts.return_value = True res = fixture_client.get( f'/account/{new_account.account_id}', headers={ 'Content-Type': 'application/json', 'Orchard-Identity-Id': 'd5ca8ac3-7e51-4793-8775-50d11282504c', 'Orchard-Profile-Id': '35109', 'Orchard-Profile-Type': 'NotProfile', 'Orchard-Roles': "['administrator']", 'Orchard-Requestor-Service': 'graphql-abacus' } ) assert res.status_code == 200 assert res.json == { 'account_id': new_account.account_id, 'account_name': new_account.account_name, 'account_payee_id': None, 'account_payment_term_id': None, 'created_by': new_account.created_by, 'sap_created_at': None } def test_get_accounts(fixture_client): """GET /accounts.""" account_1 = AccountFactory.create(account_name='A Account') account_2 = AccountFactory.create(account_name='B Account') accounts = [account_1, account_2] res = fixture_client.get('/accounts') assert res.status_code == 200 assert len(res.json['items']) == len(accounts) sorted_data = sorted(res.json['items'], key=lambda x: x['account_name']) assert sorted_data[0]['account_id'] == accounts[0].account_id assert sorted_data[1]['account_id'] == accounts[1].account_id def test_get_accounts_by_ids(fixture_client): """POST /accounts.""" AccountFactory.create(account_id=1, account_name='A Account') AccountFactory.create(account_id=2, account_name='B Account') AccountFactory.create(account_id=3, account_name='C Account') account_ids_to_search = [1, 2] res = fixture_client.post('/accounts', json={'account_ids': account_ids_to_search}) assert res.status_code == 200 assert len(res.json['items']) == len(account_ids_to_search) assert res.json['total_count'] == len(account_ids_to_search) assert list(map(lambda v: v['account_id'], res.json['items'])) == \ account_ids_to_search res = fixture_client.get('/accounts?account_ids=1&account_ids=2') assert res.status_code == 200 assert len(res.json['items']) == len(account_ids_to_search) assert res.json['total_count'] == len(account_ids_to_search) assert list(map(lambda v: v['account_id'], res.json['items'])) == \ account_ids_to_search def test_get_accounts_eligible_for_payment( account_payment_eligible, fixture_client ): """GET /eligible-accounts when accounts are eligible.""" result = fixture_client.get('/eligible-accounts?payment_group_id=123') assert result.status_code == 200 accounts = result.json assert len(accounts) == 1 assert all([account.get('account_id') for account in accounts]) assert all([account.get('account_name') for account in accounts]) assert all([account.get('country_of_tax_residence') for account in accounts]) assert all([account.get('currency_code') for account in accounts]) assert all([account.get('contracts_payable') for account in accounts]) assert all([account.get('current_balance') for account in accounts]) assert all([account.get('eligibility_status') for account in accounts]) assert all([account.get('payment_entity_id') for account in accounts]) assert all([account.get('payment_minimum') for account in accounts]) assert all([account.get('payment_schedule') for account in accounts]) assert all([account.get('payoneer_program_id') for account in accounts]) def test_get_accounts_eligible_for_payment_none( account_payment_eligible, fixture_client ): """GET /eligible-accounts when no accounts meet payment_group criteria.""" res = fixture_client.get('/eligible-accounts?payment_group_id=1') assert res.status_code == 200 assert res.json == [] def test_get_accounts_eligible_for_payment_multiple_contracts( account_payment_multiple_contract_balances, fixture_client, ): """Get accounts eligible for payment with multiple contract balances.""" result = fixture_client.get('/eligible-accounts?payment_group_id=123') assert result.status_code == 200 accounts = result.json print(accounts[0]['contracts_payable']) assert len(accounts) == 1 assert accounts[0]['current_balance'] == '425.86' assert {'contract_id': 111, 'currency_code': 'GBP', 'current_balance': '100.0'}\ in accounts[0]['contracts_payable'] assert {'contract_id': 333, 'currency_code': 'GBP', 'current_balance': '325.86'}\ in accounts[0]['contracts_payable'] def test_get_accounts_eligible_for_payment_below_minimum( account_payment_ineligible_balance, fixture_client ): """GET /eligible-accounts when account's balance is below payment_minimum.""" res = fixture_client.get('/eligible-accounts?payment_group_id=123') assert res.status_code == 200 assert not res.json def test_get_accounts_eligible_for_payment_incomplete_payee_info( account_payment_ineligible_payee_info, fixture_client ): """GET /eligible-accounts when account's payee has not been accepted by payoneer.""" res = fixture_client.get('/eligible-accounts?payment_group_id=123') assert res.status_code == 200 assert not res.json def test_get_accounts_eligible_for_payment_incomplete_tax_details( account_payment_ineligible_tax_details, fixture_client ): """GET /eligible-accounts when account's payee has not submitted tax info.""" res = fixture_client.get('/eligible-accounts?payment_group_id=123') assert res.status_code == 200 assert not res.json def test_get_accounts_eligible_for_payment_missing_payoneer_payee_id( account_payment_ineligible_payoneer_payee_id, fixture_client ): """GET /eligible-accounts when account's payee missing payoneer_payee_id.""" res = fixture_client.get('/eligible-accounts?payment_group_id=123') assert res.status_code == 200 assert not res.json def test_get_accounts_eligible_for_payment_missing_payoneer_program_id( account_payment_ineligible_payoneer_program_id, fixture_client ): """GET /eligible-accounts when account's payee missing payoneer_program_id.""" res = fixture_client.get('/eligible-accounts?payment_group_id=123') assert res.status_code == 200 assert not res.json def test_get_payment_eligibility_status_no_payment_hold(fixture_client): """GET /account//payment-eligibility-status/ with no hold.""" account = AccountFactory.create() account_id = account.account_id result = fixture_client.get(f'/account/{account_id}/payment-eligibility-status/') assert result.status_code == 200 assert result.json == {'eligibility_status': constants.ELIGIBILITY_STATUSES.ACTIVE} def test_get_payment_eligibility_status_on_hold(fixture_client): """GET /account//payment-eligibility-status/ with payment_hold.""" payment_hold = PaymentHoldFactory.create() account_id = payment_hold.account_id res = fixture_client.get(f'/account/{account_id}/payment-eligibility-status/') assert res.status_code == 200 assert res.json == {'eligibility_status': constants.ELIGIBILITY_STATUSES.ON_HOLD} def test_get_payment_hold_eligibility_status_removed_hold(fixture_client): """GET /account//payment-eligibility-status/ with removed hold.""" payment_hold = PaymentHoldFactory.create(is_on_hold=False) account_id = payment_hold.account_id res = fixture_client.get(f'/account/{account_id}/payment-eligibility-status/') assert res.status_code == 200 assert res.json == {'eligibility_status': constants.ELIGIBILITY_STATUSES.ACTIVE} def test_get_payment_hold_eligibility_status_future_hold(fixture_client): """GET /account//payment-eligibility-status/ with future hold.""" payment_hold = PaymentHoldFactory.create(is_on_hold=True, start_date='2099-01-01') account_id = payment_hold.account_id res = fixture_client.get(f'/account/{account_id}/payment-eligibility-status/') assert res.status_code == 200 assert res.json == {'eligibility_status': constants.ELIGIBILITY_STATUSES.ACTIVE} def test_get_payment_hold_eligibility_status_future_remove_hold(fixture_client): """GET /account//payment-eligibility-status/ with future remove hold.""" # noqa: E501 payment_hold = PaymentHoldFactory.create(is_on_hold=False, start_date='2099-01-01') account_id = payment_hold.account_id res = fixture_client.get(f'/account/{account_id}/payment-eligibility-status/') assert res.status_code == 200 assert res.json == {'eligibility_status': constants.ELIGIBILITY_STATUSES.ON_HOLD} @patch('abacus_account.blueprints.account.authorize_many_accounts') def test_get_payment_hold_eligibility_status_invalid_profile( mock_authorize_many_accounts: MagicMock, fixture_client): """GET /account//payment-eligibility-status/ with invalid profile.""" payment_hold = PaymentHoldFactory.create() account_id = payment_hold.account_id mock_authorize_many_accounts.return_value = True res = fixture_client.get( f'/account/{account_id}/payment-eligibility-status/', headers={ 'Content-Type': 'application/json', 'Orchard-Identity-Id': 'd5ca8ac3-7e51-4793-8775-50d11282504c', 'Orchard-Profile-Id': '35109', 'Orchard-Profile-Type': 'NotProfile', 'Orchard-Roles': "['administrator']", 'Orchard-Requestor-Service': 'graphql-abacus' } ) assert res.status_code == 200 assert res.json == {'eligibility_status': constants.ELIGIBILITY_STATUSES.ON_HOLD} def test_get_sap_formatted_account_info(fixture_client): """GET /account//sap.""" mock_account = AccountFactory.create() result = fixture_client.get(f'/account/{mock_account.account_id}/sap/') assert result.status_code == 200 assert result.json == { 'AccountId': str(mock_account.account_id), 'AcctName': mock_account.account_name, 'Kunnr': None, 'Lifnr': None, 'Zzfield1': None, 'Zzfield2': None } def test_update_account(reference_payment_entity_fixture, fixture_client): """PUT /account//.""" mock_account = AccountFactory.create() mock_account_payee = AccountPayeeFactory.create(account=mock_account) mock_account_payment_term = AccountPaymentTermFactory.create(account=mock_account) sap_created_at_datetime = safe_format_datetime(datetime.date(2022, 2, 1)) put_body = {'sap_created_at': sap_created_at_datetime} response = fixture_client.put(f'/account/{mock_account.account_id}', json=put_body) assert response.status_code == 200 assert response.json == { 'account_id': mock_account.account_id, 'account_name': mock_account.account_name, 'account_payee_id': mock_account_payee.account_payee_id, 'account_payment_term_id': mock_account_payment_term.account_payment_term_id, 'created_by': mock_account.created_by, 'sap_created_at': sap_created_at_datetime } def test_update_account_name(reference_payment_entity_fixture, fixture_client): """PUT /account// endpoint to update the account_name.""" mock_account = AccountFactory.create() mock_account_payee = AccountPayeeFactory.create(account=mock_account) mock_account_payment_term = AccountPaymentTermFactory.create(account=mock_account) put_body = {'account_name': 'Test Account Name'} response = fixture_client.put(f'/account/{mock_account.account_id}', json=put_body) assert response.status_code == 200 assert response.json == { 'account_id': mock_account.account_id, 'account_name': put_body['account_name'], 'account_payee_id': mock_account_payee.account_payee_id, 'account_payment_term_id': mock_account_payment_term.account_payment_term_id, 'created_by': mock_account.created_by, 'sap_created_at': None } def test_get_accounts_by_search_term_account_id(fixture_client): """GET /accounts by account_id.""" AccountFactory.create(account_id=78901, account_name='2012 Music') AccountFactory.create(account_id=80002, account_name='2020 Gold') res = fixture_client.post('/accounts', json={'search_term': 890}) assert res.status_code == 200 assert res.json['total_count'] == 1 assert res.json['items'][0]['account_id'] == 78901 assert res.json['items'][0]['account_name'] == '2012 Music' res = fixture_client.get('/accounts?search_term=890') assert res.status_code == 200 assert res.json['total_count'] == 1 assert res.json['items'][0]['account_id'] == 78901 assert res.json['items'][0]['account_name'] == '2012 Music' def test_get_accounts_by_search_term_account_name(fixture_client): """GET /accounts by account_name.""" AccountFactory.create(account_id=78901, account_name='2012 Music') AccountFactory.create(account_id=80002, account_name='2020 Gold') res = fixture_client.post('/accounts', json={'search_term': 'gold'}) assert res.status_code == 200 assert res.json['total_count'] == 1 assert res.json['items'][0]['account_id'] == 80002 assert res.json['items'][0]['account_name'] == '2020 Gold' res = fixture_client.get('/accounts?search_term=gold') assert res.status_code == 200 assert res.json['total_count'] == 1 assert res.json['items'][0]['account_id'] == 80002 assert res.json['items'][0]['account_name'] == '2020 Gold' def test_get_accounts_by_payment_entity_id(fixture_client): """GET /accounts by payment_entity_id.""" test_account = AccountFactory.create( account_id=78901, account_name='2012 Music' ) AccountPaymentTermFactory.create(account=test_account, payment_entity_id=1) AccountFactory.create(account_id=80002, account_name='2020 Gold') res = fixture_client.get('/accounts?payment_entity_id=1') assert res.status_code == 200 assert res.json['total_count'] == 1 assert res.json['items'][0]['account_id'] == 78901 assert res.json['items'][0]['account_name'] == '2012 Music' res = fixture_client.post('/accounts', json={'payment_entity_id': 1}) assert res.status_code == 200 assert res.json['total_count'] == 1 assert res.json['items'][0]['account_id'] == 78901 assert res.json['items'][0]['account_name'] == '2012 Music' def test_get_accounts_by_reference_payment_type_id(fixture_client): """GET /accounts by reference_payment_type_id.""" test_account = AccountFactory.create( account_id=78901, account_name='2012 Music' ) AccountPayeeFactory.create( account=test_account, reference_payment_type_id=7 ) AccountFactory.create(account_id=80002, account_name='2020 Gold') res = fixture_client.get('/accounts?reference_payment_type_id=7') assert res.status_code == 200 assert res.json['total_count'] == 1 assert res.json['items'][0]['account_id'] == 78901 assert res.json['items'][0]['account_name'] == '2012 Music' res = fixture_client.post('/accounts', json={'reference_payment_type_id': 7}) assert res.status_code == 200 assert res.json['total_count'] == 1 assert res.json['items'][0]['account_id'] == 78901 assert res.json['items'][0]['account_name'] == '2012 Music' def test_get_accounts_by_agreement_type_ids(fixture_client): """GET /accounts by agreement_type_ids.""" test_account = AccountFactory.create( account_id=78901, account_name='2012 Music' ) AccountPaymentTermFactory.create( account=test_account, agreement_type_id=1, payment_entity_id=1 ) AccountFactory.create(account_id=80002, account_name='2020 Gold') res = fixture_client.get('/accounts?agreement_type_ids=1') assert res.status_code == 200 assert res.json['total_count'] == 1 assert res.json['items'][0]['account_id'] == 78901 assert res.json['items'][0]['account_name'] == '2012 Music' res = fixture_client.get('/accounts?agreement_type_ids=999') assert res.status_code == 200 assert res.json['total_count'] == 0 res = fixture_client.post('/accounts', json={'agreement_type_ids': [1]}) assert res.status_code == 200 assert res.json['total_count'] == 1 assert res.json['items'][0]['account_id'] == 78901 assert res.json['items'][0]['account_name'] == '2012 Music' res = fixture_client.post('/accounts', json={'agreement_type_ids': [999]}) assert res.json['total_count'] == 0 @patch('abacus_account.blueprints.account.authorize_many_accounts') def test_account_dataloader_with_invalid_profile( mock_authorize_many_accounts: MagicMock, fixture_client: FlaskClient, ) -> None: """Test the /account/dataloader endpoint for different feature flag values. - If True, then response.status_code should be 200. - If False, then response.status_code should be 403. """ mock_authorize_many_accounts.return_value = True res = fixture_client.post( '/account/dataloader', headers={ 'Content-Type': 'application/json', 'Orchard-Identity-Id': 'd5ca8ac3-7e51-4793-8775-50d11282504c', 'Orchard-Profile-Id': '35109', 'Orchard-Profile-Type': 'NotProfile', 'Orchard-Roles': "['administrator']", 'Orchard-Requestor-Service': 'graphql-abacus' }, json=[1, 2]) assert res.status_code == 200 def test_post_accounts_with_invalid_profile( fixture_client: FlaskClient, ) -> None: """Test the POST /accounts endpoint for different feature flag values. - If True, then response.status_code should be 200. - If False, then response.status_code should be 403. """ account_ids_to_search = [1, 2] res = fixture_client.post( '/accounts', headers={ 'Content-Type': 'application/json', 'Orchard-Identity-Id': 'd5ca8ac3-7e51-4793-8775-50d11282504c', 'Orchard-Profile-Id': '35109', 'Orchard-Profile-Type': 'NotProfile', 'Orchard-Roles': "['administrator']", 'Orchard-Requestor-Service': 'graphql-abacus' }, json={'account_ids': account_ids_to_search}) assert res.status_code == 200