clean up registration fee messages

online_payment
Raz 7 months ago
parent ea370cec79
commit 54e872622f
  1. 8
      PadelClub.xcodeproj/project.pbxproj
  2. 42
      PadelClub/Data/CustomUser.swift
  3. 126
      PadelClub/Utils/Network/ConfigurationService.swift
  4. 80
      PadelClub/Views/Shared/OnlineWaitingListFaqSheetView.swift
  5. 69
      PadelClub/Views/Tournament/Screen/RegistrationSetupView.swift

@ -985,6 +985,9 @@
FFE8B5CB2DAA42A000BDE966 /* XlsToCsvService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFE8B5CA2DAA429E00BDE966 /* XlsToCsvService.swift */; };
FFE8B5CC2DAA42A000BDE966 /* XlsToCsvService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFE8B5CA2DAA429E00BDE966 /* XlsToCsvService.swift */; };
FFE8B5CD2DAA42A000BDE966 /* XlsToCsvService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFE8B5CA2DAA429E00BDE966 /* XlsToCsvService.swift */; };
FFE8B63A2DACEAED00BDE966 /* ConfigurationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFE8B6392DACEAEC00BDE966 /* ConfigurationService.swift */; };
FFE8B63B2DACEAED00BDE966 /* ConfigurationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFE8B6392DACEAEC00BDE966 /* ConfigurationService.swift */; };
FFE8B63C2DACEAED00BDE966 /* ConfigurationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFE8B6392DACEAEC00BDE966 /* ConfigurationService.swift */; };
FFE8C2C02C7601E80046B243 /* ConfirmButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFE8C2BF2C7601E80046B243 /* ConfirmButtonView.swift */; };
FFEF7F4E2BDE69130033D0F0 /* MenuWarningView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFEF7F4D2BDE69130033D0F0 /* MenuWarningView.swift */; };
FFF0241E2BF48B15001F14B4 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = FFF0241D2BF48B15001F14B4 /* Localizable.strings */; };
@ -1427,6 +1430,7 @@
FFE8B5BE2DAA325400BDE966 /* RefundResultsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefundResultsView.swift; sourceTree = "<group>"; };
FFE8B5C62DAA390000BDE966 /* StripeValidationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StripeValidationService.swift; sourceTree = "<group>"; };
FFE8B5CA2DAA429E00BDE966 /* XlsToCsvService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XlsToCsvService.swift; sourceTree = "<group>"; };
FFE8B6392DACEAEC00BDE966 /* ConfigurationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationService.swift; sourceTree = "<group>"; };
FFE8C2BF2C7601E80046B243 /* ConfirmButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfirmButtonView.swift; sourceTree = "<group>"; };
FFEF7F4D2BDE69130033D0F0 /* MenuWarningView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuWarningView.swift; sourceTree = "<group>"; };
FFF0241C2BF48B15001F14B4 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = "<group>"; };
@ -2066,6 +2070,7 @@
FFE8B5BA2DA9896800BDE966 /* RefundService.swift */,
FFE8B5C62DAA390000BDE966 /* StripeValidationService.swift */,
FFE8B5CA2DAA429E00BDE966 /* XlsToCsvService.swift */,
FFE8B6392DACEAEC00BDE966 /* ConfigurationService.swift */,
);
path = Network;
sourceTree = "<group>";
@ -2835,6 +2840,7 @@
FF9AC3952BE3627B00C2E883 /* GroupStageTeamReplacementView.swift in Sources */,
FF4C7F022BBBD7150031B6A3 /* TabItemModifier.swift in Sources */,
FFDDD40C2B93B2BB00C91A49 /* DeferredViewModifier.swift in Sources */,
FFE8B63C2DACEAED00BDE966 /* ConfigurationService.swift in Sources */,
FF0E0B6D2BC254C6005F00A9 /* TournamentScheduleView.swift in Sources */,
FF025AF12BD1AEBD00A86CF8 /* MatchFormatStorageView.swift in Sources */,
FF3F74F62B919E45004CFE0E /* UmpireView.swift in Sources */,
@ -3129,6 +3135,7 @@
FF4CBFE12C996C0600151637 /* PrintSettingsView.swift in Sources */,
FF4CBFE22C996C0600151637 /* TournamentMatchFormatsSettingsView.swift in Sources */,
FF4CBFE32C996C0600151637 /* DatePickingView.swift in Sources */,
FFE8B63B2DACEAED00BDE966 /* ConfigurationService.swift in Sources */,
FF4CBFE42C996C0600151637 /* MatchFormatRowView.swift in Sources */,
FF4CBFE52C996C0600151637 /* MonthData.swift in Sources */,
FF4CBFE62C996C0600151637 /* MenuWarningView.swift in Sources */,
@ -3428,6 +3435,7 @@
FF70FB602C90584900129CC2 /* PrintSettingsView.swift in Sources */,
FF70FB612C90584900129CC2 /* TournamentMatchFormatsSettingsView.swift in Sources */,
FF70FB622C90584900129CC2 /* DatePickingView.swift in Sources */,
FFE8B63A2DACEAED00BDE966 /* ConfigurationService.swift in Sources */,
FF70FB632C90584900129CC2 /* MatchFormatRowView.swift in Sources */,
FF70FB642C90584900129CC2 /* MonthData.swift in Sources */,
FF70FB652C90584900129CC2 /* MenuWarningView.swift in Sources */,

@ -20,20 +20,6 @@ enum RegistrationPaymentMode: Int, Codable {
case noFee = 2
case stripe = 3
func fee() -> Double? {
switch self {
case .disabled:
return nil
case .corporate:
return nil
case .noFee:
return nil
case .stripe:
let fee = 0.0075
return fee
}
}
func canEnableOnlinePayment() -> Bool {
switch self {
case .disabled:
@ -46,33 +32,7 @@ enum RegistrationPaymentMode: Int, Codable {
return true
}
}
func localizedRegistrationPaymentFee() -> String? {
switch self {
case .disabled:
return nil
case .corporate:
return nil
case .noFee:
return nil
case .stripe:
if let fee = self.fee() {
return String(format: "%.1f%%", fee * 100)
} else {
return nil
}
}
}
func sample(entryFee: Double) -> String {
if let fee = self.fee() {
let feeAmount = entryFee * fee
return String(format: "%.2f", feeAmount)
} else {
return "0"
}
}
func requiresStripe() -> Bool {
switch self {
case .disabled:

@ -0,0 +1,126 @@
//
// ConfigurationService.swift
// PadelClub
//
// Created by razmig on 14/04/2025.
//
import Foundation
import LeStorage
class ConfigurationService {
static func fetchTournamentConfig() async throws -> TimeToConfirmConfig {
let service = try StoreCenter.main.service()
let urlRequest = try service._baseRequest(servicePath: "config/tournament/", method: .get, requiresToken: true)
let (data, _) = try await URLSession.shared.data(for: urlRequest)
return try JSONDecoder().decode(TimeToConfirmConfig.self, from: data)
}
static func fetchPaymentConfig() async throws -> PaymentConfig {
let service = try StoreCenter.main.service()
let urlRequest = try service._baseRequest(servicePath: "config/payment/", method: .get, requiresToken: true)
let (data, _) = try await URLSession.shared.data(for: urlRequest)
return try JSONDecoder().decode(PaymentConfig.self, from: data)
}
}
struct TimeToConfirmConfig: Codable {
let timeProximityRules: [String: Int]
let waitingListRules: [String: Int]
let businessRules: BusinessRules
let urgencyOverride: UrgencyOverride
private enum CodingKeys: String, CodingKey {
case timeProximityRules = "time_proximity_rules"
case waitingListRules = "waiting_list_rules"
case businessRules = "business_rules"
case urgencyOverride = "urgency_override"
}
// Default configuration
static let defaultConfig = TimeToConfirmConfig(
timeProximityRules: [
"24": 30, // within 24h 30 min
"48": 60, // within 48h 60 min
"72": 120, // within 72h 120 min
"default": 240
],
waitingListRules: [
"30": 30, // 30+ teams 30 min
"20": 60, // 20+ teams 60 min
"10": 120, // 10+ teams 120 min
"default": 240
],
businessRules: BusinessRules(
hours: Hours(
start: 8,
end: 21,
defaultConfirmationHour: 8
),
days: Days(
workingDays: [0, 1, 2, 3, 4, 5, 6],
weekend: []
)
),
urgencyOverride: UrgencyOverride(
thresholds: [
"24": true,
"12": true
],
minimumResponseTime: 30
)
)
}
struct BusinessRules: Codable {
let hours: Hours
let days: Days
}
struct Hours: Codable {
let start: Int
let end: Int
let defaultConfirmationHour: Int
private enum CodingKeys: String, CodingKey {
case start
case end
case defaultConfirmationHour = "default_confirmation_hour"
}
}
struct Days: Codable {
let workingDays: [Int]
let weekend: [Int]
private enum CodingKeys: String, CodingKey {
case workingDays = "working_days"
case weekend
}
}
struct UrgencyOverride: Codable {
let thresholds: [String: Bool]
let minimumResponseTime: Int
private enum CodingKeys: String, CodingKey {
case thresholds
case minimumResponseTime = "minimum_response_time"
}
}
struct PaymentConfig: Codable {
let stripeFee: Double
// Default configuration
static let defaultConfig = PaymentConfig(stripeFee: 0.0075)
private enum CodingKeys: String, CodingKey {
case stripeFee = "stripe_fee"
}
}

@ -9,42 +9,70 @@ import SwiftUI
struct OnlineWaitingListFaqSheetView: View {
@Environment(\.dismiss) private var dismiss
let faqText: String =
"""
FAQ pour les Arbitres - Confirmation des Équipes
let timeToConfirmConfig: TimeToConfirmConfig
var faqText: String {
// Helper function to convert minutes to hours string
func formatMinutes(_ minutes: Int) -> String {
if minutes < 60 {
return "\(minutes) minutes"
} else {
return "\(minutes) minutes (\(minutes/60) heure\(minutes/60 > 1 ? "s" : ""))"
}
}
// Get business hours
let startHour = timeToConfirmConfig.businessRules.hours.start
let endHour = timeToConfirmConfig.businessRules.hours.end
// Get time proximity rules
let proximityRules = timeToConfirmConfig.timeProximityRules
let defaultTime = proximityRules["default"] ?? 240
// Get waiting list rules
let waitingRules = timeToConfirmConfig.waitingListRules
// Get urgency thresholds
let urgencyThresholds = timeToConfirmConfig.urgencyOverride.thresholds
let minResponseTime = timeToConfirmConfig.urgencyOverride.minimumResponseTime
return """
FAQ pour les Arbitres - Confirmation des Équipes
Comment fonctionne le délai de confirmation pour les équipes ?
Comment fonctionne le délai de confirmation pour les équipes ?
Notre système calcule automatiquement un délai de confirmation adapté pour les équipes en fonction de trois facteurs principaux :
- Proximité du tournoi : Plus le tournoi est proche, plus le délai est court
- Pression de la liste d'attente : Plus il y a d'équipes en attente, plus le délai est court
- Heures ouvrables : Les délais respectent généralement les heures ouvrables (8h-21h)
Notre système calcule automatiquement un délai de confirmation adapté pour les équipes en fonction de trois facteurs principaux :
- Proximité du tournoi : Plus le tournoi est proche, plus le délai est court
- Pression de la liste d'attente : Plus il y a d'équipes en attente, plus le délai est court
- Heures ouvrables : Les délais respectent généralement les heures ouvrables (\(startHour)h-\(endHour)h)
Quels sont les délais typiques de confirmation ?
- Tournoi dans moins de 24h 30 minutes
- Tournoi dans moins de 48h 60 minutes (1 heure)
- Tournoi dans moins de 72h 120 minutes (2 heures)
- Tournoi dans plus de 72h 240 minutes (4 heures)
Quels sont les délais typiques de confirmation ?
- Tournoi dans moins de 24h \(formatMinutes(proximityRules["24"] ?? 30))
- Tournoi dans moins de 48h \(formatMinutes(proximityRules["48"] ?? 60))
- Tournoi dans moins de 72h \(formatMinutes(proximityRules["72"] ?? 120))
- Tournoi dans plus de 72h \(formatMinutes(defaultTime))
Ces délais peuvent être raccourcis en fonction du nombre d'équipes en liste d'attente :
- 30+ équipes en attente 30 minutes
- 20+ équipes en attente 60 minutes (1 heure)
- 10+ équipes en attente 120 minutes (2 heures)
Ces délais peuvent être raccourcis en fonction du nombre d'équipes en liste d'attente :
- \(waitingRules["30"] ?? 30)+ équipes en attente \(formatMinutes(waitingRules["30"] ?? 30))
- \(waitingRules["20"] ?? 20)+ équipes en attente \(formatMinutes(waitingRules["20"] ?? 60))
- \(waitingRules["10"] ?? 10)+ équipes en attente \(formatMinutes(waitingRules["10"] ?? 120))
Y a-t-il des exceptions à ces règles ?
Y a-t-il des exceptions à ces règles ?
Oui, dans les situations urgentes :
- Si le tournoi commence dans moins de 24h, les restrictions d'heures ouvrables sont ignorées
- Si le tournoi commence dans moins de 12h, toutes les restrictions sont assouplies avec un minimum de 30 minutes de délai
Oui, dans les situations urgentes :
- Si le tournoi commence dans moins de 24h, les restrictions d'heures ouvrables sont ignorées
- Si le tournoi commence dans moins de 12h, toutes les restrictions sont assouplies avec un minimum de \(minResponseTime) minutes de délai
Comment les délais sont-ils arrondis ?
Comment les délais sont-ils arrondis ?
Les délais sont toujours arrondis à la demi-heure supérieure pour plus de simplicité.
Les délais sont toujours arrondis à la demi-heure supérieure pour plus de simplicité.
Que se passe-t-il si le délai tombe en dehors des heures ouvrables ?
Que se passe-t-il si le délai tombe en dehors des heures ouvrables ?
Si le délai calculé tombe en dehors des heures ouvrables (avant 8h ou après 21h), il est automatiquement reporté au jour ouvrable suivant à 8h du matin.
"""
Si le délai calculé tombe en dehors des heures ouvrables (avant \(startHour)h ou après \(endHour)h), il est automatiquement reporté au jour ouvrable suivant à \(timeToConfirmConfig.businessRules.hours.defaultConfirmationHour)h du matin.
"""
}
var body: some View {
NavigationView {
ScrollView {

@ -40,6 +40,10 @@ struct RegistrationSetupView: View {
@State private var refundDateLimitEnabled: Bool
@State private var stripeAccountId: String
@State private var stripeAccountIdIsInvalid: Bool = false
@State private var paymentConfig: PaymentConfig?
@State private var timeToConfirmConfig: TimeToConfirmConfig?
@FocusState private var focusedField: Tournament.CodingKeys?
@State private var hasChanges: Bool = false
@ -169,6 +173,13 @@ struct RegistrationSetupView: View {
}
}
}
.task {
do {
self.timeToConfirmConfig = try await ConfigurationService.fetchTournamentConfig()
} catch {
print("Error fetching configuration: \(error)")
}
}
}
Section {
@ -276,7 +287,7 @@ struct RegistrationSetupView: View {
RegistrationInfoSheetView()
}
.sheet(isPresented: $showMoreOnlineWaitingListInfos) {
OnlineWaitingListFaqSheetView()
OnlineWaitingListFaqSheetView(timeToConfirmConfig: timeToConfirmConfig ?? TimeToConfirmConfig.defaultConfig)
}
.sheet(isPresented: $showMorePaymentInfos) {
PaymentInfoSheetView()
@ -367,20 +378,21 @@ struct RegistrationSetupView: View {
}
}
@ViewBuilder
private func _onlinePaymentsView() -> some View {
let entryFee = tournament.entryFee ?? 20
Section {
Toggle(isOn: $enableOnlinePayment) {
Text("Activer le paiement en ligne")
Text("Cette fonction peut entraîner un coût supplémentaire.")
.foregroundStyle(.logoRed)
.bold()
}
if enableOnlinePayment {
if let fee = dataStore.user.registrationPaymentMode.localizedRegistrationPaymentFee() {
let entryFee = tournament.entryFee ?? 20
let sample = dataStore.user.registrationPaymentMode.sample(entryFee: entryFee)
Text("Cette fonction entraîne un coût supplémentaire, en effet Padel Club touchera une commission de \(fee) par paiement en ligne. Soit \(sample) centimes pour une inscription de \(entryFee)€ par exemple.").foregroundStyle(.logoRed).bold()
}
Toggle(isOn: $onlinePaymentIsMandatory) {
Text("Paiement obligatoire")
}
@ -444,7 +456,13 @@ struct RegistrationSetupView: View {
}
}
}
.task {
do {
self.paymentConfig = try await ConfigurationService.fetchPaymentConfig()
} catch {
print("Error fetching configuration: \(error)")
}
}
.onChange(of: [enableOnlinePayment, onlinePaymentIsMandatory, enableOnlinePaymentRefund]) {
_hasChanged()
}
@ -460,6 +478,39 @@ struct RegistrationSetupView: View {
}
}
if dataStore.user.registrationPaymentMode.requiresStripe() {
Section {
let fixedFee = 0.25 // Fixed fee in euros
let percentageFee = 0.012 // 1.2%
let totalStripeFee = fixedFee + (entryFee * percentageFee)
LabeledContent {
Text("\(fixedFee, format: .currency(code: "EUR")) + \(percentageFee, format: .percent)")
} label: {
Text("Commission Stripe")
}
Text("Soit \(totalStripeFee, format: .currency(code: "EUR")) pour \(entryFee, format: .currency(code: "EUR")).")
}
}
Section {
if isCorporateTournament == false, dataStore.user.registrationPaymentMode.requiresStripe() {
let padelClubFee = paymentConfig?.stripeFee ?? PaymentConfig.defaultConfig.stripeFee
let feeAmount = entryFee * padelClubFee
LabeledContent {
Text(padelClubFee, format: .percent)
} label: {
Text("Commission Padel Club")
}
Text("Soit \(feeAmount, format: .currency(code: "EUR")) pour \(entryFee, format: .currency(code: "EUR")).")
} else {
Text("Aucune commission Padel Club ne sera prélevée.").foregroundStyle(.logoRed).bold()
}
}
}
private func _confirmStripeAccountId() {

Loading…
Cancel
Save