""" Preference Manager for user notification preferences """ import logging from typing import Optional, Dict, Any, List from datetime import datetime import motor.motor_asyncio from models import NotificationPreference, NotificationChannel, NotificationCategory logger = logging.getLogger(__name__) class PreferenceManager: """Manages user notification preferences""" def __init__(self, mongodb_url: str = "mongodb://mongodb:27017", database_name: str = "notifications"): self.mongodb_url = mongodb_url self.database_name = database_name self.client = None self.db = None self.preferences_collection = None self.is_connected = False # In-memory cache for demo self.preferences_cache = {} async def connect(self): """Connect to MongoDB""" try: self.client = motor.motor_asyncio.AsyncIOMotorClient(self.mongodb_url) self.db = self.client[self.database_name] self.preferences_collection = self.db["preferences"] # Test connection await self.client.admin.command('ping') self.is_connected = True # Create indexes await self._create_indexes() logger.info("Connected to MongoDB for preferences") except Exception as e: logger.error(f"Failed to connect to MongoDB: {e}") # Fallback to in-memory storage self.is_connected = False logger.warning("Using in-memory storage for preferences") async def close(self): """Close MongoDB connection""" if self.client: self.client.close() self.is_connected = False logger.info("Disconnected from MongoDB") async def _create_indexes(self): """Create database indexes""" if self.preferences_collection: try: await self.preferences_collection.create_index("user_id", unique=True) logger.info("Created indexes for preferences collection") except Exception as e: logger.error(f"Failed to create indexes: {e}") async def get_user_preferences(self, user_id: str) -> Optional[NotificationPreference]: """Get notification preferences for a user""" try: # Check cache first if user_id in self.preferences_cache: return self.preferences_cache[user_id] if self.is_connected and self.preferences_collection: # Get from MongoDB doc = await self.preferences_collection.find_one({"user_id": user_id}) if doc: # Convert document to model doc.pop('_id', None) # Remove MongoDB ID preference = NotificationPreference(**doc) # Update cache self.preferences_cache[user_id] = preference return preference # Return default preferences if not found return self._get_default_preferences(user_id) except Exception as e: logger.error(f"Failed to get preferences for user {user_id}: {e}") return self._get_default_preferences(user_id) async def update_user_preferences( self, user_id: str, preferences: NotificationPreference ) -> bool: """Update notification preferences for a user""" try: preferences.user_id = user_id preferences.updated_at = datetime.now() # Update cache self.preferences_cache[user_id] = preferences if self.is_connected and self.preferences_collection: # Convert to dict for MongoDB pref_dict = preferences.dict() # Upsert in MongoDB result = await self.preferences_collection.update_one( {"user_id": user_id}, {"$set": pref_dict}, upsert=True ) logger.info(f"Updated preferences for user {user_id}") return result.modified_count > 0 or result.upserted_id is not None # If not connected, just use cache return True except Exception as e: logger.error(f"Failed to update preferences for user {user_id}: {e}") return False async def unsubscribe_category(self, user_id: str, category: str) -> bool: """Unsubscribe user from a notification category""" try: preferences = await self.get_user_preferences(user_id) if not preferences: preferences = self._get_default_preferences(user_id) # Update category preference if hasattr(NotificationCategory, category.upper()): cat_enum = NotificationCategory(category.lower()) preferences.categories[cat_enum] = False # Save updated preferences return await self.update_user_preferences(user_id, preferences) return False except Exception as e: logger.error(f"Failed to unsubscribe user {user_id} from {category}: {e}") return False async def subscribe_category(self, user_id: str, category: str) -> bool: """Subscribe user to a notification category""" try: preferences = await self.get_user_preferences(user_id) if not preferences: preferences = self._get_default_preferences(user_id) # Update category preference if hasattr(NotificationCategory, category.upper()): cat_enum = NotificationCategory(category.lower()) preferences.categories[cat_enum] = True # Save updated preferences return await self.update_user_preferences(user_id, preferences) return False except Exception as e: logger.error(f"Failed to subscribe user {user_id} to {category}: {e}") return False async def enable_channel(self, user_id: str, channel: NotificationChannel) -> bool: """Enable a notification channel for user""" try: preferences = await self.get_user_preferences(user_id) if not preferences: preferences = self._get_default_preferences(user_id) preferences.channels[channel] = True return await self.update_user_preferences(user_id, preferences) except Exception as e: logger.error(f"Failed to enable channel {channel} for user {user_id}: {e}") return False async def disable_channel(self, user_id: str, channel: NotificationChannel) -> bool: """Disable a notification channel for user""" try: preferences = await self.get_user_preferences(user_id) if not preferences: preferences = self._get_default_preferences(user_id) preferences.channels[channel] = False return await self.update_user_preferences(user_id, preferences) except Exception as e: logger.error(f"Failed to disable channel {channel} for user {user_id}: {e}") return False async def set_quiet_hours( self, user_id: str, start_time: str, end_time: str ) -> bool: """Set quiet hours for user""" try: preferences = await self.get_user_preferences(user_id) if not preferences: preferences = self._get_default_preferences(user_id) preferences.quiet_hours = { "start": start_time, "end": end_time } return await self.update_user_preferences(user_id, preferences) except Exception as e: logger.error(f"Failed to set quiet hours for user {user_id}: {e}") return False async def clear_quiet_hours(self, user_id: str) -> bool: """Clear quiet hours for user""" try: preferences = await self.get_user_preferences(user_id) if not preferences: preferences = self._get_default_preferences(user_id) preferences.quiet_hours = None return await self.update_user_preferences(user_id, preferences) except Exception as e: logger.error(f"Failed to clear quiet hours for user {user_id}: {e}") return False async def set_email_frequency(self, user_id: str, frequency: str) -> bool: """Set email notification frequency""" try: if frequency not in ["immediate", "daily", "weekly"]: return False preferences = await self.get_user_preferences(user_id) if not preferences: preferences = self._get_default_preferences(user_id) preferences.email_frequency = frequency return await self.update_user_preferences(user_id, preferences) except Exception as e: logger.error(f"Failed to set email frequency for user {user_id}: {e}") return False async def batch_get_preferences(self, user_ids: List[str]) -> Dict[str, NotificationPreference]: """Get preferences for multiple users""" results = {} for user_id in user_ids: pref = await self.get_user_preferences(user_id) if pref: results[user_id] = pref return results async def delete_user_preferences(self, user_id: str) -> bool: """Delete all preferences for a user""" try: # Remove from cache if user_id in self.preferences_cache: del self.preferences_cache[user_id] if self.is_connected and self.preferences_collection: # Delete from MongoDB result = await self.preferences_collection.delete_one({"user_id": user_id}) logger.info(f"Deleted preferences for user {user_id}") return result.deleted_count > 0 return True except Exception as e: logger.error(f"Failed to delete preferences for user {user_id}: {e}") return False def _get_default_preferences(self, user_id: str) -> NotificationPreference: """Get default notification preferences""" return NotificationPreference( user_id=user_id, channels={ NotificationChannel.EMAIL: True, NotificationChannel.SMS: False, NotificationChannel.PUSH: True, NotificationChannel.IN_APP: True }, categories={ NotificationCategory.SYSTEM: True, NotificationCategory.MARKETING: False, NotificationCategory.TRANSACTION: True, NotificationCategory.SOCIAL: True, NotificationCategory.SECURITY: True, NotificationCategory.UPDATE: True }, email_frequency="immediate", timezone="UTC", language="en" ) async def is_notification_allowed( self, user_id: str, channel: NotificationChannel, category: NotificationCategory ) -> bool: """Check if notification is allowed based on preferences""" preferences = await self.get_user_preferences(user_id) if not preferences: return True # Allow by default if no preferences # Check channel preference if not preferences.channels.get(channel, True): return False # Check category preference if not preferences.categories.get(category, True): return False # Check quiet hours if preferences.quiet_hours and channel != NotificationChannel.IN_APP: # Would need to check current time against quiet hours # For demo, we'll allow all pass return True