diff --git a/backend/lcfs/db/migrations/versions/2024-12-05-02-19_b69a33bbd135.py b/backend/lcfs/db/migrations/versions/2024-12-05-02-19_b69a33bbd135.py
new file mode 100644
index 000000000..3f1477892
--- /dev/null
+++ b/backend/lcfs/db/migrations/versions/2024-12-05-02-19_b69a33bbd135.py
@@ -0,0 +1,34 @@
+"""Add notifications email to user_profile
+
+Revision ID: b69a33bbd135
+Revises: 8491890dd688
+Create Date: 2024-12-05 20:48:24.724112
+
+"""
+
+import sqlalchemy as sa
+from alembic import op
+
+# revision identifiers, used by Alembic.
+revision = "b69a33bbd135"
+down_revision = "8491890dd688"
+branch_labels = None
+depends_on = None
+
+
+def upgrade() -> None:
+ # Add notifications_email column to user_profile table
+ op.add_column(
+ "user_profile",
+ sa.Column(
+ "notifications_email",
+ sa.String(length=255),
+ nullable=True,
+ comment="Email address used for notifications",
+ ),
+ )
+
+
+def downgrade() -> None:
+ # Remove notifications_email column from user_profile table
+ op.drop_column("user_profile", "notifications_email")
diff --git a/backend/lcfs/db/migrations/versions/2024-12-05-02-20_d4104af84f2b.py b/backend/lcfs/db/migrations/versions/2024-12-05-02-20_d4104af84f2b.py
new file mode 100644
index 000000000..a77e0cb43
--- /dev/null
+++ b/backend/lcfs/db/migrations/versions/2024-12-05-02-20_d4104af84f2b.py
@@ -0,0 +1,87 @@
+"""Update Notification Types and remove data
+
+Revision ID: d4104af84f2b
+Revises: b69a33bbd135
+Create Date: 2024-12-05 02:20:33.898150
+
+"""
+
+import sqlalchemy as sa
+from alembic import op
+from sqlalchemy.dialects.postgresql import ENUM
+
+# Revision identifiers, used by Alembic.
+revision = "d4104af84f2b"
+down_revision = "b69a33bbd135"
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+ # Remove the notification types added in the previous migration
+ op.execute("DELETE FROM notification_type;")
+
+ # Alter the `name` column in `notification_type` to be a VARCHAR
+ with op.batch_alter_table("notification_type") as batch_op:
+ batch_op.alter_column(
+ "name",
+ existing_type=ENUM(
+ "TRANSFER_PARTNER_UPDATE",
+ "TRANSFER_DIRECTOR_REVIEW",
+ "INITIATIVE_APPROVED",
+ "INITIATIVE_DA_REQUEST",
+ "SUPPLEMENTAL_REQUESTED",
+ "DIRECTOR_ASSESSMENT",
+ name="notification_type_enum_v2",
+ ),
+ type_=sa.String(length=255),
+ existing_nullable=False,
+ )
+
+ # Drop the old enum types
+ op.execute("DROP TYPE IF EXISTS notification_type_enum_v2;")
+
+
+def downgrade():
+ # Re-create the old enum type
+ notification_type_enum_v2 = ENUM(
+ "TRANSFER_PARTNER_UPDATE",
+ "TRANSFER_DIRECTOR_REVIEW",
+ "INITIATIVE_APPROVED",
+ "INITIATIVE_DA_REQUEST",
+ "SUPPLEMENTAL_REQUESTED",
+ "DIRECTOR_ASSESSMENT",
+ name="notification_type_enum_v2",
+ )
+ notification_type_enum_v2.create(op.get_bind(), checkfirst=False)
+
+ # Alter the `name` column back to the old enum
+ with op.batch_alter_table("notification_type") as batch_op:
+ batch_op.alter_column(
+ "name",
+ type_=notification_type_enum_v2,
+ existing_type=sa.String(length=255),
+ postgresql_using="name::text::notification_type_enum_v2",
+ existing_nullable=False,
+ )
+
+ # Re-insert the previous notification types
+ op.execute(
+ """
+ INSERT INTO notification_type (notification_type_id, name, description, email_content, create_user, update_user)
+ VALUES
+ (1, 'TRANSFER_PARTNER_UPDATE', 'Transfer partner update notification', 'Email content for transfer partner update', 'system', 'system'),
+ (2, 'TRANSFER_DIRECTOR_REVIEW', 'Director review notification', 'Email content for director review', 'system', 'system'),
+ (3, 'INITIATIVE_APPROVED', 'Initiative approved notification', 'Email content for initiative approval', 'system', 'system'),
+ (4, 'INITIATIVE_DA_REQUEST', 'DA request notification', 'Email content for DA request', 'system', 'system'),
+ (5, 'SUPPLEMENTAL_REQUESTED', 'Supplemental requested notification', 'Email content for supplemental request', 'system', 'system'),
+ (6, 'DIRECTOR_ASSESSMENT', 'Director assessment notification', 'Email content for director assessment', 'system', 'system');
+ """
+ )
+
+ # Reset the sequence for the id column
+ op.execute(
+ """
+ SELECT setval('notification_type_id_seq', (SELECT MAX(notification_type_id) FROM notification_type));
+ """
+ )
diff --git a/backend/lcfs/db/models/notification/NotificationChannel.py b/backend/lcfs/db/models/notification/NotificationChannel.py
index f27a61eb2..2d1103b51 100644
--- a/backend/lcfs/db/models/notification/NotificationChannel.py
+++ b/backend/lcfs/db/models/notification/NotificationChannel.py
@@ -5,8 +5,8 @@
class ChannelEnum(enum.Enum):
- EMAIL = "Email"
- IN_APP = "In-Application"
+ EMAIL = "EMAIL"
+ IN_APP = "IN_APP"
class NotificationChannel(BaseModel, Auditable):
diff --git a/backend/lcfs/db/models/notification/NotificationType.py b/backend/lcfs/db/models/notification/NotificationType.py
index 2311d834d..9c8f31914 100644
--- a/backend/lcfs/db/models/notification/NotificationType.py
+++ b/backend/lcfs/db/models/notification/NotificationType.py
@@ -1,28 +1,17 @@
-import enum
from lcfs.db.base import BaseModel, Auditable
-from sqlalchemy import Column, Integer, Enum, Text
+from sqlalchemy import Column, Integer, Text, String
from sqlalchemy.orm import relationship
-class NotificationTypeEnum(enum.Enum):
- TRANSFER_PARTNER_UPDATE = "Transfer partner proposed, declined, rescinded, or signed"
- TRANSFER_DIRECTOR_REVIEW = "Director recorded/refused"
- INITIATIVE_APPROVED = "Director approved"
- INITIATIVE_DA_REQUEST = "DA request"
- SUPPLEMENTAL_REQUESTED = "Supplemental requested"
- DIRECTOR_ASSESSMENT = "Director assessment"
-
-
class NotificationType(BaseModel, Auditable):
__tablename__ = "notification_type"
__table_args__ = {"comment": "Represents a Notification type"}
notification_type_id = Column(Integer, primary_key=True, autoincrement=True)
- name = Column(
- Enum(NotificationTypeEnum, name="notification_type_enum", create_type=True),
- nullable=False,
- )
+ name = Column(String(255), nullable=False)
description = Column(Text, nullable=True)
email_content = Column(Text, nullable=True)
- subscriptions = relationship("NotificationChannelSubscription", back_populates="notification_type")
+ subscriptions = relationship(
+ "NotificationChannelSubscription", back_populates="notification_type"
+ )
diff --git a/backend/lcfs/db/models/notification/__init__.py b/backend/lcfs/db/models/notification/__init__.py
index ebc71c209..e16003e7e 100644
--- a/backend/lcfs/db/models/notification/__init__.py
+++ b/backend/lcfs/db/models/notification/__init__.py
@@ -1,10 +1,12 @@
from .NotificationChannel import NotificationChannel
+from .NotificationChannel import ChannelEnum
from .NotificationChannelSubscription import NotificationChannelSubscription
from .NotificationMessage import NotificationMessage
from .NotificationType import NotificationType
__all__ = [
"NotificationChannel",
+ "ChannelEnum",
"NotificationChannelSubscription",
"NotificationMessage",
"NotificationType",
diff --git a/backend/lcfs/db/models/user/UserProfile.py b/backend/lcfs/db/models/user/UserProfile.py
index 7fecca2f8..681d19aa0 100644
--- a/backend/lcfs/db/models/user/UserProfile.py
+++ b/backend/lcfs/db/models/user/UserProfile.py
@@ -26,6 +26,9 @@ class UserProfile(BaseModel, Auditable):
String(150), unique=True, nullable=False, comment="keycloak Username"
)
email = Column(String(255), nullable=True, comment="Primary email address")
+ notifications_email = Column(
+ String(255), nullable=True, comment="Email address used for notifications"
+ )
title = Column(String(100), nullable=True, comment="Professional Title")
phone = Column(String(50), nullable=True, comment="Primary phone number")
mobile_phone = Column(String(50), nullable=True, comment="Mobile phone number")
diff --git a/backend/lcfs/db/seeders/common/notifications_seeder.py b/backend/lcfs/db/seeders/common/notifications_seeder.py
index e1bfe6fc8..363ae6c98 100644
--- a/backend/lcfs/db/seeders/common/notifications_seeder.py
+++ b/backend/lcfs/db/seeders/common/notifications_seeder.py
@@ -58,44 +58,121 @@ async def seed_notification_types(session):
"""
types_to_seed = [
{
- "name": "TRANSFER_PARTNER_UPDATE",
- "description": "Transfer partner update notification",
- "email_content": "Email content for transfer partner update",
+ "name": "BCEID__COMPLIANCE_REPORT__DIRECTOR_ASSESSMENT",
+ "description": "Director assessed a compliance report or supplemental report.",
+ "email_content": "Email content",
"create_user": "system",
"update_user": "system",
},
{
- "name": "TRANSFER_DIRECTOR_REVIEW",
- "description": "Director review notification",
- "email_content": "Email content for director review",
+ "name": "BCEID__INITIATIVE_AGREEMENT__DIRECTOR_APPROVAL",
+ "description": "Director approved the initiative agreement or transaction",
+ "email_content": "Email content",
"create_user": "system",
"update_user": "system",
},
{
- "name": "INITIATIVE_APPROVED",
- "description": "Initiative approved notification",
- "email_content": "Email content for initiative approval",
+ "name": "BCEID__TRANSFER__DIRECTOR_DECISION",
+ "description": "Director recorded or refused a transfer request",
+ "email_content": "Email content",
"create_user": "system",
"update_user": "system",
},
{
- "name": "INITIATIVE_DA_REQUEST",
- "description": "DA request notification",
- "email_content": "Email content for DA request",
+ "name": "BCEID__TRANSFER__PARTNER_ACTIONS",
+ "description": "A transfer partner took action (proposed, declined, rescinded, or signed & submitted) on a transfer request",
+ "email_content": "Email content",
"create_user": "system",
"update_user": "system",
},
{
- "name": "SUPPLEMENTAL_REQUESTED",
- "description": "Supplemental requested notification",
- "email_content": "Email content for supplemental request",
+ "name": "IDIR_ANALYST__COMPLIANCE_REPORT__DIRECTOR_DECISION",
+ "description": "Director assessed compliance report",
+ "email_content": "Email content",
"create_user": "system",
"update_user": "system",
},
{
- "name": "DIRECTOR_ASSESSMENT",
- "description": "Director assessment notification",
- "email_content": "Email content for director assessment",
+ "name": "IDIR_ANALYST__COMPLIANCE_REPORT__MANAGER_RECOMMENDATION",
+ "description": "Compliance manager recommended action on the compliance report.",
+ "email_content": "Email content",
+ "create_user": "system",
+ "update_user": "system",
+ },
+ {
+ "name": "IDIR_ANALYST__COMPLIANCE_REPORT__SUBMITTED_FOR_REVIEW",
+ "description": "Compliance report submitted for government analyst review or returned by compliance manager",
+ "email_content": "Email content",
+ "create_user": "system",
+ "update_user": "system",
+ },
+ {
+ "name": "IDIR_ANALYST__INITIATIVE_AGREEMENT__RETURNED_TO_ANALYST",
+ "description": "Director approved/returned the initiative agreement to the analyst",
+ "email_content": "Email content",
+ "create_user": "system",
+ "update_user": "system",
+ },
+ {
+ "name": "IDIR_ANALYST__TRANSFER__DIRECTOR_RECORDED",
+ "description": "Director recorded or refused a transfer request",
+ "email_content": "Email content",
+ "create_user": "system",
+ "update_user": "system",
+ },
+ {
+ "name": "IDIR_ANALYST__TRANSFER__RESCINDED_ACTION",
+ "description": "A transfer request was rescinded by a transfer partner",
+ "email_content": "Email content",
+ "create_user": "system",
+ "update_user": "system",
+ },
+ {
+ "name": "IDIR_ANALYST__TRANSFER__SUBMITTED_FOR_REVIEW",
+ "description": "Transfer request submitted for government analyst review",
+ "email_content": "Email content",
+ "create_user": "system",
+ "update_user": "system",
+ },
+ {
+ "name": "IDIR_COMPLIANCE_MANAGER__COMPLIANCE_REPORT__ANALYST_RECOMMENDATION",
+ "description": "Analyst recommendation on the compliance report or returned by the director",
+ "email_content": "Email content",
+ "create_user": "system",
+ "update_user": "system",
+ },
+ {
+ "name": "IDIR_COMPLIANCE_MANAGER__COMPLIANCE_REPORT__DIRECTOR_ASSESSMENT",
+ "description": "Director assessed a compliance report",
+ "email_content": "Email content",
+ "create_user": "system",
+ "update_user": "system",
+ },
+ {
+ "name": "IDIR_COMPLIANCE_MANAGER__COMPLIANCE_REPORT__SUBMITTED_FOR_REVIEW",
+ "description": "Compliance report submitted for government analyst review",
+ "email_content": "Email content",
+ "create_user": "system",
+ "update_user": "system",
+ },
+ {
+ "name": "IDIR_DIRECTOR__COMPLIANCE_REPORT__MANAGER_RECOMMENDATION",
+ "description": "Compliance manager recommended action on the compliance report",
+ "email_content": "Email content",
+ "create_user": "system",
+ "update_user": "system",
+ },
+ {
+ "name": "IDIR_DIRECTOR__INITIATIVE_AGREEMENT__ANALYST_RECOMMENDATION",
+ "description": "Analyst recommendation provided for the initiative agreement",
+ "email_content": "Email content",
+ "create_user": "system",
+ "update_user": "system",
+ },
+ {
+ "name": "IDIR_DIRECTOR__TRANSFER__ANALYST_RECOMMENDATION",
+ "description": "Analyst recommendation provided for the transfer request",
+ "email_content": "Email content",
"create_user": "system",
"update_user": "system",
},
diff --git a/backend/lcfs/tests/notification/test_notification_repo.py b/backend/lcfs/tests/notification/test_notification_repo.py
index f3dbdcd12..20eb31169 100644
--- a/backend/lcfs/tests/notification/test_notification_repo.py
+++ b/backend/lcfs/tests/notification/test_notification_repo.py
@@ -1,11 +1,15 @@
import pytest
from sqlalchemy.ext.asyncio import AsyncSession
-from sqlalchemy import delete
+from sqlalchemy import delete
from lcfs.web.api.notification.repo import NotificationRepository
-from lcfs.db.models.notification import NotificationMessage, NotificationChannelSubscription
+from lcfs.db.models.notification import (
+ NotificationMessage,
+ NotificationChannelSubscription,
+)
from lcfs.web.exception.exceptions import DataNotFoundException
from unittest.mock import AsyncMock, MagicMock
+
@pytest.fixture
def mock_db_session():
session = AsyncMock(spec=AsyncSession)
@@ -37,7 +41,9 @@ def notification_repo(mock_db_session):
async def test_create_notification_message(notification_repo, mock_db_session):
new_notification_message = MagicMock(spec=NotificationMessage)
- result = await notification_repo.create_notification_message(new_notification_message)
+ result = await notification_repo.create_notification_message(
+ new_notification_message
+ )
assert result == new_notification_message
mock_db_session.add.assert_called_once_with(new_notification_message)
@@ -66,9 +72,10 @@ async def mock_execute(*args, **kwargs):
mock_result_chain.scalar_one_or_none.assert_called_once()
-
@pytest.mark.anyio
-async def test_get_notification_message_by_id_not_found(notification_repo, mock_db_session):
+async def test_get_notification_message_by_id_not_found(
+ notification_repo, mock_db_session
+):
mock_result_chain = MagicMock()
mock_result_chain.scalars = MagicMock(return_value=mock_result_chain)
mock_result_chain.scalar_one_or_none.side_effect = DataNotFoundException
@@ -97,7 +104,10 @@ async def test_get_notification_messages_by_user(notification_repo, mock_db_sess
mock_notification2.message = "Test message 2"
mock_result_chain = MagicMock()
- mock_result_chain.scalars.return_value.all.return_value = [mock_notification1, mock_notification2]
+ mock_result_chain.scalars.return_value.all.return_value = [
+ mock_notification1,
+ mock_notification2,
+ ]
async def mock_execute(*args, **kwargs):
return mock_result_chain
@@ -113,7 +123,9 @@ async def mock_execute(*args, **kwargs):
@pytest.mark.anyio
-async def test_get_unread_notification_message_count_by_user_id(notification_repo, mock_db_session):
+async def test_get_unread_notification_message_count_by_user_id(
+ notification_repo, mock_db_session
+):
user_id = 1
expected_unread_count = 5
@@ -122,7 +134,9 @@ async def test_get_unread_notification_message_count_by_user_id(notification_rep
mock_db_session.execute = AsyncMock(return_value=mock_result)
- result = await notification_repo.get_unread_notification_message_count_by_user_id(user_id)
+ result = await notification_repo.get_unread_notification_message_count_by_user_id(
+ user_id
+ )
assert result == expected_unread_count
mock_db_session.execute.assert_called_once()
@@ -148,7 +162,6 @@ async def test_delete_notification_message(notification_repo, mock_db_session):
mock_db_session.flush.assert_called_once()
-
@pytest.mark.anyio
async def test_update_notification_message(notification_repo, mock_db_session):
mock_notification = MagicMock(spec=NotificationMessage)
@@ -197,14 +210,20 @@ async def mock_execute(*args, **kwargs):
assert result.is_read is True # Verify that is_read was set to True
mock_db_session.commit.assert_called_once() # Ensure commit was called
- mock_db_session.refresh.assert_called_once_with(mock_notification) # Check refresh was called
+ mock_db_session.refresh.assert_called_once_with(
+ mock_notification
+ ) # Check refresh was called
@pytest.mark.anyio
-async def test_create_notification_channel_subscription(notification_repo, mock_db_session):
+async def test_create_notification_channel_subscription(
+ notification_repo, mock_db_session
+):
new_subscription = MagicMock(spec=NotificationChannelSubscription)
- result = await notification_repo.create_notification_channel_subscription(new_subscription)
+ result = await notification_repo.create_notification_channel_subscription(
+ new_subscription
+ )
assert result == new_subscription
mock_db_session.add.assert_called_once_with(new_subscription)
@@ -213,16 +232,18 @@ async def test_create_notification_channel_subscription(notification_repo, mock_
@pytest.mark.anyio
-async def test_get_notification_channel_subscriptions_by_user(notification_repo, mock_db_session):
+async def test_get_notification_channel_subscriptions_by_user(
+ notification_repo, mock_db_session
+):
mock_subscription = MagicMock(spec=NotificationChannelSubscription)
mock_subscription.user_profile_id = 1
mock_subscription.notification_channel_subscription_id = 1
mock_subscription.notification_type_id = 1
mock_subscription.notification_channel_id = 1
-
+
mock_result_chain = MagicMock()
mock_result_chain.scalars.return_value.all.return_value = [mock_subscription]
-
+
async def mock_execute(*args, **kwargs):
return mock_result_chain
@@ -235,54 +256,24 @@ async def mock_execute(*args, **kwargs):
@pytest.mark.anyio
-async def test_get_notification_channel_subscriptions_by_id(notification_repo, mock_db_session):
+async def test_get_notification_channel_subscriptions_by_id(
+ notification_repo, mock_db_session
+):
subscription_id = 1
mock_subscription = MagicMock(spec=NotificationChannelSubscription)
mock_subscription.notification_channel_subscription_id = subscription_id
-
mock_result_chain = MagicMock()
- mock_result_chain.scalar_one.return_value = mock_subscription
-
+ mock_result_chain.scalar_one.return_value = mock_subscription
+
async def mock_execute(*args, **kwargs):
return mock_result_chain
mock_db_session.execute = mock_execute
- result = await notification_repo.get_notification_channel_subscription_by_id(subscription_id)
+ result = await notification_repo.get_notification_channel_subscription_by_id(
+ subscription_id
+ )
assert result is not None
assert result.notification_channel_subscription_id == subscription_id
-
-
-@pytest.mark.anyio
-async def test_update_notification_channel_subscription(notification_repo, mock_db_session):
- mock_subscription = MagicMock(spec=NotificationChannelSubscription)
- mock_subscription.notification_channel_subscription_id = 1
- mock_subscription.is_enabled = True
- mock_subscription.user_profile_id = 1
- mock_subscription.notification_type_id= 1
- mock_subscription.notification_channel_id = 1
-
- updated_subscription = NotificationChannelSubscription(
- notification_channel_subscription_id=1,
- is_enabled=False,
- user_profile_id=1,
- notification_type_id=2,
- notification_channel_id=1
- )
-
- mock_db_session.merge.return_value = updated_subscription
- mock_db_session.flush = AsyncMock()
-
- notification_repo.db = mock_db_session
-
- result = await notification_repo.update_notification_channel_subscription(mock_subscription)
-
- mock_db_session.merge.assert_called_once_with(mock_subscription)
- mock_db_session.flush.assert_called_once()
- assert result == updated_subscription
- assert result.is_enabled is False
- assert result.notification_type_id == 2
-
-
diff --git a/backend/lcfs/tests/notification/test_notification_services.py b/backend/lcfs/tests/notification/test_notification_services.py
index 2d2cc6ef7..a23b49388 100644
--- a/backend/lcfs/tests/notification/test_notification_services.py
+++ b/backend/lcfs/tests/notification/test_notification_services.py
@@ -272,49 +272,41 @@ async def test_create_notification_channel_subscription(notification_service):
subscription_data = SubscriptionSchema(
is_enabled=True,
- user_profile_id=1,
- notification_type_id=2,
- notification_channel_id=3,
+ notification_channel_name="EMAIL",
+ notification_type_name="BCEID__COMPLIANCE_REPORT__DIRECTOR_ASSESSMENT",
)
+ user_profile_id = 1
+
+ # Mock the methods that fetch IDs from keys
+ service.get_notification_channel_id_by_key = AsyncMock(return_value=3)
+ service.get_notification_type_id_by_key = AsyncMock(return_value=2)
+ # Mock the repo method
created_subscription = NotificationChannelSubscription(
notification_channel_subscription_id=123,
is_enabled=True,
- user_profile_id=1,
+ user_profile_id=user_profile_id,
notification_type_id=2,
notification_channel_id=3,
)
-
mock_repo.create_notification_channel_subscription = AsyncMock(
return_value=created_subscription
)
- result = await service.create_notification_channel_subscription(subscription_data)
+ result = await service.create_notification_channel_subscription(
+ subscription_data, user_profile_id
+ )
+
+ # Assertions
+ assert isinstance(result, SubscriptionSchema)
+ assert result.notification_channel_subscription_id == 123
+ assert result.is_enabled == True
+ # Verify that the repo method was called with the correct subscription
called_args, _ = mock_repo.create_notification_channel_subscription.await_args
passed_subscription = called_args[0]
-
- assert (
- result.notification_channel_subscription_id
- == created_subscription.notification_channel_subscription_id
- )
- assert result.is_enabled == created_subscription.is_enabled
- assert result.user_profile_id == created_subscription.user_profile_id
- assert result.notification_type_id == created_subscription.notification_type_id
- assert (
- result.notification_channel_id == created_subscription.notification_channel_id
- )
-
- assert passed_subscription.is_enabled == subscription_data.is_enabled
- assert passed_subscription.user_profile_id == subscription_data.user_profile_id
- assert (
- passed_subscription.notification_type_id
- == subscription_data.notification_type_id
- )
- assert (
- passed_subscription.notification_channel_id
- == subscription_data.notification_channel_id
- )
+ assert passed_subscription.is_enabled == True
+ assert passed_subscription.user_profile_id == user_profile_id
@pytest.mark.anyio
@@ -322,23 +314,33 @@ async def test_get_notification_channel_subscriptions_by_user_id(notification_se
service, mock_repo = notification_service
user_id = 1
- expected_subscriptions = [
- NotificationChannelSubscription(
- notification_channel_subscription_id=123,
- is_enabled=True,
- user_profile_id=user_id,
- notification_type_id=2,
- notification_channel_id=3,
- )
- ]
+ # Mock subscription data
+ mock_subscription = MagicMock(spec=NotificationChannelSubscription)
+ mock_subscription.notification_channel_subscription_id = 123
+ mock_subscription.user_profile_id = user_id
+ mock_subscription.is_enabled = True
+
+ # Mock associated channel and type
+ mock_channel = MagicMock()
+ mock_channel.channel_name.name = "email"
+ mock_subscription.notification_channel = mock_channel
+
+ mock_type = MagicMock()
+ mock_type.name = "new_message"
+ mock_subscription.notification_type = mock_type
mock_repo.get_notification_channel_subscriptions_by_user = AsyncMock(
- return_value=expected_subscriptions
+ return_value=[mock_subscription]
)
result = await service.get_notification_channel_subscriptions_by_user_id(user_id)
- assert result == expected_subscriptions
+ assert len(result) == 1
+ subscription = result[0]
+ assert subscription["notification_channel_subscription_id"] == 123
+ assert subscription["notification_channel_name"] == "email"
+ assert subscription["notification_type_name"] == "new_message"
+
mock_repo.get_notification_channel_subscriptions_by_user.assert_awaited_once_with(
user_id
)
@@ -369,63 +371,16 @@ async def test_get_notification_channel_subscription_by_id(notification_service)
)
-@pytest.mark.anyio
-async def test_update_notification_channel_subscription(notification_service):
- service, mock_repo = notification_service
-
- subscription_data = SubscriptionSchema(
- notification_channel_subscription_id=123,
- is_enabled=False,
- user_profile_id=1,
- notification_type_id=2,
- notification_channel_id=3,
- )
-
- updated_subscription = NotificationChannelSubscription(
- notification_channel_subscription_id=123,
- is_enabled=False,
- user_profile_id=1,
- notification_type_id=2,
- notification_channel_id=3,
- )
-
- mock_repo.update_notification_channel_subscription = AsyncMock(
- return_value=updated_subscription
- )
-
- result = await service.update_notification_channel_subscription(subscription_data)
-
- called_args, _ = mock_repo.update_notification_channel_subscription.await_args
- passed_subscription = called_args[0]
- assert (
- passed_subscription.notification_channel_subscription_id
- == updated_subscription.notification_channel_subscription_id
- )
- assert passed_subscription.is_enabled == subscription_data.is_enabled
- assert passed_subscription.user_profile_id == subscription_data.user_profile_id
- assert (
- passed_subscription.notification_type_id
- == subscription_data.notification_type_id
- )
- assert (
- passed_subscription.notification_channel_id
- == subscription_data.notification_channel_id
- )
-
-
@pytest.mark.anyio
async def test_delete_notification_channel_subscription(notification_service):
service, mock_repo = notification_service
- user_id = 1
+ user_profile_id = 1
subscription_id = 456
mock_subscription_data = NotificationChannelSubscription(
notification_channel_subscription_id=subscription_id,
- is_enabled=True,
- user_profile_id=user_id,
- notification_type_id=2,
- notification_channel_id=3,
+ user_profile_id=user_profile_id,
)
mock_repo.get_notification_channel_subscription_by_id = AsyncMock(
@@ -433,7 +388,9 @@ async def test_delete_notification_channel_subscription(notification_service):
)
mock_repo.delete_notification_channel_subscription = AsyncMock()
- await service.delete_notification_channel_subscription(subscription_id)
+ await service.delete_notification_channel_subscription(
+ subscription_id, user_profile_id
+ )
mock_repo.get_notification_channel_subscription_by_id.assert_awaited_once_with(
subscription_id
diff --git a/backend/lcfs/tests/notification/test_notification_views.py b/backend/lcfs/tests/notification/test_notification_views.py
index 9ce72fa86..7a7e2fa93 100644
--- a/backend/lcfs/tests/notification/test_notification_views.py
+++ b/backend/lcfs/tests/notification/test_notification_views.py
@@ -172,11 +172,9 @@ async def test_get_notification_channel_subscription_by_id(
mock_subscription = SubscriptionSchema(
notification_channel_subscription_id=1,
user_profile_id=1, # Match mock user
- channel_name="Test Channel",
- is_active=True,
- notification_type_id=1,
- created_at="2023-01-01T00:00:00",
- updated_at="2023-01-01T00:00:00",
+ notification_channel_name="EMAIL",
+ notification_type_name="BCEID__COMPLIANCE_REPORT__DIRECTOR_ASSESSMENT",
+ is_enabled=True,
)
mock_get_subscription.return_value = mock_subscription
@@ -200,7 +198,6 @@ async def test_get_notification_channel_subscription_by_id(
)
-
@pytest.mark.anyio
async def test_create_subscription(client, fastapi_app, set_mock_user):
with patch(
@@ -233,16 +230,19 @@ async def test_delete_subscription(client, fastapi_app, set_mock_user):
"lcfs.web.api.notification.views.NotificationService.delete_notification_channel_subscription",
return_value=None,
) as mock_delete_subscription:
- set_mock_user(fastapi_app, [RoleEnum.GOVERNMENT])
-
- mock_delete_subscription.return_value = {
- "message": "Notification Subscription deleted successfully"
- }
-
- subscription_data = {"notification_channel_subscription_id": 1, "deleted": True}
+ set_mock_user(
+ fastapi_app,
+ [RoleEnum.GOVERNMENT],
+ user_details={"user_profile_id": 1},
+ )
url = fastapi_app.url_path_for("save_subscription")
+ subscription_data = {
+ "notification_channel_subscription_id": 1,
+ "deleted": True,
+ }
+
response = await client.post(url, json=subscription_data)
assert response.status_code == 200
@@ -250,32 +250,6 @@ async def test_delete_subscription(client, fastapi_app, set_mock_user):
response.json()["message"]
== "Notification Subscription deleted successfully"
)
- mock_delete_subscription.assert_called_once_with(1)
-
-
-@pytest.mark.anyio
-async def test_update_subscription(client, fastapi_app, set_mock_user):
- with patch(
- "lcfs.web.api.notification.views.NotificationService.update_notification_channel_subscription"
- ) as mock_update_subscription:
- updated_subscription_data = {
- "notification_channel_subscription_id": 1,
- "is_enabled": False,
- "notification_channel_id": 1,
- "user_profile_id": 1,
- "notification_type_id": 1,
- }
-
- mock_update_subscription.return_value = updated_subscription_data
-
- set_mock_user(fastapi_app, [RoleEnum.GOVERNMENT])
-
- url = fastapi_app.url_path_for("save_subscription")
-
- response = await client.post(url, json=updated_subscription_data)
-
- assert response.status_code == 200
- assert response.json()["isEnabled"] is False
- mock_update_subscription.assert_called_once_with(
- SubscriptionSchema(**updated_subscription_data)
- )
+ mock_delete_subscription.assert_called_once_with(
+ 1, 1
+ ) # subscription_id, user_profile_id
diff --git a/backend/lcfs/tests/user/test_user_repo.py b/backend/lcfs/tests/user/test_user_repo.py
index 19468ebc3..56cb75403 100644
--- a/backend/lcfs/tests/user/test_user_repo.py
+++ b/backend/lcfs/tests/user/test_user_repo.py
@@ -5,6 +5,7 @@
from lcfs.db.models import UserProfile, UserLoginHistory
from lcfs.web.api.user.repo import UserRepository
from lcfs.tests.user.user_payloads import user_orm_model
+from lcfs.web.exception.exceptions import DataNotFoundException
@pytest.fixture
@@ -46,3 +47,33 @@ async def test_create_login_history(dbsession, user_repo):
assert added_object.external_username == user.keycloak_username
assert added_object.keycloak_user_id == user.keycloak_user_id
assert added_object.is_login_successful is True
+
+
+@pytest.mark.anyio
+async def test_update_notifications_email_success(dbsession, user_repo):
+ # Arrange: Create a user in the database
+ user = UserProfile(
+ keycloak_user_id="user_id_1",
+ keycloak_email="user1@domain.com",
+ keycloak_username="username1",
+ email="user1@domain.com",
+ notifications_email=None,
+ title="Developer",
+ phone="1234567890",
+ mobile_phone="0987654321",
+ first_name="John",
+ last_name="Doe",
+ is_active=True,
+ organization_id=1,
+ )
+ dbsession.add(user)
+ await dbsession.commit()
+ await dbsession.refresh(user)
+
+ # Act: Update the notifications email
+ updated_user = await user_repo.update_notifications_email(
+ user_profile_id=1, email="new_email@domain.com"
+ )
+
+ # Assert: Check if the notifications email was updated
+ assert updated_user.notifications_email == "new_email@domain.com"
diff --git a/backend/lcfs/tests/user/test_user_views.py b/backend/lcfs/tests/user/test_user_views.py
index 2d4f1bafa..75a228c0c 100644
--- a/backend/lcfs/tests/user/test_user_views.py
+++ b/backend/lcfs/tests/user/test_user_views.py
@@ -22,6 +22,7 @@
UserActivitiesResponseSchema,
UserLoginHistoryResponseSchema,
)
+from lcfs.web.exception.exceptions import DataNotFoundException
@pytest.mark.anyio
@@ -464,3 +465,23 @@ async def test_track_logged_in_success(client: AsyncClient, fastapi_app, set_moc
# Extract the first argument of the first call
user_profile = mock_track_user_login.call_args[0][0]
assert isinstance(user_profile, UserProfile)
+
+
+@pytest.mark.anyio
+async def test_update_notifications_email_success(
+ client: AsyncClient,
+ fastapi_app,
+ set_mock_user,
+ add_models,
+):
+ set_mock_user(fastapi_app, [RoleEnum.GOVERNMENT])
+
+ # Prepare request data
+ request_data = {"notifications_email": "new_email@domain.com"}
+
+ # Act: Send POST request to the endpoint
+ url = fastapi_app.url_path_for("update_notifications_email")
+ response = await client.post(url, json=request_data)
+
+ # Assert: Check response status and content
+ assert response.status_code == status.HTTP_200_OK
diff --git a/backend/lcfs/tests/user/user_payloads.py b/backend/lcfs/tests/user/user_payloads.py
index 6361299a0..3491cfd5e 100644
--- a/backend/lcfs/tests/user/user_payloads.py
+++ b/backend/lcfs/tests/user/user_payloads.py
@@ -5,9 +5,11 @@
keycloak_user_id="user_id",
keycloak_email="email@domain.com",
keycloak_username="username",
+ email="email@domain.com",
+ notifications_email=None,
title="Developer",
phone="1234567890",
- mobile_phone="1234567890",
+ mobile_phone="0987654321",
first_name="John",
last_name="Smith",
is_active=True,
diff --git a/backend/lcfs/web/api/email/repo.py b/backend/lcfs/web/api/email/repo.py
index d05e6863d..7d8c6cd6e 100644
--- a/backend/lcfs/web/api/email/repo.py
+++ b/backend/lcfs/web/api/email/repo.py
@@ -4,7 +4,6 @@
ChannelEnum,
NotificationChannel,
)
-from lcfs.db.models.notification.NotificationType import NotificationTypeEnum
from lcfs.web.core.decorators import repo_handler
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, or_
@@ -32,7 +31,7 @@ async def get_subscribed_user_emails(
.join(NotificationChannelSubscription.notification_channel)
.filter(
NotificationChannelSubscription.notification_type.has(
- name=NotificationTypeEnum[notification_type.upper()]
+ name=notification_type
),
NotificationChannelSubscription.is_enabled == True,
NotificationChannel.channel_name
@@ -59,9 +58,7 @@ async def get_notification_template(self, notification_type: str) -> str:
NotificationChannelSubscription.notification_type_id
== NotificationType.notification_type_id,
)
- .filter(
- NotificationType.name == NotificationTypeEnum[notification_type.upper()]
- )
+ .filter(NotificationType.name == notification_type)
.limit(1) # Fetch only one record
)
diff --git a/backend/lcfs/web/api/email/services.py b/backend/lcfs/web/api/email/services.py
index f2ab678e1..856874d92 100644
--- a/backend/lcfs/web/api/email/services.py
+++ b/backend/lcfs/web/api/email/services.py
@@ -1,5 +1,4 @@
import os
-from lcfs.db.models.notification.NotificationType import NotificationTypeEnum
import requests
import structlog
from fastapi import Depends
@@ -88,7 +87,7 @@ def _render_email_template(
"""
# Fetch template file path from the imported mapping
template_file = TEMPLATE_MAPPING.get(
- NotificationTypeEnum[template_name], TEMPLATE_MAPPING["default"]
+ template_name, TEMPLATE_MAPPING["default"]
)
# Render the template
diff --git a/backend/lcfs/web/api/email/template_mapping.py b/backend/lcfs/web/api/email/template_mapping.py
index 41f041d18..d31291666 100644
--- a/backend/lcfs/web/api/email/template_mapping.py
+++ b/backend/lcfs/web/api/email/template_mapping.py
@@ -1,12 +1,20 @@
-from lcfs.db.models.notification.NotificationType import NotificationTypeEnum
-
-
TEMPLATE_MAPPING = {
- NotificationTypeEnum.TRANSFER_DIRECTOR_REVIEW: "transfer_director_review.html",
- NotificationTypeEnum.INITIATIVE_APPROVED: "initiative_approved.html",
- NotificationTypeEnum.INITIATIVE_DA_REQUEST: "initiative_da_request.html",
- NotificationTypeEnum.SUPPLEMENTAL_REQUESTED: "supplemental_requested.html",
- NotificationTypeEnum.DIRECTOR_ASSESSMENT: "director_assessment.html",
- NotificationTypeEnum.TRANSFER_PARTNER_UPDATE: "transfer_partner_update.html",
- "default": "default.html"
+ "BCEID__COMPLIANCE_REPORT__DIRECTOR_ASSESSMENT": "bceid__compliance_report__director_assessment.html",
+ "BCEID__INITIATIVE_AGREEMENT__DIRECTOR_APPROVAL": "bceid__initiative_agreement__director_approval.html",
+ "BCEID__TRANSFER__DIRECTOR_DECISION": "bceid__transfer__director_decision.html",
+ "BCEID__TRANSFER__PARTNER_ACTIONS": "bceid__transfer__partner_actions.html",
+ "IDIR_ANALYST__COMPLIANCE_REPORT__DIRECTOR_DECISION": "idir_analyst__compliance_report__director_decision.html",
+ "IDIR_ANALYST__COMPLIANCE_REPORT__MANAGER_RECOMMENDATION": "idir_analyst__compliance_report__manager_recommendation.html",
+ "IDIR_ANALYST__COMPLIANCE_REPORT__SUBMITTED_FOR_REVIEW": "idir_analyst__compliance_report__submitted_for_review.html",
+ "IDIR_ANALYST__INITIATIVE_AGREEMENT__RETURNED_TO_ANALYST": "idir_analyst__initiative_agreement__returned_to_analyst.html",
+ "IDIR_ANALYST__TRANSFER__DIRECTOR_RECORDED": "idir_analyst__transfer__director_recorded.html",
+ "IDIR_ANALYST__TRANSFER__RESCINDED_ACTION": "idir_analyst__transfer__rescinded_action.html",
+ "IDIR_ANALYST__TRANSFER__SUBMITTED_FOR_REVIEW": "idir_analyst__transfer__submitted_for_review.html",
+ "IDIR_COMPLIANCE_MANAGER__COMPLIANCE_REPORT__ANALYST_RECOMMENDATION": "idir_compliance_manager__compliance_report__analyst_recommendation.html",
+ "IDIR_COMPLIANCE_MANAGER__COMPLIANCE_REPORT__DIRECTOR_ASSESSMENT": "idir_compliance_manager__compliance_report__director_assessment.html",
+ "IDIR_COMPLIANCE_MANAGER__COMPLIANCE_REPORT__SUBMITTED_FOR_REVIEW": "idir_compliance_manager__compliance_report__submitted_for_review.html",
+ "IDIR_DIRECTOR__COMPLIANCE_REPORT__MANAGER_RECOMMENDATION": "idir_director__compliance_report__manager_recommendation.html",
+ "IDIR_DIRECTOR__INITIATIVE_AGREEMENT__ANALYST_RECOMMENDATION": "idir_director__initiative_agreement__analyst_recommendation.html",
+ "IDIR_DIRECTOR__TRANSFER__ANALYST_RECOMMENDATION": "idir_director__transfer__analyst_recommendation.html",
+ "default": "default.html",
}
diff --git a/backend/lcfs/web/api/notification/repo.py b/backend/lcfs/web/api/notification/repo.py
index 12788666e..c12689ee4 100644
--- a/backend/lcfs/web/api/notification/repo.py
+++ b/backend/lcfs/web/api/notification/repo.py
@@ -1,20 +1,20 @@
from lcfs.db.models.notification import (
NotificationChannelSubscription,
NotificationMessage,
+ NotificationChannel,
+ NotificationType,
+ ChannelEnum,
)
-from lcfs.web.api.notification.schema import NotificationMessageSchema
import structlog
-from datetime import date
-from typing import List, Dict, Any, Optional, Union
+
+from typing import List, Optional
from fastapi import Depends
from lcfs.db.dependencies import get_async_db_session
from lcfs.web.exception.exceptions import DataNotFoundException
-from sqlalchemy import and_, delete, or_, select, func, text, update, distinct
+from sqlalchemy import delete, select, func
from sqlalchemy.ext.asyncio import AsyncSession
-from sqlalchemy.orm import joinedload, contains_eager, selectinload
-from sqlalchemy.exc import NoResultFound
-from fastapi import HTTPException
+from sqlalchemy.orm import selectinload
from lcfs.web.core.decorators import repo_handler
@@ -162,12 +162,17 @@ async def create_notification_channel_subscription(
@repo_handler
async def get_notification_channel_subscriptions_by_user(
self, user_profile_id: int
- ) -> Optional[NotificationChannelSubscription]:
+ ) -> List[NotificationChannelSubscription]:
"""
- Retrieve channel subscriptions for a user
+ Retrieve channel subscriptions for a user, including channel name and notification type name.
"""
- query = select(NotificationChannelSubscription).where(
- NotificationChannelSubscription.user_profile_id == user_profile_id
+ query = (
+ select(NotificationChannelSubscription)
+ .options(
+ selectinload(NotificationChannelSubscription.notification_channel),
+ selectinload(NotificationChannelSubscription.notification_type),
+ )
+ .where(NotificationChannelSubscription.user_profile_id == user_profile_id)
)
result = await self.db.execute(query)
subscriptions = result.scalars().all()
@@ -202,18 +207,6 @@ async def get_notification_channel_subscription_by_id(
return subscription
- @repo_handler
- async def update_notification_channel_subscription(
- self, notification_channel_subscription: NotificationChannelSubscription
- ) -> NotificationChannelSubscription:
- """
- Update a notification chanel subscription
- """
- merged_subscription = await self.db.merge(notification_channel_subscription)
- await self.db.flush()
-
- return merged_subscription
-
@repo_handler
async def delete_notification_channel_subscription(
self, notification_channel_subscription_id: int
@@ -227,3 +220,28 @@ async def delete_notification_channel_subscription(
)
await self.db.execute(query)
await self.db.flush()
+
+ @repo_handler
+ async def get_notification_type_by_name(self, name: str) -> Optional[int]:
+ """
+ Retrieve a NotificationType by its name
+ """
+ query = select(NotificationType.notification_type_id).where(
+ NotificationType.name == name
+ )
+ result = await self.db.execute(query)
+ x = result.scalars().first()
+ return x
+
+ @repo_handler
+ async def get_notification_channel_by_name(
+ self, name: ChannelEnum
+ ) -> Optional[int]:
+ """
+ Retrieve a NotificationChannel by its name
+ """
+ query = select(NotificationChannel.notification_channel_id).where(
+ NotificationChannel.channel_name == name.value
+ )
+ result = await self.db.execute(query)
+ return result.scalars().first()
diff --git a/backend/lcfs/web/api/notification/schema.py b/backend/lcfs/web/api/notification/schema.py
index e6a17504c..419f212c5 100644
--- a/backend/lcfs/web/api/notification/schema.py
+++ b/backend/lcfs/web/api/notification/schema.py
@@ -16,9 +16,11 @@ class NotificationMessageSchema(BaseSchema):
notification_type_id: Optional[int] = None
deleted: Optional[bool] = None
+
class NotificationCountSchema(BaseSchema):
count: int
+
class DeleteNotificationMessageSchema(BaseSchema):
notification_message_id: int
deleted: bool
@@ -31,9 +33,9 @@ class DeleteNotificationMessageResponseSchema(BaseSchema):
class SubscriptionSchema(BaseSchema):
notification_channel_subscription_id: Optional[int] = None
is_enabled: Optional[bool] = True
- notification_channel_id: Optional[int] = None
- user_profile_id: Optional[int]= None
- notification_type_id: Optional[int] = None
+ notification_channel_name: Optional[str] = None
+ user_profile_id: Optional[int] = None
+ notification_type_name: Optional[str] = None
deleted: Optional[bool] = None
diff --git a/backend/lcfs/web/api/notification/services.py b/backend/lcfs/web/api/notification/services.py
index 5340df504..f9bc27951 100644
--- a/backend/lcfs/web/api/notification/services.py
+++ b/backend/lcfs/web/api/notification/services.py
@@ -2,6 +2,7 @@
from lcfs.db.models.notification import (
NotificationChannelSubscription,
NotificationMessage,
+ ChannelEnum,
)
from lcfs.web.api.notification.schema import (
SubscriptionSchema,
@@ -9,7 +10,6 @@
)
from lcfs.web.exception.exceptions import DataNotFoundException
import structlog
-import math
from fastapi import Depends
from lcfs.web.api.notification.repo import NotificationRepository
from lcfs.web.core.decorators import service_handler
@@ -106,21 +106,42 @@ async def delete_notification_message(self, notification_id: int):
else:
raise DataNotFoundException(f"Notification with ID {notification_id}.")
+ @service_handler
+ async def get_notification_type_id_by_name(self, name: str) -> int:
+ notification_type = await self.repo.get_notification_type_by_name(name)
+ if not notification_type:
+ raise ValueError(f"Invalid notification type name: {name}")
+ return notification_type
+
+ @service_handler
+ async def get_notification_channel_id_by_name(self, name: ChannelEnum) -> int:
+ notification_channel = await self.repo.get_notification_channel_by_name(name)
+ if not notification_channel:
+ raise ValueError(f"Invalid notification channel name: {name}")
+ return notification_channel
+
@service_handler
async def create_notification_channel_subscription(
- self, subscription_data: SubscriptionSchema
+ self, subscription_data: SubscriptionSchema, user_profile_id: int
):
- """
- Create a new notification channel subscription.
- """
+ channel_enum_name = ChannelEnum(subscription_data.notification_channel_name)
+ notification_channel_id = await self.get_notification_channel_id_by_name(
+ channel_enum_name
+ )
+ notification_type_id = await self.get_notification_type_id_by_name(
+ subscription_data.notification_type_name
+ )
+
subscription = NotificationChannelSubscription(
- **subscription_data.model_dump(exclude={"deleted"})
+ user_profile_id=user_profile_id,
+ notification_channel_id=notification_channel_id,
+ notification_type_id=notification_type_id,
+ is_enabled=subscription_data.is_enabled,
)
created_subscription = await self.repo.create_notification_channel_subscription(
subscription
)
-
- # Convert the SQLAlchemy model instance to the Pydantic model
+ x = 1
return SubscriptionSchema.model_validate(created_subscription)
@service_handler
@@ -128,7 +149,20 @@ async def get_notification_channel_subscriptions_by_user_id(self, user_id: int):
"""
Retrieve all notification channel subscriptions for a user.
"""
- return await self.repo.get_notification_channel_subscriptions_by_user(user_id)
+ subscriptions = await self.repo.get_notification_channel_subscriptions_by_user(
+ user_id
+ )
+
+ subscriptions_with_names = [
+ {
+ "notification_channel_subscription_id": subscription.notification_channel_subscription_id,
+ "notification_channel_name": subscription.notification_channel.channel_name.name,
+ "notification_type_name": subscription.notification_type.name,
+ }
+ for subscription in subscriptions
+ ]
+
+ return subscriptions_with_names
@service_handler
async def get_notification_channel_subscription_by_id(
@@ -141,36 +175,17 @@ async def get_notification_channel_subscription_by_id(
notification_channel_subscription_id
)
- @service_handler
- async def update_notification_channel_subscription(
- self, subscription_data: SubscriptionSchema
- ):
- """
- Update an existing notification channel subscription.
- """
- subscription = NotificationChannelSubscription(
- **subscription_data.model_dump(exclude={"deleted"})
- )
- return await self.repo.update_notification_channel_subscription(subscription)
-
@service_handler
async def delete_notification_channel_subscription(
- self, subscription_id: int
+ self, subscription_id: int, user_profile_id: int
):
- """
- Delete a notification channel subscription.
- """
- # Ensure the subscription exists
subscription = await self.repo.get_notification_channel_subscription_by_id(
subscription_id
)
- if not subscription:
+ if not subscription or subscription.user_profile_id != user_profile_id:
raise DataNotFoundException(
- f"Subscription with ID {subscription_id} not found."
+ "Subscription not found or you are not authorized to delete it."
)
- # Proceed to delete if it exists and belongs to the user
await self.repo.delete_notification_channel_subscription(subscription_id)
- logger.info(
- f"Deleted notification channel subscription {subscription_id}."
- )
+ logger.info(f"Deleted notification channel subscription {subscription_id}.")
diff --git a/backend/lcfs/web/api/notification/views.py b/backend/lcfs/web/api/notification/views.py
index 6f4828d32..f4f98d0e9 100644
--- a/backend/lcfs/web/api/notification/views.py
+++ b/backend/lcfs/web/api/notification/views.py
@@ -5,8 +5,7 @@
from typing import Union, List
from lcfs.web.exception.exceptions import DataNotFoundException
import structlog
-from fastapi import APIRouter, Body, Depends, HTTPException, Request, Query
-from lcfs.db import dependencies
+from fastapi import APIRouter, Body, Depends, HTTPException, Request
from lcfs.db.models.user.Role import RoleEnum
from lcfs.web.api.notification.schema import (
DeleteNotificationChannelSubscriptionResponseSchema,
@@ -49,7 +48,7 @@ async def get_notification_messages_by_user_id(
response_model=NotificationCountSchema,
status_code=status.HTTP_200_OK,
)
-@view_handler([RoleEnum.GOVERNMENT])
+@view_handler(["*"])
async def get_unread_notifications(
request: Request, service: NotificationService = Depends()
):
@@ -68,11 +67,10 @@ async def get_unread_notifications(
response_model=List[SubscriptionSchema],
status_code=status.HTTP_200_OK,
)
-@view_handler([RoleEnum.GOVERNMENT])
+@view_handler(["*"])
async def get_notifications_channel_subscriptions_by_user_id(
request: Request, service: NotificationService = Depends()
):
-
return await service.get_notification_channel_subscriptions_by_user_id(
user_id=request.user.user_profile_id
)
@@ -118,25 +116,29 @@ async def save_notification(
],
status_code=status.HTTP_200_OK,
)
-@view_handler([RoleEnum.GOVERNMENT])
+@view_handler(["*"])
async def save_subscription(
request: Request,
request_data: Union[SubscriptionSchema, DeleteSubscriptionSchema] = Body(...),
service: NotificationService = Depends(),
):
+ user_profile_id = request.user.user_profile_id
subscription_id = request_data.notification_channel_subscription_id
+
if request_data.deleted:
try:
- await service.delete_notification_channel_subscription(subscription_id)
+ await service.delete_notification_channel_subscription(
+ subscription_id, user_profile_id
+ )
return DeleteNotificationChannelSubscriptionResponseSchema(
message="Notification Subscription deleted successfully"
)
except DataNotFoundException as e:
raise HTTPException(status_code=404, detail=str(e))
- elif subscription_id:
- return await service.update_notification_channel_subscription(request_data)
else:
- return await service.create_notification_channel_subscription(request_data)
+ return await service.create_notification_channel_subscription(
+ request_data, user_profile_id
+ )
@router.get(
diff --git a/backend/lcfs/web/api/user/repo.py b/backend/lcfs/web/api/user/repo.py
index ab3b76485..c1906c34c 100644
--- a/backend/lcfs/web/api/user/repo.py
+++ b/backend/lcfs/web/api/user/repo.py
@@ -14,9 +14,11 @@
func,
cast,
String,
+ update,
)
from sqlalchemy.orm import joinedload
from sqlalchemy.ext.asyncio import AsyncSession
+from lcfs.web.exception.exceptions import DataNotFoundException
from lcfs.services.keycloak.dependencies import parse_external_username
from lcfs.web.core.decorators import repo_handler
@@ -665,3 +667,23 @@ async def create_login_history(self, user: UserProfile):
)
self.db.add(login_history)
+
+ @repo_handler
+ async def update_notifications_email(
+ self, user_profile_id: int, email: str
+ ) -> UserProfile:
+ # Fetch the user profile
+ query = select(UserProfile).where(
+ UserProfile.user_profile_id == user_profile_id
+ )
+ result = await self.db.execute(query)
+ user_profile = result.scalar_one_or_none()
+
+ # Update the notifications_email field
+ user_profile.notifications_email = email
+
+ # Flush and refresh without committing
+ await self.db.flush()
+ await self.db.refresh(user_profile)
+
+ return user_profile
diff --git a/backend/lcfs/web/api/user/schema.py b/backend/lcfs/web/api/user/schema.py
index 8ad458383..6cf34fe46 100644
--- a/backend/lcfs/web/api/user/schema.py
+++ b/backend/lcfs/web/api/user/schema.py
@@ -40,6 +40,7 @@ class UserBaseSchema(BaseSchema):
keycloak_username: str
keycloak_email: EmailStr
email: Optional[EmailStr] = None
+ notifications_email: Optional[EmailStr] = None
title: Optional[str] = None
phone: Optional[str] = None
first_name: Optional[str] = None
@@ -80,6 +81,7 @@ class UserActivitiesResponseSchema(BaseSchema):
activities: List[UserActivitySchema]
pagination: PaginationResponseSchema
+
class UserLoginHistorySchema(BaseSchema):
user_login_history_id: int
keycloak_email: str
@@ -93,3 +95,7 @@ class UserLoginHistorySchema(BaseSchema):
class UserLoginHistoryResponseSchema(BaseSchema):
histories: List[UserLoginHistorySchema]
pagination: PaginationResponseSchema
+
+
+class UpdateNotificationsEmailSchema(BaseSchema):
+ notifications_email: EmailStr
diff --git a/backend/lcfs/web/api/user/services.py b/backend/lcfs/web/api/user/services.py
index aad7ed006..48bad17de 100644
--- a/backend/lcfs/web/api/user/services.py
+++ b/backend/lcfs/web/api/user/services.py
@@ -332,3 +332,17 @@ async def _has_access_to_user_activities(
async def track_user_login(self, user: UserProfile):
await self.repo.create_login_history(user)
+
+ @service_handler
+ async def update_notifications_email(self, user_id: int, email: str):
+ try:
+ # Update the notifications_email field of the user
+ return await self.repo.update_notifications_email(user_id, email)
+ # Return the updated user
+ return UserBaseSchema.model_validate(user)
+ except DataNotFoundException as e:
+ logger.error(f"User not found: {e}")
+ raise HTTPException(status_code=404, detail=str(e))
+ except Exception as e:
+ logger.error(f"Error updating notifications email: {e}")
+ raise HTTPException(status_code=500, detail="Internal Server Error")
diff --git a/backend/lcfs/web/api/user/views.py b/backend/lcfs/web/api/user/views.py
index 54ae147e0..ff1a69aca 100644
--- a/backend/lcfs/web/api/user/views.py
+++ b/backend/lcfs/web/api/user/views.py
@@ -9,6 +9,7 @@
Response,
Depends,
Query,
+ HTTPException,
)
from fastapi.responses import StreamingResponse
@@ -22,6 +23,7 @@
UserLoginHistoryResponseSchema,
UsersSchema,
UserActivitiesResponseSchema,
+ UpdateNotificationsEmailSchema,
)
from lcfs.web.api.user.services import UserServices
from lcfs.web.core.decorators import view_handler
@@ -250,3 +252,21 @@ async def get_all_user_login_history(
"""
current_user = request.user
return await service.get_all_user_login_history(current_user, pagination)
+
+
+@router.post(
+ "/update-notifications-email",
+ response_model=UpdateNotificationsEmailSchema,
+ status_code=status.HTTP_200_OK,
+)
+@view_handler(["*"])
+async def update_notifications_email(
+ request: Request,
+ email_data: UpdateNotificationsEmailSchema = Body(...),
+ service: UserServices = Depends(),
+):
+ user_id = request.user.user_profile_id
+ email = email_data.notifications_email
+
+ user = await service.update_notifications_email(user_id, email)
+ return user
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index f9ec1948b..127024c7d 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -7,8 +7,8 @@ import { ViewUser } from '@/views/Admin/AdminMenu/components/ViewUser'
import { ComplianceReports } from './views/ComplianceReports'
import { Dashboard } from './views/Dashboard'
import { FileSubmissions } from './views/FileSubmissions'
+import { NotificationMenu } from '@/views/Notifications/NotificationMenu'
import { FuelCodes, AddEditFuelCode } from './views/FuelCodes'
-import { Notifications, NotificationSettings } from './views/Notifications'
import {
Organizations,
AddEditOrg,
@@ -201,13 +201,13 @@ const router = createBrowserRouter([
},
{
path: ROUTES.NOTIFICATIONS,
- element: