Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Envoi automatique d'emails de rappel cinq jours avant l'expiration #841

Merged
merged 2 commits into from
Aug 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading