Skip to content

Commit d15c91d

Browse files
authored
Merge pull request #2584 from bcgov/release
Release Hotfix v1.0.5.1
2 parents 8ab100e + ef3b7fd commit d15c91d

File tree

5 files changed

+199
-139
lines changed

5 files changed

+199
-139
lines changed

backend/lcfs/tests/transfer/test_transfer_services.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import pytest
22
from unittest.mock import AsyncMock, MagicMock, patch
33
from datetime import date, datetime, timedelta
4+
from types import SimpleNamespace
45

56
from lcfs.db.models import UserProfile
67
from lcfs.db.models.user.Role import RoleEnum
@@ -300,3 +301,57 @@ async def should_not_be_called(*args, **kwargs):
300301

301302
assert dummy_transfer.to_transaction == "new_transaction"
302303
assert dummy_transfer.category == "Existing"
304+
305+
306+
@pytest.mark.anyio
307+
async def test_director_record_transfer_with_none_category(
308+
transfer_service, dummy_transfer, mock_director
309+
):
310+
"""
311+
Test that director_record_transfer correctly assigns a category
312+
when transfer_category is None. This tests the bug fix for the
313+
case where hasattr(None, 'category') would raise an AttributeError.
314+
"""
315+
# Set transfer_category to None to simulate the bug scenario
316+
dummy_transfer.transfer_category = None
317+
dummy_transfer.agreement_date = datetime.now() - timedelta(
318+
days=10
319+
) # Recent agreement date for category A
320+
321+
# Track if update_category was called
322+
category_updated = False
323+
324+
async def dummy_update_category_fn(transfer_id, category):
325+
nonlocal category_updated
326+
category_updated = True
327+
dummy_transfer.called_category = category
328+
dummy_transfer.transfer_category = SimpleNamespace(category=category)
329+
return dummy_transfer
330+
331+
transfer_service.update_category = dummy_update_category_fn
332+
333+
async def confirm_success(tx_id):
334+
return True
335+
336+
transfer_service.transaction_repo.confirm_transaction = confirm_success
337+
338+
async def dummy_adjust_balance_fn(
339+
*, transaction_action, compliance_units, organization_id
340+
):
341+
return "new_transaction"
342+
343+
transfer_service.org_service.adjust_balance = dummy_adjust_balance_fn
344+
345+
# Mock repo.refresh_transfer to do nothing
346+
async def mock_refresh(transfer):
347+
return
348+
349+
transfer_service.repo.refresh_transfer = mock_refresh
350+
transfer_service.repo.update_transfer = AsyncMock(return_value=dummy_transfer)
351+
352+
await transfer_service.director_record_transfer(dummy_transfer, mock_director)
353+
354+
# Verify that update_category was called and with the correct category
355+
assert category_updated is True
356+
assert getattr(dummy_transfer, "called_category", None) == "A"
357+
assert dummy_transfer.to_transaction == "new_transaction"

backend/lcfs/web/api/transfer/services.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -453,7 +453,9 @@ async def director_record_transfer(self, transfer: Transfer, user: UserProfile):
453453

454454
await self.repo.refresh_transfer(transfer)
455455

456-
if not hasattr(transfer.transfer_category, "category"):
456+
if transfer.transfer_category is None or not hasattr(
457+
transfer.transfer_category, "category"
458+
):
457459
today = datetime.now()
458460
diff_seconds = today.timestamp() - transfer.agreement_date.timestamp()
459461
# Define approximate thresholds in seconds

frontend/src/assets/locales/en/reports.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@
128128
{
129129
"header": "Audits of reports",
130130
"content": [
131-
"<p>Responsible parties must keep sufficient records to verify and evaluate any reports and information submitted to the director. Under section 34 of the Low Carbon Fuels Act, the director can require an organization that submits a report to have the report audited by a designated inspector to verify compliance with the legislation. Any inconsistencies noted during the audit are deemed contraventions under the Low Carbon Fuels Act and may be subject to penalty after substantiation.</p>"
131+
"<p>Responsible parties must keep sufficient records to verify and evaluate any reports and information submitted to the director. Under section 34 of the Low Carbon Fuels Act, the director can require an organization that submits a report to have the report audited to verify compliance with the legislation. Any inconsistencies noted during the audit may be contraventions under the Low Carbon Fuels Act and may be subject to penalty after substantiation.</p>"
132132
]
133133
},
134134
{

frontend/src/views/ComplianceReports/components/HistoryCard.jsx

Lines changed: 93 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { useTranslation } from 'react-i18next'
1616
const Accordion = styled((props) => (
1717
<MuiAccordion disableGutters elevation={0} square {...props} />
1818
))(() => ({
19-
border: `none`,
19+
border: 'none',
2020
'&::before': {
2121
display: 'none'
2222
}
@@ -49,15 +49,53 @@ export const HistoryCard = ({ report, defaultExpanded = false }) => {
4949
const { data: currentUser } = useCurrentUser()
5050
const isGovernmentUser = currentUser?.isGovernmentUser
5151
const { t } = useTranslation(['report'])
52-
const filteredHistory = useMemo(() => {
53-
if (!report.history || report.history.length === 0) {
54-
return []
55-
}
56-
// Sort the history array by date in descending order
52+
53+
const isCurrentAssessed =
54+
report.currentStatus?.status === COMPLIANCE_REPORT_STATUSES.ASSESSED
55+
56+
/**
57+
* Helper: build the two assessment list items.
58+
* We use it twice – once top‑level for gov users (pre‑assessment)
59+
* and once nested under the Assessed history entry for all users.
60+
*/
61+
const AssessmentLines = () => (
62+
<>
63+
<StyledListItem disablePadding>
64+
<ListItemText primaryTypographyProps={{ variant: 'body4' }}>
65+
<strong>
66+
{t('report:complianceReportHistory.renewableTarget')}:&nbsp;
67+
</strong>
68+
{t('report:assessmentLn1', {
69+
name: report.organization.name,
70+
hasMet:
71+
report.summary.line11FossilDerivedBaseFuelTotal <= 0
72+
? 'has met'
73+
: 'has not met'
74+
})}
75+
</ListItemText>
76+
</StyledListItem>
77+
<StyledListItem disablePadding>
78+
<ListItemText primaryTypographyProps={{ variant: 'body4' }}>
79+
<strong>
80+
{t('report:complianceReportHistory.lowCarbonTarget')}:&nbsp;
81+
</strong>
82+
{t('report:assessmentLn2', {
83+
name: report.organization.name,
84+
hasMet:
85+
report.summary.line21NonCompliancePenaltyPayable <= 0
86+
? 'has met'
87+
: 'has not met'
88+
})}
89+
</ListItemText>
90+
</StyledListItem>
91+
</>
92+
)
93+
94+
const sortedHistory = useMemo(() => {
95+
if (!Array.isArray(report.history) || report.history.length === 0) return []
96+
5797
return [...report.history]
58-
.sort((a, b) => {
59-
return new Date(b.createDate) - new Date(a.createDate)
60-
})
98+
.sort((a, b) => new Date(b.createDate) - new Date(a.createDate))
6199
.map((item) => {
62100
if (
63101
item.status.status === COMPLIANCE_REPORT_STATUSES.ASSESSED &&
@@ -82,12 +120,12 @@ export const HistoryCard = ({ report, defaultExpanded = false }) => {
82120
: {report.currentStatus.status}
83121
</BCTypography>
84122
</AccordionSummary>
85-
{filteredHistory.length > 0 && (
123+
124+
{sortedHistory.length > 0 && (
86125
<AccordionDetails>
87126
<List>
88127
{report.assessmentStatement &&
89-
((!isGovernmentUser &&
90-
report.currentStatus.status === 'Assessed') ||
128+
((!isGovernmentUser && isCurrentAssessed) ||
91129
isGovernmentUser) && (
92130
<StyledListItem disablePadding>
93131
<ListItemText
@@ -101,71 +139,49 @@ export const HistoryCard = ({ report, defaultExpanded = false }) => {
101139
</ListItemText>
102140
</StyledListItem>
103141
)}
104-
{filteredHistory.map((item, index) => (
105-
<StyledListItem key={index} disablePadding>
106-
<ListItemText
107-
data-test="list-item"
108-
primaryTypographyProps={{ variant: 'body4' }}
109-
>
110-
<span
111-
dangerouslySetInnerHTML={{
112-
__html: t(
113-
`report:complianceReportHistory.${item.status.status}`,
114-
{
115-
createDate: timezoneFormatter({
116-
value: item?.createDate
117-
}),
118-
displayName:
119-
item.displayName ||
120-
`${item.userProfile.firstName} ${item.userProfile.lastName}`
121-
}
122-
)
123-
}}
124-
/>
125-
</ListItemText>
126-
{[COMPLIANCE_REPORT_STATUSES.ASSESSED, 'AssessedBy'].includes(
127-
item.status.status
128-
) && (
129-
<List sx={{ p: 0, m: 0 }}>
130-
<StyledListItem disablePadding>
131-
<ListItemText
132-
primaryTypographyProps={{ variant: 'body4' }}
133-
>
134-
<strong>
135-
{t('report:complianceReportHistory.renewableTarget')}
136-
:&nbsp;
137-
</strong>
138-
{t('report:assessmentLn1', {
139-
name: report.organization.name,
140-
hasMet:
141-
report.summary.line11FossilDerivedBaseFuelTotal <= 0
142-
? 'has met'
143-
: 'has not met'
144-
})}
145-
</ListItemText>
146-
</StyledListItem>
147-
<StyledListItem disablePadding>
148-
<ListItemText
149-
primaryTypographyProps={{ variant: 'body4' }}
150-
>
151-
<strong>
152-
{t('report:complianceReportHistory.lowCarbonTarget')}
153-
:&nbsp;
154-
</strong>
155-
{t('report:assessmentLn2', {
156-
name: report.organization.name,
157-
hasMet:
158-
report.summary.line21NonCompliancePenaltyPayable <=
159-
0
160-
? 'has met'
161-
: 'has not met'
162-
})}
163-
</ListItemText>
164-
</StyledListItem>
165-
</List>
166-
)}
167-
</StyledListItem>
168-
))}
142+
143+
{/* GOV users – show assessment lines immediately (top‑level) until Assessed */}
144+
{isGovernmentUser && !isCurrentAssessed && <AssessmentLines />}
145+
146+
{/* History timeline */}
147+
{sortedHistory.map((item, index) => {
148+
const showNestedAssessment = [
149+
COMPLIANCE_REPORT_STATUSES.ASSESSED,
150+
'AssessedBy'
151+
].includes(item.status.status)
152+
153+
return (
154+
<StyledListItem key={index} disablePadding>
155+
<ListItemText
156+
data-test="list-item"
157+
primaryTypographyProps={{ variant: 'body4' }}
158+
>
159+
<span
160+
dangerouslySetInnerHTML={{
161+
__html: t(
162+
`report:complianceReportHistory.${item.status.status}`,
163+
{
164+
createDate: timezoneFormatter({
165+
value: item.createDate
166+
}),
167+
displayName:
168+
item.displayName ||
169+
`${item.userProfile.firstName} ${item.userProfile.lastName}`
170+
}
171+
)
172+
}}
173+
/>
174+
</ListItemText>
175+
176+
{/* Nested assessment – appears once the status is Assessed */}
177+
{showNestedAssessment && (
178+
<List sx={{ p: 0, m: 0 }}>
179+
<AssessmentLines />
180+
</List>
181+
)}
182+
</StyledListItem>
183+
)
184+
})}
169185
</List>
170186
</AccordionDetails>
171187
)}

0 commit comments

Comments
 (0)