"""Tests for custom report handlers.""" from unittest.mock import patch from fastapi import HTTPException import pytest from moneyhub.constants.constants import CORRELATION_ID_HEADER from moneyhub.constants.constants import NumberFormat from moneyhub.constants.constants import ReportCustomColumnDimension from moneyhub.constants.constants import ReportCustomFileType from moneyhub.constants.constants import ReportCustomRowDimension from moneyhub.constants.constants import ReportCustomStatus from moneyhub.constants.constants import RevenueType from moneyhub.constants.error import FORBIDDEN_REPORT_URL_ACCESS from moneyhub.constants.error import MISSING_PROFILE_HEADERS from moneyhub.handlers.report_custom import _verify_report_access from moneyhub.schemas import ReportCustomCreateSchema from moneyhub.schemas.report_custom import ReportCustomFiltersSchema from tests.utils.factories import ReportCustomFactory @pytest.mark.parametrize('subaccount_id', [ None, 123, ]) @patch('moneyhub.handlers.report_custom.profile_has_access_to_resource') @patch('moneyhub.handlers.report_custom.models') def test_verify_report_access(mock_models, mock_profile_has_access_to_resource, subaccount_id): """Test verifying access to a custom report.""" account_id = 24601 profile_type = 'MoneyhubProfile' profile_id = 54321 report_custom_id = 123 mock_profile_has_access_to_resource.return_value = True mock_models.ReportCustom.get_by_id_or_error.return_value = ReportCustomFactory.build( report_custom_id=report_custom_id, account_id=account_id, subaccount_id=subaccount_id ) _verify_report_access(profile_type, profile_id, report_custom_id) mock_profile_has_access_to_resource.assert_called_once_with( profile_type, profile_id, account_id, subaccount_id) mock_models.ReportCustom.get_by_id_or_error.assert_called_once_with(report_custom_id) def test_verify_report_access_missing_headers(): """Test verifying access to a custom report when headers are missing.""" with pytest.raises(HTTPException) as exception: _verify_report_access(None, None, 123) assert exception.value.detail == MISSING_PROFILE_HEADERS @patch('moneyhub.handlers.report_custom.profile_has_access_to_resource') @patch('moneyhub.handlers.report_custom.models') def test_verify_report_access_forbidden(mock_models, mock_profile_has_access_to_resource): """Test verifying access to a custom report when not having access.""" report_custom_id = 123 mock_profile_has_access_to_resource.return_value = False mock_models.ReportCustom.get_by_id_or_error.return_value = ReportCustomFactory.build( report_custom_id=report_custom_id, account_id=24601, ) with pytest.raises(HTTPException) as exception: _verify_report_access('MoneyhubProfile', 54321, report_custom_id) assert exception.value.detail == FORBIDDEN_REPORT_URL_ACCESS @pytest.mark.parametrize('dimension_column, dimension_row, revenue_type, filters', [ ('territory', 'product', 'distribution', { 'store_ids': [286, 1, 348], 'country_codes': ['US', 'GB'], 'transaction_type_ids': [1, 2, 3], 'imprint_ids': [1, 2, 3], 'activity_period_ids': [1, 2, 3], 'artist_ids': [2, 3, 4], 'product_ids': [3, 4, 5], 'subaccount_ids': [4, 5, 6], 'recording_ids': ['5', '6', '7'], 'track_unique_ids': [6, 7, 8], 'project_ids': [1, 2, 3], }), ('territory', 'statement_period', 'distribution', { 'store_ids': [286, 1, 348], 'country_codes': ['US', 'GB'], 'transaction_type_ids': [1, 2, 3], 'imprint_ids': [1, 2, 3], 'activity_period_ids': [], 'artist_ids': [], 'product_ids': [3, 4, 5], 'subaccount_ids': [4, 5, 6], 'recording_ids': ['5', '6', '7'], 'track_unique_ids': [6, 7, 8], 'project_ids': [1, 2, 3], }), ('territory', 'collection_society', 'neighbouring_rights', { 'store_ids': [286, 1, 348], 'country_codes': ['US', 'GB'], 'transaction_type_ids': [1, 2, 3], 'imprint_ids': [], 'activity_period_ids': [1, 2, 3], 'artist_ids': [2, 3, 4], 'product_ids': [], 'subaccount_ids': [4, 5, 6], 'recording_ids': ['5', '6', '7'], 'track_unique_ids': [6, 7, 8], 'project_ids': [1, 2, 3], }), ('financial_detail', 'service', 'distribution', { 'store_ids': [286, 1, 348], 'country_codes': ['US', 'GB'], 'transaction_type_ids': [], 'imprint_ids': [1, 2, 3], 'activity_period_ids': [1, 2, 3], 'artist_ids': [2, 3, 4], 'product_ids': [3, 4, 5], 'subaccount_ids': [], 'recording_ids': ['5', '6', '7'], 'track_unique_ids': [6, 7, 8], 'project_ids': [1, 2, 3], }), ('financial_detail', 'territory', 'distribution', { 'store_ids': [286, 1, 348], 'country_codes': [], 'transaction_type_ids': [1, 2, 3], 'imprint_ids': [1, 2, 3], 'activity_period_ids': [1, 2, 3], 'artist_ids': [2, 3, 4], 'product_ids': [3, 4, 5], 'subaccount_ids': [4, 5, 6], 'recording_ids': [], 'track_unique_ids': [6, 7, 8], 'project_ids': [1, 2, 3], }), ('financial_detail', 'transaction_type', 'neighbouring_rights', { 'store_ids': [], 'country_codes': ['US', 'GB'], 'transaction_type_ids': [1, 2, 3], 'imprint_ids': [1, 2, 3], 'activity_period_ids': [1, 2, 3], 'artist_ids': [2, 3, 4], 'product_ids': [3, 4, 5], 'subaccount_ids': [4, 5, 6], 'recording_ids': ['5', '6', '7'], 'track_unique_ids': [], 'project_ids': [1, 2, 3], }), ('statement_period', 'track', 'distribution', { 'store_ids': [286, 1, 348], 'country_codes': ['Denmark', 'GB'], 'transaction_type_ids': [1, 2, 3], 'imprint_ids': [1, 2, 3], 'activity_period_ids': [1, 2, 3], 'artist_ids': [2, 3, 4], 'product_ids': [3, 4, 5], 'subaccount_ids': [4, 5, 6], 'recording_ids': ['5', '6', '10'], 'track_unique_ids': [6, 7, 8], 'project_ids': [1, 2, 3], }), ('statement_period', 'recording', 'distribution', { 'store_ids': [286, 1, 348], 'country_codes': ['US', 'GB'], 'transaction_type_ids': [1, 2, 3], 'imprint_ids': [1, 2, 3], 'activity_period_ids': [5, 6, 7], 'artist_ids': [2, 3, 4], 'product_ids': [3, 4, 5], 'subaccount_ids': [4, 5, 6], 'recording_ids': ['5', '6', '7'], 'track_unique_ids': [8, 7, 5], 'project_ids': [1, 2, 3], }), ('statement_period', 'product_artist', 'neighbouring_rights', { 'store_ids': [286, 1, 348], 'country_codes': ['US', 'GB'], 'transaction_type_ids': [1, 2, 3], 'imprint_ids': [3, 2, 1], 'activity_period_ids': [1, 2, 3], 'artist_ids': [2, 3, 4], 'product_ids': [3, 4, 5], 'subaccount_ids': [11, 12, 13], 'recording_ids': ['5', '6', '7'], 'track_unique_ids': [6, 7, 8], 'project_ids': [1, 2, 3], }), ]) @patch('moneyhub.handlers.report_custom.logic') def test_create_report( mock_logic, dimension_column, dimension_row, revenue_type, filters, fixture_client ): """Test creating a report.""" account_id = 123 contract_id = 11 subaccount_id = 456 statement_period_ids = [456, 789] correlation_id = 'abc' orchard_identity_id = 'me' mock_response = ReportCustomFactory.build( account_id=account_id, contract_id=11, subaccount_id=subaccount_id, statement_period_ids='456,789', revenue_type=revenue_type, dimension_column=dimension_column, dimension_row=dimension_row, filters=ReportCustomFiltersSchema(**filters), number_format=NumberFormat.US, file_type=ReportCustomFileType.CSV, report_custom_status=ReportCustomStatus.IN_PROGRESS, file_location=None, ) mock_logic.create_report.return_value = mock_response endpoint_url = f'/reports/custom/account/{account_id}' payload = ReportCustomCreateSchema( contract_id=contract_id, subaccount_id=subaccount_id, statement_period_ids=statement_period_ids, revenue_type=RevenueType(revenue_type), dimension_column=ReportCustomColumnDimension(dimension_column), dimension_row=ReportCustomRowDimension(dimension_row), filters=ReportCustomFiltersSchema(**filters), ) result = fixture_client.post( endpoint_url, headers={CORRELATION_ID_HEADER: correlation_id}, json={ 'contract_id': contract_id, 'subaccount_id' : subaccount_id, 'statement_period_ids': statement_period_ids, 'revenue_type': revenue_type, 'dimension_column': dimension_column, 'dimension_row': dimension_row, 'filters': filters, }) result_body = result.json() assert result.status_code == 200 del result_body['created_at'] assert result_body == { 'report_custom_id': 1, 'account_id': account_id, 'contract_id': 11, 'subaccount_id': subaccount_id, 'statement_period_ids': statement_period_ids, 'revenue_type': revenue_type, 'revenue_display_type': 'net', 'dimension_column': dimension_column, 'dimension_row': dimension_row, 'filters': filters, 'number_format': NumberFormat.US, 'file_type': ReportCustomFileType.CSV, 'report_custom_status': ReportCustomStatus.IN_PROGRESS, 'file_location': None, 'created_by': 'me' } mock_logic.create_report.assert_called_with( account_id, payload, orchard_identity_id, correlation_id) @patch('moneyhub.handlers.report_custom.logic') def test_create_report_no_contract_id(mock_logic, fixture_client): """Test creating a report without a contract ID.""" account_id = 123 statement_period_ids = [456, 789] revenue_type = RevenueType.DISTRIBUTION dimension_column = ReportCustomColumnDimension.TERRITORY dimension_row = ReportCustomRowDimension.PRODUCT number_format = NumberFormat.US file_type = ReportCustomFileType.XLS correlation_id = 'abc' orchard_identity_id = 'me' mock_response = ReportCustomFactory.build( account_id=account_id, contract_id=None, statement_period_ids='456,789', revenue_type=revenue_type, dimension_column=dimension_column, dimension_row=dimension_row, report_custom_status=ReportCustomStatus.IN_PROGRESS, number_format=number_format, file_type=file_type, file_location=None, ) payload = ReportCustomCreateSchema( statement_period_ids=statement_period_ids, revenue_type=RevenueType(revenue_type), dimension_column=ReportCustomColumnDimension(dimension_column), dimension_row=ReportCustomRowDimension(dimension_row), number_format=number_format, file_type=file_type ) mock_logic.create_report.return_value = mock_response endpoint_url = f'/reports/custom/account/{account_id}' result = fixture_client.post( endpoint_url, headers={CORRELATION_ID_HEADER: correlation_id}, json=payload.model_dump() ) assert result.status_code == 200 result_body = result.json() del result_body['created_at'] assert result_body == { 'report_custom_id': 1, 'account_id': account_id, 'contract_id': None, 'subaccount_id': None, 'statement_period_ids': statement_period_ids, 'revenue_type': revenue_type, 'revenue_display_type': 'net', 'dimension_column': dimension_column, 'dimension_row': dimension_row, 'filters': None, 'number_format': number_format, 'file_type': file_type, 'report_custom_status': ReportCustomStatus.IN_PROGRESS, 'file_location': None, 'created_by': 'me' } mock_logic.create_report.assert_called_with( account_id, payload, orchard_identity_id, correlation_id) @patch('moneyhub.handlers.report_custom._verify_report_access') @patch('moneyhub.handlers.report_custom.logic') def test_get_report(mock_logic, mock_verify_report_access, fixture_client): """Test fetching a single report.""" report_custom_id = 42 mock_response = ReportCustomFactory.build( report_custom_id=42, contract_id=None, created_at='2022-07-26T17:23:44Z' ) mock_logic.get_custom_report.return_value = mock_response endpoint_url = f'/reports/custom/{report_custom_id}' result = fixture_client.get(endpoint_url) assert result.status_code == 200 result_body = result.json() assert result_body == { 'report_custom_id': report_custom_id, 'account_id': 24601, 'contract_id': None, 'subaccount_id': None, 'statement_period_ids': [1, 2, 3], 'revenue_type': 'distribution', 'revenue_display_type': 'net', 'dimension_column': ReportCustomColumnDimension.TERRITORY, 'dimension_row': ReportCustomRowDimension.PRODUCT, 'filters': None, 'number_format': NumberFormat.EU, 'file_type': ReportCustomFileType.CSV, 'report_custom_status': ReportCustomStatus.IN_PROGRESS, 'file_location': None, 'created_by': 'me', 'created_at': '2022-07-26T17:23:44Z' } mock_logic.get_custom_report.assert_called_with(report_custom_id) @patch('moneyhub.handlers.report_custom.logic') def test_get_reports(mock_logic, fixture_client): """Test fetching reports.""" account_id = 123 statement_period_ids = [456, 789] revenue_type = RevenueType.DISTRIBUTION dimension_column = ReportCustomColumnDimension.TERRITORY dimension_row = ReportCustomRowDimension.PRODUCT created_at = '2022-07-20T16:52:45.309486Z' mock_response = [ReportCustomFactory.build( account_id=account_id, contract_id=None, statement_period_ids='456,789', revenue_type=revenue_type, dimension_column=dimension_column, dimension_row=dimension_row, report_custom_status=ReportCustomStatus.IN_PROGRESS, file_location=None, created_at=created_at )] mock_logic.get_custom_reports.return_value = mock_response endpoint_url = f'/reports/custom/account/{account_id}' result = fixture_client.get(endpoint_url) assert result.status_code == 200 result_body = result.json() assert result_body == [{ 'report_custom_id': 1, 'account_id': account_id, 'contract_id': None, 'subaccount_id': None, 'statement_period_ids': statement_period_ids, 'revenue_type': revenue_type, 'revenue_display_type': 'net', 'dimension_column': dimension_column, 'dimension_row': dimension_row, 'filters': None, 'number_format': NumberFormat.EU, 'file_type': ReportCustomFileType.CSV, 'report_custom_status': ReportCustomStatus.IN_PROGRESS, 'file_location': None, 'created_by': 'me', 'created_at': created_at }] mock_logic.get_custom_reports.assert_called_with(account_id, None, None, False) @patch('moneyhub.handlers.report_custom.logic') def test_get_custom_reports_by_account_id_and_contract_id(mock_logic, fixture_client): """Test fetching reports by account_id and contract_id.""" account_id = 123 contract_id = 77 statement_period_ids = [456, 789] revenue_type = RevenueType.DISTRIBUTION dimension_column = ReportCustomColumnDimension.TERRITORY dimension_row = ReportCustomRowDimension.PRODUCT created_at = '2022-07-20T16:52:45.309486Z' mock_response = [ReportCustomFactory.build( account_id=account_id, contract_id=contract_id, statement_period_ids='456,789', revenue_type=revenue_type, dimension_column=dimension_column, dimension_row=dimension_row, report_custom_status=ReportCustomStatus.IN_PROGRESS, file_location=None, created_at=created_at )] mock_logic.get_custom_reports.return_value = mock_response endpoint_url = f'/reports/custom/account/{account_id}?contract_id={contract_id}' result = fixture_client.get(endpoint_url) assert result.status_code == 200 result_body = result.json() assert result_body == [{ 'report_custom_id': 1, 'account_id': account_id, 'contract_id': contract_id, 'subaccount_id': None, 'statement_period_ids': statement_period_ids, 'revenue_type': revenue_type, 'revenue_display_type': 'net', 'dimension_column': dimension_column, 'dimension_row': dimension_row, 'filters': None, 'number_format': NumberFormat.EU, 'file_type': ReportCustomFileType.CSV, 'report_custom_status': ReportCustomStatus.IN_PROGRESS, 'file_location': None, 'created_by': 'me', 'created_at': created_at, }] mock_logic.get_custom_reports.assert_called_with(account_id, contract_id, None, False) @patch('moneyhub.handlers.report_custom.logic') def test_get_custom_reports_with_subaccount_filter(mock_logic, fixture_client): """Test fetching reports filtered by subaccount_id.""" account_id = 123 contract_id = 77 subaccount_id = 456 statement_period_ids = [456, 789] revenue_type = RevenueType.DISTRIBUTION dimension_column = ReportCustomColumnDimension.TERRITORY dimension_row = ReportCustomRowDimension.PRODUCT created_at = '2022-07-26T17:23:44Z' mock_response = [ReportCustomFactory.build( account_id=account_id, contract_id=contract_id, subaccount_id=subaccount_id, statement_period_ids='456,789', revenue_type=revenue_type, dimension_column=dimension_column, dimension_row=dimension_row, report_custom_status=ReportCustomStatus.IN_PROGRESS, file_location=None, created_at=created_at, )] mock_logic.get_custom_reports.return_value = mock_response endpoint_url = f'/reports/custom/account/{account_id}?contract_id={contract_id}&subaccount_id={subaccount_id}&is_subaccount=true' # noqa: E501 result = fixture_client.get(endpoint_url) assert result.status_code == 200 assert result.json() == [{ 'report_custom_id': 1, 'account_id': account_id, 'contract_id': contract_id, 'subaccount_id': subaccount_id, 'statement_period_ids': statement_period_ids, 'revenue_type': revenue_type, 'revenue_display_type': 'net', 'dimension_column': dimension_column, 'dimension_row': dimension_row, 'filters': None, 'number_format': NumberFormat.EU, 'file_type': ReportCustomFileType.CSV, 'report_custom_status': ReportCustomStatus.IN_PROGRESS, 'file_location': None, 'created_by': 'me', 'created_at': created_at, }] mock_logic.get_custom_reports.assert_called_with(account_id, contract_id, subaccount_id, True) @patch('moneyhub.handlers.report_custom._verify_report_access') @patch('moneyhub.handlers.report_custom.logic') def test_get_report_presigned_url( mock_logic, mock_verify_report_access, fixture_client, request_engine ): """Test getting report's presigned url to download.""" profile_id = '98787' profile_type = 'MoneyhubProfile' report_custom_id = 1 mock_url = 's3://report-files/2021/report_1.pdf' mock_logic.get_report_presigned_url.return_value = mock_url endpoint_url = f'/reports/custom/{report_custom_id}/download' result = fixture_client.get( endpoint_url, headers={ 'orchard-profile-id': profile_id, 'orchard-profile-type': profile_type } ) assert result.status_code == 200 assert result.json() == {'url': 's3://report-files/2021/report_1.pdf'} mock_logic.get_report_presigned_url.assert_called_with(report_custom_id) mock_verify_report_access.assert_called_with(profile_type, profile_id, report_custom_id) @patch('moneyhub.handlers.report_custom._verify_report_access') @patch('moneyhub.handlers.report_custom.logic') def test_update_report(mock_logic, mock_verify_report_access, fixture_client): """Test updating a report.""" profile_id = '98787' profile_type = 'MoneyhubProfile' report_custom_id = 43 file_location = 's3://test.csv' report_custom_status = ReportCustomStatus.COMPLETE mock_response = ReportCustomFactory.build( report_custom_id=report_custom_id, report_custom_status=report_custom_status, file_location=file_location ) mock_logic.update_custom_report.return_value = mock_response endpoint_url = f'/reports/custom/{report_custom_id}' payload = { 'file_location': file_location, 'report_custom_status': report_custom_status, } result = fixture_client.put( endpoint_url, headers={ 'orchard-profile-id': profile_id, 'orchard-profile-type': profile_type }, json=payload) assert result.status_code == 200 mock_logic.update_custom_report.assert_called_with( report_custom_id, payload) @patch('moneyhub.handlers.report_custom._verify_report_access') @patch('moneyhub.handlers.report_custom.logic') def test_delete_report(mock_logic, mock_verify_report_access, fixture_client): """Test deleting a report.""" report_custom_id = 42 file_location = 's3://test.csv' report_custom_status = ReportCustomStatus.IN_PROGRESS mock_response = ReportCustomFactory.build( report_custom_id=report_custom_id, report_custom_status=report_custom_status, file_location=file_location ) mock_logic.delete_custom_report.return_value = mock_response endpoint_url = f'/reports/custom/{report_custom_id}' result = fixture_client.delete(endpoint_url) assert result.status_code == 204 assert result.text == '' mock_logic.delete_custom_report.assert_called_with(report_custom_id) @patch('moneyhub.handlers.report_custom.logic') def test_regenerate_custom_reports(mock_logic, fixture_client): """Test regenerating custom reports.""" statement_period_id = 123 correlation_id = 'abcd' mock_logic.regenerate_custom_reports.return_value = [ ReportCustomFactory.build( account_id=1, report_custom_id=1, report_custom_status='in_progress', statement_period_ids=str(statement_period_id), created_at='2022-11-17T22:02:42' ), ] url = f'/reports/custom/statement-period/{statement_period_id}/regenerate' result = fixture_client.post(url, headers={CORRELATION_ID_HEADER: correlation_id}) assert result.status_code == 200 assert result.json() == [ { 'account_id': 1, 'contract_id': 11, 'subaccount_id': None, 'file_location': None, 'report_custom_id': 1, 'report_custom_status': 'in_progress', 'statement_period_ids': [statement_period_id], 'created_at': '2022-11-17T22:02:42', 'created_by': 'me', 'revenue_type': 'distribution', 'revenue_display_type': 'net', 'dimension_column': 'territory', 'dimension_row': 'product', 'filters': None, 'number_format': NumberFormat.EU, 'file_type': ReportCustomFileType.CSV, } ] mock_logic.regenerate_custom_reports.assert_called_with( statement_period_id, correlation_id)