Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add versioning to other uses #1240

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions backend/lcfs/db/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,6 @@ async def set_user_context(session: AsyncSession, username: str):
try:
await session.execute(text(f"SET SESSION app.username = '{username}'"))

logging.info(f"SET SESSION app.username = '{username}' executed successfully")

except Exception as e:
logging.error(f"Failed to execute SET LOCAL app.user_id = '{username}': {e}")
raise e
Expand Down
100 changes: 100 additions & 0 deletions backend/lcfs/db/migrations/versions/2024-11-13-00-39_654858661ae7.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
"""Add Versioning to Other Uses

Revision ID: 654858661ae7
Revises: b659816d0a86
Create Date: 2024-11-13 00:39:22.594912

"""

import uuid

import sqlalchemy as sa
from alembic import op
from sqlalchemy.dialects import postgresql

# revision identifiers, used by Alembic.
revision = "654858661ae7"
down_revision = "b659816d0a86"
branch_labels = None
depends_on = None


def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.add_column(
"other_uses",
sa.Column(
"group_uuid",
sa.String(length=36),
nullable=True,
comment="UUID that groups all versions of a record series",
),
)
op.add_column(
"other_uses",
sa.Column(
"version",
sa.Integer(),
nullable=False,
server_default="0",
comment="Version number of the record",
),
)
op.add_column(
"other_uses",
sa.Column(
"user_type",
postgresql.ENUM(
"SUPPLIER", "GOVERNMENT", name="usertypeenum", create_type=False
),
nullable=False,
server_default=sa.text("'SUPPLIER'"),
comment="Indicates whether the record was created/modified by a supplier or government user",
),
)
op.add_column(
"other_uses",
sa.Column(
"action_type",
postgresql.ENUM(
"CREATE", "UPDATE", "DELETE", name="actiontypeenum", create_type=False
),
server_default=sa.text("'CREATE'"),
nullable=True,
comment="Action type for this record",
),
)

#Update existing records with generated UUIDs
connection = op.get_bind()

# Update other_uses table
fuel_exports = connection.execute(
sa.text("SELECT other_uses_id FROM other_uses WHERE group_uuid IS NULL")
).fetchall()
for export in fuel_exports:
export_id = export[0]
connection.execute(
sa.text(
"UPDATE other_uses SET group_uuid = :uuid WHERE other_uses_id = :export_id"
),
{"uuid": str(uuid.uuid4()), "export_id": export_id},
)

#Alter the column to be non-nullable
op.alter_column(
"other_uses",
"group_uuid",
existing_type=sa.String(length=36),
nullable=False,
)
# ### end Alembic commands ###


def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column("other_uses", "action_type")
op.drop_column("other_uses", "user_type")
op.drop_column("other_uses", "version")
op.drop_column("other_uses", "group_uuid")
# ### end Alembic commands ###
4 changes: 2 additions & 2 deletions backend/lcfs/db/models/compliance/OtherUses.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
from sqlalchemy import Column, Integer, String, ForeignKey
from sqlalchemy.orm import relationship
from lcfs.db.base import BaseModel, Auditable
from lcfs.db.base import BaseModel, Auditable, Versioning


class OtherUses(BaseModel, Auditable):
class OtherUses(BaseModel, Auditable, Versioning):
__tablename__ = "other_uses"
__table_args__ = {
"comment": "Records other uses of fuels that are subject to renewable requirements but do not earn credits."
Expand Down
Empty file.
49 changes: 49 additions & 0 deletions backend/lcfs/tests/other_uses/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
from unittest.mock import MagicMock

from lcfs.db.base import UserTypeEnum, ActionTypeEnum
from lcfs.db.models import OtherUses
from lcfs.web.api.other_uses.schema import OtherUsesCreateSchema


def create_mock_entity(overrides: dict):
mock_entity = MagicMock(spec=OtherUses)
mock_entity.other_uses_id = 1
mock_entity.compliance_report_id = 1
mock_entity.fuel_type.fuel_type = "Gasoline"
mock_entity.fuel_category.category = "Petroleum-based"
mock_entity.expected_use.name = "Transportation"
mock_entity.units = "L"
mock_entity.rationale = "Test rationale"
mock_entity.group_uuid = "test-group-uuid"
mock_entity.version = 1
mock_entity.quantity_supplied = 1000
mock_entity.action_type = ActionTypeEnum.CREATE
mock_entity.user_type = UserTypeEnum.SUPPLIER

# Apply overrides
if overrides:
for key, value in overrides.items():
setattr(mock_entity, key, value)

return mock_entity


def create_mock_schema(overrides: dict):
mock_schema = OtherUsesCreateSchema(
compliance_report_id=1,
quantity_supplied=1000,
fuel_type="Gasoline",
fuel_category="Petroleum-based",
expected_use="Transportation",
units="L",
rationale="Test rationale",
group_uuid="test-group-uuid",
version=1,
)

# Apply overrides
if overrides:
for key, value in overrides.items():
setattr(mock_schema, key, value)

return mock_schema
149 changes: 68 additions & 81 deletions backend/lcfs/tests/other_uses/test_other_uses_repo.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@
from unittest.mock import MagicMock, AsyncMock
from sqlalchemy.ext.asyncio import AsyncSession

from lcfs.db.base import UserTypeEnum
from lcfs.db.models.compliance import OtherUses
from lcfs.web.api.other_uses.repo import OtherUsesRepository
from lcfs.web.api.other_uses.schema import OtherUsesSchema
from lcfs.web.api.base import PaginationRequestSchema
from lcfs.tests.other_uses.conftest import create_mock_entity


@pytest.fixture
def mock_db_session():
Expand All @@ -18,6 +20,7 @@ def mock_db_session():
session.execute.return_value = execute_result
return session


@pytest.fixture
def other_uses_repo(mock_db_session):
repo = OtherUsesRepository(db=mock_db_session)
Expand All @@ -27,6 +30,7 @@ def other_uses_repo(mock_db_session):
repo.fuel_code_repo.get_expected_use_types = AsyncMock(return_value=[])
return repo


@pytest.mark.anyio
async def test_get_table_options(other_uses_repo):
result = await other_uses_repo.get_table_options()
Expand All @@ -37,20 +41,28 @@ async def test_get_table_options(other_uses_repo):
assert "units_of_measure" in result
assert "expected_uses" in result


@pytest.mark.anyio
async def test_get_other_uses(other_uses_repo, mock_db_session):
compliance_report_id = 1
mock_other_use = MagicMock(spec=OtherUses)
mock_other_use.other_uses_id = 1
mock_other_use.compliance_report_id = compliance_report_id
mock_other_use.quantity_supplied = 1000
mock_other_use.fuel_type.fuel_type = "Gasoline"
mock_other_use.fuel_category.category = "Petroleum-based"
mock_other_use.expected_use.name = "Transportation"
mock_other_use.units = "L"
mock_other_use.rationale = "Test rationale"
mock_result = [mock_other_use]
mock_db_session.execute.return_value.unique.return_value.scalars.return_value.all.return_value = mock_result
mock_other_use = create_mock_entity({})
mock_result_other_uses = [mock_other_use]
mock_compliance_report_uuid = "mock_group_uuid"

# Mock the first db.execute call for fetching compliance report group UUID
mock_first_execute = MagicMock()
mock_first_execute.scalar.return_value = mock_compliance_report_uuid

# Mock the second db.execute call for fetching other uses
mock_second_execute = MagicMock()
mock_second_execute.unique.return_value.scalars.return_value.all.return_value = (
mock_result_other_uses
)

# Assign side effects to return these mocked execute calls in sequence
mock_db_session.execute = AsyncMock(
side_effect=[mock_first_execute, mock_second_execute]
)

result = await other_uses_repo.get_other_uses(compliance_report_id)

Expand All @@ -61,93 +73,68 @@ async def test_get_other_uses(other_uses_repo, mock_db_session):
assert result[0].fuel_category == "Petroleum-based"
assert result[0].expected_use == "Transportation"


@pytest.mark.anyio
async def test_get_other_uses_paginated(other_uses_repo, mock_db_session):
pagination = PaginationRequestSchema(page=1, size=10)
compliance_report_id = 1
async def test_get_latest_other_uses_by_group_uuid(other_uses_repo, mock_db_session):
group_uuid = "test-group-uuid"
mock_other_use_gov = MagicMock(spec=OtherUses)
mock_other_use_gov.user_type = UserTypeEnum.GOVERNMENT
mock_other_use_gov.version = 2

# Create a mock OtherUsesSchema instance
mock_other_use = MagicMock(spec=OtherUsesSchema)
mock_other_use.other_uses_id = 1
mock_other_use.compliance_report_id = compliance_report_id
mock_other_use.quantity_supplied = 1000
mock_other_use.fuel_type = "Gasoline"
mock_other_use.fuel_category = "Petroleum-based"
mock_other_use.expected_use = "Transportation"
mock_other_use.units = "L"
mock_other_use.rationale = "Test rationale"
mock_result = [mock_other_use]
mock_other_use_supplier = MagicMock(spec=OtherUses)
mock_other_use_supplier.user_type = UserTypeEnum.SUPPLIER
mock_other_use_supplier.version = 3

# Mock the result of the count query
mock_count_result = MagicMock()
mock_count_result.scalar.return_value = 1
# Mock response with both government and supplier versions
mock_db_session.execute.return_value.scalars.return_value.first.side_effect = [
mock_other_use_gov,
mock_other_use_supplier,
]

# Mock the result of the main query
mock_main_result = MagicMock()
mock_main_result.unique.return_value.scalars.return_value.all.return_value = mock_result
result = await other_uses_repo.get_latest_other_uses_by_group_uuid(group_uuid)

# Configure the execute method to return different results based on the call sequence
mock_db_session.execute = AsyncMock(side_effect=[mock_count_result, mock_main_result])
assert result.user_type == UserTypeEnum.GOVERNMENT
assert result.version == 2

# Call the repository method
result, total_count = await other_uses_repo.get_other_uses_paginated(pagination, compliance_report_id)

# Assertions
assert isinstance(result, list)
assert len(result) == 1
assert isinstance(result[0], OtherUsesSchema)
assert result[0].fuel_type == "Gasoline"
assert result[0].fuel_category == "Petroleum-based"
assert result[0].expected_use == "Transportation"
assert isinstance(total_count, int)
assert total_count == 1
@pytest.mark.anyio
async def test_get_other_use_version_by_user(other_uses_repo, mock_db_session):
group_uuid = "test-group-uuid"
version = 2
user_type = UserTypeEnum.SUPPLIER

mock_other_use = MagicMock(spec=OtherUses)
mock_other_use.group_uuid = group_uuid
mock_other_use.version = version
mock_other_use.user_type = user_type

mock_db_session.execute.return_value.scalars.return_value.first.return_value = (
mock_other_use
)

result = await other_uses_repo.get_other_use_version_by_user(
group_uuid, version, user_type
)

assert result.group_uuid == group_uuid
assert result.version == version
assert result.user_type == user_type


@pytest.mark.anyio
async def test_update_other_use(other_uses_repo, mock_db_session):
updated_other_use = MagicMock(spec=OtherUses)
updated_other_use.other_uses_id = 1
updated_other_use.compliance_report_id = 1
updated_other_use = create_mock_entity({})
updated_other_use.quantity_supplied = 2000
updated_other_use.fuel_type.fuel_type = "Diesel"
updated_other_use.fuel_category.category = "Petroleum-based"
updated_other_use.expected_use.name = "Transportation"
updated_other_use.units = "L"
updated_other_use.rationale = "Updated rationale"

mock_db_session.flush = AsyncMock()
mock_db_session.refresh = AsyncMock()
mock_db_session.merge.return_value = updated_other_use

result = await other_uses_repo.update_other_use(updated_other_use)

assert isinstance(result, OtherUses)
assert result.fuel_type.fuel_type == "Diesel"
assert result.fuel_category.category == "Petroleum-based"
assert result.expected_use.name == "Transportation"

@pytest.mark.anyio
async def test_get_other_use(other_uses_repo, mock_db_session):
other_uses_id = 1

# Create a mock OtherUses instance
mock_result = MagicMock(spec=OtherUses)
mock_result.other_uses_id = other_uses_id
mock_result.compliance_report_id = 1
mock_result.quantity_supplied = 1000
mock_result.fuel_type.fuel_type = "Gasoline"
mock_result.fuel_category.category = "Petroleum-based"
mock_result.expected_use.name = "Transportation"
mock_result.units = "L"
mock_result.rationale = "Test rationale"

# Configure the scalar method to return the mock_result
mock_db_session.scalar = AsyncMock(return_value=mock_result)

# Call the repository method
result = await other_uses_repo.get_other_use(other_uses_id)

# Assertions
assert isinstance(result, OtherUses)
assert result.other_uses_id == other_uses_id
assert result.fuel_type.fuel_type == "Gasoline"
assert result.fuel_category.category == "Petroleum-based"
assert result.expected_use.name == "Transportation"
assert mock_db_session.flush.call_count == 1
assert mock_db_session.flush.call_count == 1
Loading