diff --git a/lego/apps/events/notifications.py b/lego/apps/events/notifications.py index 35ab8edbb..021a979c9 100644 --- a/lego/apps/events/notifications.py +++ b/lego/apps/events/notifications.py @@ -8,8 +8,10 @@ EVENT_BUMP, EVENT_PAYMENT_OVERDUE, EVENT_PAYMENT_OVERDUE_CREATOR, + EVENT_PAYMENT_OVERDUE_PENALTY, ) from lego.apps.notifications.notification import Notification +from lego.apps.users.constants import PENALTY_WEIGHTS class EventBumpNotification(Notification): @@ -75,6 +77,28 @@ def generate_push(self): ) +class EventPaymentOverduePenaltyNotification(Notification): + name = EVENT_PAYMENT_OVERDUE_PENALTY + + def generate_mail(self): + event = self.kwargs["event"] + + return self._delay_mail( + to_email=self.user.email, + context={ + "event": event.title, + "first_name": self.user.first_name, + "id": event.id, + }, + subject=f"Du har ikke betalt påmeldingen på arrangementet {event.title}", + plain_template="events/email/payment_overdue_penalty.txt", + html_template="events/email/payment_overdue_penalty.html", + ) + + def generate_push(self): + return super().generate_push() + + class EventPaymentOverdueCreatorNotification(Notification): name = EVENT_PAYMENT_OVERDUE_CREATOR @@ -89,6 +113,7 @@ def generate_mail(self): "users": users, "first_name": self.user.first_name, "id": event.id, + "weight": PENALTY_WEIGHTS.PAYMENT_OVERDUE, }, subject=f"Følgende registrerte har ikke betalt påmeldingen til arrangementet" f" {event.title}", diff --git a/lego/apps/events/tasks.py b/lego/apps/events/tasks.py index 661e2b9e0..8294b9169 100644 --- a/lego/apps/events/tasks.py +++ b/lego/apps/events/tasks.py @@ -18,7 +18,10 @@ WebhookDidNotFindRegistration, ) from lego.apps.events.models import Event, Registration -from lego.apps.events.notifications import EventPaymentOverdueCreatorNotification +from lego.apps.events.notifications import ( + EventPaymentOverdueCreatorNotification, + EventPaymentOverduePenaltyNotification, +) from lego.apps.events.serializers.registrations import ( StripeChargeSerializer, StripePaymentIntentSerializer, @@ -30,6 +33,8 @@ notify_user_payment_initiated, notify_user_registration, ) +from lego.apps.users.constants import PENALTY_TYPES, PENALTY_WEIGHTS +from lego.apps.users.models import Penalty from lego.utils.tasks import AbakusTask log = get_logger() @@ -571,6 +576,65 @@ def notify_event_creator_when_payment_overdue(self, logger_context=None): ) +@celery_app.task(serializer="json", bind=True, base=AbakusTask) +def handle_overdue_payment(self, logger_context=None): + """ + Task that automatically assigns penalty, unregisters user from event + and notifies them when payment is overdue. + """ + + self.setup_logger(logger_context) + + time = timezone.now() + events = ( + Event.objects.filter( + payment_due_date__lte=time, + is_priced=True, + use_stripe=True, + end_time__gte=time, + ) + .exclude(registrations=None) + .prefetch_related("registrations") + ) + for event in events: + overdue_registrations = ( + event.registrations.exclude(pool=None) + .exclude( + payment_status__in=[constants.PAYMENT_MANUAL, constants.PAYMENT_SUCCESS] + ) + .prefetch_related("user") + ) + if overdue_registrations: + for registration in overdue_registrations: + user = registration.user + + if not user.penalties.filter(source_event=event).exists(): + Penalty.objects.create( + user=user, + reason=f"Betalte ikke for {event.title} i tide.", + weight=PENALTY_WEIGHTS.PAYMENT_OVERDUE, + source_event=event, + type=PENALTY_TYPES.PAYMENT, + ) + + event.unregister( + registration, + # Needed to not give default penalty + admin_unregistration_reason="Automated unregister", + ) + + notification = EventPaymentOverduePenaltyNotification( + user=user, + event=event, + ) + notification.notify() + log.info( + "user_is_given_penalty_is_unregistered_and_notified", + event_id=event.id, + registration=registration, + ) + + @celery_app.task(serializer="json", bind=True, base=AbakusTask) def check_that_pool_counters_match_registration_number(self, logger_context=None): """ diff --git a/lego/apps/events/templates/events/email/payment_overdue_penalty.html b/lego/apps/events/templates/events/email/payment_overdue_penalty.html new file mode 100644 index 000000000..f77b157f3 --- /dev/null +++ b/lego/apps/events/templates/events/email/payment_overdue_penalty.html @@ -0,0 +1,47 @@ +{% extends "email/base.html" %} + +{% block alert %} + + + + Du har ikke betalt påmelding til et arrangement. Du har derfor mottatt en prikk og blitt avmeldt arrangementet. + + + +{% endblock %} + +{% block content %} + + + + Hei, {{ first_name }}! + + + + + + Du har ikke betalt for {{ event }} og fristen for betaling har gått ut. + Dermed har du blitt avregistrert fra arrangementet og mottatt en prikk med vekt {{ weight }}. + Du har også blitt avregistrert fra arrangementet. + + + + + +
+ + + Betal her + +
+ + + + +{% endblock %} diff --git a/lego/apps/events/templates/events/email/payment_overdue_penalty.txt b/lego/apps/events/templates/events/email/payment_overdue_penalty.txt new file mode 100644 index 000000000..f46b70053 --- /dev/null +++ b/lego/apps/events/templates/events/email/payment_overdue_penalty.txt @@ -0,0 +1,13 @@ +{% extends "email/base.txt" %} + +{% block content %} + +Hei, {{ first_name }}! + +Du har ikke betalt for {{ event }} og fristen for betaling har gått ut. + +Dermed har du blitt avregistrert fra arrangementet og mottatt en prikk med vekt {{ weight }}. + +Du kan se alle prikkene dine på {{ frontend_url }}/users/me/ + +{% endblock %} diff --git a/lego/apps/events/tests/test_async_tasks.py b/lego/apps/events/tests/test_async_tasks.py index 5aaaa5ece..acc0221fd 100644 --- a/lego/apps/events/tests/test_async_tasks.py +++ b/lego/apps/events/tests/test_async_tasks.py @@ -15,11 +15,13 @@ bump_waiting_users_to_new_pool, check_events_for_registrations_with_expired_penalties, check_that_pool_counters_match_registration_number, + handle_overdue_payment, notify_event_creator_when_payment_overdue, notify_user_when_payment_soon_overdue, set_all_events_ready_and_bump, ) from lego.apps.events.tests.utils import get_dummy_users, make_penalty_expire +from lego.apps.users.constants import PENALTY_WEIGHTS from lego.apps.users.models import AbakusGroup, Penalty from lego.utils.test_utils import BaseAPITestCase, BaseTestCase @@ -780,3 +782,73 @@ def test_creator_notification_is_not_sent_past_end_time(self, mock_notification) notify_event_creator_when_payment_overdue.delay() mock_notification.assert_not_called() + + @mock.patch("lego.apps.events.tasks.EventPaymentOverduePenaltyNotification") + def test_user_is_given_penalty_is_unregistered_and_notified( + self, mock_notification + ): + """ + Test that user is given a penalty, is unregistered and notified when payment is overdue + """ + + self.event.payment_due_date = timezone.now() - timedelta(days=2) + self.event.save() + + user = get_dummy_users(1)[0] + AbakusGroup.objects.get(name="Abakus").add_user(user) + registration_two = Registration.objects.get_or_create( + event=self.event, user=user + )[0] + registration_two.payment_status = constants.PAYMENT_PENDING + self.event.register(registration_two) + + number_of_registrations_before = self.event.number_of_registrations + number_of_penalties_before = registration_two.user.number_of_penalties() + + handle_overdue_payment.delay() + registration_two.refresh_from_db() + + self.assertLess( + self.event.number_of_registrations, number_of_registrations_before + ) + self.assertEqual(registration_two.status, constants.SUCCESS_UNREGISTER) + self.assertEqual( + registration_two.user.number_of_penalties(), + number_of_penalties_before + int(PENALTY_WEIGHTS.PAYMENT_OVERDUE), + ) + mock_notification.assert_called() + call = mock_notification.mock_calls[2] + self.assertEqual(call[2]["user"], user) + + @mock.patch("lego.apps.events.tasks.EventPaymentOverduePenaltyNotification") + def test_user_is_not_given_penalty_is_not_unregistered_and_not_notified( + self, mock_notification + ): + """ + Test that user is NOT given a penalty, is unregistered and notified when payment is overdue + """ + self.event.payment_due_date = timezone.now() + timedelta(days=2) + self.event.save() + + user = get_dummy_users(1)[0] + AbakusGroup.objects.get(name="Abakus").add_user(user) + registration_two = Registration.objects.get_or_create( + event=self.event, user=user + )[0] + registration_two.payment_status = constants.PAYMENT_PENDING + self.event.register(registration_two) + + number_of_registrations_before = self.event.number_of_registrations + number_of_penalties_before = registration_two.user.number_of_penalties() + + handle_overdue_payment.delay() + registration_two.refresh_from_db() + + self.assertEqual( + self.event.number_of_registrations, number_of_registrations_before + ) + self.assertEqual(registration_two.status, constants.SUCCESS_REGISTER) + self.assertEqual( + registration_two.user.number_of_penalties(), number_of_penalties_before + ) + mock_notification.assert_not_called() diff --git a/lego/apps/notifications/constants.py b/lego/apps/notifications/constants.py index fdf9a5363..1db9ff751 100644 --- a/lego/apps/notifications/constants.py +++ b/lego/apps/notifications/constants.py @@ -14,6 +14,7 @@ EVENT_ADMIN_UNREGISTRATION = "event_admin_unregistration" EVENT_PAYMENT_OVERDUE = "event_payment_overdue" EVENT_PAYMENT_OVERDUE_CREATOR = "event_payment_overdue_creator" +EVENT_PAYMENT_OVERDUE_PENALTY = "event_payment_overdue_penalty" # Meeting MEETING_INVITE = "meeting_invite" @@ -55,6 +56,7 @@ EVENT_ADMIN_REGISTRATION, EVENT_ADMIN_UNREGISTRATION, EVENT_PAYMENT_OVERDUE, + EVENT_PAYMENT_OVERDUE_PENALTY, MEETING_INVITE, MEETING_INVITATION_REMINDER, PENALTY_CREATION, diff --git a/lego/apps/notifications/migrations/0017_alter_notificationsetting_notification_type.py b/lego/apps/notifications/migrations/0017_alter_notificationsetting_notification_type.py new file mode 100644 index 000000000..9056edf18 --- /dev/null +++ b/lego/apps/notifications/migrations/0017_alter_notificationsetting_notification_type.py @@ -0,0 +1,41 @@ +# Generated by Django 4.0.10 on 2024-10-09 21:17 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("notifications", "0016_alter_notificationsetting_channels"), + ] + + operations = [ + migrations.AlterField( + model_name="notificationsetting", + name="notification_type", + field=models.CharField( + choices=[ + ("announcement", "announcement"), + ("restricted_mail_sent", "restricted_mail_sent"), + ("weekly_mail", "weekly_mail"), + ("event_bump", "event_bump"), + ("event_admin_registration", "event_admin_registration"), + ("event_admin_unregistration", "event_admin_unregistration"), + ("event_payment_overdue", "event_payment_overdue"), + ("event_payment_overdue_penalty", "event_payment_overdue_penalty"), + ("meeting_invite", "meeting_invite"), + ("meeting_invitation_reminder", "meeting_invitation_reminder"), + ("penalty_creation", "penalty_creation"), + ("comment", "comment"), + ("comment_reply", "comment_reply"), + ("registration_reminder", "registration_reminder"), + ("survey_created", "survey_created"), + ("event_payment_overdue_creator", "event_payment_overdue_creator"), + ("company_interest_created", "company_interest_created"), + ("inactive_warning", "inactive_warning"), + ("deleted_warning", "deleted_warning"), + ], + max_length=64, + ), + ), + ] diff --git a/lego/apps/users/constants.py b/lego/apps/users/constants.py index 899ff7242..62f65fe08 100644 --- a/lego/apps/users/constants.py +++ b/lego/apps/users/constants.py @@ -151,6 +151,7 @@ def values(cls) -> list[str]: class PENALTY_WEIGHTS(models.TextChoices): LATE_PRESENCE = 1 + PAYMENT_OVERDUE = 2 class PENALTY_TYPES(models.TextChoices): diff --git a/lego/settings/celery.py b/lego/settings/celery.py index 0d98bea98..0fc5ef53d 100644 --- a/lego/settings/celery.py +++ b/lego/settings/celery.py @@ -56,6 +56,10 @@ def on_setup_logging(**kwargs): "task": "lego.apps.events.tasks.notify_event_creator_when_payment_overdue", "schedule": crontab(hour=9, minute=0), }, + "handle_overdue_payment": { + "task": "lego.apps.events.tasks.handle_overdue_payment", + "schedule": crontab(hour=21, minute=0), + }, "sync-external-systems": { "task": "lego.apps.external_sync.tasks.sync_external_systems", "schedule": crontab(hour="*", minute=0),