from django.utils import timezone import uuid import datetime from ..models import PlayerRegistration, TeamRegistration, Tournament from ..utils.licence_validator import LicenseValidator from ..utils.player_search import get_player_name_from_csv from ..models.enums import FederalCategory, RegistrationStatus from ..models.player_enums import PlayerSexType, PlayerDataSource from django.contrib.auth import get_user_model from django.conf import settings from django.utils.dateparse import parse_datetime class RegistrationCartManager: """ Manages the registration cart for tournament registrations. Handles session-based cart operations, player additions/removals, and checkout processes. """ CART_EXPIRY_SECONDS = 300 def __init__(self, request): self.request = request self.session = request.session self.first_tournament = False def get_or_create_cart_id(self): """Get or create a registration cart ID in the session""" if 'registration_cart_id' not in self.session: self.session['registration_cart_id'] = str(uuid.uuid4()) # Ensure it's a string self.session.modified = True return self.session['registration_cart_id'] def get_cart_expiry(self): """Get the cart expiry time from the session""" if 'registration_cart_expiry' not in self.session: # Set default expiry to 30 minutes from now expiry = timezone.now() + datetime.timedelta(seconds=self.CART_EXPIRY_SECONDS) self.session['registration_cart_expiry'] = expiry.isoformat() self.session.modified = True return self.session['registration_cart_expiry'] def is_cart_expired(self): """Check if the registration cart is expired""" if 'registration_cart_expiry' not in self.session: return False expiry_str = self.session['registration_cart_expiry'] try: expiry = datetime.datetime.fromisoformat(expiry_str) return timezone.now() > expiry except (ValueError, TypeError): return True def reset_cart_expiry(self): """Reset the cart expiry time""" expiry = timezone.now() + datetime.timedelta(seconds=self.CART_EXPIRY_SECONDS) self.session['registration_cart_expiry'] = expiry.isoformat() self.session.modified = True def get_tournament_id(self): """Get the tournament ID associated with the current cart""" return self.session.get('registration_tournament_id') def initialize_cart(self, tournament_id): """Initialize a new registration cart for a tournament""" # Clear any existing cart self.clear_cart() try: tournament = Tournament.objects.get(id=tournament_id) except Tournament.DoesNotExist: return False, "Tournoi introuvable." # Update tournament reserved spots waiting_list_position = tournament.get_waiting_list_position() # Set up the new cart self.session['registration_cart_id'] = str(uuid.uuid4()) # Ensure it's a string self.session['waiting_list_position'] = waiting_list_position self.session['registration_tournament_id'] = str(tournament_id) # Ensure it's a string self.session['registration_cart_players'] = [] self.reset_cart_expiry() self.session.modified = True return True, "Cart initialized successfully" def get_cart_data(self): """Get the data for the current registration cart""" # Ensure cart players array exists if 'registration_cart_players' not in self.session: self.session['registration_cart_players'] = [] self.session.modified = True # Ensure tournament ID exists if 'registration_tournament_id' not in self.session: # If no tournament ID but we have players, this is an inconsistency if self.session.get('registration_cart_players'): print("WARNING: Found players but no tournament ID - clearing players") self.session['registration_cart_players'] = [] self.session.modified = True # Get user phone if authenticated user_phone = '' if hasattr(self.request.user, 'phone'): user_phone = self.request.user.phone # Parse the expiry time from ISO format to datetime expiry_str = self.get_cart_expiry() expiry_datetime = None if expiry_str: try: # Parse the ISO format string to datetime expiry_datetime = parse_datetime(expiry_str) except (ValueError, TypeError): # If parsing fails, set a new expiry expiry_datetime = timezone.now() + datetime.timedelta(seconds=self.CART_EXPIRY_SECONDS) cart_data = { 'cart_id': self.get_or_create_cart_id(), 'tournament_id': self.session.get('registration_tournament_id'), 'waiting_list_position': self.session.get('waiting_list_position'), 'players': self.session.get('registration_cart_players', []), 'expiry': expiry_datetime, # Now a datetime object, not a string 'is_cart_expired': self.is_cart_expired(), 'team_fee_from_cart_players': self.team_fee_from_cart_players(), 'mobile_number': self.session.get('registration_mobile_number', user_phone) } # Debug: print the cart content print(f"Cart data - Tournament ID: {cart_data['tournament_id']}") print(f"Cart data - Players count: {len(cart_data['players'])}") return cart_data def team_fee_from_cart_players(self): # Get tournament tournament_id = self.session.get('registration_tournament_id') try: tournament = Tournament.objects.get(id=tournament_id) except Tournament.DoesNotExist: return 0 players = self.session.get('registration_cart_players', []), entry_fee = tournament.entry_fee if entry_fee is not None and entry_fee > 0 and tournament.enable_online_payment: fee = entry_fee * tournament.minimum_player_per_team players = self.session.get('registration_cart_players', []) club_members = sum(1 for player in players if player.get('club_member', False)) if tournament.club_member_fee_deduction is not None: return fee - club_members * tournament.club_member_fee_deduction return fee else: return 0 def add_player(self, player_data): """Add a player to the registration cart""" if self.is_cart_expired(): return False, "Votre session d'inscription a expiré, veuillez réessayer." # Get cart data tournament_id = self.session.get('registration_tournament_id') if not tournament_id: return False, "Pas d'inscription active." # Get tournament try: tournament = Tournament.objects.get(id=tournament_id) except Tournament.DoesNotExist: return False, "Tournoi introuvable." # Get existing players directly from session players = self.session.get('registration_cart_players', []) # Check if we've reached the team limit if len(players) >= 2: # Assuming teams of 2 for padel return False, "Nombre maximum de joueurs déjà ajouté." # Process player data licence_id = player_data.get('licence_id', '').upper() if player_data.get('licence_id') else None first_name = player_data.get('first_name', '') last_name = player_data.get('last_name', '').upper() if player_data.get('last_name') else '' # Handle license validation logic result = self._process_player_license( tournament, licence_id, first_name, last_name, players, len(players) == 0 ) if not result[0]: return result # Return the error tournament_federal_category = tournament.federal_category if tournament_federal_category == FederalCategory.MIXED and len(players) == 1: other_player_is_woman = players[0].get('is_woman', False) if other_player_is_woman is False: tournament_federal_category = FederalCategory.WOMEN if licence_id: # Get federation data fed_data, found = get_player_name_from_csv(tournament_federal_category, licence_id) if found is False and fed_data: player_data.update({ 'rank': fed_data['rank'], 'is_woman': fed_data['is_woman'], }) if found and fed_data: # Use federation data (including check for eligibility) player_register_check = tournament.player_register_check(licence_id) if player_register_check: return False, ", ".join(player_register_check) # Update player data from federation data player_data.update({ 'first_name': fed_data['first_name'], 'last_name': fed_data['last_name'], 'rank': fed_data['rank'], 'is_woman': fed_data['is_woman'], 'points': fed_data.get('points'), 'assimilation': fed_data.get('assimilation'), 'tournament_count': fed_data.get('tournament_count'), 'ligue_name': fed_data.get('ligue_name'), 'club_name': fed_data.get('club_name'), 'club_code': fed_data.get('club_code'), 'birth_year': fed_data.get('birth_year'), 'found_in_french_federation': True, 'club_member': tournament.is_cart_player_from_club(fed_data) }) elif not first_name or not last_name: # License not required or not found, but name is needed self.first_tournament = True return False, "Le prénom et le nom sont obligatoires pour les joueurs dont la licence n'a pas été trouvée." elif not tournament.license_is_required: # License not required, check if name is provided if not first_name or not last_name: return False, "Le prénom et le nom sont obligatoires." else: # License is required but not provided return False, "Le numéro de licence est obligatoire." # Create player registrations sex, rank, computed_rank = self._compute_rank_and_sex( tournament, player_data ) player_data['computed_rank'] = computed_rank # Add player to cart players.append(player_data) self.session['registration_cart_players'] = players self.reset_cart_expiry() self.session.modified = True if sex == PlayerSexType.FEMALE: return True, "Joueuse ajoutée avec succès." else: return True, "Joueur ajouté avec succès." def _process_player_license(self, tournament, licence_id, first_name, last_name, players, is_first_player): """ Process and validate player license Returns (True, None) if valid, (False, error_message) if invalid """ # Handle case where license is required if tournament.license_is_required: # If license is required but not provided if not licence_id: # First player (authentication check) or partner user_message = "Le numéro de licence est obligatoire." if is_first_player else "Le numéro de licence de votre partenaire est obligatoire." return False, user_message # Validate the license format validator = LicenseValidator(licence_id) if not validator.validate_license(): if settings.DEBUG: return False, f"Le numéro de licence est invalide, la lettre ne correspond pas. {validator.get_computed_license_key(validator.stripped_license)}" else: return False, "Le numéro de licence est invalide, la lettre ne correspond pas." # Check if player is already registered in tournament stripped_license = validator.stripped_license if self._is_player_already_registered(stripped_license, tournament): return False, "Un joueur avec ce numéro de licence est déjà inscrit dans une équipe." # Check if this is the authenticated user trying to register as first player if self.request.user.is_authenticated and is_first_player and self.request.user.licence_id is None: # Try to update the user's license ID in the database try: self.request.user.licence_id = validator.computed_licence_id self.request.user.save() self.request.user.refresh_from_db() except: return False, "Erreur lors de la mise à jour de votre licence: cette licence est déjà utilisée par un autre joueur." # Check for duplicate licenses in cart existing_licenses = [p.get('licence_id') for p in players if p.get('licence_id')] if licence_id and licence_id in existing_licenses: return False, "Ce joueur est déjà dans l'équipe." return True, None def remove_player(self): """Remove the last player from the cart""" if self.is_cart_expired(): return False, "Votre session d'inscription a expiré, veuillez réessayer." players = self.session.get('registration_cart_players', []) if not players: return False, "Pas de joueur à supprimer." # Remove last player players.pop() self.session['registration_cart_players'] = players self.reset_cart_expiry() self.session.modified = True return True, "Joueur retiré." def update_contact_info(self, mobile_number=None): """Update contact info for the cart""" if self.is_cart_expired(): return False, "Votre session d'inscription a expiré, veuillez réessayer." if mobile_number is not None: self.session['registration_mobile_number'] = mobile_number self.reset_cart_expiry() self.session.modified = True return True, "Informations de contact mises à jour." def checkout(self): """Convert cart to an actual tournament registration""" if self.is_cart_expired(): return False, "Votre session d'inscription a expiré, veuillez réessayer." # Get cart data cart_data = self.get_cart_data() tournament_id = cart_data.get('tournament_id') players = cart_data.get('players') mobile_number = cart_data.get('mobile_number') # Validate cart data if not tournament_id: return False, "Aucun tournoi sélectionné." if not players: return False, "Aucun joueur dans l'inscription." # Get tournament try: tournament = Tournament.objects.get(id=tournament_id) except Tournament.DoesNotExist: return False, "Tournoi introuvable." # Check minimum players if len(players) < tournament.minimum_player_per_team: return False, f"Vous avez besoin d'au moins {tournament.minimum_player_per_team} joueurs pour vous inscrire." # Identify captain from user's license # # Update user phone if provided if self.request.user.is_authenticated and mobile_number: self.request.user.phone = mobile_number self.request.user.save(update_fields=['phone']) stripped_license = None if self.request.user.is_authenticated and self.request.user.licence_id: validator = LicenseValidator(self.request.user.licence_id) stripped_license = validator.stripped_license weight = sum(int(player_data.get('computed_rank', 0) or 0) for player_data in players) # Create team registration team_registration = TeamRegistration.objects.create( tournament=tournament, registration_date=timezone.now(), walk_out=False, weight=weight, user=self.request.user ) for player_data in players: # Compute rank and sex using the original logic # Determine if this player is the captain is_captain = False player_licence_id = player_data.get('licence_id') if player_licence_id and stripped_license: if stripped_license.lower() in player_licence_id.lower(): is_captain = True # Determine data source data_source = None if player_data.get('found_in_french_federation', False) == True: data_source = PlayerDataSource.FRENCH_FEDERATION # Now using the enum value User = get_user_model() matching_user = self.request.user if player_licence_id and (stripped_license is None or is_captain is False): try: # Using icontains for case-insensitive match matching_user = User.objects.get(licence_id__icontains=player_licence_id) if matching_user is None: matching_user = self.request.user except User.DoesNotExist: pass is_woman = player_data.get('is_woman') if is_woman is not None: if is_woman: sex = 0 else: sex = 1 else: sex = None tournament_count = player_data.get('tournament_count', None) if tournament_count is not None: try: tournament_played = int(tournament_count) except ValueError: tournament_played = None else: tournament_played = None points = player_data.get('points', None) if points is not None: try: points = float(points) except ValueError: points = None else: points = None # Create player registration with all the original fields PlayerRegistration.objects.create( team_registration=team_registration, user=matching_user, captain=is_captain, source=data_source, registered_online=True, first_name=player_data.get('first_name'), last_name=player_data.get('last_name'), points=points, assimilation=player_data.get('assimilation'), tournament_played=tournament_played, ligue_name=player_data.get('ligue_name'), club_name=player_data.get('club_name'), club_code=player_data.get('club_code'), club_member=tournament.is_cart_player_from_club(player_data), birthdate=player_data.get('birth_year'), sex= sex, rank=player_data.get('rank'), computed_rank=player_data.get('computed_rank'), licence_id=player_data.get('licence_id'), email=matching_user.email if matching_user else player_data.get('email'), phone_number=matching_user.phone if matching_user else player_data.get('mobile_number'), registration_status=RegistrationStatus.CONFIRMED if self.session.get('waiting_list_position', 0) < 0 else RegistrationStatus.WAITING ) # Clear the cart self.clear_cart() tournament.reserved_spots = max(0, tournament.reserved_spots - 1) tournament.save() return True, team_registration def clear_cart(self): """Clear the registration cart""" keys_to_clear = [ 'registration_cart_id', 'team_registration_id', 'registration_tournament_id', 'registration_cart_players', 'registration_cart_expiry', 'registration_mobile_number' ] for key in keys_to_clear: if key in self.session: del self.session[key] self.session.modified = True def _is_player_already_registered(self, stripped_license, tournament): """Check if a player is already registered in the tournament""" return PlayerRegistration.objects.filter( team_registration__tournament=tournament, licence_id__icontains=stripped_license, team_registration__walk_out=False ).exists() def add_authenticated_user(self): """ Adds the authenticated user to the cart if they have a valid license. Returns True if added, False otherwise. """ if not self.request.user.is_authenticated or not self.request.user.licence_id: return False # Create player data for the authenticated user player_data = { 'first_name': self.request.user.first_name, 'last_name': self.request.user.last_name, 'licence_id': self.request.user.licence_id, 'email': self.request.user.email, 'phone': self.request.user.phone } # Add the user to the cart success, _ = self.add_player(player_data) return success def _compute_rank_and_sex(self, tournament, player_data): """ Compute the player's sex, rank, and computed rank based on tournament category. This reimplements the original logic from TournamentRegistrationRepository. """ is_woman = player_data.get('is_woman', False) rank = player_data.get('rank', None) if rank is None: rank_int = None computed_rank = 100000 else: # Ensure rank is an integer for calculations try: rank_int = int(rank) computed_rank = rank_int except (ValueError, TypeError): # If rank can't be converted to int, set a default rank_int = None computed_rank = 100000 # Use the integer enum values sex = PlayerSexType.FEMALE if is_woman else PlayerSexType.MALE # Apply assimilation for women playing in men's tournaments if is_woman and tournament.federal_category == FederalCategory.MEN and rank_int is not None: assimilation_addition = FederalCategory.female_in_male_assimilation_addition(rank_int) computed_rank = computed_rank + assimilation_addition print(f"_compute_rank_and_sex: {player_data.get('last_name')}, {sex}, {rank}, {computed_rank}") return sex, rank, str(computed_rank)