diff --git a/api/views/declaration/declaration_flow.py b/api/views/declaration/declaration_flow.py index fca297d2..dbca696d 100644 --- a/api/views/declaration/declaration_flow.py +++ b/api/views/declaration/declaration_flow.py @@ -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): diff --git a/config/tasks.py b/config/tasks.py index d55ad608..3faa41df 100644 --- a/config/tasks.py +++ b/config/tasks.py @@ -1,5 +1,5 @@ import logging -from datetime import timedelta +from datetime import datetime, time from django.utils import timezone @@ -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): @@ -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() diff --git a/config/tests/test_expiration_reminder.py b/config/tests/test_expiration_reminder.py index 9d6725e3..2bce73a0 100644 --- a/config/tests/test_expiration_reminder.py +++ b/config/tests/test_expiration_reminder.py @@ -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="contact@example.com") + @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="contact@example.com") + @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="contact@example.com") + @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="contact@example.com") + @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() diff --git a/data/models/declaration.py b/data/models/declaration.py index b653aeda..618d3dc5 100644 --- a/data/models/declaration.py +++ b/data/models/declaration.py @@ -1,4 +1,5 @@ import json +from datetime import timedelta from django.conf import settings from django.db import models @@ -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} »"