"""Unit tests for contract_flowthrough logic.""" from datetime import datetime from unittest.mock import MagicMock, patch import pytest from abacus_common_logic.constants.constants import SYSTEM_TIMEZONE from abacus_common_logic.utils.users import get_flask_user_id from freezegun import freeze_time from sqlalchemy.exc import IntegrityError from abacus_contract.constants.constants import CONTRACT_FLOWTHROUGH_STATUSES from abacus_contract.constants.error import ERROR_CONTRACT_FLOWTHROUGH_ALREADY_EXISTS from abacus_contract.logic import contract_flowthrough as logic from abacus_contract.tests.utils.factories import ( ContractFactory, ContractFlowthroughFactory, ) from abacus_contract.utils.exception import ObjectAlreadyExistException @patch('abacus_contract.logic.contract_flowthrough.ContractFlowthrough') @patch('abacus_contract.logic.contract_flowthrough.Contract') def test_get_contract_flowthrough_by_contract_id( mock_contract_model, mock_contract_flowthrough_model ): """Test getting a contract_flowthrough by contract_id.""" mock_contract = ContractFactory.create() mock_contract_flowthrough = ContractFlowthroughFactory.create( contract=mock_contract ) mock_contract_model.get_by_id_or_error.return_value = mock_contract mock_contract_flowthrough_model.get_by_contract_id.return_value = ( mock_contract_flowthrough ) contract_id = mock_contract.contract_id res = logic.get_contract_flowthrough_by_contract_id(contract_id) assert res.status == 200 assert res.message['contract_id'] == contract_id def test_get_non_existent_contract_flowthrough_by_contract_id(): """Test getting a non-existent contract_flowthrough by contract_id.""" mock_contract = ContractFactory.create() contract_id = mock_contract.contract_id res = logic.get_contract_flowthrough_by_contract_id(contract_id) assert res.status == 200 assert res.message == dict() @patch('abacus_contract.logic.contract_flowthrough.ContractFlowthrough') def test_get_flowthrough_records_by_contract_ids(mock_model): """Records are flat and each carries its contract_id.""" mock_contract = ContractFactory.create() contract_id = mock_contract.contract_id mock_flowthrough = ContractFlowthroughFactory.create(contract=mock_contract) mock_model.get_by_contract_ids.return_value = [mock_flowthrough] res = logic.get_flowthrough_records_by_contract_ids([contract_id]) mock_model.get_by_contract_ids.assert_called_once_with([contract_id]) # A flat list of records, not the {'data': ...} dataload wrapping. assert len(res) == 1 assert res[0]['contract_id'] == contract_id @patch('abacus_contract.logic.contract_flowthrough.ContractFlowthrough') def test_get_flowthrough_records_by_contract_ids_empty_skips_query(mock_model): """No authorized ids: returns an empty list without querying. The dataloader helper only ever passes the authorized ids through, so an id that never resolved to an account, or whose account wasn't authorized, must never reach this function's query. """ res = logic.get_flowthrough_records_by_contract_ids([]) assert res == [] mock_model.get_by_contract_ids.assert_not_called() @patch('abacus_contract.logic.contract_flowthrough.ContractFlowthrough') def test_soft_delete_contract_flowthrough(mock_model): """Test soft deleting contract_flowthrough.""" mock_contract_flowthrough = ContractFlowthroughFactory.create() mock_contract_flowthrough._soft_delete = MagicMock() res = logic.soft_delete_contract_flowthrough(mock_contract_flowthrough) assert res.status == 204 assert res.message is None mock_contract_flowthrough._soft_delete.assert_called_once() mock_model.commit_changes.assert_called_once() @patch('abacus_contract.logic.contract_flowthrough.db') def test_update_contract_flowthrough_rate(mock_db): """Test updating contract_flowthrough rate.""" mock_contract_flowthrough = ContractFlowthroughFactory.create() mock_put_request_body = {'flowthrough_rate': 2.10} mock_contract_flowthrough.update_attributes = MagicMock() mock_db.session.commit.return_value = True res = logic.update_contract_flowthrough( mock_contract_flowthrough, **mock_put_request_body ) assert res.status == 200 mock_contract_flowthrough.update_attributes.assert_called_once_with( **mock_put_request_body ) mock_db.session.commit.assert_called_once() @freeze_time(datetime(2025, 3, 3, 0, 0, 0, tzinfo=SYSTEM_TIMEZONE)) @patch('abacus_contract.logic.contract_flowthrough.db') def test_update_contract_flowthrough_status_to_paused(mock_db): """Test updating contract_flowthrough to PAUSED.""" mock_contract_flowthrough = ContractFlowthroughFactory.create() mock_put_request_body = { 'flowthrough_rate': 1.78, 'flowthrough_status': CONTRACT_FLOWTHROUGH_STATUSES.PAUSED, 'has_automatic_shutoff': 0, } mock_contract_flowthrough.update_attributes = MagicMock() mock_db.session.commit.return_value = True res = logic.update_contract_flowthrough( mock_contract_flowthrough, **mock_put_request_body ) assert res.status == 200 mock_contract_flowthrough.update_attributes.assert_called_once_with( **mock_put_request_body, previous_flowthrough_status='active', status_last_modified_by=get_flask_user_id(), status_last_modified=datetime(2025, 3, 3, 0, 0, 0, tzinfo=SYSTEM_TIMEZONE), ) mock_db.session.commit.assert_called_once() @patch('abacus_contract.logic.contract_flowthrough.db') @patch( 'abacus_contract.logic.contract_flowthrough' '._check_whether_contract_flowthrough_exist' ) @patch('abacus_contract.logic.contract_flowthrough.ContractFlowthrough') def test_create_contract_flowthrough( mock_contract_flowthrough_model, mock_validation, mock_db ): """Test creating contract_flowthrough.""" mock_contract = ContractFactory.create() mock_contract_flowthrough = ContractFlowthroughFactory.create( contract=mock_contract ) mock_post_request = { 'reference_flowthrough_calculation_id': 2, 'flowthrough_rate': '90.78', 'has_automatic_shutoff': 1, 'recoupment_cap': 908786, } mock_validation.return_value = True mock_contract_flowthrough_model.build.return_value = mock_contract_flowthrough mock_db.session.commit.return_value = True res = logic.create_contract_flowthrough( mock_contract.contract_id, **mock_post_request ) assert res.status == 201 mock_contract_flowthrough_model.build.assert_called_once_with( contract_id=mock_contract.contract_id, reference_flowthrough_calculation_id=2, flowthrough_rate='90.78', has_automatic_shutoff=1, recoupment_cap=908786, calculation_comment=None, ) mock_db.session.commit.assert_called_once() @patch('abacus_contract.logic.contract_flowthrough.db') @patch( 'abacus_contract.logic.contract_flowthrough' '._check_whether_contract_flowthrough_exist' ) @patch('abacus_contract.logic.contract_flowthrough.ContractFlowthrough') def test_create_contract_flowthrough_validation_error( mock_contract_flowthrough_model, mock_validation, mock_db ): """Test validation error is thrown when creating contract_flowthrough.""" mock_contract = ContractFactory.create() mock_post_request = { 'reference_flowthrough_calculation_id': 2, 'flowthrough_rate': '90.78', 'has_automatic_shutoff': 1, 'recoupment_cap': 908786, } mock_validation.side_effect = ObjectAlreadyExistException('Already Exist') mock_contract_flowthrough_model.build.return_value = False mock_db.session.commit.return_value = False res = logic.create_contract_flowthrough( mock_contract.contract_id, **mock_post_request ) assert res.status == 409 assert res.errors['message'] == 'Already Exist' mock_contract_flowthrough_model.build.assert_not_called() mock_db.session.commit.assert_not_called() @patch('abacus_contract.logic.contract_flowthrough.db') @patch( 'abacus_contract.logic.contract_flowthrough' '._check_whether_contract_flowthrough_exist' ) @patch('abacus_contract.logic.contract_flowthrough.ContractFlowthrough') def test_create_contract_flowthrough_integrity_error( mock_contract_flowthrough_model, mock_validation, mock_db ): """Test IntegrityError on commit returns 409 with human-readable message.""" mock_contract = ContractFactory.create() mock_post_request = { 'reference_flowthrough_calculation_id': 2, 'flowthrough_rate': '90.78', } mock_validation.return_value = None mock_contract_flowthrough_model.build.return_value = MagicMock() mock_db.session.commit.side_effect = IntegrityError( statement=None, params=None, orig=Exception('unique constraint') ) res = logic.create_contract_flowthrough( mock_contract.contract_id, **mock_post_request ) assert res.status == 409 assert res.errors['message'] == ERROR_CONTRACT_FLOWTHROUGH_ALREADY_EXISTS.format( contract_id=mock_contract.contract_id ) mock_db.session.rollback.assert_called_once() @patch('abacus_contract.logic.contract_flowthrough.ContractFlowthrough') def test__check_whether_contract_flowthrough_exist(mock_contract_flowthrough_model): """Test _check_whether_contract_flowthrough_exist function". returns False if the contact_flowthrough is not added to a contract. """ mock_contract = ContractFactory.create() mock_contract_flowthrough_model.get_by_contract_id.return_value = None res = logic._check_whether_contract_flowthrough_exist(mock_contract.contract_id) assert res is False @patch('abacus_contract.logic.contract_flowthrough.ContractFlowthrough') def test__check_whether_contract_flowthrough_exist_error( mock_contract_flowthrough_model, ): """Test _check_whether_contract_flowthrough_exist function". throws an error if the contact_flowthrough is added to a contract. """ mock_contract = ContractFactory.create() mock_contract_flowthrough = ContractFlowthroughFactory.create( contract=mock_contract ) mock_contract_flowthrough_model.get_by_contract_id.return_value = ( mock_contract_flowthrough ) with pytest.raises(ObjectAlreadyExistException) as excinfo: logic._check_whether_contract_flowthrough_exist(mock_contract.contract_id) assert str(excinfo.value) == ERROR_CONTRACT_FLOWTHROUGH_ALREADY_EXISTS.format( contract_id=mock_contract.contract_id )