From ae7cbb9beefb4648d9c2f972c7dacc019cae71ab Mon Sep 17 00:00:00 2001 From: Alex Zorkin Date: Tue, 24 Dec 2024 13:09:54 -0800 Subject: [PATCH 1/2] feat: updated syncing logic for supplemental report creation --- .../lcfs/services/rabbitmq/report_consumer.py | 47 ++++++++++++++----- .../web/api/compliance_report/services.py | 19 +++++--- 2 files changed, 46 insertions(+), 20 deletions(-) diff --git a/backend/lcfs/services/rabbitmq/report_consumer.py b/backend/lcfs/services/rabbitmq/report_consumer.py index 551f3781b..11f58873a 100644 --- a/backend/lcfs/services/rabbitmq/report_consumer.py +++ b/backend/lcfs/services/rabbitmq/report_consumer.py @@ -96,6 +96,7 @@ async def process_message(self, body: bytes): action=action, compliance_period=message.get("compliance_period"), compliance_units=message.get("credits"), + root_report_id=message["root_report_id"], legacy_id=message["tfrs_id"], nickname=message.get("nickname"), org_id=org_id, @@ -127,6 +128,7 @@ async def handle_message( action: str, compliance_period: str, compliance_units: Optional[int], + root_report_id: int, legacy_id: int, nickname: Optional[str], org_id: int, @@ -157,15 +159,18 @@ async def handle_message( user = await UserRepository(db=session).get_user_by_id(user_id) if not user: - logger.error(f"Cannot parse Report {legacy_id} from TFRS, no user with ID {user_id}") + logger.error( + f"Cannot parse Report {legacy_id} from TFRS, no user with ID {user_id}" + ) if action == "Created": await self._handle_created( org_id, + root_report_id, legacy_id, compliance_period, - nickname, user, + compliance_report_repo, compliance_report_service, ) elif action == "Submitted": @@ -190,25 +195,41 @@ async def handle_message( async def _handle_created( self, org_id: int, + root_report_id: int, legacy_id: int, compliance_period: str, - nickname: str, user: UserProfile, + compliance_report_repo: ComplianceReportRepository, compliance_report_service: ComplianceReportServices, ): """ Handle the 'Created' action by creating a new compliance report draft. """ - lcfs_report = ComplianceReportCreateSchema( - legacy_id=legacy_id, - compliance_period=compliance_period, - organization_id=org_id, - nickname=nickname, - status=ComplianceReportStatusEnum.Draft.value, - ) - await compliance_report_service.create_compliance_report( - org_id, lcfs_report, user - ) + if root_report_id == legacy_id: # this is a new initial report + lcfs_report = ComplianceReportCreateSchema( + legacy_id=legacy_id, + compliance_period=compliance_period, + organization_id=org_id, + nickname="Original Report", + status=ComplianceReportStatusEnum.Draft.value, + ) + await compliance_report_service.create_compliance_report( + org_id, lcfs_report, user + ) + else: + # Process a new supplemental report + root_report = ( + await compliance_report_repo.get_compliance_report_by_legacy_id( + root_report_id + ) + ) + if not root_report: + raise ServiceException( + f"No original compliance report found for legacy ID {root_report_id}" + ) + await compliance_report_service.create_supplemental_report( + root_report_id, user, legacy_id + ) async def _handle_approved( self, diff --git a/backend/lcfs/web/api/compliance_report/services.py b/backend/lcfs/web/api/compliance_report/services.py index dac78edd9..880a94be7 100644 --- a/backend/lcfs/web/api/compliance_report/services.py +++ b/backend/lcfs/web/api/compliance_report/services.py @@ -80,15 +80,16 @@ async def create_compliance_report( @service_handler async def create_supplemental_report( - self, report_id: int + self, report_id: int, user: UserProfile = None, legacy_id: int = None ) -> ComplianceReportBaseSchema: """ Creates a new supplemental compliance report. The report_id can be any report in the series (original or supplemental). Supplemental reports are only allowed if the status of the current report is 'Assessed'. """ - - user: UserProfile = self.request.user + # check if we're passing a specifc user otherwise use request user + if not user: + user = self.request.user # Fetch the current report using the provided report_id current_report = await self.repo.get_compliance_report_by_id( @@ -103,11 +104,14 @@ async def create_supplemental_report( "You do not have permission to create a supplemental report for this organization." ) + # TODO this logic to be re-instated once TFRS is shutdown + # TFRS allows supplementals on previously un-accepted reports + # so we have to support this until LCFS and TFRS are no longer synced # Validate that the status of the current report is 'Assessed' - if current_report.current_status.status != ComplianceReportStatusEnum.Assessed: - raise ServiceException( - "A supplemental report can only be created if the current report's status is 'Assessed'." - ) + # if current_report.current_status.status != ComplianceReportStatusEnum.Assessed: + # raise ServiceException( + # "A supplemental report can only be created if the current report's status is 'Assessed'." + # ) # Get the group_uuid from the current report group_uuid = current_report.compliance_report_group_uuid @@ -127,6 +131,7 @@ async def create_supplemental_report( # Create the new supplemental compliance report new_report = ComplianceReport( compliance_period_id=current_report.compliance_period_id, + legacy_id=legacy_id, organization_id=current_report.organization_id, current_status_id=draft_status.compliance_report_status_id, reporting_frequency=current_report.reporting_frequency, From d23ce4bd03365ac730ae5eb0a48b85c74f932ded Mon Sep 17 00:00:00 2001 From: Alex Zorkin Date: Tue, 24 Dec 2024 13:53:14 -0800 Subject: [PATCH 2/2] fix: cleanup and test updates --- .../lcfs/services/rabbitmq/report_consumer.py | 4 +- .../services/rabbitmq/test_report_consumer.py | 82 +++++++++++++++---- 2 files changed, 69 insertions(+), 17 deletions(-) diff --git a/backend/lcfs/services/rabbitmq/report_consumer.py b/backend/lcfs/services/rabbitmq/report_consumer.py index 11f58873a..630a2ddeb 100644 --- a/backend/lcfs/services/rabbitmq/report_consumer.py +++ b/backend/lcfs/services/rabbitmq/report_consumer.py @@ -70,9 +70,9 @@ async def process_message(self, body: bytes): Expected message structure: { "tfrs_id": int, + "root_report_id": int, "organization_id": int, "compliance_period": str, - "nickname": str, "action": "Created"|"Submitted"|"Approved", "credits": int (optional), "user_id": int @@ -98,7 +98,6 @@ async def process_message(self, body: bytes): compliance_units=message.get("credits"), root_report_id=message["root_report_id"], legacy_id=message["tfrs_id"], - nickname=message.get("nickname"), org_id=org_id, user_id=message["user_id"], ) @@ -130,7 +129,6 @@ async def handle_message( compliance_units: Optional[int], root_report_id: int, legacy_id: int, - nickname: Optional[str], org_id: int, user_id: int, ): diff --git a/backend/lcfs/tests/services/rabbitmq/test_report_consumer.py b/backend/lcfs/tests/services/rabbitmq/test_report_consumer.py index 838c9fe0c..3635da484 100644 --- a/backend/lcfs/tests/services/rabbitmq/test_report_consumer.py +++ b/backend/lcfs/tests/services/rabbitmq/test_report_consumer.py @@ -3,14 +3,12 @@ from unittest.mock import AsyncMock, patch, MagicMock import pytest -from pandas.io.formats.format import return_docstring from lcfs.db.models.transaction.Transaction import TransactionActionEnum, Transaction -from lcfs.services.rabbitmq.report_consumer import ( - ReportConsumer, -) -from lcfs.tests.fuel_export.conftest import mock_compliance_report_repo +from lcfs.services.rabbitmq.report_consumer import ReportConsumer from lcfs.web.api.compliance_report.schema import ComplianceReportCreateSchema +from lcfs.db.models.compliance.ComplianceReportStatus import ComplianceReportStatusEnum +from lcfs.db.models.compliance.ComplianceReport import SupplementalInitiatorType @pytest.fixture @@ -33,11 +31,11 @@ def mock_session(): mock_session = AsyncMock(spec=AsyncSession) - # `async with mock_session:` should work, so we define what happens on enter/exit + # `async with mock_session:` should work, so define behavior for enter/exit mock_session.__aenter__.return_value = mock_session mock_session.__aexit__.return_value = None - # Now mock the transaction context manager returned by `session.begin()` + # Mock the transaction context manager returned by `session.begin()` mock_transaction = AsyncMock() mock_transaction.__aenter__.return_value = mock_transaction mock_transaction.__aexit__.return_value = None @@ -130,27 +128,30 @@ def setup_patches(mock_redis, mock_session, mock_repositories): @pytest.mark.anyio -async def test_process_message_created(mock_app, setup_patches, mock_repositories): +async def test_process_message_created_new_initial_report( + mock_app, setup_patches, mock_repositories +): + """Test the 'Created' action when root_report_id == legacy_id, indicating a new initial report.""" consumer = ReportConsumer(mock_app) - # Prepare a sample message for "Created" action + # Prepare a sample message for "Created" action (new report) + # Note root_report_id == tfrs_id => new initial report message = { "tfrs_id": 123, + "root_report_id": 123, "organization_id": 1, "compliance_period": "2023", - "nickname": "Test Report", "action": "Created", "user_id": 42, } body = json.dumps(message).encode() - # Ensure correct mock setup mock_user = MagicMock() mock_repositories["user_repo"].get_user_by_id.return_value = mock_user await consumer.process_message(body) - # Assertions for "Created" action + # Assertions for "Created" action, new initial report mock_repositories[ "compliance_service" ].create_compliance_report.assert_called_once_with( @@ -159,13 +160,65 @@ async def test_process_message_created(mock_app, setup_patches, mock_repositorie legacy_id=123, compliance_period="2023", organization_id=1, - nickname="Test Report", + nickname="Original Report", status="Draft", ), mock_user, ) +@pytest.mark.anyio +async def test_process_message_created_supplemental_report( + mock_app, setup_patches, mock_repositories +): + """ + Test the 'Created' action when root_report_id != legacy_id, indicating a supplemental report. + """ + consumer = ReportConsumer(mock_app) + + # Prepare a sample message for "Created" action (supplemental) + message = { + "tfrs_id": 999, # This is the new supplemental's legacy ID + "root_report_id": 123, # The original (root) report ID + "organization_id": 1, + "compliance_period": "2023", + "action": "Created", + "user_id": 42, + } + body = json.dumps(message).encode() + + mock_user = MagicMock() + mock_repositories["user_repo"].get_user_by_id.return_value = mock_user + + # Mock root report so the repository call returns a valid object + mock_root_report = MagicMock() + mock_root_report.version = 2 + mock_root_report.compliance_report_group_uuid = "test-uuid" + mock_repositories[ + "compliance_report_repo" + ].get_compliance_report_by_legacy_id.return_value = mock_root_report + + await consumer.process_message(body) + + # The code should create a supplemental report using the root report's group UUID + # and increment the version by 1 + mock_repositories[ + "compliance_service" + ].create_supplemental_report.assert_called_once() + + called_args = mock_repositories[ + "compliance_service" + ].create_supplemental_report.call_args[0] + root_report_id_arg = called_args[0] + user_arg = called_args[1] + legacy_id_arg = called_args[2] + + assert root_report_id_arg == 123 + assert user_arg == mock_user + # Check that the new supplemental report schema was built correctly + assert legacy_id_arg == 999 + + @pytest.mark.anyio async def test_process_message_submitted(mock_app, setup_patches, mock_repositories): consumer = ReportConsumer(mock_app) @@ -173,9 +226,9 @@ async def test_process_message_submitted(mock_app, setup_patches, mock_repositor # Prepare a sample message for "Submitted" action message = { "tfrs_id": 123, + "root_report_id": 123, "organization_id": 1, "compliance_period": "2023", - "nickname": "Test Report", "action": "Submitted", "credits": 50, "user_id": 42, @@ -203,6 +256,7 @@ async def test_process_message_approved(mock_app, setup_patches, mock_repositori # Prepare a sample message for "Approved" action message = { "tfrs_id": 123, + "root_report_id": 123, "organization_id": 1, "action": "Approved", "user_id": 42,