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.
+ |
+
+
+
+
+
+ |
+
+
+
+{% 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),