diff --git a/tournaments/admin.py b/tournaments/admin.py
index 3b67f56..708fd9d 100644
--- a/tournaments/admin.py
+++ b/tournaments/admin.py
@@ -90,7 +90,7 @@ class TournamentAdmin(SyncedObjectAdmin):
list_display = ['display_name', 'event', 'is_private', 'start_date', 'payment', 'creator', 'is_canceled']
list_filter = [StartDateRangeFilter, 'is_deleted', 'event__creator']
ordering = ['-start_date']
- search_fields = ['id']
+ search_fields = ['id', 'display_name', 'federal_level_category']
def dashboard_view(self, request):
"""Tournament dashboard view with comprehensive statistics"""
diff --git a/tournaments/migrations/0129_tournament_currency_code.py b/tournaments/migrations/0129_tournament_currency_code.py
new file mode 100644
index 0000000..c1cdd58
--- /dev/null
+++ b/tournaments/migrations/0129_tournament_currency_code.py
@@ -0,0 +1,18 @@
+# Generated by Django 5.1 on 2025-06-23 16:47
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('tournaments', '0128_club_data_access_ids_court_data_access_ids_and_more'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='tournament',
+ name='currency_code',
+ field=models.CharField(blank=True, default='EUR', max_length=3, null=True),
+ ),
+ ]
diff --git a/tournaments/models/team_registration.py b/tournaments/models/team_registration.py
index 5e19969..deb7ed3 100644
--- a/tournaments/models/team_registration.py
+++ b/tournaments/models/team_registration.py
@@ -6,6 +6,7 @@ from .enums import RegistrationStatus
from .player_enums import PlayerPaymentType
from ..services.email_service import TournamentEmailService, TeamEmailType
from ..utils.extensions import format_seconds
+from ..services.currency_service import CurrencyService
import uuid
@@ -412,6 +413,13 @@ class TeamRegistration(TournamentSubModel):
payment_statuses = [player.get_remaining_fee() for player in player_registrations]
return sum(payment_statuses)
+ def get_remaining_fee_formatted(self):
+ """Get the remaining fee formatted with the tournament's currency."""
+ remaining_fee = self.get_remaining_fee()
+ tournament = self.get_tournament()
+ currency_code = tournament.currency_code if tournament and tournament.currency_code else 'EUR'
+ return CurrencyService.format_amount(remaining_fee, currency_code)
+
def is_confirmation_expired(self):
"""
Check if the confirmation deadline has expired.
diff --git a/tournaments/models/tournament.py b/tournaments/models/tournament.py
index d653089..bfdaa0c 100644
--- a/tournaments/models/tournament.py
+++ b/tournaments/models/tournament.py
@@ -12,6 +12,7 @@ from django.utils.formats import date_format
from ..utils.licence_validator import LicenseValidator
from django.apps import apps
from django.conf import settings
+from tournaments.services.currency_service import CurrencyService
class TeamSortingType(models.IntegerChoices):
RANK = 1, 'Rank'
@@ -94,6 +95,7 @@ class Tournament(BaseModel):
show_teams_in_prog = models.BooleanField(default=False)
club_member_fee_deduction = models.FloatField(null=True, blank=True)
unregister_delta_in_hours = models.IntegerField(default=24)
+ currency_code = models.CharField(null=True, blank=True, max_length=3, default='EUR')
def delete_dependencies(self):
for team_registration in self.team_registrations.all():
@@ -1106,18 +1108,18 @@ class Tournament(BaseModel):
return True
def options_fee(self):
- def format_currency(amount):
- """Format currency amount, removing unnecessary decimals"""
- return f"{amount:g}" if amount % 1 == 0 else f"{amount:.2f}"
-
options = []
+ currency_service = CurrencyService()
+
# Entry fee
if self.entry_fee is not None and self.entry_fee > 0:
- options.append(f"Frais d'inscription: {format_currency(self.entry_fee)} € par joueur")
+ formatted_fee = currency_service.format_amount(self.entry_fee, self.currency_code)
+ options.append(f"Frais d'inscription: {formatted_fee} par joueur")
# Club member fee reduction
if self.club_member_fee_deduction and self.club_member_fee_deduction > 0:
- options.append(f"Réduction de {format_currency(self.club_member_fee_deduction)} € pour les membres du club")
+ formatted_deduction = currency_service.format_amount(self.club_member_fee_deduction, self.currency_code)
+ options.append(f"Réduction de {formatted_deduction} pour les membres du club")
return options
diff --git a/tournaments/services/currency_service.py b/tournaments/services/currency_service.py
new file mode 100644
index 0000000..69952c5
--- /dev/null
+++ b/tournaments/services/currency_service.py
@@ -0,0 +1,189 @@
+"""
+Currency service for handling multi-currency support in tournaments.
+"""
+
+class CurrencyService:
+ """Service for handling currency formatting, symbols, and conversions."""
+
+ # Currency symbols mapping
+ CURRENCY_SYMBOLS = {
+ 'EUR': '€',
+ 'USD': '$',
+ 'GBP': '£',
+ 'CHF': 'CHF',
+ 'CAD': 'C$',
+ 'AUD': 'A$',
+ 'JPY': '¥',
+ 'CNY': '¥',
+ 'SEK': 'kr',
+ 'NOK': 'kr',
+ 'DKK': 'kr',
+ 'PLN': 'zł',
+ 'CZK': 'Kč',
+ 'HUF': 'Ft',
+ 'RON': 'lei',
+ 'BGN': 'лв',
+ 'HRK': 'kn',
+ 'RSD': 'RSD',
+ 'BAM': 'KM',
+ 'MKD': 'ден',
+ 'ALL': 'L',
+ 'MDL': 'L',
+ 'UAH': '₴',
+ 'RUB': '₽',
+ 'BYN': 'Br',
+ 'LTL': 'Lt',
+ 'LVL': 'Ls',
+ 'EEK': 'kr',
+ 'ISK': 'kr',
+ 'TRY': '₺',
+ 'ILS': '₪',
+ 'AED': 'د.إ',
+ 'SAR': 'ر.س',
+ 'QAR': 'ر.ق',
+ 'KWD': 'د.ك',
+ 'BHD': '.د.ب',
+ 'OMR': 'ر.ع.',
+ 'JOD': 'د.أ',
+ 'LBP': 'ل.ل',
+ 'EGP': 'ج.م',
+ 'MAD': 'د.م.',
+ 'TND': 'د.ت',
+ 'DZD': 'د.ج',
+ 'LYD': 'ل.د',
+ 'ZAR': 'R',
+ 'NGN': '₦',
+ 'GHS': '₵',
+ 'KES': 'KSh',
+ 'UGX': 'USh',
+ 'TZS': 'TSh',
+ 'ETB': 'Br',
+ 'XOF': 'CFA',
+ 'XAF': 'FCFA',
+ 'MZN': 'MT',
+ 'BWP': 'P',
+ 'ZMW': 'ZK',
+ 'INR': '₹',
+ 'PKR': '₨',
+ 'BDT': '৳',
+ 'LKR': '₨',
+ 'MVR': 'Rf',
+ 'NPR': '₨',
+ 'BTN': 'Nu.',
+ 'THB': '฿',
+ 'MYR': 'RM',
+ 'SGD': 'S$',
+ 'IDR': 'Rp',
+ 'PHP': '₱',
+ 'VND': '₫',
+ 'KHR': '៛',
+ 'LAK': '₭',
+ 'MMK': 'K',
+ 'KRW': '₩',
+ 'TWD': 'NT$',
+ 'HKD': 'HK$',
+ 'MOP': 'MOP$',
+ 'BND': 'B$',
+ 'FJD': 'FJ$',
+ 'PGK': 'K',
+ 'WST': 'WS$',
+ 'TOP': 'T$',
+ 'VUV': 'VT',
+ 'SBD': 'SI$',
+ 'MXN': '$',
+ 'GTQ': 'Q',
+ 'BZD': 'BZ$',
+ 'SVC': '$',
+ 'HNL': 'L',
+ 'NIO': 'C$',
+ 'CRC': '₡',
+ 'PAB': 'B/.',
+ 'COP': '$',
+ 'VES': 'Bs.S',
+ 'GYD': 'GY$',
+ 'SRD': 'Sr$',
+ 'UYU': '$U',
+ 'PYG': '₲',
+ 'BOB': 'Bs.',
+ 'BRL': 'R$',
+ 'PEN': 'S/',
+ 'ECU': '$',
+ 'CLP': '$',
+ 'ARS': '$',
+ 'FKP': '£',
+ 'XPF': '₣'
+ }
+
+ # Zero-decimal currencies (amounts are in their base unit, not subdivided)
+ ZERO_DECIMAL_CURRENCIES = {
+ 'BIF', 'CLP', 'DJF', 'GNF', 'JPY', 'KMF', 'KRW', 'MGA', 'PYG', 'RWF',
+ 'UGX', 'VND', 'VUV', 'XAF', 'XOF', 'XPF'
+ }
+
+ @classmethod
+ def get_currency_symbol(cls, currency_code):
+ """Get the currency symbol for a given currency code."""
+ if not currency_code:
+ return '€' # Default to Euro symbol
+ return cls.CURRENCY_SYMBOLS.get(currency_code.upper(), currency_code.upper())
+
+ @classmethod
+ def format_amount(cls, amount, currency_code, show_symbol=True):
+ """Format amount with proper currency symbol and decimal places."""
+ if amount is None:
+ return "0"
+
+ currency_code = currency_code.upper() if currency_code else 'EUR'
+
+ # Handle zero-decimal currencies
+ if currency_code in cls.ZERO_DECIMAL_CURRENCIES:
+ formatted_amount = f"{int(amount)}"
+ else:
+ # Format with 2 decimal places, removing unnecessary zeros
+ if amount == int(amount):
+ formatted_amount = f"{int(amount)}"
+ else:
+ formatted_amount = f"{amount:.2f}"
+
+ if show_symbol:
+ symbol = cls.get_currency_symbol(currency_code)
+ return f"{formatted_amount} {symbol}"
+
+ return formatted_amount
+
+ @classmethod
+ def convert_to_stripe_amount(cls, amount, currency_code):
+ """Convert amount to Stripe's expected format (minor units)."""
+ if not amount:
+ return 0
+
+ currency_code = currency_code.upper() if currency_code else 'EUR'
+
+ # Zero-decimal currencies don't need conversion
+ if currency_code in cls.ZERO_DECIMAL_CURRENCIES:
+ return int(amount)
+
+ # Two-decimal currencies need to be multiplied by 100
+ return int(amount * 100)
+
+ @classmethod
+ def convert_from_stripe_amount(cls, stripe_amount, currency_code):
+ """Convert Stripe amount (minor units) back to standard decimal format."""
+ if not stripe_amount:
+ return 0.0
+
+ currency_code = currency_code.upper() if currency_code else 'EUR'
+
+ # Zero-decimal currencies are already in the correct format
+ if currency_code in cls.ZERO_DECIMAL_CURRENCIES:
+ return float(stripe_amount)
+
+ # Two-decimal currencies need to be divided by 100
+ return float(stripe_amount) / 100
+
+ @classmethod
+ def validate_currency_code(cls, currency_code):
+ """Validate if a currency code is supported."""
+ if not currency_code:
+ return False
+ return currency_code.upper() in cls.CURRENCY_SYMBOLS
diff --git a/tournaments/services/email_service.py b/tournaments/services/email_service.py
index 9d732a8..70fbb75 100644
--- a/tournaments/services/email_service.py
+++ b/tournaments/services/email_service.py
@@ -3,6 +3,7 @@ from enum import Enum
from ..models.enums import RegistrationStatus, FederalLevelCategory
from ..models.tournament import TeamSortingType
from django.utils import timezone
+from tournaments.services.currency_service import CurrencyService
class TeamEmailType(Enum):
REGISTERED = "registered"
@@ -666,14 +667,16 @@ class TournamentEmailService:
if payment_status == 'PAID':
return "\n\n✅ Le paiement de votre inscription a bien été reçu."
- # If the team is on the waiting list, don't mention payment
if team_registration.is_in_waiting_list() >= 0:
return ""
+ currency_service = CurrencyService()
+
# For unpaid teams, add payment instructions
+ formatted_fee = currency_service.format_amount(tournament.entry_fee, tournament.currency_code)
payment_info = [
"\n\n⚠️ Paiement des frais d'inscription requis",
- f"Les frais d'inscription de {tournament.entry_fee:.2f}€ par joueur doivent être payés pour confirmer votre participation.",
+ f"Les frais d'inscription de {formatted_fee} par joueur doivent être payés pour confirmer votre participation.",
"Vous pouvez effectuer le paiement en vous connectant à votre compte Padel Club.",
f"Lien pour payer: https://padelclub.app/tournament/{tournament.id}/info"
]
@@ -694,12 +697,17 @@ class TournamentEmailService:
# Calculate payment amount
payment_amount = None
- if payment and 'amount' in payment:
- # Convert cents to euros
- payment_amount = payment['amount'] / 100
+ currency_service = CurrencyService()
- if payment_amount is None:
- payment_amount = team_registration.get_remaining_fee()
+ if payment and 'amount' in payment:
+ # Get currency from payment metadata or tournament
+ payment_currency = payment.get('currency', tournament.currency_code or 'EUR')
+ # Convert cents/minor units to standard format
+ payment_amount = currency_service.convert_from_stripe_amount(payment['amount'], payment_currency)
+ formatted_amount = currency_service.format_amount(payment_amount, payment_currency)
+ else:
+ # Fallback to tournament fee
+ formatted_amount = currency_service.format_amount(team_registration.get_team_registration_fee(), tournament.currency_code)
federal_level_category = FederalLevelCategory(tournament.federal_level_category)
tournament_word = federal_level_category.localized_word()
@@ -725,7 +733,7 @@ class TournamentEmailService:
# Add payment details
body_parts.append(
- f"\n\nMontant payé : {payment_amount:.2f}€"
+ f"\n\nMontant payé : {formatted_amount}"
)
payment_date = timezone.now().strftime("%d/%m/%Y")
@@ -768,6 +776,19 @@ class TournamentEmailService:
"""
player_registrations = team_registration.players_sorted_by_rank
refund_amount = None
+
+ currency_service = CurrencyService()
+
+ if refund_details and 'amount' in refund_details:
+ # Get currency from refund details or tournament
+ refund_currency = refund_details.get('currency', tournament.currency_code or 'EUR')
+ # Convert cents/minor units to standard format
+ refund_amount = currency_service.convert_from_stripe_amount(refund_details['amount'], refund_currency)
+ formatted_amount = currency_service.format_amount(refund_amount, refund_currency)
+ else:
+ # Fallback to tournament fee
+ formatted_amount = currency_service.format_amount(tournament.entry_fee, tournament.currency_code)
+
if refund_details and 'amount' in refund_details:
# Convert cents to euros
refund_amount = refund_details['amount'] / 100
@@ -803,7 +824,7 @@ class TournamentEmailService:
# Add refund details
body_parts.append(
- f"\n\nMontant remboursé : {refund_amount:.2f}€"
+ f"\n\nMontant remboursé : {formatted_amount}"
)
refund_date = timezone.now().strftime("%d/%m/%Y")
diff --git a/tournaments/services/payment_service.py b/tournaments/services/payment_service.py
index 2b5d51d..064d732 100644
--- a/tournaments/services/payment_service.py
+++ b/tournaments/services/payment_service.py
@@ -11,6 +11,7 @@ from ..models.player_registration import PlayerPaymentType
from .email_service import TournamentEmailService
from .tournament_registration import RegistrationCartManager
from ..utils.extensions import is_not_sqlite_backend
+from tournaments.services.currency_service import CurrencyService
class PaymentService:
"""
@@ -28,10 +29,20 @@ class PaymentService:
stripe.api_key = self.stripe_api_key
tournament = get_object_or_404(Tournament, id=tournament_id)
+ currency_service = CurrencyService()
+
# Check if payments are enabled for this tournament
if not tournament.should_request_payment():
raise Exception("Les paiements ne sont pas activés pour ce tournoi.")
+ # Get currency code and validate it
+ currency_code = tournament.currency_code or 'EUR'
+ if not currency_service.validate_currency_code(currency_code):
+ raise Exception(f"Devise non supportée: {currency_code}")
+
+ # Convert amount to Stripe format
+ stripe_amount = currency_service.convert_to_stripe_amount(team_fee, currency_code)
+
# Get user email if authenticated
customer_email = self.request.user.email if self.request.user.is_authenticated else None
@@ -52,6 +63,7 @@ class PaymentService:
'user_id': str(self.request.user.id) if self.request.user.is_authenticated else None,
'payment_source': 'tournament', # Identify payment source
'source_page': 'tournament_info' if team_registration_id else 'register_tournament',
+ 'currency_code': currency_code, # Store currency for later reference
}
if tournament.is_corporate_tournament:
@@ -98,12 +110,12 @@ class PaymentService:
'payment_method_types': ['card'],
'line_items': [{
'price_data': {
- 'currency': 'eur',
+ 'currency': currency_code.lower(), # Use tournament currency
'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': stripe_amount, # Amount in proper currency format
},
'quantity': 1,
}],
@@ -122,18 +134,20 @@ class PaymentService:
# Calculate commission
commission_rate = tournament.event.creator.effective_commission_rate()
- platform_amount = int((team_fee * commission_rate) * 100) # Commission in cents
+ platform_amount = currency_service.convert_to_stripe_amount(
+ team_fee * commission_rate, currency_code
+ )
checkout_session_params = {
'payment_method_types': ['card'],
'line_items': [{
'price_data': {
- 'currency': 'eur',
+ 'currency': currency_code.lower(), # Use tournament currency
'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': stripe_amount, # Amount in proper currency format
},
'quantity': 1,
}],
@@ -151,13 +165,6 @@ class PaymentService:
'metadata': metadata
}
- # # Add cart or team data to metadata based on payment context
- # if cart_data:
- # checkout_session_params['metadata']['registration_cart_id'] = str(cart_data['cart_id']) # Convert to string
- # elif team_registration_id:
- # checkout_session_params['metadata']['team_registration_id'] = str(team_registration_id) # Convert to string
- # self.request.session['team_registration_id'] = str(team_registration_id) # Convert to string
-
# Add customer_email if available
if customer_email:
checkout_session_params['customer_email'] = customer_email
diff --git a/tournaments/services/tournament_registration.py b/tournaments/services/tournament_registration.py
index c3accba..21fe47b 100644
--- a/tournaments/services/tournament_registration.py
+++ b/tournaments/services/tournament_registration.py
@@ -9,6 +9,7 @@ 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
+from .currency_service import CurrencyService
class RegistrationCartManager:
"""
@@ -126,6 +127,7 @@ class RegistrationCartManager:
'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(),
+ 'team_fee_from_cart_players_formatted': self.team_fee_from_cart_players_formatted(),
'mobile_number': self.session.get('registration_mobile_number', user_phone)
}
@@ -156,6 +158,19 @@ class RegistrationCartManager:
else:
return 0
+ def team_fee_from_cart_players_formatted(self):
+ """Get the team fee formatted with the tournament's currency."""
+ team_fee = self.team_fee_from_cart_players()
+ tournament_id = self.session.get('registration_tournament_id')
+
+ try:
+ tournament = Tournament.objects.get(id=tournament_id)
+ currency_code = tournament.currency_code if tournament and tournament.currency_code else 'EUR'
+ except Tournament.DoesNotExist:
+ currency_code = 'EUR'
+
+ return CurrencyService.format_amount(team_fee, currency_code)
+
def add_player(self, player_data):
"""Add a player to the registration cart"""
print("add_player", player_data)
diff --git a/tournaments/templates/register_tournament.html b/tournaments/templates/register_tournament.html
index eda6feb..ace6a93 100644
--- a/tournaments/templates/register_tournament.html
+++ b/tournaments/templates/register_tournament.html
@@ -197,7 +197,7 @@
Confirmer votre inscription en payant immédiatement :
{% endif %}
{% if tournament.should_request_payment is False or tournament.online_payment_is_mandatory is False or cart_data.waiting_list_position >= 0 %}
diff --git a/tournaments/templates/tournaments/tournament_info.html b/tournaments/templates/tournaments/tournament_info.html
index 3613823..5d1a09d 100644
--- a/tournaments/templates/tournaments/tournament_info.html
+++ b/tournaments/templates/tournaments/tournament_info.html
@@ -102,9 +102,9 @@
{% else %}
{% if team.needs_confirmation %}
- Confirmer en payant {{ team.get_remaining_fee|floatformat:2 }}€
+ Confirmer en payant {{ team.get_remaining_fee_formatted }}
{% else %}
- Procéder au paiement de {{ team.get_remaining_fee|floatformat:2 }}€
+ Procéder au paiement de {{ team.get_remaining_fee_formatted }}
{% endif %}
{% endif %}