timetoconfirm
Raz 7 months ago
parent 185aa29f3d
commit e019cea984
  1. 12
      api/serializers.py
  2. 2
      padelclub_backend/settings_app.py
  3. 4
      tournaments/admin.py
  4. 28
      tournaments/migrations/0122_customuser_enable_online_payments_and_more.py
  5. 28
      tournaments/migrations/0123_tournament_is_staff_tournament_and_more.py
  6. 18
      tournaments/migrations/0124_alter_playerregistration_registration_status.py
  7. 43
      tournaments/migrations/0125_customuser_disable_ranking_federal_ruling_and_more.py
  8. 27
      tournaments/migrations/0126_remove_customuser_enable_online_payments_and_more.py
  9. 18
      tournaments/migrations/0127_rename_is_staff_tournament_tournament_is_corporate_tournament.py
  10. 24
      tournaments/models/custom_user.py
  11. 17
      tournaments/models/enums.py
  12. 10
      tournaments/models/player_registration.py
  13. 23
      tournaments/models/team_registration.py
  14. 34
      tournaments/models/tournament.py
  15. 81
      tournaments/repositories.py
  16. 2
      tournaments/services/email_service.py
  17. 111
      tournaments/services/payment_service.py
  18. 4
      tournaments/services/tournament_registration.py
  19. 2
      tournaments/tasks.py
  20. 3
      tournaments/views.py

@ -6,7 +6,7 @@ from django.conf import settings
# email # email
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode from django.utils.http import urlsafe_base64_encode
from django.utils.encoding import force_bytes from django.utils.encoding import force_bytes
from django.core.mail import EmailMessage from django.core.mail import EmailMessage
from django.contrib.sites.shortcuts import get_current_site from django.contrib.sites.shortcuts import get_current_site
@ -15,7 +15,7 @@ from api.tokens import account_activation_token
from shared.cryptography import encryption_util from shared.cryptography import encryption_util
from tournaments.models.draw_log import DrawLog from tournaments.models.draw_log import DrawLog
from tournaments.models.enums import UserOrigin from tournaments.models.enums import UserOrigin, RegistrationPaymentMode
class EncryptedUserField(serializers.Field): class EncryptedUserField(serializers.Field):
def to_representation(self, value): def to_representation(self, value):
@ -78,6 +78,14 @@ class UserSerializer(serializers.ModelSerializer):
loser_bracket_match_format_preference=validated_data.get('loser_bracket_match_format_preference'), loser_bracket_match_format_preference=validated_data.get('loser_bracket_match_format_preference'),
loser_bracket_mode=validated_data.get('loser_bracket_mode'), loser_bracket_mode=validated_data.get('loser_bracket_mode'),
origin=UserOrigin.APP, origin=UserOrigin.APP,
user_role=None,
registration_payment_mode=validated_data.get('registration_payment_mode', RegistrationPaymentMode.DISABLED),
umpire_custom_mail=validated_data.get('umpire_custom_mail'),
umpire_custom_contact=validated_data.get('umpire_custom_contact'),
umpire_custom_phone=validated_data.get('umpire_custom_phone'),
hide_umpire_mail=validated_data.get('hide_umpire_mail', False),
hide_umpire_phone=validated_data.get('hide_umpire_phone', True),
disable_ranking_federal_ruling=validated_data.get('disable_ranking_federal_ruling', False)
) )
if not settings.DEBUG: if not settings.DEBUG:

@ -58,3 +58,5 @@ SHOP_MANAGERS = [
] ]
SHOP_SITE_ROOT_URL = 'https://padelclub.app' SHOP_SITE_ROOT_URL = 'https://padelclub.app'
SHOP_SUPPORT_EMAIL = 'shop@padelclub.app' SHOP_SUPPORT_EMAIL = 'shop@padelclub.app'
STRIPE_FEE = 0.0075

@ -15,11 +15,11 @@ class CustomUserAdmin(UserAdmin):
model = CustomUser model = CustomUser
search_fields = ['username', 'email', 'phone', 'first_name', 'last_name', 'licence_id'] search_fields = ['username', 'email', 'phone', 'first_name', 'last_name', 'licence_id']
list_display = ['email', 'first_name', 'last_name', 'username', 'licence_id', 'date_joined', 'latest_event_club_name', 'is_active', 'event_count', 'origin'] list_display = ['email', 'first_name', 'last_name', 'username', 'registration_payment_mode', 'licence_id', 'date_joined', 'latest_event_club_name', 'is_active', 'event_count', 'origin']
list_filter = ['is_active', 'origin'] list_filter = ['is_active', 'origin']
ordering = ['-date_joined'] ordering = ['-date_joined']
fieldsets = [ fieldsets = [
(None, {'fields': ['id', 'username', 'email', 'password', 'first_name', 'last_name', 'is_active', (None, {'fields': ['id', 'username', 'email', 'password', 'first_name', 'last_name', 'is_active', 'registration_payment_mode',
'clubs', 'country', 'phone', 'licence_id', 'umpire_code', 'clubs', 'country', 'phone', 'licence_id', 'umpire_code',
'summons_message_body', 'summons_message_signature', 'summons_available_payment_methods', 'summons_message_body', 'summons_message_signature', 'summons_available_payment_methods',
'summons_display_format', 'summons_display_entry_fee', 'summons_use_full_custom_message', 'summons_display_format', 'summons_display_entry_fee', 'summons_use_full_custom_message',

@ -0,0 +1,28 @@
# Generated by Django 5.1 on 2025-04-10 19:13
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('tournaments', '0121_tournament_stripe_account_id'),
]
operations = [
migrations.AddField(
model_name='customuser',
name='enable_online_payments',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='customuser',
name='user_role',
field=models.IntegerField(blank=True, choices=[(0, 'Juge-Arbitre'), (1, 'Club Owner'), (2, 'Player')], null=True),
),
migrations.AddField(
model_name='tournament',
name='enable_time_to_confirm',
field=models.BooleanField(default=False),
),
]

@ -0,0 +1,28 @@
# Generated by Django 5.1 on 2025-04-11 05:03
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('tournaments', '0122_customuser_enable_online_payments_and_more'),
]
operations = [
migrations.AddField(
model_name='tournament',
name='is_staff_tournament',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='tournament',
name='is_template',
field=models.BooleanField(default=False),
),
migrations.AlterField(
model_name='customuser',
name='is_staff',
field=models.BooleanField(default=False),
),
]

@ -0,0 +1,18 @@
# Generated by Django 5.1 on 2025-04-11 05:13
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('tournaments', '0123_tournament_is_staff_tournament_and_more'),
]
operations = [
migrations.AlterField(
model_name='playerregistration',
name='registration_status',
field=models.IntegerField(choices=[(0, 'Waiting'), (1, 'Pending'), (2, 'Confirmed'), (3, 'Canceled')], default=0),
),
]

@ -0,0 +1,43 @@
# Generated by Django 5.1 on 2025-04-11 07:17
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('tournaments', '0124_alter_playerregistration_registration_status'),
]
operations = [
migrations.AddField(
model_name='customuser',
name='disable_ranking_federal_ruling',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='customuser',
name='hide_umpire_mail',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='customuser',
name='hide_umpire_phone',
field=models.BooleanField(default=True),
),
migrations.AddField(
model_name='customuser',
name='umpire_custom_contact',
field=models.CharField(blank=True, max_length=200, null=True),
),
migrations.AddField(
model_name='customuser',
name='umpire_custom_mail',
field=models.EmailField(blank=True, max_length=254, null=True),
),
migrations.AddField(
model_name='customuser',
name='umpire_custom_phone',
field=models.CharField(blank=True, max_length=15, null=True),
),
]

@ -0,0 +1,27 @@
# Generated by Django 5.1 on 2025-04-11 08:25
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('tournaments', '0125_customuser_disable_ranking_federal_ruling_and_more'),
]
operations = [
migrations.RemoveField(
model_name='customuser',
name='enable_online_payments',
),
migrations.AddField(
model_name='customuser',
name='registration_payment_mode',
field=models.IntegerField(choices=[(0, 'No Payment'), (1, 'Direct Payment'), (2, 'No Service Fee')], default=0),
),
migrations.AlterField(
model_name='customuser',
name='is_staff',
field=models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status'),
),
]

@ -0,0 +1,18 @@
# Generated by Django 5.1 on 2025-04-11 08:26
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('tournaments', '0126_remove_customuser_enable_online_payments_and_more'),
]
operations = [
migrations.RenameField(
model_name='tournament',
old_name='is_staff_tournament',
new_name='is_corporate_tournament',
),
]

@ -3,7 +3,9 @@ from django.apps import apps
from django.contrib.auth.models import AbstractUser from django.contrib.auth.models import AbstractUser
from django.utils.timezone import now from django.utils.timezone import now
from django.conf import settings
from . import club, enums from . import club, enums
from .enums import RegistrationPaymentMode
import uuid import uuid
class CustomUser(AbstractUser): class CustomUser(AbstractUser):
@ -40,6 +42,15 @@ class CustomUser(AbstractUser):
origin = models.IntegerField(default=enums.UserOrigin.ADMIN, choices=enums.UserOrigin.choices, null=True, blank=True) origin = models.IntegerField(default=enums.UserOrigin.ADMIN, choices=enums.UserOrigin.choices, null=True, blank=True)
should_synchronize = models.BooleanField(default=False) should_synchronize = models.BooleanField(default=False)
user_role = models.IntegerField(choices=enums.UserRole.choices, null=True, blank=True)
registration_payment_mode = models.IntegerField(default=RegistrationPaymentMode.DISABLED, choices=RegistrationPaymentMode.choices)
umpire_custom_mail = models.EmailField(null=True, blank=True)
umpire_custom_contact = models.CharField(max_length=200, null=True, blank=True)
umpire_custom_phone = models.CharField(max_length=15, null=True, blank=True)
hide_umpire_mail = models.BooleanField(default=False)
hide_umpire_phone = models.BooleanField(default=True)
disable_ranking_federal_ruling = models.BooleanField(default=False)
### ### ### ### ### ### ### ### ### ### ### WARNING ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### WARNING ### ### ### ### ### ### ### ### ### ###
### WARNING : Any added field MUST be inserted in the method below: fields_for_update() ### ### WARNING : Any added field MUST be inserted in the method below: fields_for_update() ###
### ### ### ### ### ### ### ### ### ### ### WARNING ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### WARNING ### ### ### ### ### ### ### ### ### ###
@ -51,7 +62,10 @@ class CustomUser(AbstractUser):
'summons_message_body', 'summons_message_signature', 'summons_available_payment_methods', 'summons_message_body', 'summons_message_signature', 'summons_available_payment_methods',
'summons_display_format', 'summons_display_entry_fee', 'summons_display_format', 'summons_display_entry_fee',
'summons_use_full_custom_message', 'match_formats_default_duration', 'bracket_match_format_preference', 'summons_use_full_custom_message', 'match_formats_default_duration', 'bracket_match_format_preference',
'group_stage_match_format_preference', 'loser_bracket_match_format_preference', 'device_id', 'loser_bracket_mode', 'origin', 'agents', 'should_synchronize'] 'group_stage_match_format_preference', 'loser_bracket_match_format_preference', 'device_id', 'loser_bracket_mode',
'origin', 'agents', 'should_synchronize', 'user_role', 'registration_payment_mode',
'umpire_custom_mail', 'umpire_custom_contact', 'umpire_custom_phone', 'hide_umpire_mail', 'hide_umpire_phone',
'disable_ranking_federal_ruling']
def __str__(self): def __str__(self):
return self.username return self.username
@ -78,3 +92,11 @@ class CustomUser(AbstractUser):
return None return None
return None return None
def effective_commission_rate(self):
if self.registration_payment_mode == RegistrationPaymentMode.STRIPE:
return settings.STRIPE_FEE
if self.registration_payment_mode == RegistrationPaymentMode.NO_FEE:
return 0.0000
if self.registration_payment_mode == RegistrationPaymentMode.CORPORATE:
return 0.0000

@ -248,3 +248,20 @@ class UserOrigin(models.IntegerChoices):
ADMIN = 0, 'Admin' ADMIN = 0, 'Admin'
SITE = 1, 'Site' SITE = 1, 'Site'
APP = 2, 'App' APP = 2, 'App'
class UserRole(models.IntegerChoices):
JAP = 0, 'Juge-Arbitre'
CLUB_OWNER = 1, 'Club Owner'
PLAYER = 2, 'Player'
class RegistrationStatus(models.IntegerChoices):
WAITING = 0, 'Waiting'
PENDING = 1, 'Pending'
CONFIRMED = 2, 'Confirmed'
CANCELED = 3, 'Canceled'
class RegistrationPaymentMode(models.IntegerChoices):
DISABLED = 0, 'Disabled'
CORPORATE = 1, 'Corporate'
NO_FEE = 2, 'No Service Fee'
STRIPE = 3, 'Stripe'

@ -1,15 +1,9 @@
from django.db import models from django.db import models
from . import SideStoreModel, TeamRegistration, PlayerSexType, PlayerDataSource, PlayerPaymentType, OnlineRegistrationStatus from . import SideStoreModel, TeamRegistration, PlayerSexType, PlayerDataSource, PlayerPaymentType, OnlineRegistrationStatus
from .enums import RegistrationStatus
import uuid import uuid
from django.utils import timezone from django.utils import timezone
class RegistrationStatus(models.TextChoices):
WAITING = 'WAITING', 'Waiting'
PENDING = 'PENDING', 'Pending'
CONFIRMED = 'CONFIRMED', 'Confirmed'
CANCELED = 'CANCELED', 'Canceled'
class PlayerRegistration(SideStoreModel): class PlayerRegistration(SideStoreModel):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=True) id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=True)
team_registration = models.ForeignKey(TeamRegistration, on_delete=models.SET_NULL, related_name='player_registrations', null=True) team_registration = models.ForeignKey(TeamRegistration, on_delete=models.SET_NULL, related_name='player_registrations', null=True)
@ -45,7 +39,7 @@ class PlayerRegistration(SideStoreModel):
coach = models.BooleanField(default=False) coach = models.BooleanField(default=False)
registered_online = models.BooleanField(default=False) registered_online = models.BooleanField(default=False)
time_to_confirm = models.DateTimeField(null=True, blank=True) time_to_confirm = models.DateTimeField(null=True, blank=True)
registration_status = models.CharField(max_length=20, choices=RegistrationStatus.choices, default=RegistrationStatus.WAITING) registration_status = models.IntegerField(choices=RegistrationStatus.choices, default=RegistrationStatus.WAITING)
payment_id = models.CharField(max_length=255, blank=True, null=True) payment_id = models.CharField(max_length=255, blank=True, null=True)
def delete_dependencies(self): def delete_dependencies(self):

@ -2,6 +2,8 @@ from django.db import models
from . import SideStoreModel, Tournament, GroupStage, Match from . import SideStoreModel, Tournament, GroupStage, Match
import uuid import uuid
from django.utils import timezone from django.utils import timezone
from .enums import RegistrationStatus
from .player_enums import PlayerPaymentType
class TeamRegistration(SideStoreModel): class TeamRegistration(SideStoreModel):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=True) id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=True)
@ -309,15 +311,16 @@ class TeamRegistration(SideStoreModel):
return "" return ""
def set_time_to_confirm(self, ttc): def set_time_to_confirm(self, ttc):
from .player_registration import RegistrationStatus
for p in self.player_registrations.all(): for p in self.player_registrations.all():
if p.registered_online: if p.registered_online:
p.time_to_confirm = ttc p.time_to_confirm = ttc
p.registration_status = RegistrationStatus.PENDING if ttc is None:
p.registration_status = RegistrationStatus.WAITING
else:
p.registration_status = RegistrationStatus.PENDING
p.save() p.save()
def needs_confirmation(self): def needs_confirmation(self):
from .player_registration import RegistrationStatus
"""Check if this team needs to confirm their registration""" """Check if this team needs to confirm their registration"""
# Check if any player has status PENDING and is registered online # Check if any player has status PENDING and is registered online
return any(p.registration_status == RegistrationStatus.PENDING and p.registered_online return any(p.registration_status == RegistrationStatus.PENDING and p.registered_online
@ -328,17 +331,17 @@ class TeamRegistration(SideStoreModel):
deadlines = [p.time_to_confirm for p in self.player_registrations.all() if p.time_to_confirm is not None] deadlines = [p.time_to_confirm for p in self.player_registrations.all() if p.time_to_confirm is not None]
return max(deadlines) if deadlines else None return max(deadlines) if deadlines else None
def confirm_registration(self): def confirm_registration(self, payment_intent_id=None):
"""Confirm the team's registration after being moved from waiting list""" """Confirm the team's registration after being moved from waiting list"""
now = timezone.now() now = timezone.now()
from .player_registration import RegistrationStatus
# Update all players in the team # Update all players in the team
for player in self.player_registrations.all(): for player in self.player_registrations.all():
if player.time_to_confirm is not None: player.time_to_confirm = None
player.time_to_confirm = None player.payment_id = payment_intent_id
player.registration_status = RegistrationStatus.CONFIRMED if payment_intent_id is not None:
player.save() player.payment_type = PlayerPaymentType.CREDIT_CARD
player.registration_status = RegistrationStatus.CONFIRMED
player.save()
# Save confirmation date # Save confirmation date
self.confirmation_date = now self.confirmation_date = now

@ -84,6 +84,9 @@ class Tournament(BaseModel):
enable_online_payment_refund = models.BooleanField(default=False) enable_online_payment_refund = models.BooleanField(default=False)
refund_date_limit = models.DateTimeField(null=True, blank=True) # Equivalent to Date? = nil refund_date_limit = models.DateTimeField(null=True, blank=True) # Equivalent to Date? = nil
stripe_account_id = models.CharField(max_length=255, blank=True, null=True) stripe_account_id = models.CharField(max_length=255, blank=True, null=True)
enable_time_to_confirm = models.BooleanField(default=False)
is_corporate_tournament = models.BooleanField(default=False)
is_template = models.BooleanField(default=False)
def delete_dependencies(self): def delete_dependencies(self):
for team_registration in self.team_registrations.all(): for team_registration in self.team_registrations.all():
@ -1481,32 +1484,32 @@ class Tournament(BaseModel):
datetime: The confirmation deadline datetime datetime: The confirmation deadline datetime
""" """
# Skip if feature not enabled # Skip if feature not enabled
# if not tournament.has_time_to_confirm: if self.enable_time_to_confirm is False:
# return None return None
if waiting_list_count == 0: if waiting_list_count == 0:
return None return None
# Configuration rules # Configuration rules
TIME_PROXIMITY_RULES = { TIME_PROXIMITY_RULES = {
48: 30, # within 48h → 30 min 24: 30, # within 24h → 30 min
24: 20, # within 24h → 20 min 48: 60, # within 48h → 60 min
72: 60, # within 72h → 60 min 72: 120, # within 72h → 120 min
"default": 120 "default": 240
} }
WAITING_LIST_RULES = { WAITING_LIST_RULES = {
15: 15, # 15+ teams → 15 min 30: 30, # 30+ teams → 30 min
10: 30, # 10+ teams → 30 min 20: 60, # 20+ teams → 60 min
5: 60, # 5+ teams → 60 min 10: 120, # 10+ teams → 120 min
"default": 120 "default": 240
} }
BUSINESS_RULES = { BUSINESS_RULES = {
"hours": { "hours": {
"start": 8, # 8:00 "start": 8, # 8:00
"end": 21, # 20:00 "end": 21, # 21:00
"default_confirmation_hour": 10 # When extending to next day "default_confirmation_hour": 8 # When extending to next day
}, },
"days": { "days": {
"working_days": [0, 1, 2, 3, 4, 5, 6], # Monday = 0, Friday = 4 "working_days": [0, 1, 2, 3, 4, 5, 6], # Monday = 0, Friday = 4
@ -1519,7 +1522,7 @@ class Tournament(BaseModel):
24: True, # If ≤ 24h until tournament: ignore business hours 24: True, # If ≤ 24h until tournament: ignore business hours
12: True # If ≤ 12h until tournament: ignore all restrictions 12: True # If ≤ 12h until tournament: ignore all restrictions
}, },
"minimum_response_time": 15 # minutes "minimum_response_time": 30 # minutes
} }
# 1. Get current time in tournament's timezone # 1. Get current time in tournament's timezone
@ -1614,9 +1617,6 @@ class Tournament(BaseModel):
return raw_deadline return raw_deadline
@property @property
def week_day(self): def week_day(self):
"""Return the weekday name (e.g., 'Monday')""" """Return the weekday name (e.g., 'Monday')"""
@ -1727,7 +1727,7 @@ class Tournament(BaseModel):
return None return None
def should_request_payment(self): def should_request_payment(self):
if self.entry_fee is not None and self.entry_fee > 0 and self.enable_online_payment and self.stripe_account_id is not None: if self.enable_online_payment:
return True return True
else: else:
return False return False

@ -1,81 +0,0 @@
from .models import TeamRegistration, PlayerRegistration
from .models.player_enums import PlayerSexType, PlayerDataSource
from .models.enums import FederalCategory
from tournaments.utils.licence_validator import LicenseValidator
class TournamentRegistrationRepository:
@staticmethod
def create_team_registration(tournament, registration_date):
team_registration = TeamRegistration.objects.create(
tournament=tournament,
registration_date=registration_date
)
return team_registration
@staticmethod
def create_player_registrations(request, team_registration, players_data, team_form_data):
stripped_license = None
if request.user.is_authenticated and request.user.licence_id:
stripped_license = LicenseValidator(request.user.licence_id).stripped_license
for player_data in players_data:
is_captain = False
player_licence_id = player_data['licence_id']
if player_licence_id and stripped_license:
if stripped_license.lower() in player_licence_id.lower():
is_captain = True
sex, rank, computed_rank = TournamentRegistrationRepository._compute_rank_and_sex(
team_registration.tournament,
player_data
)
print("create_player_registrations", player_data.get('last_name'), sex, rank, computed_rank)
data_source = None
if player_data.get('found_in_french_federation', False) == True:
data_source = PlayerDataSource.FRENCH_FEDERATION
player_registration = PlayerRegistration.objects.create(
team_registration=team_registration,
captain=is_captain,
source=data_source,
registered_online=True,
first_name=player_data.get('first_name'),
last_name=player_data.get('last_name'),
points=player_data.get('points'),
assimilation=player_data.get('assimilation'),
tournament_played=player_data.get('tournament_count'),
ligue_name=player_data.get('ligue_name'),
club_name=player_data.get('club_name'),
birthdate=player_data.get('birth_year'),
sex=sex,
rank=rank,
computed_rank=computed_rank,
licence_id=player_data['licence_id'],
email=player_data.get('email'),
phone_number=player_data.get('mobile_number'),
)
player_registration.save()
team_registration.set_weight()
team_registration.save()
@staticmethod
def _compute_rank_and_sex(tournament, player_data):
is_woman = player_data.get('is_woman', False)
rank = player_data.get('rank', None)
if rank is None:
computed_rank = 100000
else:
computed_rank = rank
sex = PlayerSexType.MALE
if is_woman:
sex = PlayerSexType.FEMALE
if tournament.federal_category == FederalCategory.MEN:
computed_rank = str(int(computed_rank) +
FederalCategory.female_in_male_assimilation_addition(int(rank)))
print("_compute_rank_and_sex", sex, rank, computed_rank)
return sex, rank, computed_rank

@ -1,6 +1,6 @@
from django.core.mail import EmailMessage from django.core.mail import EmailMessage
from enum import Enum from enum import Enum
from ..models.player_registration import RegistrationStatus from ..models.enums import RegistrationStatus
from ..models.tournament import TeamSortingType from ..models.tournament import TeamSortingType
import django.utils.timezone import django.utils.timezone

@ -4,7 +4,7 @@ from django.urls import reverse
import stripe import stripe
from ..models import TeamRegistration, PlayerRegistration, Tournament from ..models import TeamRegistration, PlayerRegistration, Tournament
from ..models.player_registration import PlayerPaymentType, RegistrationStatus from ..models.player_registration import PlayerPaymentType
from .email_service import TournamentEmailService from .email_service import TournamentEmailService
from .tournament_registration import RegistrationCartManager from .tournament_registration import RegistrationCartManager
from ..utils.extensions import is_not_sqlite_backend from ..utils.extensions import is_not_sqlite_backend
@ -29,15 +29,6 @@ class PaymentService:
if not tournament.should_request_payment(): if not tournament.should_request_payment():
raise Exception("Les paiements ne sont pas activés pour ce tournoi.") raise Exception("Les paiements ne sont pas activés pour ce tournoi.")
# Get the umpire's Stripe account ID
stripe_account_id = tournament.stripe_account_id
if not stripe_account_id:
raise Exception("L'arbitre n'a pas configuré son compte Stripe.")
# Calculate commission
commission_rate = tournament.effective_commission_rate() / 100
platform_amount = int((team_fee * commission_rate) * 100) # Commission in cents
# Get user email if authenticated # Get user email if authenticated
customer_email = self.request.user.email if self.request.user.is_authenticated else None customer_email = self.request.user.email if self.request.user.is_authenticated else None
@ -54,35 +45,72 @@ class PaymentService:
) )
# Common checkout session parameters # Common checkout session parameters
checkout_session_params = { if tournament.is_corporate_tournament:
'payment_method_types': ['card'], # Direct charge without transfers when umpire is platform owner
'line_items': [{ checkout_session_params = {
'price_data': { 'payment_method_types': ['card'],
'currency': 'eur', 'line_items': [{
'product_data': { 'price_data': {
'name': f'{tournament.broadcast_display_name()} du {tournament.formatted_start_date()}', 'currency': 'eur',
'description': f'Lieu {tournament.event.club.name}', 'product_data': {
'name': f'{tournament.broadcast_display_name()} du {tournament.formatted_start_date()}',
'description': f'Lieu {tournament.event.club.name}',
},
'unit_amount': int(team_fee * 100), # Amount in cents
}, },
'unit_amount': int(team_fee * 100), # Amount in cents 'quantity': 1,
}, }],
'quantity': 1, 'mode': 'payment',
}], 'success_url': self.request.build_absolute_uri(
'mode': 'payment', reverse('tournament-payment-success', kwargs={'tournament_id': tournament_id})
'success_url': self.request.build_absolute_uri( ),
reverse('tournament-payment-success', kwargs={'tournament_id': tournament_id}) 'cancel_url': cancel_url,
), 'metadata': {
'cancel_url': cancel_url, 'tournament_id': str(tournament_id),
'payment_intent_data': { 'user_id': str(self.request.user.id) if self.request.user.is_authenticated else None,
'application_fee_amount': platform_amount, 'source_page': 'tournament_info' if team_registration_id else 'register_tournament',
'transfer_data': { 'is_corporate_tournament': 'true'
'destination': stripe_account_id, }
},
},
'metadata': {
'tournament_id': str(tournament_id), # Convert UUID to string
'user_id': str(self.request.user.id) if self.request.user.is_authenticated else None, # Convert UUID to string
'source_page': 'tournament_info' if team_registration_id else 'register_tournament',
} }
else:
# Get the umpire's Stripe account ID
stripe_account_id = tournament.stripe_account_id
if not stripe_account_id:
raise Exception("L'arbitre n'a pas configuré son compte Stripe.")
# Calculate commission
commission_rate = tournament.event.creator.effective_commission_rate()
platform_amount = int((team_fee * commission_rate) * 100) # Commission in cents
checkout_session_params = {
'payment_method_types': ['card'],
'line_items': [{
'price_data': {
'currency': 'eur',
'product_data': {
'name': f'{tournament.broadcast_display_name()} du {tournament.formatted_start_date()}',
'description': f'Lieu {tournament.event.club.name}',
},
'unit_amount': int(team_fee * 100), # Amount in cents
},
'quantity': 1,
}],
'mode': 'payment',
'success_url': self.request.build_absolute_uri(
reverse('tournament-payment-success', kwargs={'tournament_id': tournament_id})
),
'cancel_url': cancel_url,
'payment_intent_data': {
'application_fee_amount': platform_amount,
'transfer_data': {
'destination': stripe_account_id,
},
},
'metadata': {
'tournament_id': str(tournament_id), # Convert UUID to string
'user_id': str(self.request.user.id) if self.request.user.is_authenticated else None, # Convert UUID to string
'source_page': 'tournament_info' if team_registration_id else 'register_tournament',
}
} }
# Add cart or team data to metadata based on payment context # Add cart or team data to metadata based on payment context
@ -207,14 +235,7 @@ class PaymentService:
def _update_registration_payment_info(self, team_registration, payment_intent_id): def _update_registration_payment_info(self, team_registration, payment_intent_id):
"""Update player registrations with payment information""" """Update player registrations with payment information"""
player_registrations = PlayerRegistration.objects.filter(team_registration=team_registration) team_registration.confirm_registration(payment_intent_id)
for player_reg in player_registrations:
player_reg.payment_type = PlayerPaymentType.CREDIT_CARD
player_reg.registration_status = RegistrationStatus.CONFIRMED
player_reg.payment_id = payment_intent_id
player_reg.save()
return True return True
def process_refund(self, team_registration_id): def process_refund(self, team_registration_id):

@ -4,7 +4,7 @@ import datetime
from ..models import PlayerRegistration, TeamRegistration, Tournament from ..models import PlayerRegistration, TeamRegistration, Tournament
from ..utils.licence_validator import LicenseValidator from ..utils.licence_validator import LicenseValidator
from ..utils.player_search import get_player_name_from_csv from ..utils.player_search import get_player_name_from_csv
from ..models.enums import FederalCategory from ..models.enums import FederalCategory, RegistrationStatus
from ..models.player_enums import PlayerSexType, PlayerDataSource from ..models.player_enums import PlayerSexType, PlayerDataSource
class RegistrationCartManager: class RegistrationCartManager:
@ -371,7 +371,7 @@ class RegistrationCartManager:
licence_id=player_data.get('licence_id'), licence_id=player_data.get('licence_id'),
email=player_data.get('email'), email=player_data.get('email'),
phone_number=player_data.get('mobile_number', mobile_number), phone_number=player_data.get('mobile_number', mobile_number),
registration_status='CONFIRMED' if self.session.get('waiting_list_position', 0) < 0 else 'WAITING' registration_status=RegistrationStatus.CONFIRMED if self.session.get('waiting_list_position', 0) < 0 else RegistrationStatus.WAITING
) )
# Calculate and set team weight # Calculate and set team weight

@ -3,7 +3,7 @@ from django.utils import timezone
from django.db import transaction from django.db import transaction
from .models import PlayerRegistration from .models import PlayerRegistration
from .models.player_registration import RegistrationStatus from .models.enums import RegistrationStatus
from .services.email_service import TournamentEmailService, TeamEmailType from .services.email_service import TournamentEmailService, TeamEmailType
@background(schedule=1) # Run every 30 minutes (30*60 seconds) @background(schedule=1) # Run every 30 minutes (30*60 seconds)

@ -26,8 +26,7 @@ from django.core.files.storage import default_storage
from django.core.files.base import ContentFile from django.core.files.base import ContentFile
from django.views.generic import View from django.views.generic import View
from django.db.models import Q from django.db.models import Q
from .models import Club, Tournament, CustomUser, Event, Round, Match, TeamScore, TeamRegistration, PlayerRegistration, UserOrigin, PlayerPaymentType from .models import Club, Tournament, CustomUser, Event, Round, Match, TeamScore, TeamRegistration, PlayerRegistration, UserOrigin
from .models.player_registration import RegistrationStatus
from datetime import datetime, timedelta from datetime import datetime, timedelta
import time import time
import json import json

Loading…
Cancel
Save