feat(events): harden registration checkout lifecycle

This commit is contained in:
2026-10-01 12:26:49 +03:30
parent 1daa755543
commit 962238c32c
29 changed files with 3048 additions and 207 deletions

View File

@@ -5,6 +5,8 @@ from ninja import ModelSchema, Schema
from pydantic import field_validator
from typing import Literal, Optional, List
from datetime import datetime
from django.db.models import Q
from django.utils import timezone
from apps.blog.api.schemas import AuthorSchema
from apps.events.models import Event, Registration
@@ -85,7 +87,11 @@ class EventSchema(ModelSchema):
@staticmethod
def resolve_registration_count(obj):
return obj.registrations.filter(status__in=[Registration.StatusChoices.CONFIRMED, Registration.StatusChoices.ATTENDED]).count()
now = timezone.now()
return obj.registrations.filter(is_deleted=False).filter(
Q(status__in=[Registration.StatusChoices.CONFIRMED, Registration.StatusChoices.ATTENDED])
| Q(status=Registration.StatusChoices.PENDING, checkout_expires_at__gt=now)
).count()
@staticmethod
def resolve_description_html(obj):
@@ -102,6 +108,7 @@ class EventListSchema(Schema):
absolute_featured_image_thumbnail_url: Optional[str] = None
absolute_featured_image_preview_url: Optional[str] = None
event_type: str
address: Optional[str] = None
start_time: datetime
end_time: datetime
registration_start_date: Optional[datetime] = None
@@ -133,7 +140,22 @@ class EventListSchema(Schema):
@staticmethod
def resolve_registration_count(obj):
return obj.registrations.filter(status__in=[Registration.StatusChoices.CONFIRMED, Registration.StatusChoices.ATTENDED]).count()
if hasattr(obj, "browse_registration_count"):
return obj.browse_registration_count
now = timezone.now()
return obj.registrations.filter(is_deleted=False).filter(
Q(status__in=[Registration.StatusChoices.CONFIRMED, Registration.StatusChoices.ATTENDED])
| Q(status=Registration.StatusChoices.PENDING, checkout_expires_at__gt=now)
).count()
class PaginatedEventBrowseSchema(Schema):
count: int
page: int
page_size: int
total_pages: int
price_ceiling: int
results: List[EventListSchema]
class EventCreateSchema(Schema):
"""Payload for creating events via the API."""
@@ -185,6 +207,9 @@ class RegistrationSchema(ModelSchema):
'id',
'status',
'registered_at',
'checkout_started_at',
'checkout_expires_at',
'cancellation_reason',
'ticket_id',
'discount_amount',
'final_price',
@@ -326,9 +351,16 @@ class EventBriefSchema(Schema):
class MyEventRegistrationOut(Schema):
"""Registration information as returned to authenticated users."""
id: int
ticket_id: UUID
created_at: datetime
status: Literal["pending", "confirmed", "cancelled", "attended"]
event: EventBriefSchema
checkout_state: Literal["none", "active", "legacy_pending", "expired", "unresolved", "cancelled"] = "none"
checkout_expires_at: Optional[datetime] = None
payment_id: Optional[int] = None
payment_status: Optional[Literal["initiated", "pending", "paid", "failed", "canceled"]] = None
refund_status: Optional[Literal["none", "pending", "succeeded", "manual_review"]] = None
can_resume: bool = False
class PaginatedMyEventRegistrationsSchema(Schema):
@@ -340,6 +372,79 @@ class PaginatedMyEventRegistrationsSchema(Schema):
class RegistrationStatusOut(Schema):
is_registered: bool
checkout_state: Literal[
"none", "confirmed", "active", "legacy_pending", "expired", "unresolved", "cancelled"
] = "none"
registration_id: Optional[int] = None
registration_status: Optional[str] = None
ticket_id: Optional[UUID] = None
checkout_expires_at: Optional[datetime] = None
payment_id: Optional[int] = None
payment_status: Optional[Literal["initiated", "pending", "paid", "failed", "canceled"]] = None
refund_status: Optional[Literal["none", "pending", "succeeded", "manual_review"]] = None
can_resume: bool = False
can_cancel: bool = False
class RegistrationHubEventOut(Schema):
id: int
title: str
slug: str
description: str
event_type: str
address: Optional[str] = None
location: Optional[str] = None
start_time: datetime
end_time: datetime
registration_start_date: Optional[datetime] = None
registration_end_date: Optional[datetime] = None
capacity: Optional[int] = None
price: int
status: str
registration_count: int
image_url: Optional[str] = None
registration_success_markdown: Optional[str] = None
class RegistrationHubRegistrationOut(Schema):
ticket_id: UUID
status: str
registered_at: datetime
checkout_started_at: Optional[datetime] = None
checkout_expires_at: Optional[datetime] = None
cancellation_reason: str = ""
final_price: Optional[int] = None
discount_amount: int = 0
class RegistrationHubPaymentOut(Schema):
id: int
status: Literal["initiated", "pending", "paid", "failed", "canceled"]
verification_state: Literal[
"awaiting_verification", "checking", "retry_scheduled", "manual_review",
"verified", "failed", "canceled",
]
refund_status: Literal["none", "pending", "succeeded", "manual_review"]
base_amount: int
discount_amount: int
amount: int
ref_id: Optional[str] = None
created_at: datetime
gateway_paid_at: Optional[datetime] = None
class RegistrationHubOut(Schema):
state: Literal[
"ready", "active_checkout", "legacy_pending", "verification_pending",
"manual_review", "paid_success", "free_success", "failed", "cancelled",
"expired", "refund_pending", "refund_manual_review", "refund_succeeded",
]
event: RegistrationHubEventOut
registration: Optional[RegistrationHubRegistrationOut] = None
payment: Optional[RegistrationHubPaymentOut] = None
can_register: bool = False
can_resume: bool = False
can_cancel: bool = False
class RegistrationCreateSchema(Schema):