From f6b4c1d540573b03022a1fda4285ee2b08b2af81 Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Tue, 3 Dec 2024 10:43:03 -0800 Subject: [PATCH] feat: Add schema validation to fuel code dates * Fix existing rule for nameplate and nameplate capacity, use much simpler after validator * Add tests --- .../tests/fuel_code/test_fuel_code_service.py | 6 + .../tests/fuel_code/test_fuel_code_views.py | 396 +++++++++++------- backend/lcfs/web/api/fuel_code/schema.py | 109 ++++- frontend/src/assets/locales/en/fuelCode.json | 2 +- .../views/FuelCodes/AddFuelCode/_schema.jsx | 5 +- 5 files changed, 351 insertions(+), 167 deletions(-) diff --git a/backend/lcfs/tests/fuel_code/test_fuel_code_service.py b/backend/lcfs/tests/fuel_code/test_fuel_code_service.py index d6d778bf3..8560a4401 100644 --- a/backend/lcfs/tests/fuel_code/test_fuel_code_service.py +++ b/backend/lcfs/tests/fuel_code/test_fuel_code_service.py @@ -68,6 +68,9 @@ async def test_create_fuel_code_success(): carbon_intensity=20.5, company="XYZ Corp", application_date="2023-10-01", + approval_date="2023-10-02", + effective_date="2023-10-03", + expiration_date="2024-10-01", edrms="EDRMS-123", feedstock="Corn oil", feedstock_location="Canada", @@ -124,6 +127,9 @@ async def test_update_fuel_code_success(): carbon_intensity=20.5, company="XYZ Corp", application_date="2023-10-01", + approval_date="2023-10-02", + effective_date="2023-10-03", + expiration_date="2024-10-01", edrms="EDRMS-123", feedstock="Corn oil", feedstock_location="Canada", diff --git a/backend/lcfs/tests/fuel_code/test_fuel_code_views.py b/backend/lcfs/tests/fuel_code/test_fuel_code_views.py index 2dc217a82..12d3ccce4 100644 --- a/backend/lcfs/tests/fuel_code/test_fuel_code_views.py +++ b/backend/lcfs/tests/fuel_code/test_fuel_code_views.py @@ -1,12 +1,141 @@ -from datetime import datetime, date -import pytest +from datetime import date from unittest.mock import patch -from httpx import AsyncClient + +import pytest from fastapi import FastAPI +from fastapi.exceptions import RequestValidationError +from httpx import AsyncClient +from starlette import status + from lcfs.db.models.user.Role import RoleEnum from lcfs.web.api.fuel_code.schema import FuelCodeCreateUpdateSchema from lcfs.web.exception.exceptions import DataNotFoundException -from starlette import status + + +# Fixtures for mock data +@pytest.fixture +def mock_table_options(): + return { + "fuelTypes": ["Diesel", "Electric"], + "transportModes": ["Road", "Rail"], + "latestFuelCodes": ["FC-2021-001", "FC-2021-002"], + "facilityNameplateCapacityUnits": ["kW", "MW"], + } + + +@pytest.fixture +def mock_fuel_code_data(): + return { + "fuel_code_id": 1, + "company": "ABC Corp", + "fuel_suffix": "001.0", + "prefixId": 1, + "carbonIntensity": 25.0, + "edrms": "EDRMS-123", + "lastUpdated": "2023-10-01", + "applicationDate": "2023-09-15", + "fuelTypeId": 2, + "feedstock": "Corn", + "feedstockLocation": "Canada", + } + + +@pytest.fixture +def updated_fuel_code_data(): + return FuelCodeCreateUpdateSchema( + fuel_code_id=1, + prefix_id=1001, + fuel_suffix="001", + carbon_intensity=20.5, + edrms="EDRMS-123", + company="XYZ Energy Corp", + contact_name="John Doe", + contact_email="john.doe@example.com", + application_date=date(2023, 9, 15), + approval_date=date(2023, 10, 1), + effective_date=date(2023, 10, 15), + expiration_date=date(2024, 10, 15), + fuel_type_id=2, + feedstock="Corn", + feedstock_location="USA", + fuel_production_facility_city="Vancouver", + fuel_production_facility_province_state="BC", + fuel_production_facility_country="Canada", + facility_nameplate_capacity=1000, + facility_nameplate_capacity_unit="L", + feedstock_fuel_transport_mode=["Truck", "Ship"], + finished_fuel_transport_mode=["Truck"], + is_valid=True, + validation_msg="Validated successfully", + deleted=False, + ) + + +@pytest.fixture +def request_fuel_code_data(): + return { + "status": "Draft", + "prefix": "ABC", + "prefixId": 1001, + "fuelSuffix": "001", + "carbonIntensity": 20.5, + "edrms": "EDRMS-123", + "company": "XYZ Energy Corp", + "lastUpdated": "2023-11-05T10:30:00", + "contactName": "John Doe", + "contactEmail": "john.doe@example.com", + "applicationDate": "2023-09-15", + "approvalDate": "2023-10-01", + "effectiveDate": "2023-10-15", + "expirationDate": "2024-10-15", + "fuel": "Diesel", + "fuelTypeId": 2, + "feedstock": "Corn", + "feedstockLocation": "USA", + "fuelProductionFacilityCity": "Vancouver", + "fuelProductionFacilityProvinceState": "BC", + "fuelProductionFacilityCountry": "Canada", + "facilityNameplateCapacity": 1000, + "facilityNameplateCapacityUnit": "L", + "feedstockFuelTransportMode": ["Truck", "Ship"], + "finishedFuelTransportMode": ["Truck"], + "isValid": True, + "validationMsg": "Validated successfully", + "deleted": False, + } + + +@pytest.fixture +def valid_fuel_code_data(): + return { + "fuel_code_id": 1, + "prefix_id": 1, + "fuel_suffix": "A", + "carbon_intensity": 1.23, + "edrms": "12345", + "company": "Test Company", + "contact_name": "John Doe", + "contact_email": "johndoe@example.com", + "application_date": date(2023, 1, 1), + "approval_date": date(2023, 2, 1), + "effective_date": date(2023, 3, 1), + "expiration_date": date(2023, 12, 31), + "fuel_type_id": 1, + "feedstock": "Test Feedstock", + "feedstock_location": "Location", + "fuel_production_facility_city": "City", + "fuel_production_facility_province_state": "Province", + "fuel_production_facility_country": "Country", + } + + +# Fixture to set user role +@pytest.fixture +def set_user_role(fastapi_app, set_mock_user): + def _set_user_role(role): + set_mock_user(fastapi_app, [role]) + + return _set_user_role # get_table_options @@ -45,9 +174,9 @@ async def test_get_table_options_success( async def test_get_table_options_forbidden( client: AsyncClient, fastapi_app: FastAPI, - set_mock_user, + set_user_role, ): - set_mock_user(fastapi_app, [RoleEnum.SUPPLIER]) # Incorrect role + set_user_role(RoleEnum.SUPPLIER) # Incorrect role url = "/api/fuel-codes/table-options" response = await client.get(url) @@ -59,12 +188,12 @@ async def test_get_table_options_forbidden( async def test_search_table_options_success( client: AsyncClient, fastapi_app: FastAPI, - set_mock_user, + set_user_role, ): with patch( "lcfs.web.api.fuel_code.services.FuelCodeServices.search_fuel_code" ) as mock_search_fuel_code: - set_mock_user(fastapi_app, [RoleEnum.GOVERNMENT]) + set_user_role(RoleEnum.GOVERNMENT) mock_search_fuel_code.return_value = {"fuel_codes": ["AB001"]} @@ -84,10 +213,10 @@ async def test_search_table_options_success( async def test_search_table_options_invalid_params( client: AsyncClient, fastapi_app: FastAPI, - set_mock_user, + set_user_role, ): - set_mock_user(fastapi_app, [RoleEnum.GOVERNMENT]) - url = url = "/api/fuel-codes/search" + set_user_role(RoleEnum.GOVERNMENT) + url = "/api/fuel-codes/search" params = {"invalidParam": "invalid"} response = await client.get(url, params=params) @@ -99,13 +228,13 @@ async def test_search_table_options_invalid_params( async def test_get_fuel_codes_success( client: AsyncClient, fastapi_app: FastAPI, - set_mock_user, + set_user_role, pagination_request_schema, ): with patch( "lcfs.web.api.fuel_code.services.FuelCodeServices.get_fuel_codes" ) as mock_get_fuel_codes: - set_mock_user(fastapi_app, [RoleEnum.GOVERNMENT]) + set_user_role(RoleEnum.GOVERNMENT) mock_get_fuel_codes.return_value = { "fuel_codes": [ @@ -149,27 +278,15 @@ async def test_get_fuel_codes_success( async def test_get_fuel_code_success( client: AsyncClient, fastapi_app: FastAPI, - set_mock_user, + set_user_role, + mock_fuel_code_data, ): with patch( "lcfs.web.api.fuel_code.services.FuelCodeServices.get_fuel_code" ) as mock_get_fuel_code: - set_mock_user(fastapi_app, [RoleEnum.GOVERNMENT]) - - mock_fuel_code = { - "fuel_code_id": 1, - "company": "ABC Corp", - "fuel_suffix": "001.0", - "prefixId": 1, - "carbonIntensity": 25.0, - "edrms": "EDRMS-123", - "lastUpdated": "2023-10-01", - "applicationDate": "2023-09-15", - "fuelTypeId": 2, - "feedstock": "Corn", - "feedstockLocation": "Canada", - } - mock_get_fuel_code.return_value = mock_fuel_code + set_user_role(RoleEnum.GOVERNMENT) + + mock_get_fuel_code.return_value = mock_fuel_code_data url = "/api/fuel-codes/1" response = await client.get(url) @@ -185,12 +302,12 @@ async def test_get_fuel_code_success( async def test_get_fuel_code_not_found( client: AsyncClient, fastapi_app: FastAPI, - set_mock_user, + set_user_role, ): with patch( "lcfs.web.api.fuel_code.services.FuelCodeServices.get_fuel_code" ) as mock_get_fuel_code: - set_mock_user(fastapi_app, [RoleEnum.GOVERNMENT]) + set_user_role(RoleEnum.GOVERNMENT) mock_get_fuel_code.side_effect = DataNotFoundException("Fuel code not found") url = "/api/fuel-codes/9999" @@ -203,52 +320,22 @@ async def test_get_fuel_code_not_found( async def test_update_fuel_code_success( client: AsyncClient, fastapi_app: FastAPI, - set_mock_user, + set_user_role, + updated_fuel_code_data, ): with patch( "lcfs.web.api.fuel_code.services.FuelCodeServices.update_fuel_code" ) as mock_update_fuel_code: - set_mock_user(fastapi_app, [RoleEnum.ANALYST]) - updated_fuel_code = FuelCodeCreateUpdateSchema( - fuel_code_id=1, - status="Draft", - prefix="ABC", - prefix_id=1001, - fuel_suffix="001", - carbon_intensity=20.5, - edrms="EDRMS-123", - company="XYZ Energy Corp", - last_updated=datetime(2023, 11, 5, 10, 30), - contact_name="John Doe", - contact_email="john.doe@example.com", - application_date=date(2023, 9, 15), - approval_date=date(2023, 10, 1), - effective_date=date(2023, 10, 15), - expiration_date=date(2024, 10, 15), - fuel="Diesel", - fuel_type_id=2, - feedstock="Corn", - feedstock_location="USA", - fuel_production_facility_city="Vancouver", - fuel_production_facility_province_state="BC", - fuel_production_facility_country="Canada", - facility_nameplate_capacity=1000, - facility_nameplate_capacity_unit="L", - feedstock_fuel_transport_mode=["Truck", "Ship"], - finished_fuel_transport_mode=["Truck"], - is_valid=True, - validation_msg="Validated successfully", - deleted=False, - ) + set_user_role(RoleEnum.ANALYST) - mock_update_fuel_code.return_value = updated_fuel_code.model_dump( + mock_update_fuel_code.return_value = updated_fuel_code_data.model_dump( by_alias=True, mode="json" ) url = "/api/fuel-codes" # Send the PUT request with the mock updated data response = await client.post( - url, json=updated_fuel_code.model_dump(by_alias=True, mode="json") + url, json=updated_fuel_code_data.model_dump(by_alias=True, mode="json") ) # Assert the response status and data assert response.status_code == status.HTTP_200_OK @@ -260,10 +347,10 @@ async def test_update_fuel_code_success( @pytest.mark.anyio async def test_delete_fuel_code_success( - client: AsyncClient, fastapi_app: FastAPI, set_mock_user + client: AsyncClient, fastapi_app: FastAPI, set_user_role ): """Test successful deletion of a fuel code.""" - set_mock_user(fastapi_app, [RoleEnum.ANALYST]) + set_user_role(RoleEnum.ANALYST) url = "/api/fuel-codes/1" mock_response = {"message": "Fuel code deleted successfully"} @@ -280,10 +367,10 @@ async def test_delete_fuel_code_success( @pytest.mark.anyio async def test_delete_fuel_code_not_found( - client: AsyncClient, fastapi_app: FastAPI, set_mock_user + client: AsyncClient, fastapi_app: FastAPI, set_user_role ): """Test deletion of a non-existent fuel code.""" - set_mock_user(fastapi_app, [RoleEnum.ANALYST]) + set_user_role(RoleEnum.ANALYST) url = "/api/fuel-codes/9999" with patch( @@ -298,10 +385,10 @@ async def test_delete_fuel_code_not_found( @pytest.mark.anyio async def test_delete_fuel_code_unauthorized( - client: AsyncClient, fastapi_app: FastAPI, set_mock_user + client: AsyncClient, fastapi_app: FastAPI, set_user_role ): """Test deletion of a fuel code with unauthorized user role.""" - set_mock_user(fastapi_app, [RoleEnum.SUPPLIER]) # Unauthorized role + set_user_role(RoleEnum.SUPPLIER) # Unauthorized role url = "/api/fuel-codes/1" response = await client.delete(url) @@ -310,63 +397,36 @@ async def test_delete_fuel_code_unauthorized( @pytest.mark.anyio async def test_create_fuel_code_success( - client: AsyncClient, fastapi_app: FastAPI, set_mock_user + client: AsyncClient, + fastapi_app: FastAPI, + set_user_role, + request_fuel_code_data, ): """Test successful creation of a fuel code.""" - set_mock_user(fastapi_app, [RoleEnum.ANALYST]) + set_user_role(RoleEnum.ANALYST) url = "/api/fuel-codes" - request_data = { - "status": "Draft", - "prefix": "ABC", - "prefixId": 1001, - "fuelSuffix": "001", - "carbonIntensity": 20.5, - "edrms": "EDRMS-123", - "company": "XYZ Energy Corp", - "lastUpdated": "2023-11-05T10:30:00", - "contactName": "John Doe", - "contactEmail": "john.doe@example.com", - "applicationDate": "2023-09-15", - "approvalDate": "2023-10-01", - "effectiveDate": "2023-10-15", - "expirationDate": "2024-10-15", - "fuel": "Diesel", - "fuelTypeId": 2, - "feedstock": "Corn", - "feedstockLocation": "USA", - "fuelProductionFacilityCity": "Vancouver", - "fuelProductionFacilityProvinceState": "BC", - "fuelProductionFacilityCountry": "Canada", - "facilityNameplateCapacity": 1000, - "facilityNameplateCapacityUnit": "L", - "feedstockFuelTransportMode": ["Truck", "Ship"], - "finishedFuelTransportMode": ["Truck"], - "isValid": True, - "validationMsg": "Validated successfully", - "deleted": False, - } with patch( "lcfs.web.api.fuel_code.services.FuelCodeServices.create_fuel_code" ) as mock_create_fuel_code: - mock_create_fuel_code.return_value = request_data - response = await client.post(url, json=request_data) + mock_create_fuel_code.return_value = request_fuel_code_data + response = await client.post(url, json=request_fuel_code_data) assert response.status_code == status.HTTP_200_OK result = response.json() - assert result["company"] == request_data["company"] - assert result["fuelTypeId"] == request_data["fuelTypeId"] + assert result["company"] == request_fuel_code_data["company"] + assert result["fuelTypeId"] == request_fuel_code_data["fuelTypeId"] mock_create_fuel_code.assert_called_once_with( - FuelCodeCreateUpdateSchema(**request_data) + FuelCodeCreateUpdateSchema(**request_fuel_code_data) ) @pytest.mark.anyio async def test_create_fuel_code_invalid_data( - client: AsyncClient, fastapi_app: FastAPI, set_mock_user + client: AsyncClient, fastapi_app: FastAPI, set_user_role ): """Test creation of a fuel code with invalid data.""" - set_mock_user(fastapi_app, [RoleEnum.ANALYST]) + set_user_role(RoleEnum.ANALYST) url = "/api/fuel-codes" invalid_data = {"invalidField": "Invalid"} @@ -377,53 +437,23 @@ async def test_create_fuel_code_invalid_data( @pytest.mark.anyio async def test_create_fuel_code_unauthorized( - client: AsyncClient, fastapi_app: FastAPI, set_mock_user + client: AsyncClient, fastapi_app: FastAPI, set_user_role, request_fuel_code_data ): """Test creation of a fuel code with unauthorized user role.""" - set_mock_user(fastapi_app, [RoleEnum.SUPPLIER]) # Unauthorized role + set_user_role(RoleEnum.SUPPLIER) # Unauthorized role url = "/api/fuel-codes" - request_data = { - "status": "Draft", - "prefix": "ABC", - "prefixId": 1001, - "fuelSuffix": "001", - "carbonIntensity": 20.5, - "edrms": "EDRMS-123", - "company": "XYZ Energy Corp", - "lastUpdated": "2023-11-05T10:30:00", - "contactName": "John Doe", - "contactEmail": "john.doe@example.com", - "applicationDate": "2023-09-15", - "approvalDate": "2023-10-01", - "effectiveDate": "2023-10-15", - "expirationDate": "2024-10-15", - "fuel": "Diesel", - "fuelTypeId": 2, - "feedstock": "Corn", - "feedstockLocation": "USA", - "fuelProductionFacilityCity": "Vancouver", - "fuelProductionFacilityProvinceState": "BC", - "fuelProductionFacilityCountry": "Canada", - "facilityNameplateCapacity": 1000, - "facilityNameplateCapacityUnit": "L", - "feedstockFuelTransportMode": ["Truck", "Ship"], - "finishedFuelTransportMode": ["Truck"], - "isValid": True, - "validationMsg": "Validated successfully", - "deleted": False, - } - response = await client.post(url, json=request_data) + response = await client.post(url, json=request_fuel_code_data) assert response.status_code == status.HTTP_403_FORBIDDEN @pytest.mark.anyio async def test_approve_fuel_code_success( - client: AsyncClient, fastapi_app: FastAPI, set_mock_user + client: AsyncClient, fastapi_app: FastAPI, set_user_role ): """Test successful approval of a fuel code.""" - set_mock_user(fastapi_app, [RoleEnum.GOVERNMENT]) + set_user_role(RoleEnum.GOVERNMENT) url = "/api/fuel-codes/1/approve" mock_response = {"message": "Fuel code approved successfully"} @@ -440,10 +470,10 @@ async def test_approve_fuel_code_success( @pytest.mark.anyio async def test_approve_fuel_code_not_found( - client: AsyncClient, fastapi_app: FastAPI, set_mock_user + client: AsyncClient, fastapi_app: FastAPI, set_user_role ): """Test approval of a non-existent fuel code.""" - set_mock_user(fastapi_app, [RoleEnum.GOVERNMENT]) + set_user_role(RoleEnum.GOVERNMENT) url = "/api/fuel-codes/9999/approve" with patch( @@ -460,10 +490,10 @@ async def test_approve_fuel_code_not_found( @pytest.mark.anyio async def test_approve_fuel_code_invalid_request( - client: AsyncClient, fastapi_app: FastAPI, set_mock_user + client: AsyncClient, fastapi_app: FastAPI, set_user_role ): """Test approval of a fuel code with invalid data.""" - set_mock_user(fastapi_app, [RoleEnum.GOVERNMENT]) + set_user_role(RoleEnum.GOVERNMENT) url = "/api/fuel-codes/invalid_id/approve" response = await client.post(url) @@ -473,10 +503,70 @@ async def test_approve_fuel_code_invalid_request( @pytest.mark.anyio async def test_approve_fuel_code_unauthorized( - client: AsyncClient, fastapi_app: FastAPI, set_mock_user + client: AsyncClient, fastapi_app: FastAPI, set_user_role ): - set_mock_user(fastapi_app, [RoleEnum.SUPPLIER]) + set_user_role(RoleEnum.SUPPLIER) url = "/api/fuel-codes/1/approve" response = await client.post(url) assert response.status_code == status.HTTP_403_FORBIDDEN + + +# Tests for FuelCodeCreateUpdateSchema validation +@pytest.mark.anyio +def test_valid_dates(valid_fuel_code_data): + # Valid input + model = FuelCodeCreateUpdateSchema(**valid_fuel_code_data) + assert model.application_date == date(2023, 1, 1) + + +@pytest.mark.anyio +def test_invalid_application_date_before_approval_date(valid_fuel_code_data): + # Application Date is after Approval Date + invalid_data = valid_fuel_code_data.copy() + invalid_data["application_date"] = date(2023, 2, 2) + invalid_data["approval_date"] = date(2023, 2, 1) + with pytest.raises(RequestValidationError) as excinfo: + FuelCodeCreateUpdateSchema(**invalid_data) + assert "applicationDate" in str(excinfo.value) + + +@pytest.mark.anyio +def test_invalid_effective_date_before_application_date(valid_fuel_code_data): + # Effective Date is before Application Date + invalid_data = valid_fuel_code_data.copy() + invalid_data["effective_date"] = date(2022, 12, 31) + with pytest.raises(RequestValidationError) as excinfo: + FuelCodeCreateUpdateSchema(**invalid_data) + assert "effectiveDate" in str(excinfo.value) + + +@pytest.mark.anyio +def test_invalid_expiration_date_before_effective_date(valid_fuel_code_data): + # Expiration Date is before Effective Date + invalid_data = valid_fuel_code_data.copy() + invalid_data["expiration_date"] = date(2023, 2, 28) + with pytest.raises(RequestValidationError) as excinfo: + FuelCodeCreateUpdateSchema(**invalid_data) + assert "expirationDate" in str(excinfo.value) + + +@pytest.mark.anyio +def test_missing_capacity_unit(valid_fuel_code_data): + # Facility capacity is provided but no unit + invalid_data = valid_fuel_code_data.copy() + invalid_data["facility_nameplate_capacity"] = 100 + invalid_data["facility_nameplate_capacity_unit"] = None + with pytest.raises(RequestValidationError) as excinfo: + FuelCodeCreateUpdateSchema(**invalid_data) + assert "facilityNameplateCapacityUnit" in str(excinfo.value) + + +@pytest.mark.anyio +def test_valid_capacity_unit(valid_fuel_code_data): + # Valid capacity and unit + valid_data = valid_fuel_code_data.copy() + valid_data["facility_nameplate_capacity"] = 100 + valid_data["facility_nameplate_capacity_unit"] = "Gallons" + model = FuelCodeCreateUpdateSchema(**valid_data) + assert model.facility_nameplate_capacity == 100 diff --git a/backend/lcfs/web/api/fuel_code/schema.py b/backend/lcfs/web/api/fuel_code/schema.py index 011b41f42..1a356d227 100644 --- a/backend/lcfs/web/api/fuel_code/schema.py +++ b/backend/lcfs/web/api/fuel_code/schema.py @@ -1,7 +1,15 @@ from typing import Optional, List, Union + +from fastapi.exceptions import RequestValidationError + from lcfs.web.api.base import BaseSchema, PaginationResponseSchema from datetime import date, datetime -from pydantic import Field, ValidationError, field_validator, model_validator +from pydantic import ( + Field, + ValidationError, + field_validator, + model_validator, +) from enum import Enum @@ -80,6 +88,7 @@ class EndUseTypeSchema(BaseSchema): type: str sub_type: Optional[str] = None + class EndUserTypeSchema(BaseSchema): end_user_type_id: int type_name: str @@ -254,9 +263,9 @@ class FuelCodeCreateUpdateSchema(BaseSchema): contact_name: Optional[str] = None contact_email: Optional[str] = None application_date: date - approval_date: Optional[date] = None - effective_date: Optional[date] = None - expiration_date: Optional[date] = None + approval_date: date + effective_date: date + expiration_date: date fuel_type_id: int feedstock: str feedstock_location: str @@ -283,21 +292,97 @@ class FuelCodeCreateUpdateSchema(BaseSchema): validation_msg: Optional[str] = None deleted: Optional[bool] = None - @model_validator(mode="before") - def check_capacity_and_unit(cls, values): - facility_nameplate_capacity = values.get("facility_nameplate_capacity") - facility_nameplate_capacity_unit = values.get("facility_nameplate_capacity_unit") + @model_validator(mode="after") + def check_capacity_and_unit(self): + facility_nameplate_capacity = self.facility_nameplate_capacity + facility_nameplate_capacity_unit = self.facility_nameplate_capacity_unit if facility_nameplate_capacity is None: - values["facility_nameplate_capacity_unit"] = None + self.facility_nameplate_capacity = None elif ( facility_nameplate_capacity is not None and facility_nameplate_capacity_unit is None ): - raise ValidationError( - "facility_nameplate_capacity_unit must be provided when facility_nameplate_capacity is not None" + errors = [ + { + "loc": ("facilityNameplateCapacityUnit",), + "msg": "must be provided when the facility nameplate capacity is set", + "type": "value_error", + } + ] + raise RequestValidationError(errors) + return self + + @model_validator(mode="after") + def validate_dates(self): + application_date = self.application_date + approval_date = self.approval_date + effective_date = self.effective_date + expiration_date = self.expiration_date + + errors = [] + + # Application Date: Must be before Approval Date and Expiry Date + if application_date >= approval_date: + errors.append( + { + "loc": ("applicationDate",), + "msg": "must be before Approval Date.", + "type": "value_error", + } + ) + if application_date >= expiration_date: + errors.append( + { + "loc": ("applicationDate",), + "msg": "must be before Expiration Date.", + "type": "value_error", + } + ) + + # Approval Date: Must be after Application Date + if approval_date <= application_date: + errors.append( + { + "loc": ("approvalDate",), + "msg": "must be after Application Date.", + "type": "value_error", + } + ) + + # Effective Date: Must be on/after Application Date and before Expiry Date + if effective_date < application_date: + errors.append( + { + "loc": ("effectiveDate",), + "msg": "must be on or after Application Date.", + "type": "value_error", + } + ) + if expiration_date and effective_date >= expiration_date: + errors.append( + { + "loc": ("effectiveDate",), + "msg": "must be before Expiry Date.", + "type": "value_error", + } ) - return values + + # Expiry Date: Must be after Effective Date + if expiration_date <= effective_date: + errors.append( + { + "loc": ("expirationDate",), + "msg": "must be after Effective Date.", + "type": "value_error", + } + ) + + # Raise RequestValidationError if any errors exist + if errors: + raise RequestValidationError(errors) + + return self class DeleteFuelCodeResponseSchema(BaseSchema): diff --git a/frontend/src/assets/locales/en/fuelCode.json b/frontend/src/assets/locales/en/fuelCode.json index 50d99f989..5ff43d9ff 100644 --- a/frontend/src/assets/locales/en/fuelCode.json +++ b/frontend/src/assets/locales/en/fuelCode.json @@ -35,7 +35,7 @@ "applicationDate": "Application date", "approvalDate": "Approval date", "effectiveDate": "Effective date", - "expiryDate": "Expiry date", + "expirationDate": "Expiry date", "fuel": "Fuel", "feedstock": "Feedstock", "feedstockLocation": "Feedstock location", diff --git a/frontend/src/views/FuelCodes/AddFuelCode/_schema.jsx b/frontend/src/views/FuelCodes/AddFuelCode/_schema.jsx index 682cb52cb..0e90b55d3 100644 --- a/frontend/src/views/FuelCodes/AddFuelCode/_schema.jsx +++ b/frontend/src/views/FuelCodes/AddFuelCode/_schema.jsx @@ -260,6 +260,7 @@ export const fuelCodeColDefs = (optionsData, errors, isCreate, canEdit) => [ { field: 'approvalDate', editable: canEdit, + headerComponent: canEdit ? RequiredHeader : undefined, headerName: i18n.t('fuelCode:fuelCodeColLabels.approvalDate'), maxWidth: 220, minWidth: 220, @@ -275,6 +276,7 @@ export const fuelCodeColDefs = (optionsData, errors, isCreate, canEdit) => [ { field: 'effectiveDate', editable: canEdit, + headerComponent: canEdit ? RequiredHeader : undefined, headerName: i18n.t('fuelCode:fuelCodeColLabels.effectiveDate'), maxWidth: 220, minWidth: 220, @@ -289,7 +291,8 @@ export const fuelCodeColDefs = (optionsData, errors, isCreate, canEdit) => [ { field: 'expirationDate', editable: canEdit, - headerName: i18n.t('fuelCode:fuelCodeColLabels.expiryDate'), + headerComponent: canEdit ? RequiredHeader : undefined, + headerName: i18n.t('fuelCode:fuelCodeColLabels.expirationDate'), maxWidth: 220, minWidth: 220, cellRenderer: createCellRenderer('expirationDate', (params) => (