from datetime import timedelta import pytest from dirty_equals import IsList from fansifter_common.utils import timezone from app.enums import CampaignStatus from app.models import Campaign, CampaignBatch from app.repositories import CampaignRepository from tests.unit.types import CreateModel class TestCampaignRepositoryFindReadyToDispatch: @pytest.mark.db def test_find_ready_to_dispatch_returns_ready_campaigns( self, repository: CampaignRepository, create_model: CreateModel, ) -> None: """ Should return campaigns that are scheduled/in-progress and ready to dispatch. """ now = timezone.now() # ✅ Ready: scheduled and send_at <= now scheduled_ready_campaign = create_model( Campaign, status=CampaignStatus.SCHEDULED, send_at=now - timedelta(minutes=5), cancelled_at=None, ) # ✅ Ready: in-progress and send_at <= now in_progress_ready_campaign = create_model( Campaign, status=CampaignStatus.IN_PROGRESS, send_at=now - timedelta(minutes=1), cancelled_at=None, ) # 🚫 Not ready: scheduled in the future create_model( Campaign, status=CampaignStatus.SCHEDULED, send_at=now + timedelta(hours=1), ) # 🚫 Not ready: cancelled create_model( Campaign, status=CampaignStatus.CANCELLED, send_at=now - timedelta(minutes=10), cancelled_at=now, ) # 🚫 Not ready: completed create_model( Campaign, status=CampaignStatus.SENT, send_at=now - timedelta(minutes=10), ) # 🚫 Not ready: invalid status (e.g., DRAFT) create_model( Campaign, status=CampaignStatus.DRAFT, send_at=now - timedelta(minutes=10), ) result = repository.find_ready_to_dispatch() # Expect only the two ready campaigns assert result == IsList( scheduled_ready_campaign, in_progress_ready_campaign, check_order=False, ) @pytest.mark.db def test_find_ready_to_dispatch_returns_empty_when_none_ready( self, repository: CampaignRepository, create_model: CreateModel, ) -> None: """ Should return empty list when no campaigns meet readiness criteria. """ now = timezone.now() # 🚫 Not ready: Scheduled but send_at in future create_model( Campaign, status=CampaignStatus.SCHEDULED, send_at=now + timedelta(hours=2), ) # 🚫 Not ready: In-progress but cancelled create_model( Campaign, status=CampaignStatus.CANCELLED, send_at=now - timedelta(minutes=10), cancelled_at=now, ) # 🚫 Not ready: Completed campaign create_model( Campaign, status=CampaignStatus.SENT, send_at=now - timedelta(minutes=30), ) result = repository.find_ready_to_dispatch() assert result == [] @pytest.mark.db def test_find_ready_to_dispatch_multiple_ready_campaigns( self, repository: CampaignRepository, create_model: CreateModel, ) -> None: """ Should return multiple campaigns when several meet readiness criteria. """ now = timezone.now() # ✅ Ready: scheduled campaign with send_at in the past scheduled_ready_1_campaign = create_model( Campaign, status=CampaignStatus.SCHEDULED, send_at=now - timedelta(minutes=5), ) # ✅ Ready: another scheduled campaign with send_at in the past scheduled_ready_2_campaign = create_model( Campaign, status=CampaignStatus.SCHEDULED, send_at=now - timedelta(minutes=2), ) # ✅ Ready: in-progress campaign with send_at already reached in_progress_ready_campaign = create_model( Campaign, status=CampaignStatus.IN_PROGRESS, send_at=now - timedelta(minutes=1), ) result = repository.find_ready_to_dispatch() assert result == IsList( scheduled_ready_1_campaign, scheduled_ready_2_campaign, in_progress_ready_campaign, check_order=False, ) @pytest.mark.db def test_update_as_sent( self, repository: CampaignRepository, create_model: CreateModel ) -> None: campaign = create_model(Campaign, status="IN_PROGRESS") create_model( CampaignBatch, campaign_id=campaign.id, completed_at=timezone.now(), cancelled_at=None, ) create_model( CampaignBatch, campaign_id=campaign.id, completed_at=timezone.now(), cancelled_at=None, ) is_sent = repository.update_as_sent(campaign.id) assert is_sent @pytest.mark.db def test_update_as_sent_not_sent( self, repository: CampaignRepository, create_model: CreateModel ) -> None: campaign = create_model(Campaign, status="IN_PROGRESS") create_model( CampaignBatch, campaign_id=campaign.id, completed_at=timezone.now(), cancelled_at=None, ) create_model( CampaignBatch, campaign_id=campaign.id, completed_at=None, cancelled_at=None, ) is_sent = repository.update_as_sent(campaign.id) assert not is_sent