"""Communication repositories.""" from __future__ import annotations from datetime import datetime, timezone from typing import Sequence from uuid import UUID from sqlalchemy import asc, desc, func, or_, select from app.models.foundation import ( CommunicationAuditLog, ContactSource, DeliveryEvent, ManualContact, Message, MessageTemplate, OTPChallenge, ProviderConfig, ProviderLog, QueueItem, SenderNumber, WebhookReceipt, ) from app.repositories.base import TenantBaseRepository class ProviderConfigRepository(TenantBaseRepository[ProviderConfig]): model = ProviderConfig async def list_by_channel( self, tenant_id: UUID, channel: str, *, active_only: bool = True ) -> Sequence[ProviderConfig]: clauses = [ self.model.tenant_id == tenant_id, self.model.channel == channel, self.model.is_deleted.is_(False), ] if active_only: clauses.append(self.model.status.in_(["active", "degraded"])) stmt = ( select(self.model) .where(*clauses) .order_by(asc(self.model.priority), asc(self.model.created_at)) ) result = await self.session.execute(stmt) return result.scalars().all() async def list_all_status(self, tenant_id: UUID) -> Sequence[ProviderConfig]: stmt = ( select(self.model) .where( self.model.tenant_id == tenant_id, self.model.is_deleted.is_(False), ) .order_by(asc(self.model.channel), asc(self.model.priority)) ) result = await self.session.execute(stmt) return result.scalars().all() class SenderNumberRepository(TenantBaseRepository[SenderNumber]): model = SenderNumber async def get_default(self, tenant_id: UUID, channel: str) -> SenderNumber | None: stmt = select(self.model).where( self.model.tenant_id == tenant_id, self.model.channel == channel, self.model.is_default.is_(True), self.model.is_active.is_(True), self.model.is_deleted.is_(False), ) result = await self.session.execute(stmt) return result.scalar_one_or_none() class MessageTemplateRepository(TenantBaseRepository[MessageTemplate]): model = MessageTemplate async def get_by_key( self, tenant_id: UUID, template_key: str, *, locale: str = "fa", approved_only: bool = True, ) -> MessageTemplate | None: clauses = [ self.model.tenant_id == tenant_id, self.model.template_key == template_key, self.model.locale == locale, self.model.is_deleted.is_(False), ] if approved_only: clauses.append(self.model.status == "approved") stmt = ( select(self.model) .where(*clauses) .order_by(desc(self.model.version)) .limit(1) ) result = await self.session.execute(stmt) return result.scalar_one_or_none() async def list_versions( self, tenant_id: UUID, template_key: str, locale: str = "fa" ) -> Sequence[MessageTemplate]: stmt = ( select(self.model) .where( self.model.tenant_id == tenant_id, self.model.template_key == template_key, self.model.locale == locale, self.model.is_deleted.is_(False), ) .order_by(desc(self.model.version)) ) result = await self.session.execute(stmt) return result.scalars().all() class ManualContactRepository(TenantBaseRepository[ManualContact]): model = ManualContact class ContactSourceRepository(TenantBaseRepository[ContactSource]): model = ContactSource async def list_active(self, tenant_id: UUID) -> Sequence[ContactSource]: stmt = select(self.model).where( self.model.tenant_id == tenant_id, self.model.is_active.is_(True), self.model.is_deleted.is_(False), ) result = await self.session.execute(stmt) return result.scalars().all() class MessageRepository(TenantBaseRepository[Message]): model = Message async def get_by_correlation( self, tenant_id: UUID, correlation_id: str ) -> Sequence[Message]: stmt = ( select(self.model) .where( self.model.tenant_id == tenant_id, self.model.correlation_id == correlation_id, self.model.is_deleted.is_(False), ) .order_by(asc(self.model.created_at)) ) result = await self.session.execute(stmt) return result.scalars().all() async def list_by_status( self, tenant_id: UUID, status: str, *, offset: int = 0, limit: int = 20 ) -> Sequence[Message]: stmt = ( select(self.model) .where( self.model.tenant_id == tenant_id, self.model.status == status, self.model.is_deleted.is_(False), ) .order_by(desc(self.model.created_at)) .offset(offset) .limit(limit) ) result = await self.session.execute(stmt) return result.scalars().all() async def count_by_status(self, tenant_id: UUID) -> dict[str, int]: stmt = ( select(self.model.status, func.count()) .where( self.model.tenant_id == tenant_id, self.model.is_deleted.is_(False), ) .group_by(self.model.status) ) result = await self.session.execute(stmt) return {row[0]: int(row[1]) for row in result.all()} class QueueItemRepository(TenantBaseRepository[QueueItem]): model = QueueItem async def enqueue(self, item: QueueItem) -> QueueItem: return await self.add(item) async def claim_next( self, tenant_id: UUID | None = None, *, limit: int = 10, increment_attempt: bool = True, ) -> Sequence[QueueItem]: now = datetime.now(timezone.utc) clauses = [ self.model.status.in_(["pending", "scheduled"]), self.model.is_dead_letter.is_(False), ] if tenant_id is not None: clauses.append(self.model.tenant_id == tenant_id) from sqlalchemy import case priority_case = case( (self.model.priority == "urgent", 1), (self.model.priority == "high", 2), (self.model.priority == "normal", 3), else_=4, ) stmt = ( select(self.model) .where(*clauses) .order_by(asc(priority_case), asc(self.model.available_at)) .limit(limit * 3) ) bind = self.session.get_bind() if bind is not None and bind.dialect.name == "postgresql": stmt = stmt.with_for_update(skip_locked=True) result = await self.session.execute(stmt) candidates = list(result.scalars().all()) items: list[QueueItem] = [] for item in candidates: available = item.available_at if available.tzinfo is None: available = available.replace(tzinfo=timezone.utc) if available > now: continue item.status = "processing" item.locked_at = now if increment_attempt: item.attempt += 1 items.append(item) if len(items) >= limit: break await self.session.flush() return items async def count_by_status(self, tenant_id: UUID) -> dict[str, int]: stmt = ( select(self.model.status, func.count()) .where(self.model.tenant_id == tenant_id) .group_by(self.model.status) ) result = await self.session.execute(stmt) return {row[0]: int(row[1]) for row in result.all()} async def list_dead_letters( self, tenant_id: UUID, *, offset: int = 0, limit: int = 20 ) -> Sequence[QueueItem]: stmt = ( select(self.model) .where( self.model.tenant_id == tenant_id, or_( self.model.is_dead_letter.is_(True), self.model.status == "dead_letter", ), ) .offset(offset) .limit(limit) ) result = await self.session.execute(stmt) return result.scalars().all() class DeliveryEventRepository(TenantBaseRepository[DeliveryEvent]): model = DeliveryEvent async def list_for_message( self, tenant_id: UUID, message_id: UUID ) -> Sequence[DeliveryEvent]: stmt = ( select(self.model) .where( self.model.tenant_id == tenant_id, self.model.message_id == message_id, ) .order_by(asc(self.model.created_at)) ) result = await self.session.execute(stmt) return result.scalars().all() class ProviderLogRepository(TenantBaseRepository[ProviderLog]): model = ProviderLog class OTPChallengeRepository(TenantBaseRepository[OTPChallenge]): model = OTPChallenge async def get_latest_pending( self, tenant_id: UUID, destination: str ) -> OTPChallenge | None: stmt = ( select(self.model) .where( self.model.tenant_id == tenant_id, self.model.destination == destination, self.model.status == "pending", ) .order_by(desc(self.model.created_at)) .limit(1) ) result = await self.session.execute(stmt) return result.scalar_one_or_none() async def count_recent( self, tenant_id: UUID, destination: str, since: datetime ) -> int: stmt = select(func.count()).select_from(self.model).where( self.model.tenant_id == tenant_id, self.model.destination == destination, self.model.created_at >= since, ) result = await self.session.execute(stmt) return int(result.scalar_one()) class WebhookReceiptRepository(TenantBaseRepository[WebhookReceipt]): model = WebhookReceipt class AuditLogRepository(TenantBaseRepository[CommunicationAuditLog]): model = CommunicationAuditLog