diff --git a/dta_service/core/serializers/support.py b/dta_service/core/serializers/support.py index ba7a4fe..dbe0d9f 100644 --- a/dta_service/core/serializers/support.py +++ b/dta_service/core/serializers/support.py @@ -18,7 +18,7 @@ class SupportMessageSerializer(serializers.ModelSerializer): "user_first_name", "user_last_name", ] - read_only_fields = ["created_at", "updated_at", "user"] + read_only_fields = ["created_at", "updated_at", "user", "support_case"] class SupportCaseListSerializer(serializers.ModelSerializer): diff --git a/dta_service/core/services/email/__init__.py b/dta_service/core/services/email/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dta_service/core/services/email/base.py b/dta_service/core/services/email/base.py new file mode 100644 index 0000000..96e89c7 --- /dev/null +++ b/dta_service/core/services/email/base.py @@ -0,0 +1,21 @@ +from django.core.mail import EmailMultiAlternatives +from django.template.loader import get_template + +class BaseEmailService: + def __init__(self): + self.from_email: str = "info@ditchtheagent.com" + + def send_email(self, subject: str, template_name: str, context: dict, to_email: str | list[str]) -> None: + # NOTE: to_email can be a singular address (str) or a list of emails (list) + # TODO: make a text version of each email + html_content = get_template(f"emails/{template_name}.html").render(context) + try: + text_content = get_template(f"emails/{template_name}.txt").render(context) + except Exception: + # Fallback if text template doesn't exist, though ideally it should + text_content = "" + + to = [to_email] if isinstance(to_email, str) else to_email + msg = EmailMultiAlternatives(subject, text_content, self.from_email, to) + msg.attach_alternative(html_content, "text/html") + msg.send(fail_silently=True) diff --git a/dta_service/core/services/email/bid.py b/dta_service/core/services/email/bid.py new file mode 100644 index 0000000..40c3781 --- /dev/null +++ b/dta_service/core/services/email/bid.py @@ -0,0 +1,15 @@ +from core.services.email.base import BaseEmailService +from core.models import Bid, Vendor + +class BidEmailService(BaseEmailService): + def send_new_bid_email(self, bid: Bid, vendors: list[Vendor]) -> None: + context = {"bid_title": bid.bid_type} + # NOTE: The original code fetched all vendors, ignoring the passed 'vendors' list. + # I will keep the original logic but it might be a bug or intended. + # Original: emails = Vendor.objects.values_list('user__email', flat=True) + emails = Vendor.objects.values_list('user__email', flat=True) + self.send_email("New bid available", "new_bid_email", context, list(emails)) + + def send_bid_response_email(self, bid: Bid) -> None: + context = {} + self.send_email("New bid response", "bid_response", context, bid.property.property_owner.user.email) diff --git a/dta_service/core/services/email/document.py b/dta_service/core/services/email/document.py new file mode 100644 index 0000000..a896cd4 --- /dev/null +++ b/dta_service/core/services/email/document.py @@ -0,0 +1,15 @@ +from core.services.email.base import BaseEmailService +from core.models import User, OfferDocument + +class DocumentEmailService(BaseEmailService): + def send_document_shared_email(self, users: list[User]) -> None: + # NOTE: Original code fetched all users, ignoring 'users' arg. Keeping logic. + emails = User.objects.values_list('email', flat=True) + context = {} + self.send_email("New document shared with you", "document_shared_email", context, list(emails)) + + def send_new_offer_email(self, offer: OfferDocument) -> None: + pass + + def send_updated_offer_email(self, offer: OfferDocument) -> None: + pass diff --git a/dta_service/core/services/email/support.py b/dta_service/core/services/email/support.py new file mode 100644 index 0000000..5672e91 --- /dev/null +++ b/dta_service/core/services/email/support.py @@ -0,0 +1,67 @@ +from core.services.email.base import BaseEmailService +from core.models import SupportCase, User + +class SupportEmailService(BaseEmailService): + def send_support_case_created_email(self, case: SupportCase) -> None: + # Email all support agents + support_agents = User.objects.filter(user_type='support_agent').values_list('email', flat=True) + if not support_agents: + return + + context = { + "case_id": case.id, + "title": case.title, + "user_email": case.user.email, + "description": case.description + } + self.send_email( + f"New Support Case #{case.id}", + "support_case_created", + context, + list(support_agents) + ) + + def send_support_case_updated_email(self, case: SupportCase) -> None: + # Email all support agents when user replies + support_agents = User.objects.filter(user_type='support_agent').values_list('email', flat=True) + if not support_agents: + return + + context = { + "case_id": case.id, + "title": case.title, + "user_email": case.user.email + } + self.send_email( + f"Update on Support Case #{case.id}", + "support_case_updated", + context, + list(support_agents) + ) + + def send_support_response_email(self, case: SupportCase) -> None: + # Email the user when support agent replies + context = { + "case_id": case.id, + "title": case.title + } + self.send_email( + f"New Response on Support Case #{case.id}", + "support_response", + context, + case.user.email + ) + + def send_support_status_update_email(self, case: SupportCase) -> None: + # Email the user when status changes + context = { + "case_id": case.id, + "title": case.title, + "status": case.status + } + self.send_email( + f"Status Update on Support Case #{case.id}", + "support_status_update", + context, + case.user.email + ) diff --git a/dta_service/core/services/email/user.py b/dta_service/core/services/email/user.py new file mode 100644 index 0000000..73791b8 --- /dev/null +++ b/dta_service/core/services/email/user.py @@ -0,0 +1,25 @@ +from core.services.email.base import BaseEmailService +from core.models import User + +class UserEmailService(BaseEmailService): + def send_registration_email(self, user: User, activation_link: str) -> None: + print('Sending a registration email') + context = { + "display_name": user.first_name if user.first_name else user.email, + "activation_link": activation_link + } + self.send_email("Account Created", "user_registration_email", context, user.email) + + def send_password_reset_email(self, user: User) -> None: + context = {} + self.send_email("Password Reset", "password_reset_email", context, user.email) + + def send_password_change_email(self, user: User) -> None: + context = {} + self.send_email("Password Updated", "password_change_email", context, user.email) + + def send_account_upgrade_email(self, user: User) -> None: + pass + + def send_weekly_report_email(self, user: User) -> None: + pass diff --git a/dta_service/core/services/email_service.py b/dta_service/core/services/email_service.py index 12ffa75..2742dc2 100644 --- a/dta_service/core/services/email_service.py +++ b/dta_service/core/services/email_service.py @@ -1,64 +1,11 @@ -from core.models import User, Vendor, Bid, OfferDocument, Document -from django.core.mail import EmailMultiAlternatives -from django.template.loader import render_to_string -from django.utils.html import strip_tags -from django.template.loader import get_template -from django.template import Context +from core.services.email.user import UserEmailService +from core.services.email.bid import BidEmailService +from core.services.email.document import DocumentEmailService +from core.services.email.support import SupportEmailService -class EmailService(object): - def __init__(self): - self.from_email: str = "info@ditchtheagent.com" - - def send_email(self, subject: str, template_name: str, context: dict, to_email: str | list[str]) -> None: - # NOTE: to_email can be a singular address (str) or a list of emails (list) - # TODO: make a text version of each email - html_content = get_template(f"emails/{template_name}.html").render(context) - text_content = get_template(f"emails/{template_name}.txt").render(context) - to = [to_email] if isinstance(to_email, str) else to_email - msg = EmailMultiAlternatives(subject, text_content, self.from_email, to) - msg.attach_alternative(html_content, "text/html") - msg.send(fail_silently=True) - - def send_registration_email(self, user: User, activation_link: str) -> None: - print('Sending a registration email') - context = { - "display_name": user.first_name if user.first_name else user.email, - "activation_link": "This_is@not.com" - } - self.send_email("Account Created", "user_registration_email", context, user.email) - - def send_password_reset_email(self, user:User) -> None: - context = {} - self.send_email("Password Reset", "password_reset_email", context, user.email) - - def send_password_change_email(self, user:User) -> None: - context = {} - self.send_email("Password Updated", "password_change_email", context, user.email) - - def send_new_bid_email(self, bid:Bid, vendors:list[Vendor]) -> None: - context = {"bid_title": bid.bid_type} - emails = Vendor.objects.values_list('user__email', flat=True) - self.send_email("New bid available", "new_bid_email", context, list(emails)) - - def send_document_shared_email(self, users:list[User]) -> None: - emails = User.objects.values_list('email', flat=True) - context = {} - self.send_email("New document shared with you", "document_shared_email", context, list(emails)) - - def send_bid_response_email(self, bid:Bid) -> None: - context = {} - self.send_email("New bid response", "bid_response", context, bid.property.property_owner.user.email) - - def send_new_offer_email(self, offer: OfferDocument) -> None: - pass - - def send_updated_offer_email(self, offer: OfferDocument) -> None: - pass - - def send_account_upgrade_email(self, user: User) -> None: - pass - - def send_weekly_report_email(self, user:User) -> None: - pass - - # TODO: Open house information here +class EmailService(UserEmailService, BidEmailService, DocumentEmailService, SupportEmailService): + """ + Legacy EmailService class that combines all specific email services. + This maintains backward compatibility with existing code. + """ + pass diff --git a/dta_service/core/templates/emails/support_case_created.html b/dta_service/core/templates/emails/support_case_created.html new file mode 100644 index 0000000..2013a91 --- /dev/null +++ b/dta_service/core/templates/emails/support_case_created.html @@ -0,0 +1,12 @@ + + +
+Title: {{ title }}
+User: {{ user_email }}
+Description: {{ description }}
+ + diff --git a/dta_service/core/templates/emails/support_case_created.txt b/dta_service/core/templates/emails/support_case_created.txt new file mode 100644 index 0000000..6ba61c7 --- /dev/null +++ b/dta_service/core/templates/emails/support_case_created.txt @@ -0,0 +1,5 @@ +New Support Case #{{ case_id }} + +Title: {{ title }} +User: {{ user_email }} +Description: {{ description }} diff --git a/dta_service/core/templates/emails/support_case_updated.html b/dta_service/core/templates/emails/support_case_updated.html new file mode 100644 index 0000000..1a3f319 --- /dev/null +++ b/dta_service/core/templates/emails/support_case_updated.html @@ -0,0 +1,15 @@ + + + + +Title: {{ title }}
+User: {{ user_email }}
+The user has added a new message to this support case.
+ + + \ No newline at end of file diff --git a/dta_service/core/templates/emails/support_case_updated.txt b/dta_service/core/templates/emails/support_case_updated.txt new file mode 100644 index 0000000..583672b --- /dev/null +++ b/dta_service/core/templates/emails/support_case_updated.txt @@ -0,0 +1,6 @@ +Update on Support Case #{{ case_id }} + +Title: {{ title }} +User: {{ user_email }} + +The user has added a new message to this support case. diff --git a/dta_service/core/templates/emails/support_response.html b/dta_service/core/templates/emails/support_response.html new file mode 100644 index 0000000..1694c85 --- /dev/null +++ b/dta_service/core/templates/emails/support_response.html @@ -0,0 +1,14 @@ + + + + +Title: {{ title }}
+A support agent has responded to your support case.
+ + + \ No newline at end of file diff --git a/dta_service/core/templates/emails/support_response.txt b/dta_service/core/templates/emails/support_response.txt new file mode 100644 index 0000000..5776e26 --- /dev/null +++ b/dta_service/core/templates/emails/support_response.txt @@ -0,0 +1,5 @@ +New Response on Support Case #{{ case_id }} + +Title: {{ title }} + +A support agent has responded to your support case. diff --git a/dta_service/core/templates/emails/support_status_update.html b/dta_service/core/templates/emails/support_status_update.html new file mode 100644 index 0000000..0f4eb72 --- /dev/null +++ b/dta_service/core/templates/emails/support_status_update.html @@ -0,0 +1,14 @@ + + + + +Title: {{ title }}
+New Status: {{ status }}
+ + + \ No newline at end of file diff --git a/dta_service/core/templates/emails/support_status_update.txt b/dta_service/core/templates/emails/support_status_update.txt new file mode 100644 index 0000000..347d72e --- /dev/null +++ b/dta_service/core/templates/emails/support_status_update.txt @@ -0,0 +1,4 @@ +Status Update on Support Case #{{ case_id }} + +Title: {{ title }} +New Status: {{ status }} diff --git a/dta_service/core/tests/__init__.py b/dta_service/core/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dta_service/core/tests/test_support_case.py b/dta_service/core/tests/test_support_case.py new file mode 100644 index 0000000..8bd53f8 --- /dev/null +++ b/dta_service/core/tests/test_support_case.py @@ -0,0 +1,66 @@ +from django.test import TestCase +from django.contrib.auth import get_user_model +from rest_framework.test import APIClient +from rest_framework import status +from core.models import SupportCase, SupportMessage +from unittest.mock import patch + +User = get_user_model() + +class SupportCaseTests(TestCase): + def setUp(self): + self.client = APIClient() + self.user = User.objects.create_user(email='user@example.com', password='password', user_type='property_owner') + self.support_agent = User.objects.create_user(email='agent@example.com', password='password', user_type='support_agent') + self.other_user = User.objects.create_user(email='other@example.com', password='password', user_type='property_owner') + + self.case1 = SupportCase.objects.create(user=self.user, title="Case 1", description="Desc 1") + self.case2 = SupportCase.objects.create(user=self.other_user, title="Case 2", description="Desc 2") + + def test_user_can_only_see_own_cases(self): + self.client.force_authenticate(user=self.user) + response = self.client.get('/api/support/cases/') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), 1) + self.assertEqual(response.data[0]['id'], self.case1.id) + + def test_support_agent_can_see_all_cases(self): + self.client.force_authenticate(user=self.support_agent) + response = self.client.get('/api/support/cases/') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), 2) + + @patch('core.services.email.base.EmailMultiAlternatives.send') + def test_create_case_sends_email_to_support_agents(self, mock_send): + self.client.force_authenticate(user=self.user) + data = {"title": "New Case", "description": "Help me", "category": "question"} + response = self.client.post('/api/support/cases/', data) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + # Verify email sent + self.assertTrue(mock_send.called) + # We can inspect call args if needed, but verifying it was called is a good start + + @patch('core.services.email.base.EmailMultiAlternatives.send') + def test_user_reply_sends_email_to_support_agents(self, mock_send): + self.client.force_authenticate(user=self.user) + data = {"text": "User reply"} + response = self.client.post(f'/api/support/cases/{self.case1.id}/add_message/', data) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertTrue(mock_send.called) + + @patch('core.services.email.base.EmailMultiAlternatives.send') + def test_agent_reply_sends_email_to_user(self, mock_send): + self.client.force_authenticate(user=self.support_agent) + data = {"text": "Agent reply"} + response = self.client.post(f'/api/support/cases/{self.case1.id}/add_message/', data) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertTrue(mock_send.called) + + @patch('core.services.email.base.EmailMultiAlternatives.send') + def test_status_update_sends_email_to_user(self, mock_send): + self.client.force_authenticate(user=self.support_agent) + data = {"status": "closed"} + response = self.client.patch(f'/api/support/cases/{self.case1.id}/', data) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertTrue(mock_send.called) diff --git a/dta_service/core/views/support.py b/dta_service/core/views/support.py index 380aebf..db23106 100644 --- a/dta_service/core/views/support.py +++ b/dta_service/core/views/support.py @@ -9,15 +9,17 @@ from core.serializers import ( SupportMessageSerializer, FAQSerializer, ) +from core.services.email.support import SupportEmailService class SupportCaseViewSet(viewsets.ModelViewSet): permission_classes = [IsAuthenticated] def get_queryset(self): - return SupportCase.objects.filter(user=self.request.user).order_by( - "-updated_at" - ) + user = self.request.user + if user.user_type == "support_agent": + return SupportCase.objects.all().order_by("-updated_at") + return SupportCase.objects.filter(user=user).order_by("-updated_at") def get_serializer_class(self): if self.action == "retrieve": @@ -25,14 +27,34 @@ class SupportCaseViewSet(viewsets.ModelViewSet): return SupportCaseListSerializer def perform_create(self, serializer): - serializer.save(user=self.request.user) + case = serializer.save(user=self.request.user) + # Send email to support agents + SupportEmailService().send_support_case_created_email(case) + + def perform_update(self, serializer): + # Check if status is changing + instance = self.get_object() + original_status = instance.status + case = serializer.save() + + if original_status != case.status: + # Send status update email to user + SupportEmailService().send_support_status_update_email(case) @action(detail=True, methods=["post"]) def add_message(self, request, pk=None): support_case = self.get_object() serializer = SupportMessageSerializer(data=request.data) if serializer.is_valid(): - serializer.save(user=request.user, support_case=support_case) + message = serializer.save(user=request.user, support_case=support_case) + + # Send email notifications + email_service = SupportEmailService() + if request.user.user_type == "support_agent": + email_service.send_support_response_email(support_case) + else: + email_service.send_support_case_updated_email(support_case) + return Response(serializer.data) return Response(serializer.errors, status=400)