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. 59
      PadelClub/Views/Team/EditingTeamView.swift
  9. 135
      PadelClub/Views/Tournament/Screen/Components/TournamentStatusView.swift

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

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

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

@ -2379,6 +2379,14 @@ defer {
unsortedTeams().filter({ $0.hasRegisteredOnline() })
}
func paidOnlineTeams() -> [TeamRegistration] {
unsortedTeams().filter({ $0.hasPaidOnline() })
}
func refundTeams() {
}
func shouldWarnOnlineRegistrationUpdates() -> Bool {
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,7 +25,9 @@ struct EditingTeamView: View {
@State private var wildCardGroupStage : Bool
@State private var name: String
@FocusState private var focusedField: TeamRegistration.CodingKeys?
@State private var isProcessingRefund = false
@State private var refundMessage: String?
var messageSentFailed: Binding<Bool> {
Binding {
sentError != nil
@ -101,6 +103,35 @@ struct EditingTeamView: View {
}
.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 {
if let callDate = team.callDate {
LabeledContent() {
@ -378,6 +409,32 @@ struct EditingTeamView: View {
private var _networkErrorMessage: String {
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 {

@ -42,65 +42,123 @@ struct TournamentStatusView: View {
}
}
let paidOnlineTeams = tournament.paidOnlineTeams()
Section {
RowButtonView("Supprimer le tournoi", role: .destructive) {
if tournament.payment == nil {
do {
let event = tournament.eventObject()
let isLastTournament = event?.tournaments.count == 1
if tournament.onlineTeams().isEmpty == false {
tournament.isDeleted = true
try dataStore.tournaments.addOrUpdate(instance: tournament)
} else {
if let event, isLastTournament {
try dataStore.events.delete(instance: event)
} else {
try dataStore.tournaments.delete(instance: tournament)
}
}
if eventDismiss == false || isLastTournament {
navigation.path = NavigationPath()
let event = tournament.eventObject()
let isLastTournament = event?.tournaments.count == 1
if tournament.onlineTeams().isEmpty == false {
tournament.isDeleted = true
dataStore.tournaments.addOrUpdate(instance: tournament)
} else {
if let event, isLastTournament {
dataStore.events.delete(instance: event)
} else {
dismiss()
dataStore.tournaments.delete(instance: tournament)
}
} catch {
Logger.error(error)
}
if eventDismiss == false || isLastTournament {
navigation.path = NavigationPath()
} else {
dismiss()
}
} else {
tournament.endDate = Date()
tournament.isDeleted = true
tournament.navigationPath.removeAll()
do {
try dataStore.tournaments.addOrUpdate(instance: tournament)
if eventDismiss == false {
navigation.path = NavigationPath()
} else {
dismiss()
}
} catch {
Logger.error(error)
_save()
if eventDismiss == false {
navigation.path = NavigationPath()
} else {
dismiss()
}
}
}
} 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.")
} else {
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 {
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) {
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.isCanceled = true
do {
try dataStore.tournaments.addOrUpdate(instance: tournament)
} catch {
Logger.error(error)
}
_save()
dismiss()
}
} footer: {
@ -118,6 +176,9 @@ struct TournamentStatusView: View {
}
.navigationTitle("Gestion du tournoi")
.toolbarBackground(.visible, for: .navigationBar)
.onChange(of: tournament.enableOnlinePaymentRefund) {
_save()
}
.onChange(of: tournament.endDate) {
_save()
}
@ -130,11 +191,7 @@ struct TournamentStatusView: View {
}
private func _save() {
do {
try dataStore.tournaments.addOrUpdate(instance: tournament)
} catch {
Logger.error(error)
}
dataStore.tournaments.addOrUpdate(instance: tournament)
}
}

Loading…
Cancel
Save