"""Tests for accounting period logic.""" from datetime import datetime from unittest.mock import patch import pytest from royalties.constants import error from royalties.constants.constants import ( ACCOUNTING_PERIOD_STATUSES as PERIOD_STATUSES, CONTRACT_TYPES, STATEMENT_PERIOD_STATUSES, ) from royalties.logic import accounting_period as logic from royalties.tests.utils.factories import ( AccountingPeriodFactory, AccountingRunFactory, RunControllerFactory, StatementPeriodFactory, ) @patch('royalties.logic.accounting_period._validate_accounting_period') @patch('royalties.logic.accounting_period._validate_statement_period_state') @patch('royalties.logic.accounting_period.models') def test_create_accounting_period_success( mock_models, mock_validate_statement_period_state, mock_validate_accounting_period, test_app_request, ): """Test create accounting period success. When accounting period is valid, an accounting period is created with it's accounting runs for all eligible run controllers. """ mock_rc = RunControllerFactory.build() mock_acct_run = AccountingRunFactory.build() mock_statement_period = StatementPeriodFactory.create( statement_period_status=STATEMENT_PERIOD_STATUSES.CURRENT ) params = { 'accounting_period_name': 'Holland, 1945', 'contract_type': CONTRACT_TYPES.DISTRIBUTION, 'statement_period_id': mock_statement_period.statement_period_id, } mock_validate_statement_period_state.return_value = None mock_validate_accounting_period.return_value = None mock_models.RunController.get_all_associated_to_contracts.return_value = [mock_rc] mock_models.AccountingRun.build.return_value = mock_acct_run response = logic.create_accounting_period(**params) assert response.status == 201 mock_models.AccountingPeriod.create.assert_called_with( accounting_period_name=params['accounting_period_name'], accounting_runs=[mock_acct_run], accounting_period_status=PERIOD_STATUSES.OPEN, statement_period_id=params['statement_period_id'], contract_type=params['contract_type'], ) @patch('royalties.logic.accounting_period._validate_statement_period_state') def test_create_accounting_period_fails_with_duplicate_name( mock_validate_statement_period_state, test_app_request ): """Test creating an accounting period with an existing name fails.""" mock_statement_period = StatementPeriodFactory.create( statement_period_status=STATEMENT_PERIOD_STATUSES.CURRENT ) mock_accounting_period = AccountingPeriodFactory.create( accounting_period_status=PERIOD_STATUSES.CLOSED, closed_date='2020-01-01', statement_period=mock_statement_period, ) params = { 'accounting_period_name': mock_accounting_period.accounting_period_name, 'contract_type': CONTRACT_TYPES.DISTRIBUTION, 'statement_period_id': mock_statement_period.statement_period_id, } mock_validate_statement_period_state.return_value = None response = logic.create_accounting_period(**params) assert response.status == 400 assert response.errors['message'] == error.ERROR_ALREADY_EXISTS.format( object_type='Accounting Period' ) @patch('royalties.logic.accounting_period._validate_statement_period_state') @patch('royalties.logic.accounting_period.models') def test_create_accounting_period_fails_when_another_period_is_open( mock_models, mock_validate_statement_period_state, test_app_request ): """Test creation fails when another accounting period is still open.""" contract_type = CONTRACT_TYPES.DISTRIBUTION statement_period_id = 1 params = { 'accounting_period_name': 'Holland, 1945', 'contract_type': contract_type, 'statement_period_id': statement_period_id, } mock_validate_statement_period_state.return_value = None response = logic.create_accounting_period(**params) assert response.status == 400 assert response.errors['message'] == error.ERROR_OPEN_PERIOD_EXISTS.format( contract_type=contract_type, statement_period_id=statement_period_id ) mock_models.AccountingPeriod.create.assert_not_called() @patch('royalties.logic.accounting_period.models') def test_get_accounting_period_by_accounting_run_success(mock_models, test_app_request): """Test get_accounting_period_by_accounting_run success.""" accounting_run = mock_models.AccountingRun.get_by_id_or_error.return_value = ( AccountingRunFactory() ) response = logic.get_accounting_period_by_accounting_run( accounting_run.accounting_run_id ) assert response.status == 200 assert response.message == logic.accounting_period_detail_schema.dump( accounting_run.accounting_period ) @patch('royalties.logic.accounting_period.models') def test_update_accounting_period_success(mock_models, test_app_request): """Test update accounting period success.""" mock_models.AccountingPeriod.find_by_name.return_value = None period = AccountingPeriodFactory.build( accounting_period_status=PERIOD_STATUSES.OPEN ) response = logic.update_accounting_period( period, accounting_period_status=PERIOD_STATUSES.CLOSED ) assert response.status == 201 assert period.accounting_period_status == PERIOD_STATUSES.CLOSED mock_models.AccountingPeriod.commit_changes.assert_called_once() @patch('royalties.logic.accounting_period.models') @patch('royalties.logic.accounting_period.current_timestamp') def test_update_accounting_period_to_closed( mock_current_timestamp, mock_models, test_app_request ): """Test closing an accounting period.""" mock_models.AccountingPeriod.find_by_name.return_value = None period = AccountingPeriodFactory.build( accounting_period_status=PERIOD_STATUSES.OPEN ) mock_current_timestamp.return_value = datetime.now() response = logic.update_accounting_period( period, accounting_period_status=PERIOD_STATUSES.CLOSED ) assert response.status == 201 assert period.accounting_period_status == PERIOD_STATUSES.CLOSED assert period.closed_date mock_models.AccountingPeriod.commit_changes.assert_called_once() mock_models.AccountingPeriod.find_by_name.assert_not_called() @patch('royalties.logic.accounting_period.models') def test_update_accounting_period_fails_on_invalid_status_change( mock_models, test_app_request ): """Test update fails if the change to the new status is invalid.""" mock_period = AccountingPeriodFactory.build( accounting_period_status=PERIOD_STATUSES.CLOSED ) open_status = PERIOD_STATUSES.OPEN params = {'accounting_period_status': open_status} response = logic.update_accounting_period(mock_period, **params) assert response.status == 400 assert response.errors['message'] == error.ERROR_INVALID_PERIOD_STATUS.format( current_status=mock_period.accounting_period_status, new_status=open_status ) @patch('royalties.logic.accounting_period.models') def test_update_accounting_period_fails_with_duplicate_name( mock_models, test_app_request ): """Test update fails if the given name already exists.""" mock_period = AccountingPeriodFactory.build(accounting_period_name='period name') mock_period_2 = AccountingPeriodFactory.build(accounting_period_name='other period') mock_models.AccountingPeriod.find_by_name.return_value = mock_period params = {'accounting_period_name': mock_period.accounting_period_name} response = logic.update_accounting_period(mock_period_2, **params) assert response.status == 400 assert response.errors['message'] == error.ERROR_ALREADY_EXISTS.format( object_type='Accounting Period' ) @patch('royalties.logic.accounting_period.models') def test_validate_accounting_period_state_no_period(mock_models): """Test validation fails when accounting period does not exist.""" fake_period_id = 123 mock_models.AccountingPeriod.get_by_id.return_value = None result = logic.validate_accounting_period_state(fake_period_id) assert result == error.ERROR_ENTITY_DOES_NOT_EXIST.format( object_type='AccountingPeriod', object_id=fake_period_id ) @patch('royalties.logic.accounting_period.models') def test_validate_period_state_closed(mock_models): """Test validation fails when accounting period is closed.""" mock_period = AccountingPeriodFactory.build( closed_date='2020-01-01', accounting_period_status=PERIOD_STATUSES.CLOSED ) mock_models.AccountingPeriod.get_by_id.return_value = mock_period result = logic.validate_accounting_period_state(mock_period.accounting_period_id) assert result == error.ERROR_NO_OPEN_PERIOD @patch('royalties.logic.accounting_period.models') def test_validate_period_state_is_valid(mock_models): """Test validation passes when accounting period has valid status.""" mock_period = AccountingPeriodFactory.build() mock_models.AccountingPeriod.get_by_id.return_value = mock_period result = logic.validate_accounting_period_state(mock_period.accounting_period_id) assert result is None @patch('royalties.logic.accounting_period.models') def test_validate_accounting_period(mock_models): """Test _validate_accounting_period for existing accounting_period.""" mock_statement_period = StatementPeriodFactory.create( statement_period_status=STATEMENT_PERIOD_STATUSES.CURRENT ) mock_accounting_period = AccountingPeriodFactory.create( statement_period=mock_statement_period ) accounting_period_name = mock_accounting_period.accounting_period_name contract_type = CONTRACT_TYPES.DISTRIBUTION statement_period_id = mock_statement_period.statement_period_id with pytest.raises( Exception, match=error.ERROR_OPEN_PERIOD_EXISTS.format( contract_type=contract_type, statement_period_id=statement_period_id ), ): mock_models.AccountingPeriod.get_current_period.return_value = ( mock_accounting_period ) logic._validate_accounting_period( accounting_period_name, contract_type, statement_period_id ) with pytest.raises( Exception, match=error.ERROR_ALREADY_EXISTS.format(object_type='Accounting Period'), ): mock_models.AccountingPeriod.get_current_period.return_value = None mock_models.AccountingPeriod.find_by_name.return_value = mock_accounting_period logic._validate_accounting_period( accounting_period_name, contract_type, statement_period_id )