diff --git a/bc_obps/registration/tests/models/test_user.py b/bc_obps/registration/tests/models/test_user.py index 9816cead9c..f9ec0aed6b 100644 --- a/bc_obps/registration/tests/models/test_user.py +++ b/bc_obps/registration/tests/models/test_user.py @@ -140,9 +140,12 @@ def setUpTestData(cls): ("reportnewentrant_created", "report new entrant", None, None), ("reportnewentrant_updated", "report new entrant", None, None), ("reportnewentrant_archived", "report new entrant", None, None), - ("reportversion_created", "report version", None, None), - ("reportversion_updated", "report version", None, None), - ("reportversion_archived", "report version", None, None), + ("reportverification_created", "report verification", None, None), + ("reportverification_updated", "report verification", None, None), + ("reportverification_archived", "report verification", None, None), + ("reportverificationvisit_created", "report verification visit", None, None), + ("reportverificationvisit_updated", "report verification visit", None, None), + ("reportverificationvisit_archived", "report verification visit", None, None), ("reportattachment_created", "report attachment", None, None), ("reportattachment_updated", "report attachment", None, None), ("reportattachment_archived", "report attachment", None, None), diff --git a/bc_obps/reporting/api/report_verification.py b/bc_obps/reporting/api/report_verification.py index b51ea725ad..9df4354856 100644 --- a/bc_obps/reporting/api/report_verification.py +++ b/bc_obps/reporting/api/report_verification.py @@ -1,4 +1,5 @@ from typing import Literal +from reporting.models.report_verification import ReportVerification from common.permissions import authorize from django.http import HttpRequest from registration.decorators import handle_http_errors @@ -8,7 +9,6 @@ from .router import router from reporting.schema.report_verification import ReportVerificationIn, ReportVerificationOut from reporting.service.report_verification_service import ReportVerificationService -from reporting.models import ReportVerification @router.get( @@ -22,8 +22,7 @@ def get_report_verification_by_version_id( request: HttpRequest, report_version_id: int ) -> tuple[Literal[200], ReportVerification]: - report_verification = ReportVerificationService.get_report_verification_by_version_id(report_version_id) - return 200, report_verification + return 200, ReportVerificationService.get_report_verification_by_version_id(report_version_id) @router.get( diff --git a/bc_obps/reporting/migrations/0050_remove_reportverification_other_facility_coordinates_and_more.py b/bc_obps/reporting/migrations/0050_remove_reportverification_other_facility_coordinates_and_more.py new file mode 100644 index 0000000000..54a030b2c3 --- /dev/null +++ b/bc_obps/reporting/migrations/0050_remove_reportverification_other_facility_coordinates_and_more.py @@ -0,0 +1,125 @@ +# Generated by Django 5.0.10 on 2025-02-03 23:56 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ( + 'registration', + '0072_alter_historicaloptedinoperationdetail_meets_producing_gger_schedule_a1_regulated_product_and_more', + ), + ('reporting', '0049_alter_gcs_add_CEMS'), + ] + + operations = [ + migrations.RemoveField( + model_name='reportverification', + name='other_facility_coordinates', + ), + migrations.RemoveField( + model_name='reportverification', + name='other_facility_name', + ), + migrations.RemoveField( + model_name='reportverification', + name='visit_name', + ), + migrations.RemoveField( + model_name='reportverification', + name='visit_type', + ), + migrations.CreateModel( + name='ReportVerificationVisit', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True, null=True)), + ('updated_at', models.DateTimeField(blank=True, null=True)), + ('archived_at', models.DateTimeField(blank=True, null=True)), + ( + 'visit_name', + models.CharField( + db_comment='The name of the site visited (Facility X, Other, or None)', max_length=100 + ), + ), + ( + 'visit_type', + models.CharField( + blank=True, + choices=[('In person', 'In Person'), ('Virtual', 'Virtual')], + db_comment='The type of visit conducted (Virtual or In Person)', + max_length=10, + null=True, + ), + ), + ( + 'visit_coordinates', + models.CharField( + blank=True, + db_comment='Geographic location of an other facility visited', + max_length=100, + null=True, + ), + ), + ( + 'is_other_visit', + models.BooleanField( + db_comment='Flag to indicate the visit is an other facility visited', default=False + ), + ), + ( + 'archived_by', + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name='%(class)s_archived', + to='registration.user', + ), + ), + ( + 'created_by', + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name='%(class)s_created', + to='registration.user', + ), + ), + ( + 'report_verification', + models.ForeignKey( + db_comment='The report verification associated with this visit', + on_delete=django.db.models.deletion.CASCADE, + related_name='report_verification_visits', + to='reporting.reportverification', + ), + ), + ( + 'updated_by', + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name='%(class)s_updated', + to='registration.user', + ), + ), + ], + options={ + 'db_table': 'erc"."verification_visit', + 'db_table_comment': 'Table to store individual verification visit information', + }, + ), + migrations.AddConstraint( + model_name='reportverificationvisit', + constraint=models.CheckConstraint( + check=models.Q(('is_other_visit', False), ('visit_coordinates__isnull', False), _connector='OR'), + name='other_facility_must_have_coordinates', + violation_error_message='Coordinates must be provided for an other facility visit', + ), + ), + ] diff --git a/bc_obps/reporting/models/__init__.py b/bc_obps/reporting/models/__init__.py index f88b5d3244..2e0014bf9e 100644 --- a/bc_obps/reporting/models/__init__.py +++ b/bc_obps/reporting/models/__init__.py @@ -34,6 +34,7 @@ from .report_non_attributable_emissions import ReportNonAttributableEmissions from .report_product import ReportProduct from .report_verification import ReportVerification +from .report_verification_visit import ReportVerificationVisit from .report_attachment import ReportAttachment from .naics_regulatory_value import NaicsRegulatoryValue from .product_emission_intensity import ProductEmissionIntensity @@ -74,6 +75,7 @@ "ReportNewEntrantProduction", "ReportNewEntrantEmission", "ReportVerification", + "ReportVerificationVisit", "ReportAttachment", "NaicsRegulatoryValue", "ProductEmissionIntensity", diff --git a/bc_obps/reporting/models/report_verification.py b/bc_obps/reporting/models/report_verification.py index f749455bc1..31438eef16 100644 --- a/bc_obps/reporting/models/report_verification.py +++ b/bc_obps/reporting/models/report_verification.py @@ -52,36 +52,6 @@ class VerificationConclusion(models.TextChoices): db_comment="The conclusion of the verification", ) - visit_name = models.CharField( - max_length=100, db_comment="The name of the site visited (Facility X, Other, or None)" - ) - - class VisitType(models.TextChoices): - IN_PERSON = "In person" - VIRTUAL = "Virtual" - - visit_type = models.CharField( - max_length=10, - choices=VisitType.choices, - null=True, - blank=True, - db_comment="The type of visit conducted (Virtual or In Person)", - ) - - other_facility_name = models.CharField( - max_length=100, - null=True, - blank=True, - db_comment="Name of the other facility visited if 'Other' is selected", - ) - - other_facility_coordinates = models.CharField( - max_length=100, - null=True, - blank=True, - db_comment="Geographic location of the other facility visited", - ) - class Meta: db_table = 'erc"."report_verification' db_table_comment = "Table to store verification information associated with a report version" diff --git a/bc_obps/reporting/models/report_verification_visit.py b/bc_obps/reporting/models/report_verification_visit.py new file mode 100644 index 0000000000..b7996898d9 --- /dev/null +++ b/bc_obps/reporting/models/report_verification_visit.py @@ -0,0 +1,57 @@ +from django.db import models +from django.db.models import Q +from registration.models.time_stamped_model import TimeStampedModel +from reporting.models.report_verification import ReportVerification + + +class ReportVerificationVisit(TimeStampedModel): + """ + Model to store information about a verification visit for a report verification. + """ + + report_verification = models.ForeignKey( + ReportVerification, + on_delete=models.CASCADE, + related_name="report_verification_visits", + db_comment="The report verification associated with this visit", + ) + + visit_name = models.CharField( + max_length=100, db_comment="The name of the site visited (Facility X, Other, or None)" + ) + + class VisitType(models.TextChoices): + IN_PERSON = "In person" + VIRTUAL = "Virtual" + + visit_type = models.CharField( + max_length=10, + choices=VisitType.choices, + null=True, + blank=True, + db_comment="The type of visit conducted (Virtual or In Person)", + ) + + visit_coordinates = models.CharField( + max_length=100, + null=True, + blank=True, + db_comment="Geographic location of an other facility visited", + ) + + is_other_visit = models.BooleanField( + db_comment="Flag to indicate the visit is an other facility visited", + default=False, + ) + + class Meta: + db_table = 'erc"."verification_visit' + db_table_comment = "Table to store individual verification visit information" + app_label = 'reporting' + constraints = [ + models.CheckConstraint( + name="other_facility_must_have_coordinates", + check=Q(is_other_visit=False) | Q(visit_coordinates__isnull=False), + violation_error_message="Coordinates must be provided for an other facility visit", + ), + ] diff --git a/bc_obps/reporting/schema/facility_report.py b/bc_obps/reporting/schema/facility_report.py index bc99d32af3..f6a362e25e 100644 --- a/bc_obps/reporting/schema/facility_report.py +++ b/bc_obps/reporting/schema/facility_report.py @@ -20,7 +20,6 @@ class FacilityReportOut(ModelSchema): @staticmethod def resolve_facility(obj: FacilityReport) -> str: - print(obj.facility) return str(obj.facility) class Meta: diff --git a/bc_obps/reporting/schema/report_verification.py b/bc_obps/reporting/schema/report_verification.py index 0fc29f2b66..4e20e5e38a 100644 --- a/bc_obps/reporting/schema/report_verification.py +++ b/bc_obps/reporting/schema/report_verification.py @@ -1,10 +1,11 @@ -from typing import Optional +from typing import List, Optional from ninja import ModelSchema from pydantic import Field -from reporting.models import ReportVerification +from reporting.models import ReportVerification, ReportVerificationVisit -class BaseReportVerificationSchema(ModelSchema): + +class ReportVerificationBase(ModelSchema): """ Base schema for shared fields in ReportVerification schemas """ @@ -12,10 +13,6 @@ class BaseReportVerificationSchema(ModelSchema): verification_body_name: str accredited_by: str scope_of_verification: str - visit_name: str - visit_type: Optional[str] = Field(None) - other_facility_name: Optional[str] = Field(None) - other_facility_coordinates: Optional[str] = Field(None) threats_to_independence: bool verification_conclusion: str @@ -25,27 +22,48 @@ class Meta: 'verification_body_name', 'accredited_by', 'scope_of_verification', - 'visit_name', - 'visit_type', - 'other_facility_name', - 'other_facility_coordinates', 'threats_to_independence', 'verification_conclusion', ] -class ReportVerificationIn(BaseReportVerificationSchema): +class ReportVerificationVisitSchema(ModelSchema): + """ + Schema for ReportVerificationVisit model + """ + + visit_name: str + visit_type: Optional[str] = Field(None) + is_other_visit: bool + visit_coordinates: str + + class Meta: + model = ReportVerificationVisit + fields = [ + 'visit_name', + 'visit_type', + 'is_other_visit', + 'visit_coordinates', + ] + + +class ReportVerificationIn(ReportVerificationBase): """ Schema for the input of report verification data """ - pass + report_verification_visits: List[ReportVerificationVisitSchema] = Field(default_factory=list) + class Meta(ReportVerificationBase.Meta): + fields = ReportVerificationBase.Meta.fields -class ReportVerificationOut(BaseReportVerificationSchema): + +class ReportVerificationOut(ReportVerificationBase): """ Schema for the output of report verification data """ - class Meta(BaseReportVerificationSchema.Meta): - fields = BaseReportVerificationSchema.Meta.fields + ['report_version'] + report_verification_visits: List[ReportVerificationVisitSchema] = Field(default_factory=list) + + class Meta(ReportVerificationBase.Meta): + fields = ReportVerificationBase.Meta.fields + ['report_version'] diff --git a/bc_obps/reporting/service/report_verification_service.py b/bc_obps/reporting/service/report_verification_service.py index dfb821f4f3..2bbc8dbd62 100644 --- a/bc_obps/reporting/service/report_verification_service.py +++ b/bc_obps/reporting/service/report_verification_service.py @@ -1,12 +1,13 @@ from decimal import Decimal from django.db import transaction from reporting.models.report_verification import ReportVerification +from reporting.models.report_verification_visit import ReportVerificationVisit 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 +from reporting.schema.report_verification import ReportVerificationIn class ReportVerificationService: @@ -18,12 +19,13 @@ def get_report_verification_by_version_id( Retrieve a ReportVerification instance for a given report version ID. Args: - version_id: The report version ID + report_version_id: The report version ID Returns: ReportVerification instance """ - return ReportVerification.objects.get(report_version__id=report_version_id) + report_verification = ReportVerification.objects.get(report_version__id=report_version_id) + return report_verification @staticmethod @transaction.atomic @@ -39,24 +41,46 @@ def save_report_verification(version_id: int, data: ReportVerificationIn) -> Rep ReportVerification instance """ # Retrieve the associated report version + report_version = ReportVersion.objects.get(pk=version_id) + + # Prepare the defaults for the ReportVerification object data_defaults = { "verification_body_name": data.verification_body_name, "accredited_by": data.accredited_by, "scope_of_verification": data.scope_of_verification, "threats_to_independence": data.threats_to_independence, "verification_conclusion": data.verification_conclusion, - "visit_name": data.visit_name, - "visit_type": data.visit_type, - "other_facility_name": data.other_facility_name, - "other_facility_coordinates": data.other_facility_coordinates, } - report_version = ReportVersion.objects.get(pk=version_id) + # Update or create ReportVerification record - report_verification, created = ReportVerification.objects.update_or_create( + report_verification, _ = ReportVerification.objects.update_or_create( report_version=report_version, defaults=data_defaults, ) + # Process ReportVerificationVisit records + provided_visits = data.report_verification_visits + visit_ids_to_keep = [] + + for visit_data in provided_visits: + visit_defaults = { + "visit_type": visit_data.visit_type, + "is_other_visit": visit_data.is_other_visit, + "visit_coordinates": visit_data.visit_coordinates, + } + + visit, _ = ReportVerificationVisit.objects.update_or_create( + report_verification=report_verification, + visit_name=visit_data.visit_name, + defaults=visit_defaults, + ) + visit_ids_to_keep.append(visit.id) + + # Delete any visits not included in the current payload + ReportVerificationVisit.objects.filter(report_verification=report_verification).exclude( + id__in=visit_ids_to_keep + ).delete() + return report_verification @staticmethod diff --git a/bc_obps/reporting/tests/api/test_fuel.py b/bc_obps/reporting/tests/api/test_fuel.py index 0deb5a3e10..6da7d15b9b 100644 --- a/bc_obps/reporting/tests/api/test_fuel.py +++ b/bc_obps/reporting/tests/api/test_fuel.py @@ -17,7 +17,6 @@ def test_invalid_fuel(self): def test_returns_fuel_data(self): response = client.get('/api/reporting/fuel?fuel_name=Acetylene') - print(response.json()) assert response.status_code == 200 assert response.json().get('name') == 'Acetylene' assert response.json().get('classification') == 'Exempted Non-biomass' 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 c45756e01c..b548cb1e83 100644 --- a/bc_obps/reporting/tests/api/test_report_verification_api.py +++ b/bc_obps/reporting/tests/api/test_report_verification_api.py @@ -8,9 +8,23 @@ class TestSaveReportVerificationApi(CommonTestSetup): def setup_method(self): + # Create ReportVersion instance self.report_version = baker.make_recipe('reporting.tests.utils.report_version') - self.report_verification = baker.make_recipe('reporting.tests.utils.report_verification') + # Create ReportVerification instance associated with the ReportVersion + self.report_verification = baker.make_recipe( + 'reporting.tests.utils.report_verification', report_version=self.report_version + ) + + # Create and attach related ReportVerificationVisit instances + report_verification_visits = baker.make_recipe( + 'reporting.tests.utils.report_verification_visit', + report_verification=self.report_verification, + _quantity=2, + ) + self.report_verification.report_verification_visits.set(report_verification_visits) + + # Call parent setup and authorize user super().setup_method() TestUtils.authorize_current_user_as_operator_user(self, operator=self.report_version.report.operator) @@ -23,7 +37,7 @@ def test_returns_verification_data_for_report_version_id( self, mock_get_report_verification: MagicMock, ): - # Arrange: Mock report version and report verification data + # Arrange: Mock report verification data with associated visits mock_get_report_verification.return_value = self.report_verification # Act: Authorize user and perform GET request @@ -49,62 +63,13 @@ def test_returns_verification_data_for_report_version_id( assert response_json["scope_of_verification"] == self.report_verification.scope_of_verification assert response_json["threats_to_independence"] == self.report_verification.threats_to_independence assert response_json["verification_conclusion"] == self.report_verification.verification_conclusion - assert response_json["visit_name"] == self.report_verification.visit_name - assert response_json["visit_type"] == self.report_verification.visit_type - 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 + # Validate associated visits + assert len(response_json["report_verification_visits"]) == 2 + for visit_data in response_json["report_verification_visits"]: + assert "visit_name" in visit_data + assert "visit_type" in visit_data + assert "visit_coordinates" in visit_data """Tests for the save_report_verification endpoint.""" @@ -117,14 +82,21 @@ def test_returns_data_as_provided_by_the_service( payload = ReportVerificationIn( verification_body_name="Verifier Co.", accredited_by="ANAB", # AccreditedBy choices: "ANAB" or "SCC" - scope_of_verification="B.C. OBPS Annual Report", # ScopeOfVerification choices: "B.C. OBPS Annual Report"; "Supplementary Report"; "Corrected Report" + scope_of_verification="B.C. OBPS Annual Report", threats_to_independence=False, - verification_conclusion="Positive", # VerificationConclusion choices: "Positive", "Modified", "Negative" - visit_name="Site Visit 1", - visit_type="Virtual", # VisitType choices: "In person", "Virtual" - other_facility_name=None, - other_facility_coordinates=None, + verification_conclusion="Positive", + report_verification_visits=[ + { + "visit_name": visit.visit_name, + "visit_type": visit.visit_type, + "visit_coordinates": visit.visit_coordinates, + "is_other_visit": visit.is_other_visit, + } + for visit in self.report_verification.report_verification_visits.all() + ], ) + + # Prepare the mock response with expected data mock_response = ReportVerification( report_version=self.report_version, verification_body_name=payload.verification_body_name, @@ -132,11 +104,9 @@ def test_returns_data_as_provided_by_the_service( scope_of_verification=payload.scope_of_verification, threats_to_independence=payload.threats_to_independence, verification_conclusion=payload.verification_conclusion, - visit_name=payload.visit_name, - visit_type=payload.visit_type, - other_facility_name=payload.other_facility_name, - other_facility_coordinates=payload.other_facility_coordinates, ) + + # Set the mock return value for the service mock_save_report_verification.return_value = mock_response # Act: Authorize user and perform POST request @@ -164,7 +134,13 @@ def test_returns_data_as_provided_by_the_service( assert response_json["scope_of_verification"] == payload.scope_of_verification assert response_json["threats_to_independence"] == payload.threats_to_independence assert response_json["verification_conclusion"] == payload.verification_conclusion - assert response_json["visit_name"] == payload.visit_name - assert response_json["visit_type"] == payload.visit_type - assert response_json["other_facility_name"] == payload.other_facility_name - assert response_json["other_facility_coordinates"] == payload.other_facility_coordinates + + # Validate the saved visits in the response + assert len(response_json["report_verification_visits"]) == len(payload.report_verification_visits) + for i, visit_data in enumerate(response_json["report_verification_visits"]): + expected_visit = payload.report_verification_visits[i] + + assert visit_data["visit_name"] == expected_visit.visit_name + assert visit_data["visit_type"] == expected_visit.visit_type + assert visit_data["visit_coordinates"] == expected_visit.visit_coordinates + assert visit_data["is_other_visit"] == expected_visit.is_other_visit diff --git a/bc_obps/reporting/tests/models/test_report_verification.py b/bc_obps/reporting/tests/models/test_report_verification.py index 1dcd7b8f01..118c718686 100644 --- a/bc_obps/reporting/tests/models/test_report_verification.py +++ b/bc_obps/reporting/tests/models/test_report_verification.py @@ -21,8 +21,5 @@ def setUpTestData(cls): ("scope_of_verification", "scope of verification", None, None), ("threats_to_independence", "threats to independence", None, None), ("verification_conclusion", "verification conclusion", None, None), - ("visit_name", "visit name", None, None), - ("visit_type", "visit type", None, None), - ("other_facility_name", "other facility name", None, None), - ("other_facility_coordinates", "other facility coordinates", None, None), + ("report_verification_visits", "report verification visit", None, 0), ] 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 0dc169e506..11550de912 100644 --- a/bc_obps/reporting/tests/service/test_report_verification_service.py +++ b/bc_obps/reporting/tests/service/test_report_verification_service.py @@ -40,12 +40,6 @@ def test_get_report_verification_by_version_id_returns_correct_instance(self): self.assertEqual( retrieved_verification.verification_conclusion, self.report_verification.verification_conclusion ) - self.assertEqual(retrieved_verification.visit_name, self.report_verification.visit_name) - self.assertEqual(retrieved_verification.visit_type, self.report_verification.visit_type) - self.assertEqual(retrieved_verification.other_facility_name, self.report_verification.other_facility_name) - self.assertEqual( - retrieved_verification.other_facility_coordinates, self.report_verification.other_facility_coordinates - ) @patch("reporting.service.compliance_service.ComplianceService.get_emissions_attributable_for_reporting") @patch( @@ -133,7 +127,7 @@ def test_get_report_needs_verification_returns_false_for_reporting_operation_wit 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 + Test that the service returns false for report of Reporting_Operation with attributable emissions below the verification threshold """ # Arrange: Simulate a reporting operation @@ -163,10 +157,6 @@ def test_save_report_verification_saves_record(self): scope_of_verification="B.C. OBPS Annual Report", # ScopeOfVerification choices: "B.C. OBPS Annual Report"; "Supplementary Report"; "Corrected Report" threats_to_independence=False, verification_conclusion="Positive", # VerificationConclusion choices: "Positive", "Modified", "Negative" - visit_name="Site Visit 1", - visit_type="Virtual", # VisitType choices: "In person", "Virtual" - other_facility_name="Additional Facility", - other_facility_coordinates="45.4215,-75.6972", ) # Act: Call the service to save report verification data @@ -183,7 +173,3 @@ def test_save_report_verification_saves_record(self): self.assertEqual(report_verification.scope_of_verification, data.scope_of_verification) self.assertEqual(report_verification.threats_to_independence, data.threats_to_independence) self.assertEqual(report_verification.verification_conclusion, data.verification_conclusion) - self.assertEqual(report_verification.visit_name, data.visit_name) - self.assertEqual(report_verification.visit_type, data.visit_type) - self.assertEqual(report_verification.other_facility_name, data.other_facility_name) - self.assertEqual(report_verification.other_facility_coordinates, data.other_facility_coordinates) diff --git a/bc_obps/reporting/tests/utils/baker_recipes.py b/bc_obps/reporting/tests/utils/baker_recipes.py index 8522e9eb27..635421f293 100644 --- a/bc_obps/reporting/tests/utils/baker_recipes.py +++ b/bc_obps/reporting/tests/utils/baker_recipes.py @@ -28,6 +28,7 @@ from reporting.models.report_version import ReportVersion from reporting.models.facility_report import FacilityReport from reporting.models.report_verification import ReportVerification +from reporting.models.report_verification_visit import ReportVerificationVisit from registration.tests.utils.baker_recipes import operation, operator, facility, regulated_product from model_bakery.recipe import Recipe, foreign_key, seq @@ -61,6 +62,8 @@ def json_seq(json_key="generated_json", json_value="test json value", seq_value: facility_report = Recipe(FacilityReport, report_version=foreign_key(report_version), facility=foreign_key(facility)) report_operation = Recipe(ReportOperation, report_version=foreign_key(report_version)) +report_verification = Recipe(ReportVerification, report_version=foreign_key(report_version)) + configuration = Recipe( Configuration, # We make one config per week @@ -162,11 +165,17 @@ def json_seq(json_key="generated_json", json_value="test json value", seq_value: scope_of_verification=ReportVerification.ScopeOfVerification.BC_OBPS, threats_to_independence=False, verification_conclusion=ReportVerification.VerificationConclusion.POSITIVE, - visit_name="Default Site", - visit_type=ReportVerification.VisitType.IN_PERSON, - other_facility_name=None, - other_facility_coordinates=None, ) + +report_verification_visit = Recipe( + ReportVerificationVisit, + report_verification=foreign_key(report_verification), + visit_name="Default Visit Name", + visit_type=ReportVerificationVisit.VisitType.IN_PERSON, + visit_coordinates="", + is_other_visit=False, +) + report_additional_data = Recipe( ReportAdditionalData, report_version=foreign_key(report_version), diff --git a/bc_obps/service/tests/test_facility_report_service.py b/bc_obps/service/tests/test_facility_report_service.py index f9bf3b9a52..1034cc1b61 100644 --- a/bc_obps/service/tests/test_facility_report_service.py +++ b/bc_obps/service/tests/test_facility_report_service.py @@ -58,7 +58,7 @@ def test_returns_activity_id_list(): @staticmethod def test_saves_facility_report_form_data(): facility_report = baker.make_recipe('reporting.tests.utils.facility_report', facility_bcghgid='abc') - print(facility_report.facility_bcghgid) + data = FacilityReportIn( facility_name="CHANGED", facility_type=facility_report.facility_type, diff --git a/bc_obps/service/tests/test_report_version_service.py b/bc_obps/service/tests/test_report_version_service.py index 0d9bf51211..53f49b79a7 100644 --- a/bc_obps/service/tests/test_report_version_service.py +++ b/bc_obps/service/tests/test_report_version_service.py @@ -82,5 +82,6 @@ def test_report_version_cascading_models(self): "ReportOperationRepresentative", "ReportAdditionalData", "ReportVerification", + "ReportVerificationVisit", "ReportProductEmissionAllocation", } diff --git a/bciers/apps/reporting/src/app/components/verification/VerificationForm.tsx b/bciers/apps/reporting/src/app/components/verification/VerificationForm.tsx index d44824fe4e..262544c8e7 100644 --- a/bciers/apps/reporting/src/app/components/verification/VerificationForm.tsx +++ b/bciers/apps/reporting/src/app/components/verification/VerificationForm.tsx @@ -5,57 +5,79 @@ import MultiStepFormWithTaskList from "@bciers/components/form/MultiStepFormWith import { TaskListElement } from "@bciers/components/navigation/reportingTaskList/types"; import { IChangeEvent } from "@rjsf/core"; import { RJSFSchema } from "@rjsf/utils"; -import { useSearchParams } from "next/navigation"; import { baseUrlReports, cancelUrlReports, } from "@reporting/src/app/utils/constants"; import { actionHandler } from "@bciers/actions"; -import serializeSearchParams from "@bciers/utils/src/serializeSearchParams"; +import { lfoUiSchema } from "@reporting/src/data/jsonSchema/verification/verification"; +import { sfoUiSchema } from "@reporting/src/data/jsonSchema/verification/verification"; +import { handleVerificationData } from "@reporting/src/app/utils/verification/handleVerificationData"; +import { mergeVerificationData } from "@reporting/src/app/utils/verification/mergeVerificationData"; interface Props { version_id: number; + operationType: string; verificationSchema: RJSFSchema; - verificationUiSchema: RJSFSchema; initialData: any; taskListElements: TaskListElement[]; } export default function VerificationForm({ version_id, + operationType, verificationSchema, - verificationUiSchema, initialData, taskListElements, }: Props) { const [formData, setFormData] = useState(initialData); const [errors, setErrors] = useState(); - const searchParams = useSearchParams(); - const queryString = serializeSearchParams(searchParams); - const saveAndContinueUrl = `/reports/${version_id}/attachments${queryString}`; - const backUrl = `/reports/${version_id}/final-review${queryString}`; + const saveAndContinueUrl = `/reports/${version_id}/attachments`; + const backUrl = `/reports/${version_id}/compliance-summary`; + const verificationUiSchema = + operationType === "SFO" ? sfoUiSchema : lfoUiSchema; + + // ๐Ÿ› ๏ธ Function to handle form changes affecting ui schema const handleChange = (e: IChangeEvent) => { const updatedData = { ...e.formData }; - // Update the form state with the modified data + + // LFO;SFO visit_names handling logic + handleVerificationData(updatedData, operationType); + + // ๐Ÿ”„ Update the form data state with the modified data setFormData(updatedData); }; + // ๐Ÿ› ๏ธ Function to handle form submit const handleSubmit = async () => { + // ๐Ÿ“ท Clone formData as payload + const payload = { ...formData }; + + // โž• Update report_verification_visits property based on visit_types and visit_others + mergeVerificationData(payload); + + // ๐Ÿงผ Remove unnecessary properties from payload + delete payload.visit_names; + delete payload.visit_types; + delete payload.visit_others; + + // ๐Ÿš€ API variables const endpoint = `reporting/report-version/${version_id}/report-verification`; const method = "POST"; const pathToRevalidate = "reporting/reports"; - const response = await actionHandler(endpoint, method, pathToRevalidate, { - body: JSON.stringify(formData), + body: JSON.stringify(payload), }); + // ๐Ÿœ Check for errors if (response?.error) { setErrors([response.error]); return false; } + // โœ… Return Success setErrors(undefined); return true; }; @@ -75,6 +97,9 @@ export default function VerificationForm({ onSubmit={handleSubmit} errors={errors} continueUrl={saveAndContinueUrl} + formContext={{ + visit_types: formData?.visit_types, // set formContext to use in UiSchema + }} /> ); } diff --git a/bciers/apps/reporting/src/app/components/verification/VerificationPage.tsx b/bciers/apps/reporting/src/app/components/verification/VerificationPage.tsx index 4517ad0005..7ab307d94e 100644 --- a/bciers/apps/reporting/src/app/components/verification/VerificationPage.tsx +++ b/bciers/apps/reporting/src/app/components/verification/VerificationPage.tsx @@ -1,7 +1,6 @@ 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/jsonSchema/verification/verification"; import VerificationForm from "@reporting/src/app/components/verification/VerificationForm"; import { ActivePage, @@ -9,16 +8,35 @@ import { } from "@reporting/src/app/components/taskList/5_signOffSubmit"; import { HasReportVersion } from "@reporting/src/app/utils/defaultPageFactoryTypes"; import { getReportNeedsVerification } from "@reporting/src/app/utils/getReportNeedsVerification"; +import { getReportingOperation } from "@reporting/src/app/utils/getReportingOperation"; +import { extendVerificationData } from "@reporting/src/app/utils/verification/extendVerificationData"; +// import { verificationSchema } from "@reporting/src/data/jsonSchema/verification/verification"; export default async function VerificationPage({ version_id, }: HasReportVersion) { - // Fetch initial form data - const initialData = await getReportVerification(version_id); - // Fetch the list of facilities associated with the specified version ID + // Determine operationType based on reportOperation + // ๐Ÿš€ Fetch the operation associated with the specified version ID + const reportOperation = await getReportingOperation(version_id); + const operationType = + reportOperation && + reportOperation.report_operation.operation_type === + "Single Facility Operation" + ? "SFO" + : "LFO"; + + // ๐Ÿš€ Fetch initial form data + const initialData = (await getReportVerification(version_id)) || {}; + const transformedData = extendVerificationData(initialData); + + // ๐Ÿš€ Fetch the list of facilities associated with the specified version ID const facilityList = await getReportFacilityList(version_id); - // Create schema with dynamic facility list - const verificationSchema = createVerificationSchema(facilityList.facilities); + + // Create schema with dynamic facility list for operation type + const verificationSchema = createVerificationSchema( + facilityList.facilities, + operationType, + ); //๐Ÿ” Check if reports need verification const needsVerification = await getReportNeedsVerification(version_id); @@ -27,14 +45,15 @@ export default async function VerificationPage({ ActivePage.Verification, needsVerification, ); + // Render the verification form return ( <> diff --git a/bciers/apps/reporting/src/app/components/verification/createVerificationSchema.ts b/bciers/apps/reporting/src/app/components/verification/createVerificationSchema.ts index 765a27cdf9..fae035ed1d 100644 --- a/bciers/apps/reporting/src/app/components/verification/createVerificationSchema.ts +++ b/bciers/apps/reporting/src/app/components/verification/createVerificationSchema.ts @@ -1,15 +1,19 @@ import { RJSFSchema } from "@rjsf/utils"; -import { verificationSchema } from "@reporting/src/data/jsonSchema/verification/verification"; +import { lfoSchema } from "@reporting/src/data/jsonSchema/verification/verification"; +import { sfoSchema } from "@reporting/src/data/jsonSchema/verification/verification"; -export const createVerificationSchema = (facilities: string[]): RJSFSchema => { - // Retrieve a local copy of the base verification schema based - const localSchema = { ...verificationSchema }; +export const createVerificationSchema = ( + facilities: string[], + schemaType: "SFO" | "LFO", +): RJSFSchema => { + // Determine the schema based on the schemaType + const localSchema: RJSFSchema = + schemaType === "SFO" ? { ...sfoSchema } : { ...lfoSchema }; + const defaultVisistValues = ["None", "Other"]; - // Dynamically populate the "visited_facilities" field's enum with the facilities - (localSchema.properties?.visit_name as any).enum = [ + (localSchema.properties?.visit_names as any).items.enum = [ + ...defaultVisistValues, ...facilities, - "Other", - "None", ]; // Return the customized schema. diff --git a/bciers/apps/reporting/src/app/utils/verification/extendVerificationData.test.ts b/bciers/apps/reporting/src/app/utils/verification/extendVerificationData.test.ts new file mode 100644 index 0000000000..5bc6379c4e --- /dev/null +++ b/bciers/apps/reporting/src/app/utils/verification/extendVerificationData.test.ts @@ -0,0 +1,81 @@ +import { describe, it, expect } from "vitest"; +import { extendVerificationData } from "@reporting/src/app/utils/verification/extendVerificationData"; + +describe("extendVerificationData", () => { + it("should handle an empty initial data object", () => { + const result = extendVerificationData({}); + expect(result.visit_names).toEqual([]); + expect(result.visit_types).toEqual([]); + expect(result.visit_others).toEqual([{}]); + }); + + it("should correctly map visit_names from report_verification_visits", () => { + const input = { + report_verification_visits: [ + { visit_name: "Facility A", is_other_visit: false }, + { visit_name: "Facility B", is_other_visit: false }, + ], + }; + const result = extendVerificationData(input); + expect(result.visit_names).toEqual(["Facility A", "Facility B"]); + }); + + it("should add 'Other' to visit_names if any visit has is_other_visit true", () => { + const input = { + report_verification_visits: [ + { visit_name: "Facility A", is_other_visit: false }, + { visit_name: "Custom Location", is_other_visit: true }, + ], + }; + const result = extendVerificationData(input); + expect(result.visit_names).toContain("Other"); + }); + + it("should correctly map visit_types excluding 'None' and other visits", () => { + const input = { + report_verification_visits: [ + { + visit_name: "Facility A", + visit_type: "Type 1", + is_other_visit: false, + }, + { visit_name: "None", visit_type: "Type 2", is_other_visit: false }, + ], + }; + const result = extendVerificationData(input); + expect(result.visit_types).toEqual([ + { visit_name: "Facility A", visit_type: "Type 1" }, + ]); + }); + + it("should populate visit_others correctly when is_other_visit is true", () => { + const input = { + report_verification_visits: [ + { + visit_name: "Custom Location", + visit_coordinates: "123,456", + visit_type: "Special", + is_other_visit: true, + }, + ], + }; + const result = extendVerificationData(input); + expect(result.visit_others).toEqual([ + { + visit_name: "Custom Location", + visit_coordinates: "123,456", + visit_type: "Special", + }, + ]); + }); + + it("should return visit_others as [{}] when no other visits exist", () => { + const input = { + report_verification_visits: [ + { visit_name: "Facility A", is_other_visit: false }, + ], + }; + const result = extendVerificationData(input); + expect(result.visit_others).toEqual([{}]); + }); +}); diff --git a/bciers/apps/reporting/src/app/utils/verification/extendVerificationData.ts b/bciers/apps/reporting/src/app/utils/verification/extendVerificationData.ts new file mode 100644 index 0000000000..550a3edd82 --- /dev/null +++ b/bciers/apps/reporting/src/app/utils/verification/extendVerificationData.ts @@ -0,0 +1,48 @@ +// ๐Ÿ› ๏ธ Function to extend the data with additional properties for the rjsf schema +export function extendVerificationData(initialData: any) { + // Ensure report_verification_visits is always an array + const visits = initialData.report_verification_visits || []; + + // Add the visit_names property + initialData.visit_names = visits + .filter((visit: { is_other_visit: boolean }) => !visit.is_other_visit) + .map((visit: { visit_name: string }) => visit.visit_name); + + if ( + visits.some((visit: { is_other_visit: boolean }) => visit.is_other_visit) + ) { + initialData.visit_names.push("Other"); + } + + // Add the visit_types property + initialData.visit_types = visits + .filter( + (visit: { is_other_visit: boolean; visit_name: string }) => + !visit.is_other_visit && visit.visit_name !== "None", + ) + .map((visit: { visit_name: string; visit_type: string }) => ({ + visit_name: visit.visit_name, + visit_type: visit.visit_type, + })); + + // Add the visit_others property + initialData.visit_others = visits.some( + (visit: { is_other_visit: boolean }) => visit.is_other_visit, + ) + ? visits + .filter((visit: { is_other_visit: boolean }) => visit.is_other_visit) + .map( + (visit: { + visit_name: string; + visit_coordinates: string; + visit_type: string; + }) => ({ + visit_name: visit.visit_name, + visit_coordinates: visit.visit_coordinates, + visit_type: visit.visit_type, + }), + ) + : [{}]; + + return initialData; +} diff --git a/bciers/apps/reporting/src/app/utils/verification/handleVerificationData.test.ts b/bciers/apps/reporting/src/app/utils/verification/handleVerificationData.test.ts new file mode 100644 index 0000000000..0fdbdabeed --- /dev/null +++ b/bciers/apps/reporting/src/app/utils/verification/handleVerificationData.test.ts @@ -0,0 +1,91 @@ +import { describe, it, expect } from "vitest"; +import { handleVerificationData } from "@reporting/src/app/utils/verification/handleVerificationData"; + +describe("handleVerificationData", () => { + it("should remove 'None' if other selections are made", () => { + const data = { + visit_names: ["None", "Facility 1"], + visit_types: [], + visit_others: [], + }; + const result = handleVerificationData(data, "default"); + expect(result.visit_names).toEqual(["Facility 1"]); + }); + + it("should lock to 'None' and clear other fields if only 'None' is selected", () => { + const data = { + visit_names: ["None"], + visit_types: [{ visit_name: "Old" }], + visit_others: [{ key: "value" }], + }; + const result = handleVerificationData(data, "default"); + expect(result.visit_names).toEqual(["None"]); + expect(result.visit_types).toEqual([]); + expect(result.visit_others).toEqual([{}]); + }); + + it("should enforce maxItem=1 for 'SFO' operation type", () => { + const data = { + visit_names: ["Facility 1", "Facility 2"], + visit_types: [], + visit_others: [], + }; + const result = handleVerificationData(data, "SFO"); + expect(result.visit_names).toEqual(["Facility 2"]); // Last selected should be retained + }); + + it("should update visit_types based on visit_names", () => { + const data = { + visit_names: ["Facility 1", "Facility 2"], + visit_types: [], + visit_others: [], + }; + const result = handleVerificationData(data, "default"); + expect(result.visit_types).toEqual([ + { visit_name: "Facility 1", visit_type: "" }, + { visit_name: "Facility 2", visit_type: "" }, + ]); + }); + + it("should preserve existing visit_types if present", () => { + const data = { + visit_names: ["Facility 1"], + visit_types: [{ visit_name: "Facility 1", visit_type: "In-Person" }], + visit_others: [], + }; + const result = handleVerificationData(data, "default"); + expect(result.visit_types).toEqual([ + { visit_name: "Facility 1", visit_type: "In-Person" }, + ]); + }); + + it("should set visit_types to empty if only 'None' is selected", () => { + const data = { + visit_names: ["None"], + visit_types: [{ visit_name: "Facility 1" }], + visit_others: [], + }; + const result = handleVerificationData(data, "default"); + expect(result.visit_types).toEqual([]); + }); + + it("should clear visit_others if 'Other' is not selected", () => { + const data = { + visit_names: ["Facility 1"], + visit_types: [], + visit_others: [{ visit_name: "Other Visit" }], + }; + const result = handleVerificationData(data, "default"); + expect(result.visit_others).toEqual([{}]); + }); + + it("should retain visit_others if 'Other' is selected", () => { + const data = { + visit_names: ["Other"], + visit_types: [], + visit_others: [{ visit_name: "Other Visit" }], + }; + const result = handleVerificationData(data, "default"); + expect(result.visit_others).toEqual([{ visit_name: "Other Visit" }]); + }); +}); diff --git a/bciers/apps/reporting/src/app/utils/verification/handleVerificationData.ts b/bciers/apps/reporting/src/app/utils/verification/handleVerificationData.ts new file mode 100644 index 0000000000..8105e92d9a --- /dev/null +++ b/bciers/apps/reporting/src/app/utils/verification/handleVerificationData.ts @@ -0,0 +1,56 @@ +export function handleVerificationData( + updatedData: any, + operationType: string, +) { + let selectedValues = updatedData.visit_names; + + if (selectedValues.includes("None")) { + if (selectedValues.length > 1) { + // If "None" is the last selected item, clear everything + if (selectedValues[selectedValues.length - 1] === "None") { + updatedData.visit_names = ["None"]; + updatedData.visit_types = []; + updatedData.visit_others = [{}]; + return updatedData; + } else { + // Otherwise, remove "None" + updatedData.visit_names = selectedValues.filter( + (value: string) => value !== "None", + ); + } + } else { + // If only "None" is selected, lock it and clear other fields + updatedData.visit_names = ["None"]; + updatedData.visit_types = []; + updatedData.visit_others = [{}]; + return updatedData; + } + } + + if (operationType === "SFO" && selectedValues.length > 1) { + // Ensure "SFO" visit_names maxItem=1 + const lastSelected = selectedValues[selectedValues.length - 1]; + + // Set the last selected item as the only value for visit_names + updatedData.visit_names = [lastSelected]; + } + + // Update `visit_types` for each facility except "Other" and "None" + updatedData.visit_types = updatedData.visit_names + .filter( + (visit_name: string) => visit_name !== "Other" && visit_name !== "None", + ) + .map((visit_name: string) => { + const existingVisitType = updatedData.visit_types?.find( + (item: { visit_name: string }) => item.visit_name === visit_name, + ); + return existingVisitType || { visit_name, visit_type: "" }; + }); + + // Clear visit_others if "Other" is not selected + if (!updatedData.visit_names.includes("Other")) { + updatedData.visit_others = [{}]; + } + + return updatedData; +} diff --git a/bciers/apps/reporting/src/app/utils/verification/mergeVerificationData.ts b/bciers/apps/reporting/src/app/utils/verification/mergeVerificationData.ts new file mode 100644 index 0000000000..226722ac3c --- /dev/null +++ b/bciers/apps/reporting/src/app/utils/verification/mergeVerificationData.ts @@ -0,0 +1,46 @@ +// ๐Ÿ› ๏ธ Function to update the report_verification_visits property for POST to the API +export function mergeVerificationData(formData: any): void { + // Initialize the report_verification_visits array + formData.report_verification_visits = []; + + // Check if "None" is selected in visit_names + if (formData.visit_names.includes("None")) { + formData.report_verification_visits = [ + { + visit_name: "None", + is_other_visit: false, + visit_coordinates: "", + visit_type: "", + }, + ]; + return; // Exit early as "None" overrides all other data + } + + // Handle visit_types and visit_others, filtering out empty visit_names during mapping + const visits = [ + ...formData.visit_types.map((type: any) => ({ + visit_name: type.visit_name || "", + visit_type: type.visit_type || "", + is_other_visit: false, + visit_coordinates: "", // Default for non-other visits + })), + ...formData.visit_others + .map((other: any) => ({ + visit_name: other.visit_name || "", + visit_type: other.visit_type || "", + is_other_visit: true, + visit_coordinates: other.visit_coordinates || "", + })) + .filter((other: any) => other.visit_name !== ""), // Filter out items with empty visit_name + ]; + + // Assign the filtered visits to the report_verification_visits array + formData.report_verification_visits = visits.filter((visit: any) => { + return ( + visit.visit_name !== "" || // Remove empty visit_name + visit.visit_type !== "" || // Remove empty visit_type + visit.visit_coordinates !== "" || // Remove empty visit_coordinates + visit.is_other_visit !== false // Remove false is_other_visit + ); + }); +} diff --git a/bciers/apps/reporting/src/data/jsonSchema/verification/verification.ts b/bciers/apps/reporting/src/data/jsonSchema/verification/verification.ts deleted file mode 100644 index ea7b3269ad..0000000000 --- a/bciers/apps/reporting/src/data/jsonSchema/verification/verification.ts +++ /dev/null @@ -1,183 +0,0 @@ -import { RJSFSchema } from "@rjsf/utils"; -import { - FieldTemplate, - TitleOnlyFieldTemplate, -} from "@bciers/components/form/fields"; -import { attachmentNote } from "./verificationText"; - -export const verificationSchema: RJSFSchema = { - type: "object", - title: "Verification", - required: [ - "verification_body_name", - "accredited_by", - "scope_of_verification", - "visit_name", - "threats_to_independence", - "verification_conclusion", - ], - properties: { - verification_body_name: { - title: "Verification body name", - type: "string", - }, - accredited_by: { - title: "Accredited by", - type: "string", - enum: ["ANAB", "SCC"], - }, - scope_of_verification: { - title: "Scope of verification", - type: "string", - enum: [ - "B.C. OBPS Annual Report", - "Supplementary Report", - "Corrected Report", - ], - }, - visit_name: { - title: "Sites visited", - type: "string", - enum: ["Facility X", "Other", "None"], // modified in components/verification/createVerificationSchema.ts - }, - threats_to_independence: { - title: "Were there any threats to independence noted", - type: "boolean", - }, - verification_conclusion: { - title: "Verification conclusion", - type: "string", - enum: ["Positive", "Modified", "Negative"], - }, - verification_note: { - //Not an actual field in the db - this is just to make the form look like the wireframes - type: "object", - readOnly: true, - }, - }, - dependencies: { - visit_name: { - oneOf: [ - { - properties: { - visit_name: { - type: "string", - minItems: 1, - not: { - enum: ["Other", "None"], - }, - }, - visit_type: { - type: "string", - title: "Type of site visit", - enum: ["Virtual", "In person"], - }, - threats_to_independence: { - type: "boolean", - }, - verification_conclusion: { - type: "string", - enum: ["Positive", "Modified", "Negative"], - }, - }, - required: [ - "visit_type", - "threats_to_independence", - "verification_conclusion", - ], - }, - { - properties: { - visit_name: { - enum: ["Other"], - }, - other_facility_name: { - type: "string", - title: "Please indicate the site visited", - }, - other_facility_coordinates: { - type: "string", - title: "Geographic coordinates of site", - }, - visit_type: { - type: "string", - title: "Type of site visit", - enum: ["Virtual", "In person"], - }, - threats_to_independence: { - type: "boolean", - }, - verification_conclusion: { - type: "string", - enum: ["Positive", "Modified", "Negative"], - }, - }, - required: [ - "other_facility_name", - "other_facility_coordinates", - "visit_type", - "threats_to_independence", - "verification_conclusion", - ], - }, - { - properties: { - visit_name: { - enum: ["None"], - }, - threats_to_independence: { - type: "boolean", - }, - verification_conclusion: { - type: "string", - enum: ["Positive", "Modified", "Negative"], - }, - }, - required: ["threats_to_independence", "verification_conclusion"], - }, - ], - }, - }, -}; - -export const verificationUiSchema = { - "ui:FieldTemplate": FieldTemplate, - "ui:classNames": "form-heading-label", - "ui:order": [ - "verification_body_name", - "accredited_by", - "scope_of_verification", - "visit_name", - "other_facility_name", - "other_facility_coordinates", - "visit_type", - "threats_to_independence", - "verification_conclusion", - "verification_note", - ], - verification_body_name: { - "ui:placeholder": "Enter verification body name", - }, - accredited_by: { - "ui:placeholder": "Select accrediting body", - }, - scope_of_verification: { - "ui:placeholder": "Select scope of verification", - }, - visit_name: { - "ui:placeholder": "Select site visited", - }, - visit_type: { - "ui:widget": "RadioWidget", - }, - threats_to_independence: { - "ui:widget": "RadioWidget", - }, - verification_conclusion: { - "ui:placeholder": "Select verification conclusion", - }, - verification_note: { - "ui:FieldTemplate": TitleOnlyFieldTemplate, - "ui:title": attachmentNote, - }, -}; diff --git a/bciers/apps/reporting/src/data/jsonSchema/verification/verification.tsx b/bciers/apps/reporting/src/data/jsonSchema/verification/verification.tsx new file mode 100644 index 0000000000..35ea0ee68c --- /dev/null +++ b/bciers/apps/reporting/src/data/jsonSchema/verification/verification.tsx @@ -0,0 +1,451 @@ +import { RJSFSchema, UiSchema, FieldTemplateProps } from "@rjsf/utils"; +import { + FieldTemplate, + TitleOnlyFieldTemplate, +} from "@bciers/components/form/fields"; +import { attachmentNote } from "./verificationText"; + +/** + * Shared schema properties for SFO and LFO schemas + */ +const sharedSchemaProperties: RJSFSchema["properties"] = { + verification_body_name: { + title: "Verification body name", + type: "string", + }, + accredited_by: { + title: "Accredited by", + type: "string", + enum: ["ANAB", "SCC"], + }, + scope_of_verification: { + title: "Scope of verification", + type: "string", + enum: [ + "B.C. OBPS Annual Report", + "Supplementary Report", + "Corrected Report", + ], + }, + visit_names: { + type: "array", + title: "Site(s) visited", + uniqueItems: true, + items: { + type: "string", + enum: ["Facility X", "Other", "None"], // modified in components/verification/createVerificationSchema.ts + }, + }, + visit_types: { + type: "array", + items: { + $ref: "#/definitions/visitTypeItem", + }, + }, + threats_to_independence: { + title: "Were there any threats to independence noted", + type: "boolean", + }, + verification_conclusion: { + title: "Verification conclusion", + type: "string", + enum: ["Positive", "Modified", "Negative"], + }, + verification_note: { + //Not an actual field in the db - this is just to make the form look like the wireframes + type: "object", + readOnly: true, + }, +}; + +// Shared schema definitions +export const visitTypeItemDefinition: RJSFSchema = { + type: "object", + required: ["visit_type"], + properties: { + visit_name: { + title: "Visit Name", + type: "string", + readOnly: true, + }, + visit_type: { + type: "string", + enum: ["Virtual", "In person"], + }, + }, +}; +/** + * Shared required fields for SFO and LFO schemas + */ +const sharedRequiredFields = [ + "verification_body_name", + "accredited_by", + "scope_of_verification", + "visit_names", + "threats_to_independence", + "verification_conclusion", +]; +/** + * Shared ui schema properties for SFO and LFO schemas + */ +const sharedUiSchema: UiSchema = { + "ui:FieldTemplate": FieldTemplate, + "ui:classNames": "form-heading-label", + verification_body_name: { + "ui:placeholder": "Enter verification body name", + }, + accredited_by: { + "ui:placeholder": "Select accrediting body", + }, + scope_of_verification: { + "ui:placeholder": "Select scope of verification", + }, + threats_to_independence: { + "ui:widget": "RadioWidget", + }, + verification_conclusion: { + "ui:placeholder": "Select verification conclusion", + }, + verification_note: { + "ui:FieldTemplate": TitleOnlyFieldTemplate, + "ui:title": attachmentNote, + }, +}; +/** + * Shared ui:order for SFO and LFO schemas + */ +const sharedUIOrder = [ + "verification_body_name", + "accredited_by", + "scope_of_verification", + "visit_names", + "visit_types", + "visit_others", + "threats_to_independence", + "verification_conclusion", + "verification_note", +]; +/** + * Function to fetch the associated visit_name for a visit_type based on the field ID and the form context. + * @param {string} fieldId - ID of the field (e.g., "root_visit_types_0_visit_type") + * @param {any} context - Context of the form, containing the visit_types array + * @returns {string | null} - Returns the visit_name or null + */ +const getAssociatedVisitName = ( + fieldId: string, + context: any, +): string | null => { + try { + // Match for visit_types ID pattern + const visitTypesMatch = fieldId.match(/root_visit_types_(\d+)_visit_type/); + if (visitTypesMatch) { + const visitIndex = Number(visitTypesMatch[1]); // Extract index + + // Ensure context is valid and contains visit_types array + if (Array.isArray(context?.visit_types)) { + const visitTypeData = context.visit_types[visitIndex]; + + // Return the associated visit_name + return visitTypeData?.visit_name || null; + } + } + + // If no match or invalid context + return null; + } catch (error) { + return null; + } +}; +/** + * Custom Field Template for displaying a dynamic label and input field inline + * @param {FieldTemplateProps} props - Props including id, classNames, children, and formContext + * @returns {JSX.Element} - Rendered label and input field + */ + +const DynamicLabelVisitType: React.FC = ({ + id, + classNames, + children, + formContext, + rawErrors = [], +}: FieldTemplateProps): JSX.Element => { + const visitName = getAssociatedVisitName(id, formContext); + + return ( +
+
+
+ +
+ +
+ {children} +
+ + {/* Error display to the side */} + {rawErrors.length > 0 && ( +
+
+ + + +
+ Required field +
+ )} +
+
+ ); +}; +/** + * SFO Verfication Form schema + */ +export const sfoSchema: RJSFSchema = { + type: "object", + title: "Verification", + required: sharedRequiredFields, + properties: { + ...sharedSchemaProperties, + }, + dependencies: { + visit_names: { + oneOf: [ + // Rule when "None" is selected + { + properties: { + visit_names: { + type: "array", + items: { + type: "string", + enum: ["None"], // Only allow "None" + }, + maxItems: 1, + minItems: 1, + }, + }, + required: ["visit_names"], + }, + // Rule when "Other" is selected + { + properties: { + visit_names: { + type: "array", + contains: { const: "Other" }, + }, + visit_others: { + title: "Other Visit", + type: "array", + minItems: 1, + maxItems: 1, + items: { + type: "object", + required: ["visit_name", "visit_coordinates", "visit_type"], + properties: { + visit_name: { + title: "Name", + type: "string", + }, + visit_coordinates: { + title: "Coordinates", + type: "string", + }, + visit_type: { + title: "Visit Type", + type: "string", + enum: ["Virtual", "In person"], + }, + }, + }, + }, + }, + required: ["visit_others"], + }, + ], + }, + }, + definitions: { + visitTypeItem: visitTypeItemDefinition, + }, +}; + +/** + * SFO Verfication Form ui schemas + */ +export const sfoUiSchema: UiSchema = { + "ui:FieldTemplate": FieldTemplate, + "ui:classNames": "form-heading-label", + "ui:order": sharedUIOrder, + ...sharedUiSchema, + visit_names: { + "ui:widget": "MultiSelectWidget", + "ui:placeholder": "Select site visited", + }, + visit_types: { + "ui:FieldTemplate": FieldTemplate, + "ui:options": { + addable: false, + removable: false, + label: false, + }, + items: { + "ui:order": ["visit_name", "visit_type"], + visit_name: { + "ui:widget": "hidden", + }, + visit_type: { + "ui:title": "Type of site visit", + "ui:widget": "RadioWidget", + }, + }, + }, + visit_others: { + "ui:FieldTemplate": FieldTemplate, + "ui:options": { + addable: false, + }, + items: { + visit_name: { + "ui:placeholder": "Enter visit name", + }, + visit_coordinates: { + "ui:placeholder": "Enter coordinates", + }, + visit_type: { + "ui:widget": "RadioWidget", + }, + }, + }, +}; + +/** + * LFO Verfication Form schema + */ +export const lfoSchema: RJSFSchema = { + type: "object", + title: "Verification", + required: sharedRequiredFields, + properties: { + ...sharedSchemaProperties, + }, + dependencies: { + visit_names: { + oneOf: [ + // Rule when "None" is selected + { + properties: { + visit_names: { + type: "array", + items: { + type: "string", + enum: ["None"], // Only allow "None" + }, + maxItems: 1, + minItems: 1, + }, + }, + required: ["visit_names"], + }, + // Rule when "Other" is selected + { + properties: { + visit_names: { + type: "array", + contains: { const: "Other" }, + }, + visit_others: { + title: "Other Visit(s)", + type: "array", + minItems: 1, + items: { + type: "object", + required: ["visit_name", "visit_coordinates", "visit_type"], + properties: { + visit_name: { + title: "Name", + type: "string", + }, + visit_coordinates: { + title: "Coordinates", + type: "string", + }, + visit_type: { + title: "Visit Type", + type: "string", + enum: ["Virtual", "In person"], + }, + }, + }, + }, + }, + }, + ], + }, + }, + definitions: { + visitTypeItem: visitTypeItemDefinition, + }, +}; + +/** + * LFO Verfication Form ui schemas + */ +export const lfoUiSchema: UiSchema = { + "ui:FieldTemplate": FieldTemplate, + "ui:classNames": "form-heading-label", + "ui:order": sharedUIOrder, + ...sharedUiSchema, + visit_names: { + "ui:widget": "MultiSelectWidget", + "ui:placeholder": "Select site visited", + }, + visit_types: { + "ui:FieldTemplate": FieldTemplate, + "ui:options": { + addable: false, + removable: false, + label: false, + }, + items: { + "ui:order": ["visit_name", "visit_type"], + visit_name: { + "ui:widget": "hidden", + }, + visit_type: { + "ui:FieldTemplate": DynamicLabelVisitType, + "ui:widget": "RadioWidget", + }, + }, + }, + visit_others: { + "ui:FieldTemplate": FieldTemplate, + "ui:options": { + arrayAddLabel: "Add Other Visit", + addable: true, + }, + items: { + visit_name: { + "ui:placeholder": "Enter visit name", + }, + visit_coordinates: { + "ui:placeholder": "Enter coordinates", + }, + visit_type: { + "ui:widget": "RadioWidget", + }, + }, + }, +}; diff --git a/bciers/apps/reporting/src/tests/components/facility/FacilityEmissionAllocationPage.test.tsx b/bciers/apps/reporting/src/tests/components/facility/FacilityEmissionAllocationPage.test.tsx index f249a797a0..3c1e0112ee 100644 --- a/bciers/apps/reporting/src/tests/components/facility/FacilityEmissionAllocationPage.test.tsx +++ b/bciers/apps/reporting/src/tests/components/facility/FacilityEmissionAllocationPage.test.tsx @@ -167,7 +167,7 @@ describe("The FacilityEmissionAllocationPage component", () => { (getOrderedActivities as ReturnType).mockResolvedValueOnce( orderedActivities, ); - // Mock the returned value for `createVerificationSchema` + // Mock the returned value for `getEmissionAllocations` (getEmissionAllocations as ReturnType).mockReturnValueOnce( emissionAllocations, ); diff --git a/bciers/apps/reporting/src/tests/components/verification/VerificationForm.test.tsx b/bciers/apps/reporting/src/tests/components/verification/VerificationForm.test.tsx index 852008a9b1..bbe94f25d7 100644 --- a/bciers/apps/reporting/src/tests/components/verification/VerificationForm.test.tsx +++ b/bciers/apps/reporting/src/tests/components/verification/VerificationForm.test.tsx @@ -1,14 +1,12 @@ -import { render, screen, waitFor, fireEvent } from "@testing-library/react"; -import { actionHandler, useRouter } from "@bciers/testConfig/mocks"; +import { render } from "@testing-library/react"; +import { useRouter } from "@bciers/testConfig/mocks"; import { - verificationSchema, - verificationUiSchema, + sfoUiSchema, + lfoUiSchema, } 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"; -import { fillMandatoryFields } from "@bciers/testConfig/helpers/fillMandatoryFields"; - // โœจ Mocks const mockRouterPush = vi.fn(); useRouter.mockReturnValue({ @@ -30,6 +28,11 @@ const config = { mockRouteSubmit: `/reports/3/attachments?`, }; +// Mock operationType +let mockOperationType = "SFO"; +const getUiSchema = (operationType: string) => + operationType === "SFO" ? sfoUiSchema : lfoUiSchema; + // ๐Ÿท Common Fields const commonMandatoryFormFields = [ { @@ -37,18 +40,18 @@ const commonMandatoryFormFields = [ type: "text", key: "verification_body_name", }, - { label: "Accredited by", type: "combobox", key: "accredited_by" }, + // { label: "Accredited by", type: "combobox", key: "accredited_by" }, { label: "Scope of verification", type: "combobox", key: "scope_of_verification", }, - { label: "Sites visited", type: "combobox", key: "visit_name" }, - { - label: "Were there any threats to independence noted", - type: "radio", - key: "threats_to_independence", - }, + // { label: "Sites visited", type: "combobox", key: "visit_name" }, + // { + // label: "Were there any threats to independence noted", + // type: "radio", + // key: "threats_to_independence", + // }, { label: "Verification conclusion", type: "combobox", @@ -56,210 +59,39 @@ const commonMandatoryFormFields = [ }, ]; -const specificMandatoryFields = { - facility: [{ label: "Type of site visit", type: "radio", key: "visit_type" }], - other: [ - { label: "Type of site visit", type: "radio", key: "visit_type" }, - { - label: "Please indicate the site visited", - type: "text", - key: "other_facility_name", - }, - { - label: "Geographic coordinates of site", - type: "text", - key: "other_facility_coordinates", - }, - ], -}; - -const formDataSets = { - default: { - verification_body_name: "Test", - accredited_by: "SCC", - scope_of_verification: "Supplementary Report", - visit_name: "None", - threats_to_independence: "No", - verification_conclusion: "Positive", - }, - facility: { - verification_body_name: "Test", - accredited_by: "SCC", - scope_of_verification: "Supplementary Report", - visit_name: "Facility X", - visit_type: "Virtual", - threats_to_independence: "No", - verification_conclusion: "Positive", - }, - other: { - verification_body_name: "Test", - accredited_by: "SCC", - scope_of_verification: "Supplementary Report", - visit_name: "Other", - visit_type: "Virtual", - other_facility_name: "Other Facility", - other_facility_coordinates: "Lat 41; Long 35", - threats_to_independence: "No", - verification_conclusion: "Modified", - }, -}; - // โ›๏ธ Helper function to render the form -const renderVerificationForm = () => { +const renderVerificationForm = (operationType: string) => { + const verificationSchema = getUiSchema(operationType); render( , ); }; -// โ›๏ธ Helper function to simulate form POST submission and assert the result -const submitFormAndAssert = async ( - fields: { label: string; type: string; key: string }[], - data: Record, -) => { - actionHandler.mockReturnValueOnce({ - success: true, - }); - await fillMandatoryFields(fields, data); - const button = screen.getByRole("button", { - name: config.buttons.saveAndContinue, - }); - await waitFor(() => { - expect(button).toBeEnabled(); - }); - fireEvent.click(button); - - await waitFor(() => { - expect(screen.queryByText(/Required field/i)).not.toBeInTheDocument(); - // Assert expected behavior after submission - expect(actionHandler).toHaveBeenCalledTimes(1); - expect(mockRouterPush).toHaveBeenCalledTimes(1); - expect(mockRouterPush).toHaveBeenCalledWith(config.mockRouteSubmit); - }); -}; - // ๐Ÿงช Test suite describe("VerificationForm component", () => { beforeEach(() => { vi.clearAllMocks(); }); - it("renders the form with correct fields", () => { - renderVerificationForm(); + it("renders the form with SFO UI schema fields", async () => { + mockOperationType = "SFO"; // Set to SFO + renderVerificationForm(mockOperationType); expectField(commonMandatoryFormFields.map((field) => field.label)); expectButton(config.buttons.cancel); expectButton(config.buttons.saveAndContinue); }); - it("does not allow form submission if there are validation errors", async () => { - renderVerificationForm(); - fireEvent.click( - screen.getByRole("button", { name: config.buttons.saveAndContinue }), - ); - - await waitFor(() => { - expect(screen.queryAllByText(/Required field/i)).toHaveLength(6); - }); - }); - - it( - "fills mandatory fields for 'None' option and submits successfully", - { - timeout: 10000, - }, - async () => { - renderVerificationForm(); - // POST submit and assert the result - await submitFormAndAssert( - commonMandatoryFormFields, - formDataSets.default, - ); - // Assert if actionHandler was called correctly - expect(actionHandler).toHaveBeenCalledWith( - config.actionPost.endPoint, - "POST", - config.actionPost.revalidatePath, - { - body: JSON.stringify({ - verification_body_name: "Test", - accredited_by: "SCC", - scope_of_verification: "Supplementary Report", - visit_name: "None", - threats_to_independence: false, - verification_conclusion: "Positive", - }), - }, - ); - }, - ); - - it( - "fills mandatory fields for 'Facility X' option and submits successfully", - { - timeout: 10000, - }, - async () => { - renderVerificationForm(); - const fields = [ - ...commonMandatoryFormFields, - ...specificMandatoryFields.facility, - ]; - await submitFormAndAssert(fields, formDataSets.facility); - }, - ); - it( - "fills mandatory fields for 'Other' option and submits successfully", - { - timeout: 10000, - }, - async () => { - renderVerificationForm(); - const fields = [ - ...commonMandatoryFormFields, - ...specificMandatoryFields.other, - ]; - // POST submit and assert the result - await submitFormAndAssert(fields, formDataSets.other); - // Assertion if actionHandler was called correctly - expect(actionHandler).toHaveBeenCalledWith( - config.actionPost.endPoint, - "POST", - config.actionPost.revalidatePath, - { - body: JSON.stringify({ - verification_body_name: "Test", - accredited_by: "SCC", - scope_of_verification: "Supplementary Report", - visit_name: "Other", - threats_to_independence: false, - verification_conclusion: "Modified", - visit_type: "Virtual", - other_facility_name: "Other Facility", - other_facility_coordinates: "Lat 41; Long 35", - }), - }, - ); - }, - ); - it("routes to the final review summary page when the Back button is clicked", () => { - const queryString = "?"; // Update this based on your query string logic if necessary. - const expectedRoute = `/reports/${config.mockVersionId}/final-review${queryString}`; - - renderVerificationForm(); - - // Click the "Back" button - const backButton = screen.getByRole("button", { - name: config.buttons.cancel, - }); - fireEvent.click(backButton); - - // Assert that the router's push method was called with the expected route - expect(mockRouterPush).toHaveBeenCalledTimes(1); - expect(mockRouterPush).toHaveBeenCalledWith(expectedRoute); + it("renders the form with LFO UI schema fields", () => { + mockOperationType = "LFO"; // Set to LFO + renderVerificationForm(mockOperationType); + expectField(commonMandatoryFormFields.map((field) => field.label)); + expectButton(config.buttons.cancel); + expectButton(config.buttons.saveAndContinue); }); }); diff --git a/bciers/apps/reporting/src/tests/components/verification/VerificationPage.test.tsx b/bciers/apps/reporting/src/tests/components/verification/VerificationPage.test.tsx index cb0303c5f7..bb1b03ce36 100644 --- a/bciers/apps/reporting/src/tests/components/verification/VerificationPage.test.tsx +++ b/bciers/apps/reporting/src/tests/components/verification/VerificationPage.test.tsx @@ -5,6 +5,8 @@ import { getReportVerification } from "@reporting/src/app/utils/getReportVerific import { getReportFacilityList } from "@reporting/src/app/utils/getReportFacilityList"; import { createVerificationSchema } from "@reporting/src/app/components/verification/createVerificationSchema"; import { getSignOffAndSubmitSteps } from "@reporting/src/app/components/taskList/5_signOffSubmit"; +import { getReportNeedsVerification } from "@reporting/src/app/utils/getReportNeedsVerification"; +import { getReportingOperation } from "@reporting/src/app/utils/getReportingOperation"; vi.mock("@reporting/src/app/components/verification/VerificationForm", () => ({ default: vi.fn(), @@ -26,7 +28,11 @@ vi.mock( ); vi.mock("@reporting/src/app/utils/getReportNeedsVerification", () => ({ - getReportNeedsVerification: vi.fn(() => Promise.resolve(true)), // Mocking to return true + getReportNeedsVerification: vi.fn(), +})); + +vi.mock("@reporting/src/app/utils/getReportingOperation", () => ({ + getReportingOperation: vi.fn(), })); vi.mock("@reporting/src/app/components/taskList/5_signOffSubmit", () => ({ @@ -49,6 +55,12 @@ const mockCreateVerificationSchema = createVerificationSchema as ReturnType< const mockGetSignOffAndSubmitSteps = getSignOffAndSubmitSteps as ReturnType< typeof vi.fn >; +const mockGetReportNeedsVerification = getReportNeedsVerification as ReturnType< + typeof vi.fn +>; +const mockGetReportingOperation = getReportingOperation as ReturnType< + typeof vi.fn +>; describe("VerificationPage component", () => { it("renders the VerificationForm component with the correct data", async () => { @@ -64,18 +76,25 @@ describe("VerificationPage component", () => { const mockTaskListElements = [ { type: "Page", title: "Verification", isActive: true }, ]; + const mockReportOperation = { + report_operation: { operation_type: "Single Facility Operation" }, + }; mockGetReportVerification.mockResolvedValue(mockInitialData); mockGetReportFacilityList.mockResolvedValue(mockFacilityList); mockCreateVerificationSchema.mockReturnValue(mockVerificationSchema); mockGetSignOffAndSubmitSteps.mockResolvedValue(mockTaskListElements); + mockGetReportNeedsVerification.mockResolvedValue(true); + mockGetReportingOperation.mockResolvedValue(mockReportOperation); render(await VerificationPage({ version_id: mockVersionId })); + expect(mockGetReportingOperation).toHaveBeenCalledWith(mockVersionId); expect(mockGetReportVerification).toHaveBeenCalledWith(mockVersionId); expect(mockGetReportFacilityList).toHaveBeenCalledWith(mockVersionId); expect(mockCreateVerificationSchema).toHaveBeenCalledWith( mockFacilityList.facilities, + "SFO", ); expect(mockGetSignOffAndSubmitSteps).toHaveBeenCalledWith( mockVersionId, @@ -86,8 +105,8 @@ describe("VerificationPage component", () => { expect(mockVerificationForm).toHaveBeenCalledWith( { version_id: mockVersionId, + operationType: "SFO", verificationSchema: mockVerificationSchema, - verificationUiSchema: expect.any(Object), initialData: mockInitialData, taskListElements: mockTaskListElements, },