from datetime import datetime from app.models import CampaignBatch from app.strategies.base import QuotaAllocationStrategy from app.strategies.types import QuotaAllocation, QuotaStrategyType class FcfsStrategy(QuotaAllocationStrategy): """ FCFS (First Come - First Served) quota allocation strategy with spillover. Processes campaigns in chronological order based on scheduled_at timestamp. Allocates quota sequentially with spillover: unused quota passes to next eligible campaign. Key principles: - scheduled_at determines queue priority (earlier = higher priority) - Legacy campaigns (scheduled_at=NULL) get highest priority, sorted by send_at - send_at acts as time constraint (campaign cannot send before this time) - Quota spillover: unused quota automatically allocates to next eligible campaign - Per-ISP independent queues - Time-boundary preemption supported This is the new allocation algorithm designed for predictable completion times. """ strategy_type = QuotaStrategyType.FCFS def allocate_quota( self, batches: list[CampaignBatch], available_quota: int, current_time: datetime, ) -> list[QuotaAllocation]: """ Allocate ALL available quota to the first eligible campaign in FCFS queue. Algorithm: 1. Sort batches by campaign.scheduled_at (earliest first) 2. Filter campaigns that passed send_at constraint 3. Select first campaign (highest priority) 4. Allocate full available quota to that campaign (limited by remaining recipients) """ if not batches or available_quota <= 0: return [] sorted_batches = sorted( batches, key=lambda b: ( b.campaign.scheduled_at is not None, b.campaign.scheduled_at if b.campaign.scheduled_at else b.campaign.send_at, b.campaign.created_at, b.campaign.id, ), ) allocations: list[QuotaAllocation] = [] remaining_quota = available_quota for batch in sorted_batches: if remaining_quota <= 0: break campaign = batch.campaign if campaign.send_at and campaign.send_at > current_time: continue remaining_emails = batch.batch_size - batch.batch_offset emails_to_send = min(remaining_quota, remaining_emails) if emails_to_send <= 0: continue allocations.append( QuotaAllocation( batch_id=batch.id, campaign_id=batch.campaign_id, emails_to_send=emails_to_send, ) ) remaining_quota -= emails_to_send return allocations