From 696f68abdef69db7db29500227b5548cdbedbe0f Mon Sep 17 00:00:00 2001 From: shon-button Date: Thu, 19 Dec 2024 11:14:24 -0500 Subject: [PATCH 1/8] chore: add needs verification api --- .../auth/test_endpoint_permissions.py | 6 +- bc_obps/reporting/api/__init__.py | 4 +- bc_obps/reporting/api/compliance_data.py | 18 ---- bc_obps/reporting/api/report_verification.py | 25 +++-- .../service/report_verification_service.py | 30 ++++++ .../tests/api/test_compliance_data_api.py | 45 -------- .../tests/api/test_report_verification_api.py | 65 ++++++++++- .../test_report_verification_service.py | 101 +++++++++++++++++- 8 files changed, 217 insertions(+), 77 deletions(-) delete mode 100644 bc_obps/reporting/tests/api/test_compliance_data_api.py diff --git a/bc_obps/common/tests/endpoints/auth/test_endpoint_permissions.py b/bc_obps/common/tests/endpoints/auth/test_endpoint_permissions.py index b1f0e6aa5d..1eda33c72f 100644 --- a/bc_obps/common/tests/endpoints/auth/test_endpoint_permissions.py +++ b/bc_obps/common/tests/endpoints/auth/test_endpoint_permissions.py @@ -95,7 +95,7 @@ class TestEndpointPermissions(TestCase): { "method": "get", "endpoint_name": "get_report_verification_by_version_id", - "kwargs": {"version_id": mock_int}, + "kwargs": {"report_version_id": mock_int}, }, { "method": "get", @@ -139,7 +139,7 @@ class TestEndpointPermissions(TestCase): }, { "method": "get", - "endpoint_name": "get_attributable_emissions", + "endpoint_name": "get_report_needs_verification", "kwargs": {"report_version_id": mock_int}, }, {"method": "post", "endpoint_name": "create_facilities"}, @@ -170,7 +170,7 @@ class TestEndpointPermissions(TestCase): { "method": "post", "endpoint_name": "save_report_verification", - "kwargs": {"version_id": mock_int}, + "kwargs": {"report_version_id": mock_int}, }, {"method": "post", "endpoint_name": "save_report_contact", "kwargs": {"version_id": mock_int}}, { diff --git a/bc_obps/reporting/api/__init__.py b/bc_obps/reporting/api/__init__.py index c2e51964fd..fed8b6eabe 100644 --- a/bc_obps/reporting/api/__init__.py +++ b/bc_obps/reporting/api/__init__.py @@ -20,8 +20,8 @@ from .report_non_attributable_emissions import save_report from .report_activity import save_report_activity_data, load_report_activity_data from .report_facilities import get_report_facility_list_by_version_id -from .report_verification import get_report_verification_by_version_id, save_report_verification +from .report_verification import get_report_verification_by_version_id, get_report_needs_verification, save_report_verification from .report_attachments import save_report_attachments, get_report_attachments from .report_emission_allocations import get_emission_allocations, save_emission_allocation_data -from .compliance_data import get_compliance_summary_data, get_attributable_emissions +from .compliance_data import get_compliance_summary_data from .submit import submit_report_version diff --git a/bc_obps/reporting/api/compliance_data.py b/bc_obps/reporting/api/compliance_data.py index d6105cda3b..5aa66dfd24 100644 --- a/bc_obps/reporting/api/compliance_data.py +++ b/bc_obps/reporting/api/compliance_data.py @@ -26,21 +26,3 @@ def get_compliance_summary_data( compliance_data = ComplianceService.get_calculated_compliance_data(report_version_id) return 200, compliance_data - - -@router.get( - "report-version/{report_version_id}/attributable-emissions", - response={200: Decimal, custom_codes_4xx: Message}, - tags=EMISSIONS_REPORT_TAGS, - description="""Retrieves the total attributable emissions for a given report version.""", - exclude_none=True, - auth=authorize("approved_industry_user"), -) -@handle_http_errors() -def get_attributable_emissions(request: HttpRequest, report_version_id: int) -> Tuple[Literal[200], Decimal | int]: - """ - Endpoint to retrieve the total emissions attributable for reporting. - """ - attributable_emissions = ComplianceService.get_emissions_attributable_for_reporting(report_version_id) - - return 200, attributable_emissions diff --git a/bc_obps/reporting/api/report_verification.py b/bc_obps/reporting/api/report_verification.py index 21980ee3b5..bd1224341b 100644 --- a/bc_obps/reporting/api/report_verification.py +++ b/bc_obps/reporting/api/report_verification.py @@ -12,7 +12,7 @@ @router.get( - "/report-version/{version_id}/report-verification", + "/report-version/{report_version_id}/report-verification", response={200: ReportVerificationOut, custom_codes_4xx: Message}, tags=EMISSIONS_REPORT_TAGS, description="""Fetches the Verification data associated with the given report version ID.""", @@ -20,14 +20,27 @@ ) @handle_http_errors() def get_report_verification_by_version_id( - request: HttpRequest, version_id: int + request: HttpRequest, report_version_id: int ) -> tuple[Literal[200], ReportVerification]: - report_verification = ReportVerificationService.get_report_verification_by_version_id(version_id) + report_verification = ReportVerificationService.get_report_verification_by_version_id(report_version_id) return 200, report_verification +@router.get( + "/report-version/{report_version_id}/report-needs-verification", + response={200: bool, custom_codes_4xx: Message}, + tags=EMISSIONS_REPORT_TAGS, + description="""Checks if a report needs verification based on its purpose and attributable emissions.""", + auth=authorize("approved_industry_user"), +) +@handle_http_errors() +def get_report_needs_verification( + request: HttpRequest, report_version_id: int) -> tuple[Literal[200], bool]: + return 200, ReportVerificationService.get_report_needs_verification(report_version_id) + + @router.post( - "/report-version/{version_id}/report-verification", + "/report-version/{report_version_id}/report-verification", response={200: ReportVerificationOut, custom_codes_4xx: Message}, tags=EMISSIONS_REPORT_TAGS, description="""Creates or updates the Verification data for the given report version ID.""", @@ -35,7 +48,7 @@ def get_report_verification_by_version_id( ) @handle_http_errors() def save_report_verification( - request: HttpRequest, version_id: int, payload: ReportVerificationIn + request: HttpRequest, report_version_id: int, payload: ReportVerificationIn ) -> tuple[Literal[200], ReportVerification]: - report_verification = ReportVerificationService.save_report_verification(version_id, payload) + report_verification = ReportVerificationService.save_report_verification(report_version_id, payload) return 200, report_verification diff --git a/bc_obps/reporting/service/report_verification_service.py b/bc_obps/reporting/service/report_verification_service.py index 298eb65576..9c2e39eebd 100644 --- a/bc_obps/reporting/service/report_verification_service.py +++ b/bc_obps/reporting/service/report_verification_service.py @@ -1,10 +1,15 @@ + +from decimal import Decimal from django.db import transaction from reporting.models.report_verification import ReportVerification from reporting.models import ReportVersion from reporting.schema.report_verification import ReportVerificationIn +from reporting.service.report_additional_data import ReportAdditionalDataService +from reporting.service.compliance_service import ComplianceService class ReportVerificationService: + @staticmethod def get_report_verification_by_version_id( report_version_id: int, @@ -53,3 +58,28 @@ def save_report_verification(version_id: int, data: ReportVerificationIn) -> Rep ) return report_verification + + @staticmethod + def get_report_needs_verification(version_id: int) -> bool: + """ + Determines if a report needs verification based on its purpose + and attributable emissions. + """ + REGULATED_OPERATION_PURPOSES = { + "OBPS_Regulated_Operation", + "Opt-in", + "New_Entrants", + } + ATTRIBUTABLE_EMISSION_THRESHOLD = Decimal("25000000") + registration_purpose = ReportAdditionalDataService.get_registration_purpose_by_version_id(version_id) + + # Registration Purpose: Users must complete the verification page if the registration purpose is in REGULATED_OPERATION_PURPOSES + if registration_purpose in REGULATED_OPERATION_PURPOSES: + return True + + # Emission threshold: Users must complete the verification page if the registration purpose is Reporting Operation, and total TCo₂e >+ 25,000 + if registration_purpose == "Reporting_Operation": + attributable_emissions = ComplianceService.get_emissions_attributable_for_reporting(version_id) + return attributable_emissions >= ATTRIBUTABLE_EMISSION_THRESHOLD + + return False diff --git a/bc_obps/reporting/tests/api/test_compliance_data_api.py b/bc_obps/reporting/tests/api/test_compliance_data_api.py deleted file mode 100644 index 14f3ef6195..0000000000 --- a/bc_obps/reporting/tests/api/test_compliance_data_api.py +++ /dev/null @@ -1,45 +0,0 @@ -from model_bakery import baker -from unittest.mock import patch, MagicMock -from registration.tests.utils.helpers import CommonTestSetup, TestUtils -from registration.utils import custom_reverse_lazy - - -class TestComplianceDataApi(CommonTestSetup): - def setup_method(self): - self.report_version = baker.make_recipe('reporting.tests.utils.report_version') - self.mock_emission = 54321.1234 - super().setup_method() - TestUtils.authorize_current_user_as_operator_user(self, operator=self.report_version.report.operator) - - """Tests for the get_attributable_emissions endpoint.""" - - @patch( - "reporting.service.compliance_service.ComplianceService.get_emissions_attributable_for_reporting", autospec=True - ) - def test_returns_attributable_emissions( - self, - mock_get_attributable_emissions: MagicMock, - ): - # Arrange: Mock report version and report verification data - mock_get_attributable_emissions.return_value = self.mock_emission - - # Act: Authorize user and perform GET request - response = TestUtils.mock_get_with_auth_role( - self, - "industry_user", - custom_reverse_lazy( - "get_attributable_emissions", - kwargs={"report_version_id": self.report_version.id}, - ), - ) - - # Assert: Verify the response status - assert response.status_code == 200 - - # Assert: Verify the service was called with the correct version ID - mock_get_attributable_emissions.assert_called_once_with(self.report_version.id) - - # Assert: Validate the response data - response_json = response.json() - - assert float(response_json) == self.mock_emission diff --git a/bc_obps/reporting/tests/api/test_report_verification_api.py b/bc_obps/reporting/tests/api/test_report_verification_api.py index bd0350c422..51a3accc66 100644 --- a/bc_obps/reporting/tests/api/test_report_verification_api.py +++ b/bc_obps/reporting/tests/api/test_report_verification_api.py @@ -32,7 +32,7 @@ def test_returns_verification_data_for_report_version_id( "industry_user", custom_reverse_lazy( "get_report_verification_by_version_id", - kwargs={"version_id": self.report_version.id}, + kwargs={"report_version_id": self.report_version.id}, ), ) @@ -54,6 +54,67 @@ def test_returns_verification_data_for_report_version_id( assert response_json["other_facility_name"] == self.report_verification.other_facility_name assert response_json["other_facility_coordinates"] == self.report_verification.other_facility_coordinates + + """Tests for the get_report_needs_verification endpoint.""" + + @patch( + "reporting.service.report_verification_service.ReportVerificationService.get_report_needs_verification" + ) + def test_returns_verification_needed_for_report_version_id( + self, mock_get_report_needs_verification: MagicMock + ): + # Arrange: Mock the service to return True + mock_get_report_needs_verification.return_value = True + + # Act: Authorize user and perform GET request + response = TestUtils.mock_get_with_auth_role( + self, + "industry_user", + custom_reverse_lazy( + "get_report_needs_verification", + kwargs={"report_version_id": self.report_version.id}, + ), + ) + + # Assert: Verify the response status + assert response.status_code == 200 + + # Assert: Verify the service was called with the correct version ID + mock_get_report_needs_verification.assert_called_once_with(self.report_version.id) + + # Assert: Validate the response data + response_json = response.json() + assert response_json is True + + @patch( + "reporting.service.report_verification_service.ReportVerificationService.get_report_needs_verification" + ) + def test_returns_verification_not_needed_for_report_version_id( + self, mock_get_report_needs_verification: MagicMock + ): + # Arrange: Mock the service to return False + mock_get_report_needs_verification.return_value = False + + # Act: Authorize user and perform GET request + response = TestUtils.mock_get_with_auth_role( + self, + "industry_user", + custom_reverse_lazy( + "get_report_needs_verification", + kwargs={"report_version_id": self.report_version.id}, + ), + ) + + # Assert: Verify the response status + assert response.status_code == 200 + + # Assert: Verify the service was called with the correct version ID + mock_get_report_needs_verification.assert_called_once_with(self.report_version.id) + + # Assert: Validate the response data + response_json = response.json() + assert response_json is False + """Tests for the save_report_verification endpoint.""" @patch("reporting.service.report_verification_service.ReportVerificationService.save_report_verification") @@ -95,7 +156,7 @@ def test_returns_data_as_provided_by_the_service( payload.dict(), custom_reverse_lazy( "save_report_verification", - kwargs={"version_id": self.report_version.id}, + kwargs={"report_version_id": self.report_version.id}, ), ) diff --git a/bc_obps/reporting/tests/service/test_report_verification_service.py b/bc_obps/reporting/tests/service/test_report_verification_service.py index 65a2f8384d..ff63726fb7 100644 --- a/bc_obps/reporting/tests/service/test_report_verification_service.py +++ b/bc_obps/reporting/tests/service/test_report_verification_service.py @@ -1,10 +1,18 @@ +from decimal import Decimal from django.test import TestCase from model_bakery.baker import make_recipe from reporting.service.report_verification_service import ReportVerificationService from reporting.schema.report_verification import ReportVerificationIn - +from unittest.mock import patch, MagicMock class TestReportVerificationService(TestCase): + REGULATED_OPERATION_PURPOSES = { + "OBPS_Regulated_Operation", + "Opt-in", + "New_Entrants" + } + + ATTRIBUTABLE_EMISSION_THRESHOLD = Decimal('25000000') def setUp(self): # Arrange: Create a report version self.report_version = make_recipe('reporting.tests.utils.report_version') @@ -43,6 +51,97 @@ def test_get_report_verification_by_version_id_returns_correct_instance(self): retrieved_verification.other_facility_coordinates, self.report_verification.other_facility_coordinates ) + @patch("reporting.service.compliance_service.ComplianceService.get_emissions_attributable_for_reporting") + @patch("reporting.service.report_additional_data.ReportAdditionalDataService.get_registration_purpose_by_version_id") + def test_get_report_needs_verification_returns_true_for_regulated_purpose( + self, mock_get_registration_purpose, mock_get_emissions + ): + """ + Test that the service returns true for reports with regulated_purpose is a REGULATED_OPERATION_PURPOSES. + """ + + # Arrange: Mock the registration purpose to simulate a regulated operation + # The purpose is one of REGULATED_OPERATION_PURPOSES + mock_get_registration_purpose.return_value = "OBPS_Regulated_Operation" + + # Act: Call the method to determine if the report needs verification + result = ReportVerificationService.get_report_needs_verification(self.report_version.id) + + # Assert: Verify that the method correctly identifies the need for verification + self.assertTrue(result) + # Ensure the registration purpose method was called with the correct version ID + mock_get_registration_purpose.assert_called_once_with(self.report_version.id) + + + @patch("reporting.service.compliance_service.ComplianceService.get_emissions_attributable_for_reporting") + @patch("reporting.service.report_additional_data.ReportAdditionalDataService.get_registration_purpose_by_version_id") + def test_get_report_needs_verification_returns_false_for_non_regulated_purpose( + self, mock_get_registration_purpose, mock_get_emissions + ): + """ + Test that the service returns false for reports with regulated_purpose NOT a REGULATED_OPERATION_PURPOSES. + """ + + # Arrange: Simulate a purpose that is not in REGULATED_OPERATION_PURPOSES + mock_get_registration_purpose.return_value = "Electricity Import Operation" + + # Act: Call the method to determine if the report needs verification + result = ReportVerificationService.get_report_needs_verification(self.report_version.id) + + # Assert: The method should correctly determine no verification is needed + self.assertFalse(result) + # Ensure the registration purpose method was called with the correct version ID + mock_get_registration_purpose.assert_called_once_with(self.report_version.id) + # Verify that emissions calculation is not called for unregulated purposes + mock_get_emissions.assert_not_called() + + + @patch("reporting.service.compliance_service.ComplianceService.get_emissions_attributable_for_reporting") + @patch("reporting.service.report_additional_data.ReportAdditionalDataService.get_registration_purpose_by_version_id") + def test_get_report_needs_verification_returns_true_for_reporting_operation_with_high_emissions( + self, mock_get_registration_purpose, mock_get_emissions + ): + """ + Test that the service returns true for report of Reporting_Operation with attributable emissions exceeding the verification threshold + """ + + # Arrange: Simulate a reporting operation + mock_get_registration_purpose.return_value = "Reporting_Operation" + # Simulate high attributable emissions exceeding the verification threshold + mock_get_emissions.return_value = Decimal('30000000') + + # Act: Call the method to determine if the report needs verification + result = ReportVerificationService.get_report_needs_verification(self.report_version.id) + + # Assert: The method should identify the need for verification + self.assertTrue(result) + # Verify the mocked methods were called with the expected arguments + mock_get_registration_purpose.assert_called_once_with(self.report_version.id) + mock_get_emissions.assert_called_once_with(self.report_version.id) + + @patch("reporting.service.compliance_service.ComplianceService.get_emissions_attributable_for_reporting") + @patch("reporting.service.report_additional_data.ReportAdditionalDataService.get_registration_purpose_by_version_id") + def test_get_report_needs_verification_returns_false_for_reporting_operation_with_low_emissions( + self, mock_get_registration_purpose, mock_get_emissions + ): + """ + Test that the service returns false for report of Reporting_Operation with attributable emissions exceeding the verification threshold + """ + + # Arrange: Simulate a reporting operation + mock_get_registration_purpose.return_value = "Reporting_Operation" + # Simulate low attributable emissions below the verification threshold + mock_get_emissions.return_value = Decimal('20000000') + + # Act: Call the method to determine if the report needs verification + result = ReportVerificationService.get_report_needs_verification(self.report_version.id) + + # Assert: The method should correctly determine no verification is needed + self.assertFalse(result) + # Verify the mocked methods were called as expected + mock_get_registration_purpose.assert_called_once_with(self.report_version.id) + mock_get_emissions.assert_called_once_with(self.report_version.id) + def test_save_report_verification_saves_record(self): """ Test that the service updates or creates ReportVerification instance From 823053fb6be6755ce47ba89a66895af1b1495abb Mon Sep 17 00:00:00 2001 From: shon-button Date: Thu, 19 Dec 2024 11:23:07 -0500 Subject: [PATCH 2/8] chore: refactor compliance summary needs verification chore: prettier --- bc_obps/reporting/api/__init__.py | 6 ++- bc_obps/reporting/api/compliance_data.py | 1 - bc_obps/reporting/api/report_verification.py | 5 +-- .../service/report_verification_service.py | 9 ++-- .../tests/api/test_report_verification_api.py | 17 ++------ .../test_report_verification_service.py | 38 +++++++++-------- .../ComplianceSummaryPage.tsx | 26 ++---------- .../src/app/utils/getAttributableEmissions.ts | 15 ------- .../app/utils/getReportNeedsVerification.ts | 15 +++++++ .../ComplianceSummaryPage.test.tsx | 41 ++++--------------- 10 files changed, 62 insertions(+), 111 deletions(-) delete mode 100644 bciers/apps/reporting/src/app/utils/getAttributableEmissions.ts create mode 100644 bciers/apps/reporting/src/app/utils/getReportNeedsVerification.ts diff --git a/bc_obps/reporting/api/__init__.py b/bc_obps/reporting/api/__init__.py index fed8b6eabe..933b502a8d 100644 --- a/bc_obps/reporting/api/__init__.py +++ b/bc_obps/reporting/api/__init__.py @@ -20,7 +20,11 @@ from .report_non_attributable_emissions import save_report from .report_activity import save_report_activity_data, load_report_activity_data from .report_facilities import get_report_facility_list_by_version_id -from .report_verification import get_report_verification_by_version_id, get_report_needs_verification, save_report_verification +from .report_verification import ( + get_report_verification_by_version_id, + get_report_needs_verification, + save_report_verification, +) from .report_attachments import save_report_attachments, get_report_attachments from .report_emission_allocations import get_emission_allocations, save_emission_allocation_data from .compliance_data import get_compliance_summary_data diff --git a/bc_obps/reporting/api/compliance_data.py b/bc_obps/reporting/api/compliance_data.py index 5aa66dfd24..c01687efdc 100644 --- a/bc_obps/reporting/api/compliance_data.py +++ b/bc_obps/reporting/api/compliance_data.py @@ -1,4 +1,3 @@ -from decimal import Decimal from typing import Literal, Tuple from common.permissions import authorize from django.http import HttpRequest diff --git a/bc_obps/reporting/api/report_verification.py b/bc_obps/reporting/api/report_verification.py index bd1224341b..0e02f78f09 100644 --- a/bc_obps/reporting/api/report_verification.py +++ b/bc_obps/reporting/api/report_verification.py @@ -27,15 +27,14 @@ def get_report_verification_by_version_id( @router.get( - "/report-version/{report_version_id}/report-needs-verification", + "/report-version/{report_version_id}/report-needs-verification", response={200: bool, custom_codes_4xx: Message}, tags=EMISSIONS_REPORT_TAGS, description="""Checks if a report needs verification based on its purpose and attributable emissions.""", auth=authorize("approved_industry_user"), ) @handle_http_errors() -def get_report_needs_verification( - request: HttpRequest, report_version_id: int) -> tuple[Literal[200], bool]: +def get_report_needs_verification(request: HttpRequest, report_version_id: int) -> tuple[Literal[200], bool]: return 200, ReportVerificationService.get_report_needs_verification(report_version_id) diff --git a/bc_obps/reporting/service/report_verification_service.py b/bc_obps/reporting/service/report_verification_service.py index 9c2e39eebd..3d46a956de 100644 --- a/bc_obps/reporting/service/report_verification_service.py +++ b/bc_obps/reporting/service/report_verification_service.py @@ -1,4 +1,3 @@ - from decimal import Decimal from django.db import transaction from reporting.models.report_verification import ReportVerification @@ -8,8 +7,8 @@ from reporting.service.report_additional_data import ReportAdditionalDataService from reporting.service.compliance_service import ComplianceService -class ReportVerificationService: +class ReportVerificationService: @staticmethod def get_report_verification_by_version_id( report_version_id: int, @@ -72,12 +71,12 @@ def get_report_needs_verification(version_id: int) -> bool: } ATTRIBUTABLE_EMISSION_THRESHOLD = Decimal("25000000") registration_purpose = ReportAdditionalDataService.get_registration_purpose_by_version_id(version_id) - + # Registration Purpose: Users must complete the verification page if the registration purpose is in REGULATED_OPERATION_PURPOSES if registration_purpose in REGULATED_OPERATION_PURPOSES: return True - - # Emission threshold: Users must complete the verification page if the registration purpose is Reporting Operation, and total TCo₂e >+ 25,000 + + # Emission threshold: Users must complete the verification page if the registration purpose is Reporting Operation, and total TCo₂e >+ 25,000 if registration_purpose == "Reporting_Operation": attributable_emissions = ComplianceService.get_emissions_attributable_for_reporting(version_id) return attributable_emissions >= ATTRIBUTABLE_EMISSION_THRESHOLD diff --git a/bc_obps/reporting/tests/api/test_report_verification_api.py b/bc_obps/reporting/tests/api/test_report_verification_api.py index 51a3accc66..c45756e01c 100644 --- a/bc_obps/reporting/tests/api/test_report_verification_api.py +++ b/bc_obps/reporting/tests/api/test_report_verification_api.py @@ -54,15 +54,10 @@ def test_returns_verification_data_for_report_version_id( assert response_json["other_facility_name"] == self.report_verification.other_facility_name assert response_json["other_facility_coordinates"] == self.report_verification.other_facility_coordinates - """Tests for the get_report_needs_verification endpoint.""" - @patch( - "reporting.service.report_verification_service.ReportVerificationService.get_report_needs_verification" - ) - def test_returns_verification_needed_for_report_version_id( - self, mock_get_report_needs_verification: MagicMock - ): + @patch("reporting.service.report_verification_service.ReportVerificationService.get_report_needs_verification") + def test_returns_verification_needed_for_report_version_id(self, mock_get_report_needs_verification: MagicMock): # Arrange: Mock the service to return True mock_get_report_needs_verification.return_value = True @@ -86,12 +81,8 @@ def test_returns_verification_needed_for_report_version_id( response_json = response.json() assert response_json is True - @patch( - "reporting.service.report_verification_service.ReportVerificationService.get_report_needs_verification" - ) - def test_returns_verification_not_needed_for_report_version_id( - self, mock_get_report_needs_verification: MagicMock - ): + @patch("reporting.service.report_verification_service.ReportVerificationService.get_report_needs_verification") + def test_returns_verification_not_needed_for_report_version_id(self, mock_get_report_needs_verification: MagicMock): # Arrange: Mock the service to return False mock_get_report_needs_verification.return_value = False diff --git a/bc_obps/reporting/tests/service/test_report_verification_service.py b/bc_obps/reporting/tests/service/test_report_verification_service.py index ff63726fb7..6240478127 100644 --- a/bc_obps/reporting/tests/service/test_report_verification_service.py +++ b/bc_obps/reporting/tests/service/test_report_verification_service.py @@ -3,16 +3,14 @@ from model_bakery.baker import make_recipe from reporting.service.report_verification_service import ReportVerificationService from reporting.schema.report_verification import ReportVerificationIn -from unittest.mock import patch, MagicMock +from unittest.mock import patch + class TestReportVerificationService(TestCase): - REGULATED_OPERATION_PURPOSES = { - "OBPS_Regulated_Operation", - "Opt-in", - "New_Entrants" - } + REGULATED_OPERATION_PURPOSES = {"OBPS_Regulated_Operation", "Opt-in", "New_Entrants"} ATTRIBUTABLE_EMISSION_THRESHOLD = Decimal('25000000') + def setUp(self): # Arrange: Create a report version self.report_version = make_recipe('reporting.tests.utils.report_version') @@ -52,14 +50,16 @@ def test_get_report_verification_by_version_id_returns_correct_instance(self): ) @patch("reporting.service.compliance_service.ComplianceService.get_emissions_attributable_for_reporting") - @patch("reporting.service.report_additional_data.ReportAdditionalDataService.get_registration_purpose_by_version_id") + @patch( + "reporting.service.report_additional_data.ReportAdditionalDataService.get_registration_purpose_by_version_id" + ) def test_get_report_needs_verification_returns_true_for_regulated_purpose( self, mock_get_registration_purpose, mock_get_emissions - ): + ): """ Test that the service returns true for reports with regulated_purpose is a REGULATED_OPERATION_PURPOSES. """ - + # Arrange: Mock the registration purpose to simulate a regulated operation # The purpose is one of REGULATED_OPERATION_PURPOSES mock_get_registration_purpose.return_value = "OBPS_Regulated_Operation" @@ -72,9 +72,10 @@ def test_get_report_needs_verification_returns_true_for_regulated_purpose( # Ensure the registration purpose method was called with the correct version ID mock_get_registration_purpose.assert_called_once_with(self.report_version.id) - @patch("reporting.service.compliance_service.ComplianceService.get_emissions_attributable_for_reporting") - @patch("reporting.service.report_additional_data.ReportAdditionalDataService.get_registration_purpose_by_version_id") + @patch( + "reporting.service.report_additional_data.ReportAdditionalDataService.get_registration_purpose_by_version_id" + ) def test_get_report_needs_verification_returns_false_for_non_regulated_purpose( self, mock_get_registration_purpose, mock_get_emissions ): @@ -95,16 +96,17 @@ def test_get_report_needs_verification_returns_false_for_non_regulated_purpose( # Verify that emissions calculation is not called for unregulated purposes mock_get_emissions.assert_not_called() - @patch("reporting.service.compliance_service.ComplianceService.get_emissions_attributable_for_reporting") - @patch("reporting.service.report_additional_data.ReportAdditionalDataService.get_registration_purpose_by_version_id") + @patch( + "reporting.service.report_additional_data.ReportAdditionalDataService.get_registration_purpose_by_version_id" + ) def test_get_report_needs_verification_returns_true_for_reporting_operation_with_high_emissions( self, mock_get_registration_purpose, mock_get_emissions - ): + ): """ Test that the service returns true for report of Reporting_Operation with attributable emissions exceeding the verification threshold """ - + # Arrange: Simulate a reporting operation mock_get_registration_purpose.return_value = "Reporting_Operation" # Simulate high attributable emissions exceeding the verification threshold @@ -120,14 +122,16 @@ def test_get_report_needs_verification_returns_true_for_reporting_operation_with mock_get_emissions.assert_called_once_with(self.report_version.id) @patch("reporting.service.compliance_service.ComplianceService.get_emissions_attributable_for_reporting") - @patch("reporting.service.report_additional_data.ReportAdditionalDataService.get_registration_purpose_by_version_id") + @patch( + "reporting.service.report_additional_data.ReportAdditionalDataService.get_registration_purpose_by_version_id" + ) def test_get_report_needs_verification_returns_false_for_reporting_operation_with_low_emissions( self, mock_get_registration_purpose, mock_get_emissions ): """ Test that the service returns false for report of Reporting_Operation with attributable emissions exceeding the verification threshold """ - + # Arrange: Simulate a reporting operation mock_get_registration_purpose.return_value = "Reporting_Operation" # Simulate low attributable emissions below the verification threshold diff --git a/bciers/apps/reporting/src/app/components/complianceSummary/ComplianceSummaryPage.tsx b/bciers/apps/reporting/src/app/components/complianceSummary/ComplianceSummaryPage.tsx index a808592ffd..304f2a00f3 100644 --- a/bciers/apps/reporting/src/app/components/complianceSummary/ComplianceSummaryPage.tsx +++ b/bciers/apps/reporting/src/app/components/complianceSummary/ComplianceSummaryPage.tsx @@ -3,12 +3,7 @@ import { actionHandler } from "@bciers/actions"; import ComplianceSummaryForm from "./ComplianceSummaryForm"; import { tasklistData } from "./TaskListElements"; import { HasReportVersion } from "@reporting/src/app/utils/defaultPageFactoryTypes"; -import { getRegistrationPurpose } from "@reporting/src/app/utils/getRegistrationPurpose"; -import { getAttributableEmissions } from "@reporting/src/app/utils/getAttributableEmissions"; -import { - RegistrationPurposes, - regulatedOperationPurposes, -} from "@/registration/app/components/operations/registration/enums"; +import { getReportNeedsVerification } from "@reporting/src/app/utils/getReportNeedsVerification"; const getComplianceData = async (versionId: number) => { return actionHandler( @@ -21,23 +16,8 @@ export default async function ComplianceSummaryPage({ version_id, }: HasReportVersion) { const complianceData = await getComplianceData(version_id); - //🔍 Check if reports need verification step... - let needsVerification = false; - //🔍 Check if registration purpose is OBPS Regulated Operation, Opt-in or New Entrants - const registrationPurpose = (await getRegistrationPurpose(version_id)) - ?.registration_purpose; - needsVerification = regulatedOperationPurposes.includes( - registrationPurpose as RegistrationPurposes, - ); - if ( - needsVerification === false && - registrationPurpose === RegistrationPurposes.REPORTING_OPERATION - ) { - //🔍 Check if the registration purpose is Reporting Operation AND their total emissions attributable for reporting threshold is = or > than 25,000 TCo2 - const attributableEmissionThreshold = 25000000; - const attributableEmissions = await getAttributableEmissions(version_id); - needsVerification = attributableEmissions >= attributableEmissionThreshold; - } + //🔍 Check if reports need verification + const needsVerification = await getReportNeedsVerification(version_id); return ( ({ actionHandler: vi.fn(), })); -vi.mock("@reporting/src/app/utils/getRegistrationPurpose", () => ({ - getRegistrationPurpose: vi.fn(), -})); - -vi.mock("@reporting/src/app/utils/getAttributableEmissions", () => ({ - getAttributableEmissions: vi.fn(), +vi.mock("@reporting/src/app/utils/getReportNeedsVerification", () => ({ + getReportNeedsVerification: vi.fn(), })); const mockComplianceSummaryForm = ComplianceSummaryForm as ReturnType< typeof vi.fn >; const mockActionHandler = actionHandler as ReturnType; -const mockGetRegistrationPurpose = getRegistrationPurpose as ReturnType< - typeof vi.fn ->; -const mockGetAttributableEmissions = getAttributableEmissions as ReturnType< + +const mockGetReportNeedsVerification = getReportNeedsVerification as ReturnType< typeof vi.fn >; @@ -46,14 +38,8 @@ describe("ComplianceSummaryPage", () => { // Mock the data for the test const complianceData = { some: "data" }; - const registrationPurpose = RegistrationPurposes.REPORTING_OPERATION; - const attributableEmissions = 10000000; // Below the threshold - mockActionHandler.mockResolvedValue(complianceData); - mockGetRegistrationPurpose.mockResolvedValue({ - registration_purpose: registrationPurpose, - }); - mockGetAttributableEmissions.mockResolvedValue(attributableEmissions); + mockGetReportNeedsVerification.mockResolvedValue(false); // Render the page render(await ComplianceSummaryPage({ version_id: versionId })); @@ -75,12 +61,8 @@ describe("ComplianceSummaryPage", () => { // Mock the data for the test const complianceData = { some: "data" }; - const registrationPurpose = RegistrationPurposes.OBPS_REGULATED_OPERATION; - mockActionHandler.mockResolvedValue(complianceData); - mockGetRegistrationPurpose.mockResolvedValue({ - registration_purpose: registrationPurpose, - }); + mockGetReportNeedsVerification.mockResolvedValue(true); // Render the page render(await ComplianceSummaryPage({ version_id: versionId })); @@ -102,15 +84,8 @@ describe("ComplianceSummaryPage", () => { // Mock the data for the test const complianceData = { some: "data" }; - const registrationPurpose = - RegistrationPurposes.ELECTRICITY_IMPORT_OPERATION; - const attributableEmissions = 30000000; // Above the threshold - mockActionHandler.mockResolvedValue(complianceData); - mockGetRegistrationPurpose.mockResolvedValue({ - registration_purpose: registrationPurpose, - }); - mockGetAttributableEmissions.mockResolvedValue(attributableEmissions); + mockGetReportNeedsVerification.mockResolvedValue(true); // Render the page render(await ComplianceSummaryPage({ version_id: versionId })); From 7bac5b205443502cf46dad5dab779a1be76753ef Mon Sep 17 00:00:00 2001 From: shon-button Date: Thu, 19 Dec 2024 13:35:44 -0500 Subject: [PATCH 3/8] chore: add needs verification call to attachment page --- bc_obps/reporting/api/report_verification.py | 2 +- .../reporting/service/report_verification_service.py | 10 +++++++--- .../tests/service/test_report_verification_service.py | 6 +++--- .../src/app/components/attachments/AttachmentsPage.tsx | 6 +++++- 4 files changed, 16 insertions(+), 8 deletions(-) diff --git a/bc_obps/reporting/api/report_verification.py b/bc_obps/reporting/api/report_verification.py index 0e02f78f09..b51ea725ad 100644 --- a/bc_obps/reporting/api/report_verification.py +++ b/bc_obps/reporting/api/report_verification.py @@ -30,7 +30,7 @@ def get_report_verification_by_version_id( "/report-version/{report_version_id}/report-needs-verification", response={200: bool, custom_codes_4xx: Message}, tags=EMISSIONS_REPORT_TAGS, - description="""Checks if a report needs verification based on its purpose and attributable emissions.""", + description="""Checks if a report needs verification data based on its purpose and attributable emissions.""", auth=authorize("approved_industry_user"), ) @handle_http_errors() diff --git a/bc_obps/reporting/service/report_verification_service.py b/bc_obps/reporting/service/report_verification_service.py index 3d46a956de..6637370747 100644 --- a/bc_obps/reporting/service/report_verification_service.py +++ b/bc_obps/reporting/service/report_verification_service.py @@ -4,6 +4,7 @@ from reporting.models import ReportVersion from reporting.schema.report_verification import ReportVerificationIn +from registration.models import Operation from reporting.service.report_additional_data import ReportAdditionalDataService from reporting.service.compliance_service import ComplianceService @@ -65,14 +66,17 @@ def get_report_needs_verification(version_id: int) -> bool: and attributable emissions. """ REGULATED_OPERATION_PURPOSES = { - "OBPS_Regulated_Operation", - "Opt-in", - "New_Entrants", + Operation.Purposes.OBPS_REGULATED_OPERATION, + Operation.Purposes.OPTED_IN_OPERATION, + Operation.Purposes.NEW_ENTRANT_OPERATION, } ATTRIBUTABLE_EMISSION_THRESHOLD = Decimal("25000000") registration_purpose = ReportAdditionalDataService.get_registration_purpose_by_version_id(version_id) # Registration Purpose: Users must complete the verification page if the registration purpose is in REGULATED_OPERATION_PURPOSES + if isinstance(registration_purpose, dict): + registration_purpose = registration_purpose.get("registration_purpose") + if registration_purpose in REGULATED_OPERATION_PURPOSES: return True diff --git a/bc_obps/reporting/tests/service/test_report_verification_service.py b/bc_obps/reporting/tests/service/test_report_verification_service.py index 6240478127..dadb1d1c3b 100644 --- a/bc_obps/reporting/tests/service/test_report_verification_service.py +++ b/bc_obps/reporting/tests/service/test_report_verification_service.py @@ -62,7 +62,7 @@ def test_get_report_needs_verification_returns_true_for_regulated_purpose( # Arrange: Mock the registration purpose to simulate a regulated operation # The purpose is one of REGULATED_OPERATION_PURPOSES - mock_get_registration_purpose.return_value = "OBPS_Regulated_Operation" + mock_get_registration_purpose.return_value = "OBPS Regulated Operation" # Act: Call the method to determine if the report needs verification result = ReportVerificationService.get_report_needs_verification(self.report_version.id) @@ -108,7 +108,7 @@ def test_get_report_needs_verification_returns_true_for_reporting_operation_with """ # Arrange: Simulate a reporting operation - mock_get_registration_purpose.return_value = "Reporting_Operation" + mock_get_registration_purpose.return_value = "Reporting Operation" # Simulate high attributable emissions exceeding the verification threshold mock_get_emissions.return_value = Decimal('30000000') @@ -133,7 +133,7 @@ def test_get_report_needs_verification_returns_false_for_reporting_operation_wit """ # Arrange: Simulate a reporting operation - mock_get_registration_purpose.return_value = "Reporting_Operation" + mock_get_registration_purpose.return_value = "Reporting Operation" # Simulate low attributable emissions below the verification threshold mock_get_emissions.return_value = Decimal('20000000') diff --git a/bciers/apps/reporting/src/app/components/attachments/AttachmentsPage.tsx b/bciers/apps/reporting/src/app/components/attachments/AttachmentsPage.tsx index 2dce08355a..f735628a3c 100644 --- a/bciers/apps/reporting/src/app/components/attachments/AttachmentsPage.tsx +++ b/bciers/apps/reporting/src/app/components/attachments/AttachmentsPage.tsx @@ -3,6 +3,7 @@ import AttachmentsForm from "./AttachmentsForm"; import getAttachments from "@reporting/src/app/utils/getAttachments"; import { UploadedAttachment } from "./types"; import { getSignOffAndSubmitSteps } from "../taskList/5_signOffSubmit"; +import { getReportNeedsVerification } from "@reporting/src/app/utils/getReportNeedsVerification"; export const getDictFromAttachmentArray = (array: UploadedAttachment[]) => Object.fromEntries(array.map((a) => [a.attachment_type, a])); @@ -16,12 +17,15 @@ const AttachmentsPage: React.FC = async ({ version_id }) => { const taskListElements = getSignOffAndSubmitSteps(version_id, 1); + //🔍 Check if reports need verification + const needsVerification = await getReportNeedsVerification(version_id); + return ( ); }; From dd0f9a5cf46999dfd7cd2bac9bea4f8dbe6e006b Mon Sep 17 00:00:00 2001 From: shon-button Date: Thu, 19 Dec 2024 15:00:46 -0500 Subject: [PATCH 4/8] tests: attachment page chore: cleanup --- .../service/report_verification_service.py | 2 +- .../test_report_verification_service.py | 25 ++++++++++++------- .../attachments/AttachmentsForm.tsx | 5 +++- .../verification/VerificationPage.tsx | 2 +- .../verification/createVerificationSchema.ts | 2 +- .../verification/verification.ts | 0 .../verification/verificationText.tsx | 0 .../attachments/AttachmentPage.test.tsx | 9 +++++++ .../verification/VerificationForm.test.tsx | 2 +- .../verification/VerificationPage.test.tsx | 2 +- 10 files changed, 34 insertions(+), 15 deletions(-) rename bciers/apps/reporting/src/data/{ => jsonSchema}/verification/verification.ts (100%) rename bciers/apps/reporting/src/data/{ => jsonSchema}/verification/verificationText.tsx (100%) diff --git a/bc_obps/reporting/service/report_verification_service.py b/bc_obps/reporting/service/report_verification_service.py index 6637370747..12171a4d9f 100644 --- a/bc_obps/reporting/service/report_verification_service.py +++ b/bc_obps/reporting/service/report_verification_service.py @@ -81,7 +81,7 @@ def get_report_needs_verification(version_id: int) -> bool: return True # Emission threshold: Users must complete the verification page if the registration purpose is Reporting Operation, and total TCo₂e >+ 25,000 - if registration_purpose == "Reporting_Operation": + if registration_purpose == Operation.Purposes.REPORTING_OPERATION: attributable_emissions = ComplianceService.get_emissions_attributable_for_reporting(version_id) return attributable_emissions >= ATTRIBUTABLE_EMISSION_THRESHOLD diff --git a/bc_obps/reporting/tests/service/test_report_verification_service.py b/bc_obps/reporting/tests/service/test_report_verification_service.py index dadb1d1c3b..b524975594 100644 --- a/bc_obps/reporting/tests/service/test_report_verification_service.py +++ b/bc_obps/reporting/tests/service/test_report_verification_service.py @@ -5,10 +5,14 @@ from reporting.schema.report_verification import ReportVerificationIn from unittest.mock import patch - -class TestReportVerificationService(TestCase): - REGULATED_OPERATION_PURPOSES = {"OBPS_Regulated_Operation", "Opt-in", "New_Entrants"} - +from registration.models import Operation + +class TestReportVerificationService(TestCase): + REGULATED_OPERATION_PURPOSES = { + Operation.Purposes.OBPS_REGULATED_OPERATION, + Operation.Purposes.OPTED_IN_OPERATION, + Operation.Purposes.NEW_ENTRANT_OPERATION, + } ATTRIBUTABLE_EMISSION_THRESHOLD = Decimal('25000000') def setUp(self): @@ -62,7 +66,8 @@ def test_get_report_needs_verification_returns_true_for_regulated_purpose( # Arrange: Mock the registration purpose to simulate a regulated operation # The purpose is one of REGULATED_OPERATION_PURPOSES - mock_get_registration_purpose.return_value = "OBPS Regulated Operation" + mock_get_registration_purpose.return_value = Operation.Purposes.OBPS_REGULATED_OPERATION + # Act: Call the method to determine if the report needs verification result = ReportVerificationService.get_report_needs_verification(self.report_version.id) @@ -71,7 +76,9 @@ def test_get_report_needs_verification_returns_true_for_regulated_purpose( self.assertTrue(result) # Ensure the registration purpose method was called with the correct version ID mock_get_registration_purpose.assert_called_once_with(self.report_version.id) - + # Verify that emissions calculation is not called for regulated purpose + mock_get_emissions.assert_not_called() + @patch("reporting.service.compliance_service.ComplianceService.get_emissions_attributable_for_reporting") @patch( "reporting.service.report_additional_data.ReportAdditionalDataService.get_registration_purpose_by_version_id" @@ -83,7 +90,7 @@ def test_get_report_needs_verification_returns_false_for_non_regulated_purpose( Test that the service returns false for reports with regulated_purpose NOT a REGULATED_OPERATION_PURPOSES. """ - # Arrange: Simulate a purpose that is not in REGULATED_OPERATION_PURPOSES + # Arrange: Simulate a purpose that is not in REGULATED_OPERATION_PURPOSES and NOT Operation.Purposes.REPORTING_OPERATION mock_get_registration_purpose.return_value = "Electricity Import Operation" # Act: Call the method to determine if the report needs verification @@ -108,7 +115,7 @@ def test_get_report_needs_verification_returns_true_for_reporting_operation_with """ # Arrange: Simulate a reporting operation - mock_get_registration_purpose.return_value = "Reporting Operation" + mock_get_registration_purpose.return_value = Operation.Purposes.REPORTING_OPERATION # Simulate high attributable emissions exceeding the verification threshold mock_get_emissions.return_value = Decimal('30000000') @@ -133,7 +140,7 @@ def test_get_report_needs_verification_returns_false_for_reporting_operation_wit """ # Arrange: Simulate a reporting operation - mock_get_registration_purpose.return_value = "Reporting Operation" + mock_get_registration_purpose.return_value = Operation.Purposes.REPORTING_OPERATION # Simulate low attributable emissions below the verification threshold mock_get_emissions.return_value = Decimal('20000000') diff --git a/bciers/apps/reporting/src/app/components/attachments/AttachmentsForm.tsx b/bciers/apps/reporting/src/app/components/attachments/AttachmentsForm.tsx index 12fe948b88..2ef6878ee5 100644 --- a/bciers/apps/reporting/src/app/components/attachments/AttachmentsForm.tsx +++ b/bciers/apps/reporting/src/app/components/attachments/AttachmentsForm.tsx @@ -149,7 +149,10 @@ const AttachmentsForm: React.FC = ({ {buildAttachmentElement( "Verification Statement", "verification_statement", - { required: true, error: validationErrors.verification_statement }, + { + required: isVerificationStatementMandatory, + error: validationErrors.verification_statement, + }, )} {buildAttachmentElement("WCI.352 and WCI.362", "wci_352_362")} {buildAttachmentElement( diff --git a/bciers/apps/reporting/src/app/components/verification/VerificationPage.tsx b/bciers/apps/reporting/src/app/components/verification/VerificationPage.tsx index 37777a1063..05f7674151 100644 --- a/bciers/apps/reporting/src/app/components/verification/VerificationPage.tsx +++ b/bciers/apps/reporting/src/app/components/verification/VerificationPage.tsx @@ -1,7 +1,7 @@ import { getReportVerification } from "@reporting/src/app/utils/getReportVerification"; import { getReportFacilityList } from "@reporting/src/app/utils/getReportFacilityList"; import { createVerificationSchema } from "@reporting/src/app/components/verification/createVerificationSchema"; -import { verificationUiSchema } from "@reporting/src/data/verification/verification"; +import { verificationUiSchema } from "@reporting/src/data/jsonSchema/verification/verification"; import VerificationForm from "@reporting/src/app/components/verification/VerificationForm"; import { getSignOffAndSubmitSteps } from "@reporting/src/app/components/taskList/5_signOffSubmit"; diff --git a/bciers/apps/reporting/src/app/components/verification/createVerificationSchema.ts b/bciers/apps/reporting/src/app/components/verification/createVerificationSchema.ts index 0b503a9021..765a27cdf9 100644 --- a/bciers/apps/reporting/src/app/components/verification/createVerificationSchema.ts +++ b/bciers/apps/reporting/src/app/components/verification/createVerificationSchema.ts @@ -1,5 +1,5 @@ import { RJSFSchema } from "@rjsf/utils"; -import { verificationSchema } from "@reporting/src/data/verification/verification"; +import { verificationSchema } from "@reporting/src/data/jsonSchema/verification/verification"; export const createVerificationSchema = (facilities: string[]): RJSFSchema => { // Retrieve a local copy of the base verification schema based diff --git a/bciers/apps/reporting/src/data/verification/verification.ts b/bciers/apps/reporting/src/data/jsonSchema/verification/verification.ts similarity index 100% rename from bciers/apps/reporting/src/data/verification/verification.ts rename to bciers/apps/reporting/src/data/jsonSchema/verification/verification.ts diff --git a/bciers/apps/reporting/src/data/verification/verificationText.tsx b/bciers/apps/reporting/src/data/jsonSchema/verification/verificationText.tsx similarity index 100% rename from bciers/apps/reporting/src/data/verification/verificationText.tsx rename to bciers/apps/reporting/src/data/jsonSchema/verification/verificationText.tsx diff --git a/bciers/apps/reporting/src/tests/components/attachments/AttachmentPage.test.tsx b/bciers/apps/reporting/src/tests/components/attachments/AttachmentPage.test.tsx index 254d07a482..e5e22a6852 100644 --- a/bciers/apps/reporting/src/tests/components/attachments/AttachmentPage.test.tsx +++ b/bciers/apps/reporting/src/tests/components/attachments/AttachmentPage.test.tsx @@ -1,6 +1,7 @@ import AttachmentsForm from "@reporting/src/app/components/attachments/AttachmentsForm"; import AttachmentsPage from "@reporting/src/app/components/attachments/AttachmentsPage"; import getAttachments from "@reporting/src/app/utils/getAttachments"; +import { getReportNeedsVerification } from "@reporting/src/app/utils/getReportNeedsVerification"; import { render } from "@testing-library/react"; vi.mock("@reporting/src/app/components/attachments/AttachmentsForm", () => ({ @@ -15,8 +16,15 @@ vi.mock("@reporting/src/app/components/taskList/5_signOffSubmit", () => ({ getSignOffAndSubmitSteps: vi.fn().mockReturnValue("test task list"), })); +vi.mock("@reporting/src/app/utils/getReportNeedsVerification", () => ({ + getReportNeedsVerification: vi.fn(), +})); + const mockAttachmentsForm = AttachmentsForm as ReturnType; const mockGetAttachments = getAttachments as ReturnType; +const mockGetReportNeedsVerification = getReportNeedsVerification as ReturnType< + typeof vi.fn +>; describe("The attachment page component", () => { it("must render the client side component with the fetched data", async () => { @@ -32,6 +40,7 @@ describe("The attachment page component", () => { }; mockGetAttachments.mockReturnValue([attachment1, attachment2]); + mockGetReportNeedsVerification.mockResolvedValue(true); render(await AttachmentsPage({ version_id: 12345 })); diff --git a/bciers/apps/reporting/src/tests/verification/VerificationForm.test.tsx b/bciers/apps/reporting/src/tests/verification/VerificationForm.test.tsx index af9276f629..9b333c1661 100644 --- a/bciers/apps/reporting/src/tests/verification/VerificationForm.test.tsx +++ b/bciers/apps/reporting/src/tests/verification/VerificationForm.test.tsx @@ -3,7 +3,7 @@ import { actionHandler, useRouter } from "@bciers/testConfig/mocks"; import { verificationSchema, verificationUiSchema, -} from "@reporting/src/data/verification/verification"; +} from "@reporting/src/data/jsonSchema/verification/verification"; import VerificationForm from "@reporting/src/app/components/verification/VerificationForm"; import expectButton from "@bciers/testConfig/helpers/expectButton"; import expectField from "@bciers/testConfig/helpers/expectField"; diff --git a/bciers/apps/reporting/src/tests/verification/VerificationPage.test.tsx b/bciers/apps/reporting/src/tests/verification/VerificationPage.test.tsx index 1a974fc062..f148ae61fc 100644 --- a/bciers/apps/reporting/src/tests/verification/VerificationPage.test.tsx +++ b/bciers/apps/reporting/src/tests/verification/VerificationPage.test.tsx @@ -1,6 +1,6 @@ import { render, screen, waitFor } from "@testing-library/react"; import VerificationPage from "@reporting/src/app/components/verification/VerificationPage"; -import { verificationSchema } from "@reporting/src/data/verification/verification"; +import { verificationSchema } from "@reporting/src/data/jsonSchema/verification/verification"; import { createVerificationSchema } from "@reporting/src/app/components/verification/createVerificationSchema"; import { getReportFacilityList } from "@reporting/src/app/utils/getReportFacilityList"; From c71ad7668df92ec48978ad2fdb40d4c2fd531948 Mon Sep 17 00:00:00 2001 From: shon-button Date: Thu, 19 Dec 2024 16:00:41 -0500 Subject: [PATCH 5/8] chore: validate_report checks verification mandatory chore: cleanup --- .../service/report_submission_service.py | 14 ++++++++++---- .../service/report_verification_service.py | 2 +- .../test_report_verification_service.py | 18 +++++++++--------- 3 files changed, 20 insertions(+), 14 deletions(-) diff --git a/bc_obps/reporting/service/report_submission_service.py b/bc_obps/reporting/service/report_submission_service.py index fa37c65717..3c4ffa3256 100644 --- a/bc_obps/reporting/service/report_submission_service.py +++ b/bc_obps/reporting/service/report_submission_service.py @@ -2,6 +2,7 @@ from reporting.models.report_attachment import ReportAttachment from reporting.models.report_version import ReportVersion +from reporting.service.report_verification_service import ReportVerificationService class ReportSubmissionService: @@ -16,10 +17,15 @@ def validate_report(version_id: int) -> None: Django-ninja could then have a special way of parsing that error with a custom error code. """ try: - ReportAttachment.objects.get( - report_version_id=version_id, - attachment_type=ReportAttachment.ReportAttachmentType.VERIFICATION_STATEMENT, - ) + # Check if verification statement is mandatory + isVerificationStatementMandatory = ReportVerificationService.get_report_needs_verification(version_id) + + if isVerificationStatementMandatory: + # Check for the attachment only if mandatory + ReportAttachment.objects.get( + report_version_id=version_id, + attachment_type=ReportAttachment.ReportAttachmentType.VERIFICATION_STATEMENT, + ) except ReportAttachment.DoesNotExist: raise Exception("verification_statement") diff --git a/bc_obps/reporting/service/report_verification_service.py b/bc_obps/reporting/service/report_verification_service.py index 12171a4d9f..6d3ee73538 100644 --- a/bc_obps/reporting/service/report_verification_service.py +++ b/bc_obps/reporting/service/report_verification_service.py @@ -81,7 +81,7 @@ def get_report_needs_verification(version_id: int) -> bool: return True # Emission threshold: Users must complete the verification page if the registration purpose is Reporting Operation, and total TCo₂e >+ 25,000 - if registration_purpose == Operation.Purposes.REPORTING_OPERATION: + if registration_purpose == Operation.Purposes.REPORTING_OPERATION: attributable_emissions = ComplianceService.get_emissions_attributable_for_reporting(version_id) return attributable_emissions >= ATTRIBUTABLE_EMISSION_THRESHOLD diff --git a/bc_obps/reporting/tests/service/test_report_verification_service.py b/bc_obps/reporting/tests/service/test_report_verification_service.py index b524975594..a58c882e0c 100644 --- a/bc_obps/reporting/tests/service/test_report_verification_service.py +++ b/bc_obps/reporting/tests/service/test_report_verification_service.py @@ -7,12 +7,13 @@ from registration.models import Operation -class TestReportVerificationService(TestCase): + +class TestReportVerificationService(TestCase): REGULATED_OPERATION_PURPOSES = { - Operation.Purposes.OBPS_REGULATED_OPERATION, - Operation.Purposes.OPTED_IN_OPERATION, - Operation.Purposes.NEW_ENTRANT_OPERATION, - } + Operation.Purposes.OBPS_REGULATED_OPERATION, + Operation.Purposes.OPTED_IN_OPERATION, + Operation.Purposes.NEW_ENTRANT_OPERATION, + } ATTRIBUTABLE_EMISSION_THRESHOLD = Decimal('25000000') def setUp(self): @@ -67,7 +68,6 @@ def test_get_report_needs_verification_returns_true_for_regulated_purpose( # Arrange: Mock the registration purpose to simulate a regulated operation # The purpose is one of REGULATED_OPERATION_PURPOSES mock_get_registration_purpose.return_value = Operation.Purposes.OBPS_REGULATED_OPERATION - # Act: Call the method to determine if the report needs verification result = ReportVerificationService.get_report_needs_verification(self.report_version.id) @@ -78,7 +78,7 @@ def test_get_report_needs_verification_returns_true_for_regulated_purpose( mock_get_registration_purpose.assert_called_once_with(self.report_version.id) # Verify that emissions calculation is not called for regulated purpose mock_get_emissions.assert_not_called() - + @patch("reporting.service.compliance_service.ComplianceService.get_emissions_attributable_for_reporting") @patch( "reporting.service.report_additional_data.ReportAdditionalDataService.get_registration_purpose_by_version_id" @@ -115,7 +115,7 @@ def test_get_report_needs_verification_returns_true_for_reporting_operation_with """ # Arrange: Simulate a reporting operation - mock_get_registration_purpose.return_value = Operation.Purposes.REPORTING_OPERATION + mock_get_registration_purpose.return_value = Operation.Purposes.REPORTING_OPERATION # Simulate high attributable emissions exceeding the verification threshold mock_get_emissions.return_value = Decimal('30000000') @@ -140,7 +140,7 @@ def test_get_report_needs_verification_returns_false_for_reporting_operation_wit """ # Arrange: Simulate a reporting operation - mock_get_registration_purpose.return_value = Operation.Purposes.REPORTING_OPERATION + mock_get_registration_purpose.return_value = Operation.Purposes.REPORTING_OPERATION # Simulate low attributable emissions below the verification threshold mock_get_emissions.return_value = Decimal('20000000') From 75ea627db05e0630b85814eb60bb0fc7c44a2360 Mon Sep 17 00:00:00 2001 From: shon-button Date: Thu, 19 Dec 2024 16:18:40 -0500 Subject: [PATCH 6/8] tests: report submission service chore: cleanup chore: cleanup chore: cleanup --- .../service/report_verification_service.py | 8 +-- .../service/test_report_submission_service.py | 70 +++++++++++++++++++ .../test_report_verification_service.py | 2 +- 3 files changed, 75 insertions(+), 5 deletions(-) create mode 100644 bc_obps/reporting/tests/service/test_report_submission_service.py diff --git a/bc_obps/reporting/service/report_verification_service.py b/bc_obps/reporting/service/report_verification_service.py index 6d3ee73538..b4437a6935 100644 --- a/bc_obps/reporting/service/report_verification_service.py +++ b/bc_obps/reporting/service/report_verification_service.py @@ -62,7 +62,7 @@ def save_report_verification(version_id: int, data: ReportVerificationIn) -> Rep @staticmethod def get_report_needs_verification(version_id: int) -> bool: """ - Determines if a report needs verification based on its purpose + Determines if a report needs verification data based on its purpose and attributable emissions. """ REGULATED_OPERATION_PURPOSES = { @@ -73,14 +73,14 @@ def get_report_needs_verification(version_id: int) -> bool: ATTRIBUTABLE_EMISSION_THRESHOLD = Decimal("25000000") registration_purpose = ReportAdditionalDataService.get_registration_purpose_by_version_id(version_id) - # Registration Purpose: Users must complete the verification page if the registration purpose is in REGULATED_OPERATION_PURPOSES + # Registration Purpose: verification data is required if the registration purpose is in REGULATED_OPERATION_PURPOSES if isinstance(registration_purpose, dict): - registration_purpose = registration_purpose.get("registration_purpose") + registration_purpose = registration_purpose.get("registration_purpose", {}) if registration_purpose in REGULATED_OPERATION_PURPOSES: return True - # Emission threshold: Users must complete the verification page if the registration purpose is Reporting Operation, and total TCo₂e >+ 25,000 + # Emission threshold: verification data is required if the registration purpose is Reporting Operation, and total TCo₂e >+ 25,000 if registration_purpose == Operation.Purposes.REPORTING_OPERATION: attributable_emissions = ComplianceService.get_emissions_attributable_for_reporting(version_id) return attributable_emissions >= ATTRIBUTABLE_EMISSION_THRESHOLD diff --git a/bc_obps/reporting/tests/service/test_report_submission_service.py b/bc_obps/reporting/tests/service/test_report_submission_service.py new file mode 100644 index 0000000000..616c85569b --- /dev/null +++ b/bc_obps/reporting/tests/service/test_report_submission_service.py @@ -0,0 +1,70 @@ +import pytest +from unittest.mock import patch, MagicMock +from uuid import UUID +from reporting.models.report_attachment import ReportAttachment +from reporting.models.report_version import ReportVersion +from reporting.service.report_submission_service import ReportSubmissionService + + +class TestReportSubmissionService: + @patch("reporting.service.report_submission_service.ReportVerificationService.get_report_needs_verification") + @patch("reporting.models.report_attachment.ReportAttachment.objects.get") + def test_validate_report_with_verification_statement(self, mock_get_attachment, mock_get_verification): + # Arrange + version_id = 1 + mock_get_verification.return_value = True # Verification statement is mandatory + + # Act + ReportSubmissionService.validate_report(version_id) + + # Assert + mock_get_attachment.assert_called_once_with( + report_version_id=version_id, + attachment_type=ReportAttachment.ReportAttachmentType.VERIFICATION_STATEMENT, + ) + + @patch("reporting.service.report_submission_service.ReportVerificationService.get_report_needs_verification") + @patch("reporting.models.report_attachment.ReportAttachment.objects.get") + def test_validate_report_without_verification_statement(self, mock_get_attachment, mock_get_verification): + # Arrange + version_id = 1 + mock_get_verification.return_value = False # Verification statement is not mandatory + + # Act + ReportSubmissionService.validate_report(version_id) + + # Assert + mock_get_attachment.assert_not_called() + + @patch("reporting.service.report_submission_service.ReportVerificationService.get_report_needs_verification") + @patch("reporting.models.report_attachment.ReportAttachment.objects.get") + def test_validate_report_raises_exception_if_verification_missing(self, mock_get_attachment, mock_get_verification): + # Arrange + version_id = 1 + mock_get_verification.return_value = True # Verification statement is mandatory + mock_get_attachment.side_effect = ReportAttachment.DoesNotExist + + # Act & Assert + with pytest.raises(Exception, match="verification_statement"): + ReportSubmissionService.validate_report(version_id) + + @patch("reporting.models.report_version.ReportVersion.objects.get") + @patch("reporting.service.report_submission_service.ReportSubmissionService.validate_report") + def test_submit_report(self, mock_validate_report, mock_get_report_version): + # Arrange + version_id = 1 + user_guid = UUID("12345678-1234-5678-1234-567812345678") + + mock_report_version = MagicMock() + mock_get_report_version.return_value = mock_report_version + + # Act + result = ReportSubmissionService.submit_report(version_id, user_guid) + + # Assert + mock_validate_report.assert_called_once_with(version_id) + mock_get_report_version.assert_called_once_with(id=version_id) + mock_report_version.set_create_or_update.assert_called_once_with(user_guid) + assert mock_report_version.status == ReportVersion.ReportVersionStatus.Submitted + mock_report_version.save.assert_called_once() + assert result == mock_report_version diff --git a/bc_obps/reporting/tests/service/test_report_verification_service.py b/bc_obps/reporting/tests/service/test_report_verification_service.py index a58c882e0c..d5690ef678 100644 --- a/bc_obps/reporting/tests/service/test_report_verification_service.py +++ b/bc_obps/reporting/tests/service/test_report_verification_service.py @@ -91,7 +91,7 @@ def test_get_report_needs_verification_returns_false_for_non_regulated_purpose( """ # Arrange: Simulate a purpose that is not in REGULATED_OPERATION_PURPOSES and NOT Operation.Purposes.REPORTING_OPERATION - mock_get_registration_purpose.return_value = "Electricity Import Operation" + mock_get_registration_purpose.return_value = Operation.Purposes.ELECTRICITY_IMPORT_OPERATION # Act: Call the method to determine if the report needs verification result = ReportVerificationService.get_report_needs_verification(self.report_version.id) From 2d413e48a2c3df8bf6931c8c89a543f9f9340505 Mon Sep 17 00:00:00 2001 From: Shon <120038448+shon-button@users.noreply.github.com> Date: Mon, 6 Jan 2025 10:17:12 -0500 Subject: [PATCH 7/8] Update bc_obps/reporting/service/report_verification_service.py Co-authored-by: Pierre Bastianelli --- bc_obps/reporting/service/report_verification_service.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bc_obps/reporting/service/report_verification_service.py b/bc_obps/reporting/service/report_verification_service.py index b4437a6935..cc38db076b 100644 --- a/bc_obps/reporting/service/report_verification_service.py +++ b/bc_obps/reporting/service/report_verification_service.py @@ -74,8 +74,7 @@ def get_report_needs_verification(version_id: int) -> bool: registration_purpose = ReportAdditionalDataService.get_registration_purpose_by_version_id(version_id) # Registration Purpose: verification data is required if the registration purpose is in REGULATED_OPERATION_PURPOSES - if isinstance(registration_purpose, dict): - registration_purpose = registration_purpose.get("registration_purpose", {}) + registration_purpose_value = registration_purpose.get("registration_purpose", {}) if registration_purpose in REGULATED_OPERATION_PURPOSES: return True From 284ad9ea554e21da39f575454bcf13a643fae50b Mon Sep 17 00:00:00 2001 From: Shon <120038448+shon-button@users.noreply.github.com> Date: Mon, 6 Jan 2025 10:17:28 -0500 Subject: [PATCH 8/8] Update bc_obps/reporting/service/report_verification_service.py Co-authored-by: Pierre Bastianelli chore: implement review suggestions --- .../service/report_verification_service.py | 13 +++++++------ .../service/test_report_verification_service.py | 7 ------- 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/bc_obps/reporting/service/report_verification_service.py b/bc_obps/reporting/service/report_verification_service.py index cc38db076b..69fec78938 100644 --- a/bc_obps/reporting/service/report_verification_service.py +++ b/bc_obps/reporting/service/report_verification_service.py @@ -71,15 +71,16 @@ def get_report_needs_verification(version_id: int) -> bool: Operation.Purposes.NEW_ENTRANT_OPERATION, } ATTRIBUTABLE_EMISSION_THRESHOLD = Decimal("25000000") - registration_purpose = ReportAdditionalDataService.get_registration_purpose_by_version_id(version_id) - # Registration Purpose: verification data is required if the registration purpose is in REGULATED_OPERATION_PURPOSES - registration_purpose_value = registration_purpose.get("registration_purpose", {}) + # Fetch registration purpose + registration_purpose = ReportAdditionalDataService.get_registration_purpose_by_version_id(version_id) - if registration_purpose in REGULATED_OPERATION_PURPOSES: - return True + # Compare the enum value + if isinstance(registration_purpose, Operation.Purposes): + if registration_purpose in REGULATED_OPERATION_PURPOSES: + return True - # Emission threshold: verification data is required if the registration purpose is Reporting Operation, and total TCo₂e >+ 25,000 + # Emission threshold: verification data is required if the registration purpose is Reporting Operation, and total TCo₂e >= 25,000 if registration_purpose == Operation.Purposes.REPORTING_OPERATION: attributable_emissions = ComplianceService.get_emissions_attributable_for_reporting(version_id) return attributable_emissions >= ATTRIBUTABLE_EMISSION_THRESHOLD diff --git a/bc_obps/reporting/tests/service/test_report_verification_service.py b/bc_obps/reporting/tests/service/test_report_verification_service.py index d5690ef678..3daac57d28 100644 --- a/bc_obps/reporting/tests/service/test_report_verification_service.py +++ b/bc_obps/reporting/tests/service/test_report_verification_service.py @@ -9,13 +9,6 @@ class TestReportVerificationService(TestCase): - REGULATED_OPERATION_PURPOSES = { - Operation.Purposes.OBPS_REGULATED_OPERATION, - Operation.Purposes.OPTED_IN_OPERATION, - Operation.Purposes.NEW_ENTRANT_OPERATION, - } - ATTRIBUTABLE_EMISSION_THRESHOLD = Decimal('25000000') - def setUp(self): # Arrange: Create a report version self.report_version = make_recipe('reporting.tests.utils.report_version')