From 9c83663f1db2521f5faf5c006d9b26792876f3a5 Mon Sep 17 00:00:00 2001 From: Anna Sirota Date: Thu, 4 Jul 2024 17:15:28 +0200 Subject: [PATCH 1/3] . --- blender_fund_main/email.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/blender_fund_main/email.py b/blender_fund_main/email.py index 9661ca9..8776801 100644 --- a/blender_fund_main/email.py +++ b/blender_fund_main/email.py @@ -8,9 +8,10 @@ from django.dispatch import receiver from django.template import loader from django.urls import reverse -import looper.signals -import looper.models from . import models, signals +from looper.pdf import PDFResponse +import looper.models +import looper.signals log = logging.getLogger(__name__) @@ -137,14 +138,19 @@ def automatic_payment_performed(sender: looper.models.Order, log.debug('Sending payment %r notification to %s', sender.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], ) + file_data = b'TODO' + file_name = f'blender-development-fund-receipt-{sender.display_number}.pdf' + print(file_name) + file_data = PDFResponse().rendered_content + msg.attach(file_name, file_data, 'application/pdf') + msg.attach_alternative(email_body_html, 'text/html') + msg.send(fail_silently=False) except Exception: # Template rendering errors shouldn't interfere with the Looper clock, so # catch all errors here. -- 2.30.2 From 6593b9e6a746644b1419515e01d52c45af9a64ef Mon Sep 17 00:00:00 2001 From: Anna Sirota Date: Fri, 5 Jul 2024 09:59:18 +0200 Subject: [PATCH 2/3] Attach PDF to "payment successful" email --- blender_fund_main/tasks.py | 13 ++++++++----- blender_fund_main/tests/test_clock.py | 19 +++++++++++++++++++ blender_fund_main/tests/test_receipts_pdf.py | 19 +++++++------------ blender_fund_main/tests/utils.py | 10 ++++++++++ 4 files changed, 44 insertions(+), 17 deletions(-) create mode 100644 blender_fund_main/tests/utils.py diff --git a/blender_fund_main/tasks.py b/blender_fund_main/tasks.py index 7b97062..f810164 100644 --- a/blender_fund_main/tasks.py +++ b/blender_fund_main/tasks.py @@ -139,12 +139,15 @@ def send_mail_automatic_payment_performed(order_id: int, transaction_id: int): from_email=None, # just use the configured default From-address. to=[email], ) - file_data = b'TODO' - file_name = f'blender-development-fund-receipt-{order.display_number}.pdf' - print(file_name) - file_data = PDFResponse().rendered_content - msg.attach(file_name, file_data, 'application/pdf') 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 df669b4..6633b0f 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 from blender_fund_main.utils import html_to_text import blender_fund_main.tasks as tasks @@ -314,6 +315,7 @@ class TestClockEmails(AbstractLooperTestCase): expected_soft_failed_email_html_stripped, html_to_text(email.alternatives[0][0]), ) + self.assertEqual(len(email.attachments), 0) @responses.activate def test_automated_payment_failed_email_is_sent(self): @@ -378,6 +380,7 @@ class TestClockEmails(AbstractLooperTestCase): expected_failed_email_html_stripped, html_to_text(email.alternatives[0][0]), ) + self.assertEqual(len(email.attachments), 0) @responses.activate def test_automated_payment_paid_email_is_sent(self): @@ -438,6 +441,21 @@ class TestClockEmails(AbstractLooperTestCase): expected_payment_received_email_html_stripped, html_to_text(email.alternatives[0][0]), ) + self.assertEqual(len(email.attachments), 1) + + self.assertEqual(email.attachments[0][1], 'application/pdf') + pdf_text = extract_text_from_pdf(email.attachments[0][0]) + self.assertEqual( + pdf_text, + expected_text_tmpl.format( + order=order, + expected_address=' -', + expected_currency_symbol='$', + expected_payment_method='PayPal account test@example.com', + expected_total='14.80', + ), + pdf_text, + ) @override_settings(LOOPER_MANAGER_MAIL='admin@example.com') def test_managed_subscription_notification_email_is_sent(self): @@ -469,3 +487,4 @@ class TestClockEmails(AbstractLooperTestCase): expected_managed_email_html_stripped, 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..f36b893 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,6 +14,9 @@ from looper.tests.factories import ( create_customer_with_billing_address, ) +from blender_fund_main.tests.utils import extract_text_from_pdf + + production_storage = settings.STATICFILES_STORAGE expected_text_tmpl = '''Stichting Blender Foundation @@ -132,12 +133,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,7 +172,7 @@ 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( @@ -214,7 +209,7 @@ 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( @@ -248,7 +243,7 @@ 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( @@ -280,7 +275,7 @@ 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( diff --git a/blender_fund_main/tests/utils.py b/blender_fund_main/tests/utils.py new file mode 100644 index 0000000..2834f0b --- /dev/null +++ b/blender_fund_main/tests/utils.py @@ -0,0 +1,10 @@ +from io import BytesIO + +from pypdf import PdfReader + + +def extract_text_from_pdf(response): + pdf = PdfReader(BytesIO(response.content)) + assert 1 == len(pdf.pages) + pdf_page = pdf.pages[0] + return pdf_page.extract_text() -- 2.30.2 From bf859d608cc3b6c24f3fb57ef2f3e187d60de50c Mon Sep 17 00:00:00 2001 From: Anna Sirota Date: Fri, 5 Jul 2024 15:04:50 +0200 Subject: [PATCH 3/3] Update tests --- blender_fund_main/tests/test_clock.py | 26 +++++---- blender_fund_main/tests/test_receipts_pdf.py | 57 ++++++++------------ blender_fund_main/tests/utils.py | 37 ++++++++++++- 3 files changed, 73 insertions(+), 47 deletions(-) diff --git a/blender_fund_main/tests/test_clock.py b/blender_fund_main/tests/test_clock.py index 01864ce..b4bf7f7 100644 --- a/blender_fund_main/tests/test_clock.py +++ b/blender_fund_main/tests/test_clock.py @@ -14,7 +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 +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 @@ -373,18 +373,24 @@ class TestClockEmails(AbstractLooperTestCase): expected_payment_received_email_body, html_to_text(email.alternatives[0][0]), ) - self.assertEqual(len(email.attachments), 1) - self.assertEqual(email.attachments[0][1], 'application/pdf') - pdf_text = extract_text_from_pdf(email.attachments[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_text_tmpl.format( - order=order, - expected_address=' -', - expected_currency_symbol='$', - expected_payment_method='PayPal account test@example.com', - expected_total='14.80', + 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, ) diff --git a/blender_fund_main/tests/test_receipts_pdf.py b/blender_fund_main/tests/test_receipts_pdf.py index f36b893..6f9db2e 100644 --- a/blender_fund_main/tests/test_receipts_pdf.py +++ b/blender_fund_main/tests/test_receipts_pdf.py @@ -14,40 +14,15 @@ from looper.tests.factories import ( create_customer_with_billing_address, ) -from blender_fund_main.tests.utils import extract_text_from_pdf +from blender_fund_main.tests.utils import ( + extract_text_from_pdf, + expected_pdf_text_tmpl, + expected_pdf_text_bank_details, +) production_storage = settings.STATICFILES_STORAGE -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 -''' - # This test needs to use the static file storage we also use in production, # regardless of what the superclass overrides. There was an issue finding @@ -175,7 +150,7 @@ class TestReceiptPDFContent(AbstractLooperTestCase): 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' @@ -185,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', ), @@ -212,10 +190,13 @@ class TestReceiptPDFContent(AbstractLooperTestCase): 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', ), @@ -246,13 +227,16 @@ class TestReceiptPDFContent(AbstractLooperTestCase): 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, ) @@ -278,10 +262,13 @@ class TestReceiptPDFContent(AbstractLooperTestCase): 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 index 2834f0b..ad56317 100644 --- a/blender_fund_main/tests/utils.py +++ b/blender_fund_main/tests/utils.py @@ -2,9 +2,42 @@ 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(response): - pdf = PdfReader(BytesIO(response.content)) + +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) -- 2.30.2