Skip to content

Commit

Permalink
Merge pull request #2625 from bcgov/chore/405-report-needs-verificati…
Browse files Browse the repository at this point in the history
…on-api

chore: 405 - refactor report needs verification logic as service
  • Loading branch information
shon-button authored Jan 6, 2025
2 parents 724ce1e + 284ad9e commit 1a2d0dd
Show file tree
Hide file tree
Showing 23 changed files with 345 additions and 158 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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"},
Expand Down Expand Up @@ -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}},
{
Expand Down
8 changes: 6 additions & 2 deletions bc_obps/reporting/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,12 @@
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
19 changes: 0 additions & 19 deletions bc_obps/reporting/api/compliance_data.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
from decimal import Decimal
from typing import Literal, Tuple
from common.permissions import authorize
from django.http import HttpRequest
Expand Down Expand Up @@ -26,21 +25,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
24 changes: 18 additions & 6 deletions bc_obps/reporting/api/report_verification.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,30 +12,42 @@


@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.""",
auth=authorize("approved_industry_user"),
)
@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 data 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.""",
auth=authorize("approved_industry_user"),
)
@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
14 changes: 10 additions & 4 deletions bc_obps/reporting/service/report_submission_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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")

Expand Down
33 changes: 33 additions & 0 deletions bc_obps/reporting/service/report_verification_service.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
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 registration.models import Operation
from reporting.service.report_additional_data import ReportAdditionalDataService
from reporting.service.compliance_service import ComplianceService


class ReportVerificationService:
@staticmethod
Expand Down Expand Up @@ -53,3 +58,31 @@ 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 data based on its purpose
and attributable emissions.
"""
REGULATED_OPERATION_PURPOSES = {
Operation.Purposes.OBPS_REGULATED_OPERATION,
Operation.Purposes.OPTED_IN_OPERATION,
Operation.Purposes.NEW_ENTRANT_OPERATION,
}
ATTRIBUTABLE_EMISSION_THRESHOLD = Decimal("25000000")

# Fetch registration purpose
registration_purpose = ReportAdditionalDataService.get_registration_purpose_by_version_id(version_id)

# 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
if registration_purpose == Operation.Purposes.REPORTING_OPERATION:
attributable_emissions = ComplianceService.get_emissions_attributable_for_reporting(version_id)
return attributable_emissions >= ATTRIBUTABLE_EMISSION_THRESHOLD

return False
45 changes: 0 additions & 45 deletions bc_obps/reporting/tests/api/test_compliance_data_api.py

This file was deleted.

56 changes: 54 additions & 2 deletions bc_obps/reporting/tests/api/test_report_verification_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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},
),
)

Expand All @@ -54,6 +54,58 @@ 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")
Expand Down Expand Up @@ -95,7 +147,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},
),
)

Expand Down
70 changes: 70 additions & 0 deletions bc_obps/reporting/tests/service/test_report_submission_service.py
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit 1a2d0dd

Please sign in to comment.