diff --git a/blender_fund_main/email.py b/blender_fund_main/email.py index af0640b..b6be95d 100644 --- a/blender_fund_main/email.py +++ b/blender_fund_main/email.py @@ -6,9 +6,9 @@ from django.template import loader from django.urls import reverse import django.core.mail -import looper.signals -import looper.models from . import models +import looper.models +import looper.signals from blender_fund_main.utils import absolute_url, is_noreply, html_to_text diff --git a/blender_fund_main/tasks.py b/blender_fund_main/tasks.py index 184cc31..f810164 100644 --- a/blender_fund_main/tasks.py +++ b/blender_fund_main/tasks.py @@ -11,12 +11,13 @@ from django.contrib.auth import get_user_model from django.db.transaction import atomic import django.core.mail -import looper.stripe_utils +from looper.pdf import PDFResponse from looper.stripe_utils import ( upsert_order_from_payment_intent_and_product, upsert_subscription_payment_method_from_setup_intent, ) import looper.models +import looper.stripe_utils import stripe from blender_fund_main.utils import is_noreply @@ -132,15 +133,22 @@ def send_mail_automatic_payment_performed(order_id: int, transaction_id: int): ) logger.debug('Sending payment %r notification to %s', order.status, email) - - 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') + + # Attach the PDF receipt + if order.status == 'paid': + file_name = f'blender-development-fund-receipt-{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') + msg.send(fail_silently=False) logger.info('Sent %r notification to %s', order.status, email) diff --git a/blender_fund_main/tests/test_clock.py b/blender_fund_main/tests/test_clock.py index 6b4b606..b4bf7f7 100644 --- a/blender_fund_main/tests/test_clock.py +++ b/blender_fund_main/tests/test_clock.py @@ -14,6 +14,7 @@ from looper.tests import AbstractLooperTestCase from looper.tests.factories import SubscriptionFactory, create_customer_with_billing_address import looper.exceptions +from blender_fund_main.tests.utils import extract_text_from_pdf_bytes, expected_pdf_text_tmpl from blender_fund_main.utils import html_to_text import blender_fund_main.tasks as tasks @@ -246,6 +247,7 @@ class TestClockEmails(AbstractLooperTestCase): expected_soft_failed_email_body, html_to_text(email.alternatives[0][0]), ) + self.assertEqual(len(email.attachments), 0) @responses.activate def test_automated_payment_failed_email_is_sent(self): @@ -310,6 +312,7 @@ class TestClockEmails(AbstractLooperTestCase): expected_failed_email_body, html_to_text(email.alternatives[0][0]), ) + self.assertEqual(len(email.attachments), 0) @responses.activate def test_automated_payment_paid_email_is_sent(self): @@ -371,6 +374,27 @@ class TestClockEmails(AbstractLooperTestCase): html_to_text(email.alternatives[0][0]), ) + # Check that receipt PDF is attached to the email + self.assertEqual(len(email.attachments), 1) + title, content, mime = email.attachments[0] + self.assertEqual(title, f'blender-development-fund-receipt-{new_order.pk}.pdf') + self.assertEqual(mime, 'application/pdf') + pdf_text = extract_text_from_pdf_bytes(content) + self.assertEqual( + pdf_text, + expected_pdf_text_tmpl.format( + order=new_order, + expected_address=' Jane Doe\nNetherlands', + expected_currency_symbol='€', + expected_date=new_order.paid_at.strftime('%B %-d, %Y'), + expected_email='jane.doe+billing@example.com', + expected_level='Silver', + expected_payment_method='Test payment method', + expected_total='10', + ), + pdf_text, + ) + @override_settings(LOOPER_MANAGER_MAIL='admin@example.com') def test_managed_subscription_notification_email_is_sent(self): now = timezone.now() @@ -401,3 +425,4 @@ class TestClockEmails(AbstractLooperTestCase): expected_managed_email_body, html_to_text(email.alternatives[0][0]), ) + self.assertEqual(len(email.attachments), 0) diff --git a/blender_fund_main/tests/test_receipts_pdf.py b/blender_fund_main/tests/test_receipts_pdf.py index 74eeca4..6f9db2e 100644 --- a/blender_fund_main/tests/test_receipts_pdf.py +++ b/blender_fund_main/tests/test_receipts_pdf.py @@ -1,11 +1,9 @@ -from io import BytesIO from unittest.mock import patch, Mock from django.conf import settings from django.test import override_settings from django.urls import reverse from freezegun import freeze_time -from pypdf import PdfReader from looper.tests import AbstractLooperTestCase from looper.tests.factories import ( @@ -16,36 +14,14 @@ from looper.tests.factories import ( create_customer_with_billing_address, ) -production_storage = settings.STATICFILES_STORAGE +from blender_fund_main.tests.utils import ( + extract_text_from_pdf, + expected_pdf_text_tmpl, + expected_pdf_text_bank_details, +) -expected_text_tmpl = '''Stichting Blender Foundation -Buikslotermeerplein 161 -1025 ET Amsterdam, the Netherlands -Tax number NL 8111.66.223 -Blender Development Fund Donation Receipt - Receipt ID - {order.pk} - Payment received on - July 4, 2024 - Membership level - Bronze -Billing Address - Address -{expected_address} - E-mail - billing@example.com -Payment Information - Amount paid - {expected_currency_symbol} {expected_total} - Payment method - {expected_payment_method} -''' -expected_text_bank_details = ''' Bank details - Bank: ING Bank, P/O Box 1800, Amsterdam, the Netherlands -BIC/Swift code: INGB NL 2A (international code) -IBAN: NL45 INGB 0009356121 (for euro accounts) -Account #: 93 56 121 -''' + +production_storage = settings.STATICFILES_STORAGE # This test needs to use the static file storage we also use in production, @@ -132,12 +108,6 @@ class TestReceiptPDFContent(AbstractLooperTestCase): self.paid_order.status = 'paid' self.paid_order.save() - def _extract_text_from_pdf(self, response): - pdf = PdfReader(BytesIO(response.content)) - self.assertEqual(1, len(pdf.pages)) - pdf_page = pdf.pages[0] - return pdf_page.extract_text() - def test_get_pdf_unpaid_order_not_found(self): unpaid_order = OrderFactory( customer=self.payment_method.customer, @@ -177,10 +147,10 @@ class TestReceiptPDFContent(AbstractLooperTestCase): self.assertEqual(200, response.status_code) - pdf_text = self._extract_text_from_pdf(response) + pdf_text = extract_text_from_pdf(response) self.assertEqual( pdf_text, - expected_text_tmpl.format( + expected_pdf_text_tmpl.format( order=self.paid_order, expected_address=( ' Main st. 123\n' @@ -190,6 +160,9 @@ class TestReceiptPDFContent(AbstractLooperTestCase): 'Netherlands' ), expected_currency_symbol='€', + expected_date=self.paid_order.paid_at.strftime('%B %-d, %Y'), + expected_email='billing@example.com', + expected_level='Bronze', expected_payment_method='PayPal account test@example.com', expected_total='9.90', ), @@ -214,13 +187,16 @@ class TestReceiptPDFContent(AbstractLooperTestCase): self.assertEqual(200, response.status_code) - pdf_text = self._extract_text_from_pdf(response) + pdf_text = extract_text_from_pdf(response) self.assertEqual( pdf_text, - expected_text_tmpl.format( + expected_pdf_text_tmpl.format( order=order, expected_address=' -', expected_currency_symbol='€', + expected_date=order.paid_at.strftime('%B %-d, %Y'), + expected_email='billing@example.com', + expected_level='Bronze', expected_payment_method='PayPal account test@example.com', expected_total='9.90', ), @@ -248,16 +224,19 @@ class TestReceiptPDFContent(AbstractLooperTestCase): self.assertEqual(200, response.status_code) - pdf_text = self._extract_text_from_pdf(response) + pdf_text = extract_text_from_pdf(response) self.assertEqual( pdf_text, - expected_text_tmpl.format( + expected_pdf_text_tmpl.format( order=order, expected_address=' -', expected_currency_symbol='€', + expected_date=order.paid_at.strftime('%B %-d, %Y'), + expected_email='billing@example.com', + expected_level='Bronze', expected_payment_method='Bank Transfer', expected_total='9.90', - ) + expected_text_bank_details, + ) + expected_pdf_text_bank_details, pdf_text, ) @@ -280,13 +259,16 @@ class TestReceiptPDFContent(AbstractLooperTestCase): self.assertEqual(200, response.status_code) - pdf_text = self._extract_text_from_pdf(response) + pdf_text = extract_text_from_pdf(response) self.assertEqual( pdf_text, - expected_text_tmpl.format( + expected_pdf_text_tmpl.format( order=order, expected_address=' -', expected_currency_symbol='$', + expected_date=order.paid_at.strftime('%B %-d, %Y'), + expected_email='billing@example.com', + expected_level='Bronze', expected_payment_method='PayPal account test@example.com', expected_total='14.80', ), diff --git a/blender_fund_main/tests/utils.py b/blender_fund_main/tests/utils.py new file mode 100644 index 0000000..ad56317 --- /dev/null +++ b/blender_fund_main/tests/utils.py @@ -0,0 +1,43 @@ +from io import BytesIO + +from pypdf import PdfReader + +expected_pdf_text_tmpl = '''Stichting Blender Foundation +Buikslotermeerplein 161 +1025 ET Amsterdam, the Netherlands +Tax number NL 8111.66.223 +Blender Development Fund Donation Receipt + Receipt ID + {order.pk} + Payment received on + {expected_date} + Membership level + {expected_level} +Billing Address + Address +{expected_address} + E-mail + {expected_email} +Payment Information + Amount paid + {expected_currency_symbol} {expected_total} + Payment method + {expected_payment_method} +''' +expected_pdf_text_bank_details = ''' Bank details + Bank: ING Bank, P/O Box 1800, Amsterdam, the Netherlands +BIC/Swift code: INGB NL 2A (international code) +IBAN: NL45 INGB 0009356121 (for euro accounts) +Account #: 93 56 121 +''' + + +def extract_text_from_pdf_bytes(content: bytes): + pdf = PdfReader(BytesIO(content)) + assert 1 == len(pdf.pages) + pdf_page = pdf.pages[0] + return pdf_page.extract_text() + + +def extract_text_from_pdf(response): + return extract_text_from_pdf_bytes(response.content)