Skip to content

Commit d2a5b29

Browse files
authored
Postpaid follow up email (#4712)
This adds an email for challenge organizers and the invoice contact person that informs them of the approaching follow_up date for post-paid invoices and the consequences of this date. The email is sent every 2 weeks (if necessary, i.e. for initialized post-paid invoices with an approaching follow-up date). Part of DIAGNijmegen/rse-roadmap#475
1 parent b7e94e0 commit d2a5b29

5 files changed

Lines changed: 175 additions & 0 deletions

File tree

app/config/settings.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1250,6 +1250,10 @@ def get_private_ip():
12501250
"task": "grandchallenge.invoices.tasks.send_challenge_invoice_overdue_reminder_emails",
12511251
"schedule": crontab(day_of_month=1, hour=6, minute=0),
12521252
},
1253+
"send_post_paid_invoice_follow_up_emails": {
1254+
"task": "grandchallenge.invoices.tasks.send_post_paid_invoice_follow_up_emails",
1255+
"schedule": crontab(day_of_month="1,14", hour=5, minute=0),
1256+
},
12531257
"send_challenge_request_draft_reminder_emails": {
12541258
"task": "grandchallenge.challenges.tasks.send_challenge_request_draft_reminder_emails",
12551259
"schedule": crontab(day_of_month="1,14", hour=6, minute=0),

app/grandchallenge/invoices/emails.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from django.contrib.sites.models import Site
55
from django.core.mail import mail_managers
66
from django.template.loader import render_to_string
7+
from django.utils.formats import date_format
78
from django.utils.html import format_html
89

910
from grandchallenge.emails.emails import send_standard_email_batch
@@ -93,3 +94,30 @@ def send_challenge_invoice_issued_notification(invoice):
9394
recipients=recipients,
9495
subscription_type=EmailSubscriptionTypes.SYSTEM,
9596
)
97+
98+
99+
def send_postpaid_invoice_follow_up_date_approaching_email(invoice):
100+
subject = format_html(
101+
"[{challenge_name}] Post-paid invoice date approaching",
102+
challenge_name=invoice.challenge.short_name,
103+
)
104+
message = render_to_string(
105+
"invoices/partials/post_paid_invoice_follow_up_email.md",
106+
context={
107+
"challenge_name": invoice.challenge.short_name,
108+
"follow_up_on_date": date_format(invoice.follow_up_on, "F jS Y"),
109+
"billing_address": invoice.billing_address,
110+
"contact_name": invoice.contact_name,
111+
"contact_email": invoice.contact_email,
112+
"vat_number": invoice.vat_number,
113+
},
114+
)
115+
recipients = get_challenge_invoice_recipients(invoice)
116+
117+
send_standard_email_batch(
118+
site=Site.objects.get_current(),
119+
subject=subject,
120+
markdown_message=message,
121+
recipients=recipients,
122+
subscription_type=EmailSubscriptionTypes.SYSTEM,
123+
)

app/grandchallenge/invoices/tasks.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from dateutil.relativedelta import relativedelta
12
from django.core.mail import mail_managers
23
from django.db import transaction
34
from django.template.loader import render_to_string
@@ -10,6 +11,7 @@
1011
from grandchallenge.invoices.emails import (
1112
send_challenge_invoice_issued_notification,
1213
send_challenge_invoice_overdue_reminder,
14+
send_postpaid_invoice_follow_up_date_approaching_email,
1315
)
1416

1517

@@ -80,3 +82,17 @@ def send_open_invoices_email():
8082
subject=subject,
8183
message=message,
8284
)
85+
86+
87+
@acks_late_micro_short_task
88+
@transaction.atomic
89+
def send_post_paid_invoice_follow_up_emails():
90+
from grandchallenge.invoices.models import Invoice
91+
92+
invoices = Invoice.objects.filter(
93+
payment_type=Invoice.PaymentTypeChoices.POSTPAID,
94+
payment_status=Invoice.PaymentStatusChoices.INITIALIZED,
95+
follow_up_on__lte=now().date() + relativedelta(months=1, days=1),
96+
)
97+
for invoice in invoices:
98+
send_postpaid_invoice_follow_up_date_approaching_email(invoice)
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
We're writing to let you know that a post-paid invoice for your challenge {{challenge_name}} is scheduled to be issued
2+
on {{follow_up_on_date}}.
3+
4+
The invoice amount will be based on your usage up to that date and will be rounded up to the nearest €250.
5+
If your challenge has not incurred any costs beyond what has already been invoiced, no further invoice will be sent.
6+
7+
We have the following billing details on file for this invoice. Please let us know if any of those details are
8+
incorrect.
9+
10+
**Billing address:**
11+
{{billing_address}}
12+
**Contact name:**
13+
{{contact_name}}
14+
**Contact email:**
15+
{{contact_email}}
16+
**VAT number:**
17+
{{vat_number}}
18+
19+
Please note that this invoice marks the end of your challenge. Once it has been issued, any remaining post-paid budget —
20+
beyond any excess within the €250 block — will no longer be valid and your challenge will be closed for new submissions.
21+
If you would like to keep your challenge open, please get in touch to arrange a new post-paid capacity reservation.
22+
23+
If you have any questions or would like to make changes before the invoice is issued, please reply to this email and we
24+
will be happy to help.

app/tests/invoices_tests/test_tasks.py

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
1+
from unittest.mock import MagicMock, call
12
from zoneinfo import ZoneInfo
23

34
import pytest
5+
from dateutil.utils import today
46
from django.core import mail
7+
from django.utils.formats import date_format
58
from django.utils.timezone import datetime, now, timedelta
69

710
from grandchallenge.invoices.models import Invoice
811
from grandchallenge.invoices.tasks import (
912
send_challenge_invoice_issued_notification_emails,
1013
send_challenge_invoice_overdue_reminder_emails,
1114
send_open_invoices_email,
15+
send_post_paid_invoice_follow_up_emails,
1216
)
1317
from tests.factories import ChallengeFactory, UserFactory
1418
from tests.invoices_tests.factories import InvoiceFactory
@@ -449,3 +453,102 @@ def test_send_open_invoices_email_not_sent_when_no_invoices(settings):
449453
send_open_invoices_email()
450454

451455
assert len(mail.outbox) == 0
456+
457+
458+
@pytest.mark.django_db
459+
def test_send_post_paid_invoice_follow_up_emails_content():
460+
challenge = ChallengeFactory()
461+
challenge_admin = challenge.creator
462+
contact_email = "contact_person@example.com"
463+
464+
invoice = InvoiceFactory(
465+
challenge=challenge,
466+
support_costs_euros=0,
467+
compute_costs_euros=10,
468+
storage_costs_euros=0,
469+
payment_type=Invoice.PaymentTypeChoices.POSTPAID,
470+
payment_status=Invoice.PaymentStatusChoices.INITIALIZED,
471+
follow_up_on=today() + timedelta(days=1),
472+
contact_email=contact_email,
473+
contact_name="John Doe",
474+
billing_address="Some Street 12, 12345 SomeCity, SomeCountry",
475+
vat_number="12345",
476+
)
477+
478+
send_post_paid_invoice_follow_up_emails()
479+
480+
expected_subject = (
481+
"[{challenge_name}] Post-paid invoice date approaching".format(
482+
challenge_name=challenge.short_name,
483+
)
484+
)
485+
486+
# challenge admin and invoice contact person are emailed
487+
assert len(mail.outbox) == 2
488+
for email in mail.outbox:
489+
assert expected_subject in email.subject
490+
assert date_format(invoice.follow_up_on, "F jS Y") in email.body
491+
assert invoice.billing_address in email.body
492+
assert invoice.contact_name in email.body
493+
assert invoice.contact_email in email.body
494+
assert invoice.vat_number in email.body
495+
496+
recipient_emails = [email.to[0] for email in mail.outbox]
497+
assert challenge_admin.email in recipient_emails
498+
assert contact_email in recipient_emails
499+
500+
501+
@pytest.mark.django_db
502+
def test_send_post_paid_invoice_follow_up_emails_queryset(mocker):
503+
challenge = ChallengeFactory()
504+
505+
postpaid_relevant = InvoiceFactory(
506+
challenge=challenge,
507+
payment_type=Invoice.PaymentTypeChoices.POSTPAID,
508+
payment_status=Invoice.PaymentStatusChoices.INITIALIZED,
509+
follow_up_on=today() + timedelta(days=1),
510+
)
511+
# create a bunch of other invoices that should not trigger an email
512+
InvoiceFactory(
513+
challenge=challenge,
514+
payment_type=Invoice.PaymentTypeChoices.POSTPAID,
515+
payment_status=Invoice.PaymentStatusChoices.INITIALIZED,
516+
follow_up_on=today() + timedelta(days=50),
517+
)
518+
InvoiceFactory(
519+
challenge=challenge,
520+
payment_type=Invoice.PaymentTypeChoices.POSTPAID,
521+
payment_status=Invoice.PaymentStatusChoices.REQUESTED,
522+
)
523+
InvoiceFactory(
524+
challenge=challenge,
525+
payment_type=Invoice.PaymentTypeChoices.POSTPAID,
526+
payment_status=Invoice.PaymentStatusChoices.ISSUED,
527+
)
528+
InvoiceFactory(
529+
challenge=challenge,
530+
payment_type=Invoice.PaymentTypeChoices.POSTPAID,
531+
payment_status=Invoice.PaymentStatusChoices.PAID,
532+
)
533+
InvoiceFactory(
534+
challenge=challenge,
535+
payment_type=Invoice.PaymentTypeChoices.PREPAID,
536+
)
537+
InvoiceFactory(
538+
challenge=challenge,
539+
payment_type=Invoice.PaymentTypeChoices.COMPLIMENTARY,
540+
)
541+
542+
mock_method = mocker.patch(
543+
"grandchallenge.invoices.tasks.send_postpaid_invoice_follow_up_date_approaching_email",
544+
return_value=MagicMock(),
545+
)
546+
547+
send_post_paid_invoice_follow_up_emails()
548+
549+
assert mock_method.call_count == 1
550+
mock_method.assert_has_calls(
551+
[
552+
call(postpaid_relevant),
553+
],
554+
)

0 commit comments

Comments
 (0)