Skip to content

Commit

Permalink
Merge branch 'release-0.2.0' into feat/daniel-auto-grid-resize-1580
Browse files Browse the repository at this point in the history
  • Loading branch information
dhaselhan authored Jan 11, 2025
2 parents 13c14b3 + 994ce63 commit a48488a
Show file tree
Hide file tree
Showing 20 changed files with 236 additions and 79 deletions.
129 changes: 129 additions & 0 deletions backend/lcfs/db/migrations/versions/2025-01-10-13-39_d25e7c47659e.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
"""mv update on org balances
Revision ID: d25e7c47659e
Revises: fa98709e7952
Create Date: 2025-01-10 13:39:31.688471
"""

import sqlalchemy as sa
from alembic import op

# revision identifiers, used by Alembic.
revision = "d25e7c47659e"
down_revision = "fa98709e7952"
branch_labels = None
depends_on = None


def upgrade() -> None:
# Create or replace the function with updated logic:
# 1) total_balance now sums:
# - All compliance_units from 'Adjustment'
# - Negative compliance_units from 'Reserved'
# 2) reserved_balance sums only negative compliance_units from 'Reserved'
op.execute(
"""
CREATE OR REPLACE FUNCTION update_organization_balance()
RETURNS TRIGGER AS $$
DECLARE
new_total_balance BIGINT;
new_reserved_balance BIGINT;
org_id INT := COALESCE(NEW.organization_id, OLD.organization_id);
BEGIN
-- Calculate new total_balance:
-- adjustments + negative reserved units
SELECT COALESCE(
SUM(
CASE
WHEN transaction_action = 'Adjustment' THEN compliance_units
WHEN transaction_action = 'Reserved' AND compliance_units < 0 THEN compliance_units
ELSE 0
END
),
0
)
INTO new_total_balance
FROM "transaction"
WHERE organization_id = org_id;
-- Calculate new reserved_balance from negative compliance_units
SELECT COALESCE(SUM(compliance_units), 0)
INTO new_reserved_balance
FROM "transaction"
WHERE organization_id = org_id
AND transaction_action = 'Reserved'
AND compliance_units < 0;
UPDATE organization
SET total_balance = new_total_balance,
reserved_balance = new_reserved_balance
WHERE organization_id = org_id;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
"""
)

op.execute(
"""
DROP TRIGGER IF EXISTS update_organization_balance_trigger ON "transaction";
"""
)
op.execute(
"""
CREATE TRIGGER update_organization_balance_trigger
AFTER INSERT OR UPDATE OR DELETE ON "transaction"
FOR EACH ROW EXECUTE FUNCTION update_organization_balance();
"""
)


def downgrade() -> None:
# Revert to the original logic:
# 1) total_balance sums only 'Adjustment'
# 2) reserved_balance sums all (positive and negative) 'Reserved'
op.execute(
"""
CREATE OR REPLACE FUNCTION update_organization_balance()
RETURNS TRIGGER AS $$
DECLARE
new_total_balance BIGINT;
new_reserved_balance BIGINT;
org_id INT := COALESCE(NEW.organization_id, OLD.organization_id);
BEGIN
SELECT COALESCE(SUM(compliance_units), 0)
INTO new_total_balance
FROM "transaction"
WHERE organization_id = org_id
AND transaction_action = 'Adjustment';
SELECT COALESCE(SUM(compliance_units), 0)
INTO new_reserved_balance
FROM "transaction"
WHERE organization_id = org_id
AND transaction_action = 'Reserved';
UPDATE organization
SET total_balance = new_total_balance,
reserved_balance = new_reserved_balance
WHERE organization_id = org_id;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
"""
)

op.execute(
"""
DROP TRIGGER IF EXISTS update_organization_balance_trigger ON "transaction";
"""
)
op.execute(
"""
CREATE TRIGGER update_organization_balance_trigger
AFTER INSERT OR UPDATE OR DELETE ON "transaction"
FOR EACH ROW EXECUTE FUNCTION update_organization_balance();
"""
)
8 changes: 4 additions & 4 deletions backend/lcfs/tests/organizations/test_organizations_repo.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,8 @@ async def test_get_organizations_paginated_balances_with_reserved_transactions(
BASE_TOTAL_BALANCE + 100
), f"Expected total balance to be 100, got {org.total_balance}"
assert (
org.reserved_balance == 30
), f"Expected reserved balance to be 30, got {org.reserved_balance}"
org.reserved_balance == 0
), f"Expected reserved balance to be 0, got {org.reserved_balance}"


@pytest.mark.anyio
Expand Down Expand Up @@ -142,8 +142,8 @@ async def test_get_organizations_paginated_balances_with_released_transactions(
org.total_balance == 51100
), f"Expected total balance to be 100, got {org.total_balance}"
assert (
org.reserved_balance == 10
), f"Expected reserved balance to be 10, got {org.reserved_balance}"
org.reserved_balance == 0
), f"Expected reserved balance to be 0, got {org.reserved_balance}"


@pytest.mark.anyio
Expand Down
6 changes: 6 additions & 0 deletions backend/lcfs/web/api/transfer/repo.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ async def get_transfer_by_id(self, transfer_id: int) -> Transfer:
"""
Queries the database for a transfer by its ID and returns the ORM model.
Eagerly loads related entities to prevent lazy loading issues.
Orders the transfer history by create_date.
"""
query = (
select(Transfer)
Expand All @@ -88,6 +89,11 @@ async def get_transfer_by_id(self, transfer_id: int) -> Transfer:

result = await self.db.execute(query)
transfer = result.scalars().first()

# Ensure transfer_history is ordered by create_date
if transfer and transfer.transfer_history:
transfer.transfer_history.sort(key=lambda history: history.create_date)

return transfer

@repo_handler
Expand Down
2 changes: 1 addition & 1 deletion backend/lcfs/web/api/transfer/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ class TransferSchema(BaseSchema):
transfer_id: int
from_organization: TransferOrganizationSchema
to_organization: TransferOrganizationSchema
agreement_date: date
agreement_date: Optional[date] = None
quantity: int
price_per_unit: float
comments: Optional[List[TransferCommentSchema]] = None
Expand Down
31 changes: 18 additions & 13 deletions etl/nifi_scripts/transfer.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -89,12 +89,7 @@ def SOURCE_QUERY = """
WHEN cts.status = 'Refused' THEN 'Declined'
WHEN cts.status = 'Submitted' THEN 'Sent'
ELSE cts.status
END AS current_status,
CASE
WHEN cts.status = 'Not Recommended' THEN 'Refuse'
WHEN cts.status = 'Recommended' THEN 'Record'
ELSE NULL
END AS recommendation
END AS current_status
FROM
credit_trade ct
JOIN credit_trade_type ctt ON ct.type_id = ctt.id
Expand Down Expand Up @@ -203,13 +198,21 @@ try {
continue
}

// Identify if the history ever contained "Recommended" or "Not Recommended"
def recommendationValue = null
if (creditTradeHistoryJson.any { it.transfer_status == 'Recommended' }) {
recommendationValue = 'Record' // matches "Record" in the transfer_recommendation_enum
} else if (creditTradeHistoryJson.any { it.transfer_status == 'Not Recommended' }) {
recommendationValue = 'Refuse' // matches "Refuse" in the transfer_recommendation_enum
}

// Only if transfer does not exist, proceed to create transactions and then insert the transfer.
def (fromTransactionId, toTransactionId) = processTransactions(resultSet.getString('current_status'),
resultSet,
statements.transactionStmt)

def transferId = insertTransfer(resultSet, statements.transferStmt,
fromTransactionId, toTransactionId, preparedData, destinationConn)
fromTransactionId, toTransactionId, preparedData, destinationConn, recommendationValue)

if (transferId) {
processHistory(transferId, creditTradeHistoryJson, statements.historyStmt, preparedData)
Expand Down Expand Up @@ -398,23 +401,26 @@ def transferExists(Connection conn, int transferId) {
def processHistory(Integer transferId, List creditTradeHistory, PreparedStatement historyStmt, Map preparedData) {
if (!creditTradeHistory) return

// Sort the records by create_timestamp to preserve chronological order
def sortedHistory = creditTradeHistory.sort { a, b ->
toSqlTimestamp(a.create_timestamp ?: '2013-01-01T00:00:00Z') <=> toSqlTimestamp(b.create_timestamp ?: '2013-01-01T00:00:00Z')
}

// Use a Set to track unique combinations of transfer_id and transfer_status
def processedEntries = new HashSet<String>()

creditTradeHistory.each { historyItem ->
sortedHistory.each { historyItem ->
try {
def statusId = getStatusId(historyItem.transfer_status, preparedData)
def uniqueKey = "${transferId}_${statusId}"

// Check if this combination has already been processed
if (!processedEntries.contains(uniqueKey)) {
// If not processed, add to batch and mark as processed
historyStmt.setInt(1, transferId)
historyStmt.setInt(2, statusId)
historyStmt.setInt(3, historyItem.user_profile_id)
historyStmt.setTimestamp(4, toSqlTimestamp(historyItem.create_timestamp ?: '2013-01-01T00:00:00Z'))
historyStmt.addBatch()

processedEntries.add(uniqueKey)
}
} catch (Exception e) {
Expand All @@ -426,7 +432,6 @@ def processHistory(Integer transferId, List creditTradeHistory, PreparedStatemen
historyStmt.executeBatch()
}


def processInternalComments(Integer transferId, List internalComments,
PreparedStatement internalCommentStmt,
PreparedStatement transferInternalCommentStmt) {
Expand Down Expand Up @@ -475,7 +480,7 @@ def getAudienceScope(String roleNames) {
}

def insertTransfer(ResultSet rs, PreparedStatement transferStmt, Long fromTransactionId,
Long toTransactionId, Map preparedData, Connection conn) {
Long toTransactionId, Map preparedData, Connection conn, String recommendationValue) {
// Check for duplicates in the `transfer` table
def transferId = rs.getInt('transfer_id')
def duplicateCheckStmt = conn.prepareStatement('SELECT COUNT(*) FROM transfer WHERE transfer_id = ?')
Expand Down Expand Up @@ -507,7 +512,7 @@ def insertTransfer(ResultSet rs, PreparedStatement transferStmt, Long fromTransa
transferStmt.setString(11, rs.getString('gov_comment'))
transferStmt.setObject(12, categoryId)
transferStmt.setObject(13, statusId)
transferStmt.setString(14, rs.getString('recommendation'))
transferStmt.setString(14, recommendationValue)
transferStmt.setTimestamp(15, rs.getTimestamp('create_date'))
transferStmt.setTimestamp(16, rs.getTimestamp('update_date'))
transferStmt.setInt(17, rs.getInt('create_user'))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,7 @@ const SupplierBalance = () => {
const { data: orgBalance } = useCurrentOrgBalance()
const formattedTotalBalance =
orgBalance?.totalBalance != null
? numberFormatter({
value: orgBalance.totalBalance - Math.abs(orgBalance.reservedBalance)
})
? numberFormatter({ value: orgBalance.totalBalance })
: 'N/A'
const formattedReservedBalance =
orgBalance?.reservedBalance != null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,7 @@ export const EditViewComplianceReport = ({ reportData, isError, error }) => {
hasRoles
} = useCurrentUser()
const isGovernmentUser = currentUser?.isGovernmentUser
const isAnalystRole =
currentUser?.roles?.some((role) => role.name === roles.analyst) || false
const userRoles = currentUser?.roles

const currentStatus = reportData?.report.currentStatus?.status
const { data: orgData, isLoading } = useOrganization(
Expand Down Expand Up @@ -228,7 +227,7 @@ export const EditViewComplianceReport = ({ reportData, isError, error }) => {
<>
<ReportDetails
currentStatus={currentStatus}
isAnalystRole={isAnalystRole}
userRoles={userRoles}
/>
<ComplianceReportSummary
reportID={complianceReportId}
Expand Down
29 changes: 25 additions & 4 deletions frontend/src/views/ComplianceReports/components/ReportDetails.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,19 +34,41 @@ import DocumentUploadDialog from '@/components/Documents/DocumentUploadDialog'
import { useComplianceReportDocuments } from '@/hooks/useComplianceReports'
import { COMPLIANCE_REPORT_STATUSES } from '@/constants/statuses'

const ReportDetails = ({ currentStatus = 'Draft', isAnalystRole }) => {
const ReportDetails = ({ currentStatus = 'Draft', userRoles }) => {
const { t } = useTranslation()
const { compliancePeriod, complianceReportId } = useParams()
const navigate = useNavigate()

const [isFileDialogOpen, setFileDialogOpen] = useState(false)
const isAnalystRole = userRoles.some(role => role.name === roles.analyst) || false;
const isSupplierRole = userRoles.some(role => role.name === roles.supplier) || false;

const editSupportingDocs = useMemo(() => {
return isAnalystRole && (
currentStatus === COMPLIANCE_REPORT_STATUSES.SUBMITTED ||
currentStatus === COMPLIANCE_REPORT_STATUSES.ASSESSED
) || currentStatus === COMPLIANCE_REPORT_STATUSES.DRAFT;
)
}, [isAnalystRole, currentStatus]);

const editAnalyst = useMemo(() => {
return isAnalystRole && (
currentStatus === COMPLIANCE_REPORT_STATUSES.REASSESSED
)
}, [isAnalystRole, currentStatus]);

const editSupplier = useMemo(() => {
return isSupplierRole && (
currentStatus === COMPLIANCE_REPORT_STATUSES.DRAFT
)
}, [isSupplierRole, currentStatus]);

const shouldShowEditIcon = (activityName) => {
if (activityName === t('report:supportingDocs')) {
return editSupportingDocs;
}
return editAnalyst || editSupplier;
};

const isArrayEmpty = useCallback((data) => {
if (Array.isArray(data)) {
return data.length === 0
Expand Down Expand Up @@ -260,13 +282,12 @@ const ReportDetails = ({ currentStatus = 'Draft', isAnalystRole }) => {
component="div"
>
{activity.name}&nbsp;&nbsp;
{editSupportingDocs && (
{shouldShowEditIcon(activity.name) && (
<>
<Role
roles={[
roles.supplier,
roles.compliance_reporting,
roles.compliance_reporting,
roles.analyst
]}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,6 @@ const OrgBalanceCard = () => {
orgBalance.reservedBalance
).toLocaleString()

const availableCredits =
orgBalance.totalBalance - Math.abs(orgBalance.reservedBalance)

return (
<>
<BCTypography
Expand All @@ -57,7 +54,7 @@ const OrgBalanceCard = () => {
style={{ fontSize: '32px', color: '#578260', marginBottom: '-4px' }}
component="span"
>
{availableCredits.toLocaleString()}
{orgBalance.totalBalance.toLocaleString()}
</BCTypography>
<BCTypography
style={{ fontSize: '18px', color: '#003366', marginBottom: '-5px' }}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,10 +86,7 @@ const OrganizationsSummaryCard = () => {
style={{ fontSize: '32px', color: '#578260', marginBottom: '-2px' }}
component="span"
>
{numberFormatter(
selectedOrganization.totalBalance -
Math.abs(selectedOrganization.reservedBalance)
)}
{numberFormatter(selectedOrganization.totalBalance)}
</BCTypography>
<BCTypography
style={{ fontSize: '18px', color: '#003366', marginBottom: '-4px' }}
Expand Down
Loading

0 comments on commit a48488a

Please sign in to comment.