REFACTOR - Database logic
This commit is contained in:
259
services/notification/app/repositories/base.py
Normal file
259
services/notification/app/repositories/base.py
Normal file
@@ -0,0 +1,259 @@
|
||||
"""
|
||||
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"]:
|
||||
valid_priorities = ["low", "normal", "high", "urgent"]
|
||||
if data["priority"] 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"]:
|
||||
valid_types = ["email", "whatsapp", "push", "sms"]
|
||||
if data["type"] 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"]:
|
||||
valid_statuses = ["pending", "sent", "delivered", "failed", "cancelled"]
|
||||
if data["status"] not in valid_statuses:
|
||||
errors.append(f"Invalid status. Must be one of: {valid_statuses}")
|
||||
|
||||
return {
|
||||
"is_valid": len(errors) == 0,
|
||||
"errors": errors
|
||||
}
|
||||
Reference in New Issue
Block a user