add refund system

online_payment
Raz 7 months ago
parent 02969cc971
commit b95c7acb83
  1. 12
      PadelClub.xcodeproj/project.pbxproj
  2. 4
      PadelClub/Data/PlayerRegistration.swift
  3. 4
      PadelClub/Data/TeamRegistration.swift
  4. 8
      PadelClub/Data/Tournament.swift
  5. 44
      PadelClub/Utils/Network/RefundService.swift
  6. 0
      PadelClub/Views/Shared/InscriptionLegendView.swift
  7. 0
      PadelClub/Views/Shared/RegistrationInfoSheetView.swift
  8. 57
      PadelClub/Views/Team/EditingTeamView.swift
  9. 133
      PadelClub/Views/Tournament/Screen/Components/TournamentStatusView.swift

@ -976,6 +976,9 @@
FFE8B5B72DA8763800BDE966 /* PaymentInfoSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFE8B5B62DA8763800BDE966 /* PaymentInfoSheetView.swift */; }; FFE8B5B72DA8763800BDE966 /* PaymentInfoSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFE8B5B62DA8763800BDE966 /* PaymentInfoSheetView.swift */; };
FFE8B5B82DA8763800BDE966 /* PaymentInfoSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFE8B5B62DA8763800BDE966 /* PaymentInfoSheetView.swift */; }; FFE8B5B82DA8763800BDE966 /* PaymentInfoSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFE8B5B62DA8763800BDE966 /* PaymentInfoSheetView.swift */; };
FFE8B5B92DA8763800BDE966 /* PaymentInfoSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFE8B5B62DA8763800BDE966 /* PaymentInfoSheetView.swift */; }; FFE8B5B92DA8763800BDE966 /* PaymentInfoSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFE8B5B62DA8763800BDE966 /* PaymentInfoSheetView.swift */; };
FFE8B5BB2DA9896800BDE966 /* RefundService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFE8B5BA2DA9896800BDE966 /* RefundService.swift */; };
FFE8B5BC2DA9896800BDE966 /* RefundService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFE8B5BA2DA9896800BDE966 /* RefundService.swift */; };
FFE8B5BD2DA9896800BDE966 /* RefundService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFE8B5BA2DA9896800BDE966 /* RefundService.swift */; };
FFE8C2C02C7601E80046B243 /* ConfirmButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFE8C2BF2C7601E80046B243 /* ConfirmButtonView.swift */; }; FFE8C2C02C7601E80046B243 /* ConfirmButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFE8C2BF2C7601E80046B243 /* ConfirmButtonView.swift */; };
FFEF7F4E2BDE69130033D0F0 /* MenuWarningView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFEF7F4D2BDE69130033D0F0 /* MenuWarningView.swift */; }; FFEF7F4E2BDE69130033D0F0 /* MenuWarningView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFEF7F4D2BDE69130033D0F0 /* MenuWarningView.swift */; };
FFF0241E2BF48B15001F14B4 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = FFF0241D2BF48B15001F14B4 /* Localizable.strings */; }; FFF0241E2BF48B15001F14B4 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = FFF0241D2BF48B15001F14B4 /* Localizable.strings */; };
@ -1415,6 +1418,7 @@
FFE2D2E12C231BEE00D0C7BE /* SupportButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SupportButtonView.swift; sourceTree = "<group>"; }; FFE2D2E12C231BEE00D0C7BE /* SupportButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SupportButtonView.swift; sourceTree = "<group>"; };
FFE8B5B22DA848D300BDE966 /* OnlineWaitingListFaqSheetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnlineWaitingListFaqSheetView.swift; sourceTree = "<group>"; }; FFE8B5B22DA848D300BDE966 /* OnlineWaitingListFaqSheetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnlineWaitingListFaqSheetView.swift; sourceTree = "<group>"; };
FFE8B5B62DA8763800BDE966 /* PaymentInfoSheetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentInfoSheetView.swift; sourceTree = "<group>"; }; FFE8B5B62DA8763800BDE966 /* PaymentInfoSheetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentInfoSheetView.swift; sourceTree = "<group>"; };
FFE8B5BA2DA9896800BDE966 /* RefundService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefundService.swift; sourceTree = "<group>"; };
FFE8C2BF2C7601E80046B243 /* ConfirmButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfirmButtonView.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>"; }; 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>"; }; FFF0241C2BF48B15001F14B4 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = "<group>"; };
@ -1521,8 +1525,6 @@
FFD784002B91BF79000F62A6 /* Launch Screen.storyboard */, FFD784002B91BF79000F62A6 /* Launch Screen.storyboard */,
FF2B515F2C7E300500FFF126 /* SeedData */, FF2B515F2C7E300500FFF126 /* SeedData */,
C4A47D722B72881500ADC637 /* Views */, C4A47D722B72881500ADC637 /* Views */,
FF3A73F22D37C34C007E3032 /* RegistrationInfoSheetView.swift */,
FF3A74312D37DCF2007E3032 /* InscriptionLegendView.swift */,
FF3F74FD2B91A087004CFE0E /* ViewModel */, FF3F74FD2B91A087004CFE0E /* ViewModel */,
C4A47D5F2B6D3B2D00ADC637 /* Data */, C4A47D5F2B6D3B2D00ADC637 /* Data */,
FFF8ACD02B9238A2008466FA /* Utils */, FFF8ACD02B9238A2008466FA /* Utils */,
@ -2019,6 +2021,8 @@
FFE103112C366E5900684FC9 /* ImagePickerView.swift */, FFE103112C366E5900684FC9 /* ImagePickerView.swift */,
FFBFC3942CF05CBB000EBD8D /* DateMenuView.swift */, FFBFC3942CF05CBB000EBD8D /* DateMenuView.swift */,
FFE8B5B62DA8763800BDE966 /* PaymentInfoSheetView.swift */, FFE8B5B62DA8763800BDE966 /* PaymentInfoSheetView.swift */,
FF3A73F22D37C34C007E3032 /* RegistrationInfoSheetView.swift */,
FF3A74312D37DCF2007E3032 /* InscriptionLegendView.swift */,
); );
path = Shared; path = Shared;
sourceTree = "<group>"; sourceTree = "<group>";
@ -2051,6 +2055,7 @@
FF4AB6B42B9248200002987F /* NetworkManager.swift */, FF4AB6B42B9248200002987F /* NetworkManager.swift */,
FFC1E1092BAC2A77008D6F59 /* NetworkFederalService.swift */, FFC1E1092BAC2A77008D6F59 /* NetworkFederalService.swift */,
FF6EC9052B947A1000EA7F5A /* NetworkManagerError.swift */, FF6EC9052B947A1000EA7F5A /* NetworkManagerError.swift */,
FFE8B5BA2DA9896800BDE966 /* RefundService.swift */,
); );
path = Network; path = Network;
sourceTree = "<group>"; sourceTree = "<group>";
@ -2671,6 +2676,7 @@
C488C8352CC7E4240082001F /* BaseMatchScheduler.swift in Sources */, C488C8352CC7E4240082001F /* BaseMatchScheduler.swift in Sources */,
C488C8372CC7E4240082001F /* BasePlayerRegistration.swift in Sources */, C488C8372CC7E4240082001F /* BasePlayerRegistration.swift in Sources */,
C488C8382CC7E4240082001F /* BaseTeamScore.swift in Sources */, C488C8382CC7E4240082001F /* BaseTeamScore.swift in Sources */,
FFE8B5BB2DA9896800BDE966 /* RefundService.swift in Sources */,
C488C8392CC7E4240082001F /* BaseTournament.swift in Sources */, C488C8392CC7E4240082001F /* BaseTournament.swift in Sources */,
FF6087EC2BE26A2F004E1E47 /* BroadcastView.swift in Sources */, FF6087EC2BE26A2F004E1E47 /* BroadcastView.swift in Sources */,
FFF964552BC266CF00EEF017 /* SchedulerView.swift in Sources */, FFF964552BC266CF00EEF017 /* SchedulerView.swift in Sources */,
@ -3130,6 +3136,7 @@
FF4CBFF22C996C0600151637 /* TournamentFormatSelectionView.swift in Sources */, FF4CBFF22C996C0600151637 /* TournamentFormatSelectionView.swift in Sources */,
FF17CA592CC02FEB003C7323 /* CoachListView.swift in Sources */, FF17CA592CC02FEB003C7323 /* CoachListView.swift in Sources */,
FF4CBFF32C996C0600151637 /* MatchListView.swift in Sources */, FF4CBFF32C996C0600151637 /* MatchListView.swift in Sources */,
FFE8B5BD2DA9896800BDE966 /* RefundService.swift in Sources */,
FF4CBFF42C996C0600151637 /* PadelClubApp.swift in Sources */, FF4CBFF42C996C0600151637 /* PadelClubApp.swift in Sources */,
FF4CBFF52C996C0600151637 /* TournamentSettingsView.swift in Sources */, FF4CBFF52C996C0600151637 /* TournamentSettingsView.swift in Sources */,
C4A36F582CE2626A003738C6 /* TournamentLibrary.swift in Sources */, C4A36F582CE2626A003738C6 /* TournamentLibrary.swift in Sources */,
@ -3426,6 +3433,7 @@
FF70FB712C90584900129CC2 /* TournamentFormatSelectionView.swift in Sources */, FF70FB712C90584900129CC2 /* TournamentFormatSelectionView.swift in Sources */,
FF17CA582CC02FEB003C7323 /* CoachListView.swift in Sources */, FF17CA582CC02FEB003C7323 /* CoachListView.swift in Sources */,
FF70FB722C90584900129CC2 /* MatchListView.swift in Sources */, FF70FB722C90584900129CC2 /* MatchListView.swift in Sources */,
FFE8B5BC2DA9896800BDE966 /* RefundService.swift in Sources */,
FF70FB732C90584900129CC2 /* PadelClubApp.swift in Sources */, FF70FB732C90584900129CC2 /* PadelClubApp.swift in Sources */,
FF70FB742C90584900129CC2 /* TournamentSettingsView.swift in Sources */, FF70FB742C90584900129CC2 /* TournamentSettingsView.swift in Sources */,
C4A36F592CE2626A003738C6 /* TournamentLibrary.swift in Sources */, C4A36F592CE2626A003738C6 /* TournamentLibrary.swift in Sources */,

@ -378,6 +378,10 @@ final class PlayerRegistration: BasePlayerRegistration, SideStorable {
return false return false
} }
func hasPaidOnline() -> Bool {
registrationStatus == .confirmed && paymentId != nil && paymentType == .creditCard
}
enum PlayerDataSource: Int, Codable { enum PlayerDataSource: Int, Codable {
case frenchFederation = 0 case frenchFederation = 0
case beachPadel = 1 case beachPadel = 1

@ -79,6 +79,10 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
players().anySatisfy({ $0.registeredOnline }) players().anySatisfy({ $0.registeredOnline })
} }
func hasPaidOnline() -> Bool {
players().anySatisfy({ $0.hasPaidOnline() })
}
func unrankedOrUnknown() -> Bool { func unrankedOrUnknown() -> Bool {
players().anySatisfy({ $0.source == nil }) players().anySatisfy({ $0.source == nil })
} }

@ -2379,6 +2379,14 @@ defer {
unsortedTeams().filter({ $0.hasRegisteredOnline() }) unsortedTeams().filter({ $0.hasRegisteredOnline() })
} }
func paidOnlineTeams() -> [TeamRegistration] {
unsortedTeams().filter({ $0.hasPaidOnline() })
}
func refundTeams() {
}
func shouldWarnOnlineRegistrationUpdates() -> Bool { func shouldWarnOnlineRegistrationUpdates() -> Bool {
enableOnlineRegistration && onlineTeams().isEmpty == false && hasEnded() == false && hasStarted() == false enableOnlineRegistration && onlineTeams().isEmpty == false && hasEnded() == false && hasStarted() == false
} }

@ -0,0 +1,44 @@
//
// RefundService.swift
// PadelClub
//
// Created by razmig on 11/04/2025.
//
import Foundation
import LeStorage
class RefundService {
static func processRefund(teamRegistrationId: String) async throws -> RefundResponse {
let service = try StoreCenter.main.service()
let urlRequest = try service._baseRequest(servicePath: "refund-tournament/\(teamRegistrationId)/", method: .post, requiresToken: true)
let (data, response) = try await URLSession.shared.data(for: urlRequest)
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
throw RefundError.requestFailed
}
let refundResponse = try JSONDecoder().decode(RefundResponse.self, from: data)
return refundResponse
}
}
struct RefundResponse: Codable {
let success: Bool
let message: String
let players: [PlayerRegistration]?
enum CodingKeys: String, CodingKey {
case success
case message
case players
}
}
enum RefundError: Error {
case requestFailed
case unauthorized
case unknown
}

@ -25,6 +25,8 @@ struct EditingTeamView: View {
@State private var wildCardGroupStage : Bool @State private var wildCardGroupStage : Bool
@State private var name: String @State private var name: String
@FocusState private var focusedField: TeamRegistration.CodingKeys? @FocusState private var focusedField: TeamRegistration.CodingKeys?
@State private var isProcessingRefund = false
@State private var refundMessage: String?
var messageSentFailed: Binding<Bool> { var messageSentFailed: Binding<Bool> {
Binding { Binding {
@ -101,6 +103,35 @@ struct EditingTeamView: View {
} }
.headerProminence(.increased) .headerProminence(.increased)
if team.hasRegisteredOnline() || team.hasPaidOnline() {
Section {
LabeledContent {
Text(team.hasRegisteredOnline() ? "Oui" : "Non")
} label: {
Text("Inscrits en ligne")
}
LabeledContent {
Text(team.hasPaidOnline() ? "Oui" : "Non")
} label: {
Text("Payé en ligne")
}
if team.hasPaidOnline() {
if let refundMessage, refundMessage.isEmpty == false {
Text(refundMessage).foregroundStyle(.logoRed)
}
RowButtonView("Rembourser l'équipe", role: .destructive) {
await _processRefund()
}
}
} footer: {
if team.hasPaidOnline() {
Text("Le remboursement passe part le service de Stripe qui re-crédite le moyen de paiement utilisé du montant payé.")
}
}
}
Section { Section {
if let callDate = team.callDate { if let callDate = team.callDate {
LabeledContent() { LabeledContent() {
@ -378,6 +409,32 @@ struct EditingTeamView: View {
private var _networkErrorMessage: String { private var _networkErrorMessage: String {
ContactManagerError.getNetworkErrorMessage(sentError: sentError, networkMonitorConnected: networkMonitor.connected) ContactManagerError.getNetworkErrorMessage(sentError: sentError, networkMonitorConnected: networkMonitor.connected)
} }
private func _processRefund() async {
isProcessingRefund = true
do {
let response = try await RefundService.processRefund(teamRegistrationId: team.id)
await MainActor.run {
isProcessingRefund = false
refundMessage = response.message
if response.success {
if let players = response.players {
tournamentStore?.playerRegistrations.addOrUpdate(contentOfs: players)
} else {
refundMessage = response.message + "\nLa mise à jour des équipes n'a pas été reçue pour le moment."
}
}
}
} catch {
await MainActor.run {
isProcessingRefund = false
refundMessage = "Erreur lors du remboursement. Veuillez réessayer."
}
}
}
} }
//#Preview { //#Preview {

@ -42,65 +42,123 @@ struct TournamentStatusView: View {
} }
} }
let paidOnlineTeams = tournament.paidOnlineTeams()
Section { Section {
RowButtonView("Supprimer le tournoi", role: .destructive) { RowButtonView("Supprimer le tournoi", role: .destructive) {
if tournament.payment == nil { if tournament.payment == nil {
do { let event = tournament.eventObject()
let event = tournament.eventObject() let isLastTournament = event?.tournaments.count == 1
let isLastTournament = event?.tournaments.count == 1
if tournament.onlineTeams().isEmpty == false { if tournament.onlineTeams().isEmpty == false {
tournament.isDeleted = true tournament.isDeleted = true
try dataStore.tournaments.addOrUpdate(instance: tournament) dataStore.tournaments.addOrUpdate(instance: tournament)
} else { } else {
if let event, isLastTournament { if let event, isLastTournament {
try dataStore.events.delete(instance: event) dataStore.events.delete(instance: event)
} else {
try dataStore.tournaments.delete(instance: tournament)
}
}
if eventDismiss == false || isLastTournament {
navigation.path = NavigationPath()
} else { } else {
dismiss() dataStore.tournaments.delete(instance: tournament)
} }
} catch { }
Logger.error(error) if eventDismiss == false || isLastTournament {
navigation.path = NavigationPath()
} else {
dismiss()
} }
} else { } else {
tournament.endDate = Date() tournament.endDate = Date()
tournament.isDeleted = true tournament.isDeleted = true
tournament.navigationPath.removeAll() tournament.navigationPath.removeAll()
do { _save()
try dataStore.tournaments.addOrUpdate(instance: tournament) if eventDismiss == false {
if eventDismiss == false { navigation.path = NavigationPath()
navigation.path = NavigationPath() } else {
} else { dismiss()
dismiss()
}
} catch {
Logger.error(error)
} }
} }
} }
} footer: { } footer: {
if tournament.payment == nil { if paidOnlineTeams.isEmpty == false {
Text("Ce tournoi ne peut pas étre supprimé, seulement annulé car il contient des équipes ayant payé en ligne.")
} else if tournament.payment == nil {
Text("Ce tournoi sera supprimé et n'a pas été comptabilisé dans vos achats. Toutes les données seront supprimées.") Text("Ce tournoi sera supprimé et n'a pas été comptabilisé dans vos achats. Toutes les données seront supprimées.")
} else { } else {
Text("Ce tournoi sera supprimé et a déjà été comptabilisé dans vos achats. Toutes les données seront supprimées.") Text("Ce tournoi sera supprimé et a déjà été comptabilisé dans vos achats. Toutes les données seront supprimées.")
} }
} }
.disabled(paidOnlineTeams.isEmpty == false)
if tournament.hasEnded() == false && tournament.isCanceled == false { if tournament.hasEnded() == false && tournament.isCanceled == false {
Section { Section {
@Bindable var bindableTournament: Tournament = tournament
if paidOnlineTeams.isEmpty == false {
LabeledContent {
Text(paidOnlineTeams.count.formatted())
} label: {
Text("Équipes ayant payé en ligne")
}
Toggle(isOn: $bindableTournament.enableOnlinePaymentRefund) {
Text("Remboursement possible")
}
if tournament.enableOnlinePaymentRefund {
if let refundDateLimit = tournament.refundDateLimit {
LabeledContent {
Text(refundDateLimit.formatted())
} label: {
Text("Date limite")
}
if refundDateLimit.isEarlierThan(Date()) == false {
Text("Le remboursement est toujours possible")
} else {
Text("La date limite a été dépassé")
FooterButtonView("Retirer la date limite ?", role: .destructive) {
tournament.refundDateLimit = nil
_save()
}
}
}
}
if tournament.enableOnlinePaymentRefund {
if let refundDateLimit = tournament.refundDateLimit {
if refundDateLimit.isEarlierThan(Date()) == false {
Text("\(paidOnlineTeams.count) équipe\(paidOnlineTeams.count.pluralSuffix) seront remboursée\(paidOnlineTeams.count.pluralSuffix)")
} else {
Text("Les équipes ayant payé en ligne ne seront pas automatiquement remboursées car la date limite a été dépassé")
}
} else {
Text("Les équipes ayant payé en ligne seront remboursées")
}
} else {
Text("Les équipes ayant payé en ligne ne seront pas automatiquement remboursées vous n'avez pas autorisé le remboursement.")
}
Text("Si vous annulez ce tournoi vous pouvez toujours gérer les remboursements au cas par cas dans la vue gestion des inscriptions du tournoi.")
}
RowButtonView("Annuler le tournoi", role: .destructive) { RowButtonView("Annuler le tournoi", role: .destructive) {
if paidOnlineTeams.isEmpty == false {
if tournament.enableOnlinePaymentRefund {
if let refundDateLimit = tournament.refundDateLimit {
if refundDateLimit.isEarlierThan(Date()) == false {
tournament.refundTeams()
}
} else {
tournament.refundTeams()
}
}
}
tournament.endDate = Date() tournament.endDate = Date()
tournament.isCanceled = true tournament.isCanceled = true
do { _save()
try dataStore.tournaments.addOrUpdate(instance: tournament)
} catch {
Logger.error(error)
}
dismiss() dismiss()
} }
} footer: { } footer: {
@ -118,6 +176,9 @@ struct TournamentStatusView: View {
} }
.navigationTitle("Gestion du tournoi") .navigationTitle("Gestion du tournoi")
.toolbarBackground(.visible, for: .navigationBar) .toolbarBackground(.visible, for: .navigationBar)
.onChange(of: tournament.enableOnlinePaymentRefund) {
_save()
}
.onChange(of: tournament.endDate) { .onChange(of: tournament.endDate) {
_save() _save()
} }
@ -130,11 +191,7 @@ struct TournamentStatusView: View {
} }
private func _save() { private func _save() {
do { dataStore.tournaments.addOrUpdate(instance: tournament)
try dataStore.tournaments.addOrUpdate(instance: tournament)
} catch {
Logger.error(error)
}
} }
} }

Loading…
Cancel
Save