From 955a9b5bb52c57737ae86ab267927bdc3b67f1be Mon Sep 17 00:00:00 2001 From: Alex Zorkin Date: Wed, 4 Dec 2024 14:51:51 -0800 Subject: [PATCH 1/8] fix: default allocation agreement to 1 row on empty data --- frontend/src/App.jsx | 2 +- ...ts.jsx => AddEditAllocationAgreements.jsx} | 16 ++- .../AddEditAllocationAgreements.test.jsx | 133 ++++++++++++++++++ 3 files changed, 149 insertions(+), 2 deletions(-) rename frontend/src/views/AllocationAgreements/{AddAllocationAgreements.jsx => AddEditAllocationAgreements.jsx} (94%) create mode 100644 frontend/src/views/AllocationAgreements/__tests__/AddEditAllocationAgreements.test.jsx diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index f9ec1948b..0fca499df 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -32,7 +32,7 @@ import { AddEditOtherUses } from './views/OtherUses/AddEditOtherUses' import { AddEditFinalSupplyEquipments } from './views/FinalSupplyEquipments/AddEditFinalSupplyEquipments' import { AddEditFuelSupplies } from './views/FuelSupplies/AddEditFuelSupplies' import { AddEditFuelExports } from './views/FuelExports/AddEditFuelExports' -import { AddEditAllocationAgreements } from './views/AllocationAgreements/AddAllocationAgreements' +import { AddEditAllocationAgreements } from './views/AllocationAgreements/AddEditAllocationAgreements' import { logout } from '@/utils/keycloak.js' import { CompareReports } from '@/views/CompareReports/CompareReports' diff --git a/frontend/src/views/AllocationAgreements/AddAllocationAgreements.jsx b/frontend/src/views/AllocationAgreements/AddEditAllocationAgreements.jsx similarity index 94% rename from frontend/src/views/AllocationAgreements/AddAllocationAgreements.jsx rename to frontend/src/views/AllocationAgreements/AddEditAllocationAgreements.jsx index 2a9d258bf..390c792d5 100644 --- a/frontend/src/views/AllocationAgreements/AddAllocationAgreements.jsx +++ b/frontend/src/views/AllocationAgreements/AddEditAllocationAgreements.jsx @@ -71,7 +71,21 @@ export const AddEditAllocationAgreements = () => { const onGridReady = useCallback( async (params) => { setGridApi(params.api) - setRowData([...(data.allocationAgreements || { id: uuid() })]) + + if ( + Array.isArray(data.allocationAgreements) && + data.allocationAgreements.length > 0 + ) { + const updatedRowData = data.allocationAgreements.map((item) => ({ + ...item, + id: item.id || uuid() // Ensure every item has a unique ID + })) + setRowData(updatedRowData) + } else { + // If allocationAgreements is not available or empty, initialize with a single row + setRowData([{ id: uuid() }]) + } + params.api.sizeColumnsToFit() }, [data] diff --git a/frontend/src/views/AllocationAgreements/__tests__/AddEditAllocationAgreements.test.jsx b/frontend/src/views/AllocationAgreements/__tests__/AddEditAllocationAgreements.test.jsx new file mode 100644 index 000000000..ce7d72113 --- /dev/null +++ b/frontend/src/views/AllocationAgreements/__tests__/AddEditAllocationAgreements.test.jsx @@ -0,0 +1,133 @@ +// src/views/AllocationAgreements/__tests__/AddEditAllocationAgreements.test.jsx + +import React from 'react' +import { render, screen, fireEvent, waitFor } from '@testing-library/react' +import { describe, it, expect, beforeEach, vi } from 'vitest' +import { AddEditAllocationAgreements } from '../AddEditAllocationAgreements' +import * as useAllocationAgreementHook from '@/hooks/useAllocationAgreement' +import { wrapper } from '@/tests/utils/wrapper' + +// 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 hooks related to allocation agreements +vi.mock('@/hooks/useAllocationAgreement') + +// Mock BCGridEditor component +vi.mock('@/components/BCDataGrid/BCGridEditor', () => ({ + BCGridEditor: ({ + gridRef, + alertRef, + onGridReady, + rowData, + onCellValueChanged, + onCellEditingStopped + }) => ( +
+
+ {rowData.map((row, index) => ( +
+ {row.id} +
+ ))} +
+
+ ) +})) + +describe('AddEditAllocationAgreements', () => { + beforeEach(() => { + vi.resetAllMocks() + + // Mock react-router-dom hooks with complete location object + mockUseLocation.mockReturnValue({ + pathname: '/test-path', // Include pathname to prevent undefined errors + state: {} + }) + mockUseNavigate.mockReturnValue(vi.fn()) + mockUseParams.mockReturnValue({ + complianceReportId: 'testReportId', + compliancePeriod: '2024' + }) + + // Mock useGetAllocationAgreements hook to return empty data initially + vi.mocked( + useAllocationAgreementHook.useGetAllocationAgreements + ).mockReturnValue({ + data: { allocationAgreements: [] }, + isLoading: false + }) + + // Mock useAllocationAgreementOptions hook + vi.mocked( + useAllocationAgreementHook.useAllocationAgreementOptions + ).mockReturnValue({ + data: { fuelTypes: [] }, + isLoading: false, + isFetched: true + }) + + // Mock useSaveAllocationAgreement hook + vi.mocked( + useAllocationAgreementHook.useSaveAllocationAgreement + ).mockReturnValue({ + mutateAsync: vi.fn() + }) + }) + + it('renders the component', () => { + render(, { wrapper }) + expect( + screen.getByText('allocationAgreement:addAllocationAgreementRowsTitle') + ).toBeInTheDocument() + }) + + it('initializes with at least one row in the empty state', () => { + render(, { wrapper }) + const rows = screen.getAllByTestId('grid-row') + expect(rows.length).toBe(1) // Ensure at least one row exists + }) + + it('loads data when allocationAgreements are available', async () => { + // Update the mock to return allocation agreements + vi.mocked( + useAllocationAgreementHook.useGetAllocationAgreements + ).mockReturnValue({ + data: { + allocationAgreements: [ + { allocationAgreementId: 'testId1' }, + { allocationAgreementId: 'testId2' } + ] + }, + isLoading: false + }) + + render(, { wrapper }) + + // Use findAllByTestId for asynchronous elements + const rows = await screen.findAllByTestId('grid-row') + expect(rows.length).toBe(2) + // Check that each row's textContent matches UUID format + const uuidRegex = + /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i + rows.forEach((row) => { + expect(uuidRegex.test(row.textContent)).toBe(true) + }) + }) +}) From 820caa4cf9a9f4b287739f87fa401213b7eadeb1 Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Mon, 9 Dec 2024 10:53:23 -0800 Subject: [PATCH 2/8] fix: Allow negative symbol in transaction amount input --- .../layouts/MainLayout/components/UserProfileActions.jsx | 1 - .../views/Transactions/components/TransactionDetails.jsx | 7 +++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/frontend/src/layouts/MainLayout/components/UserProfileActions.jsx b/frontend/src/layouts/MainLayout/components/UserProfileActions.jsx index f5f7dbc68..37ebd0557 100644 --- a/frontend/src/layouts/MainLayout/components/UserProfileActions.jsx +++ b/frontend/src/layouts/MainLayout/components/UserProfileActions.jsx @@ -38,7 +38,6 @@ export const UserProfileActions = () => { refetchInterval: 60000 // Automatically refetch every 1 minute (60000ms) }) const notificationsCount = notificationsData?.count || 0 - console.log(notificationsData) // Call refetch whenever the route changes useEffect(() => { diff --git a/frontend/src/views/Transactions/components/TransactionDetails.jsx b/frontend/src/views/Transactions/components/TransactionDetails.jsx index 0c3b51199..81fdd1b3b 100644 --- a/frontend/src/views/Transactions/components/TransactionDetails.jsx +++ b/frontend/src/views/Transactions/components/TransactionDetails.jsx @@ -244,8 +244,11 @@ export const TransactionDetails = ({ transactionId, isEditable }) => { helperText={errors.complianceUnits?.message} value={formattedValue} onChange={(e) => { - // Remove all non-digit characters - const numericValue = e.target.value.replace(/\D/g, '') + // Remove all non-digit characters (other than - at the front) + const numericValue = e.target.value.replace( + /(?!^-)[^0-9]/g, + '' + ) // Update the form state with the raw number onChange(numericValue) }} From 62c9aac5e15d6fb77872a5d769f789272884821a Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Mon, 9 Dec 2024 11:55:33 -0800 Subject: [PATCH 3/8] feat: Bypass auth for health check * Add code to bypass auth check for health endpoint --- backend/lcfs/web/api/monitoring/views.py | 3 ++- backend/lcfs/web/application.py | 19 +++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/backend/lcfs/web/api/monitoring/views.py b/backend/lcfs/web/api/monitoring/views.py index 1b36caf02..c599ef0b1 100644 --- a/backend/lcfs/web/api/monitoring/views.py +++ b/backend/lcfs/web/api/monitoring/views.py @@ -4,9 +4,10 @@ @router.get("/health") -def health_check() -> None: +def health_check() -> str: """ Checks the health of a project. It returns 200 if the project is healthy. """ + return "healthy" diff --git a/backend/lcfs/web/application.py b/backend/lcfs/web/application.py index 6d31484d0..e7117a105 100644 --- a/backend/lcfs/web/application.py +++ b/backend/lcfs/web/application.py @@ -1,30 +1,26 @@ -from importlib import metadata -import structlog import logging +import uuid -import os -import debugpy +import structlog from fastapi import FastAPI, HTTPException from fastapi.exceptions import RequestValidationError -from fastapi.responses import UJSONResponse from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import UJSONResponse from prometheus_fastapi_instrumentator import Instrumentator -from starlette.middleware.authentication import AuthenticationMiddleware from starlette.authentication import ( AuthenticationBackend, AuthCredentials, UnauthenticatedUser, ) +from starlette.middleware.authentication import AuthenticationMiddleware from starlette.middleware.base import BaseHTTPMiddleware from starlette.requests import Request from starlette.responses import JSONResponse -import uuid -import contextvars -from lcfs.settings import settings from lcfs.logging_config import setup_logging, correlation_id_var -from lcfs.web.api.router import api_router from lcfs.services.keycloak.authentication import UserAuthentication +from lcfs.settings import settings +from lcfs.web.api.router import api_router from lcfs.web.exception.exception_handler import validation_exception_handler from lcfs.web.lifetime import register_shutdown_event, register_startup_event @@ -67,6 +63,9 @@ async def authenticate(self, request): if request.scope["method"] == "OPTIONS": return AuthCredentials([]), UnauthenticatedUser() + if request.url.path == "/api/health": # Skip for health check + return AuthCredentials([]), UnauthenticatedUser() + # Lazily retrieve Redis, session, and settings from app state redis_client = self.app.state.redis_client session_factory = self.app.state.db_session_factory From 8b8fdac79140e7deb1d97a0bf84cc592a73bf571 Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Mon, 9 Dec 2024 16:36:03 -0800 Subject: [PATCH 4/8] feat: Expand Compliance Report Test * Now include checking rounding on line 6, submitting the report, and checking the IDIR can see it --- .../ComplianceReport/ComplianceReport.test.js | 44 ++++++++++++++++++- .../ComplianceReportManagement.feature | 17 +++++-- .../e2e/Pages/User/UserCreation.test.js | 42 +++++++++--------- frontend/cypress/e2e/accessibility.cy.js | 2 +- frontend/cypress/e2e/add__edit_org.cy.js | 2 +- frontend/cypress/e2e/disclaimer_banner.cy.js | 4 +- frontend/cypress/e2e/organization.cy.js | 2 +- frontend/cypress/e2e/user_flow.cy.js | 10 ++--- 8 files changed, 87 insertions(+), 36 deletions(-) diff --git a/frontend/cypress/e2e/Pages/ComplianceReport/ComplianceReport.test.js b/frontend/cypress/e2e/Pages/ComplianceReport/ComplianceReport.test.js index e175c8f20..8e92fff86 100644 --- a/frontend/cypress/e2e/Pages/ComplianceReport/ComplianceReport.test.js +++ b/frontend/cypress/e2e/Pages/ComplianceReport/ComplianceReport.test.js @@ -2,7 +2,7 @@ import { Given, When, Then } from '@badeball/cypress-cucumber-preprocessor' const currentYear = new Date().getFullYear().toString() -Given('the supplier is on the login page', () => { +Given('the user is on the login page', () => { cy.clearAllCookies() cy.clearAllLocalStorage() cy.clearAllSessionStorage() @@ -20,7 +20,7 @@ When('the supplier logs in with valid credentials', () => { cy.getByDataTest('dashboard-container').should('exist') }) -When('the supplier navigates to the compliance reports page', () => { +When('they navigate to the compliance reports page', () => { cy.get('a[href="/compliance-reporting"]').click() }) @@ -140,3 +140,43 @@ Then('the compliance report summary includes the quantity', () => { .should('be.visible') .and('have.text', '500') }) + +When('the supplier fills out line 6', () => { + cy.get( + '[data-test="renewable-summary"] > .MuiTable-root > .MuiTableBody-root > :nth-child(6) > :nth-child(3)' + ) + .find('input') + .type('50{enter}') + .blur() +}) + +Then('it should round the amount to 25', () => { + cy.get( + '[data-test="renewable-summary"] > .MuiTable-root > .MuiTableBody-root > :nth-child(6) > :nth-child(3)' + ) + .find('input') + .should('be.visible') + .and('have.value', '25') +}) + +When('the supplier accepts the agreement', () => { + cy.get('#signing-authority-declaration').click() +}) + +When('the supplier submits the report', () => { + cy.contains('button', 'Submit report').click() + cy.get('#modal-btn-submit-report').click() + cy.wait(2000) +}) + +Then('the status should change to Submitted', () => { + cy.get('[data-test="compliance-report-status"]') + .should('be.visible') + .and('have.text', 'Status: Submitted') +}) + +Then('they see the previously submitted report', () => { + cy.get('.ag-column-first > a > .MuiBox-root') + .should('be.visible') + .and('have.text', currentYear) +}) diff --git a/frontend/cypress/e2e/Pages/ComplianceReport/ComplianceReportManagement.feature b/frontend/cypress/e2e/Pages/ComplianceReport/ComplianceReportManagement.feature index a0e9c1975..501cf3790 100644 --- a/frontend/cypress/e2e/Pages/ComplianceReport/ComplianceReportManagement.feature +++ b/frontend/cypress/e2e/Pages/ComplianceReport/ComplianceReportManagement.feature @@ -1,12 +1,23 @@ Feature: Compliance Report Management Scenario: Supplier saves a draft compliance report - Given the supplier is on the login page - When the supplier logs in with valid credentials - And the supplier navigates to the compliance reports page + Given the user is on the login page + And the supplier logs in with valid credentials + And they navigate to the compliance reports page And the supplier creates a new compliance report Then the compliance report introduction is shown When the supplier navigates to the fuel supply page And the supplier enters a valid fuel supply row And saves and returns to the report Then the compliance report summary includes the quantity + When the supplier fills out line 6 + Then it should round the amount to 25 + When the supplier accepts the agreement + And the supplier submits the report + Then the status should change to Submitted + + Scenario: Analyst logs in to review a compliance report + Given the user is on the login page + And the analyst logs in with valid credentials + And they navigate to the compliance reports page + Then they see the previously submitted report \ No newline at end of file diff --git a/frontend/cypress/e2e/Pages/User/UserCreation.test.js b/frontend/cypress/e2e/Pages/User/UserCreation.test.js index bf9b8ff25..079dd77a6 100644 --- a/frontend/cypress/e2e/Pages/User/UserCreation.test.js +++ b/frontend/cypress/e2e/Pages/User/UserCreation.test.js @@ -25,39 +25,39 @@ When('the IDIR user logs in with valid credentials', () => { }) When('the IDIR user navigates to the user creation page', () => { - cy.get('a[href="/admin"]').click() - cy.url().should('include', '/admin/users') - cy.contains('New user').click() - cy.url().should('include', '/admin/users/add-user') + cy.get('a[href="/admin"]').click() + cy.url().should('include', '/admin/users') + cy.contains('New user').click() + cy.url().should('include', '/admin/users/add-user') }) When('the IDIR user fills out the form with valid data', () => { - cy.get('input[id="firstName"]').type('John') - cy.get('input[id="lastName"]').type('Doe') - cy.get('input[id="jobTitle"]').type('Senior Analyst') - cy.get('input[id="userName"]').type('johndoe') - cy.get('input[id="keycloakEmail"]').type('john.doe@example.com') - cy.get('input[id="phone"]').type('1234567890') - cy.get('input[id="mobilePhone"]').type('0987654321') + cy.get('input[id="firstName"]').type('John') + cy.get('input[id="lastName"]').type('Doe') + cy.get('input[id="jobTitle"]').type('Senior Analyst') + cy.get('input[id="userName"]').type('johndoe') + cy.get('input[id="keycloakEmail"]').type('john.doe@example.com') + cy.get('input[id="phone"]').type('1234567890') + cy.get('input[id="mobilePhone"]').type('0987654321') - // Select the Analyst role - cy.get('input[type="radio"][value="analyst"]').check() + // Select the Analyst role + cy.get('input[type="radio"][value="analyst"]').check() }) When('the IDIR user submits the form', () => { - cy.get('button[data-test="saveUser"]').click() + cy.get('button[data-test="saveUser"]').click() }) Then('a success message is displayed', () => { - cy.get("[data-test='alert-box'] .MuiBox-root").should( - 'contain', - 'User has been successfully saved.' - ) + cy.get("[data-test='alert-box'] .MuiBox-root").should( + 'contain', + 'User has been successfully saved.' + ) }) Then('the new user appears in the user list', () => { - cy.visit('/admin/users') - cy.contains('a', Cypress.env('john.doe@example.com')).should('be.visible') + cy.visit('/admin/users') + cy.contains('a', Cypress.env('john.doe@example.com')).should('be.visible') }) // Test for validation error @@ -75,7 +75,7 @@ When('the IDIR user fills out the form with invalid data', () => { }) Then('an error message is displayed for validation', () => { - cy.get('#userName-helper-text').should('contain', 'User name is required') + cy.get('#userName-helper-text').should('contain', 'User name is required') }) // Cleanup after the test diff --git a/frontend/cypress/e2e/accessibility.cy.js b/frontend/cypress/e2e/accessibility.cy.js index 4c44b2543..44147beff 100644 --- a/frontend/cypress/e2e/accessibility.cy.js +++ b/frontend/cypress/e2e/accessibility.cy.js @@ -18,7 +18,7 @@ describe('Accessibility Tests for LCFS', () => { it('Should have no accessibility violations in the navigation bar', () => { cy.visit('/') cy.injectAxe() - cy.login( + cy.loginWith( 'idir', Cypress.env('IDIR_TEST_USER'), Cypress.env('IDIR_TEST_PASS') diff --git a/frontend/cypress/e2e/add__edit_org.cy.js b/frontend/cypress/e2e/add__edit_org.cy.js index 1cd1e136e..1a714ee3f 100644 --- a/frontend/cypress/e2e/add__edit_org.cy.js +++ b/frontend/cypress/e2e/add__edit_org.cy.js @@ -6,7 +6,7 @@ describe('Add Organization Test Suite', () => { beforeEach(() => { - cy.login( + cy.loginWith( 'idir', Cypress.env('IDIR_TEST_USER'), Cypress.env('IDIR_TEST_PASS') diff --git a/frontend/cypress/e2e/disclaimer_banner.cy.js b/frontend/cypress/e2e/disclaimer_banner.cy.js index 363cc207a..ded9a66a4 100644 --- a/frontend/cypress/e2e/disclaimer_banner.cy.js +++ b/frontend/cypress/e2e/disclaimer_banner.cy.js @@ -5,7 +5,7 @@ describe('Disclaimer Banner Visibility Test Suite', () => { context('BCeID User', () => { beforeEach(() => { - cy.login( + cy.loginWith( 'bceid', Cypress.env('BCEID_TEST_USER'), Cypress.env('BCEID_TEST_PASS') @@ -29,7 +29,7 @@ describe('Disclaimer Banner Visibility Test Suite', () => { context('IDIR User', () => { beforeEach(() => { - cy.login( + cy.loginWith( 'idir', Cypress.env('IDIR_TEST_USER'), Cypress.env('IDIR_TEST_PASS') diff --git a/frontend/cypress/e2e/organization.cy.js b/frontend/cypress/e2e/organization.cy.js index 4ca2e7f38..2ac742e9b 100644 --- a/frontend/cypress/e2e/organization.cy.js +++ b/frontend/cypress/e2e/organization.cy.js @@ -5,7 +5,7 @@ describe('Organization Test Suite', () => { beforeEach(() => { // Login and visit the page - cy.login( + cy.loginWith( 'idir', Cypress.env('IDIR_TEST_USER'), Cypress.env('IDIR_TEST_PASS') diff --git a/frontend/cypress/e2e/user_flow.cy.js b/frontend/cypress/e2e/user_flow.cy.js index e99dde6be..233823650 100644 --- a/frontend/cypress/e2e/user_flow.cy.js +++ b/frontend/cypress/e2e/user_flow.cy.js @@ -28,12 +28,12 @@ describe('User Login Test Suite', () => { describe('IDIR Login Flow', () => { it('fails login with wrong IDIR user credentials', () => { - cy.login('idir', 'wrong_username', 'wrong_password') + cy.loginWith('idir', 'wrong_username', 'wrong_password') cy.getByDataTest('main-layout-navbar').should('not.exist') }) it('completes login with IDIR user credentials', () => { - cy.login( + cy.loginWith( 'idir', Cypress.env('IDIR_TEST_USER'), Cypress.env('IDIR_TEST_PASS') @@ -42,7 +42,7 @@ describe('User Login Test Suite', () => { }) it('executes logout functionality for IDIR user', () => { - cy.login( + cy.loginWith( 'idir', Cypress.env('IDIR_TEST_USER'), Cypress.env('IDIR_TEST_PASS') @@ -53,12 +53,12 @@ describe('User Login Test Suite', () => { describe('BCeID Login Flow', () => { it('fails login with wrong BCeID user credentials', () => { - cy.login('bceid', 'wrong_username', 'wrong_password') + cy.loginWith('bceid', 'wrong_username', 'wrong_password') cy.getByDataTest('main-layout-navbar').should('not.exist') }) it('completes login with BCeID user credentials', () => { - cy.login( + cy.loginWith( 'bceid', Cypress.env('BCEID_TEST_USER'), Cypress.env('BCEID_TEST_PASS') From 4f51e107b05eb33d53f53eeab0c15c7e2310f2cb Mon Sep 17 00:00:00 2001 From: prv-proton Date: Tue, 10 Dec 2024 03:49:30 -0800 Subject: [PATCH 5/8] Fix: LCFS - BUG AG Grid copy and paste function for Fuel codes table on IDIR side not functioning #1302 --- backend/lcfs/web/api/fuel_code/repo.py | 22 +++---- backend/lcfs/web/api/fuel_code/services.py | 4 ++ backend/lcfs/web/core/decorators.py | 2 +- .../Renderers/ValidationRenderer2.jsx | 2 +- frontend/src/constants/routes/apiRoutes.js | 1 + frontend/src/hooks/useFuelCode.js | 2 +- .../FuelCodes/AddFuelCode/AddEditFuelCode.jsx | 58 ++++++++++++++++++- .../views/FuelCodes/AddFuelCode/_schema.jsx | 9 +-- 8 files changed, 76 insertions(+), 24 deletions(-) diff --git a/backend/lcfs/web/api/fuel_code/repo.py b/backend/lcfs/web/api/fuel_code/repo.py index aa0577466..7e0d0f534 100644 --- a/backend/lcfs/web/api/fuel_code/repo.py +++ b/backend/lcfs/web/api/fuel_code/repo.py @@ -396,19 +396,8 @@ async def create_fuel_code(self, fuel_code: FuelCode) -> FuelCode: """ self.db.add(fuel_code) await self.db.flush() - await self.db.refresh( - fuel_code, - [ - "fuel_code_status", - "fuel_code_prefix", - "fuel_type", - "feedstock_fuel_transport_modes", - "finished_fuel_transport_modes", - ], - ) - # Manually load nested relationships - await self.db.refresh(fuel_code.fuel_type, ["provision_1", "provision_2"]) - return fuel_code + result = await self.get_fuel_code(fuel_code.fuel_code_id) + return result @repo_handler async def get_fuel_code(self, fuel_code_id: int) -> FuelCode: @@ -593,9 +582,14 @@ async def validate_fuel_code(self, suffix: str, prefix_id: int) -> str: result = (await self.db.execute(query)).scalar_one_or_none() if result: fuel_code_main_version = suffix.split(".")[0] - return await self.get_next_available_sub_version_fuel_code_by_prefix( + suffix = await self.get_next_available_sub_version_fuel_code_by_prefix( fuel_code_main_version, prefix_id ) + if int(suffix.split(".")[1]) > 9: + return await self.get_next_available_fuel_code_by_prefix( + result.fuel_code_prefix.prefix + ) + return suffix else: return suffix diff --git a/backend/lcfs/web/api/fuel_code/services.py b/backend/lcfs/web/api/fuel_code/services.py index 039634e6a..d40fde544 100644 --- a/backend/lcfs/web/api/fuel_code/services.py +++ b/backend/lcfs/web/api/fuel_code/services.py @@ -208,6 +208,8 @@ async def convert_to_model( transport_mode_id=matching_transport_mode.transport_mode_id, ) ) + else: + raise ValueError(f"Invalid transport mode: {transport_mode}") for transport_mode in fuel_code_schema.finished_fuel_transport_mode or []: matching_transport_mode = next( @@ -221,6 +223,8 @@ async def convert_to_model( transport_mode_id=matching_transport_mode.transport_mode_id, ) ) + else: + raise ValueError(f"Invalid transport mode: {transport_mode}") return fuel_code diff --git a/backend/lcfs/web/core/decorators.py b/backend/lcfs/web/core/decorators.py index 07dc7c5ab..e67d9afca 100644 --- a/backend/lcfs/web/core/decorators.py +++ b/backend/lcfs/web/core/decorators.py @@ -215,7 +215,7 @@ async def wrapper(*args, **kwargs): return await func(*args, **kwargs) # raise the error to the view layer - except (DatabaseException, HTTPException, DataNotFoundException): + except (DatabaseException, HTTPException, DataNotFoundException, ValueError): raise # all other errors that occur in the service layer will log an error except Exception as e: diff --git a/frontend/src/components/BCDataGrid/components/Renderers/ValidationRenderer2.jsx b/frontend/src/components/BCDataGrid/components/Renderers/ValidationRenderer2.jsx index 043b127b5..5d4931865 100644 --- a/frontend/src/components/BCDataGrid/components/Renderers/ValidationRenderer2.jsx +++ b/frontend/src/components/BCDataGrid/components/Renderers/ValidationRenderer2.jsx @@ -18,7 +18,7 @@ export const ValidationRenderer2 = ({ data }) => { ) case 'error': return ( - + { ...options, mutationFn: async (fuelCodeID) => { return await client.delete( - apiRoutes.updateFuelCode.replace(':fuelCodeId', fuelCodeID) + apiRoutes.deleteFuelCode.replace(':fuelCodeId', fuelCodeID) ) } }) diff --git a/frontend/src/views/FuelCodes/AddFuelCode/AddEditFuelCode.jsx b/frontend/src/views/FuelCodes/AddFuelCode/AddEditFuelCode.jsx index 044c767a7..f4b30e304 100644 --- a/frontend/src/views/FuelCodes/AddFuelCode/AddEditFuelCode.jsx +++ b/frontend/src/views/FuelCodes/AddFuelCode/AddEditFuelCode.jsx @@ -25,6 +25,7 @@ import BCModal from '@/components/BCModal' import BCTypography from '@/components/BCTypography' import { FUEL_CODE_STATUSES } from '@/constants/statuses' import { useCurrentUser } from '@/hooks/useCurrentUser' +import Papa from 'papaparse' const AddEditFuelCodeBase = () => { const { fuelCodeID } = useParams() @@ -197,6 +198,7 @@ const AddEditFuelCodeBase = () => { } else { const res = await createFuelCode(updatedData) updatedData.fuelCodeId = res.data.fuelCodeId + updatedData.fuelSuffix = res.data.fuelSuffix } updatedData = { @@ -210,7 +212,7 @@ const AddEditFuelCodeBase = () => { }) } catch (error) { setErrors({ - [params.node.data.id]: error.response.data.errors[0].fields + [params.node.data.id]: error.response.data?.errors && error.response.data?.errors[0]?.fields }) updatedData = { @@ -229,10 +231,12 @@ const AddEditFuelCodeBase = () => { const errMsg = `Error updating row: ${ fieldLabels.length === 1 ? fieldLabels[0] : '' } ${message}` - + updatedData.validationMsg = errMsg handleError(error, errMsg) } else { - handleError(error, `Error updating row: ${error.message}`) + const errMsg = error.response?.data?.detail || error.message + updatedData.validationMsg = errMsg + handleError(error, `Error updating row: ${errMsg}`) } } @@ -241,6 +245,53 @@ const AddEditFuelCodeBase = () => { [updateFuelCode, t] ) + const handlePaste = useCallback( + (event, { api, columnApi }) => { + const newData = [] + const clipboardData = event.clipboardData || window.clipboardData + const pastedData = clipboardData.getData('text/plain') + const headerRow = api + .getAllDisplayedColumns() + .map((column) => column.colDef.field) + .filter((col) => col) + .join('\t') + const parsedData = Papa.parse(headerRow + '\n' + pastedData, { + delimiter: '\t', + header: true, + transform: (value) => { + const num = Number(value) // Attempt to convert to a number if possible + return isNaN(num) ? value : num // Return the number if valid, otherwise keep as string + }, + skipEmptyLines: true + }) + if (parsedData.data?.length < 0) { + return + } + parsedData.data.forEach((row) => { + const newRow = { ...row } + newRow.id = uuid() + newRow.prefixId = optionsData?.fuelCodePrefixes?.find(o => o.prefix === row.prefix)?.fuelCodePrefixId + newRow.fuelTypeId = optionsData?.fuelTypes?.find(o => o.fuelType === row.fuelType)?.fuelTypeId + newRow.fuelSuffix = newRow.fuelSuffix.toString() + newRow.feedstockFuelTransportMode = row.feedstockFuelTransportMode.split(',').map(item => item.trim()) + newRow.finishedFuelTransportMode = row.finishedFuelTransportMode.split(',').map(item => item.trim()) + newRow.modified = true + newData.push(newRow) + }) + const transactions = api.applyTransaction({ add: newData }) + // Trigger onCellEditingStopped event to update the row in backend. + transactions.add.forEach((node) => { + onCellEditingStopped({ + node, + oldValue: '', + newvalue: undefined, + ...api + }) + }) + }, + [onCellEditingStopped, optionsData] + ) + const duplicateFuelCode = async (params) => { const rowData = { ...params.data, @@ -327,6 +378,7 @@ const AddEditFuelCodeBase = () => { onAction={onAction} showAddRowsButton={!existingFuelCode && hasRoles(roles.analyst)} context={{ errors }} + handlePaste={handlePaste} /> {existingFuelCode?.fuelCodeStatus.status !== FUEL_CODE_STATUSES.APPROVED && ( diff --git a/frontend/src/views/FuelCodes/AddFuelCode/_schema.jsx b/frontend/src/views/FuelCodes/AddFuelCode/_schema.jsx index 3c39a6f4c..96186c584 100644 --- a/frontend/src/views/FuelCodes/AddFuelCode/_schema.jsx +++ b/frontend/src/views/FuelCodes/AddFuelCode/_schema.jsx @@ -97,9 +97,9 @@ export const fuelCodeColDefs = (optionsData, errors, isCreate, canEdit) => [ const selectedPrefix = optionsData?.fuelCodePrefixes?.find( (obj) => obj.prefix === params.newValue ) - params.data.fuelTypeId = selectedPrefix.fuelCodePrefixId + params.data.fuelCodePrefixId = selectedPrefix.fuelCodePrefixId - params.data.fuelCode = optionsData?.fuelCodePrefixes?.find( + params.data.fuelSuffix = optionsData?.fuelCodePrefixes?.find( (obj) => obj.prefix === params.newValue )?.nextFuelCode params.data.company = undefined @@ -327,12 +327,12 @@ export const fuelCodeColDefs = (optionsData, errors, isCreate, canEdit) => [ return selectedOption.fuelType } const selectedOption = optionsData?.fuelTypes?.find( - (obj) => obj.fuelType === params.data.fuel + (obj) => obj.fuelType === params.data.fuelType ) if (selectedOption) { params.data.fuelTypeId = selectedOption.fuelTypeId } - return params.data.fuel + return params.data.fuelType }, valueSetter: (params) => { if (params.newValue) { @@ -341,6 +341,7 @@ export const fuelCodeColDefs = (optionsData, errors, isCreate, canEdit) => [ ) params.data.fuelTypeId = selectedFuelType.fuelTypeId } + return params.data.fuelType }, cellEditorParams: { options: optionsData?.fuelTypes From 28ecdb9feb8a1edb816067b68c1889e1c61fef45 Mon Sep 17 00:00:00 2001 From: prv-proton Date: Tue, 10 Dec 2024 04:16:00 -0800 Subject: [PATCH 6/8] test fixes. --- backend/lcfs/tests/fuel_code/test_fuel_code_service.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/lcfs/tests/fuel_code/test_fuel_code_service.py b/backend/lcfs/tests/fuel_code/test_fuel_code_service.py index c71608031..5dce49e52 100644 --- a/backend/lcfs/tests/fuel_code/test_fuel_code_service.py +++ b/backend/lcfs/tests/fuel_code/test_fuel_code_service.py @@ -210,7 +210,7 @@ async def test_approve_fuel_code_not_found(): repo_mock.get_fuel_code.return_value = None # Act & Assert - with pytest.raises(ServiceException): + with pytest.raises(ValueError, match="Fuel code not found"): await service.approve_fuel_code(fuel_code_id) repo_mock.get_fuel_code.assert_called_once_with(fuel_code_id) @@ -229,7 +229,7 @@ async def test_approve_fuel_code_invalid_status(): repo_mock.get_fuel_code.return_value = mock_fuel_code # Act & Assert - with pytest.raises(ServiceException): + with pytest.raises(ValueError, match="Fuel code is not in Draft"): await service.approve_fuel_code(fuel_code_id) repo_mock.get_fuel_code.assert_called_once_with(fuel_code_id) From d5ebc64d024b7acf97f1b1922e862a87bb05530b Mon Sep 17 00:00:00 2001 From: prv-proton Date: Tue, 10 Dec 2024 04:17:38 -0800 Subject: [PATCH 7/8] . --- backend/lcfs/tests/other_uses/test_other_uses_services.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/lcfs/tests/other_uses/test_other_uses_services.py b/backend/lcfs/tests/other_uses/test_other_uses_services.py index 3f4705501..14b3bb518 100644 --- a/backend/lcfs/tests/other_uses/test_other_uses_services.py +++ b/backend/lcfs/tests/other_uses/test_other_uses_services.py @@ -189,7 +189,7 @@ async def test_update_other_use_not_found(other_uses_service): mock_repo.get_other_use_version_by_user = AsyncMock(return_value=None) - with pytest.raises(ServiceException): + with pytest.raises(ValueError, match="Other use not found"): await service.update_other_use(other_use_data, UserTypeEnum.SUPPLIER) From 5544e39efb5a65ab02fae0b48021eaaaaeae351e Mon Sep 17 00:00:00 2001 From: prv-proton Date: Tue, 10 Dec 2024 12:21:37 -0800 Subject: [PATCH 8/8] adding support for different date types. --- .../FuelCodes/AddFuelCode/AddEditFuelCode.jsx | 30 +++++++++++++++---- 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/frontend/src/views/FuelCodes/AddFuelCode/AddEditFuelCode.jsx b/frontend/src/views/FuelCodes/AddFuelCode/AddEditFuelCode.jsx index f4b30e304..6841ea7ac 100644 --- a/frontend/src/views/FuelCodes/AddFuelCode/AddEditFuelCode.jsx +++ b/frontend/src/views/FuelCodes/AddFuelCode/AddEditFuelCode.jsx @@ -212,7 +212,9 @@ const AddEditFuelCodeBase = () => { }) } catch (error) { setErrors({ - [params.node.data.id]: error.response.data?.errors && error.response.data?.errors[0]?.fields + [params.node.data.id]: + error.response.data?.errors && + error.response.data?.errors[0]?.fields }) updatedData = { @@ -258,7 +260,15 @@ const AddEditFuelCodeBase = () => { const parsedData = Papa.parse(headerRow + '\n' + pastedData, { delimiter: '\t', header: true, - transform: (value) => { + transform: (value, field) => { + // Check for date fields and format them + const dateRegex = /^\d{4}-\d{2}-\d{2}$/ // Matches YYYY-MM-DD format + if (field.toLowerCase().includes('date') && !dateRegex.test(value)) { + const parsedDate = new Date(value) + if (!isNaN(parsedDate)) { + return parsedDate.toISOString().split('T')[0] // Format as YYYY-MM-DD + } + } const num = Number(value) // Attempt to convert to a number if possible return isNaN(num) ? value : num // Return the number if valid, otherwise keep as string }, @@ -270,11 +280,19 @@ const AddEditFuelCodeBase = () => { parsedData.data.forEach((row) => { const newRow = { ...row } newRow.id = uuid() - newRow.prefixId = optionsData?.fuelCodePrefixes?.find(o => o.prefix === row.prefix)?.fuelCodePrefixId - newRow.fuelTypeId = optionsData?.fuelTypes?.find(o => o.fuelType === row.fuelType)?.fuelTypeId + newRow.prefixId = optionsData?.fuelCodePrefixes?.find( + (o) => o.prefix === row.prefix + )?.fuelCodePrefixId + newRow.fuelTypeId = optionsData?.fuelTypes?.find( + (o) => o.fuelType === row.fuelType + )?.fuelTypeId newRow.fuelSuffix = newRow.fuelSuffix.toString() - newRow.feedstockFuelTransportMode = row.feedstockFuelTransportMode.split(',').map(item => item.trim()) - newRow.finishedFuelTransportMode = row.finishedFuelTransportMode.split(',').map(item => item.trim()) + newRow.feedstockFuelTransportMode = row.feedstockFuelTransportMode + .split(',') + .map((item) => item.trim()) + newRow.finishedFuelTransportMode = row.finishedFuelTransportMode + .split(',') + .map((item) => item.trim()) newRow.modified = true newData.push(newRow) })