Attach receipt PDF to "payment successful" email #96850
@ -6,9 +6,9 @@ from django.template import loader
|
|||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
import django.core.mail
|
import django.core.mail
|
||||||
|
|
||||||
import looper.signals
|
|
||||||
import looper.models
|
|
||||||
from . import models
|
from . import models
|
||||||
|
import looper.models
|
||||||
|
import looper.signals
|
||||||
|
|
||||||
from blender_fund_main.utils import absolute_url, is_noreply, html_to_text
|
from blender_fund_main.utils import absolute_url, is_noreply, html_to_text
|
||||||
|
|
||||||
|
@ -11,12 +11,13 @@ from django.contrib.auth import get_user_model
|
|||||||
from django.db.transaction import atomic
|
from django.db.transaction import atomic
|
||||||
import django.core.mail
|
import django.core.mail
|
||||||
|
|
||||||
import looper.stripe_utils
|
from looper.pdf import PDFResponse
|
||||||
from looper.stripe_utils import (
|
from looper.stripe_utils import (
|
||||||
upsert_order_from_payment_intent_and_product,
|
upsert_order_from_payment_intent_and_product,
|
||||||
upsert_subscription_payment_method_from_setup_intent,
|
upsert_subscription_payment_method_from_setup_intent,
|
||||||
)
|
)
|
||||||
import looper.models
|
import looper.models
|
||||||
|
import looper.stripe_utils
|
||||||
import stripe
|
import stripe
|
||||||
|
|
||||||
from blender_fund_main.utils import is_noreply
|
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)
|
logger.debug('Sending payment %r notification to %s', order.status, email)
|
||||||
|
msg = django.core.mail.EmailMultiAlternatives(
|
||||||
django.core.mail.send_mail(
|
subject=subject,
|
||||||
subject,
|
body=email_body_txt,
|
||||||
message=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')
|
||||||
|
|
||||||
|
# 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)
|
logger.info('Sent %r notification to %s', order.status, email)
|
||||||
|
|
||||||
|
|
||||||
|
@ -14,6 +14,7 @@ from looper.tests import AbstractLooperTestCase
|
|||||||
from looper.tests.factories import SubscriptionFactory, create_customer_with_billing_address
|
from looper.tests.factories import SubscriptionFactory, create_customer_with_billing_address
|
||||||
import looper.exceptions
|
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
|
from blender_fund_main.utils import html_to_text
|
||||||
import blender_fund_main.tasks as tasks
|
import blender_fund_main.tasks as tasks
|
||||||
|
|
||||||
@ -246,6 +247,7 @@ class TestClockEmails(AbstractLooperTestCase):
|
|||||||
expected_soft_failed_email_body,
|
expected_soft_failed_email_body,
|
||||||
html_to_text(email.alternatives[0][0]),
|
html_to_text(email.alternatives[0][0]),
|
||||||
)
|
)
|
||||||
|
self.assertEqual(len(email.attachments), 0)
|
||||||
|
|
||||||
@responses.activate
|
@responses.activate
|
||||||
def test_automated_payment_failed_email_is_sent(self):
|
def test_automated_payment_failed_email_is_sent(self):
|
||||||
@ -310,6 +312,7 @@ class TestClockEmails(AbstractLooperTestCase):
|
|||||||
expected_failed_email_body,
|
expected_failed_email_body,
|
||||||
html_to_text(email.alternatives[0][0]),
|
html_to_text(email.alternatives[0][0]),
|
||||||
)
|
)
|
||||||
|
self.assertEqual(len(email.attachments), 0)
|
||||||
|
|
||||||
@responses.activate
|
@responses.activate
|
||||||
def test_automated_payment_paid_email_is_sent(self):
|
def test_automated_payment_paid_email_is_sent(self):
|
||||||
@ -371,6 +374,27 @@ class TestClockEmails(AbstractLooperTestCase):
|
|||||||
html_to_text(email.alternatives[0][0]),
|
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')
|
@override_settings(LOOPER_MANAGER_MAIL='admin@example.com')
|
||||||
def test_managed_subscription_notification_email_is_sent(self):
|
def test_managed_subscription_notification_email_is_sent(self):
|
||||||
now = timezone.now()
|
now = timezone.now()
|
||||||
@ -401,3 +425,4 @@ class TestClockEmails(AbstractLooperTestCase):
|
|||||||
expected_managed_email_body,
|
expected_managed_email_body,
|
||||||
html_to_text(email.alternatives[0][0]),
|
html_to_text(email.alternatives[0][0]),
|
||||||
)
|
)
|
||||||
|
self.assertEqual(len(email.attachments), 0)
|
||||||
|
@ -1,11 +1,9 @@
|
|||||||
from io import BytesIO
|
|
||||||
from unittest.mock import patch, Mock
|
from unittest.mock import patch, Mock
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.test import override_settings
|
from django.test import override_settings
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from freezegun import freeze_time
|
from freezegun import freeze_time
|
||||||
from pypdf import PdfReader
|
|
||||||
|
|
||||||
from looper.tests import AbstractLooperTestCase
|
from looper.tests import AbstractLooperTestCase
|
||||||
from looper.tests.factories import (
|
from looper.tests.factories import (
|
||||||
@ -16,36 +14,14 @@ from looper.tests.factories import (
|
|||||||
create_customer_with_billing_address,
|
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
|
production_storage = settings.STATICFILES_STORAGE
|
||||||
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,
|
# 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.status = 'paid'
|
||||||
self.paid_order.save()
|
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):
|
def test_get_pdf_unpaid_order_not_found(self):
|
||||||
unpaid_order = OrderFactory(
|
unpaid_order = OrderFactory(
|
||||||
customer=self.payment_method.customer,
|
customer=self.payment_method.customer,
|
||||||
@ -177,10 +147,10 @@ class TestReceiptPDFContent(AbstractLooperTestCase):
|
|||||||
|
|
||||||
self.assertEqual(200, response.status_code)
|
self.assertEqual(200, response.status_code)
|
||||||
|
|
||||||
pdf_text = self._extract_text_from_pdf(response)
|
pdf_text = extract_text_from_pdf(response)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
pdf_text,
|
pdf_text,
|
||||||
expected_text_tmpl.format(
|
expected_pdf_text_tmpl.format(
|
||||||
order=self.paid_order,
|
order=self.paid_order,
|
||||||
expected_address=(
|
expected_address=(
|
||||||
' Main st. 123\n'
|
' Main st. 123\n'
|
||||||
@ -190,6 +160,9 @@ class TestReceiptPDFContent(AbstractLooperTestCase):
|
|||||||
'Netherlands'
|
'Netherlands'
|
||||||
),
|
),
|
||||||
expected_currency_symbol='€',
|
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_payment_method='PayPal account test@example.com',
|
||||||
expected_total='9.90',
|
expected_total='9.90',
|
||||||
),
|
),
|
||||||
@ -214,13 +187,16 @@ class TestReceiptPDFContent(AbstractLooperTestCase):
|
|||||||
|
|
||||||
self.assertEqual(200, response.status_code)
|
self.assertEqual(200, response.status_code)
|
||||||
|
|
||||||
pdf_text = self._extract_text_from_pdf(response)
|
pdf_text = extract_text_from_pdf(response)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
pdf_text,
|
pdf_text,
|
||||||
expected_text_tmpl.format(
|
expected_pdf_text_tmpl.format(
|
||||||
order=order,
|
order=order,
|
||||||
expected_address=' -',
|
expected_address=' -',
|
||||||
expected_currency_symbol='€',
|
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_payment_method='PayPal account test@example.com',
|
||||||
expected_total='9.90',
|
expected_total='9.90',
|
||||||
),
|
),
|
||||||
@ -248,16 +224,19 @@ class TestReceiptPDFContent(AbstractLooperTestCase):
|
|||||||
|
|
||||||
self.assertEqual(200, response.status_code)
|
self.assertEqual(200, response.status_code)
|
||||||
|
|
||||||
pdf_text = self._extract_text_from_pdf(response)
|
pdf_text = extract_text_from_pdf(response)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
pdf_text,
|
pdf_text,
|
||||||
expected_text_tmpl.format(
|
expected_pdf_text_tmpl.format(
|
||||||
order=order,
|
order=order,
|
||||||
expected_address=' -',
|
expected_address=' -',
|
||||||
expected_currency_symbol='€',
|
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_payment_method='Bank Transfer',
|
||||||
expected_total='9.90',
|
expected_total='9.90',
|
||||||
) + expected_text_bank_details,
|
) + expected_pdf_text_bank_details,
|
||||||
pdf_text,
|
pdf_text,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -280,13 +259,16 @@ class TestReceiptPDFContent(AbstractLooperTestCase):
|
|||||||
|
|
||||||
self.assertEqual(200, response.status_code)
|
self.assertEqual(200, response.status_code)
|
||||||
|
|
||||||
pdf_text = self._extract_text_from_pdf(response)
|
pdf_text = extract_text_from_pdf(response)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
pdf_text,
|
pdf_text,
|
||||||
expected_text_tmpl.format(
|
expected_pdf_text_tmpl.format(
|
||||||
order=order,
|
order=order,
|
||||||
expected_address=' -',
|
expected_address=' -',
|
||||||
expected_currency_symbol='$',
|
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_payment_method='PayPal account test@example.com',
|
||||||
expected_total='14.80',
|
expected_total='14.80',
|
||||||
),
|
),
|
||||||
|
43
blender_fund_main/tests/utils.py
Normal file
43
blender_fund_main/tests/utils.py
Normal file
@ -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)
|
Loading…
Reference in New Issue
Block a user