WIP: Attach invoice PDF to payment emails #104418

Draft
Anna Sirota wants to merge 4 commits from attach-invoice-pdf into main

When changing the target branch, be careful to rebase the branch in your fork to match. See documentation.
2 changed files with 123 additions and 13 deletions

View File

@ -8,6 +8,7 @@ from django.conf import settings
from django.template import loader from django.template import loader
import django.core.mail import django.core.mail
from looper.pdf import PDFResponse
from looper.stripe_utils import ( from looper.stripe_utils import (
create_active_subscription_from_payment_intent, create_active_subscription_from_payment_intent,
upsert_subscription_payment_method_from_setup_intent, upsert_subscription_payment_method_from_setup_intent,
@ -27,6 +28,33 @@ logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG) 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]: def _construct_subscription_mail(mail_name: str, context: Dict[str, Any]) -> Tuple[str, str, str]:
"""Construct a mail about a subscription. """Construct a mail about a subscription.
@ -116,6 +144,7 @@ def send_mail_subscription_status_changed(subscription_id: int):
verb = 'activated' verb = 'activated'
else: else:
verb = 'deactivated' verb = 'deactivated'
logger.info('Sending subscription-%s notification to %s', verb, email)
context = { context = {
'user': user, 'user': user,
@ -126,15 +155,20 @@ def send_mail_subscription_status_changed(subscription_id: int):
mail_name = f'subscription_{verb}' mail_name = f'subscription_{verb}'
email_body_html, email_body_txt, subject = _construct_subscription_mail(mail_name, context) email_body_html, email_body_txt, subject = _construct_subscription_mail(mail_name, context)
django.core.mail.send_mail( msg = django.core.mail.EmailMultiAlternatives(
subject, subject=subject,
message=email_body_txt, body=email_body_txt,
html_message=email_body_html,
from_email=None, # just use the configured default From-address. from_email=None, # just use the configured default From-address.
recipient_list=[email], to=[email],
fail_silently=False,
) )
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() @background()
@ -172,14 +206,18 @@ def send_mail_automatic_payment_performed(order_id: int, transaction_id: int):
mail_name = f'payment_{order.status}' mail_name = f'payment_{order.status}'
email_body_html, email_body_txt, subject = _construct_subscription_mail(mail_name, context) email_body_html, email_body_txt, subject = _construct_subscription_mail(mail_name, context)
django.core.mail.send_mail( msg = django.core.mail.EmailMultiAlternatives(
subject, subject=subject,
message=email_body_txt, body=email_body_txt,
html_message=email_body_html,
from_email=None, # just use the configured default From-address. from_email=None, # just use the configured default From-address.
recipient_list=[email], to=[email],
fail_silently=False,
) )
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) logger.info('Sent %r notification to %s', order.status, email)

View File

@ -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