"""Unit tests for reference_signing_entity logic.""" import datetime from unittest.mock import patch import pytest from werkzeug.exceptions import HTTPException from abacus_contract import models from abacus_contract.constants.constants import ( DEFAULT_PAGE_LIMIT, DEFAULT_PAGE_OFFSET, SIGNING_ENTITY_SORT_OPTIONS, SORT_ORDER_OPTIONS, ) from abacus_contract.logic import reference_signing_entity as logic from abacus_contract.tests.utils.factories import ( ReferenceSapProfitCenterFactory, ReferenceSigningEntityFactory, SigningEntitySapProfitCenterFactory, ) def test_list_sap_profit_centers_for_signing_entity_returns_only_authorized_live(): """Only PCs with a live junction row for the SE are returned.""" se = ReferenceSigningEntityFactory.create(with_junction_row=False) authorized_pc = ReferenceSapProfitCenterFactory.create(display_name='Authorized') deleted_pc = ReferenceSapProfitCenterFactory.create(display_name='Was authorized') _unrelated_pc = ReferenceSapProfitCenterFactory.create(display_name='Unrelated') SigningEntitySapProfitCenterFactory.create( reference_signing_entity=se, reference_sap_profit_center=authorized_pc ) soft_deleted_junction = SigningEntitySapProfitCenterFactory.create( reference_signing_entity=se, reference_sap_profit_center=deleted_pc ) models.SigningEntitySapProfitCenter.delete_by_id_or_error( soft_deleted_junction.signing_entity_sap_profit_center_id, soft_delete=True ) result = logic.list_sap_profit_centers_for_signing_entity( se.reference_signing_entity_id ) assert result.status == 200 assert result.message['total_count'] == 1 items = result.message['items'] assert len(items) == 1 assert items[0]['sap_profit_center']['reference_sap_profit_center_id'] == ( authorized_pc.reference_sap_profit_center_id ) def test_list_sap_profit_centers_for_signing_entity_q_filters_display_name_and_profit_center(): """?search_term matches against display_name or profit_center, case-insensitive.""" se = ReferenceSigningEntityFactory.create(with_junction_row=False) pc_match_display = ReferenceSapProfitCenterFactory.create( profit_center='AAA001', display_name='Foundation Media' ) pc_match_code = ReferenceSapProfitCenterFactory.create( profit_center='SANTA42', display_name='Other label' ) pc_no_match = ReferenceSapProfitCenterFactory.create( profit_center='ZZZ999', display_name='Different' ) for pc in (pc_match_display, pc_match_code, pc_no_match): SigningEntitySapProfitCenterFactory.create( reference_signing_entity=se, reference_sap_profit_center=pc ) result_display = logic.list_sap_profit_centers_for_signing_entity( se.reference_signing_entity_id, search_term='foundation' ) assert { item['sap_profit_center']['reference_sap_profit_center_id'] for item in result_display.message['items'] } == {pc_match_display.reference_sap_profit_center_id} result_code = logic.list_sap_profit_centers_for_signing_entity( se.reference_signing_entity_id, search_term='santa' ) assert { item['sap_profit_center']['reference_sap_profit_center_id'] for item in result_code.message['items'] } == {pc_match_code.reference_sap_profit_center_id} def test_list_sap_profit_centers_for_signing_entity_search_escapes_like_wildcards(): """search_term='%' should match nothing (escaped), not every row.""" se = ReferenceSigningEntityFactory.create(with_junction_row=False) pc1 = ReferenceSapProfitCenterFactory.create(display_name='Real label') pc2 = ReferenceSapProfitCenterFactory.create(display_name='Another label') for pc in (pc1, pc2): SigningEntitySapProfitCenterFactory.create( reference_signing_entity=se, reference_sap_profit_center=pc ) result = logic.list_sap_profit_centers_for_signing_entity( se.reference_signing_entity_id, search_term='%' ) assert result.message['total_count'] == 0 assert result.message['items'] == [] def test_list_sap_profit_centers_for_signing_entity_empty_when_se_has_no_mappings(): """SE with no live mappings -> empty items, total_count 0 (not 404).""" se = ReferenceSigningEntityFactory.create(with_junction_row=False) result = logic.list_sap_profit_centers_for_signing_entity( se.reference_signing_entity_id ) assert result.status == 200 assert result.message['items'] == [] assert result.message['total_count'] == 0 def test_list_sap_profit_centers_for_signing_entity_unknown_se_raises_404(): """Non-existent SE id -> framework abort 404.""" with pytest.raises(HTTPException) as exc_info: logic.list_sap_profit_centers_for_signing_entity(99999999) assert exc_info.value.code == 404 def test_list_sap_profit_centers_for_signing_entity_soft_deleted_se_returns_404(): """Soft-deleted SE -> 404 (matches the existing GET /reference-signing-entity/:id).""" se = ReferenceSigningEntityFactory.create(with_junction_row=False) pc = ReferenceSapProfitCenterFactory.create() SigningEntitySapProfitCenterFactory.create( reference_signing_entity=se, reference_sap_profit_center=pc ) se.update_attributes(deleted_at=datetime.datetime(2026, 1, 1)) models.ReferenceSigningEntity.commit_changes() result = logic.list_sap_profit_centers_for_signing_entity( se.reference_signing_entity_id ) assert result.status == 404 def test_list_sap_profit_centers_for_signing_entity_pagination_limits_results(): """?limit caps the page; total_count still reflects the full match set.""" se = ReferenceSigningEntityFactory.create(with_junction_row=False) for n in range(5): pc = ReferenceSapProfitCenterFactory.create(display_name=f'PC {n}') SigningEntitySapProfitCenterFactory.create( reference_signing_entity=se, reference_sap_profit_center=pc ) result = logic.list_sap_profit_centers_for_signing_entity( se.reference_signing_entity_id, limit=2, offset=0 ) assert result.status == 200 assert result.message['total_count'] == 5 assert len(result.message['items']) == 2 def test_list_sap_profit_centers_for_signing_entity_offset_past_end_returns_empty(): """?offset past the matching set -> empty items, total_count still accurate.""" se = ReferenceSigningEntityFactory.create(with_junction_row=False) for n in range(3): pc = ReferenceSapProfitCenterFactory.create(display_name=f'PC {n}') SigningEntitySapProfitCenterFactory.create( reference_signing_entity=se, reference_sap_profit_center=pc ) result = logic.list_sap_profit_centers_for_signing_entity( se.reference_signing_entity_id, limit=10, offset=100 ) assert result.status == 200 assert result.message['total_count'] == 3 assert result.message['items'] == [] def test_list_sap_profit_centers_for_signing_entity_orders_by_display_name(): """Results are ordered by display_name ascending (pinned for UI stability).""" se = ReferenceSigningEntityFactory.create(with_junction_row=False) for name in ('Charlie', 'Alpha', 'Bravo'): pc = ReferenceSapProfitCenterFactory.create(display_name=name) SigningEntitySapProfitCenterFactory.create( reference_signing_entity=se, reference_sap_profit_center=pc ) result = logic.list_sap_profit_centers_for_signing_entity( se.reference_signing_entity_id ) names = [ item['sap_profit_center']['display_name'] for item in result.message['items'] ] assert names == ['Alpha', 'Bravo', 'Charlie'] def test_list_sap_profit_centers_for_signing_entity_search_does_not_strip_whitespace(): """search_term is used as-is (no implicit strip), matching account.py precedent.""" se = ReferenceSigningEntityFactory.create(with_junction_row=False) pc = ReferenceSapProfitCenterFactory.create(display_name='Match') SigningEntitySapProfitCenterFactory.create( reference_signing_entity=se, reference_sap_profit_center=pc ) # Leading/trailing whitespace is part of the search literal; with no padding # on the data side, it should not match. (Documents current behaviour; # change here would need a Product decision.) result = logic.list_sap_profit_centers_for_signing_entity( se.reference_signing_entity_id, search_term=' match ' ) assert result.message['items'] == [] assert result.message['total_count'] == 0 @patch( 'abacus_contract.logic.reference_signing_entity.SigningEntitySapProfitCenterWithSapProfitCenterSchema' ) @patch('abacus_contract.logic.reference_signing_entity.models') @patch( 'abacus_contract.logic.reference_signing_entity.prepare_dataload_with_data_as_list_response' ) def test_get_profit_centers_by_signing_entities( mock_dataload_response, mock_models, mock_mapping_schema ): """Test to get profit centers by signing entities.""" mock_profit_centers = [ { 'signing_entity_sap_profit_center_id': 1, 'reference_signing_entity_id': 1, 'created_at': '2026-06-09T00:00:00.000000+0000', 'sap_profit_center': { 'reference_sap_profit_center_id': 1, 'profit_center': 'PC001', 'company_code': '4914', 'business_group': 'ORC', 'display_name': 'Test', }, } ] mock_formatted_data = [{'data': mock_profit_centers}, {'data': None}] mock_models.ReferenceSapProfitCenter.get_authorized_for_signing_entities.return_value = ( mock_profit_centers, 1, ) mock_dataload_response.return_value = mock_formatted_data mock_mapping_schema.return_value.dump.return_value = mock_profit_centers result = logic.get_profit_centers_by_signing_entities([1, 2]) assert result.status == 200 assert result.message == mock_formatted_data mock_models.ReferenceSapProfitCenter.get_authorized_for_signing_entities.assert_called_with( [1, 2], 100, 0 ) mock_dataload_response.assert_called_with( [1, 2], mock_profit_centers, 'reference_signing_entity_id' ) @patch( 'abacus_contract.logic.reference_signing_entity.SigningEntitySapProfitCenterWithSapProfitCenterSchema' ) @patch('abacus_contract.logic.reference_signing_entity.models') @patch( 'abacus_contract.logic.reference_signing_entity.prepare_dataload_with_data_as_list_response' ) def test_get_profit_centers_by_duplicate_signing_entities( mock_dataload_response, mock_models, mock_mapping_schema ): """Test to get profit centers by duplicate signing entities.""" mock_profit_centers = [ { 'signing_entity_sap_profit_center_id': 1, 'reference_signing_entity_id': 1, 'created_at': '2026-06-09T00:00:00.000000+0000', 'sap_profit_center': { 'reference_sap_profit_center_id': 1, 'profit_center': 'PC001', 'company_code': '4914', 'business_group': 'ORC', 'display_name': 'Test', }, } ] mock_formatted_data = [{'data': mock_profit_centers}, {'data': None}] mock_models.ReferenceSapProfitCenter.get_authorized_for_signing_entities.return_value = ( mock_profit_centers, 1, ) mock_dataload_response.return_value = mock_formatted_data mock_mapping_schema.return_value.dump.return_value = mock_profit_centers result = logic.get_profit_centers_by_signing_entities([1, 2, 2, 2, 2, 2]) assert result.status == 200 assert result.message == mock_formatted_data mock_models.ReferenceSapProfitCenter.get_authorized_for_signing_entities.assert_called_with( [1, 2], 100, 0 ) mock_dataload_response.assert_called_with( [1, 2], mock_profit_centers, 'reference_signing_entity_id' ) @patch('abacus_contract.logic.reference_signing_entity.models') @patch('abacus_contract.logic.reference_signing_entity.prepare_dataload_response') def test_get_signing_entities_by_ids(mock_dataload_response, mock_models): """Test to get signing entities by ids.""" mock_signing_entities = [ { 'reference_signing_entity_id': 1, 'reference_sap_profit_center_id': 1, 'reference_payment_entity_id': 1, 'company_code': '4920', 'legal_name': 'AWAL Digital Limited', 'vat_number': 'GB 423 4787 86', 'company_registration_number': '04430703', 'address': '2 Canal Reach, London, N1C 4DB', } ] mock_formatted_data = [{'data': mock_signing_entities[0]}, {'data': None}] mock_models.ReferenceSigningEntity.get_by_ids.return_value = mock_signing_entities mock_dataload_response.return_value = mock_formatted_data result = logic.get_signing_entities_by_ids([1, 2]) assert result.status == 200 assert result.message == mock_formatted_data mock_models.ReferenceSigningEntity.get_by_ids.assert_called_with([1, 2]) mock_dataload_response.assert_called_with( [1, 2], mock_signing_entities, 'reference_signing_entity_id' ) @patch('abacus_contract.logic.reference_signing_entity.models') @patch('abacus_contract.logic.reference_signing_entity.prepare_dataload_response') def test_get_signing_entities_by_duplicate_ids(mock_dataload_response, mock_models): """Test to get signing entities by duplicate ids.""" mock_signing_entities = [ { 'reference_signing_entity_id': 1, 'reference_sap_profit_center_id': 1, 'reference_payment_entity_id': 1, 'company_code': '4920', 'legal_name': 'AWAL Digital Limited', 'vat_number': 'GB 423 4787 86', 'company_registration_number': '04430703', 'address': '2 Canal Reach, London, N1C 4DB', } ] mock_formatted_data = [{'data': mock_signing_entities[0]}, {'data': None}] mock_models.ReferenceSigningEntity.get_by_ids.return_value = mock_signing_entities mock_dataload_response.return_value = mock_formatted_data result = logic.get_signing_entities_by_ids([1, 1, 2, 2]) assert result.status == 200 assert result.message == mock_formatted_data mock_models.ReferenceSigningEntity.get_by_ids.assert_called_with([1, 2]) mock_dataload_response.assert_called_with( [1, 2], mock_signing_entities, 'reference_signing_entity_id' ) @patch('abacus_contract.logic.reference_signing_entity.models') def test_get_reference_signing_entities(mock_models): """Test to get reference signing entities.""" mock_signing_entities = [ ReferenceSigningEntityFactory.create(with_junction_row=False) ] mock_models.ReferenceSigningEntity.get_reference_signing_entities.return_value = ( mock_signing_entities, 1, ) mock_request_params = { 'limit': 10, 'offset': 0, 'sort_by': SIGNING_ENTITY_SORT_OPTIONS.COMPANY_CODE, 'sort_order': SORT_ORDER_OPTIONS.DESC, } result = logic.get_reference_signing_entities(mock_request_params) assert result.status == 200 assert result.message['total_count'] == 1 mock_models.ReferenceSigningEntity.get_reference_signing_entities.assert_called_once_with( **mock_request_params )