fix(events): keep registration status reads nonblocking

This commit is contained in:
2026-10-01 13:35:17 +03:30
parent 962238c32c
commit f924fcc52e
16 changed files with 498 additions and 107 deletions

View File

@@ -1,4 +1,4 @@
from django.db import models
from django.db import models, transaction
from django.conf import settings
from django.utils import timezone
from django.utils.text import slugify
@@ -201,39 +201,59 @@ class Registration(BaseModel):
"""Human-readable label for the current registration status."""
return self.get_status_display()
@transaction.atomic
def save(self, *args, **kwargs):
# detect create vs update
is_create = self._state.adding
old_status = None
is_create = self._state.adding
old_status = None
if not is_create and self.pk:
old_status = (
self.__class__.objects.only("status").get(pk=self.pk).status
)
if not is_create and self.pk:
old_status = self.__class__.objects.only("status").get(pk=self.pk).status
# save first (so we have a pk + final values)
super().save(*args, **kwargs)
super().save(*args, **kwargs)
# 1) on create -> send confirmation if pending/confirmed (and not sent before)
if is_create and self.status == self.StatusChoices.CONFIRMED and not self.confirmation_notification_sent_at:
# lazy import to avoid circular import
from apps.events.tasks import send_registration_confirmation_notification
send_registration_confirmation_notification.delay(str(self.pk))
self.confirmation_notification_sent_at = timezone.now()
super().save(update_fields=["confirmation_notification_sent_at"])
# 2) status changed -> cancelled
if (not is_create) and (old_status != self.StatusChoices.CANCELLED) and (self.status == self.StatusChoices.CANCELLED) and (not self.cancellation_notification_sent_at):
from apps.events.tasks import send_registration_cancellation_notification
send_registration_cancellation_notification.delay(str(self.pk))
self.cancellation_notification_sent_at = timezone.now()
super().save(update_fields=["cancellation_notification_sent_at"])
if (
self.status == self.StatusChoices.CONFIRMED
and (is_create or old_status != self.StatusChoices.CONFIRMED)
and not self.confirmation_notification_sent_at
):
RegistrationNotification.objects.get_or_create(
registration=self,
kind=RegistrationNotification.Kind.CONFIRMED,
)
if (
not is_create
and old_status != self.StatusChoices.CANCELLED
and self.status == self.StatusChoices.CANCELLED
and not self.cancellation_notification_sent_at
):
RegistrationNotification.objects.get_or_create(
registration=self,
kind=RegistrationNotification.Kind.CANCELLED,
)
# 3) status changed -> confirmed (if not sent before)
if (not is_create) and (old_status != self.StatusChoices.CONFIRMED) and (self.status == self.StatusChoices.CONFIRMED) and (not self.confirmation_notification_sent_at):
from apps.events.tasks import send_registration_confirmation_notification
send_registration_confirmation_notification.delay(str(self.pk))
self.confirmation_notification_sent_at = timezone.now()
super().save(update_fields=["confirmation_notification_sent_at"])
from apps.gamification.services import sync_registration_attendance
sync_registration_attendance(self)
from apps.gamification.services import sync_registration_attendance
sync_registration_attendance(self)
class RegistrationNotification(models.Model):
"""Durable intent to notify after a registration status transition commits."""
class Kind(models.TextChoices):
CONFIRMED = "confirmed", "Confirmed"
CANCELLED = "cancelled", "Cancelled"
registration = models.ForeignKey(Registration, on_delete=models.CASCADE, related_name="notification_outbox")
kind = models.CharField(max_length=10, choices=Kind.choices)
created_at = models.DateTimeField(auto_now_add=True)
delivered_at = models.DateTimeField(null=True, blank=True)
lease_until = models.DateTimeField(null=True, blank=True)
class Meta:
constraints = [models.UniqueConstraint(fields=["registration", "kind"], name="unique_registration_notification")]
indexes = [
models.Index(
fields=["lease_until", "created_at"],
name="reg_notif_pending_idx",
condition=models.Q(delivered_at__isnull=True),
),
]