From 64a8ad953c0c4b1164048852e88b7cbe8579f531 Mon Sep 17 00:00:00 2001 From: Anna Sirota Date: Thu, 11 Jul 2024 10:40:34 +0200 Subject: [PATCH 1/2] . --- subscriptions/tasks.py | 64 +++++++++++++++++++++++++++++++++--------- 1 file changed, 51 insertions(+), 13 deletions(-) diff --git a/subscriptions/tasks.py b/subscriptions/tasks.py index a91d0037..80007b05 100644 --- a/subscriptions/tasks.py +++ b/subscriptions/tasks.py @@ -8,6 +8,7 @@ from django.conf import settings from django.template import loader import django.core.mail +from looper.pdf import PDFResponse from looper.stripe_utils import ( create_active_subscription_from_payment_intent, upsert_subscription_payment_method_from_setup_intent, @@ -27,6 +28,33 @@ logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) +def attach_invoice_pdf(msg: django.core.mail.EmailMultiAlternatives, order: looper.models.Order): + """Attach the PDF receipt file to a given email message.""" + assert order.status == 'paid' + file_name = f'blender-studio-invoice-{order.display_number}.pdf' + pdf_context = {'order': order} + pdf_response = PDFResponse(None, 'looper/settings/receipt_pdf.html', context=pdf_context) + file_data = pdf_response.rendered_content + msg.attach(file_name, file_data, 'application/pdf') + logger.info('Attached invoice PDF of order pk=%s to email', order.pk) + + +def _attach_latest_invoice_pdf( + msg: django.core.mail.EmailMultiAlternatives, + subscription: looper.models.Subscription, +) -> None: + if subscription.status != 'active': + return + order = subscription.latest_order() + if not order: + logger.warning("Subscription pk=%s has no order, won't attach receipt", subscription.pk) + return + if order.status != 'paid': + logger.warning("Latest order pk=%s has is not paid, won't attach receipt", order.pk) + return + attach_invoice_pdf(msg, order) + + def _construct_subscription_mail(mail_name: str, context: Dict[str, Any]) -> Tuple[str, str, str]: """Construct a mail about a subscription. @@ -116,6 +144,7 @@ def send_mail_subscription_status_changed(subscription_id: int): verb = 'activated' else: verb = 'deactivated' + logger.info('Sending subscription-%s notification to %s', verb, email) context = { 'user': user, @@ -126,15 +155,20 @@ def send_mail_subscription_status_changed(subscription_id: int): mail_name = f'subscription_{verb}' email_body_html, email_body_txt, subject = _construct_subscription_mail(mail_name, context) - django.core.mail.send_mail( - subject, - message=email_body_txt, - html_message=email_body_html, + msg = django.core.mail.EmailMultiAlternatives( + subject=subject, + body=email_body_txt, from_email=None, # just use the configured default From-address. - recipient_list=[email], - fail_silently=False, + to=[email], ) - logger.info('Sent subscription-changed notification to %s', email) + msg.attach_alternative(email_body_html, 'text/html') + + # If subscription was activated, include invoice PDF of the latest paid order + if verb == 'activated': + _attach_latest_invoice_pdf(msg, subscription) + + msg.send(fail_silently=False) + logger.info('Sent subscription-%s notification to %s', verb, email) @background() @@ -172,14 +206,18 @@ def send_mail_automatic_payment_performed(order_id: int, transaction_id: int): mail_name = f'payment_{order.status}' email_body_html, email_body_txt, subject = _construct_subscription_mail(mail_name, context) - django.core.mail.send_mail( - subject, - message=email_body_txt, - html_message=email_body_html, + msg = django.core.mail.EmailMultiAlternatives( + subject=subject, + body=email_body_txt, from_email=None, # just use the configured default From-address. - recipient_list=[email], - fail_silently=False, + to=[email], ) + msg.attach_alternative(email_body_html, 'text/html') + + if order.status == 'paid': + attach_invoice_pdf(msg, order) + + msg.send(fail_silently=False) logger.info('Sent %r notification to %s', order.status, email) -- 2.30.2 From 1ffbe677cd52f23e303ddc3bb0af70349eb0f286 Mon Sep 17 00:00:00 2001 From: Anna Sirota Date: Thu, 11 Jul 2024 13:03:52 +0200 Subject: [PATCH 2/2] WIP missing tests --- subscriptions/tests/test_emails.py | 72 ++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 subscriptions/tests/test_emails.py diff --git a/subscriptions/tests/test_emails.py b/subscriptions/tests/test_emails.py new file mode 100644 index 00000000..911a9c24 --- /dev/null +++ b/subscriptions/tests/test_emails.py @@ -0,0 +1,72 @@ +from unittest.mock import patch, Mock, call +import datetime + +from django.test import TestCase +import django.core.mail +import responses + +from looper.tests.factories import SubscriptionFactory + +from common.tests.factories.users import UserFactory + +import subscriptions.tasks as tasks + + +@patch( + 'subscriptions.tasks.send_mail_bank_transfer_required', + new=tasks.send_mail_bank_transfer_required.task_function, +) +@patch( + 'subscriptions.tasks.send_mail_subscription_status_changed', + new=tasks.send_mail_subscription_status_changed.task_function, +) +@patch( + 'subscriptions.tasks.send_mail_automatic_payment_performed', + new=tasks.send_mail_automatic_payment_performed.task_function, +) +@patch( + 'subscriptions.tasks.send_mail_managed_subscription_notification', + new=tasks.send_mail_managed_subscription_notification.task_function, +) +@patch( + 'subscriptions.tasks.send_mail_subscription_expired', + new=tasks.send_mail_subscription_expired.task_function, +) +@patch( + 'subscriptions.tasks.send_mail_no_payment_method', + new=tasks.send_mail_no_payment_method.task_function, +) +class TestSubscriptionSignals(TestCase): + def test_subscription_created_needs_payment(self): + subscription = SubscriptionFactory(status='on-hold', collection_method='manual') + the_mail: django.core.mail.message.EmailMultiAlternatives = django.core.mail.outbox[-1] + self.assertIn(self.user.customer.billing_address.full_name, the_mail.body) + alt0_body, alt0_type = the_mail.alternatives[0] + self.assertEqual(alt0_type, 'text/html') + + def test_automatic_payment_failed(self): + pass + + def test_automatic_payment_failed_no_payment_method(self): + pass + + def test_automatic_payment_soft_failed(self): + pass + + def test_automatic_payment_soft_failed_no_payment_method(self): + pass + + def test_automatic_payment_succesful(self): + pass + + def test_managed_subscription_notification(self): + pass + + def test_subscription_activated(self): + pass + + def test_subscription_deactivated(self): + pass + + def test_subscription_expired(self): + pass -- 2.30.2