Skip to content

Commit

Permalink
Merge pull request #1997 from bcgov/LCFS-1945-AddCompliancePeriodDepe…
Browse files Browse the repository at this point in the history
…ndency

LCFS-1945: Add compliance_period_id to additional_carbon_intensity table
  • Loading branch information
areyeslo authored Feb 14, 2025
2 parents 61b3197 + d091d0d commit e6c2b76
Show file tree
Hide file tree
Showing 15 changed files with 207 additions and 41 deletions.
105 changes: 105 additions & 0 deletions backend/lcfs/db/migrations/versions/2025-02-11-23-51_44c6f23b71d3.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
"""add compliance period to uci
Revision ID: 44c6f23b71d3
Revises: f0d95904a9dd
Create Date: 2025-02-11 23:51:48.841478
"""

from alembic import op
import sqlalchemy as sa
from sqlalchemy import text
from datetime import datetime

# revision identifiers, used by Alembic.
revision = "44c6f23b71d3"
down_revision = "f0d95904a9dd"
branch_labels = None
depends_on = None


def upgrade():
# Add new column (initially nullable) to store compliance period
op.add_column(
'additional_carbon_intensity',
sa.Column('compliance_period_id', sa.Integer(), nullable=True)
)

# Add foreign key constraint
op.create_foreign_key(
'fk_additional_ci_compliance_period',
'additional_carbon_intensity',
'compliance_period',
['compliance_period_id'],
['compliance_period_id']
)

connection = op.get_bind()

# Get the compliance period for 2024 when the UCI was first introduced
target_year = "2024"
result = connection.execute(
text("""
SELECT compliance_period_id
FROM compliance_period
WHERE description = :year
"""),
{"year": target_year}
)
compliance_period_id = result.scalar_one()

if not compliance_period_id:
raise Exception(f"Compliance period for year {target_year} not found")

# Update all existing UCI records with the compliance period
connection.execute(
text("""
UPDATE additional_carbon_intensity
SET compliance_period_id = :period_id
WHERE compliance_period_id IS NULL
"""),
{"period_id": compliance_period_id}
)

# Verify the update
null_count = connection.execute(
text("""
SELECT COUNT(*)
FROM additional_carbon_intensity
WHERE compliance_period_id IS NULL
""")
).scalar()

if null_count > 0:
raise Exception(
f"Migration failed: {null_count} records still have null compliance_period_id"
)

# Make column not nullable only after verification
op.alter_column(
'additional_carbon_intensity',
'compliance_period_id',
existing_type=sa.Integer(),
nullable=False
)

# Create unique constraint
op.create_unique_constraint(
'uq_additional_ci_compliance_fuel_enduse',
'additional_carbon_intensity',
['compliance_period_id', 'fuel_type_id', 'end_use_type_id']
)


def downgrade():
op.drop_constraint(
'uq_additional_ci_compliance_fuel_enduse',
'additional_carbon_intensity',
type_='unique'
)
op.drop_constraint(
'fk_additional_ci_compliance_period',
'additional_carbon_intensity',
type_='foreignkey'
)
op.drop_column('additional_carbon_intensity', 'compliance_period_id')
5 changes: 5 additions & 0 deletions backend/lcfs/db/models/compliance/CompliancePeriod.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ class CompliancePeriod(BaseModel, EffectiveDates):
compliance_reports = relationship(
"ComplianceReport", back_populates="compliance_period"
)
additional_carbon_intensities = relationship(
"AdditionalCarbonIntensity",
back_populates="compliance_period",
cascade="all, delete-orphan"
)

def __repr__(self):
return f"<CompliancePeriod(id={self.compliance_period_id}, description={self.description})>"
39 changes: 31 additions & 8 deletions backend/lcfs/db/models/fuel/AdditionalCarbonIntensity.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from sqlalchemy import Column, Integer, Numeric
from sqlalchemy import Column, Integer, Numeric, UniqueConstraint
from lcfs.db.base import BaseModel, Auditable, DisplayOrder
from sqlalchemy.orm import relationship
from sqlalchemy import ForeignKey
Expand All @@ -7,20 +7,43 @@
class AdditionalCarbonIntensity(BaseModel, Auditable, DisplayOrder):

__tablename__ = "additional_carbon_intensity"
__table_args__ = {
"comment": "Additional carbon intensity attributable to the use of fuel. UCIs are added to the recorded carbon intensity of the fuel to account for additional carbon intensity attributed to the use of the fuel."
}
__table_args__ = (
UniqueConstraint(
"compliance_period_id",
"fuel_type_id",
"end_use_type_id",
name="uq_additional_ci_compliance_fuel_enduse",
),
{
"comment": "Additional carbon intensity attributable to the use of fuel with compliance period dependency"
},
)
# if both fuel type and end use type id's are null, then this is a default uci
additional_uci_id = Column(Integer, primary_key=True, autoincrement=True)
fuel_type_id = Column(Integer, ForeignKey("fuel_type.fuel_type_id"), nullable=True)
end_use_type_id = Column(
Integer, ForeignKey("end_use_type.end_use_type_id"), nullable=True
)
compliance_period_id = Column(
Integer,
ForeignKey("compliance_period.compliance_period_id"),
nullable=False,
comment="Compliance period for the UCI value"
)
uom_id = Column(Integer, ForeignKey("unit_of_measure.uom_id"), nullable=False)
intensity = Column(Numeric(10, 2), nullable=False)

fuel_type = relationship("FuelType", back_populates="additional_carbon_intensity")
end_use_type = relationship(
"EndUseType", back_populates="additional_carbon_intensity"
# Relationships
fuel_type = relationship(
"FuelType",
back_populates="additional_carbon_intensity",
overlaps="additional_carbon_intensities"
)
uom = relationship("UnitOfMeasure", back_populates="additional_carbon_intensity")
end_use_type = relationship(
"EndUseType",
back_populates="additional_carbon_intensities")
uom = relationship(
"UnitOfMeasure")
compliance_period = relationship(
"CompliancePeriod",
back_populates="additional_carbon_intensities")
5 changes: 5 additions & 0 deletions backend/lcfs/db/models/fuel/EndUseType.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,8 @@ class EndUseType(BaseModel, Auditable, DisplayOrder):
secondary=final_supply_intended_use_association,
back_populates="intended_use_types",
)
additional_carbon_intensities = relationship(
"AdditionalCarbonIntensity",
back_populates="end_use_type",
overlaps="additional_carbon_intensity"
)
5 changes: 5 additions & 0 deletions backend/lcfs/db/models/fuel/FuelType.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,8 @@ class FuelType(BaseModel, Auditable, DisplayOrder):
back_populates="fuel_type_provision_2",
)
fuel_instances = relationship("FuelInstance", back_populates="fuel_type")
additional_carbon_intensities = relationship(
"AdditionalCarbonIntensity",
back_populates="fuel_type",
overlaps="additional_carbon_intensity"
)
11 changes: 5 additions & 6 deletions backend/lcfs/tests/compliance_report/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,9 @@
from lcfs.web.api.compliance_report.update_service import (
ComplianceReportUpdateService,
)

from lcfs.web.api.common.schema import CompliancePeriodBaseSchema
from lcfs.web.api.compliance_report.schema import (
ComplianceReportBaseSchema,
CompliancePeriodSchema,
ComplianceReportOrganizationSchema,
ComplianceReportViewSchema,
SummarySchema,
Expand All @@ -37,7 +36,7 @@

@pytest.fixture
def compliance_period_schema():
return CompliancePeriodSchema(
return CompliancePeriodBaseSchema(
compliance_period_id=1,
description="2024",
effective_date=datetime(2024, 1, 1),
Expand Down Expand Up @@ -96,7 +95,7 @@ def _create_compliance_report_schema(
compliance_report_group_uuid: str = None,
version: int = 0,
compliance_period_id: int = None,
compliance_period: CompliancePeriodSchema = None,
compliance_period: CompliancePeriodBaseSchema = None,
organization_id: int = None,
organization: ComplianceReportOrganizationSchema = None,
report_type: str = "Annual Compliance",
Expand All @@ -108,7 +107,7 @@ def _create_compliance_report_schema(
compliance_period_id or compliance_period_schema.compliance_period_id
)
compliance_period = compliance_period or compliance_period_schema
if isinstance(compliance_period, CompliancePeriodSchema):
if isinstance(compliance_period, CompliancePeriodBaseSchema):
compliance_period = compliance_period.description
organization_id = (
organization_id or compliance_report_organization_schema.organization_id
Expand Down Expand Up @@ -152,7 +151,7 @@ def compliance_report_base_schema(
def _create_compliance_report_base_schema(
compliance_report_id: int = 1,
compliance_period_id: int = None,
compliance_period: CompliancePeriodSchema = None,
compliance_period: CompliancePeriodBaseSchema = None,
organization_id: int = None,
organization: ComplianceReportOrganizationSchema = None,
summary: SummarySchema = None,
Expand Down
3 changes: 2 additions & 1 deletion backend/lcfs/tests/fuel_code/test_fuel_code_repo.py
Original file line number Diff line number Diff line change
Expand Up @@ -734,5 +734,6 @@ async def test_get_additional_carbon_intensity(fuel_code_repo, mock_db):
mock_result.scalars.return_value.one_or_none.return_value = aci
mock_db.execute.return_value = mock_result

result = await fuel_code_repo.get_additional_carbon_intensity(1, 2)
# Added the compliance_period as required
result = await fuel_code_repo.get_additional_carbon_intensity(1, 2, "2025")
assert result == aci
5 changes: 3 additions & 2 deletions backend/lcfs/tests/fuel_export/conftest.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from datetime import datetime
import pytest
from unittest.mock import AsyncMock, MagicMock
from lcfs.web.api.compliance_report.schema import CompliancePeriodSchema, ComplianceReportHistorySchema, ComplianceReportOrganizationSchema, ComplianceReportStatusSchema, ComplianceReportUserSchema, SummarySchema
from lcfs.web.api.common.schema import CompliancePeriodBaseSchema
from lcfs.web.api.compliance_report.schema import ComplianceReportHistorySchema, ComplianceReportOrganizationSchema, ComplianceReportStatusSchema, ComplianceReportUserSchema, SummarySchema
from lcfs.web.api.fuel_export.repo import FuelExportRepository
from lcfs.web.api.fuel_code.repo import FuelCodeRepository
from lcfs.web.api.fuel_export.services import FuelExportServices
Expand Down Expand Up @@ -49,7 +50,7 @@ def mock_compliance_report_repo():

@pytest.fixture
def compliance_period_schema():
return CompliancePeriodSchema(
return CompliancePeriodBaseSchema(
compliance_period_id=1,
description="2024",
effective_date=datetime(2024, 1, 1),
Expand Down
11 changes: 11 additions & 0 deletions backend/lcfs/web/api/common/schema.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from typing import Optional
from datetime import datetime
from lcfs.web.api.base import BaseSchema

class CompliancePeriodBaseSchema(BaseSchema):
"""Base schema for compliance period that can be shared across modules"""
compliance_period_id: int
description: str
effective_date: Optional[datetime] = None
expiration_date: Optional[datetime] = None
display_order: Optional[int] = None
11 changes: 2 additions & 9 deletions backend/lcfs/web/api/compliance_report/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from datetime import datetime, date
from enum import Enum
from lcfs.db.models.compliance.ComplianceReportStatus import ComplianceReportStatusEnum
from lcfs.web.api.common.schema import CompliancePeriodBaseSchema
from lcfs.web.api.compliance_report.constants import FORMATS
from lcfs.web.api.fuel_code.schema import EndUseTypeSchema, EndUserTypeSchema

Expand Down Expand Up @@ -47,14 +48,6 @@ class PortsEnum(str, Enum):
DUAL = "Dual port"


class CompliancePeriodSchema(BaseSchema):
compliance_period_id: int
description: str
effective_date: Optional[datetime] = None
expiration_date: Optional[datetime] = None
display_order: Optional[int] = None


class SummarySchema(BaseSchema):
summary_id: int
is_locked: bool
Expand Down Expand Up @@ -147,7 +140,7 @@ class ComplianceReportBaseSchema(BaseSchema):
version: Optional[int]
supplemental_initiator: Optional[SupplementalInitiatorType]
compliance_period_id: int
compliance_period: CompliancePeriodSchema
compliance_period: CompliancePeriodBaseSchema
organization_id: int
organization: ComplianceReportOrganizationSchema
summary: Optional[SummarySchema]
Expand Down
8 changes: 4 additions & 4 deletions backend/lcfs/web/api/compliance_report/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@
from lcfs.db.models.user import UserProfile
from lcfs.web.api.base import PaginationResponseSchema
from lcfs.web.api.compliance_report.repo import ComplianceReportRepository
from lcfs.web.api.common.schema import CompliancePeriodBaseSchema
from lcfs.web.api.compliance_report.schema import (
CompliancePeriodSchema,
ComplianceReportBaseSchema,
ComplianceReportCreateSchema,
ComplianceReportListSchema,
Expand All @@ -39,10 +39,10 @@ def __init__(
self.snapshot_services = snapshot_services

@service_handler
async def get_all_compliance_periods(self) -> List[CompliancePeriodSchema]:
async def get_all_compliance_periods(self) -> List[CompliancePeriodBaseSchema]:
"""Fetches all compliance periods and converts them to Pydantic models."""
periods = await self.repo.get_all_compliance_periods()
return [CompliancePeriodSchema.model_validate(period) for period in periods]
return [CompliancePeriodBaseSchema.model_validate(period) for period in periods]

@service_handler
async def create_compliance_report(
Expand Down Expand Up @@ -302,5 +302,5 @@ def _mask_report_status_for_history(
@service_handler
async def get_all_org_reported_years(
self, organization_id: int
) -> List[CompliancePeriodSchema]:
) -> List[CompliancePeriodBaseSchema]:
return await self.repo.get_all_org_reported_years(organization_id)
6 changes: 3 additions & 3 deletions backend/lcfs/web/api/compliance_report/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
from lcfs.db.models.user.Role import RoleEnum
from lcfs.services.s3.client import DocumentService
from lcfs.web.api.base import FilterModel, PaginationRequestSchema
from lcfs.web.api.common.schema import CompliancePeriodBaseSchema
from lcfs.web.api.compliance_report.schema import (
CompliancePeriodSchema,
ComplianceReportBaseSchema,
ComplianceReportListSchema,
ComplianceReportSummarySchema,
Expand All @@ -40,13 +40,13 @@

@router.get(
"/compliance-periods",
response_model=List[CompliancePeriodSchema],
response_model=List[CompliancePeriodBaseSchema],
status_code=status.HTTP_200_OK,
)
@view_handler(["*"])
async def get_compliance_periods(
request: Request, service: ComplianceReportServices = Depends()
) -> list[CompliancePeriodSchema]:
) -> list[CompliancePeriodBaseSchema]:
"""
Get a list of compliance periods
"""
Expand Down
Loading

0 comments on commit e6c2b76

Please sign in to comment.