"""Unit tests for LoyaltyNotificationService.""" from unittest.mock import MagicMock, patch import pytest from app.modules.loyalty.services.notification_service import ( LoyaltyNotificationService, ) @pytest.fixture def notification_service(): return LoyaltyNotificationService() def _make_card_stub( card_id=1, customer_id=10, merchant_id=100, enrolled_at_store_id=200, card_number="1234-5678-9012", ): """Lightweight card stub for notification tests.""" card = MagicMock() card.id = card_id card.customer_id = customer_id card.merchant_id = merchant_id card.enrolled_at_store_id = enrolled_at_store_id card.card_number = card_number card.program = MagicMock() card.program.display_name = "Test Rewards" return card def _make_customer_stub(email="test@example.com", full_name="Test User"): customer = MagicMock() customer.id = 10 customer.email = email customer.full_name = full_name return customer def _make_store_stub(name="Test Store"): store = MagicMock() store.id = 200 store.name = name return store @pytest.mark.unit @pytest.mark.loyalty class TestNotificationServiceDispatch: """Verify that each notification method enqueues the correct template.""" @patch("app.modules.loyalty.tasks.notifications.send_notification_email") @patch("app.modules.tenancy.services.store_service.store_service") @patch("app.modules.customers.services.customer_service.customer_service") def test_send_enrollment_confirmation( self, mock_customer_svc, mock_store_svc, mock_task, notification_service ): mock_customer_svc.get_customer_by_id.return_value = _make_customer_stub() mock_store_svc.get_store_by_id_optional.return_value = _make_store_stub() db = MagicMock() card = _make_card_stub() notification_service.send_enrollment_confirmation(db, card) mock_task.delay.assert_called_once() call_kwargs = mock_task.delay.call_args[1] assert call_kwargs["template_code"] == "loyalty_enrollment" assert call_kwargs["to_email"] == "test@example.com" assert "card_number" in call_kwargs["variables"] @patch("app.modules.loyalty.tasks.notifications.send_notification_email") @patch("app.modules.tenancy.services.store_service.store_service") @patch("app.modules.customers.services.customer_service.customer_service") def test_send_welcome_bonus( self, mock_customer_svc, mock_store_svc, mock_task, notification_service ): mock_customer_svc.get_customer_by_id.return_value = _make_customer_stub() mock_store_svc.get_store_by_id_optional.return_value = _make_store_stub() db = MagicMock() card = _make_card_stub() notification_service.send_welcome_bonus(db, card, 50) mock_task.delay.assert_called_once() call_kwargs = mock_task.delay.call_args[1] assert call_kwargs["template_code"] == "loyalty_welcome_bonus" assert call_kwargs["variables"]["points"] == "50" @patch("app.modules.loyalty.tasks.notifications.send_notification_email") @patch("app.modules.tenancy.services.store_service.store_service") @patch("app.modules.customers.services.customer_service.customer_service") def test_send_welcome_bonus_skipped_for_zero_points( self, mock_customer_svc, mock_store_svc, mock_task, notification_service ): db = MagicMock() card = _make_card_stub() notification_service.send_welcome_bonus(db, card, 0) mock_task.delay.assert_not_called() @patch("app.modules.loyalty.tasks.notifications.send_notification_email") @patch("app.modules.tenancy.services.store_service.store_service") @patch("app.modules.customers.services.customer_service.customer_service") def test_send_points_expiration_warning( self, mock_customer_svc, mock_store_svc, mock_task, notification_service ): mock_customer_svc.get_customer_by_id.return_value = _make_customer_stub() mock_store_svc.get_store_by_id_optional.return_value = _make_store_stub() db = MagicMock() card = _make_card_stub() notification_service.send_points_expiration_warning( db, card, expiring_points=200, days_remaining=14, expiration_date="2026-05-01" ) mock_task.delay.assert_called_once() call_kwargs = mock_task.delay.call_args[1] assert call_kwargs["template_code"] == "loyalty_points_expiring" assert call_kwargs["variables"]["points"] == "200" assert call_kwargs["variables"]["days_remaining"] == "14" @patch("app.modules.loyalty.tasks.notifications.send_notification_email") @patch("app.modules.tenancy.services.store_service.store_service") @patch("app.modules.customers.services.customer_service.customer_service") def test_send_points_expired( self, mock_customer_svc, mock_store_svc, mock_task, notification_service ): mock_customer_svc.get_customer_by_id.return_value = _make_customer_stub() mock_store_svc.get_store_by_id_optional.return_value = _make_store_stub() db = MagicMock() card = _make_card_stub() notification_service.send_points_expired(db, card, 150) mock_task.delay.assert_called_once() call_kwargs = mock_task.delay.call_args[1] assert call_kwargs["template_code"] == "loyalty_points_expired" assert call_kwargs["variables"]["expired_points"] == "150" @patch("app.modules.loyalty.tasks.notifications.send_notification_email") @patch("app.modules.tenancy.services.store_service.store_service") @patch("app.modules.customers.services.customer_service.customer_service") def test_send_reward_available( self, mock_customer_svc, mock_store_svc, mock_task, notification_service ): mock_customer_svc.get_customer_by_id.return_value = _make_customer_stub() mock_store_svc.get_store_by_id_optional.return_value = _make_store_stub() db = MagicMock() card = _make_card_stub() notification_service.send_reward_available(db, card, "Free Coffee") mock_task.delay.assert_called_once() call_kwargs = mock_task.delay.call_args[1] assert call_kwargs["template_code"] == "loyalty_reward_ready" assert call_kwargs["variables"]["reward_name"] == "Free Coffee" @patch("app.modules.loyalty.tasks.notifications.send_notification_email") @patch("app.modules.tenancy.services.store_service.store_service") @patch("app.modules.customers.services.customer_service.customer_service") def test_no_email_skips_dispatch( self, mock_customer_svc, mock_store_svc, mock_task, notification_service ): """Customer without email does not trigger a send.""" no_email_customer = _make_customer_stub(email=None) mock_customer_svc.get_customer_by_id.return_value = no_email_customer db = MagicMock() card = _make_card_stub() notification_service.send_enrollment_confirmation(db, card) mock_task.delay.assert_not_called() @patch("app.modules.loyalty.tasks.notifications.send_notification_email") @patch("app.modules.tenancy.services.store_service.store_service") @patch("app.modules.customers.services.customer_service.customer_service") def test_customer_not_found_skips_dispatch( self, mock_customer_svc, mock_store_svc, mock_task, notification_service ): """Missing customer does not trigger a send.""" mock_customer_svc.get_customer_by_id.return_value = None db = MagicMock() card = _make_card_stub() notification_service.send_enrollment_confirmation(db, card) mock_task.delay.assert_not_called()