REFACTOR - Database logic
This commit is contained in:
474
services/notification/app/repositories/preference_repository.py
Normal file
474
services/notification/app/repositories/preference_repository.py
Normal file
@@ -0,0 +1,474 @@
|
||||
"""
|
||||
Preference Repository
|
||||
Repository for notification preference operations
|
||||
"""
|
||||
|
||||
from typing import Optional, List, Dict, Any
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, text, and_
|
||||
from datetime import datetime
|
||||
import structlog
|
||||
|
||||
from .base import NotificationBaseRepository
|
||||
from app.models.notifications import NotificationPreference
|
||||
from shared.database.exceptions import DatabaseError, ValidationError, DuplicateRecordError
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
|
||||
class PreferenceRepository(NotificationBaseRepository):
|
||||
"""Repository for notification preference operations"""
|
||||
|
||||
def __init__(self, session: AsyncSession, cache_ttl: Optional[int] = 900):
|
||||
# Preferences are relatively stable, medium cache time (15 minutes)
|
||||
super().__init__(NotificationPreference, session, cache_ttl)
|
||||
|
||||
async def create_preferences(self, preference_data: Dict[str, Any]) -> NotificationPreference:
|
||||
"""Create user notification preferences with validation"""
|
||||
try:
|
||||
# Validate preference data
|
||||
validation_result = self._validate_notification_data(
|
||||
preference_data,
|
||||
["user_id", "tenant_id"]
|
||||
)
|
||||
|
||||
if not validation_result["is_valid"]:
|
||||
raise ValidationError(f"Invalid preference data: {validation_result['errors']}")
|
||||
|
||||
# Check if preferences already exist for this user and tenant
|
||||
existing_prefs = await self.get_user_preferences(
|
||||
preference_data["user_id"],
|
||||
preference_data["tenant_id"]
|
||||
)
|
||||
|
||||
if existing_prefs:
|
||||
raise DuplicateRecordError(f"Preferences already exist for user in this tenant")
|
||||
|
||||
# Set default values
|
||||
defaults = {
|
||||
"email_enabled": True,
|
||||
"email_alerts": True,
|
||||
"email_marketing": False,
|
||||
"email_reports": True,
|
||||
"whatsapp_enabled": False,
|
||||
"whatsapp_alerts": False,
|
||||
"whatsapp_reports": False,
|
||||
"push_enabled": True,
|
||||
"push_alerts": True,
|
||||
"push_reports": False,
|
||||
"quiet_hours_start": "22:00",
|
||||
"quiet_hours_end": "08:00",
|
||||
"timezone": "Europe/Madrid",
|
||||
"digest_frequency": "daily",
|
||||
"max_emails_per_day": 10,
|
||||
"language": "es"
|
||||
}
|
||||
|
||||
# Apply defaults for any missing fields
|
||||
for key, default_value in defaults.items():
|
||||
if key not in preference_data:
|
||||
preference_data[key] = default_value
|
||||
|
||||
# Create preferences
|
||||
preferences = await self.create(preference_data)
|
||||
|
||||
logger.info("User notification preferences created",
|
||||
preferences_id=preferences.id,
|
||||
user_id=preferences.user_id,
|
||||
tenant_id=preferences.tenant_id)
|
||||
|
||||
return preferences
|
||||
|
||||
except (ValidationError, DuplicateRecordError):
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error("Failed to create preferences",
|
||||
user_id=preference_data.get("user_id"),
|
||||
tenant_id=preference_data.get("tenant_id"),
|
||||
error=str(e))
|
||||
raise DatabaseError(f"Failed to create preferences: {str(e)}")
|
||||
|
||||
async def get_user_preferences(
|
||||
self,
|
||||
user_id: str,
|
||||
tenant_id: str
|
||||
) -> Optional[NotificationPreference]:
|
||||
"""Get notification preferences for a specific user and tenant"""
|
||||
try:
|
||||
preferences = await self.get_multi(
|
||||
filters={
|
||||
"user_id": user_id,
|
||||
"tenant_id": tenant_id
|
||||
},
|
||||
limit=1
|
||||
)
|
||||
return preferences[0] if preferences else None
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to get user preferences",
|
||||
user_id=user_id,
|
||||
tenant_id=tenant_id,
|
||||
error=str(e))
|
||||
raise DatabaseError(f"Failed to get preferences: {str(e)}")
|
||||
|
||||
async def update_user_preferences(
|
||||
self,
|
||||
user_id: str,
|
||||
tenant_id: str,
|
||||
update_data: Dict[str, Any]
|
||||
) -> Optional[NotificationPreference]:
|
||||
"""Update user notification preferences"""
|
||||
try:
|
||||
preferences = await self.get_user_preferences(user_id, tenant_id)
|
||||
if not preferences:
|
||||
# Create preferences if they don't exist
|
||||
create_data = {
|
||||
"user_id": user_id,
|
||||
"tenant_id": tenant_id,
|
||||
**update_data
|
||||
}
|
||||
return await self.create_preferences(create_data)
|
||||
|
||||
# Validate specific preference fields
|
||||
self._validate_preference_updates(update_data)
|
||||
|
||||
updated_preferences = await self.update(str(preferences.id), update_data)
|
||||
|
||||
logger.info("User preferences updated",
|
||||
preferences_id=preferences.id,
|
||||
user_id=user_id,
|
||||
tenant_id=tenant_id,
|
||||
updated_fields=list(update_data.keys()))
|
||||
|
||||
return updated_preferences
|
||||
|
||||
except ValidationError:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error("Failed to update user preferences",
|
||||
user_id=user_id,
|
||||
tenant_id=tenant_id,
|
||||
error=str(e))
|
||||
raise DatabaseError(f"Failed to update preferences: {str(e)}")
|
||||
|
||||
async def get_users_with_email_enabled(
|
||||
self,
|
||||
tenant_id: str,
|
||||
notification_category: str = "alerts"
|
||||
) -> List[NotificationPreference]:
|
||||
"""Get users who have email notifications enabled for a category"""
|
||||
try:
|
||||
filters = {
|
||||
"tenant_id": tenant_id,
|
||||
"email_enabled": True
|
||||
}
|
||||
|
||||
# Add category-specific filter
|
||||
if notification_category == "alerts":
|
||||
filters["email_alerts"] = True
|
||||
elif notification_category == "marketing":
|
||||
filters["email_marketing"] = True
|
||||
elif notification_category == "reports":
|
||||
filters["email_reports"] = True
|
||||
|
||||
return await self.get_multi(filters=filters)
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to get users with email enabled",
|
||||
tenant_id=tenant_id,
|
||||
category=notification_category,
|
||||
error=str(e))
|
||||
return []
|
||||
|
||||
async def get_users_with_whatsapp_enabled(
|
||||
self,
|
||||
tenant_id: str,
|
||||
notification_category: str = "alerts"
|
||||
) -> List[NotificationPreference]:
|
||||
"""Get users who have WhatsApp notifications enabled for a category"""
|
||||
try:
|
||||
filters = {
|
||||
"tenant_id": tenant_id,
|
||||
"whatsapp_enabled": True
|
||||
}
|
||||
|
||||
# Add category-specific filter
|
||||
if notification_category == "alerts":
|
||||
filters["whatsapp_alerts"] = True
|
||||
elif notification_category == "reports":
|
||||
filters["whatsapp_reports"] = True
|
||||
|
||||
return await self.get_multi(filters=filters)
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to get users with WhatsApp enabled",
|
||||
tenant_id=tenant_id,
|
||||
category=notification_category,
|
||||
error=str(e))
|
||||
return []
|
||||
|
||||
async def get_users_with_push_enabled(
|
||||
self,
|
||||
tenant_id: str,
|
||||
notification_category: str = "alerts"
|
||||
) -> List[NotificationPreference]:
|
||||
"""Get users who have push notifications enabled for a category"""
|
||||
try:
|
||||
filters = {
|
||||
"tenant_id": tenant_id,
|
||||
"push_enabled": True
|
||||
}
|
||||
|
||||
# Add category-specific filter
|
||||
if notification_category == "alerts":
|
||||
filters["push_alerts"] = True
|
||||
elif notification_category == "reports":
|
||||
filters["push_reports"] = True
|
||||
|
||||
return await self.get_multi(filters=filters)
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to get users with push enabled",
|
||||
tenant_id=tenant_id,
|
||||
category=notification_category,
|
||||
error=str(e))
|
||||
return []
|
||||
|
||||
async def check_quiet_hours(
|
||||
self,
|
||||
user_id: str,
|
||||
tenant_id: str,
|
||||
check_time: datetime = None
|
||||
) -> bool:
|
||||
"""Check if current time is within user's quiet hours"""
|
||||
try:
|
||||
preferences = await self.get_user_preferences(user_id, tenant_id)
|
||||
if not preferences:
|
||||
return False # No quiet hours if no preferences
|
||||
|
||||
if not check_time:
|
||||
check_time = datetime.utcnow()
|
||||
|
||||
# Convert time to user's timezone (simplified - using hour comparison)
|
||||
current_hour = check_time.hour
|
||||
quiet_start = int(preferences.quiet_hours_start.split(":")[0])
|
||||
quiet_end = int(preferences.quiet_hours_end.split(":")[0])
|
||||
|
||||
# Handle quiet hours that span midnight
|
||||
if quiet_start > quiet_end:
|
||||
return current_hour >= quiet_start or current_hour < quiet_end
|
||||
else:
|
||||
return quiet_start <= current_hour < quiet_end
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to check quiet hours",
|
||||
user_id=user_id,
|
||||
tenant_id=tenant_id,
|
||||
error=str(e))
|
||||
return False
|
||||
|
||||
async def get_users_for_digest(
|
||||
self,
|
||||
tenant_id: str,
|
||||
frequency: str = "daily"
|
||||
) -> List[NotificationPreference]:
|
||||
"""Get users who want digest notifications for a frequency"""
|
||||
try:
|
||||
return await self.get_multi(
|
||||
filters={
|
||||
"tenant_id": tenant_id,
|
||||
"digest_frequency": frequency,
|
||||
"email_enabled": True
|
||||
}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to get users for digest",
|
||||
tenant_id=tenant_id,
|
||||
frequency=frequency,
|
||||
error=str(e))
|
||||
return []
|
||||
|
||||
async def can_send_email(
|
||||
self,
|
||||
user_id: str,
|
||||
tenant_id: str,
|
||||
category: str = "alerts"
|
||||
) -> Dict[str, Any]:
|
||||
"""Check if an email can be sent to a user based on their preferences"""
|
||||
try:
|
||||
preferences = await self.get_user_preferences(user_id, tenant_id)
|
||||
if not preferences:
|
||||
return {
|
||||
"can_send": True, # Default to allowing if no preferences set
|
||||
"reason": "No preferences found, using defaults"
|
||||
}
|
||||
|
||||
# Check if email is enabled
|
||||
if not preferences.email_enabled:
|
||||
return {
|
||||
"can_send": False,
|
||||
"reason": "Email notifications disabled"
|
||||
}
|
||||
|
||||
# Check category-specific settings
|
||||
category_enabled = True
|
||||
if category == "alerts" and not preferences.email_alerts:
|
||||
category_enabled = False
|
||||
elif category == "marketing" and not preferences.email_marketing:
|
||||
category_enabled = False
|
||||
elif category == "reports" and not preferences.email_reports:
|
||||
category_enabled = False
|
||||
|
||||
if not category_enabled:
|
||||
return {
|
||||
"can_send": False,
|
||||
"reason": f"Email {category} notifications disabled"
|
||||
}
|
||||
|
||||
# Check quiet hours
|
||||
if self.check_quiet_hours(user_id, tenant_id):
|
||||
return {
|
||||
"can_send": False,
|
||||
"reason": "Within user's quiet hours"
|
||||
}
|
||||
|
||||
# Check daily limit (simplified - would need to query recent notifications)
|
||||
# For now, just return the limit info
|
||||
return {
|
||||
"can_send": True,
|
||||
"max_daily_emails": preferences.max_emails_per_day,
|
||||
"language": preferences.language,
|
||||
"timezone": preferences.timezone
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to check if email can be sent",
|
||||
user_id=user_id,
|
||||
tenant_id=tenant_id,
|
||||
error=str(e))
|
||||
return {
|
||||
"can_send": True, # Default to allowing on error
|
||||
"reason": "Error checking preferences"
|
||||
}
|
||||
|
||||
async def bulk_update_preferences(
|
||||
self,
|
||||
tenant_id: str,
|
||||
update_data: Dict[str, Any],
|
||||
user_ids: List[str] = None
|
||||
) -> int:
|
||||
"""Bulk update preferences for multiple users"""
|
||||
try:
|
||||
conditions = ["tenant_id = :tenant_id"]
|
||||
params = {"tenant_id": tenant_id}
|
||||
|
||||
if user_ids:
|
||||
placeholders = ", ".join([f":user_id_{i}" for i in range(len(user_ids))])
|
||||
conditions.append(f"user_id IN ({placeholders})")
|
||||
for i, user_id in enumerate(user_ids):
|
||||
params[f"user_id_{i}"] = user_id
|
||||
|
||||
# Build update clause
|
||||
update_fields = []
|
||||
for key, value in update_data.items():
|
||||
update_fields.append(f"{key} = :update_{key}")
|
||||
params[f"update_{key}"] = value
|
||||
|
||||
params["updated_at"] = datetime.utcnow()
|
||||
update_fields.append("updated_at = :updated_at")
|
||||
|
||||
query_text = f"""
|
||||
UPDATE notification_preferences
|
||||
SET {', '.join(update_fields)}
|
||||
WHERE {' AND '.join(conditions)}
|
||||
"""
|
||||
|
||||
result = await self.session.execute(text(query_text), params)
|
||||
updated_count = result.rowcount
|
||||
|
||||
logger.info("Bulk preferences update completed",
|
||||
tenant_id=tenant_id,
|
||||
updated_count=updated_count,
|
||||
updated_fields=list(update_data.keys()))
|
||||
|
||||
return updated_count
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to bulk update preferences",
|
||||
tenant_id=tenant_id,
|
||||
error=str(e))
|
||||
raise DatabaseError(f"Bulk update failed: {str(e)}")
|
||||
|
||||
async def delete_user_preferences(
|
||||
self,
|
||||
user_id: str,
|
||||
tenant_id: str
|
||||
) -> bool:
|
||||
"""Delete user preferences (when user leaves tenant)"""
|
||||
try:
|
||||
preferences = await self.get_user_preferences(user_id, tenant_id)
|
||||
if not preferences:
|
||||
return False
|
||||
|
||||
await self.delete(str(preferences.id))
|
||||
|
||||
logger.info("User preferences deleted",
|
||||
user_id=user_id,
|
||||
tenant_id=tenant_id)
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to delete user preferences",
|
||||
user_id=user_id,
|
||||
tenant_id=tenant_id,
|
||||
error=str(e))
|
||||
raise DatabaseError(f"Failed to delete preferences: {str(e)}")
|
||||
|
||||
def _validate_preference_updates(self, update_data: Dict[str, Any]) -> None:
|
||||
"""Validate preference update data"""
|
||||
# Validate boolean fields
|
||||
boolean_fields = [
|
||||
"email_enabled", "email_alerts", "email_marketing", "email_reports",
|
||||
"whatsapp_enabled", "whatsapp_alerts", "whatsapp_reports",
|
||||
"push_enabled", "push_alerts", "push_reports"
|
||||
]
|
||||
|
||||
for field in boolean_fields:
|
||||
if field in update_data and not isinstance(update_data[field], bool):
|
||||
raise ValidationError(f"{field} must be a boolean value")
|
||||
|
||||
# Validate time format for quiet hours
|
||||
time_fields = ["quiet_hours_start", "quiet_hours_end"]
|
||||
for field in time_fields:
|
||||
if field in update_data:
|
||||
time_value = update_data[field]
|
||||
if not isinstance(time_value, str) or len(time_value) != 5 or ":" not in time_value:
|
||||
raise ValidationError(f"{field} must be in HH:MM format")
|
||||
|
||||
try:
|
||||
hour, minute = time_value.split(":")
|
||||
hour, minute = int(hour), int(minute)
|
||||
if hour < 0 or hour > 23 or minute < 0 or minute > 59:
|
||||
raise ValueError()
|
||||
except ValueError:
|
||||
raise ValidationError(f"{field} must be a valid time in HH:MM format")
|
||||
|
||||
# Validate digest frequency
|
||||
if "digest_frequency" in update_data:
|
||||
valid_frequencies = ["none", "daily", "weekly"]
|
||||
if update_data["digest_frequency"] not in valid_frequencies:
|
||||
raise ValidationError(f"digest_frequency must be one of: {valid_frequencies}")
|
||||
|
||||
# Validate max emails per day
|
||||
if "max_emails_per_day" in update_data:
|
||||
max_emails = update_data["max_emails_per_day"]
|
||||
if not isinstance(max_emails, int) or max_emails < 0 or max_emails > 100:
|
||||
raise ValidationError("max_emails_per_day must be an integer between 0 and 100")
|
||||
|
||||
# Validate language
|
||||
if "language" in update_data:
|
||||
valid_languages = ["es", "en", "fr", "de"]
|
||||
if update_data["language"] not in valid_languages:
|
||||
raise ValidationError(f"language must be one of: {valid_languages}")
|
||||
Reference in New Issue
Block a user