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

Automatically give penalty when payment is overdue #3646

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
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
25 changes: 25 additions & 0 deletions lego/apps/events/notifications.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -75,6 +77,28 @@
)


class EventPaymentOverduePenaltyNotification(Notification):
name = EVENT_PAYMENT_OVERDUE_PENALTY

def generate_mail(self):
event = self.kwargs["event"]

Check warning on line 84 in lego/apps/events/notifications.py

View check run for this annotation

Codecov / codecov/patch

lego/apps/events/notifications.py#L84

Added line #L84 was not covered by tests

return self._delay_mail(

Check warning on line 86 in lego/apps/events/notifications.py

View check run for this annotation

Codecov / codecov/patch

lego/apps/events/notifications.py#L86

Added line #L86 was not covered by tests
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()

Check warning on line 99 in lego/apps/events/notifications.py

View check run for this annotation

Codecov / codecov/patch

lego/apps/events/notifications.py#L99

Added line #L99 was not covered by tests


class EventPaymentOverdueCreatorNotification(Notification):
name = EVENT_PAYMENT_OVERDUE_CREATOR

Expand All @@ -89,6 +113,7 @@
"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}",
Expand Down
66 changes: 65 additions & 1 deletion lego/apps/events/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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()
Expand Down Expand Up @@ -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):
"""
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
{% extends "email/base.html" %}

{% block alert %}

<tr>
<td class="alert alert-bad">
Du har ikke betalt påmelding til et arrangement. Du har derfor mottatt en prikk og blitt avmeldt arrangementet.
</td>
</tr>

{% endblock %}

{% block content %}

<tr>
<td class="content-block">
Hei, {{ first_name }}!
</td>
</tr>

<tr>
<td class="content-block">
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.
</td>
</tr>

<tr>
<td class="button">
<div>
<!--[if mso]>
<v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" href="{{ frontend_url }}/events/{{ id }}" style="height:45px;v-text-anchor:middle;width:155px;" arcsize="15%" strokecolor="#ffffff" fillcolor="#c0392b">
<w:anchorlock/>
<center style="color:#ffffff;font-family:Helvetica, Arial, sans-serif;font-size:14px;font-weight:regular;">Betal her</center>
</v:roundrect>
<![endif]-->
<a class="btn-primary" href="{{ frontend_url }}/events/{{ id }}"
style="background-color:#c0392b;border-radius:5px;color:#ffffff;display:inline-block;font-family:'Cabin', Helvetica, Arial, sans-serif;font-size:14px;font-weight:regular;line-height:45px;text-align:center;text-decoration:none;width:155px;-webkit-text-size-adjust:none;mso-hide:all;">
Betal her
</a>
</div>
</td>
</tr>


{% endblock %}
Original file line number Diff line number Diff line change
@@ -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 %}
72 changes: 72 additions & 0 deletions lego/apps/events/tests/test_async_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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()
2 changes: 2 additions & 0 deletions lego/apps/notifications/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -55,6 +56,7 @@
EVENT_ADMIN_REGISTRATION,
EVENT_ADMIN_UNREGISTRATION,
EVENT_PAYMENT_OVERDUE,
EVENT_PAYMENT_OVERDUE_PENALTY,
MEETING_INVITE,
MEETING_INVITATION_REMINDER,
PENALTY_CREATION,
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
),
),
]
1 change: 1 addition & 0 deletions lego/apps/users/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
4 changes: 4 additions & 0 deletions lego/settings/celery.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down