Skip to content

Commit

Permalink
Merge pull request #1240 from bcgov/feat/daniel-add-versioning-to-oth…
Browse files Browse the repository at this point in the history
…er-uses-1191

feat: Add versioning to other uses
  • Loading branch information
dhaselhan authored Nov 19, 2024
2 parents d9a53dd + 6ce4acc commit b59fa85
Show file tree
Hide file tree
Showing 14 changed files with 699 additions and 354 deletions.
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

0 comments on commit b59fa85

Please sign in to comment.