Skip to content

Commit

Permalink
Merge pull request #841 from betagouv/828-reminder-email
Browse files Browse the repository at this point in the history
Envoi automatique d'emails de rappel cinque jours avant l'expiration
  • Loading branch information
alemangui authored Aug 13, 2024
2 parents 0c2f0d3 + ed7673e commit 8539084
Show file tree
Hide file tree
Showing 4 changed files with 166 additions and 6 deletions.
2 changes: 2 additions & 0 deletions api/views/declaration/declaration_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ def withdraw(self):

@status.transition(source=Status.OBSERVATION, target=Status.ABANDONED)
def abandon(self):
# Il n'est pas possible d'effectuer un abandon depuis l'API. Pour le FSM qui le prend
# en charge, regarder tasks.py.
self.error()

def ensure_validators(self, validators):
Expand Down
31 changes: 27 additions & 4 deletions config/tasks.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import logging
from datetime import timedelta
from datetime import datetime, time

from django.utils import timezone

Expand All @@ -17,7 +17,31 @@

@app.task
def send_expiration_reminder():
pass
"""
Cette tâche ne doit être effectuée qu'une seule fois par jour, car elle s'appuie sur
cette periodicité pour ne pas envoyer des doublons d'email
"""
declarations = Declaration.objects.filter(status__in=allowed_statuses)
brevo_template_id = 10
send_days_before = 5
for declaration in declarations:
try:
if not declaration.expiration_date:
continue
end_of_expiration_day = timezone.make_aware(datetime.combine(declaration.expiration_date, time.max))
today = timezone.now()
delta = end_of_expiration_day - today
if delta.days >= send_days_before and delta.days < send_days_before + 1:
parameters = {**declaration.brevo_parameters, **{"REMAINING_DAYS": send_days_before}}
email.send_sib_template(
brevo_template_id,
parameters,
declaration.author.email,
declaration.author.get_full_name(),
)

except Exception as _:
logger.exception(f"Could not send reminder email for declaration f{declaration.id}")


class EarlyExpirationError(Exception):
Expand Down Expand Up @@ -62,8 +86,7 @@ def abandon(self):
raise Exception()

today = timezone.now()
expiration_date = latest_snapshot.creation_date + timedelta(days=latest_snapshot.expiration_days)
if today < expiration_date:
if today < self.declaration.expiration_date:
raise EarlyExpirationError()


Expand Down
117 changes: 115 additions & 2 deletions config/tests/test_expiration_reminder.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,122 @@
from datetime import timedelta
from unittest import mock

from django.test import TestCase
from django.test.utils import override_settings
from django.utils import timezone

from config.tasks import send_expiration_reminder
from data.factories import ObservationDeclarationFactory, SnapshotFactory
from data.models import Declaration


class TestExpirationReminder(TestCase):
def test_send_reminders_before_expiration(self):
@override_settings(ANYMAIL={"SENDINBLUE_API_KEY": "fake-api-key"})
@override_settings(CONTACT_EMAIL="[email protected]")
@mock.patch("config.email.send_sib_template")
def test_send_reminders_before_expiration(self, mocked_brevo):
"""
Un rappel doit être envoyé à l'auteur·ice d'une déclaration avant son expiration
"""
pass
template_number = 10
today = timezone.now()
declaration = ObservationDeclarationFactory()
snapshot = SnapshotFactory(
declaration=declaration,
status=Declaration.DeclarationStatus.OBSERVATION,
expiration_days=10,
)
# Le snapshot a été créé il y a 5 jours. Vu que la date d'expiration
# est de 10 jours, on devrait envoyer l'email
snapshot.creation_date = today - timedelta(days=5, minutes=1)
snapshot.save()

send_expiration_reminder()

mocked_brevo.assert_called_once_with(
template_number,
{**declaration.brevo_parameters, **{"REMAINING_DAYS": 5}},
declaration.author.email,
declaration.author.get_full_name(),
)

@override_settings(ANYMAIL={"SENDINBLUE_API_KEY": "fake-api-key"})
@override_settings(CONTACT_EMAIL="[email protected]")
@mock.patch("config.email.send_sib_template")
def test_multiple_snapshots(self, mocked_brevo):
"""
Seulement le dernier snapshot « observation » ou « objection » sera pris en compte
"""
template_number = 10
today = timezone.now()
declaration = ObservationDeclarationFactory()
snapshot = SnapshotFactory(
declaration=declaration,
status=Declaration.DeclarationStatus.OBSERVATION,
expiration_days=10,
)

snapshot_old = SnapshotFactory(
declaration=declaration,
status=Declaration.DeclarationStatus.OBSERVATION,
expiration_days=15,
)
# Le dernier snapshot a été créé il y a 5 jours. Vu que la date d'expiration
# est de 10 jours, on devrait envoyer l'email
snapshot.creation_date = today - timedelta(days=5, minutes=1)
snapshot_old.creation_date = today - timedelta(days=25, minutes=1)
snapshot.save()
snapshot_old.save()

send_expiration_reminder()

mocked_brevo.assert_called_once_with(
template_number,
{**declaration.brevo_parameters, **{"REMAINING_DAYS": 5}},
declaration.author.email,
declaration.author.get_full_name(),
)

@override_settings(ANYMAIL={"SENDINBLUE_API_KEY": "fake-api-key"})
@override_settings(CONTACT_EMAIL="[email protected]")
@mock.patch("config.email.send_sib_template")
def test_do_not_send_early_reminders(self, mocked_brevo):
"""
Un rappel ne doit pas être envoyé trop tôt
"""
today = timezone.now()
declaration = ObservationDeclarationFactory()
snapshot = SnapshotFactory(
declaration=declaration,
status=Declaration.DeclarationStatus.OBSERVATION,
expiration_days=10,
)
# Le snapshot a été créé il y a 4 jours. Vu que la date d'expiration
# est de 10 jours, on a encore 6 jours devant nous. Donc pas d'envoi
snapshot.creation_date = today - timedelta(days=4, minutes=1)
snapshot.save()

send_expiration_reminder()
mocked_brevo.assert_not_called()

@override_settings(ANYMAIL={"SENDINBLUE_API_KEY": "fake-api-key"})
@override_settings(CONTACT_EMAIL="[email protected]")
@mock.patch("config.email.send_sib_template")
def test_do_not_send_late_reminders(self, mocked_brevo):
"""
Un rappel ne doit pas être envoyé trop tard
"""
today = timezone.now()
declaration = ObservationDeclarationFactory()
snapshot = SnapshotFactory(
declaration=declaration,
status=Declaration.DeclarationStatus.OBSERVATION,
expiration_days=10,
)
# Le snapshot a été créé il y a 6 jours. Vu que la date d'expiration
# est de 10 jours, on n'a que 4 jours devant nous. Donc pas d'envoi
snapshot.creation_date = today - timedelta(days=6, minutes=1)
snapshot.save()

send_expiration_reminder()
mocked_brevo.assert_not_called()
22 changes: 22 additions & 0 deletions data/models/declaration.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import json
from datetime import timedelta

from django.conf import settings
from django.db import models
Expand Down Expand Up @@ -210,6 +211,27 @@ def brevo_parameters(self):
"EXPIRATION_DAYS": expiration_days,
}

@property
def expiration_date(self):
expirable_statuses = [
Declaration.DeclarationStatus.OBJECTION,
Declaration.DeclarationStatus.OBSERVATION,
Declaration.DeclarationStatus.ABANDONED,
]
if self.status not in expirable_statuses:
return None
try:
latest_snapshot = self.snapshots.filter(
status__in=[
Declaration.DeclarationStatus.OBJECTION,
Declaration.DeclarationStatus.OBSERVATION,
]
).latest("creation_date")
expiration_date = latest_snapshot.creation_date + timedelta(days=latest_snapshot.expiration_days)
return expiration_date
except Exception as _:
return None

def __str__(self):
return f"Déclaration « {self.name} »"

Expand Down

0 comments on commit 8539084

Please sign in to comment.