diff --git a/backend/lcfs/db/migrations/versions/2024-12-22-23-46_ab04810d4d7c.py b/backend/lcfs/db/migrations/versions/2024-12-22-23-46_ab04810d4d7c.py new file mode 100644 index 000000000..524d59140 --- /dev/null +++ b/backend/lcfs/db/migrations/versions/2024-12-22-23-46_ab04810d4d7c.py @@ -0,0 +1,36 @@ +"""Add UQ to TCI + +Revision ID: ab04810d4d7c +Revises: 5fbcb508c1be +Create Date: 2024-12-22 23:46:37.505166 + +""" + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = "ab04810d4d7c" +down_revision = "5fbcb508c1be" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_unique_constraint( + "uq_target_carbon_intensity_compliance_fuel", + "target_carbon_intensity", + ["compliance_period_id", "fuel_category_id"], + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint( + "uq_target_carbon_intensity_compliance_fuel", + "target_carbon_intensity", + type_="unique", + ) + # ### end Alembic commands ### diff --git a/backend/lcfs/db/models/admin_adjustment/AdminAdjustment.py b/backend/lcfs/db/models/admin_adjustment/AdminAdjustment.py index 49443d9d2..f23c0d306 100644 --- a/backend/lcfs/db/models/admin_adjustment/AdminAdjustment.py +++ b/backend/lcfs/db/models/admin_adjustment/AdminAdjustment.py @@ -1,13 +1,12 @@ from sqlalchemy import Column, Integer, BigInteger, ForeignKey, DateTime, String from sqlalchemy.orm import relationship -from sqlalchemy import UniqueConstraint + from lcfs.db.base import BaseModel, Auditable, EffectiveDates class AdminAdjustment(BaseModel, Auditable, EffectiveDates): __tablename__ = "admin_adjustment" __table_args__ = ( - UniqueConstraint("admin_adjustment_id"), {"comment": "Goverment to organization compliance units admin_adjustment"}, ) diff --git a/backend/lcfs/db/models/admin_adjustment/AdminAdjustmentHistory.py b/backend/lcfs/db/models/admin_adjustment/AdminAdjustmentHistory.py index 36fbf0d6f..4243891f7 100644 --- a/backend/lcfs/db/models/admin_adjustment/AdminAdjustmentHistory.py +++ b/backend/lcfs/db/models/admin_adjustment/AdminAdjustmentHistory.py @@ -1,13 +1,12 @@ -from sqlalchemy import Column, Integer, BigInteger, ForeignKey, DateTime +from sqlalchemy import Column, Integer, ForeignKey from sqlalchemy.orm import relationship -from sqlalchemy import UniqueConstraint + from lcfs.db.base import BaseModel, Auditable, EffectiveDates class AdminAdjustmentHistory(BaseModel, Auditable, EffectiveDates): __tablename__ = "admin_adjustment_history" __table_args__ = ( - UniqueConstraint("admin_adjustment_history_id"), {"comment": "History record for admin_adjustment status change."}, ) diff --git a/backend/lcfs/db/models/comment/InternalComment.py b/backend/lcfs/db/models/comment/InternalComment.py index 56dcecbc9..aba68d742 100644 --- a/backend/lcfs/db/models/comment/InternalComment.py +++ b/backend/lcfs/db/models/comment/InternalComment.py @@ -1,18 +1,22 @@ -from sqlalchemy import Column, Integer, Text, UniqueConstraint -from sqlalchemy.orm import relationship +from sqlalchemy import Column, Integer, Text from sqlalchemy.dialects.postgresql import ENUM +from sqlalchemy.orm import relationship + from lcfs.db.base import BaseModel, Auditable # ENUM for audience scope audience_scope_enum = ENUM( - "Director", "Analyst", "Compliance Manager", name="audience_scope", create_type=False + "Director", + "Analyst", + "Compliance Manager", + name="audience_scope", + create_type=False, ) class InternalComment(BaseModel, Auditable): __tablename__ = "internal_comment" __table_args__ = ( - UniqueConstraint("internal_comment_id"), {"comment": "Stores internal comments with scope and related metadata."}, ) @@ -42,4 +46,4 @@ class InternalComment(BaseModel, Auditable): ) compliance_report_internal_comments = relationship( "ComplianceReportInternalComment", back_populates="internal_comment" - ) \ No newline at end of file + ) diff --git a/backend/lcfs/db/models/compliance/CompliancePeriod.py b/backend/lcfs/db/models/compliance/CompliancePeriod.py index 187f7ee67..07e330583 100644 --- a/backend/lcfs/db/models/compliance/CompliancePeriod.py +++ b/backend/lcfs/db/models/compliance/CompliancePeriod.py @@ -1,5 +1,6 @@ -from sqlalchemy import Column, Integer, String, Date +from sqlalchemy import Column, Integer, String from sqlalchemy.orm import relationship + from lcfs.db.base import BaseModel, EffectiveDates diff --git a/backend/lcfs/db/models/compliance/ComplianceReport.py b/backend/lcfs/db/models/compliance/ComplianceReport.py index 6656cc6ec..43f28a00e 100644 --- a/backend/lcfs/db/models/compliance/ComplianceReport.py +++ b/backend/lcfs/db/models/compliance/ComplianceReport.py @@ -1,17 +1,17 @@ -import uuid import enum +import uuid from pydantic import computed_field from sqlalchemy import ( Column, Integer, String, - ForeignKey, Enum, Table, ForeignKey, ) -from sqlalchemy.orm import relationship, backref +from sqlalchemy.orm import relationship + from lcfs.db.base import BaseModel, Auditable diff --git a/backend/lcfs/db/models/compliance/listeners.py b/backend/lcfs/db/models/compliance/listeners.py index 49ab43c34..1ea2b22b8 100644 --- a/backend/lcfs/db/models/compliance/listeners.py +++ b/backend/lcfs/db/models/compliance/listeners.py @@ -9,9 +9,3 @@ def prevent_update_if_locked(mapper, connection, target): raise InvalidRequestError("Cannot update a locked ComplianceReportSummary") -@event.listens_for(ComplianceReportSummary.is_locked, "set") -def prevent_unlock(target, value, oldvalue, initiator): - if oldvalue and not value: - raise InvalidRequestError( - "Cannot unlock a ComplianceReportSummary once it's locked" - ) diff --git a/backend/lcfs/db/models/document/Document.py b/backend/lcfs/db/models/document/Document.py index 865df76ee..8639d8229 100644 --- a/backend/lcfs/db/models/document/Document.py +++ b/backend/lcfs/db/models/document/Document.py @@ -1,5 +1,4 @@ -from sqlalchemy import Column, Integer, String, ForeignKey -from sqlalchemy import UniqueConstraint +from sqlalchemy import Column, Integer, String from sqlalchemy.orm import relationship from lcfs.db.base import BaseModel, Auditable @@ -11,7 +10,6 @@ class Document(BaseModel, Auditable): __tablename__ = "document" __table_args__ = ( - UniqueConstraint("document_id"), {"comment": "Main document table for storing base document information"}, ) diff --git a/backend/lcfs/db/models/fuel/FuelCategory.py b/backend/lcfs/db/models/fuel/FuelCategory.py index f4d3f0791..ab1615ef0 100644 --- a/backend/lcfs/db/models/fuel/FuelCategory.py +++ b/backend/lcfs/db/models/fuel/FuelCategory.py @@ -1,7 +1,8 @@ -from sqlalchemy import Column, Integer, Text, Enum, Float, Numeric -from lcfs.db.base import BaseModel, Auditable, DisplayOrder, EffectiveDates +from sqlalchemy import Column, Integer, Text, Enum, Numeric from sqlalchemy.orm import relationship +from lcfs.db.base import BaseModel, Auditable, DisplayOrder, EffectiveDates + class FuelCategory(BaseModel, Auditable, DisplayOrder, EffectiveDates): diff --git a/backend/lcfs/db/models/fuel/FuelType.py b/backend/lcfs/db/models/fuel/FuelType.py index 5f652b738..4ebbb7aae 100644 --- a/backend/lcfs/db/models/fuel/FuelType.py +++ b/backend/lcfs/db/models/fuel/FuelType.py @@ -1,9 +1,11 @@ -from sqlalchemy import Column, Integer, Text, Boolean, Float, Enum, Numeric -from lcfs.db.base import BaseModel, Auditable, DisplayOrder -from sqlalchemy.orm import relationship -from sqlalchemy import ForeignKey import enum +from sqlalchemy import Column, Integer, Text, Boolean, Enum, Numeric +from sqlalchemy import ForeignKey +from sqlalchemy.orm import relationship + +from lcfs.db.base import BaseModel, Auditable, DisplayOrder + # Enum for fuel quantity units class QuantityUnitsEnum(enum.Enum): diff --git a/backend/lcfs/db/models/fuel/TargetCarbonIntensity.py b/backend/lcfs/db/models/fuel/TargetCarbonIntensity.py index edf7c5ef8..ddc78921a 100644 --- a/backend/lcfs/db/models/fuel/TargetCarbonIntensity.py +++ b/backend/lcfs/db/models/fuel/TargetCarbonIntensity.py @@ -1,13 +1,18 @@ -from sqlalchemy import Column, Integer, Float, ForeignKey, Numeric +from sqlalchemy import Column, Integer, Float, ForeignKey, Numeric, UniqueConstraint from lcfs.db.base import BaseModel, Auditable, EffectiveDates from sqlalchemy.orm import relationship class TargetCarbonIntensity(BaseModel, Auditable, EffectiveDates): __tablename__ = "target_carbon_intensity" - __table_args__ = { - "comment": "Target carbon intensity values for various fuel categories" - } + __table_args__ = ( + UniqueConstraint( + "compliance_period_id", + "fuel_category_id", + name="uq_target_carbon_intensity_compliance_fuel", + ), + {"comment": "Target carbon intensity values for various fuel categories"}, + ) target_carbon_intensity_id = Column(Integer, primary_key=True, autoincrement=True) compliance_period_id = Column( diff --git a/backend/lcfs/db/models/initiative_agreement/InitiativeAgreement.py b/backend/lcfs/db/models/initiative_agreement/InitiativeAgreement.py index 63d5a9817..590c80239 100644 --- a/backend/lcfs/db/models/initiative_agreement/InitiativeAgreement.py +++ b/backend/lcfs/db/models/initiative_agreement/InitiativeAgreement.py @@ -1,13 +1,12 @@ from sqlalchemy import Column, Integer, BigInteger, ForeignKey, DateTime, String from sqlalchemy.orm import relationship -from sqlalchemy import UniqueConstraint + from lcfs.db.base import BaseModel, Auditable, EffectiveDates class InitiativeAgreement(BaseModel, Auditable, EffectiveDates): __tablename__ = "initiative_agreement" __table_args__ = ( - UniqueConstraint("initiative_agreement_id"), {"comment": "Goverment to organization compliance units initiative agreement"}, ) diff --git a/backend/lcfs/db/models/initiative_agreement/InitiativeAgreementHistory.py b/backend/lcfs/db/models/initiative_agreement/InitiativeAgreementHistory.py index 868c96f8d..03abf6add 100644 --- a/backend/lcfs/db/models/initiative_agreement/InitiativeAgreementHistory.py +++ b/backend/lcfs/db/models/initiative_agreement/InitiativeAgreementHistory.py @@ -1,13 +1,12 @@ -from sqlalchemy import Column, Integer, BigInteger, ForeignKey, DateTime +from sqlalchemy import Column, Integer, ForeignKey from sqlalchemy.orm import relationship -from sqlalchemy import UniqueConstraint + from lcfs.db.base import BaseModel, Auditable, EffectiveDates class InitiativeAgreementHistory(BaseModel, Auditable, EffectiveDates): __tablename__ = "initiative_agreement_history" __table_args__ = ( - UniqueConstraint("initiative_agreement_history_id"), {"comment": "History record for initiative agreement status change."}, ) diff --git a/backend/lcfs/db/models/transaction/Transaction.py b/backend/lcfs/db/models/transaction/Transaction.py index 10042ab68..978ce1796 100644 --- a/backend/lcfs/db/models/transaction/Transaction.py +++ b/backend/lcfs/db/models/transaction/Transaction.py @@ -1,7 +1,6 @@ import enum from sqlalchemy import Column, Integer, BigInteger, ForeignKey, Enum -from sqlalchemy import UniqueConstraint from sqlalchemy.orm import relationship from lcfs.db.base import BaseModel, Auditable, EffectiveDates @@ -16,7 +15,6 @@ class TransactionActionEnum(enum.Enum): class Transaction(BaseModel, Auditable, EffectiveDates): __tablename__ = "transaction" __table_args__ = ( - UniqueConstraint("transaction_id"), { "comment": "Contains a list of all of the government to organization and Organization to Organization transaction." }, diff --git a/backend/lcfs/db/models/transfer/Transfer.py b/backend/lcfs/db/models/transfer/Transfer.py index 5869dce0b..12b730d22 100644 --- a/backend/lcfs/db/models/transfer/Transfer.py +++ b/backend/lcfs/db/models/transfer/Transfer.py @@ -1,7 +1,8 @@ import enum + from sqlalchemy import Column, Integer, ForeignKey, DateTime, Enum, String, Numeric from sqlalchemy.orm import relationship -from sqlalchemy import UniqueConstraint + from lcfs.db.base import BaseModel, Auditable, EffectiveDates @@ -13,7 +14,6 @@ class TransferRecommendationEnum(enum.Enum): class Transfer(BaseModel, Auditable, EffectiveDates): __tablename__ = "transfer" __table_args__ = ( - UniqueConstraint("transfer_id"), {"comment": "Records of tranfer from Organization to Organization"}, ) diff --git a/backend/lcfs/db/models/transfer/TransferCategory.py b/backend/lcfs/db/models/transfer/TransferCategory.py index 8bab13e73..6b41e1510 100644 --- a/backend/lcfs/db/models/transfer/TransferCategory.py +++ b/backend/lcfs/db/models/transfer/TransferCategory.py @@ -14,10 +14,7 @@ class TransferCategoryEnum(enum.Enum): class TransferCategory(BaseModel, Auditable, EffectiveDates): __tablename__ = "transfer_category" - __table_args__ = ( - UniqueConstraint("transfer_category_id"), - {"comment": "Transfer Category"}, - ) + __table_args__ = ({"comment": "Transfer Category"},) transfer_category_id = Column( Integer, diff --git a/backend/lcfs/db/models/transfer/TransferHistory.py b/backend/lcfs/db/models/transfer/TransferHistory.py index 1cdd702b4..48f485015 100644 --- a/backend/lcfs/db/models/transfer/TransferHistory.py +++ b/backend/lcfs/db/models/transfer/TransferHistory.py @@ -1,5 +1,6 @@ -from sqlalchemy import Column, Integer, ForeignKey, DateTime +from sqlalchemy import Column, Integer, ForeignKey from sqlalchemy.orm import relationship + from lcfs.db.base import BaseModel, Auditable, EffectiveDates diff --git a/backend/lcfs/tests/audit_log/test_audit_log_services.py b/backend/lcfs/tests/audit_log/test_audit_log_services.py index 92dbee85a..a5e545bc4 100644 --- a/backend/lcfs/tests/audit_log/test_audit_log_services.py +++ b/backend/lcfs/tests/audit_log/test_audit_log_services.py @@ -72,9 +72,15 @@ async def test_get_audit_logs_paginated_no_data(audit_log_service, mock_repo): pagination = PaginationRequestSchema(page=1, size=10, filters=[], sort_orders=[]) mock_repo.get_audit_logs_paginated.return_value = ([], 0) - # Act & Assert - with pytest.raises(DataNotFoundException): - await audit_log_service.get_audit_logs_paginated(pagination) + # Act + result = await audit_log_service.get_audit_logs_paginated(pagination) + + # Assert + assert result.audit_logs == [], "Should return an empty list of audit logs" + assert result.pagination.total == 0, "Total should be zero if no records" + assert result.pagination.page == pagination.page + assert result.pagination.size == pagination.size + assert result.pagination.total_pages == 0 @pytest.mark.anyio diff --git a/backend/lcfs/tests/compliance_report/test_compliance_report_services.py b/backend/lcfs/tests/compliance_report/test_compliance_report_services.py index 9300d2918..2c39bbe2d 100644 --- a/backend/lcfs/tests/compliance_report/test_compliance_report_services.py +++ b/backend/lcfs/tests/compliance_report/test_compliance_report_services.py @@ -116,16 +116,30 @@ async def test_get_compliance_reports_paginated_success( async def test_get_compliance_reports_paginated_not_found( compliance_report_service, mock_repo ): + # Arrange pagination_mock = AsyncMock() pagination_mock.page = 1 pagination_mock.size = 10 + pagination_mock.filters = [] + pagination_mock.sort_orders = [] + # Mock the repository to return no records mock_repo.get_reports_paginated.return_value = ([], 0) - with pytest.raises(DataNotFoundException): - await compliance_report_service.get_compliance_reports_paginated( - pagination_mock - ) + # Act + result = await compliance_report_service.get_compliance_reports_paginated( + pagination_mock + ) + + # Assert: Verify the service returns an empty list and correct pagination metadata + assert result.reports == [], "Expected no compliance reports to be returned" + assert result.pagination.total == 0, "Expected total=0 when there are no records" + assert result.pagination.page == 1, "Page should match the requested page" + assert result.pagination.size == 10, "Size should match the requested size" + assert result.pagination.total_pages == 0, "0 records should yield 0 total_pages" + + # Also verify our repo was called exactly once + mock_repo.get_reports_paginated.assert_called_once() @pytest.mark.anyio diff --git a/backend/lcfs/tests/compliance_report/test_update_service.py b/backend/lcfs/tests/compliance_report/test_update_service.py index 12532c4e0..9fd0496f0 100644 --- a/backend/lcfs/tests/compliance_report/test_update_service.py +++ b/backend/lcfs/tests/compliance_report/test_update_service.py @@ -281,16 +281,6 @@ async def test_handle_submitted_status_with_existing_summary( report_id ) - # Ensure the adjust_balance method is called with the correct parameters - mock_org_service.adjust_balance.assert_called_once_with( - transaction_action=TransactionActionEnum.Reserved, - compliance_units=mock_report.summary.line_20_surplus_deficit_units, - organization_id=mock_report.organization_id, - ) - - # Check if the report was updated with the result of adjust_balance - assert mock_report.transaction == mock_org_service.adjust_balance.return_value - # Check if the summary is locked saved_summary = mock_repo.save_compliance_report_summary.call_args[0][0] assert saved_summary.is_locked == True diff --git a/backend/lcfs/tests/fuel_code/test_fuel_code_repo.py b/backend/lcfs/tests/fuel_code/test_fuel_code_repo.py index 2fdfc689b..b01f5a402 100644 --- a/backend/lcfs/tests/fuel_code/test_fuel_code_repo.py +++ b/backend/lcfs/tests/fuel_code/test_fuel_code_repo.py @@ -1,26 +1,23 @@ from datetime import date -from unittest import mock +from unittest.mock import AsyncMock, MagicMock import pytest -from unittest.mock import AsyncMock, MagicMock from sqlalchemy.exc import NoResultFound -from sqlalchemy.orm import joinedload -from lcfs.web.api.fuel_code.repo import FuelCodeRepository -from lcfs.db.models.fuel.TransportMode import TransportMode -from lcfs.db.models.fuel.FuelType import FuelType -from lcfs.db.models.fuel.FuelCategory import FuelCategory -from lcfs.db.models.fuel.UnitOfMeasure import UnitOfMeasure +from lcfs.db.models.fuel.AdditionalCarbonIntensity import AdditionalCarbonIntensity +from lcfs.db.models.fuel.EnergyDensity import EnergyDensity +from lcfs.db.models.fuel.EnergyEffectivenessRatio import EnergyEffectivenessRatio from lcfs.db.models.fuel.ExpectedUseType import ExpectedUseType +from lcfs.db.models.fuel.FuelCategory import FuelCategory from lcfs.db.models.fuel.FuelCode import FuelCode from lcfs.db.models.fuel.FuelCodePrefix import FuelCodePrefix from lcfs.db.models.fuel.FuelCodeStatus import FuelCodeStatus, FuelCodeStatusEnum -from lcfs.db.models.fuel.EnergyDensity import EnergyDensity -from lcfs.db.models.fuel.EnergyEffectivenessRatio import EnergyEffectivenessRatio +from lcfs.db.models.fuel.FuelType import FuelType from lcfs.db.models.fuel.ProvisionOfTheAct import ProvisionOfTheAct -from lcfs.db.models.fuel.AdditionalCarbonIntensity import AdditionalCarbonIntensity from lcfs.db.models.fuel.TargetCarbonIntensity import TargetCarbonIntensity -from lcfs.db.models.compliance.CompliancePeriod import CompliancePeriod +from lcfs.db.models.fuel.TransportMode import TransportMode +from lcfs.db.models.fuel.UnitOfMeasure import UnitOfMeasure +from lcfs.web.api.fuel_code.repo import FuelCodeRepository from lcfs.web.exception.exceptions import DatabaseException @@ -585,17 +582,16 @@ async def test_get_energy_effectiveness_ratio(fuel_code_repo, mock_db): @pytest.mark.anyio -async def test_get_target_carbon_intensities(fuel_code_repo, mock_db): +async def test_get_target_carbon_intensity(fuel_code_repo, mock_db): tci = TargetCarbonIntensity( target_carbon_intensity_id=1, target_carbon_intensity=50.0 ) mock_result = MagicMock() - mock_result.scalars.return_value.all.return_value = [tci] + mock_result.scalar_one.return_value = tci mock_db.execute.return_value = mock_result - result = await fuel_code_repo.get_target_carbon_intensities(1, "2024") - assert len(result) == 1 - assert result[0] == tci + result = await fuel_code_repo.get_target_carbon_intensity(1, "2024") + assert result == tci @pytest.mark.anyio @@ -625,14 +621,8 @@ async def test_get_standardized_fuel_data(fuel_code_repo, mock_db): ), # target carbon intensities MagicMock( - scalars=MagicMock( - return_value=MagicMock( - all=MagicMock( - return_value=[ - TargetCarbonIntensity(target_carbon_intensity=50.0) - ] - ) - ) + scalar_one=MagicMock( + return_value=TargetCarbonIntensity(target_carbon_intensity=50.0) ) ), # additional carbon intensity @@ -663,7 +653,6 @@ async def test_get_standardized_fuel_data_unrecognized(fuel_code_repo, mock_db): mock_fuel_type = FuelType( fuel_type_id=1, fuel_type="UnknownFuel", - default_carbon_intensity=None, unrecognized=True, ) @@ -699,13 +688,8 @@ async def test_get_standardized_fuel_data_unrecognized(fuel_code_repo, mock_db): ) # Target Carbon Intensities tci_result = MagicMock( - scalars=MagicMock( - return_value=MagicMock( - all=MagicMock( - return_value=[TargetCarbonIntensity( - target_carbon_intensity=50.0)] - ) - ) + scalar_one=MagicMock( + return_value=TargetCarbonIntensity(target_carbon_intensity=50.0) ) ) # Additional Carbon Intensity diff --git a/backend/lcfs/tests/fuel_supply/test_fuel_supplies_actions_service.py b/backend/lcfs/tests/fuel_supply/test_fuel_supplies_actions_service.py index df6d6eee0..ec0f33ff1 100644 --- a/backend/lcfs/tests/fuel_supply/test_fuel_supplies_actions_service.py +++ b/backend/lcfs/tests/fuel_supply/test_fuel_supplies_actions_service.py @@ -1,6 +1,3 @@ -# test_fuel_supply.py - -from datetime import datetime from unittest.mock import AsyncMock from uuid import uuid4 @@ -21,7 +18,6 @@ FUEL_SUPPLY_EXCLUDE_FIELDS = { "id", "fuel_supply_id", - "compliance_period", "deleted", "group_uuid", "user_type", @@ -129,14 +125,12 @@ def create_sample_fs_data(): fuel_category_id=1, end_use_id=1, fuel_code_id=1, - compliance_period="2024", # Schema-only field - quantity=1000.0, + quantity=1000, units="L", energy_density=35.0, group_uuid=str(uuid4()), version=0, - provisionOfTheActId=123, - exportDate=datetime.now().date(), + provision_of_the_act_id=123, ) @@ -184,7 +178,9 @@ async def test_create_fuel_supply_success( mock_repo.create_fuel_supply.return_value = created_supply # Call the method under test - result = await fuel_supply_action_service.create_fuel_supply(fe_data, user_type) + result = await fuel_supply_action_service.create_fuel_supply( + fe_data, user_type, "2024" + ) # Assign mocked related objects for schema validation result.fuel_type = { @@ -201,7 +197,7 @@ async def test_create_fuel_supply_success( fuel_category_id=fe_data.fuel_category_id, end_use_id=fe_data.end_use_id, fuel_code_id=fe_data.fuel_code_id, - compliance_period=fe_data.compliance_period, + compliance_period="2024", ) mock_repo.create_fuel_supply.assert_awaited_once() # Ensure compliance units were calculated correctly @@ -266,7 +262,9 @@ async def test_update_fuel_supply_success_existing_report( mock_repo.update_fuel_supply.return_value = updated_supply # Call the method under test - result = await fuel_supply_action_service.update_fuel_supply(fe_data, user_type) + result = await fuel_supply_action_service.update_fuel_supply( + fe_data, user_type, "2024" + ) # Assign mocked related objects for schema validation result.fuel_type = { @@ -348,7 +346,9 @@ async def test_update_fuel_supply_create_new_version( mock_repo.create_fuel_supply.return_value = new_supply # Call the method under test - result = await fuel_supply_action_service.update_fuel_supply(fe_data, user_type) + result = await fuel_supply_action_service.update_fuel_supply( + fe_data, user_type, "2024" + ) # Assign mocked related objects for schema validation result.fuel_type = { @@ -489,7 +489,7 @@ async def test_populate_fuel_supply_fields( # Call the method under test populated_supply = await fuel_supply_action_service._populate_fuel_supply_fields( - fuel_supply, fe_data + fuel_supply, fe_data, "2024" ) # Assertions @@ -507,13 +507,13 @@ async def test_populate_fuel_supply_fields( fuel_category_id=fuel_supply.fuel_category_id, end_use_id=fuel_supply.end_use_id, fuel_code_id=fuel_supply.fuel_code_id, - compliance_period=fe_data.compliance_period, + compliance_period="2024", ) @pytest.mark.anyio @pytest.mark.parametrize("case", test_cases) -async def test_compliance_units_calculation( +async def test_create_compliance_units_calculation( case, fuel_supply_action_service, mock_repo, mock_fuel_code_repo ): fe_data = FuelSupplyCreateUpdateSchema( @@ -522,14 +522,12 @@ async def test_compliance_units_calculation( fuel_category_id=2, # Adjusted to match the mock fuel_category end_use_id=1, fuel_code_id=1, - compliance_period="2024", # Schema-only field quantity=case["input"]["quantity"], units=case["input"]["units"], energy_density=case["input"]["energy_density"], group_uuid=str(uuid4()), version=0, - provisionOfTheActId=123, - exportDate=datetime.now().date(), + provision_of_the_act_id=123, ) # Mock standardized fuel data @@ -541,9 +539,6 @@ async def test_compliance_units_calculation( uci=None, ) - # Exclude invalid fields and set related objects - fe_data_dict = fe_data.model_dump(exclude=FUEL_SUPPLY_EXCLUDE_FIELDS) - # Mock the create_fuel_supply method to perform actual calculation async def create_fuel_supply_side_effect(fuel_supply): fuel_supply.fuel_supply_id = 1 @@ -561,7 +556,7 @@ async def create_fuel_supply_side_effect(fuel_supply): # Call the service to create the fuel supply result = await fuel_supply_action_service.create_fuel_supply( - fe_data, UserTypeEnum.SUPPLIER + fe_data, UserTypeEnum.SUPPLIER, "2024" ) # Assign mocked related objects for schema validation @@ -582,7 +577,7 @@ async def create_fuel_supply_side_effect(fuel_supply): fuel_category_id=fe_data.fuel_category_id, end_use_id=fe_data.end_use_id, fuel_code_id=fe_data.fuel_code_id, - compliance_period=fe_data.compliance_period, + compliance_period="2024", ) mock_repo.create_fuel_supply.assert_awaited_once() diff --git a/backend/lcfs/tests/fuel_supply/test_fuel_supplies_services.py b/backend/lcfs/tests/fuel_supply/test_fuel_supplies_services.py index bfe03cfb2..9b5114a84 100644 --- a/backend/lcfs/tests/fuel_supply/test_fuel_supplies_services.py +++ b/backend/lcfs/tests/fuel_supply/test_fuel_supplies_services.py @@ -1,17 +1,17 @@ -import uuid +from unittest.mock import MagicMock, AsyncMock + import pytest -from unittest.mock import MagicMock, AsyncMock, patch from fastapi import HTTPException +from lcfs.db.base import UserTypeEnum, ActionTypeEnum from lcfs.db.models import ( FuelType, EnergyEffectivenessRatio, EnergyDensity, - FuelCategory, ) -from lcfs.db.base import UserTypeEnum, ActionTypeEnum -from lcfs.web.api.fuel_supply.actions_service import FuelSupplyActionService +from lcfs.db.models.compliance.FuelSupply import FuelSupply from lcfs.web.api.fuel_code.repo import FuelCodeRepository +from lcfs.web.api.fuel_supply.actions_service import FuelSupplyActionService from lcfs.web.api.fuel_supply.repo import FuelSupplyRepository from lcfs.web.api.fuel_supply.schema import ( FuelSupplyCreateUpdateSchema, @@ -21,10 +21,8 @@ FuelTypeSchema, FuelCategoryResponseSchema, ) -from lcfs.db.models.compliance.FuelSupply import FuelSupply from lcfs.web.api.fuel_supply.services import FuelSupplyServices - # Fixture to set up the FuelSupplyServices with mocked dependencies # Mock common fuel type and fuel category for reuse fuel_type = FuelTypeSchema( @@ -113,7 +111,7 @@ async def test_update_fuel_supply_not_found(fuel_supply_action_service): user_type = UserTypeEnum.SUPPLIER with pytest.raises(HTTPException) as exc_info: - await service.update_fuel_supply(fs_data, user_type) + await service.update_fuel_supply(fs_data, user_type, "2024") assert exc_info.value.status_code == 404 assert exc_info.value.detail == "Fuel supply record not found." @@ -133,7 +131,6 @@ async def test_update_fuel_supply_success(fuel_supply_action_service): end_use_id=1, quantity=1000, units="L", - fuel_type_other=None, ci_of_fuel=10.5, energy_density=30.0, eer=1.0, @@ -226,7 +223,7 @@ async def test_update_fuel_supply_success(fuel_supply_action_service): user_type = UserTypeEnum.SUPPLIER # Call the service method - response = await service.update_fuel_supply(fs_data, user_type) + response = await service.update_fuel_supply(fs_data, user_type, "2024") # Assertions assert isinstance(response, FuelSupplyResponseSchema) @@ -245,7 +242,7 @@ async def test_update_fuel_supply_success(fuel_supply_action_service): fuel_category_id=fs_data.fuel_category_id, end_use_id=fs_data.end_use_id, fuel_code_id=fs_data.fuel_code_id, - compliance_period=fs_data.compliance_period, + compliance_period="2024", ) mock_repo.update_fuel_supply.assert_awaited_once_with(existing_fuel_supply) @@ -263,19 +260,6 @@ async def test_create_fuel_supply(fuel_supply_action_service): fuel_type_other=None, units="L", ) - new_fuel_supply = FuelSupply( - compliance_report_id=1, - fuel_type_id=1, - fuel_category_id=1, - provision_of_the_act_id=1, - quantity=2000, - units="L", - fuel_type_other=None, - group_uuid=str(uuid.uuid4()), - version=0, - user_type=UserTypeEnum.SUPPLIER, - action_type=ActionTypeEnum.CREATE, - ) mock_repo.create_fuel_supply = AsyncMock( return_value=MagicMock( fuel_supply_id=1, @@ -310,7 +294,7 @@ async def test_create_fuel_supply(fuel_supply_action_service): user_type = UserTypeEnum.SUPPLIER - response = await service.create_fuel_supply(fs_data, user_type) + response = await service.create_fuel_supply(fs_data, user_type, "2024") assert isinstance(response, FuelSupplyResponseSchema) mock_repo.create_fuel_supply.assert_awaited_once() @@ -319,7 +303,7 @@ async def test_create_fuel_supply(fuel_supply_action_service): fuel_category_id=fs_data.fuel_category_id, end_use_id=fs_data.end_use_id, fuel_code_id=fs_data.fuel_code_id, - compliance_period=fs_data.compliance_period, + compliance_period="2024", ) diff --git a/backend/lcfs/web/api/audit_log/services.py b/backend/lcfs/web/api/audit_log/services.py index 8a1a81529..d3ff1347a 100644 --- a/backend/lcfs/web/api/audit_log/services.py +++ b/backend/lcfs/web/api/audit_log/services.py @@ -76,9 +76,6 @@ async def get_audit_logs_paginated( offset, limit, conditions, pagination.sort_orders ) - if not audit_logs: - raise DataNotFoundException("No audit logs found") - processed_audit_logs = [] for audit_log in audit_logs: # Extract the changed_fields as a comma-separated string diff --git a/backend/lcfs/web/api/compliance_report/repo.py b/backend/lcfs/web/api/compliance_report/repo.py index 59696d6ab..80b18f67b 100644 --- a/backend/lcfs/web/api/compliance_report/repo.py +++ b/backend/lcfs/web/api/compliance_report/repo.py @@ -1,47 +1,46 @@ -import structlog -from typing import List, Optional, Dict from collections import defaultdict from datetime import datetime +from typing import List, Optional, Dict -from lcfs.db.models import UserProfile -from lcfs.db.models.organization.Organization import Organization -from lcfs.db.models.user.UserProfile import UserProfile -from lcfs.db.models.fuel.FuelType import FuelType -from lcfs.db.models.fuel.FuelCategory import FuelCategory -from lcfs.db.models.fuel.ExpectedUseType import ExpectedUseType -from sqlalchemy import func, select, and_, asc, desc -from sqlalchemy.orm import joinedload -from sqlalchemy.ext.asyncio import AsyncSession +import structlog from fastapi import Depends +from sqlalchemy import func, select, and_, asc, desc, update +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.orm import joinedload -from lcfs.web.api.base import ( - PaginationRequestSchema, - apply_filter_conditions, - get_field_for_filter, -) +from lcfs.db.dependencies import get_async_db_session from lcfs.db.models.compliance import CompliancePeriod +from lcfs.db.models.compliance.AllocationAgreement import AllocationAgreement from lcfs.db.models.compliance.ComplianceReport import ( ComplianceReport, ReportingFrequency, ) -from lcfs.db.models.compliance.ComplianceReportSummary import ComplianceReportSummary +from lcfs.db.models.compliance.ComplianceReportHistory import ComplianceReportHistory from lcfs.db.models.compliance.ComplianceReportStatus import ( ComplianceReportStatus, ComplianceReportStatusEnum, ) +from lcfs.db.models.compliance.ComplianceReportSummary import ComplianceReportSummary +from lcfs.db.models.compliance.FuelSupply import FuelSupply +from lcfs.db.models.compliance.OtherUses import OtherUses +from lcfs.db.models.fuel.ExpectedUseType import ExpectedUseType +from lcfs.db.models.fuel.FuelCategory import FuelCategory +from lcfs.db.models.fuel.FuelType import FuelType +from lcfs.db.models.initiative_agreement.InitiativeAgreement import InitiativeAgreement +from lcfs.db.models.organization.Organization import Organization +from lcfs.db.models.transfer.Transfer import Transfer +from lcfs.db.models.user.UserProfile import UserProfile +from lcfs.web.api.base import ( + PaginationRequestSchema, + apply_filter_conditions, + get_field_for_filter, +) from lcfs.web.api.compliance_report.schema import ( ComplianceReportBaseSchema, ComplianceReportSummaryUpdateSchema, ) -from lcfs.db.models.compliance.ComplianceReportHistory import ComplianceReportHistory -from lcfs.web.core.decorators import repo_handler -from lcfs.db.dependencies import get_async_db_session -from lcfs.db.models.compliance.OtherUses import OtherUses -from lcfs.db.models.transfer.Transfer import Transfer -from lcfs.db.models.initiative_agreement.InitiativeAgreement import InitiativeAgreement -from lcfs.db.models.compliance.AllocationAgreement import AllocationAgreement -from lcfs.db.models.compliance.FuelSupply import FuelSupply from lcfs.web.api.fuel_supply.repo import FuelSupplyRepository +from lcfs.web.core.decorators import repo_handler logger = structlog.get_logger(__name__) @@ -570,6 +569,16 @@ async def add_compliance_report_summary( await self.db.refresh(summary) return summary + @repo_handler + async def reset_summary_lock(self, compliance_report_id: int): + query = ( + update(ComplianceReportSummary) + .where(ComplianceReportSummary.compliance_report_id == compliance_report_id) + .values(is_locked=False) + ) + await self.db.execute(query) + return True + @repo_handler async def save_compliance_report_summary( self, summary: ComplianceReportSummaryUpdateSchema diff --git a/backend/lcfs/web/api/compliance_report/schema.py b/backend/lcfs/web/api/compliance_report/schema.py index e839bee11..aa8e34858 100644 --- a/backend/lcfs/web/api/compliance_report/schema.py +++ b/backend/lcfs/web/api/compliance_report/schema.py @@ -1,12 +1,13 @@ from enum import Enum -from typing import ClassVar, Optional, List, Union +from typing import ClassVar, Optional, List from datetime import datetime, date from enum import Enum +from lcfs.db.models.compliance.ComplianceReportStatus import ComplianceReportStatusEnum from lcfs.web.api.fuel_code.schema import EndUseTypeSchema, EndUserTypeSchema from lcfs.web.api.base import BaseSchema, FilterModel, SortOrder from lcfs.web.api.base import PaginationResponseSchema -from pydantic import Field, Extra +from pydantic import Field """ Base - all shared attributes of a resource @@ -17,6 +18,18 @@ """ +class ReturnStatus(Enum): + ANALYST = "Return to analyst" + MANAGER = "Return to manager" + SUPPLIER = "Return to supplier" + +RETURN_STATUS_MAPPER = { + ReturnStatus.ANALYST.value: ComplianceReportStatusEnum.Submitted.value, + ReturnStatus.MANAGER.value: ComplianceReportStatusEnum.Recommended_by_analyst.value, + ReturnStatus.SUPPLIER.value: ComplianceReportStatusEnum.Draft.value, +} + + class SupplementalInitiatorType(str, Enum): SUPPLIER_SUPPLEMENTAL = "Supplier Supplemental" GOVERNMENT_REASSESSMENT = "Government Reassessment" @@ -45,7 +58,7 @@ class SummarySchema(BaseSchema): is_locked: bool class Config: - extra = Extra.allow + extra = 'allow' class ComplianceReportStatusSchema(BaseSchema): diff --git a/backend/lcfs/web/api/compliance_report/services.py b/backend/lcfs/web/api/compliance_report/services.py index 880a94be7..bc3588f97 100644 --- a/backend/lcfs/web/api/compliance_report/services.py +++ b/backend/lcfs/web/api/compliance_report/services.py @@ -171,10 +171,7 @@ async def get_compliance_reports_paginated( pagination, organization_id ) - if not reports: - raise DataNotFoundException("No compliance reports found.") - - if bceid_user: + if bceid_user and reports: reports = self._mask_report_status(reports) return ComplianceReportListSchema( diff --git a/backend/lcfs/web/api/compliance_report/update_service.py b/backend/lcfs/web/api/compliance_report/update_service.py index 1a1d7d9c7..d58c51cff 100644 --- a/backend/lcfs/web/api/compliance_report/update_service.py +++ b/backend/lcfs/web/api/compliance_report/update_service.py @@ -1,4 +1,5 @@ import json +from typing import Tuple from fastapi import Depends, HTTPException, Request from lcfs.web.api.notification.schema import ( COMPLIANCE_REPORT_STATUS_NOTIFICATION_MAPPER, @@ -12,7 +13,11 @@ from lcfs.db.models.transaction.Transaction import TransactionActionEnum from lcfs.db.models.user.Role import RoleEnum from lcfs.web.api.compliance_report.repo import ComplianceReportRepository -from lcfs.web.api.compliance_report.schema import ComplianceReportUpdateSchema +from lcfs.web.api.compliance_report.schema import ( + RETURN_STATUS_MAPPER, + ComplianceReportUpdateSchema, + ReturnStatus, +) from lcfs.web.api.compliance_report.summary_service import ( ComplianceReportSummaryService, ) @@ -39,48 +44,67 @@ def __init__( self.trx_service = trx_service self.notfn_service = notfn_service - async def update_compliance_report( - self, report_id: int, report_data: ComplianceReportUpdateSchema - ) -> ComplianceReport: - """Updates an existing compliance report.""" - RETURN_STATUSES = ["Return to analyst", "Return to manager"] + async def _handle_return_status( + self, report_data: ComplianceReportUpdateSchema + ) -> Tuple[str, bool]: + """Handle return status logic and return new status and change flag.""" + mapped_status = RETURN_STATUS_MAPPER.get(report_data.status) + return mapped_status, False + + async def _check_report_exists(self, report_id: int) -> ComplianceReport: + """Verify report exists and return it.""" report = await self.repo.get_compliance_report_by_id(report_id, is_model=True) if not report: raise DataNotFoundException( f"Compliance report with ID {report_id} not found" ) + return report + + async def update_compliance_report( + self, report_id: int, report_data: ComplianceReportUpdateSchema + ) -> ComplianceReport: + """Updates an existing compliance report.""" + # Get and validate report + report = await self._check_report_exists(report_id) + + # Store original status current_status = report_data.status - # if we're just returning the compliance report back to either compliance manager or analyst, - # then neither history nor any updates to summary is required. - if report_data.status in RETURN_STATUSES: - status_has_changed = False - notifications = COMPLIANCE_REPORT_STATUS_NOTIFICATION_MAPPER.get( - report_data.status + + # Handle status changes + if report_data.status in [status.value for status in ReturnStatus]: + new_status, status_has_changed = await self._handle_return_status( + report_data ) - if report_data.status == "Return to analyst": - report_data.status = ComplianceReportStatusEnum.Submitted.value - else: - report_data.status = ( - ComplianceReportStatusEnum.Recommended_by_analyst.value - ) + report_data.status = new_status + + # Handle "Return to supplier" + if current_status == ReturnStatus.SUPPLIER.value: + await self.repo.reset_summary_lock(report.compliance_report_id) else: + # Handle normal status change status_has_changed = report.current_status.status != getattr( ComplianceReportStatusEnum, report_data.status.replace(" ", "_") ) + + # Get new status object new_status = await self.repo.get_compliance_report_status_by_desc( report_data.status ) - # Update fields + + # Update report report.current_status = new_status report.supplemental_note = report_data.supplemental_note - updated_report = await self.repo.update_compliance_report(report) + + # Handle status change related actions if status_has_changed: await self.handle_status_change(report, new_status.status) # Add history record await self.repo.add_compliance_report_history(report, self.request.user) + # Handle notifications await self._perform_notification_call(report, current_status) + return updated_report async def _perform_notification_call(self, report, status): @@ -102,7 +126,7 @@ async def _perform_notification_call(self, report, status): "status": status.lower(), } notification_data = NotificationMessageSchema( - type=f"Compliance report {status.lower()}", + type=f"Compliance report {status.lower().replace('return', 'returned')}", related_transaction_id=f"CR{report.compliance_report_id}", message=json.dumps(message_data), related_organization_id=report.organization_id, @@ -228,12 +252,18 @@ async def handle_submitted_status(self, report: ComplianceReport): report.summary = new_summary if report.summary.line_20_surplus_deficit_units != 0: - # Create a new reserved transaction for receiving organization - report.transaction = await self.org_service.adjust_balance( - transaction_action=TransactionActionEnum.Reserved, - compliance_units=report.summary.line_20_surplus_deficit_units, - organization_id=report.organization_id, - ) + if report.transaction is not None: + # Update existing transaction + report.transaction.compliance_units = ( + report.summary.line_20_surplus_deficit_units + ) + else: + # Create a new reserved transaction for receiving organization + report.transaction = await self.org_service.adjust_balance( + transaction_action=TransactionActionEnum.Reserved, + compliance_units=report.summary.line_20_surplus_deficit_units, + organization_id=report.organization_id, + ) await self.repo.update_compliance_report(report) return calculated_summary diff --git a/backend/lcfs/web/api/compliance_report/validation.py b/backend/lcfs/web/api/compliance_report/validation.py index 4f11e4a1c..91bd315d2 100644 --- a/backend/lcfs/web/api/compliance_report/validation.py +++ b/backend/lcfs/web/api/compliance_report/validation.py @@ -25,10 +25,19 @@ async def validate_organization_access(self, compliance_report_id: int): ) organization_id = compliance_report.organization_id - user_organization_id = self.request.user.organization.organization_id if self.request.user.organization else None + user_organization_id = ( + self.request.user.organization.organization_id + if self.request.user.organization + else None + ) - if not user_has_roles(self.request.user, [RoleEnum.GOVERNMENT]) and organization_id != user_organization_id: + if ( + not user_has_roles(self.request.user, [RoleEnum.GOVERNMENT]) + and organization_id != user_organization_id + ): raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="User does not have access to this compliance report.", ) + + return compliance_report diff --git a/backend/lcfs/web/api/fuel_code/repo.py b/backend/lcfs/web/api/fuel_code/repo.py index 325c3579b..174ee126b 100644 --- a/backend/lcfs/web/api/fuel_code/repo.py +++ b/backend/lcfs/web/api/fuel_code/repo.py @@ -806,9 +806,9 @@ async def get_energy_effectiveness_ratio( return energy_effectiveness_ratio @repo_handler - async def get_target_carbon_intensities( + async def get_target_carbon_intensity( self, fuel_category_id: int, compliance_period: str - ) -> List[TargetCarbonIntensity]: + ) -> TargetCarbonIntensity: compliance_period_id_subquery = ( select(CompliancePeriod.compliance_period_id) @@ -830,7 +830,7 @@ async def get_target_carbon_intensities( ) result = await self.db.execute(stmt) - return result.scalars().all() + return result.scalar_one() @repo_handler async def get_standardized_fuel_data( @@ -876,15 +876,10 @@ async def get_standardized_fuel_data( eer = energy_effectiveness.ratio if energy_effectiveness else 1.0 # Fetch target carbon intensity (TCI) - target_ci = None - target_carbon_intensities = await self.get_target_carbon_intensities( + target_carbon_intensity = await self.get_target_carbon_intensity( fuel_category_id, compliance_period ) - if target_carbon_intensities: - target_ci = next( - (tci.target_carbon_intensity for tci in target_carbon_intensities), - 0.0, - ) + target_ci = target_carbon_intensity.target_carbon_intensity # Additional Carbon Intensity (UCI) uci = await self.get_additional_carbon_intensity(fuel_type_id, end_use_id) diff --git a/backend/lcfs/web/api/fuel_export/actions_service.py b/backend/lcfs/web/api/fuel_export/actions_service.py index cd758ca85..66247d267 100644 --- a/backend/lcfs/web/api/fuel_export/actions_service.py +++ b/backend/lcfs/web/api/fuel_export/actions_service.py @@ -1,13 +1,13 @@ import uuid from logging import getLogger -from typing import Optional + from fastapi import Depends, HTTPException from lcfs.db.base import ActionTypeEnum, UserTypeEnum -from lcfs.db.models.compliance.FuelExport import FuelExport from lcfs.db.models.compliance.ComplianceReport import QuantityUnitsEnum -from lcfs.web.api.fuel_export.repo import FuelExportRepository +from lcfs.db.models.compliance.FuelExport import FuelExport from lcfs.web.api.fuel_code.repo import FuelCodeRepository +from lcfs.web.api.fuel_export.repo import FuelExportRepository from lcfs.web.api.fuel_export.schema import ( DeleteFuelExportResponseSchema, FuelExportCreateUpdateSchema, diff --git a/backend/lcfs/web/api/fuel_export/services.py b/backend/lcfs/web/api/fuel_export/services.py index 1a3279b5c..3fc118a71 100644 --- a/backend/lcfs/web/api/fuel_export/services.py +++ b/backend/lcfs/web/api/fuel_export/services.py @@ -1,13 +1,15 @@ -import structlog import math + +import structlog from fastapi import Depends, Request -from fastapi_cache.decorator import cache +from lcfs.utils.constants import default_ci from lcfs.web.api.base import ( PaginationRequestSchema, PaginationResponseSchema, - lcfs_cache_key_builder, ) +from lcfs.web.api.compliance_report.repo import ComplianceReportRepository +from lcfs.web.api.fuel_export.repo import FuelExportRepository from lcfs.web.api.fuel_export.schema import ( EndUseTypeSchema, EnergyDensitySchema, @@ -22,13 +24,8 @@ TargetCarbonIntensitySchema, UnitOfMeasureSchema, ) -from lcfs.web.api.fuel_export.repo import FuelExportRepository -from lcfs.db.models.compliance.ComplianceReport import QuantityUnitsEnum -from lcfs.web.api.compliance_report.repo import ComplianceReportRepository from lcfs.web.api.fuel_export.validation import FuelExportValidation from lcfs.web.core.decorators import service_handler -from lcfs.web.utils.calculations import calculate_compliance_units -from lcfs.utils.constants import default_ci logger = structlog.get_logger(__name__) diff --git a/backend/lcfs/web/api/fuel_supply/actions_service.py b/backend/lcfs/web/api/fuel_supply/actions_service.py index 642e97f7b..f2346b06f 100644 --- a/backend/lcfs/web/api/fuel_supply/actions_service.py +++ b/backend/lcfs/web/api/fuel_supply/actions_service.py @@ -1,12 +1,11 @@ import uuid from logging import getLogger -from typing import Optional from fastapi import Depends, HTTPException from lcfs.db.base import ActionTypeEnum, UserTypeEnum -from lcfs.db.models.compliance.FuelSupply import FuelSupply from lcfs.db.models.compliance.ComplianceReport import QuantityUnitsEnum +from lcfs.db.models.compliance.FuelSupply import FuelSupply from lcfs.web.api.fuel_code.repo import FuelCodeRepository from lcfs.web.api.fuel_supply.repo import FuelSupplyRepository from lcfs.web.api.fuel_supply.schema import ( @@ -49,7 +48,10 @@ def __init__( self.fuel_repo = fuel_repo async def _populate_fuel_supply_fields( - self, fuel_supply: FuelSupply, fs_data: FuelSupplyCreateUpdateSchema + self, + fuel_supply: FuelSupply, + fs_data: FuelSupplyCreateUpdateSchema, + compliance_period: str, ) -> FuelSupply: """ Populate additional calculated and referenced fields for a FuelSupply instance. @@ -66,8 +68,8 @@ async def _populate_fuel_supply_fields( fuel_type_id=fuel_supply.fuel_type_id, fuel_category_id=fuel_supply.fuel_category_id, end_use_id=fuel_supply.end_use_id, + compliance_period=compliance_period, fuel_code_id=fuel_supply.fuel_code_id, - compliance_period=fs_data.compliance_period, ) # Set units @@ -105,7 +107,10 @@ async def _populate_fuel_supply_fields( @service_handler async def create_fuel_supply( - self, fs_data: FuelSupplyCreateUpdateSchema, user_type: UserTypeEnum + self, + fs_data: FuelSupplyCreateUpdateSchema, + user_type: UserTypeEnum, + compliance_period: str, ) -> FuelSupplyResponseSchema: """ Create a new fuel supply record. @@ -117,6 +122,7 @@ async def create_fuel_supply( Args: fs_data (FuelSupplyCreateUpdateSchema): The data for the new fuel supply. user_type (UserTypeEnum): The type of user creating the record. + compliance_period (int): The compliance period for the new record. Returns: FuelSupplyResponseSchema: The newly created fuel supply record as a response schema. @@ -132,7 +138,9 @@ async def create_fuel_supply( ) # Populate calculated and referenced fields - fuel_supply = await self._populate_fuel_supply_fields(fuel_supply, fs_data) + fuel_supply = await self._populate_fuel_supply_fields( + fuel_supply, fs_data, compliance_period + ) # Save the populated fuel supply record created_supply = await self.repo.create_fuel_supply(fuel_supply) @@ -140,7 +148,10 @@ async def create_fuel_supply( @service_handler async def update_fuel_supply( - self, fs_data: FuelSupplyCreateUpdateSchema, user_type: UserTypeEnum + self, + fs_data: FuelSupplyCreateUpdateSchema, + user_type: UserTypeEnum, + compliance_period: str, ) -> FuelSupplyResponseSchema: """ Update an existing fuel supply record or create a new version if necessary. @@ -153,6 +164,7 @@ async def update_fuel_supply( Args: fs_data (FuelSupplyCreateUpdateSchema): The data for the fuel supply update. user_type (UserTypeEnum): The type of user performing the update. + compliance_period (str): The compliance period for the new record. Returns: FuelSupplyResponseSchema: The updated or new version of the fuel supply record. @@ -177,7 +189,7 @@ async def update_fuel_supply( # Populate calculated fields existing_fuel_supply = await self._populate_fuel_supply_fields( - existing_fuel_supply, fs_data + existing_fuel_supply, fs_data, compliance_period ) updated_supply = await self.repo.update_fuel_supply(existing_fuel_supply) @@ -204,7 +216,9 @@ async def update_fuel_supply( setattr(fuel_supply, field, value) # Populate calculated fields - fuel_supply = await self._populate_fuel_supply_fields(fuel_supply, fs_data) + fuel_supply = await self._populate_fuel_supply_fields( + fuel_supply, fs_data, compliance_period + ) # Save the new version new_supply = await self.repo.create_fuel_supply(fuel_supply) diff --git a/backend/lcfs/web/api/fuel_supply/schema.py b/backend/lcfs/web/api/fuel_supply/schema.py index 60592dffe..3ca83878c 100644 --- a/backend/lcfs/web/api/fuel_supply/schema.py +++ b/backend/lcfs/web/api/fuel_supply/schema.py @@ -114,7 +114,6 @@ class FuelSupplyCreateUpdateSchema(BaseSchema): fuel_supply_id: Optional[int] = None group_uuid: Optional[str] = None version: Optional[int] = None - compliance_period: Optional[str] = None fuel_type_id: int fuel_category_id: int end_use_id: Optional[int] = None diff --git a/backend/lcfs/web/api/fuel_supply/views.py b/backend/lcfs/web/api/fuel_supply/views.py index 3f5674408..cb95c2597 100644 --- a/backend/lcfs/web/api/fuel_supply/views.py +++ b/backend/lcfs/web/api/fuel_supply/views.py @@ -1,12 +1,13 @@ -import structlog -from typing import Optional, Union +from typing import Union +import structlog from fastapi import APIRouter, Body, Depends, Request, Response, status, HTTPException from starlette.responses import JSONResponse from lcfs.db.models.user.Role import RoleEnum from lcfs.web.api.base import PaginationRequestSchema from lcfs.web.api.compliance_report.validation import ComplianceReportValidation +from lcfs.web.api.fuel_supply.actions_service import FuelSupplyActionService from lcfs.web.api.fuel_supply.schema import ( DeleteFuelSupplyResponseSchema, FuelSuppliesSchema, @@ -17,7 +18,6 @@ ) from lcfs.web.api.fuel_supply.services import FuelSupplyServices from lcfs.web.api.fuel_supply.validation import FuelSupplyValidation -from lcfs.web.api.fuel_supply.actions_service import FuelSupplyActionService from lcfs.web.core.decorators import view_handler router = APIRouter() @@ -81,7 +81,9 @@ async def save_fuel_supply_row( ): """Endpoint to save single fuel supply row""" compliance_report_id = request_data.compliance_report_id - await report_validate.validate_organization_access(compliance_report_id) + compliance_report = await report_validate.validate_organization_access( + compliance_report_id + ) # Determine user type for record creation current_user_type = request.user.user_type @@ -102,12 +104,16 @@ async def save_fuel_supply_row( if request_data.fuel_supply_id: # Update existing fuel supply row using actions service return await action_service.update_fuel_supply( - request_data, current_user_type + request_data, + current_user_type, + compliance_report.compliance_period.description, ) else: # Create new fuel supply row using actions service return await action_service.create_fuel_supply( - request_data, current_user_type + request_data, + current_user_type, + compliance_report.compliance_period.description, ) diff --git a/backend/lcfs/web/api/notification/repo.py b/backend/lcfs/web/api/notification/repo.py index bd9d874fa..10b238604 100644 --- a/backend/lcfs/web/api/notification/repo.py +++ b/backend/lcfs/web/api/notification/repo.py @@ -124,7 +124,9 @@ def _apply_notification_filters( ) ) elif filter.field == "transaction_id": - field = get_field_for_filter(NotificationMessage, 'related_transaction_id') + field = get_field_for_filter( + NotificationMessage, "related_transaction_id" + ) conditions.append( apply_filter_conditions( field, filter_value, filter_option, filter_type @@ -353,11 +355,6 @@ async def get_notification_channel_subscriptions_by_user( result = await self.db.execute(query) subscriptions = result.scalars().all() - if not subscriptions: - raise DataNotFoundException( - f"Channel subscriptions not found for user id: '{user_profile_id}'" - ) - return subscriptions @repo_handler diff --git a/backend/lcfs/web/api/notification/schema.py b/backend/lcfs/web/api/notification/schema.py index 30466bfa8..31f0f7270 100644 --- a/backend/lcfs/web/api/notification/schema.py +++ b/backend/lcfs/web/api/notification/schema.py @@ -117,6 +117,9 @@ class NotificationRequestSchema(BaseSchema): "Return to manager": [ NotificationTypeEnum.IDIR_COMPLIANCE_MANAGER__COMPLIANCE_REPORT__ANALYST_RECOMMENDATION ], + "Return to supplier": [ + NotificationTypeEnum.BCEID__COMPLIANCE_REPORT__DIRECTOR_ASSESSMENT + ] } diff --git a/backend/lcfs/web/api/notional_transfer/services.py b/backend/lcfs/web/api/notional_transfer/services.py index 207707bf6..5b66c5cfc 100644 --- a/backend/lcfs/web/api/notional_transfer/services.py +++ b/backend/lcfs/web/api/notional_transfer/services.py @@ -6,10 +6,10 @@ from fastapi import Depends from lcfs.db.base import UserTypeEnum, ActionTypeEnum -from lcfs.web.api.notional_transfer.repo import NotionalTransferRepository -from lcfs.web.core.decorators import service_handler from lcfs.db.models.compliance.NotionalTransfer import NotionalTransfer from lcfs.web.api.base import PaginationRequestSchema, PaginationResponseSchema +from lcfs.web.api.fuel_code.repo import FuelCodeRepository +from lcfs.web.api.notional_transfer.repo import NotionalTransferRepository from lcfs.web.api.notional_transfer.schema import ( NotionalTransferCreateSchema, NotionalTransferSchema, @@ -19,7 +19,7 @@ NotionalTransfersAllSchema, DeleteNotionalTransferResponseSchema, ) -from lcfs.web.api.fuel_code.repo import FuelCodeRepository +from lcfs.web.core.decorators import service_handler logger = structlog.get_logger(__name__) diff --git a/backend/lcfs/web/api/organization/services.py b/backend/lcfs/web/api/organization/services.py index 5ebf3573d..9371c6347 100644 --- a/backend/lcfs/web/api/organization/services.py +++ b/backend/lcfs/web/api/organization/services.py @@ -102,7 +102,7 @@ def apply_transaction_filters(self, pagination, conditions): # For non-date filters, use the standard filter value filter_value = filter.filter - if field.description == 'transaction_type': + if field.description == "transaction_type": filter_value = filter_value.replace(" ", "").lower() filter_option = filter.type @@ -169,9 +169,6 @@ async def get_transactions_paginated( ) ) - if not transactions: - raise DataNotFoundException("Transactions not found") - return { "transactions": [ TransactionViewSchema.model_validate(transaction) diff --git a/backend/lcfs/web/api/organizations/services.py b/backend/lcfs/web/api/organizations/services.py index 35c2155a3..5014760d5 100644 --- a/backend/lcfs/web/api/organizations/services.py +++ b/backend/lcfs/web/api/organizations/services.py @@ -258,9 +258,6 @@ async def get_organizations( offset, limit, conditions, pagination ) - if not organizations: - raise DataNotFoundException("Organizations not found") - return OrganizationListSchema( organizations=organizations, pagination=PaginationResponseSchema( diff --git a/backend/lcfs/web/api/transaction/services.py b/backend/lcfs/web/api/transaction/services.py index 9c2bf0011..1abdfc2f0 100644 --- a/backend/lcfs/web/api/transaction/services.py +++ b/backend/lcfs/web/api/transaction/services.py @@ -121,9 +121,6 @@ async def get_transactions_paginated( offset, limit, conditions, pagination.sort_orders, None ) - if not transactions: - raise DataNotFoundException("Transactions not found") - return { "transactions": [ TransactionViewSchema.model_validate(transaction) diff --git a/frontend/src/assets/locales/en/reports.json b/frontend/src/assets/locales/en/reports.json index 91ced911b..a9d80c348 100644 --- a/frontend/src/assets/locales/en/reports.json +++ b/frontend/src/assets/locales/en/reports.json @@ -39,17 +39,19 @@ "submitReportBtn": "Submit report", "returnToAnalyst": "Return to analyst", "returnToManager": "Return to compliance manager", + "returnToSupplier": "Return report to the supplier", "recommendReportAnalystBtn": "Recommend to compliance manager", "recommendReportManagerBtn": "Recommend to director", "assessReportBtn": "Issue assessment", "reAssessReportBtn": "Re-assess report" }, - "savedSuccessText": "Compliance report successfully saved", + "savedSuccessText": "Compliance report successfully {{status}}", "submitConfirmText": "Are you sure you want to sign and submit this compliance report?", "deleteConfirmText": "Are you sure you want to delete this compliance report?", "recommendConfirmText": "Are you sure you want to recommend this compliance report?", "returnToAnalystConfirmText": "Are you sure you want to return this compliance report back to analyst?", "returnToManagerConfirmText": "Are you sure you want to return this compliance report back to compliance manager?", + "returnToSupplierConfirmText": "Are you sure you want to return this compliance report back to the supplier?", "assessConfirmText": "Are you sure you want to assess this compliance report?", "uploadLabel": "Upload supporting documents for your report.", "introduction": "Introduction", diff --git a/frontend/src/components/BCDataGrid/components/Editors/NumberEditor.jsx b/frontend/src/components/BCDataGrid/components/Editors/NumberEditor.jsx index bbfc263d5..54bc8e396 100644 --- a/frontend/src/components/BCDataGrid/components/Editors/NumberEditor.jsx +++ b/frontend/src/components/BCDataGrid/components/Editors/NumberEditor.jsx @@ -1,4 +1,4 @@ -import { forwardRef, useImperativeHandle, useRef, useState } from 'react' +import { forwardRef, useEffect, useImperativeHandle, useRef } from 'react' import { TextField } from '@mui/material' import { styled } from '@mui/material/styles' @@ -24,6 +24,12 @@ export const NumberEditor = forwardRef( ({ value, onValueChange, eventKey, rowIndex, column, ...props }, ref) => { const inputRef = useRef(null) + useEffect(() => { + if (inputRef) { + inputRef.current.focus() + } + }, []) + useImperativeHandle(ref, () => { return { getValue() { @@ -58,7 +64,7 @@ export const NumberEditor = forwardRef( return ( { onValueChange(event.target.value) } + + const inputRef = useRef(null) + + useEffect(() => { + if (inputRef) { + inputRef.current.focus() + } + }, []) + return ( <> {() => ( const checkedIcon = - +/** + * @deprecated + */ export const BCColumnSetFilter = forwardRef((props, ref) => { const { apiQuery, params } = props const [options, setOptions] = useState([]) @@ -71,7 +73,12 @@ export const BCColumnSetFilter = forwardRef((props, ref) => { limitTags={1} className="bc-column-set-filter ag-list ag-select-list ag-ltr ag-popup-child ag-popup-positioned-under" role="list-box" - sx={{ width: '100%' }} + sx={{ + width: '100%', + '.MuiInputBase-root': { + borderRadius: 'inherit' + } + }} options={options} loading={optionsIsLoading} autoHighlight @@ -126,13 +133,6 @@ export const BCColumnSetFilter = forwardRef((props, ref) => { BCColumnSetFilter.displayName = 'BCColumnSetFilter' -BCColumnSetFilter.defaultProps = { - // apiQuery: () => ({ data: [], isLoading: false }), - apiOptionField: 'name', - multiple: false, - disableCloseOnSelect: false -} - BCColumnSetFilter.propTypes = { apiQuery: PropTypes.func.isRequired, // react query or a fetch query which will return data, isLoading and Error fields. // for static data, use the following format: diff --git a/frontend/src/components/BCDataGrid/components/Filters/BCDateFloatingFilter.jsx b/frontend/src/components/BCDataGrid/components/Filters/BCDateFloatingFilter.jsx new file mode 100644 index 000000000..6a924492b --- /dev/null +++ b/frontend/src/components/BCDataGrid/components/Filters/BCDateFloatingFilter.jsx @@ -0,0 +1,128 @@ +import { useState, useEffect, useCallback } from 'react' +import { FormControl, IconButton, InputAdornment } from '@mui/material' +import { + Clear as ClearIcon, + CalendarToday as CalendarIcon +} from '@mui/icons-material' +import { DatePicker } from '@mui/x-date-pickers' +import { format, isValid } from 'date-fns' + +export const BCDateFloatingFilter = ({ + model, + onModelChange, + disabled = false, + initialFilterType = 'equals', + label = 'Select Date' +}) => { + const [selectedDate, setSelectedDate] = useState(null) + const [open, setOpen] = useState(false) + + const handleChange = useCallback((newDate) => { + setSelectedDate(newDate) + + if (newDate && isValid(newDate)) { + onModelChange({ + type: initialFilterType, + dateFrom: format(newDate, 'yyyy-MM-dd'), + dateTo: null, + filterType: 'date' + }) + } else { + onModelChange(null) + } + }, []) + + const handleClear = (event) => { + event.stopPropagation() + setSelectedDate(null) + onModelChange(null) + } + + const handleOpen = () => { + setOpen(true) + } + + const handleClose = () => { + setOpen(false) + } + + useEffect(() => { + if (!model) { + setSelectedDate(null) + return + } + + if (model.filter) { + const date = new Date(model.dateFrom) + setSelectedDate(isValid(date) ? date : null) + } + }, [model]) + + return ( + + + setOpen(true)} + aria-label="Open calendar" + > + + + + ), + endAdornment: selectedDate && ( + + event.stopPropagation()} + edge="end" + aria-label="Clear date" + > + + + + ) + } + } + }} + /> + + ) +} + +BCDateFloatingFilter.displayName = 'BCDateFloatingFilter' diff --git a/frontend/src/components/BCDataGrid/components/Filters/BCSelectFloatingFilter.jsx b/frontend/src/components/BCDataGrid/components/Filters/BCSelectFloatingFilter.jsx new file mode 100644 index 000000000..ccb30136d --- /dev/null +++ b/frontend/src/components/BCDataGrid/components/Filters/BCSelectFloatingFilter.jsx @@ -0,0 +1,140 @@ +import { useState, useCallback, useEffect } from 'react' +import { IconButton } from '@mui/material' +import { Clear as ClearIcon } from '@mui/icons-material' +const ITEM_HEIGHT = 48 +const ITEM_PADDING_TOP = 8 + +export const BCSelectFloatingFilter = ({ + model, + onModelChange, + optionsQuery, + valueKey = 'value', + labelKey = 'label', + disabled = false, + params, + initialFilterType = 'equals', + multiple = false, + initialSelectedValues = [] +}) => { + const [selectedValues, setSelectedValues] = useState([]) + const { data: optionsData, isLoading, isError, error } = optionsQuery(params) + + const handleChange = (event) => { + const { options } = event.target + const newValues = Array.from(options) + .filter((option) => option.selected) + .map((option) => option.value) + + if (!multiple) { + setSelectedValues([newValues[0] || '']) + onModelChange( + !newValues[0] || newValues[0] === '0' + ? null + : { + type: initialFilterType, + filter: newValues[0] + } + ) + } else { + setSelectedValues(newValues) + onModelChange({ + type: initialFilterType, + filter: newValues + }) + } + } + + const handleClear = (event) => { + event.stopPropagation() + setSelectedValues([]) + onModelChange(null) + } + + const renderSelectContent = useCallback(() => { + if (isLoading) { + return ( + + ) + } + + if (isError) { + return ( + + ) + } + + return (optionsData || []).map((option) => ( + + )) + }, [isLoading, isError, optionsData, error]) + + useEffect(() => { + if (!model) { + setSelectedValues(initialSelectedValues) + } else { + setSelectedValues([model?.filter]) + } + }, [model, initialSelectedValues]) + + return ( +
+
0} + aria-controls="select-filter" + > + + {selectedValues.length > 0 && ( + event.stopPropagation()} + aria-label="Clear selection" + > + + + )} +
+
+ ) +} + +BCSelectFloatingFilter.displayName = 'BCSelectFloatingFilter' diff --git a/frontend/src/components/BCDataGrid/components/index.js b/frontend/src/components/BCDataGrid/components/index.js index 14d4ca2be..16e855ef3 100644 --- a/frontend/src/components/BCDataGrid/components/index.js +++ b/frontend/src/components/BCDataGrid/components/index.js @@ -13,3 +13,5 @@ export { BCPagination } from './StatusBar/BCPagination' export { LargeTextareaEditor } from './Editors/LargeTextareaEditor' export { TextCellEditor } from './Editors/TextCellEditor' export { NumberEditor } from './Editors/NumberEditor' +export { BCDateFloatingFilter } from './Filters/BCDateFloatingFilter' +export { BCSelectFloatingFilter } from './Filters/BCSelectFloatingFilter' diff --git a/frontend/src/components/BCNavbar/components/DefaultNavbarLink.jsx b/frontend/src/components/BCNavbar/components/DefaultNavbarLink.jsx index 347b5d6b3..4031baa69 100644 --- a/frontend/src/components/BCNavbar/components/DefaultNavbarLink.jsx +++ b/frontend/src/components/BCNavbar/components/DefaultNavbarLink.jsx @@ -66,16 +66,17 @@ function DefaultNavbarLink({ onMouseLeave={() => setHover(false)} onClick={onClick} > - {icon && ( + {icon && typeof icon === 'string' ? ( - light ? primary.main : secondary.main, + color: '#fff', verticalAlign: 'middle' }} > {icon} + ) : ( + <>{icon} )} { }, onSettled: () => { queryClient.invalidateQueries(['compliance-report', reportID]) + queryClient.invalidateQueries(['compliance-report-summary', reportID]) + queryClient.invalidateQueries(['compliance-reports']) } }) } diff --git a/frontend/src/layouts/MainLayout/components/UserProfileActions.jsx b/frontend/src/layouts/MainLayout/components/UserProfileActions.jsx index 37ebd0557..cfdf3c4ec 100644 --- a/frontend/src/layouts/MainLayout/components/UserProfileActions.jsx +++ b/frontend/src/layouts/MainLayout/components/UserProfileActions.jsx @@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next' import BCBox from '@/components/BCBox' import BCButton from '@/components/BCButton' import BCTypography from '@/components/BCTypography' +import DefaultNavbarLink from '@/components/BCNavbar/components/DefaultNavbarLink' import { useCurrentUser } from '@/hooks/useCurrentUser' import { useNotificationsCount } from '@/hooks/useNotifications' import { @@ -15,14 +16,13 @@ import { Tooltip } from '@mui/material' import NotificationsIcon from '@mui/icons-material/Notifications' -import { useNavigate, useLocation } from 'react-router-dom' +import { useLocation } from 'react-router-dom' import { ROUTES } from '@/constants/routes' export const UserProfileActions = () => { const { t } = useTranslation() const { data: currentUser } = useCurrentUser() const { keycloak } = useKeycloak() - const navigate = useNavigate() const location = useLocation() // TODO: @@ -44,6 +44,22 @@ export const UserProfileActions = () => { refetch() }, [location, refetch]) + const iconBtn = ( + + 0 ? notificationsCount : null} + color="error" + > + + + + ) + return ( keycloak.authenticated && ( { ) : ( - navigate(ROUTES.NOTIFICATIONS)} - aria-label={t('Notifications')} - > - 0 ? notificationsCount : null - } - color="error" - > - - - + )} li:hover, & > li:focus, & > li:blur': { backgroundColor: primary.light, diff --git a/frontend/src/views/Admin/AdminMenu/components/UserActivity.jsx b/frontend/src/views/Admin/AdminMenu/components/UserActivity.jsx index d2bf96cfb..532214e8e 100644 --- a/frontend/src/views/Admin/AdminMenu/components/UserActivity.jsx +++ b/frontend/src/views/Admin/AdminMenu/components/UserActivity.jsx @@ -60,6 +60,7 @@ export const UserActivity = () => { getRowId={getRowId} overlayNoRowsTemplate={t('admin:activitiesNotFound')} autoSizeStrategy={{ + type: 'fitGridWidth', defaultMinWidth: 50, defaultMaxWidth: 600 }} diff --git a/frontend/src/views/Admin/AdminMenu/components/Users.jsx b/frontend/src/views/Admin/AdminMenu/components/Users.jsx index 28f2e021b..49921701b 100644 --- a/frontend/src/views/Admin/AdminMenu/components/Users.jsx +++ b/frontend/src/views/Admin/AdminMenu/components/Users.jsx @@ -43,7 +43,7 @@ export const Users = () => { navigate(ROUTES.ADMIN_USERS_ADD) } const getRowId = useCallback((params) => { - return params.data.userProfileId + return params.data.userProfileId.toString() }, []) const handleRowClicked = useCallback((params) => { diff --git a/frontend/src/views/Admin/AdminMenu/components/_schema.js b/frontend/src/views/Admin/AdminMenu/components/_schema.js index 70b652b71..7b46ba5bb 100644 --- a/frontend/src/views/Admin/AdminMenu/components/_schema.js +++ b/frontend/src/views/Admin/AdminMenu/components/_schema.js @@ -8,8 +8,11 @@ import { RoleRenderer, StatusRenderer } from '@/utils/grid/cellRenderers' -import { BCColumnSetFilter } from '@/components/BCDataGrid/components' import { useRoleList } from '@/hooks/useRole' +import { + BCSelectFloatingFilter, + BCDateFloatingFilter +} from '@/components/BCDataGrid/components/index' export const usersColumnDefs = (t) => [ { @@ -44,14 +47,14 @@ export const usersColumnDefs = (t) => [ }, suppressFilterButton: true }, - floatingFilterComponent: BCColumnSetFilter, + floatingFilterComponent: BCSelectFloatingFilter, + suppressFloatingFilterButton: true, + suppressHeaderFilterButton: true, floatingFilterComponentParams: { - apiQuery: useRoleList, // all data returned should be an array which includes an object of key 'name' - // Eg: [{id: 1, name: 'EntryListItem' }] except name all others are optional + optionsQuery: useRoleList, params: 'government_roles_only=true', - key: 'admin-users', - disableCloseOnSelect: false, - multiple: false + valueKey: 'name', + labelKey: 'name' }, cellRenderer: RoleRenderer, cellClass: 'vertical-middle' @@ -84,20 +87,21 @@ export const usersColumnDefs = (t) => [ }, cellRenderer: StatusRenderer, cellClass: 'vertical-middle', - floatingFilterComponent: BCColumnSetFilter, + floatingFilterComponent: BCSelectFloatingFilter, floatingFilterComponentParams: { - apiQuery: () => ({ + optionsQuery: () => ({ data: [ { id: 1, name: t('admin:userColLabels.active') }, { id: 0, name: t('admin:userColLabels.inactive') } ], isLoading: false }), - disableCloseOnSelect: false, - multiple: false + valueKey: 'name', + labelKey: 'name' }, minWidth: 120, - suppressHeaderMenuButton: false + suppressFloatingFilterButton: true, + suppressHeaderFilterButton: true }, { colId: 'organizationId', diff --git a/frontend/src/views/ComplianceReports/EditViewComplianceReport.jsx b/frontend/src/views/ComplianceReports/EditViewComplianceReport.jsx index 3b85bf216..f5129eb4f 100644 --- a/frontend/src/views/ComplianceReports/EditViewComplianceReport.jsx +++ b/frontend/src/views/ComplianceReports/EditViewComplianceReport.jsx @@ -1,5 +1,5 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react' -import { useLocation, useParams } from 'react-router-dom' +import { useLocation, useNavigate, useParams } from 'react-router-dom' import { useForm } from 'react-hook-form' import { FloatingAlert } from '@/components/BCAlert' import BCBox from '@/components/BCBox' @@ -7,14 +7,13 @@ import BCModal from '@/components/BCModal' import BCButton from '@/components/BCButton' import Loading from '@/components/Loading' import { Role } from '@/components/Role' -import { roles } from '@/constants/roles' +import { roles, govRoles } from '@/constants/roles' import { Fab, Stack, Tooltip } from '@mui/material' import BCTypography from '@/components/BCTypography' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp' import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown' import colors from '@/themes/base/colors.js' -import { govRoles } from '@/constants/roles' import { useTranslation } from 'react-i18next' import { useCurrentUser } from '@/hooks/useCurrentUser' import { useOrganization } from '@/hooks/useOrganization' @@ -27,6 +26,7 @@ import { ActivityListCard } from './components/ActivityListCard' import { AssessmentCard } from './components/AssessmentCard' import InternalComments from '@/components/InternalComments' import { COMPLIANCE_REPORT_STATUSES } from '@/constants/statuses' +import { ROUTES } from '@/constants/routes' const iconStyle = { width: '2rem', @@ -42,6 +42,7 @@ export const EditViewComplianceReport = ({ reportData, isError, error }) => { const [isSigningAuthorityDeclared, setIsSigningAuthorityDeclared] = useState(false) const alertRef = useRef() + const navigate = useNavigate() const { compliancePeriod, complianceReportId } = useParams() const [isScrollingUp, setIsScrollingUp] = useState(false) @@ -64,8 +65,16 @@ export const EditViewComplianceReport = ({ reportData, isError, error }) => { setInternalComment(newComment) }, []) const handleScroll = useCallback(() => { - const scrollTop = window.pageYOffset || document.documentElement.scrollTop - setIsScrollingUp(scrollTop < lastScrollTop || scrollTop === 0) + const scrollTop = window.scrollY || document.documentElement.scrollTop + const scrollPosition = window.scrollY + window.innerHeight + const documentHeight = document.documentElement.scrollHeight + if (scrollTop === 0) { + setIsScrollingUp(false) + } else if (scrollPosition >= documentHeight - 10) { + setIsScrollingUp(true) + } else { + setIsScrollingUp(scrollTop < lastScrollTop || scrollTop === 0) + } setLastScrollTop(scrollTop) }, [lastScrollTop]) @@ -81,8 +90,9 @@ export const EditViewComplianceReport = ({ reportData, isError, error }) => { hasRoles } = useCurrentUser() const isGovernmentUser = currentUser?.isGovernmentUser - const isAnalystRole = currentUser?.roles?.some(role => role.name === roles.analyst) || false; - + const isAnalystRole = + currentUser?.roles?.some((role) => role.name === roles.analyst) || false + const currentStatus = reportData?.report.currentStatus?.status const { data: orgData, isLoading } = useOrganization( reportData?.report.organizationId @@ -92,9 +102,14 @@ export const EditViewComplianceReport = ({ reportData, isError, error }) => { { onSuccess: (response) => { setModalData(null) - alertRef.current?.triggerAlert({ - message: t('report:savedSuccessText'), - severity: 'success' + const updatedStatus = JSON.parse(response.config.data)?.status + navigate(ROUTES.REPORTS, { + state: { + message: t('report:savedSuccessText', { + status: updatedStatus.toLowerCase().replace('return', 'returned') + }), + severity: 'success' + } }) }, onError: (error) => { @@ -117,7 +132,7 @@ export const EditViewComplianceReport = ({ reportData, isError, error }) => { t, setModalData, updateComplianceReport, - + compliancePeriod, isGovernmentUser, isSigningAuthorityDeclared }), @@ -127,7 +142,7 @@ export const EditViewComplianceReport = ({ reportData, isError, error }) => { t, setModalData, updateComplianceReport, - + compliancePeriod, isGovernmentUser, isSigningAuthorityDeclared ] @@ -211,7 +226,10 @@ export const EditViewComplianceReport = ({ reportData, isError, error }) => { {!location.state?.newReport && ( <> - + { ) await waitFor(() => { fireEvent.scroll(window, { target: { pageYOffset: 0 } }) - expect(screen.getByLabelText('scroll to top')).toBeInTheDocument() + expect(screen.getByLabelText('scroll to bottom')).toBeInTheDocument() }) }) }) diff --git a/frontend/src/views/ComplianceReports/buttonConfigs.jsx b/frontend/src/views/ComplianceReports/buttonConfigs.jsx index 3921efce3..fe903ede3 100644 --- a/frontend/src/views/ComplianceReports/buttonConfigs.jsx +++ b/frontend/src/views/ComplianceReports/buttonConfigs.jsx @@ -1,9 +1,6 @@ // complianceReportButtonConfigs.js -import { - faPencil, - faTrash -} from '@fortawesome/free-solid-svg-icons' +import { faPencil, faTrash } from '@fortawesome/free-solid-svg-icons' import { COMPLIANCE_REPORT_STATUSES } from '@/constants/statuses' import { roles } from '@/constants/roles' @@ -45,6 +42,7 @@ export const buttonClusterConfigFn = ({ t, setModalData, updateComplianceReport, + compliancePeriod, isGovernmentUser, isSigningAuthorityDeclared }) => { @@ -120,9 +118,7 @@ export const buttonClusterConfigFn = ({ } }, returnToManager: { - ...outlinedButton( - t('report:actionBtns.returnToManager') - ), + ...outlinedButton(t('report:actionBtns.returnToManager')), id: 'return-report-manager-btn', handler: (formData) => { setModalData({ @@ -138,6 +134,23 @@ export const buttonClusterConfigFn = ({ }) } }, + returnToSupplier: { + ...outlinedButton(t('report:actionBtns.returnToSupplier')), + id: 'return-report-supplier-btn', + handler: (formData) => { + setModalData({ + primaryButtonAction: () => + updateComplianceReport({ + ...formData, + status: COMPLIANCE_REPORT_STATUSES.RETURN_TO_SUPPLIER + }), + primaryButtonText: t('report:actionBtns.returnToSupplier'), + secondaryButtonText: t('cancelBtn'), + title: t('confirmation'), + content: t('report:returnToSupplierConfirmText') + }) + } + }, assessReport: { ...containedButton(t('report:actionBtns.assessReportBtn')), id: 'assess-report-btn', @@ -174,13 +187,21 @@ export const buttonClusterConfigFn = ({ } } + const canReturnToSupplier = () => { + const compliancePeriodYear = parseInt(compliancePeriod) + const deadlineDate = new Date(compliancePeriodYear + 1, 2, 31) // Month is 0-based, so 2 = March + const currentDate = new Date() + return currentDate <= deadlineDate + } + const buttons = { - [COMPLIANCE_REPORT_STATUSES.DRAFT]: [ - reportButtons.submitReport - ], + [COMPLIANCE_REPORT_STATUSES.DRAFT]: [reportButtons.submitReport], [COMPLIANCE_REPORT_STATUSES.SUBMITTED]: [ ...(isGovernmentUser && hasRoles('Analyst') - ? [reportButtons.recommendByAnalyst] + ? [ + reportButtons.recommendByAnalyst, + ...(canReturnToSupplier() ? [reportButtons.returnToSupplier] : []) + ] : []) ], [COMPLIANCE_REPORT_STATUSES.RECOMMENDED_BY_ANALYST]: [ diff --git a/frontend/src/views/ComplianceReports/components/NewComplianceReportButton.jsx b/frontend/src/views/ComplianceReports/components/NewComplianceReportButton.jsx index 1fd1fef87..87ebc8254 100644 --- a/frontend/src/views/ComplianceReports/components/NewComplianceReportButton.jsx +++ b/frontend/src/views/ComplianceReports/components/NewComplianceReportButton.jsx @@ -48,7 +48,7 @@ export const NewComplianceReportButton = ({ const yearAhead = currentYear + 1 return periods?.data.filter((item) => { const effectiveYear = new Date(item.effectiveDate).getFullYear() - return effectiveYear <= yearAhead && effectiveYear >= currentYear + return effectiveYear <= yearAhead && effectiveYear >= 2024 }) } diff --git a/frontend/src/views/ComplianceReports/components/_schema.jsx b/frontend/src/views/ComplianceReports/components/_schema.jsx index e7f1546fb..6c52b88b6 100644 --- a/frontend/src/views/ComplianceReports/components/_schema.jsx +++ b/frontend/src/views/ComplianceReports/components/_schema.jsx @@ -1,4 +1,7 @@ -import { BCColumnSetFilter } from '@/components/BCDataGrid/components' +import { + BCDateFloatingFilter, + BCSelectFloatingFilter +} from '@/components/BCDataGrid/components' import { SUMMARY } from '@/constants/common' import { ReportsStatusRenderer } from '@/utils/grid/cellRenderers' import { timezoneFormatter } from '@/utils/formatters' @@ -50,10 +53,10 @@ export const reportsColDefs = (t, bceidRole) => [ url: ({ data }) => `${data.compliancePeriod?.description}/${data.complianceReportId}` }, - floatingFilterComponent: BCColumnSetFilter, + floatingFilterComponent: BCSelectFloatingFilter, floatingFilterComponentParams: { // TODO: change this to api Query later - apiQuery: () => ({ + optionsQuery: () => ({ data: bceidRole ? [ { id: 1, name: 'Draft' }, @@ -70,17 +73,15 @@ export const reportsColDefs = (t, bceidRole) => [ ], isLoading: false }), - key: 'report-status', - label: t('report:reportColLabels.status'), - disableCloseOnSelect: false, - multiple: false + valueKey: 'name', + labelKey: 'name' } }, { field: 'updateDate', cellDataType: 'dateString', headerName: t('report:reportColLabels.lastUpdated'), - flex: 1, + minWidth: '80', valueGetter: ({ data }) => data.updateDate || '', valueFormatter: timezoneFormatter, filter: 'agDateColumnFilter', @@ -89,7 +90,8 @@ export const reportsColDefs = (t, bceidRole) => [ suppressAndOrCondition: true, buttons: ['clear'], maxValidYear: 2400 - } + }, + floatingFilterComponent: BCDateFloatingFilter } ] diff --git a/frontend/src/views/FuelSupplies/AddEditFuelSupplies.jsx b/frontend/src/views/FuelSupplies/AddEditFuelSupplies.jsx index 2c3c90356..735f09eee 100644 --- a/frontend/src/views/FuelSupplies/AddEditFuelSupplies.jsx +++ b/frontend/src/views/FuelSupplies/AddEditFuelSupplies.jsx @@ -108,7 +108,7 @@ export const AddEditFuelSupplies = () => { })) setRowData([...updatedRowData, { id: uuid() }]) } else { - setRowData([{ id: uuid() }]) + setRowData([{ id: uuid(), complianceReportId, compliancePeriod }]) } setTimeout(() => { const lastRowIndex = params.api.getLastDisplayedRowIndex() @@ -145,7 +145,7 @@ export const AddEditFuelSupplies = () => { })) setRowData(updatedRowData) } else { - setRowData([{ id: uuid() }]) + setRowData([{ id: uuid(), complianceReportId, compliancePeriod }]) } }, [data, fuelSuppliesLoading, complianceReportId, compliancePeriod]) @@ -162,9 +162,8 @@ export const AddEditFuelSupplies = () => { ) // Set to null if multiple options, otherwise use first item - const categoryValue = fuelCategoryOptions.length === 1 - ? fuelCategoryOptions[0] - : null + const categoryValue = + fuelCategoryOptions.length === 1 ? fuelCategoryOptions[0] : null params.node.setDataValue('fuelCategory', categoryValue) } diff --git a/frontend/src/views/FuelSupplies/__tests__/AddEditFuelSupplies.test.jsx b/frontend/src/views/FuelSupplies/__tests__/AddEditFuelSupplies.test.jsx new file mode 100644 index 000000000..c31a71d49 --- /dev/null +++ b/frontend/src/views/FuelSupplies/__tests__/AddEditFuelSupplies.test.jsx @@ -0,0 +1,122 @@ +import React from 'react' +import { render, screen } from '@testing-library/react' +import { describe, it, expect, beforeEach, vi } from 'vitest' +import { AddEditFuelSupplies } from '../AddEditFuelSupplies' +import { wrapper } from '@/tests/utils/wrapper' +import { + useFuelSupplyOptions, + useGetFuelSupplies, + useSaveFuelSupply +} from '@/hooks/useFuelSupply' +import { useLocation, useNavigate, useParams } from 'react-router-dom' + +// Mock react-router-dom hooks +const mockUseLocation = vi.fn() +const mockUseNavigate = vi.fn() +const mockUseParams = vi.fn() + +vi.mock('react-router-dom', () => ({ + ...vi.importActual('react-router-dom'), + useLocation: () => mockUseLocation(), + useNavigate: () => mockUseNavigate(), + useParams: () => mockUseParams() +})) + +// Mock react-i18next +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key) => key + }) +})) + +// Mock all hooks related to fuel supply +vi.mock('@/hooks/useFuelSupply') + +// Mock BCGridEditor to verify props without rendering the full grid +vi.mock('@/components/BCDataGrid/BCGridEditor', () => ({ + BCGridEditor: ({ + gridRef, + alertRef, + onGridReady, + rowData, + onCellValueChanged, + onCellEditingStopped + }) => ( +
+
+ {rowData.map((row, index) => ( +
+ {row.id} +
+ ))} +
+
+ ) +})) + +describe('AddEditFuelSupplies', () => { + beforeEach(() => { + vi.resetAllMocks() + + // Mock location, navigate, and params + mockUseLocation.mockReturnValue({ + pathname: '/test-path', + state: {} + }) + mockUseNavigate.mockReturnValue(vi.fn()) + mockUseParams.mockReturnValue({ + complianceReportId: 'testReportId', + compliancePeriod: '2024-Q1' + }) + + // Mock useFuelSupplyOptions to return no fuel types initially + vi.mocked(useFuelSupplyOptions).mockReturnValue({ + data: { fuelTypes: [] }, + isLoading: false, + isFetched: true + }) + + // Mock useGetFuelSupplies to return empty data initially + vi.mocked(useGetFuelSupplies).mockReturnValue({ + data: { fuelSupplies: [] }, + isLoading: false + }) + + // Mock useSaveFuelSupply hook + vi.mocked(useSaveFuelSupply).mockReturnValue({ + mutateAsync: vi.fn() + }) + }) + + it('renders the component', () => { + render(, { wrapper }) + // Check for a title or any text that indicates successful rendering + expect( + screen.getByText('fuelSupply:addFuelSupplyRowsTitle') + ).toBeInTheDocument() + }) + + it('initializes with at least one row when there are no existing fuel supplies', () => { + render(, { wrapper }) + const rows = screen.getAllByTestId('grid-row') + // Should contain exactly one blank row if data is empty + expect(rows.length).toBe(1) + }) + + it('loads existing fuel supplies when available', async () => { + // Update mock to provide some existing fuel supplies + vi.mocked(useGetFuelSupplies).mockReturnValue({ + data: { + fuelSupplies: [ + { fuelSupplyId: 'abc', fuelType: 'Diesel' }, + { fuelSupplyId: 'xyz', fuelType: 'Gasoline' } + ] + }, + isLoading: false + }) + + render(, { wrapper }) + const rows = await screen.findAllByTestId('grid-row') + expect(rows.length).toBe(2) + }) +}) diff --git a/frontend/src/views/FuelSupplies/__tests__/FuelSupplySummary.test.jsx b/frontend/src/views/FuelSupplies/__tests__/FuelSupplySummary.test.jsx new file mode 100644 index 000000000..4d058939a --- /dev/null +++ b/frontend/src/views/FuelSupplies/__tests__/FuelSupplySummary.test.jsx @@ -0,0 +1,136 @@ +import React from 'react' +import { render, screen } from '@testing-library/react' +import { describe, it, expect, beforeEach, vi } from 'vitest' +import { FuelSupplySummary } from '../FuelSupplySummary' +import { wrapper } from '@/tests/utils/wrapper' +import { useLocation, useNavigate, useParams } from 'react-router-dom' +import { COMPLIANCE_REPORT_STATUSES } from '@/constants/statuses' + +// Mock react-router-dom hooks +const mockUseLocation = vi.fn() +const mockUseNavigate = vi.fn() +const mockUseParams = vi.fn() + +vi.mock('react-router-dom', () => ({ + ...vi.importActual('react-router-dom'), + useLocation: () => mockUseLocation(), + useNavigate: () => mockUseNavigate(), + useParams: () => mockUseParams() +})) + +// Mock react-i18next +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key) => key + }) +})) + +// Mock BCDataGridServer so we can verify it renders without a full data grid +vi.mock('@/components/BCDataGrid/BCDataGridServer', () => ({ + // Provide a basic mock for both default export and named export + __esModule: true, + default: () => ( +
Mocked BCDataGridServer
+ ), + BCDataGridServer: () => ( +
Mocked BCDataGridServer
+ ) +})) + +describe('FuelSupplySummary', () => { + beforeEach(() => { + vi.resetAllMocks() + + // Default mock returns + mockUseLocation.mockReturnValue({ + pathname: '/test-fuel-supplies', + state: {} + }) + mockUseNavigate.mockReturnValue(vi.fn()) + mockUseParams.mockReturnValue({ + complianceReportId: 'testReportId', + compliancePeriod: '2024' + }) + }) + + it('renders the component', () => { + render( + , + { wrapper } + ) + // Confirm that BCDataGridServer (the mocked component) is displayed + expect(screen.getByTestId('mocked-bc-data-grid-server')).toBeInTheDocument() + }) + + it('displays alert message when location.state has a message', () => { + mockUseLocation.mockReturnValue({ + pathname: '/test-fuel-supplies', + state: { message: 'Test Alert', severity: 'error' } + }) + + render( + , + { wrapper } + ) + + // Check that the alert box is rendered with the correct message + const alertBox = screen.getByTestId('alert-box') + expect(alertBox).toBeInTheDocument() + expect(alertBox.textContent).toContain('Test Alert') + }) + + it('does not display alert message if location.state is empty', () => { + mockUseLocation.mockReturnValue({ + pathname: '/test-fuel-supplies', + state: {} + }) + + render( + , + { wrapper } + ) + + // Alert should not be present + const alertBox = screen.queryByTestId('alert-box') + expect(alertBox).not.toBeInTheDocument() + }) + + it('renders fuel supplies rows in BCDataGridServer when provided', () => { + const mockData = { + fuelSupplies: [ + { fuelSupplyId: 1, fuelType: 'Diesel' }, + { fuelSupplyId: 2, fuelType: 'Gasoline' } + ] + } + + render( + , + { wrapper } + ) + + // The actual rows are handled by the BCDataGridServer mock, so just confirm the mock rendered + expect(screen.getByTestId('mocked-bc-data-grid-server')).toBeInTheDocument() + }) + + it('does nothing special on row click if status is not DRAFT', () => { + // Here, just ensure that the component renders fine for a non-DRAFT status + render( + , + { wrapper } + ) + // Confirm the mock is rendered and no crash occurs + expect(screen.getByTestId('mocked-bc-data-grid-server')).toBeInTheDocument() + }) +}) diff --git a/frontend/src/views/Organizations/ViewOrganization/_schema.js b/frontend/src/views/Organizations/ViewOrganization/_schema.js index bc1a33fb8..8b78ce4d5 100644 --- a/frontend/src/views/Organizations/ViewOrganization/_schema.js +++ b/frontend/src/views/Organizations/ViewOrganization/_schema.js @@ -1,6 +1,6 @@ import { numberFormatter } from '@/utils/formatters' import { LinkRenderer, OrgStatusRenderer } from '@/utils/grid/cellRenderers' -import { BCColumnSetFilter } from '@/components/BCDataGrid/components' +import { BCSelectFloatingFilter } from '@/components/BCDataGrid/components' import { useOrganizationStatuses } from '@/hooks/useOrganizations' import { usersColumnDefs } from '@/views/Admin/AdminMenu/components/_schema' import { t } from 'i18next' @@ -48,15 +48,14 @@ export const organizationsColDefs = (t) => [ valueGetter: (params) => params.data.orgStatus.status, cellRenderer: OrgStatusRenderer, cellClass: 'vertical-middle', - floatingFilterComponent: BCColumnSetFilter, + floatingFilterComponent: BCSelectFloatingFilter, floatingFilterComponentParams: { - apiOptionField: 'status', - apiQuery: useOrganizationStatuses, - key: 'org-status', - disableCloseOnSelect: false, - multiple: false + valueKey: 'status', + labelKey: 'status', + optionsQuery: useOrganizationStatuses }, - suppressHeaderMenuButton: true + suppressFloatingFilterButton: true, + suppressHeaderFilterButton: true } ] diff --git a/frontend/src/views/Transactions/_schema.js b/frontend/src/views/Transactions/_schema.js index 46bbbc82c..50827ed38 100644 --- a/frontend/src/views/Transactions/_schema.js +++ b/frontend/src/views/Transactions/_schema.js @@ -5,7 +5,7 @@ import { spacesFormatter } from '@/utils/formatters' import { TransactionStatusRenderer } from '@/utils/grid/cellRenderers' -import { BCColumnSetFilter } from '@/components/BCDataGrid/components' +import { BCSelectFloatingFilter } from '@/components/BCDataGrid/components' import { useTransactionStatuses } from '@/hooks/useTransactions' const prefixMap = { @@ -102,14 +102,14 @@ export const transactionsColDefs = (t) => [ headerName: t('txn:txnColLabels.status'), cellRenderer: TransactionStatusRenderer, cellClass: 'vertical-middle', - floatingFilterComponent: BCColumnSetFilter, + floatingFilterComponent: BCSelectFloatingFilter, floatingFilterComponentParams: { - apiOptionField: 'status', - apiQuery: useTransactionStatuses, - disableCloseOnSelect: false, - multiple: false + valueKey: 'status', + labelKey: 'status', + optionsQuery: useTransactionStatuses }, - suppressHeaderMenuButton: true, + suppressFloatingFilterButton: true, + suppressHeaderFilterButton: true, minWidth: 180, width: 250 },