diff --git a/apps/notification/backend/application/email_sender_hub.py b/apps/notification/backend/application/email_sender_hub.py new file mode 100644 index 0000000..507928b --- /dev/null +++ b/apps/notification/backend/application/email_sender_hub.py @@ -0,0 +1,65 @@ +from typing import List +from backend.business.email_sender_manager import EmailSenderManager + + +class EmailSenderHub: + def __init__(self): + pass + + async def get_email_senders(self, tenant_id: str): + """get email senders for tenant""" + if not tenant_id: + raise ValueError("tenant_id is required") + + email_sender_manager = EmailSenderManager() + return await email_sender_manager.get_email_senders(tenant_id) + + async def set_email_senders(self, tenant_id: str, email_senders: List[str]): + """set email senders for tenant""" + if not tenant_id: + raise ValueError("tenant_id is required") + + if not email_senders or not isinstance(email_senders, list): + raise ValueError("email_senders must be a non-empty list") + + email_sender_manager = EmailSenderManager() + return await email_sender_manager.set_email_senders(tenant_id, email_senders) + + async def add_email_senders(self, tenant_id: str, new_senders: List[str]): + """add email senders to tenant""" + if not tenant_id: + raise ValueError("tenant_id is required") + + if not new_senders or not isinstance(new_senders, list): + raise ValueError("new_senders must be a non-empty list") + + email_sender_manager = EmailSenderManager() + return await email_sender_manager.add_email_senders(tenant_id, new_senders) + + async def remove_email_senders(self, tenant_id: str, emails_to_remove: List[str]): + """remove email senders from tenant""" + if not tenant_id: + raise ValueError("tenant_id is required") + + if not emails_to_remove or not isinstance(emails_to_remove, list): + raise ValueError("emails_to_remove must be a non-empty list") + + email_sender_manager = EmailSenderManager() + return await email_sender_manager.remove_email_senders(tenant_id, emails_to_remove) + + async def clear_email_senders(self, tenant_id: str): + """clear email senders for tenant""" + if not tenant_id: + raise ValueError("tenant_id is required") + + email_sender_manager = EmailSenderManager() + return await email_sender_manager.clear_email_senders(tenant_id) + + async def delete_email_sender(self, tenant_id: str): + """delete email sender for tenant""" + if not tenant_id: + raise ValueError("tenant_id is required") + + email_sender_manager = EmailSenderManager() + return await email_sender_manager.delete_email_sender(tenant_id) + \ No newline at end of file diff --git a/apps/notification/backend/application/template_message_hub.py b/apps/notification/backend/application/template_message_hub.py new file mode 100644 index 0000000..1847f53 --- /dev/null +++ b/apps/notification/backend/application/template_message_hub.py @@ -0,0 +1,150 @@ +from typing import Dict, List, Optional +from backend.business.template_message_manager import TemplateMessageManager +from common.constants.region import UserRegion + + +class TemplateMessageHub: + def __init__(self): + pass + + async def verify_tenant_access(self, template_id: str, tenant_id: str, region: int): + """get template by tenant and template ids with region""" + if not template_id or not tenant_id: + raise ValueError("template_id and tenant_id are required") + + template_message_manager = TemplateMessageManager() + return await template_message_manager.verify_tenant_access(template_id, tenant_id, region) + +# ==================== global templates ==================== + async def create_global_template( + self, + template_id: str, + region: int, + subject: str, + body: str, + is_active: bool = True + ): + """create global template""" + if not template_id or not subject or not body: + raise ValueError("template_id, subject, and body are required") + + template_message_manager = TemplateMessageManager() + return await template_message_manager.create_global_template_from_data( + template_id=template_id, + region=region, + subject=subject, + body=body, + is_active=is_active + ) + + async def update_global_template(self, template_id: str, data: dict, region: int): + """update global template""" + if not template_id: + raise ValueError("template_id is required") + + if not data: + raise ValueError("Update data cannot be empty") + + allowed_fields = {"subject", "body", "is_active"} + invalid_fields = set(data.keys()) - allowed_fields + if invalid_fields: + raise ValueError(f"Invalid update fields: {invalid_fields}") + + template_message_manager = TemplateMessageManager() + return await template_message_manager.update_global_template(template_id, data, region) + + async def delete_global_template(self, template_id: str): + """delete global template""" + if not template_id: + raise ValueError("template_id is required") + + template_message_manager = TemplateMessageManager() + return await template_message_manager.delete_global_template(template_id) + + async def list_global_templates(self, region: int): + """list global templates""" + template_message_manager = TemplateMessageManager() + return await template_message_manager.list_global_templates(region) + +# ==================== TENANT templates ==================== + + async def list_tenant_templates(self, tenant_id: str, region: int): + """list tenant templates""" + template_message_manager = TemplateMessageManager() + return await template_message_manager.list_tenant_templates(tenant_id, region) + + async def assign_templates_to_tenant(self, tenant_id: str, template_ids: List[str], region: int): + """assign templates to tenant""" + if not template_ids: + raise ValueError("template_ids cannot be empty") + + if not tenant_id: + raise ValueError("tenant_id is required") + + template_message_manager = TemplateMessageManager() + return await template_message_manager.assign_templates_to_tenant(tenant_id, template_ids, region) + + async def create_tenant_template( + self, + template_id: str, + region: int, + subject: str, + body: str, + tenant_id: str, + is_active: bool = True + ): + """create tenant template""" + if not template_id or not subject or not body or not tenant_id: + raise ValueError("template_id, subject, body, and tenant_id are required") + + template_message_manager = TemplateMessageManager() + return await template_message_manager.create_tenant_template_from_data( + template_id=template_id, + region=region, + subject=subject, + body=body, + tenant_id=tenant_id, + is_active=is_active + ) + + + async def update_tenant_template(self, tenant_id: str, template_id: str, data: dict, region: int): + """update tenant template""" + if not template_id or not tenant_id: + raise ValueError("template_id and tenant_id are required") + + if not data: + raise ValueError("Update data cannot be empty") + + allowed_fields = {"subject", "body", "is_active"} + invalid_fields = set(data.keys()) - allowed_fields + if invalid_fields: + raise ValueError(f"Invalid update fields: {invalid_fields}") + + template_message_manager = TemplateMessageManager() + return await template_message_manager.update_tenant_template(tenant_id, template_id, data, region) + + async def delete_tenant_template(self, tenant_id: str, template_id: str, region: int): + """delete tenant template""" + if not template_id or not tenant_id: + raise ValueError("template_id and tenant_id are required") + + template_message_manager = TemplateMessageManager() + return await template_message_manager.delete_tenant_template(tenant_id, template_id, region) + + async def render_template( + self, + tenant_id: str, + template_id: str, + properties: dict, + region: int + ): + """render template""" + if not template_id or not tenant_id: + raise ValueError("template_id and tenant_id are required") + + if not properties: + raise ValueError("properties cannot be empty") + + template_message_manager = TemplateMessageManager() + return await template_message_manager.render_template(tenant_id, template_id, properties, region) diff --git a/apps/notification/backend/business/email_sender_manager.py b/apps/notification/backend/business/email_sender_manager.py new file mode 100644 index 0000000..da8b5df --- /dev/null +++ b/apps/notification/backend/business/email_sender_manager.py @@ -0,0 +1,127 @@ +from typing import List +from backend.services.email_sender_service import EmailSenderService +from backend.models.models import EmailSenderDoc +from common.log.module_logger import ModuleLogger +from datetime import datetime, timezone + + +class EmailSenderManager: + def __init__(self): + self.email_sender_service = EmailSenderService() + self.module_logger = ModuleLogger(sender_id="EmailSenderManager") + + async def get_email_senders(self, tenant_id: str): + """get email senders for tenant""" + email_senders = await self.email_sender_service.get_email_senders(tenant_id) + + await self.module_logger.log_info( + info="Email senders retrieved", + properties={ + "tenant_id": tenant_id, + "sender_count": len(email_senders) + } + ) + + return email_senders + + async def set_email_senders(self, tenant_id: str, email_senders: List[str]): + """set email senders for tenant""" + if not email_senders: + raise ValueError("Email senders list cannot be empty") + + for email in email_senders: + if not self._is_valid_email(email): + raise ValueError(f"Invalid email format: {email}") + + result = await self.email_sender_service.set_email_senders(tenant_id, email_senders) + + await self.module_logger.log_info( + info="Email senders set", + properties={ + "tenant_id": tenant_id, + "sender_count": len(email_senders) + } + ) + + return result + + async def add_email_senders(self, tenant_id: str, new_senders: List[str]): + """add email senders to tenant""" + if not new_senders: + raise ValueError("New senders list cannot be empty") + + for email in new_senders: + if not self._is_valid_email(email): + raise ValueError(f"Invalid email format: {email}") + + result = await self.email_sender_service.add_email_senders(tenant_id, new_senders) + + await self.module_logger.log_info( + info="Email senders added", + properties={ + "tenant_id": tenant_id, + "new_sender_count": len(new_senders), + "success": result.get("success", False) + } + ) + + return result + + async def remove_email_senders(self, tenant_id: str, emails_to_remove: List[str]): + """remove email senders from tenant""" + if not emails_to_remove: + raise ValueError("Emails to remove list cannot be empty") + + result = await self.email_sender_service.remove_email_senders(tenant_id, emails_to_remove) + + await self.module_logger.log_info( + info="Email senders removed", + properties={ + "tenant_id": tenant_id, + "removed_count": len(emails_to_remove), + "success": result.get("success", False) + } + ) + + return result + + async def clear_email_senders(self, tenant_id: str): + """clear email senders for tenant""" + result = await self.email_sender_service.clear_email_senders(tenant_id) + + await self.module_logger.log_info( + info="Email senders cleared", + properties={ + "tenant_id": tenant_id, + "success": result.get("success", False) + } + ) + + return result + + async def delete_email_sender(self, tenant_id: str): + """delete email sender for tenant""" + result = await self.email_sender_service.delete_email_sender(tenant_id) + + await self.module_logger.log_info( + info="Email sender configuration deleted", + properties={ + "tenant_id": tenant_id, + "success": result.get("success", False) + } + ) + + return result + + def _is_valid_email(self, email: str) -> bool: + """validate email format""" + # TODO: add more complex email format validation if needed + import re + pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$' + return re.match(pattern, email) is not None + + + + + + \ No newline at end of file diff --git a/apps/notification/backend/business/template_message_manager.py b/apps/notification/backend/business/template_message_manager.py new file mode 100644 index 0000000..a0db479 --- /dev/null +++ b/apps/notification/backend/business/template_message_manager.py @@ -0,0 +1,217 @@ +from typing import Optional, List, Dict +from backend.services.template_message_service import TemplateMessageService +from backend.models.models import MessageTemplateDoc +from common.log.module_logger import ModuleLogger +from datetime import datetime, timezone +from common.constants.region import UserRegion + + +class TemplateMessageManager: + def __init__(self): + self.template_message_service = TemplateMessageService() + self.module_logger = ModuleLogger(sender_id="TemplateMessageManager") + + async def verify_tenant_access(self, template_id: str, tenant_id: str, region: int) -> Optional[MessageTemplateDoc]: + """get template by tenant and template ids with region""" + if not template_id or not tenant_id: + raise ValueError("template_id and tenant_id are required") + + template = await self.template_message_service.verify_tenant_access(template_id, tenant_id, region) + await self.module_logger.log_info( + info="Template retrieved by tenant and ID", + properties={"template_id": template_id, "tenant_id": tenant_id, "region": region} + ) + + return template +# ==================== global templates ==================== + async def create_global_template_from_data( + self, + template_id: str, + region: int, + subject: str, + body: str, + is_active: bool = True + ): + """create global template from data""" + template = MessageTemplateDoc( + template_id=template_id, + tenant_id=None, + region=region, + subject=subject, + body=body, + is_active=is_active, + created_at=datetime.now(timezone.utc) + ) + + result = await self.template_message_service.create_global_template(template) + + await self.module_logger.log_info( + info="Global template created", + properties={"template_id": template_id, "region": region} + ) + + return result + + async def update_global_template(self, template_id: str, data: dict, region: int): + """update global template""" + if not data.get("subject") and not data.get("body"): + raise ValueError("At least subject or body must be provided") + + result = await self.template_message_service.update_global_template(template_id, data, region) + + await self.module_logger.log_info( + info="Global template updated", + properties={"template_id": template_id, "updated_fields": list(data.keys())} + ) + + return result + + async def delete_global_template(self, template_id: str): + """delete global template""" + + result = await self.template_message_service.delete_global_template(template_id) + + await self.module_logger.log_info( + info="Global template deleted", + properties={"template_id": template_id} + ) + + return result + + async def list_global_templates(self, region: int): + """list global templates""" + templates = await self.template_message_service.list_global_templates(region) + + formatted_templates = [] + for template in templates: + formatted_templates.append({ + "id": str(template.id), + "template_id": template.template_id, + "subject": template.subject, + "region": template.region.value if hasattr(template.region, 'value') else template.region, + "is_active": template.is_active, + "created_at": template.created_at.isoformat() if template.created_at else None + }) + + return formatted_templates + +# ==================== TENANT templates ==================== + + async def list_tenant_templates(self, tenant_id: str, region: int): + """list tenant templates""" + + templates = await self.template_message_service.list_tenant_templates(tenant_id, region) + + formatted_templates = [] + for template in templates: + formatted_templates.append({ + "id": str(template.id), + "template_id": template.template_id, + "subject": template.subject, + "region": template.region.value if hasattr(template.region, 'value') else template.region, + "is_active": template.is_active, + "created_at": template.created_at.isoformat() if template.created_at else None + }) + + return formatted_templates + + async def assign_templates_to_tenant(self, tenant_id: str, template_ids: List[str], region: int): + """assign templates to tenant""" + if not template_ids: + raise ValueError("Template IDs cannot be empty") + + results = await self.template_message_service.assign_template_to_tenant(tenant_id, template_ids, region) + + success_count = sum(1 for r in results if r.get("success")) + await self.module_logger.log_info( + info="Templates assigned to tenant", + properties={ + "tenant_id": tenant_id, + "region": region, + "total_requested": len(template_ids), + "success_count": success_count + } + ) + + return results + + async def create_tenant_template_from_data( + self, + template_id: str, + region: int, + subject: str, + body: str, + tenant_id: str, + is_active: bool = True + ): + """create tenant template from data""" + template = MessageTemplateDoc( + template_id=template_id, + tenant_id=tenant_id, + region=region, + subject=subject, + body=body, + is_active=is_active, + created_at=datetime.now(timezone.utc) + ) + + result = await self.template_message_service.create_tenant_template(tenant_id, template) + + await self.module_logger.log_info( + info="Tenant template created", + properties={ + "tenant_id": tenant_id, + "template_id": template_id, + "region": region + } + ) + + return result + + async def update_tenant_template(self, tenant_id: str, template_id: str, data: dict, region: int): + """update tenant template""" + if not data.get("subject") and not data.get("body"): + raise ValueError("At least subject or body must be provided") + + result = await self.template_message_service.update_tenant_template(tenant_id, template_id, data, region) + + await self.module_logger.log_info( + info="Tenant template updated", + properties={ + "tenant_id": tenant_id, + "template_id": template_id, + "region": region, + "updated_fields": list(data.keys()) + } + ) + + return result + async def delete_tenant_template(self, tenant_id: str, template_id: str, region: int): + """delete tenant template""" + result = await self.template_message_service.delete_tenant_template(tenant_id, template_id, region) + + await self.module_logger.log_info( + info="Tenant template deleted", + properties={ + "tenant_id": tenant_id, + "template_id": template_id + } + ) + + return result + + async def render_template(self, tenant_id: str, template_id: str, properties: dict, region: int): + """render template""" + result = await self.template_message_service.render_template(tenant_id, template_id, properties, region) + + await self.module_logger.log_info( + info="Template rendered", + properties={ + "tenant_id": tenant_id, + "template_id": template_id, + "properties_count": len(properties) + } + ) + + return result + \ No newline at end of file diff --git a/apps/notification/backend/infra/email_sender_handler.py b/apps/notification/backend/infra/email_sender_handler.py new file mode 100644 index 0000000..8283613 --- /dev/null +++ b/apps/notification/backend/infra/email_sender_handler.py @@ -0,0 +1,176 @@ +from typing import List, Optional +from backend.models.models import EmailSenderDoc +from common.log.module_logger import ModuleLogger + + +class EmailSenderHandler: + def __init__(self): + self.module_logger = ModuleLogger(sender_id="EmailSenderHandler") + + async def get_email_senders(self, tenant_id: str) -> List[str]: + """get email senders for tenant""" + try: + doc = await EmailSenderDoc.find_one({"tenant_id": tenant_id, "is_active": True}) + return doc.email_senders if doc else [] + except Exception as e: + await self.module_logger.log_error( + error="Failed to get email senders", + properties={"tenant_id": tenant_id, "error": str(e)} + ) + return [] + + async def set_email_senders(self, tenant_id: str, email_senders: List[str]): + """set email senders for tenant""" + try: + doc = await EmailSenderDoc.find_one({"tenant_id": tenant_id}) + if doc: + await doc.set({"email_senders": email_senders}) + await self.module_logger.log_info( + info="Email senders set in database", + properties={ + "tenant_id": tenant_id, + "sender_count": len(email_senders) + } + ) + return {"success": True, "email_senders": email_senders} + else: + doc = EmailSenderDoc(tenant_id=tenant_id, email_senders=email_senders) + await doc.create() + await self.module_logger.log_info( + info="Email sender doc created with senders", + properties={ + "tenant_id": tenant_id, + "sender_count": len(email_senders) + } + ) + return {"success": True, "email_senders": doc.email_senders} + except Exception as e: + await self.module_logger.log_error( + error="Failed to set email senders", + properties={ + "tenant_id": tenant_id, + "error": str(e) + } + ) + raise + + async def add_email_senders(self, tenant_id: str, new_senders: List[str]): + """add email senders to tenant""" + try: + if not new_senders or not isinstance(new_senders, list): + return {"success": False, "msg": "No sender provided"} + + doc = await EmailSenderDoc.find_one({"tenant_id": tenant_id, "is_active": True}) + if doc: + original_set = set(doc.email_senders) + new_set = set(new_senders) + to_add = new_set - original_set + if not to_add: + return {"success": False, "msg": "All senders already exist"} + updated_list = list(original_set | new_set) + await doc.set({"email_senders": updated_list}) + await self.module_logger.log_info( + info="Email senders added to database", + properties={ + "tenant_id": tenant_id, + "added_count": len(to_add), + "total_count": len(updated_list) + } + ) + return {"success": True, "email_senders": updated_list} + else: + doc = EmailSenderDoc(tenant_id=tenant_id, email_senders=new_senders) + await doc.create() + await self.module_logger.log_info( + info="Email sender doc created with new senders", + properties={ + "tenant_id": tenant_id, + "sender_count": len(new_senders) + } + ) + return {"success": True, "email_senders": doc.email_senders} + except Exception as e: + await self.module_logger.log_error( + error="Failed to add email senders", + properties={ + "tenant_id": tenant_id, + "error": str(e) + } + ) + raise + + async def remove_email_senders(self, tenant_id: str, emails_to_remove: List[str]): + """remove email senders from tenant""" + try: + doc = await EmailSenderDoc.find_one({"tenant_id": tenant_id, "is_active": True}) + if not doc or not doc.email_senders: + return {"success": False, "msg": "No sender found"} + + original_count = len(doc.email_senders) + doc.email_senders = [s for s in doc.email_senders if s not in emails_to_remove] + if len(doc.email_senders) == original_count: + return {"success": False, "msg": "No sender matched for removal"} + + await doc.set({"email_senders": doc.email_senders}) + await self.module_logger.log_info( + info="Email senders removed from database", + properties={ + "tenant_id": tenant_id, + "removed_count": original_count - len(doc.email_senders), + "remaining_count": len(doc.email_senders) + } + ) + return {"success": True, "remaining": doc.email_senders} + except Exception as e: + await self.module_logger.log_error( + error="Failed to remove email senders", + properties={ + "tenant_id": tenant_id, + "error": str(e) + } + ) + raise + + async def clear_email_senders(self, tenant_id: str): + """clear up email senders for tenant""" + try: + doc = await EmailSenderDoc.find_one({"tenant_id": tenant_id, "is_active": True}) + if doc: + await doc.set({"email_senders": []}) + await self.module_logger.log_info( + info="Email senders cleared from database", + properties={"tenant_id": tenant_id} + ) + return {"success": True} + return {"success": False, "msg": "No sender config found"} + except Exception as e: + await self.module_logger.log_error( + error="Failed to clear email senders", + properties={ + "tenant_id": tenant_id, + "error": str(e) + } + ) + raise + + async def delete_email_sender(self, tenant_id: str): + """delete email sender for tenant""" + try: + doc = await EmailSenderDoc.find_one({"tenant_id": tenant_id}) + if doc: + await doc.delete() + await self.module_logger.log_info( + info="Email sender configuration deleted from database", + properties={"tenant_id": tenant_id} + ) + return {"success": True} + return {"success": False, "msg": "No sender config found"} + except Exception as e: + await self.module_logger.log_error( + error="Failed to delete email sender", + properties={ + "tenant_id": tenant_id, + "error": str(e) + } + ) + raise diff --git a/apps/notification/backend/infra/template_message_handler.py b/apps/notification/backend/infra/template_message_handler.py new file mode 100644 index 0000000..6c12048 --- /dev/null +++ b/apps/notification/backend/infra/template_message_handler.py @@ -0,0 +1,396 @@ +from backend.models.models import MessageTemplateDoc +from common.log.module_logger import ModuleLogger +from datetime import datetime +from typing import List, Optional +from common.constants.region import UserRegion + + +class TemplateMessageHandler: + def __init__(self): + self.module_logger = ModuleLogger(sender_id="TemplateMessageHandler") + async def verify_tenant_access(self, template_id: str, tenant_id: str, region: int) -> Optional[MessageTemplateDoc]: + """get template by tenant and template ids with region""" + try: + template = await MessageTemplateDoc.find_one({ + "template_id": template_id, + "tenant_id": tenant_id, + "region": region + }) + + await self.module_logger.log_info( + info="Tenant template retrieved from database", + properties={ + "template_id": template_id, + "tenant_id": tenant_id, + "region": region + } + ) + return template + except Exception as e: + await self.module_logger.log_error( + error="Failed to get tenant template from database", + properties={ + "template_id": template_id, + "tenant_id": tenant_id, + "region": region, + "error": str(e) + } + ) + raise + + +# ==================== global templates ==================== + async def create_global_template(self, template: MessageTemplateDoc) -> MessageTemplateDoc: + """create global template""" + try: + await template.create() + await self.module_logger.log_info( + info="Template created in database", + properties={ + "template_id": template.template_id, + "tenant_id": template.tenant_id, + "region": template.region + } + ) + return template + except Exception as e: + await self.module_logger.log_error( + error="Failed to create template in database", + properties={ + "template_id": template.template_id, + "error": str(e) + } + ) + raise + + async def update_global_template(self, template_id: str, data: dict, region: int) -> dict: + """update global template""" + try: + template = await MessageTemplateDoc.find_one({ + "template_id": template_id, + "tenant_id": None, + "region": region + }) + + if not template: + raise ValueError(f"Template not found for template_id: {template_id}, region: {data.get('region')}") + + data["updated_at"] = datetime.utcnow() + + # remove fields that are not allowed to be updated + if "id" in data: + del data["id"] + if "template_id" in data: + del data["template_id"] + if "tenant_id" in data: + del data["tenant_id"] + if "region" in data: + del data["region"] + + await template.set(data) + + await self.module_logger.log_info( + info="Template updated in database", + properties={ + "template_id": template_id, + "tenant_id": data.get("tenant_id"), + "region": data.get("region"), + "updated_fields": list(data.keys()) + } + ) + return {"success": True} + except Exception as e: + await self.module_logger.log_error( + error="Failed to update template in database", + properties={ + "template_id": template_id, + "region": data.get("region"), + "error": str(e) + } + ) + raise + + async def delete_global_template(self, template_id: str) -> dict: + """delete global template""" + try: + # find all global templates named as template_id + templates = await MessageTemplateDoc.find({ + "template_id": template_id, + "tenant_id": None + }).to_list() + + if not templates: + raise ValueError("Global template not found") + + # delete all found templates + deleted_count = 0 + for template in templates: + await template.delete() + deleted_count += 1 + + await self.module_logger.log_info( + info="Global templates deleted from database", + properties={ + "template_id": template_id, + "deleted_count": deleted_count, + "total_found": len(templates) + } + ) + return {"success": True, "deleted_count": deleted_count} + except Exception as e: + await self.module_logger.log_error( + error="Failed to delete global templates from database", + properties={ + "template_id": template_id, + "error": str(e) + } + ) + raise + + async def list_global_templates(self, region: int) -> List[MessageTemplateDoc]: + """list global templates""" + try: + templates = await MessageTemplateDoc.find({ + "tenant_id": None, + "region": region + }).to_list() + + await self.module_logger.log_info( + info="Global templates retrieved from database", + properties={"region": region, "count": len(templates)} + ) + return templates + except Exception as e: + await self.module_logger.log_error( + error="Failed to get global templates from database", + properties={ + "region": region, + "error": str(e) + } + ) + raise + + async def find_global_template(self, template_id: str, region: int) -> Optional[MessageTemplateDoc]: + """find global template""" + try: + template = await MessageTemplateDoc.find_one({ + "template_id": template_id, + "tenant_id": None, + "region": region + }) + + await self.module_logger.log_info( + info="Global template retrieved from database", + properties={ + "template_id": template_id, + "region": region + } + ) + return template + except Exception as e: + await self.module_logger.log_error( + error="Failed to get global template from database", + properties={ + "template_id": template_id, + "region": region, + "error": str(e) + } + ) + raise +# ==================== tenant templates ==================== + async def list_tenant_templates(self, tenant_id: str, region: int) -> List[MessageTemplateDoc]: + """list tenant templates""" + try: + templates = await MessageTemplateDoc.find({ + "tenant_id": tenant_id, + "region": region + }).to_list() + + await self.module_logger.log_info( + info="Tenant templates retrieved from database", + properties={ + "tenant_id": tenant_id, + "region": region, + "count": len(templates) + } + ) + + return templates + except Exception as e: + await self.module_logger.log_error( + error="Failed to get tenant templates from database", + properties={ + "tenant_id": tenant_id, + "region": region, + "error": str(e) + } + ) + raise + + async def find_tenant_template(self, tenant_id: str, template_id: str, region: int) -> Optional[MessageTemplateDoc]: + """find tenant template""" + try: + template = await MessageTemplateDoc.find_one({ + "template_id": template_id, + "tenant_id": tenant_id, + "region": region + }) + + await self.module_logger.log_info( + info="Tenant template retrieved from database", + properties={ + "template_id": template_id, + "tenant_id": tenant_id, + "region": region + } + ) + return template + except Exception as e: + await self.module_logger.log_error( + error="Failed to get tenant template from database", + properties={ + "template_id": template_id, + "region": region, + "error": str(e) + } + ) + raise + + async def create_tenant_template(self, tenant_id: str, template: MessageTemplateDoc) -> MessageTemplateDoc: + """create tenant template""" + try: + await template.create() + await self.module_logger.log_info( + info="Template created in database", + properties={ + "template_id": template.template_id, + "tenant_id": template.tenant_id, + "region": template.region + } + ) + return template + except Exception as e: + await self.module_logger.log_error( + error="Failed to create template in database", + properties={ + "template_id": template.template_id, + "error": str(e) + } + ) + raise + + async def update_tenant_template(self, tenant_id: str, template_id: str, data: dict, region: int) -> dict: + """update template""" + try: + template = await MessageTemplateDoc.find_one({ + "template_id": template_id, + "tenant_id": tenant_id, + "region": region + }) + + if not template: + raise ValueError(f"Template not found for template_id: {template_id}, tenant_id: {data.get('tenant_id')}, region: {data.get('region')}") + + data["updated_at"] = datetime.utcnow() + + # remove fields that are not allowed to be updated + if "id" in data: + del data["id"] + if "template_id" in data: + del data["template_id"] + if "tenant_id" in data: + del data["tenant_id"] + if "region" in data: + del data["region"] + + await template.set(data) + + await self.module_logger.log_info( + info="Template updated in database", + properties={ + "template_id": template_id, + "tenant_id": data.get("tenant_id"), + "region": data.get("region"), + "updated_fields": list(data.keys()) + } + ) + return {"success": True} + except Exception as e: + await self.module_logger.log_error( + error="Failed to update template in database", + properties={ + "template_id": template_id, + "tenant_id": data.get("tenant_id"), + "region": data.get("region"), + "error": str(e) + } + ) + raise + + async def delete_tenant_template(self, tenant_id: str, template_id: str, region: int) -> dict: + """delete tenant template""" + try: + template = await MessageTemplateDoc.find_one({ + "template_id": template_id, + "tenant_id": tenant_id, + "region": region + }) + if not template: + raise ValueError("Template not found") + + await template.delete() + + await self.module_logger.log_info( + info="Template deleted from database", + properties={"template_id": template_id} + ) + return {"success": True} + except Exception as e: + await self.module_logger.log_error( + error="Failed to delete template from database", + properties={ + "template_id": template_id, + "error": str(e) + } + ) + raise + + async def render_template(self, template: MessageTemplateDoc, properties: dict) -> dict: + """render template""" + try: + subject = template.subject.format(**properties) + body = template.body.format(**properties) + + await self.module_logger.log_info( + info="Template rendered", + properties={ + "template_id": template.template_id, + "tenant_id": template.tenant_id, + "region": template.region, + "properties_count": len(properties) + } + ) + + return {"subject": subject, "body": body} + except KeyError as e: + await self.module_logger.log_error( + error="Missing template parameter", + properties={ + "template_id": template.template_id, + "tenant_id": template.tenant_id, + "region": template.region, + "missing_parameter": str(e) + } + ) + raise ValueError(f"Missing template parameter: {e}") + except Exception as e: + await self.module_logger.log_error( + error="Template rendering error", + properties={ + "template_id": template.template_id, + "tenant_id": template.tenant_id, + "region": template.region, + "error": str(e) + } + ) + raise ValueError(f"Template rendering error: {str(e)}") diff --git a/apps/notification/backend/services/email_sender_service.py b/apps/notification/backend/services/email_sender_service.py index d4fa881..121ee0c 100644 --- a/apps/notification/backend/services/email_sender_service.py +++ b/apps/notification/backend/services/email_sender_service.py @@ -1,66 +1,59 @@ +from typing import List +from backend.infra.email_sender_handler import EmailSenderHandler from backend.models.models import EmailSenderDoc + class EmailSenderService: - # get the email sender list for the tenant - async def get_email_sender(self, tenant_id: str): - doc = await EmailSenderDoc.find_one({"tenant_id": tenant_id, "is_active": True}) - return doc.email_senders if doc else [] + def __init__(self): + self.email_sender_handler = EmailSenderHandler() + + async def get_email_senders(self, tenant_id: str) -> List[str]: + """get email senders for tenant""" + if not tenant_id: + raise ValueError("tenant_id is required") + + return await self.email_sender_handler.get_email_senders(tenant_id) - # set the email sender list for the tenant - async def set_email_sender(self, tenant_id: str, email_senders: list): - doc = await EmailSenderDoc.find_one({"tenant_id": tenant_id}) - if doc: - await doc.set({"email_senders": email_senders}) - return {"success": True, "email_senders": doc.email_senders} - else: - doc = EmailSenderDoc(tenant_id=tenant_id, email_senders=email_senders) - await doc.create() - return {"success": True, "email_senders": doc.email_senders} + async def set_email_senders(self, tenant_id: str, email_senders: List[str]): + """set email senders for tenant""" + if not tenant_id: + raise ValueError("tenant_id is required") + + if not email_senders or not isinstance(email_senders, list): + raise ValueError("email_senders must be a non-empty list") + + return await self.email_sender_handler.set_email_senders(tenant_id, email_senders) - # add new email senders to the tenant - async def add_email_senders(self, tenant_id: str, new_senders: list): + async def add_email_senders(self, tenant_id: str, new_senders: List[str]): + """add email senders to tenant""" + if not tenant_id: + raise ValueError("tenant_id is required") + if not new_senders or not isinstance(new_senders, list): return {"success": False, "msg": "No sender provided"} - - doc = await EmailSenderDoc.find_one({"tenant_id": tenant_id, "is_active": True}) - if doc: - original_set = set(doc.email_senders) - new_set = set(new_senders) - to_add = new_set - original_set - if not to_add: - return {"success": False, "msg": "All senders already exist"} - updated_list = list(original_set | new_set) - await doc.set({"email_senders": updated_list}) - return {"success": True, "email_senders": updated_list} - else: - result = await self.set_email_sender(tenant_id, new_senders) - return result - - # remove the email sender from the list - async def remove_email_senders(self, tenant_id: str, emails_to_remove: list): - doc = await EmailSenderDoc.find_one({"tenant_id": tenant_id, "is_active": True}) - if not doc or not doc.email_senders: - return {"success": False, "msg": "No sender found"} - original_count = len(doc.email_senders) - doc.email_senders = [s for s in doc.email_senders if s not in emails_to_remove] - if len(doc.email_senders) == original_count: - return {"success": False, "msg": "No sender matched for removal"} - await doc.set({"email_senders": doc.email_senders}) - return {"success": True, "remaining": doc.email_senders} + return await self.email_sender_handler.add_email_senders(tenant_id, new_senders) + + async def remove_email_senders(self, tenant_id: str, emails_to_remove: List[str]): + """remove email senders from tenant""" + if not tenant_id: + raise ValueError("tenant_id is required") + + if not emails_to_remove or not isinstance(emails_to_remove, list): + raise ValueError("emails_to_remove must be a non-empty list") + + return await self.email_sender_handler.remove_email_senders(tenant_id, emails_to_remove) - # clear the email sender list for the tenant async def clear_email_senders(self, tenant_id: str): - doc = await EmailSenderDoc.find_one({"tenant_id": tenant_id, "is_active": True}) - if doc: - await doc.set({"email_senders": []}) - return {"success": True} - return {"success": False, "msg": "No sender config found"} + """clear email senders for tenant""" + if not tenant_id: + raise ValueError("tenant_id is required") + + return await self.email_sender_handler.clear_email_senders(tenant_id) - # delete the email sender list for the tenant async def delete_email_sender(self, tenant_id: str): - doc = await EmailSenderDoc.find_one({"tenant_id": tenant_id}) - if doc: - await doc.delete() - return {"success": True} - return {"success": False, "msg": "No sender config found"} \ No newline at end of file + """delete email sender for tenant""" + if not tenant_id: + raise ValueError("tenant_id is required") + + return await self.email_sender_handler.delete_email_sender(tenant_id) diff --git a/apps/notification/backend/services/template_message_service.py b/apps/notification/backend/services/template_message_service.py index 046e69d..c2fba18 100644 --- a/apps/notification/backend/services/template_message_service.py +++ b/apps/notification/backend/services/template_message_service.py @@ -1,112 +1,363 @@ +from typing import List, Optional +from backend.infra.template_message_handler import TemplateMessageHandler from backend.models.models import MessageTemplateDoc +from common.log.module_logger import ModuleLogger from datetime import datetime +from common.constants.region import UserRegion + class TemplateMessageService: - ## operations for global templates, conducted by platform admin - # Create global template operated by platform admin - async def create_global_template(self, template: MessageTemplateDoc): - template.tenant_id = None - return await template.create() + def __init__(self): + self.template_message_handler = TemplateMessageHandler() + self.module_logger = ModuleLogger(sender_id="TemplateMessageService") - # Update global template operated by platform admin - async def update_global_template(self, id: str, data: dict): - template = await MessageTemplateDoc.get(id) - if not template or template.tenant_id is not None: - raise PermissionError("Not a global template") - await template.set(data) - return {"success": True} - - # Delete global template operated by platform admin - async def delete_global_template(self, id: str): - template = await MessageTemplateDoc.get(id) - if not template or template.tenant_id is not None: - raise PermissionError("Not a global template") - await template.delete() - return {"success": True} - - - # Just for Facilitating later addition of permissions, logging, and exception handling, etc. - async def get_template(self, template_id, tenant_id, region): - return await MessageTemplateDoc.find_one({ - "template_id": template_id, - "tenant_id": tenant_id, - "region": region, - "is_active": True - }) - - # Query global template - async def list_global_templates(self, region): - return await MessageTemplateDoc.find({"tenant_id": None, "region": region}).to_list() - - # Look up templates belonging to the tenant - async def list_tenant_templates(self, tenant_id, region): - return await MessageTemplateDoc.find({"tenant_id": tenant_id, "region": region}).to_list() - - # Tenant selects template(s) (copies one or more global templates as their own) - async def assign_template_to_tenant(self, template_ids: list, region, tenant_id: str): - results = [] - # assign a global template to a tenant - for template_id in template_ids: - # lookup global template - global_template = await MessageTemplateDoc.find_one({ - "template_id": template_id, - "tenant_id": None, - "region": region - }) - if not global_template: - results.append({"template_id": template_id, "success": False, "msg": "Template not found"}) - continue - - # check if the tenant already has the template - existing = await MessageTemplateDoc.find_one({ - "template_id": template_id, - "tenant_id": tenant_id, - "region": region - }) - if existing: - results.append({"template_id": template_id, "success": False, "msg": "Template already assigned"}) - continue - - # copy the template - new_template = MessageTemplateDoc( - template_id=global_template.template_id, - tenant_id=tenant_id, - region=global_template.region, - subject=global_template.subject, - body=global_template.body, - created_at=datetime.utcnow() - ) - await new_template.create() - results.append({"template_id": template_id, "success": True, "template_db_id": str(new_template.id)}) - return results - - # Custom templates by tenant - async def create_template(self, template:MessageTemplateDoc, tenant_id: str): - template.tenant_id = tenant_id - return await template.create() - - # Update template under certain tenant - async def update_template(self, id: str, tenant_id: str, data: dict): - template = await MessageTemplateDoc.get(id) - if not template or template.tenant_id != tenant_id: - raise PermissionError("Forbidden") - await template.set(data) - return {"success": True} - - # Delete template under certain tenant - async def delete_template(self, id: str, tenant_id: str): - template = await MessageTemplateDoc.get(id) - if not template or template.tenant_id != tenant_id: - raise PermissionError("Forbidden") - await template.delete() - return {"success": True} - - # Render template - async def render_template(self, template: MessageTemplateDoc, properties: dict): - # Use properties to replace the placeholders in the template, and return the rendered body + async def verify_tenant_access(self, template_id: str, tenant_id: str, region: int): + """verify tenant access""" try: - subject = template.subject.format(**properties) - body = template.body.format(**properties) - except KeyError as e: - raise ValueError(f"Missing template parameter: {e}") - return subject, body \ No newline at end of file + result = await self.template_message_handler.verify_tenant_access(template_id, tenant_id, region) + await self.module_logger.log_info( + info="Tenant access verified", + properties={ + "template_id": template_id, + "tenant_id": tenant_id, + "region": region + } + ) + return result + except Exception as e: + await self.module_logger.log_error( + error="Failed to verify tenant access", + properties={ + "template_id": template_id, + "tenant_id": tenant_id, + "region": region, + "error": str(e) + } + ) + raise + +# ==================== global templates ==================== + async def create_global_template(self, template: MessageTemplateDoc): + """create global template""" + try: + # Check if template already exists with same template_id and region + existing_template = await self.template_message_handler.find_global_template(template.template_id, template.region) + if existing_template: + raise ValueError(f"Global template with template_id '{template.template_id}' and region '{template.region}' already exists") + + result = await self.template_message_handler.create_global_template(template) + await self.module_logger.log_info( + info="Global template created", + properties={ + "template_id": template.template_id, + "region": template.region + } + ) + return result + except ValueError as e: + # Re-raise ValueError for duplicate template + raise + except Exception as e: + await self.module_logger.log_error( + error="Failed to create global template", + properties={ + "template_id": template.template_id, + "error": str(e) + } + ) + raise + + async def update_global_template(self, template_id: str, data: dict, region: int): + """update global template""" + try: + # check if template exists + template = await self.template_message_handler.find_global_template(template_id, region) + if not template: + raise ValueError(f"Global template not found for template_id: {template_id}, region: {region}") + + result = await self.template_message_handler.update_global_template(template_id, data, region) + await self.module_logger.log_info( + info="Global template updated", + properties={ + "template_id": template_id, + "region": region, + "updated_fields": list(data.keys()) + } + ) + return result + except Exception as e: + await self.module_logger.log_error( + error="Failed to update global template", + properties={ + "template_id": template_id, + "region": region, + "error": str(e) + } + ) + raise + + async def delete_global_template(self, template_id: str): + """delete global template""" + try: + + result = await self.template_message_handler.delete_global_template(template_id) + await self.module_logger.log_info( + info="Global template deleted", + properties={ + "template_id": template_id, + "deleted_count": result.get("deleted_count", 0) if result else 0 + } + ) + return result + except Exception as e: + await self.module_logger.log_error( + error="Failed to delete global template", + properties={ + "template_id": template_id, + "error": str(e) + } + ) + raise + + async def list_global_templates(self, region: int): + """list global templates""" + try: + result = await self.template_message_handler.list_global_templates(region) + await self.module_logger.log_info( + info="Global templates listed", + properties={ + "region": region, + "count": len(result) if result else 0 + } + ) + return result + except Exception as e: + await self.module_logger.log_error( + error="Failed to list global templates", + properties={ + "region": region, + "error": str(e) + } + ) + raise +# ==================== TENANT templates ==================== + async def list_tenant_templates(self, tenant_id: str, region: int): + """list tenant templates""" + try: + result = await self.template_message_handler.list_tenant_templates(tenant_id, region) + await self.module_logger.log_info( + info="Tenant templates listed", + properties={ + "tenant_id": tenant_id, + "region": region, + "count": len(result) if result else 0 + } + ) + return result + except Exception as e: + await self.module_logger.log_error( + error="Failed to list tenant templates", + properties={ + "tenant_id": tenant_id, + "region": region, + "error": str(e) + } + ) + raise + + async def assign_template_to_tenant(self, tenant_id: str, template_ids: List[str], region: int): + """assign templates to tenant""" + try: + results = [] + + for template_id in template_ids: + try: + global_template = await self.template_message_handler.find_global_template(template_id, region) + if not global_template: + results.append({ + "template_id": template_id, + "success": False, + "msg": "Global template not found" + }) + continue + + # check if tenant already has this template + existing = await self.template_message_handler.find_tenant_template(tenant_id, template_id, region) + if existing: + results.append({ + "template_id": template_id, + "success": False, + "msg": "Template already assigned to tenant" + }) + continue + + # copy template to tenant, use unique template_id + tenant_template_id = f"{global_template.template_id}_tenant_{tenant_id}" + new_template = MessageTemplateDoc( + template_id=tenant_template_id, + tenant_id=tenant_id, + region=global_template.region, + subject=global_template.subject, + body=global_template.body, + is_active=global_template.is_active, + created_at=datetime.utcnow() + ) + + await self.template_message_handler.create_tenant_template(tenant_id, new_template) + results.append({ + "template_id": template_id, + "success": True, + "template_db_id": str(new_template.id) + }) + + except Exception as e: + results.append({ + "template_id": template_id, + "success": False, + "msg": f"Error: {str(e)}" + }) + + success_count = sum(1 for r in results if r.get("success")) + await self.module_logger.log_info( + info="Templates assigned to tenant", + properties={ + "tenant_id": tenant_id, + "region": region, + "total_requested": len(template_ids), + "success_count": success_count + } + ) + return results + except Exception as e: + await self.module_logger.log_error( + error="Failed to assign templates to tenant", + properties={ + "tenant_id": tenant_id, + "region": region, + "error": str(e) + } + ) + raise + + async def create_tenant_template(self, tenant_id: str, template: MessageTemplateDoc): + """create tenant template""" + try: + template.tenant_id = tenant_id + + # Check if template already exists with same template_id, tenant_id and region + existing_template = await self.template_message_handler.find_tenant_template(tenant_id, template.template_id, template.region) + if existing_template: + raise ValueError(f"Tenant template with template_id '{template.template_id}', tenant_id '{tenant_id}' and region '{template.region}' already exists") + + result = await self.template_message_handler.create_tenant_template(tenant_id, template) + await self.module_logger.log_info( + info="Tenant template created", + properties={ + "tenant_id": tenant_id, + "template_id": template.template_id, + "region": template.region + } + ) + return result + except ValueError as e: + # Re-raise ValueError for duplicate template + raise + except Exception as e: + await self.module_logger.log_error( + error="Failed to create tenant template", + properties={ + "tenant_id": tenant_id, + "template_id": template.template_id, + "error": str(e) + } + ) + raise + + async def update_tenant_template(self, tenant_id: str, template_id: str, data: dict, region: int): + """update tenant template""" + try: + template = await self.template_message_handler.find_tenant_template(tenant_id, template_id, region) + if not template: + raise ValueError("Template not found") + + result = await self.template_message_handler.update_tenant_template(tenant_id, template_id, data, region) + await self.module_logger.log_info( + info="Tenant template updated", + properties={ + "tenant_id": tenant_id, + "template_id": template_id, + "region": region, + "updated_fields": list(data.keys()) + } + ) + return result + except Exception as e: + await self.module_logger.log_error( + error="Failed to update tenant template", + properties={ + "tenant_id": tenant_id, + "template_id": template_id, + "region": region, + "error": str(e) + } + ) + raise + + async def delete_tenant_template(self, tenant_id: str, template_id: str, region: int): + """delete tenant template""" + try: + template = await self.template_message_handler.find_tenant_template(tenant_id, template_id, region) + if not template: + raise ValueError("Template not found") + + result = await self.template_message_handler.delete_tenant_template(tenant_id, template_id, region) + await self.module_logger.log_info( + info="Tenant template deleted", + properties={ + "tenant_id": tenant_id, + "template_id": template_id + } + ) + return result + except Exception as e: + await self.module_logger.log_error( + error="Failed to delete tenant template", + properties={ + "tenant_id": tenant_id, + "template_id": template_id, + "error": str(e) + } + ) + raise + + + async def render_template(self, tenant_id: str, template_id: str, properties: dict, region: int): + """render template""" + try: + template = await self.template_message_handler.find_tenant_template(tenant_id, template_id, region) + if not template: + raise ValueError("Template not found") + + result = await self.template_message_handler.render_template(template, properties) + await self.module_logger.log_info( + info="Template rendered successfully", + properties={ + "tenant_id": tenant_id, + "template_id": template_id, + "region": region, + "properties_count": len(properties) + } + ) + return result + except Exception as e: + await self.module_logger.log_error( + error="Failed to render template", + properties={ + "tenant_id": tenant_id, + "template_id": template_id, + "region": region, + "error": str(e) + } + ) + raise + \ No newline at end of file