From 54e872622f8b4a4aa4a670aab6523fdd58f075a8 Mon Sep 17 00:00:00 2001 From: Raz Date: Mon, 14 Apr 2025 10:44:38 +0200 Subject: [PATCH] clean up registration fee messages --- PadelClub.xcodeproj/project.pbxproj | 8 ++ PadelClub/Data/CustomUser.swift | 42 +----- .../Utils/Network/ConfigurationService.swift | 126 ++++++++++++++++++ .../OnlineWaitingListFaqSheetView.swift | 80 +++++++---- .../Screen/RegistrationSetupView.swift | 69 ++++++++-- 5 files changed, 249 insertions(+), 76 deletions(-) create mode 100644 PadelClub/Utils/Network/ConfigurationService.swift diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index 22dbacd..8696233 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -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 = ""; }; FFE8B5C62DAA390000BDE966 /* StripeValidationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StripeValidationService.swift; sourceTree = ""; }; FFE8B5CA2DAA429E00BDE966 /* XlsToCsvService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XlsToCsvService.swift; sourceTree = ""; }; + FFE8B6392DACEAEC00BDE966 /* ConfigurationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationService.swift; sourceTree = ""; }; FFE8C2BF2C7601E80046B243 /* ConfirmButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfirmButtonView.swift; sourceTree = ""; }; FFEF7F4D2BDE69130033D0F0 /* MenuWarningView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuWarningView.swift; sourceTree = ""; }; FFF0241C2BF48B15001F14B4 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = ""; }; @@ -2066,6 +2070,7 @@ FFE8B5BA2DA9896800BDE966 /* RefundService.swift */, FFE8B5C62DAA390000BDE966 /* StripeValidationService.swift */, FFE8B5CA2DAA429E00BDE966 /* XlsToCsvService.swift */, + FFE8B6392DACEAEC00BDE966 /* ConfigurationService.swift */, ); path = Network; sourceTree = ""; @@ -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 */, diff --git a/PadelClub/Data/CustomUser.swift b/PadelClub/Data/CustomUser.swift index 6e1d48a..8a1be03 100644 --- a/PadelClub/Data/CustomUser.swift +++ b/PadelClub/Data/CustomUser.swift @@ -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: diff --git a/PadelClub/Utils/Network/ConfigurationService.swift b/PadelClub/Utils/Network/ConfigurationService.swift new file mode 100644 index 0000000..c012a8e --- /dev/null +++ b/PadelClub/Utils/Network/ConfigurationService.swift @@ -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" + } + +} diff --git a/PadelClub/Views/Shared/OnlineWaitingListFaqSheetView.swift b/PadelClub/Views/Shared/OnlineWaitingListFaqSheetView.swift index 3dcae44..ff2630a 100644 --- a/PadelClub/Views/Shared/OnlineWaitingListFaqSheetView.swift +++ b/PadelClub/Views/Shared/OnlineWaitingListFaqSheetView.swift @@ -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 { diff --git a/PadelClub/Views/Tournament/Screen/RegistrationSetupView.swift b/PadelClub/Views/Tournament/Screen/RegistrationSetupView.swift index 75dc14d..3c89a32 100644 --- a/PadelClub/Views/Tournament/Screen/RegistrationSetupView.swift +++ b/PadelClub/Views/Tournament/Screen/RegistrationSetupView.swift @@ -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() {