diff --git a/.github/workflows/cypress.yml b/.github/workflows/cypress.yml
index 60ea1c698..a252f09c7 100644
--- a/.github/workflows/cypress.yml
+++ b/.github/workflows/cypress.yml
@@ -25,8 +25,7 @@ jobs:
- 6379:6379
rabbitmq:
image: rabbitmq:3-management
- container_name: rabbitmq
- environment:
+ env:
RABBITMQ_DEFAULT_USER: lcfs
RABBITMQ_DEFAULT_PASS: development_only
RABBITMQ_DEFAULT_VHOST: lcfs
@@ -34,28 +33,49 @@ jobs:
- "15672:15672"
steps:
- uses: actions/checkout@v2
+
- name: Build and Run Backend Service
run: |
- docker build -t backend-service ./backend/Dockerfile
- docker run -d --name backend -e LCFS_DB_HOST=localhost -e LCFS_REDIS_HOST=localhost -p 8000:8000 backend-service
+ docker build -t backend-service -f ./backend/Dockerfile ./backend
+ docker run -d --name backend \
+ --network=host \
+ -e LCFS_DB_HOST=localhost \
+ -e LCFS_REDIS_HOST=localhost \
+ backend-service
+
+ - name: Wait for DB to be Ready
+ run: sleep 20
+
+ - name: Run Alembic Migrations
+ run: docker exec backend poetry run alembic upgrade head
+
- name: Data Seeding
run: docker exec backend poetry run python /app/lcfs/db/seeders/seed_database.py
+
- name: Build and Run Frontend Service
run: |
- docker build -t frontend-service ./fontend/Dockerfile.dev
- docker run -d --name frontend -p 3000:3000 frontend-service
+ docker build -t frontend-service -f ./frontend/Dockerfile.dev ./frontend
+ docker run -d --name frontend \
+ --network=host \
+ frontend-service
+
- name: Cypress run
uses: cypress-io/github-action@v2
with:
browser: chrome
wait-on: 'http://localhost:3000'
wait-on-timeout: 60
+ record: false
+ config-file: cypress.config.js
+ working-directory: frontend
env:
IDIR_TEST_USER: ${{ secrets.CYPRESS_IDIR_TEST_USER }}
IDIR_TEST_PASS: ${{ secrets.CYPRESS_IDIR_TEST_PASS }}
BCEID_TEST_USER: ${{ secrets.CYPRESS_BCEID_TEST_USER }}
BCEID_TEST_PASS: ${{ secrets.CYPRESS_BCEID_TEST_PASS }}
+
- name: Cleanup
+ if: always()
run: |
docker stop backend frontend
- docker rm backend frontend
\ No newline at end of file
+ docker rm backend frontend
diff --git a/backend/lcfs/db/migrations/versions/2025-01-13-22-13_f78e53370ed2.py b/backend/lcfs/db/migrations/versions/2025-01-13-22-13_f78e53370ed2.py
new file mode 100644
index 000000000..f97aa17c6
--- /dev/null
+++ b/backend/lcfs/db/migrations/versions/2025-01-13-22-13_f78e53370ed2.py
@@ -0,0 +1,238 @@
+"""Add CR to Transaction Aggregate
+
+Revision ID: f78e53370ed2
+Revises: d25e7c47659e
+Create Date: 2025-01-13 22:13:48.610890
+
+"""
+
+import sqlalchemy as sa
+from alembic import op
+
+# revision identifiers, used by Alembic.
+revision = "f78e53370ed2"
+down_revision = "d25e7c47659e"
+branch_labels = None
+depends_on = None
+
+
+def upgrade() -> None:
+ op.execute("DROP MATERIALIZED VIEW mv_transaction_aggregate;")
+ op.execute(
+ """
+ CREATE MATERIALIZED VIEW mv_transaction_aggregate AS
+ SELECT
+ t.transfer_id AS transaction_id,
+ 'Transfer' AS transaction_type,
+ NULL AS description,
+ org_from.organization_id AS from_organization_id,
+ org_from.name AS from_organization,
+ org_to.organization_id AS to_organization_id,
+ org_to.name AS to_organization,
+ t.quantity,
+ t.price_per_unit,
+ ts.status::text AS status,
+ NULL AS compliance_period,
+ t.from_org_comment AS COMMENT,
+ tc.category,
+ (
+ SELECT
+ th.create_date
+ FROM
+ transfer_history th
+ WHERE
+ th.transfer_id = t.transfer_id
+ AND th.transfer_status_id = 6) AS recorded_date, NULL AS approved_date, t.transaction_effective_date, t.update_date, t.create_date
+ FROM
+ transfer t
+ JOIN organization org_from ON t.from_organization_id = org_from.organization_id
+ JOIN organization org_to ON t.to_organization_id = org_to.organization_id
+ JOIN transfer_status ts ON t.current_status_id = ts.transfer_status_id
+ LEFT JOIN transfer_category tc ON t.transfer_category_id = tc.transfer_category_id
+ UNION ALL
+ SELECT
+ ia.initiative_agreement_id AS transaction_id,
+ 'InitiativeAgreement' AS transaction_type,
+ NULL AS description,
+ NULL AS from_organization_id,
+ NULL AS from_organization,
+ org.organization_id AS to_organization_id,
+ org.name AS to_organization,
+ ia.compliance_units AS quantity,
+ NULL AS price_per_unit,
+ ias.status::text AS status,
+ NULL AS compliance_period,
+ ia.gov_comment AS COMMENT,
+ NULL AS category,
+ NULL AS recorded_date,
+ (
+ SELECT
+ iah.create_date
+ FROM
+ initiative_agreement_history iah
+ WHERE
+ iah.initiative_agreement_id = ia.initiative_agreement_id
+ AND iah.initiative_agreement_status_id = 3) AS approved_date, ia.transaction_effective_date, ia.update_date, ia.create_date
+ FROM
+ initiative_agreement ia
+ JOIN organization org ON ia.to_organization_id = org.organization_id
+ JOIN initiative_agreement_status ias ON ia.current_status_id = ias.initiative_agreement_status_id
+ UNION ALL
+ SELECT
+ aa.admin_adjustment_id AS transaction_id,
+ 'AdminAdjustment' AS transaction_type,
+ NULL AS description,
+ NULL AS from_organization_id,
+ NULL AS from_organization,
+ org.organization_id AS to_organization_id,
+ org.name AS to_organization,
+ aa.compliance_units AS quantity,
+ NULL AS price_per_unit,
+ aas.status::text AS status,
+ NULL AS compliance_period,
+ aa.gov_comment AS COMMENT,
+ NULL AS category,
+ NULL AS recorded_date,
+ (
+ SELECT
+ aah.create_date
+ FROM
+ admin_adjustment_history aah
+ WHERE
+ aah.admin_adjustment_id = aa.admin_adjustment_id
+ AND aah.admin_adjustment_status_id = 3) AS approved_date, aa.transaction_effective_date, aa.update_date, aa.create_date
+ FROM
+ admin_adjustment aa
+ JOIN organization org ON aa.to_organization_id = org.organization_id
+ JOIN admin_adjustment_status aas ON aa.current_status_id = aas.admin_adjustment_status_id
+ UNION ALL
+ SELECT
+ cr.compliance_report_id AS transaction_id,
+ 'ComplianceReport' AS transaction_type,
+ cr.nickname AS description,
+ NULL AS from_organization_id,
+ NULL AS from_organization,
+ org.organization_id AS to_organization_id,
+ org.name AS to_organization,
+ tr.compliance_units AS quantity,
+ NULL AS price_per_unit,
+ crs.status::text AS status,
+ cp.description AS compliance_period,
+ NULL AS COMMENT,
+ NULL AS category,
+ NULL AS recorded_date,
+ NULL AS approved_date,
+ NULL AS transaction_effective_date,
+ cr.update_date,
+ cr.create_date
+ FROM
+ compliance_report cr
+ JOIN organization org ON cr.organization_id = org.organization_id
+ JOIN compliance_report_status crs ON cr.current_status_id = crs.compliance_report_status_id
+ JOIN compliance_period cp ON cr.compliance_period_id = cp.compliance_period_id
+ JOIN TRANSACTION tr ON cr.transaction_id = tr.transaction_id
+ AND cr.transaction_id IS NOT NULL;
+ """
+ )
+
+ # Create unique index on mv_transaction_aggregate
+ op.execute(
+ """
+ CREATE UNIQUE INDEX mv_transaction_aggregate_unique_idx ON mv_transaction_aggregate (transaction_id, description, transaction_type);
+ """
+ )
+
+
+def downgrade() -> None:
+ op.execute("DROP MATERIALIZED VIEW mv_transaction_aggregate;")
+ op.execute(
+ """
+ CREATE MATERIALIZED VIEW mv_transaction_aggregate AS
+ SELECT
+ t.transfer_id AS transaction_id,
+ 'Transfer' AS transaction_type,
+ org_from.organization_id AS from_organization_id,
+ org_from.name AS from_organization,
+ org_to.organization_id AS to_organization_id,
+ org_to.name AS to_organization,
+ t.quantity,
+ t.price_per_unit,
+ ts.status::text AS status,
+ NULL AS compliance_period,
+ t.from_org_comment AS comment,
+ tc.category,
+ (
+ SELECT th.create_date
+ FROM transfer_history th
+ WHERE th.transfer_id = t.transfer_id AND th.transfer_status_id = 6
+ ) AS recorded_date,
+ NULL AS approved_date,
+ t.transaction_effective_date,
+ t.update_date,
+ t.create_date
+ FROM transfer t
+ JOIN organization org_from ON t.from_organization_id = org_from.organization_id
+ JOIN organization org_to ON t.to_organization_id = org_to.organization_id
+ JOIN transfer_status ts ON t.current_status_id = ts.transfer_status_id
+ LEFT JOIN transfer_category tc ON t.transfer_category_id = tc.transfer_category_id
+ UNION ALL
+ SELECT
+ ia.initiative_agreement_id AS transaction_id,
+ 'InitiativeAgreement' AS transaction_type,
+ NULL AS from_organization_id,
+ NULL AS from_organization,
+ org.organization_id AS to_organization_id,
+ org.name AS to_organization,
+ ia.compliance_units AS quantity,
+ NULL AS price_per_unit,
+ ias.status::text AS status,
+ NULL AS compliance_period,
+ ia.gov_comment AS comment,
+ NULL AS category,
+ NULL AS recorded_date,
+ (
+ SELECT iah.create_date
+ FROM initiative_agreement_history iah
+ WHERE iah.initiative_agreement_id = ia.initiative_agreement_id AND iah.initiative_agreement_status_id = 3
+ ) AS approved_date,
+ ia.transaction_effective_date,
+ ia.update_date,
+ ia.create_date
+ FROM initiative_agreement ia
+ JOIN organization org ON ia.to_organization_id = org.organization_id
+ JOIN initiative_agreement_status ias ON ia.current_status_id = ias.initiative_agreement_status_id
+ UNION ALL
+ SELECT
+ aa.admin_adjustment_id AS transaction_id,
+ 'AdminAdjustment' AS transaction_type,
+ NULL AS from_organization_id,
+ NULL AS from_organization,
+ org.organization_id AS to_organization_id,
+ org.name AS to_organization,
+ aa.compliance_units AS quantity,
+ NULL AS price_per_unit,
+ aas.status::text AS status,
+ NULL AS compliance_period,
+ aa.gov_comment AS comment,
+ NULL AS category,
+ NULL AS recorded_date,
+ (
+ SELECT aah.create_date
+ FROM admin_adjustment_history aah
+ WHERE aah.admin_adjustment_id = aa.admin_adjustment_id AND aah.admin_adjustment_status_id = 3
+ ) AS approved_date,
+ aa.transaction_effective_date,
+ aa.update_date,
+ aa.create_date
+ FROM admin_adjustment aa
+ JOIN organization org ON aa.to_organization_id = org.organization_id
+ JOIN admin_adjustment_status aas ON aa.current_status_id = aas.admin_adjustment_status_id;
+ """
+ )
+
+ # Create unique index on mv_transaction_aggregate
+ op.execute(
+ """
+ CREATE UNIQUE INDEX mv_transaction_aggregate_unique_idx ON mv_transaction_aggregate (transaction_id, transaction_type);
+ """
+ )
diff --git a/backend/lcfs/db/models/transaction/TransactionView.py b/backend/lcfs/db/models/transaction/TransactionView.py
index 3f15f7222..8dd11fe6d 100644
--- a/backend/lcfs/db/models/transaction/TransactionView.py
+++ b/backend/lcfs/db/models/transaction/TransactionView.py
@@ -26,6 +26,7 @@ class TransactionView(BaseModel):
# id and type columns are defined as a composite primary key.
transaction_id = Column(Integer, primary_key=True)
transaction_type = Column(String, primary_key=True)
+ description = Column(String)
from_organization_id = Column(Integer)
from_organization = Column(String)
to_organization_id = Column(Integer)
diff --git a/backend/lcfs/web/api/compliance_report/repo.py b/backend/lcfs/web/api/compliance_report/repo.py
index cd45266d3..82b930e97 100644
--- a/backend/lcfs/web/api/compliance_report/repo.py
+++ b/backend/lcfs/web/api/compliance_report/repo.py
@@ -6,7 +6,7 @@
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 sqlalchemy.orm import joinedload, contains_eager
from lcfs.db.dependencies import get_async_db_session
from lcfs.db.models.compliance import CompliancePeriod
@@ -55,6 +55,27 @@ def __init__(
self.fuel_supply_repo = fuel_supply_repo
def apply_filters(self, pagination, conditions):
+ for filter in pagination.filters:
+ filter_value = filter.filter
+
+ filter_option = filter.type
+ filter_type = filter.filter_type
+ if filter.field == "organization":
+ field = get_field_for_filter(Organization, "name")
+ conditions.append(
+ apply_filter_conditions(
+ field, filter_value, filter_option, filter_type
+ )
+ )
+ elif filter.field == "compliance_period":
+ field = get_field_for_filter(CompliancePeriod, "description")
+ conditions.append(
+ apply_filter_conditions(
+ field, filter_value, filter_option, filter_type
+ )
+ )
+
+ def apply_sub_filters(self, pagination, conditions):
for filter in pagination.filters:
filter_value = filter.filter
# check if the date string is selected for filter
@@ -82,7 +103,7 @@ def apply_filters(self, pagination, conditions):
else:
filter_value = ComplianceReportStatusEnum(filter_value)
elif filter.field == "organization":
- field = get_field_for_filter(Organization, "name")
+ continue
elif filter.field == "type":
field = get_field_for_filter(ComplianceReport, "reporting_frequency")
filter_value = (
@@ -91,7 +112,7 @@ def apply_filters(self, pagination, conditions):
else ReportingFrequency.QUARTERLY.value
)
elif filter.field == "compliance_period":
- field = get_field_for_filter(CompliancePeriod, "description")
+ continue
else:
field = get_field_for_filter(ComplianceReport, filter.field)
@@ -340,10 +361,12 @@ async def get_reports_paginated(
"""
# Base query conditions
conditions = []
+ sub_conditions = []
if organization_id:
- conditions.append(ComplianceReport.organization_id == organization_id)
+ sub_conditions.append(ComplianceReport.organization_id == organization_id)
if pagination.filters and len(pagination.filters) > 0:
+ self.apply_sub_filters(pagination, sub_conditions)
self.apply_filters(pagination, conditions)
# Pagination and offset setup
@@ -356,7 +379,7 @@ async def get_reports_paginated(
ComplianceReport.compliance_report_group_uuid,
func.max(ComplianceReport.version).label("latest_version"),
)
- .where(and_(*conditions))
+ .where(and_(*sub_conditions))
.group_by(ComplianceReport.compliance_report_group_uuid)
)
@@ -378,9 +401,19 @@ async def get_reports_paginated(
ComplianceReport.version == subquery.c.latest_version,
),
)
+ .join(
+ Organization,
+ ComplianceReport.organization_id == Organization.organization_id,
+ )
+ .join(
+ CompliancePeriod,
+ ComplianceReport.compliance_period_id
+ == CompliancePeriod.compliance_period_id,
+ )
+ .where(and_(*conditions))
.options(
- joinedload(ComplianceReport.organization),
- joinedload(ComplianceReport.compliance_period),
+ contains_eager(ComplianceReport.organization),
+ contains_eager(ComplianceReport.compliance_period),
joinedload(ComplianceReport.current_status),
joinedload(ComplianceReport.summary),
joinedload(ComplianceReport.history).joinedload(
@@ -404,17 +437,8 @@ async def get_reports_paginated(
)
elif order.field == "compliance_period":
order.field = get_field_for_filter(CompliancePeriod, "description")
- query = query.join(
- CompliancePeriod,
- ComplianceReport.compliance_period_id
- == CompliancePeriod.compliance_period_id,
- )
elif order.field == "organization":
order.field = get_field_for_filter(Organization, "name")
- query = query.join(
- Organization,
- ComplianceReport.organization_id == Organization.organization_id,
- )
else:
order.field = get_field_for_filter(ComplianceReport, order.field)
query = query.order_by(sort_method(order.field))
diff --git a/backend/lcfs/web/api/transaction/schema.py b/backend/lcfs/web/api/transaction/schema.py
index ad0d8411e..1ec597d6a 100644
--- a/backend/lcfs/web/api/transaction/schema.py
+++ b/backend/lcfs/web/api/transaction/schema.py
@@ -69,10 +69,12 @@ class TransactionBaseSchema(BaseSchema):
class TransactionViewSchema(BaseSchema):
transaction_id: int
transaction_type: str
+ description: Optional[str] = None
from_organization: Optional[str] = None
to_organization: str
quantity: int
price_per_unit: Optional[float] = None
+ compliance_period: Optional[str] = None
status: str
create_date: datetime
update_date: datetime
diff --git a/frontend/src/assets/locales/en/fuelCode.json b/frontend/src/assets/locales/en/fuelCode.json
index 701a6231e..2f1fd186a 100644
--- a/frontend/src/assets/locales/en/fuelCode.json
+++ b/frontend/src/assets/locales/en/fuelCode.json
@@ -23,7 +23,7 @@
"addRow": "Add row",
"rows": "rows",
"fuelCodeColLabels": {
- "fuelCodeId": "Fuel Code Id",
+ "fuelCodeId": "Fuel Code ID",
"status": "Status",
"prefix": "Prefix",
"fuelSuffix": "Iteration",
diff --git a/frontend/src/assets/locales/en/transaction.json b/frontend/src/assets/locales/en/transaction.json
index 0f7d7e608..f5fdacf9c 100644
--- a/frontend/src/assets/locales/en/transaction.json
+++ b/frontend/src/assets/locales/en/transaction.json
@@ -1,7 +1,7 @@
{
"title": "Transactions",
"txnColLabels": {
- "txnId": "Id",
+ "txnId": "ID",
"compliancePeriod": "Compliance period",
"type": "Type",
"organizationFrom": "From",
diff --git a/frontend/src/assets/locales/en/transfer.json b/frontend/src/assets/locales/en/transfer.json
index 14ba125bd..47a263a61 100644
--- a/frontend/src/assets/locales/en/transfer.json
+++ b/frontend/src/assets/locales/en/transfer.json
@@ -81,5 +81,6 @@
"Declined": "Declined",
"Rescinded": "Rescinded"
},
- "zeroDollarInstructionText": "If proposing a zero-dollar transfer, use the comment box below to provide an explanation for this value."
+ "zeroDollarInstructionText": "If proposing a zero-dollar transfer, use the comment box below to provide an explanation for this value.",
+ "categoryCheckbox": "Select the checkbox to set the transfer as Category D if the price is significantly less than fair market value. This will override the default category determined by the agreement and approval dates indicated above."
}
diff --git a/frontend/src/utils/grid/cellRenderers.jsx b/frontend/src/utils/grid/cellRenderers.jsx
index 37917345a..7d210aab0 100644
--- a/frontend/src/utils/grid/cellRenderers.jsx
+++ b/frontend/src/utils/grid/cellRenderers.jsx
@@ -181,6 +181,7 @@ export const TransactionStatusRenderer = (props) => {
'Sent',
'Submitted',
'Approved',
+ 'Assessed',
'Recorded',
'Refused',
'Deleted',
@@ -194,6 +195,7 @@ export const TransactionStatusRenderer = (props) => {
'info',
'success',
'success',
+ 'success',
'error',
'error',
'error',
diff --git a/frontend/src/views/Transactions/Transactions.jsx b/frontend/src/views/Transactions/Transactions.jsx
index 3227b1ab9..d271fdafc 100644
--- a/frontend/src/views/Transactions/Transactions.jsx
+++ b/frontend/src/views/Transactions/Transactions.jsx
@@ -66,8 +66,13 @@ export const Transactions = () => {
url: (
data // Based on the user Type (BCeID or IDIR) navigate to specific view
) => {
- const { transactionId, transactionType, fromOrganization, status } =
- data.data
+ const {
+ transactionId,
+ transactionType,
+ fromOrganization,
+ status,
+ compliancePeriod
+ } = data.data
const userOrgName = currentUser?.organization?.name
// Define routes mapping for transaction types
@@ -87,6 +92,10 @@ export const Transactions = () => {
? ROUTES.INITIATIVE_AGREEMENT_VIEW
: ROUTES.ORG_INITIATIVE_AGREEMENT_VIEW,
edit: ROUTES.INITIATIVE_AGREEMENT_EDIT
+ },
+ ComplianceReport: {
+ view: ROUTES.REPORTS_VIEW,
+ edit: ROUTES.INITIATIVE_AGREEMENT_EDIT
}
}
@@ -105,6 +114,8 @@ export const Transactions = () => {
return routeTemplate
.replace(':transactionId', transactionId)
.replace(':transferId', transactionId)
+ .replace(':compliancePeriod', compliancePeriod)
+ .replace(':complianceReportId', transactionId)
} else {
console.error(
'No route defined for this transaction type and scenario'
diff --git a/frontend/src/views/Transactions/_schema.js b/frontend/src/views/Transactions/_schema.js
index e5e2fded4..969979b87 100644
--- a/frontend/src/views/Transactions/_schema.js
+++ b/frontend/src/views/Transactions/_schema.js
@@ -14,7 +14,8 @@ import { useTransactionStatuses } from '@/hooks/useTransactions'
const prefixMap = {
Transfer: 'CT',
AdminAdjustment: 'AA',
- InitiativeAgreement: 'IA'
+ InitiativeAgreement: 'IA',
+ ComplianceReport: 'CR'
}
export const transactionsColDefs = (t) => [
@@ -36,7 +37,15 @@ export const transactionsColDefs = (t) => [
colId: 'transactionType',
field: 'transactionType',
headerName: t('txn:txnColLabels.type'),
- valueFormatter: spacesFormatter,
+ valueGetter: (params) => {
+ const value = spacesFormatter({ value: params.data.transactionType })
+ const suffix = params.data.description
+
+ if (suffix) {
+ return `${value} - ${suffix}`
+ }
+ return value
+ },
filter: true, // Enable filtering
filterParams: {
textFormatter: (value) => value.replace(/\s+/g, '').toLowerCase(),
@@ -56,6 +65,9 @@ export const transactionsColDefs = (t) => [
headerName: t('txn:txnColLabels.organizationFrom'),
minWidth: 300,
flex: 2,
+ valueGetter: (params) => {
+ return params.fromOrganization || 'N/A'
+ },
filterParams: {
buttons: ['clear']
}
@@ -91,7 +103,7 @@ export const transactionsColDefs = (t) => [
width: 190,
valueGetter: (params) => {
const value = params.data?.pricePerUnit
- return value !== null && value !== undefined ? value : null
+ return value !== null && value !== undefined ? value : 'N/A'
},
filter: 'agNumberColumnFilter',
filterParams: {
diff --git a/frontend/src/views/Transfers/AddEditViewTransfer.jsx b/frontend/src/views/Transfers/AddEditViewTransfer.jsx
index 4dfb34c09..9dc9b0c54 100644
--- a/frontend/src/views/Transfers/AddEditViewTransfer.jsx
+++ b/frontend/src/views/Transfers/AddEditViewTransfer.jsx
@@ -441,7 +441,9 @@ export const AddEditViewTransfer = () => {
hasAnyRole(roles.analyst) && (
<>
-
+
>
)}
diff --git a/frontend/src/views/Transfers/components/CategoryCheckbox.jsx b/frontend/src/views/Transfers/components/CategoryCheckbox.jsx
index 968d04033..a50e73fc8 100644
--- a/frontend/src/views/Transfers/components/CategoryCheckbox.jsx
+++ b/frontend/src/views/Transfers/components/CategoryCheckbox.jsx
@@ -5,8 +5,10 @@ import { Checkbox, FormControlLabel } from '@mui/material'
import { useQueryClient } from '@tanstack/react-query'
import { useEffect } from 'react'
import { useParams } from 'react-router-dom'
+import { useTranslation } from 'react-i18next'
-export const CategoryCheckbox = () => {
+export const CategoryCheckbox = ({ isDisabled = false }) => {
+ const { t } = useTranslation(['transfer'])
const { transferId } = useParams()
const queryClient = useQueryClient()
const setLoading = useLoadingStore((state) => state.setLoading)
@@ -36,15 +38,14 @@ export const CategoryCheckbox = () => {
data-test="checkbox"
checked={transferData?.transferCategory?.category === 'D'}
onClick={(e) => updateCategory(e.target.checked ? 'D' : null)}
+ disabled={isDisabled}
/>
}
label={
-
- Select the checkbox to set the transfer as{' '}
- Category D if the price is significantly less than
- fair market value. This will override the default category
- determined by the agreement and approval dates indicated above.
-
+
}
/>
diff --git a/frontend/src/views/Transfers/components/__tests__/CategoryCheckbox.test.jsx b/frontend/src/views/Transfers/components/__tests__/CategoryCheckbox.test.jsx
index 236d84f95..f6fc8712f 100644
--- a/frontend/src/views/Transfers/components/__tests__/CategoryCheckbox.test.jsx
+++ b/frontend/src/views/Transfers/components/__tests__/CategoryCheckbox.test.jsx
@@ -7,7 +7,7 @@ import { useTransfer, useUpdateCategory } from '@/hooks/useTransfer'
import { useLoadingStore } from '@/stores/useLoadingStore'
const keycloak = vi.hoisted(() => ({
- useKeycloak: vi.fn(),
+ useKeycloak: vi.fn()
}))
vi.mock('@react-keycloak/web', () => keycloak)
@@ -16,17 +16,17 @@ vi.mock('react-router-dom', async () => {
const actual = await vi.importActual('react-router-dom')
return {
...actual,
- useParams: () => ({ transferId: '123' }),
+ useParams: () => ({ transferId: '123' })
}
})
vi.mock('@/hooks/useTransfer', () => ({
useTransfer: vi.fn(),
- useUpdateCategory: vi.fn(),
+ useUpdateCategory: vi.fn()
}))
vi.mock('@/stores/useLoadingStore', () => ({
- useLoadingStore: vi.fn(),
+ useLoadingStore: vi.fn()
}))
describe('CategoryCheckbox Component', () => {
@@ -36,14 +36,17 @@ describe('CategoryCheckbox Component', () => {
beforeEach(() => {
keycloak.useKeycloak.mockReturnValue({
keycloak: { authenticated: true },
- initialized: true,
+ initialized: true
})
// Correctly mock useLoadingStore to handle the selector
- useLoadingStore.mockImplementation((selector) => selector({ setLoading: setLoadingMock }))
+ useLoadingStore.mockImplementation((selector) =>
+ selector({ setLoading: setLoadingMock })
+ )
+ // Mock updateCategory hook
useUpdateCategory.mockReturnValue({
- mutate: mutateMock,
+ mutate: mutateMock
})
// Reset mocks
@@ -58,7 +61,7 @@ describe('CategoryCheckbox Component', () => {
it('should render the component', () => {
useTransfer.mockReturnValue({
data: {},
- isFetching: false,
+ isFetching: false
})
render(, { wrapper })
@@ -70,7 +73,7 @@ describe('CategoryCheckbox Component', () => {
it('should display the checkbox as checked when category is "D"', () => {
useTransfer.mockReturnValue({
data: { transferCategory: { category: 'D' } },
- isFetching: false,
+ isFetching: false
})
render(, { wrapper })
@@ -82,7 +85,7 @@ describe('CategoryCheckbox Component', () => {
it('should display the checkbox as unchecked when category is not "D"', () => {
useTransfer.mockReturnValue({
data: { transferCategory: { category: null } },
- isFetching: false,
+ isFetching: false
})
render(, { wrapper })
@@ -94,7 +97,7 @@ describe('CategoryCheckbox Component', () => {
it('should call updateCategory with null when unchecking the checkbox', () => {
useTransfer.mockReturnValue({
data: { transferCategory: { category: 'D' } },
- isFetching: false,
+ isFetching: false
})
render(, { wrapper })
@@ -108,7 +111,7 @@ describe('CategoryCheckbox Component', () => {
it('should call updateCategory with "D" when checking the checkbox', () => {
useTransfer.mockReturnValue({
data: { transferCategory: { category: null } },
- isFetching: false,
+ isFetching: false
})
render(, { wrapper })
@@ -122,11 +125,28 @@ describe('CategoryCheckbox Component', () => {
it('should set loading state appropriately during fetch', () => {
useTransfer.mockReturnValue({
data: {},
- isFetching: false,
+ isFetching: false
})
render(, { wrapper })
expect(setLoadingMock).toHaveBeenCalledWith(false)
})
+
+ it('should disable the checkbox when isDisabled is true', () => {
+ useTransfer.mockReturnValue({
+ data: { transferCategory: { category: null } },
+ isFetching: false
+ })
+
+ render(, { wrapper })
+
+ const checkboxWrapper = screen.getByTestId('checkbox')
+ const checkboxInput = checkboxWrapper.querySelector(
+ 'input[type="checkbox"]'
+ )
+
+ // Verify the input is indeed disabled
+ expect(checkboxInput).toBeDisabled()
+ })
})