diff --git a/core/baker_recipes.py b/core/baker_recipes.py index 7e49e09b..0abd83e0 100644 --- a/core/baker_recipes.py +++ b/core/baker_recipes.py @@ -1,7 +1,8 @@ from datetime import timedelta +from dateutil.relativedelta import relativedelta from django.conf import settings -from django.utils.timezone import now +from django.utils.timezone import localdate, now from model_bakery import seq from model_bakery.recipe import Recipe, related @@ -11,13 +12,13 @@ from subscription.models import Subscription active_subscription = Recipe( Subscription, - subscription_start=now() - timedelta(days=30), - subscription_end=now() + timedelta(days=30), + subscription_start=localdate() - timedelta(days=30), + subscription_end=localdate() + timedelta(days=30), ) ended_subscription = Recipe( Subscription, - subscription_start=now() - timedelta(days=60), - subscription_end=now() - timedelta(days=30), + subscription_start=localdate() - timedelta(days=60), + subscription_end=localdate() - timedelta(days=30), ) subscriber_user = Recipe( @@ -36,6 +37,17 @@ old_subscriber_user = Recipe( ) """A user with an ended subscription.""" +__inactivity = localdate() - settings.SITH_ACCOUNT_INACTIVITY_DELTA +very_old_subscriber_user = old_subscriber_user.extend( + subscriptions=related( + ended_subscription.extend( + subscription_start=__inactivity - relativedelta(months=6, days=1), + subscription_end=__inactivity - relativedelta(days=1), + ) + ) +) +"""A user which subscription ended enough time ago to be considered as inactive.""" + ae_board_membership = Recipe( Membership, start_date=now() - timedelta(days=30), diff --git a/core/tests/test_user.py b/core/tests/test_user.py index 5454a302..25125d5c 100644 --- a/core/tests/test_user.py +++ b/core/tests/test_user.py @@ -7,12 +7,15 @@ from django.test import Client, TestCase from django.urls import reverse from django.utils.timezone import now from model_bakery import baker, seq -from model_bakery.recipe import Recipe, related +from model_bakery.recipe import Recipe -from core.baker_recipes import old_subscriber_user, subscriber_user +from core.baker_recipes import ( + old_subscriber_user, + subscriber_user, + very_old_subscriber_user, +) from core.models import User from counter.models import Counter, Refilling, Selling -from subscription.models import Subscription class TestSearchUsers(TestCase): @@ -121,9 +124,6 @@ class TestFilterInactive(TestCase): def setUpTestData(cls): time_active = now() - settings.SITH_ACCOUNT_INACTIVITY_DELTA + timedelta(days=1) time_inactive = time_active - timedelta(days=3) - very_old_subscriber = old_subscriber_user.extend( - subscriptions=related(Recipe(Subscription, subscription_end=time_inactive)) - ) counter, seller = baker.make(Counter), baker.make(User) sale_recipe = Recipe( Selling, @@ -137,7 +137,7 @@ class TestFilterInactive(TestCase): baker.make(User), subscriber_user.make(), old_subscriber_user.make(), - *very_old_subscriber.make(_quantity=3), + *very_old_subscriber_user.make(_quantity=3), ] sale_recipe.make(customer=cls.users[3].customer, date=time_active) baker.make( diff --git a/counter/management/commands/dump_warning_mail.py b/counter/management/commands/dump_warning_mail.py index 0a1d7f78..2b8fbfdd 100644 --- a/counter/management/commands/dump_warning_mail.py +++ b/counter/management/commands/dump_warning_mail.py @@ -4,7 +4,7 @@ from smtplib import SMTPException from django.conf import settings from django.core.mail import send_mail from django.core.management.base import BaseCommand -from django.db.models import Exists, OuterRef, Subquery +from django.db.models import Exists, OuterRef, QuerySet, Subquery from django.template.loader import render_to_string from django.utils.timezone import localdate, now from django.utils.translation import gettext as _ @@ -26,23 +26,7 @@ class Command(BaseCommand): super().__init__(*args, **kwargs) def handle(self, *args, **options): - ongoing_dump_operation = AccountDump.objects.ongoing().filter( - customer__user=OuterRef("pk") - ) - users = list( - User.objects - .filter_inactive() - .filter(customer__amount__gt=0) - .exclude(Exists(ongoing_dump_operation)) - .annotate( - last_subscription_date=Subquery( - Subscription.objects.filter(member=OuterRef("pk")) - .order_by("-subscription_end") - .values("subscription_end")[:1] - ), - ) - .select_related("customer") - ) + users = list(self._get_users()) self.stdout.write(f"{len(users)} users will be warned of their account dump") dumps = [] for user in users: @@ -57,6 +41,25 @@ class Command(BaseCommand): AccountDump.objects.bulk_create(dumps) self.stdout.write("Finished !") + @staticmethod + def _get_users() -> QuerySet[User]: + ongoing_dump_operation = AccountDump.objects.ongoing().filter( + customer__user=OuterRef("pk") + ) + return ( + User.objects.filter_inactive() + .filter(customer__amount__gt=0) + .exclude(Exists(ongoing_dump_operation)) + .annotate( + last_subscription_date=Subquery( + Subscription.objects.filter(member=OuterRef("pk")) + .order_by("-subscription_end") + .values("subscription_end")[:1] + ), + ) + .select_related("customer") + ) + def _send_mail(self, user: User) -> bool: """Send the warning email to the given user. diff --git a/counter/tests/test_account_dump.py b/counter/tests/test_account_dump.py index 79228c26..49882cfe 100644 --- a/counter/tests/test_account_dump.py +++ b/counter/tests/test_account_dump.py @@ -1,7 +1,65 @@ -import pytest +from datetime import timedelta + +from django.conf import settings +from django.core import mail +from django.core.management import call_command +from django.test import TestCase +from django.utils.timezone import now +from model_bakery import baker +from model_bakery.recipe import Recipe + +from core.baker_recipes import subscriber_user, very_old_subscriber_user +from counter.management.commands.dump_warning_mail import Command +from counter.models import AccountDump, Customer, Refilling -@pytest.mark.django_db -def test_account_dump(): - # TODO write the fucking test - pass +class TestAccountDumpWarningMailCommand(TestCase): + @classmethod + def setUpTestData(cls): + # delete existing customers to avoid side effect + Customer.objects.all().delete() + refill_recipe = Recipe(Refilling, amount=10) + cls.notified_users = very_old_subscriber_user.make(_quantity=3) + inactive_date = ( + now() - settings.SITH_ACCOUNT_INACTIVITY_DELTA - timedelta(days=1) + ) + refill_recipe.make( + customer=(u.customer for u in cls.notified_users), + date=inactive_date, + _quantity=len(cls.notified_users), + ) + cls.not_notified_users = [ + subscriber_user.make(), + very_old_subscriber_user.make(), # inactive, but account already empty + very_old_subscriber_user.make(), # inactive, but with a recent transaction + very_old_subscriber_user.make(), # inactive, but already warned + ] + refill_recipe.make( + customer=cls.not_notified_users[2].customer, date=now() - timedelta(days=1) + ) + refill_recipe.make( + customer=cls.not_notified_users[3].customer, date=inactive_date + ) + baker.make( + AccountDump, + customer=cls.not_notified_users[3].customer, + dump_operation=None, + ) + + def test_user_selection(self): + """Test that the user to warn are well selected.""" + users = list(Command._get_users()) + assert len(users) == 3 + assert set(users) == set(self.notified_users) + + def test_command(self): + """The actual command test.""" + call_command("dump_warning_mail") + # 1 already existing + 3 new account dump objects + assert AccountDump.objects.count() == 4 + sent_mails = list(mail.outbox) + assert len(sent_mails) == 3 + target_emails = {u.email for u in self.notified_users} + for sent in sent_mails: + assert len(sent.to) == 1 + assert sent.to[0] in target_emails