""" Base Repository for Notification Service Service-specific repository base class with notification utilities """ from typing import Optional, List, Dict, Any, Type from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import text, and_ from datetime import datetime, timedelta import structlog from shared.database.repository import BaseRepository from shared.database.exceptions import DatabaseError logger = structlog.get_logger() class NotificationBaseRepository(BaseRepository): """Base repository for notification service with common notification operations""" def __init__(self, model: Type, session: AsyncSession, cache_ttl: Optional[int] = 300): # Notifications change frequently, shorter cache time (5 minutes) super().__init__(model, session, cache_ttl) async def get_by_tenant_id(self, tenant_id: str, skip: int = 0, limit: int = 100) -> List: """Get records by tenant ID""" if hasattr(self.model, 'tenant_id'): return await self.get_multi( skip=skip, limit=limit, filters={"tenant_id": tenant_id}, order_by="created_at", order_desc=True ) return await self.get_multi(skip=skip, limit=limit) async def get_by_user_id(self, user_id: str, skip: int = 0, limit: int = 100) -> List: """Get records by user ID (recipient or sender)""" filters = {} if hasattr(self.model, 'recipient_id'): filters["recipient_id"] = user_id elif hasattr(self.model, 'sender_id'): filters["sender_id"] = user_id elif hasattr(self.model, 'user_id'): filters["user_id"] = user_id if filters: return await self.get_multi( skip=skip, limit=limit, filters=filters, order_by="created_at", order_desc=True ) return [] async def get_by_status(self, status: str, skip: int = 0, limit: int = 100) -> List: """Get records by status""" if hasattr(self.model, 'status'): return await self.get_multi( skip=skip, limit=limit, filters={"status": status}, order_by="created_at", order_desc=True ) return await self.get_multi(skip=skip, limit=limit) async def get_active_records(self, skip: int = 0, limit: int = 100) -> List: """Get active records (if model has is_active field)""" if hasattr(self.model, 'is_active'): return await self.get_multi( skip=skip, limit=limit, filters={"is_active": True}, order_by="created_at", order_desc=True ) return await self.get_multi(skip=skip, limit=limit) async def get_recent_records(self, hours: int = 24, skip: int = 0, limit: int = 100) -> List: """Get records created in the last N hours""" try: cutoff_time = datetime.utcnow() - timedelta(hours=hours) table_name = self.model.__tablename__ query_text = f""" SELECT * FROM {table_name} WHERE created_at >= :cutoff_time ORDER BY created_at DESC LIMIT :limit OFFSET :skip """ result = await self.session.execute(text(query_text), { "cutoff_time": cutoff_time, "limit": limit, "skip": skip }) records = [] for row in result.fetchall(): record_dict = dict(row._mapping) record = self.model(**record_dict) records.append(record) return records except Exception as e: logger.error("Failed to get recent records", model=self.model.__name__, hours=hours, error=str(e)) return [] async def cleanup_old_records(self, days_old: int = 90) -> int: """Clean up old notification records (90 days by default)""" try: cutoff_date = datetime.utcnow() - timedelta(days=days_old) table_name = self.model.__tablename__ # Only delete successfully processed or cancelled records that are old conditions = [ "created_at < :cutoff_date" ] # Add status condition if model has status field if hasattr(self.model, 'status'): conditions.append("status IN ('delivered', 'cancelled', 'failed')") query_text = f""" DELETE FROM {table_name} WHERE {' AND '.join(conditions)} """ result = await self.session.execute(text(query_text), {"cutoff_date": cutoff_date}) deleted_count = result.rowcount logger.info(f"Cleaned up old {self.model.__name__} records", deleted_count=deleted_count, days_old=days_old) return deleted_count except Exception as e: logger.error("Failed to cleanup old records", model=self.model.__name__, error=str(e)) raise DatabaseError(f"Cleanup failed: {str(e)}") async def get_statistics_by_tenant(self, tenant_id: str) -> Dict[str, Any]: """Get statistics for a tenant""" try: table_name = self.model.__tablename__ # Get basic counts total_records = await self.count(filters={"tenant_id": tenant_id}) # Get recent activity (records in last 24 hours) twenty_four_hours_ago = datetime.utcnow() - timedelta(hours=24) recent_query = text(f""" SELECT COUNT(*) as count FROM {table_name} WHERE tenant_id = :tenant_id AND created_at >= :twenty_four_hours_ago """) result = await self.session.execute(recent_query, { "tenant_id": tenant_id, "twenty_four_hours_ago": twenty_four_hours_ago }) recent_records = result.scalar() or 0 # Get status breakdown if applicable status_breakdown = {} if hasattr(self.model, 'status'): status_query = text(f""" SELECT status, COUNT(*) as count FROM {table_name} WHERE tenant_id = :tenant_id GROUP BY status """) result = await self.session.execute(status_query, {"tenant_id": tenant_id}) status_breakdown = {row.status: row.count for row in result.fetchall()} return { "total_records": total_records, "recent_records_24h": recent_records, "status_breakdown": status_breakdown } except Exception as e: logger.error("Failed to get tenant statistics", model=self.model.__name__, tenant_id=tenant_id, error=str(e)) return { "total_records": 0, "recent_records_24h": 0, "status_breakdown": {} } def _validate_notification_data(self, data: Dict[str, Any], required_fields: List[str]) -> Dict[str, Any]: """Validate notification-related data""" errors = [] for field in required_fields: if field not in data or not data[field]: errors.append(f"Missing required field: {field}") # Validate tenant_id format if present if "tenant_id" in data and data["tenant_id"]: tenant_id = data["tenant_id"] if not isinstance(tenant_id, str) or len(tenant_id) < 1: errors.append("Invalid tenant_id format") # Validate user IDs if present user_fields = ["user_id", "recipient_id", "sender_id"] for field in user_fields: if field in data and data[field]: user_id = data[field] if not isinstance(user_id, str) or len(user_id) < 1: errors.append(f"Invalid {field} format") # Validate email format if present if "recipient_email" in data and data["recipient_email"]: email = data["recipient_email"] if "@" not in email or "." not in email.split("@")[-1]: errors.append("Invalid email format") # Validate phone format if present if "recipient_phone" in data and data["recipient_phone"]: phone = data["recipient_phone"] if not isinstance(phone, str) or len(phone) < 9: errors.append("Invalid phone format") # Validate priority if present if "priority" in data and data["priority"]: from enum import Enum priority_value = data["priority"].value if isinstance(data["priority"], Enum) else data["priority"] valid_priorities = ["low", "normal", "high", "urgent"] if priority_value not in valid_priorities: errors.append(f"Invalid priority. Must be one of: {valid_priorities}") # Validate notification type if present if "type" in data and data["type"]: from enum import Enum type_value = data["type"].value if isinstance(data["type"], Enum) else data["type"] valid_types = ["email", "whatsapp", "push", "sms"] if type_value not in valid_types: errors.append(f"Invalid notification type. Must be one of: {valid_types}") # Validate status if present if "status" in data and data["status"]: from enum import Enum status_value = data["status"].value if isinstance(data["status"], Enum) else data["status"] valid_statuses = ["pending", "sent", "delivered", "failed", "cancelled"] if status_value not in valid_statuses: errors.append(f"Invalid status. Must be one of: {valid_statuses}") return { "is_valid": len(errors) == 0, "errors": errors }