Merge branch 'main'

sync3
Razmig Sarkissian 3 months ago
commit 5ea1518e2e
  1. 30
      PadelClubData/Business.swift
  2. 18
      PadelClubData/Data/DataStore.swift
  3. 23
      PadelClubData/Data/Gen/BasePlayerRegistration.swift
  4. 9
      PadelClubData/Data/Gen/BaseTournament.swift
  5. 15
      PadelClubData/Data/Gen/PlayerRegistration.json
  6. 5
      PadelClubData/Data/Gen/Tournament.json
  7. 53
      PadelClubData/Data/Match.swift
  8. 3
      PadelClubData/Data/MatchScheduler.swift
  9. 4
      PadelClubData/Data/PlayerPaymentType.swift
  10. 16
      PadelClubData/Data/PlayerRegistration.swift
  11. 8
      PadelClubData/Data/TeamRegistration.swift
  12. 89
      PadelClubData/Data/Tournament.swift
  13. 24
      PadelClubData/Extensions/Date+Extensions.swift
  14. 18
      PadelClubData/Extensions/URL+Extensions.swift
  15. 36
      PadelClubData/Utils/NetworkManagerError.swift
  16. 97
      PadelClubData/ViewModel/PadelRule.swift

@ -84,6 +84,36 @@ public enum OnlineRegistrationStatus: Int {
} }
} }
public enum PaymentStatus {
case notEnabled
case notConfigured // Online payment not set up (no Stripe account)
case optionalPayment // Online payment is optional
case mandatoryRefundEnabled // Online payment is mandatory, and refunds are enabled within the refund period
case mandatoryRefundEnded // Online payment is mandatory, refunds were enabled but the refund period has ended
case mandatoryNoRefund // Online payment is mandatory, but refunds are not enabled
/**
Returns a localized string representation of the payment status.
This method allows for easy display of the status in the UI.
*/
public func statusLocalized() -> String {
switch self {
case .notEnabled:
return "Paiement en ligne désactivé"
case .notConfigured:
return "Non configuré"
case .optionalPayment:
return "Facultatif"
case .mandatoryRefundEnabled:
return "Obligatoire avec remboursement"
case .mandatoryRefundEnded:
return "Obligatoire, remboursement terminé"
case .mandatoryNoRefund:
return "Obligatoire sans remboursement"
}
}
}
public enum PlayerFilterOption: Int, Hashable, CaseIterable, Identifiable { public enum PlayerFilterOption: Int, Hashable, CaseIterable, Identifiable {
case all = -1 case all = -1
case male = 1 case male = 1

@ -106,6 +106,7 @@ public class DataStore: ObservableObject {
if Store.main.fileCollectionsAllLoaded() { if Store.main.fileCollectionsAllLoaded() {
AutomaticPatcher.applyAllWhenApplicable() AutomaticPatcher.applyAllWhenApplicable()
self.resetOngoingCache()
} }
} }
@ -188,13 +189,21 @@ public class DataStore: ObservableObject {
} }
public func deleteTournament(_ tournament: Tournament) { public func deleteTournament(_ tournament: Tournament, noSync: Bool = false) {
let event = tournament.eventObject() let event = tournament.eventObject()
let isLastTournament = event?.tournaments.count == 1 let isLastTournament = event?.tournaments.count == 1
if noSync {
self.tournaments.deleteNoSync(instance: tournament, cascading: true)
if let event, isLastTournament {
self.events.deleteNoSync(instance: event, cascading: true)
}
} else {
self.tournaments.delete(instance: tournament) self.tournaments.delete(instance: tournament)
if let event, isLastTournament { if let event, isLastTournament {
self.events.delete(instance: event) self.events.delete(instance: event)
} }
}
StoreCenter.main.destroyStore(identifier: tournament.id) StoreCenter.main.destroyStore(identifier: tournament.id)
} }
@ -222,6 +231,7 @@ public class DataStore: ObservableObject {
self.user = self._temporaryLocalUser.item ?? CustomUser.placeHolder() self.user = self._temporaryLocalUser.item ?? CustomUser.placeHolder()
self.user.clubs.removeAll() self.user.clubs.removeAll()
self.user.licenceId = nil
} }
@ -316,6 +326,12 @@ public class DataStore: ObservableObject {
private var _lastRunningAndNextCheckDate: Date? = nil private var _lastRunningAndNextCheckDate: Date? = nil
private var _cachedRunningAndNextMatches: [Match]? = nil private var _cachedRunningAndNextMatches: [Match]? = nil
public func resetOngoingCache() {
_lastEndCheckDate = nil
_lastRunningCheckDate = nil
_lastRunningAndNextCheckDate = nil
}
public func runningAndNextMatches() -> [Match] { public func runningAndNextMatches() -> [Match] {
let dateNow : Date = Date() let dateNow : Date = Date()
if let lastCheck = _lastRunningAndNextCheckDate, if let lastCheck = _lastRunningAndNextCheckDate,

@ -40,6 +40,9 @@ public class BasePlayerRegistration: SyncedModelObject, SyncedStorable {
public var paymentId: String? = nil public var paymentId: String? = nil
public var clubCode: String? = nil public var clubCode: String? = nil
public var clubMember: Bool = false public var clubMember: Bool = false
public var contactName: String? = nil
public var contactPhoneNumber: String? = nil
public var contactEmail: String? = nil
public init( public init(
id: String = Store.randomId(), id: String = Store.randomId(),
@ -68,7 +71,10 @@ public class BasePlayerRegistration: SyncedModelObject, SyncedStorable {
registrationStatus: PlayerRegistration.RegistrationStatus = PlayerRegistration.RegistrationStatus.waiting, registrationStatus: PlayerRegistration.RegistrationStatus = PlayerRegistration.RegistrationStatus.waiting,
paymentId: String? = nil, paymentId: String? = nil,
clubCode: String? = nil, clubCode: String? = nil,
clubMember: Bool = false clubMember: Bool = false,
contactName: String? = nil,
contactPhoneNumber: String? = nil,
contactEmail: String? = nil
) { ) {
super.init() super.init()
self.id = id self.id = id
@ -98,6 +104,9 @@ public class BasePlayerRegistration: SyncedModelObject, SyncedStorable {
self.paymentId = paymentId self.paymentId = paymentId
self.clubCode = clubCode self.clubCode = clubCode
self.clubMember = clubMember self.clubMember = clubMember
self.contactName = contactName
self.contactPhoneNumber = contactPhoneNumber
self.contactEmail = contactEmail
} }
required public override init() { required public override init() {
super.init() super.init()
@ -131,6 +140,9 @@ public class BasePlayerRegistration: SyncedModelObject, SyncedStorable {
case _paymentId = "paymentId" case _paymentId = "paymentId"
case _clubCode = "clubCode" case _clubCode = "clubCode"
case _clubMember = "clubMember" case _clubMember = "clubMember"
case _contactName = "contactName"
case _contactPhoneNumber = "contactPhoneNumber"
case _contactEmail = "contactEmail"
} }
required init(from decoder: Decoder) throws { required init(from decoder: Decoder) throws {
@ -162,6 +174,9 @@ public class BasePlayerRegistration: SyncedModelObject, SyncedStorable {
self.paymentId = try container.decodeIfPresent(String.self, forKey: ._paymentId) ?? nil self.paymentId = try container.decodeIfPresent(String.self, forKey: ._paymentId) ?? nil
self.clubCode = try container.decodeIfPresent(String.self, forKey: ._clubCode) ?? nil self.clubCode = try container.decodeIfPresent(String.self, forKey: ._clubCode) ?? nil
self.clubMember = try container.decodeIfPresent(Bool.self, forKey: ._clubMember) ?? false self.clubMember = try container.decodeIfPresent(Bool.self, forKey: ._clubMember) ?? false
self.contactName = try container.decodeIfPresent(String.self, forKey: ._contactName) ?? nil
self.contactPhoneNumber = try container.decodeIfPresent(String.self, forKey: ._contactPhoneNumber) ?? nil
self.contactEmail = try container.decodeIfPresent(String.self, forKey: ._contactEmail) ?? nil
try super.init(from: decoder) try super.init(from: decoder)
} }
@ -194,6 +209,9 @@ public class BasePlayerRegistration: SyncedModelObject, SyncedStorable {
try container.encode(self.paymentId, forKey: ._paymentId) try container.encode(self.paymentId, forKey: ._paymentId)
try container.encode(self.clubCode, forKey: ._clubCode) try container.encode(self.clubCode, forKey: ._clubCode)
try container.encode(self.clubMember, forKey: ._clubMember) try container.encode(self.clubMember, forKey: ._clubMember)
try container.encode(self.contactName, forKey: ._contactName)
try container.encode(self.contactPhoneNumber, forKey: ._contactPhoneNumber)
try container.encode(self.contactEmail, forKey: ._contactEmail)
try super.encode(to: encoder) try super.encode(to: encoder)
} }
@ -231,6 +249,9 @@ public class BasePlayerRegistration: SyncedModelObject, SyncedStorable {
self.paymentId = playerregistration.paymentId self.paymentId = playerregistration.paymentId
self.clubCode = playerregistration.clubCode self.clubCode = playerregistration.clubCode
self.clubMember = playerregistration.clubMember self.clubMember = playerregistration.clubMember
self.contactName = playerregistration.contactName
self.contactPhoneNumber = playerregistration.contactPhoneNumber
self.contactEmail = playerregistration.contactEmail
} }
public static func parentRelationships() -> [Relationship] { public static func parentRelationships() -> [Relationship] {

@ -84,6 +84,7 @@ public class BaseTournament: SyncedModelObject, SyncedStorable {
public var showTeamsInProg: Bool = false public var showTeamsInProg: Bool = false
public var clubMemberFeeDeduction: Double? = nil public var clubMemberFeeDeduction: Double? = nil
public var unregisterDeltaInHours: Int = 24 public var unregisterDeltaInHours: Int = 24
public var currencyCode: String? = nil
public init( public init(
id: String = Store.randomId(), id: String = Store.randomId(),
@ -156,7 +157,8 @@ public class BaseTournament: SyncedModelObject, SyncedStorable {
publishProg: Bool = false, publishProg: Bool = false,
showTeamsInProg: Bool = false, showTeamsInProg: Bool = false,
clubMemberFeeDeduction: Double? = nil, clubMemberFeeDeduction: Double? = nil,
unregisterDeltaInHours: Int = 24 unregisterDeltaInHours: Int = 24,
currencyCode: String? = nil
) { ) {
super.init() super.init()
self.id = id self.id = id
@ -230,6 +232,7 @@ public class BaseTournament: SyncedModelObject, SyncedStorable {
self.showTeamsInProg = showTeamsInProg self.showTeamsInProg = showTeamsInProg
self.clubMemberFeeDeduction = clubMemberFeeDeduction self.clubMemberFeeDeduction = clubMemberFeeDeduction
self.unregisterDeltaInHours = unregisterDeltaInHours self.unregisterDeltaInHours = unregisterDeltaInHours
self.currencyCode = currencyCode
} }
required public override init() { required public override init() {
super.init() super.init()
@ -309,6 +312,7 @@ public class BaseTournament: SyncedModelObject, SyncedStorable {
case _showTeamsInProg = "showTeamsInProg" case _showTeamsInProg = "showTeamsInProg"
case _clubMemberFeeDeduction = "clubMemberFeeDeduction" case _clubMemberFeeDeduction = "clubMemberFeeDeduction"
case _unregisterDeltaInHours = "unregisterDeltaInHours" case _unregisterDeltaInHours = "unregisterDeltaInHours"
case _currencyCode = "currencyCode"
} }
private static func _decodePayment(container: KeyedDecodingContainer<CodingKeys>) throws -> TournamentPayment? { private static func _decodePayment(container: KeyedDecodingContainer<CodingKeys>) throws -> TournamentPayment? {
@ -451,6 +455,7 @@ public class BaseTournament: SyncedModelObject, SyncedStorable {
self.showTeamsInProg = try container.decodeIfPresent(Bool.self, forKey: ._showTeamsInProg) ?? false self.showTeamsInProg = try container.decodeIfPresent(Bool.self, forKey: ._showTeamsInProg) ?? false
self.clubMemberFeeDeduction = try container.decodeIfPresent(Double.self, forKey: ._clubMemberFeeDeduction) ?? nil self.clubMemberFeeDeduction = try container.decodeIfPresent(Double.self, forKey: ._clubMemberFeeDeduction) ?? nil
self.unregisterDeltaInHours = try container.decodeIfPresent(Int.self, forKey: ._unregisterDeltaInHours) ?? 24 self.unregisterDeltaInHours = try container.decodeIfPresent(Int.self, forKey: ._unregisterDeltaInHours) ?? 24
self.currencyCode = try container.decodeIfPresent(String.self, forKey: ._currencyCode) ?? nil
try super.init(from: decoder) try super.init(from: decoder)
} }
@ -527,6 +532,7 @@ public class BaseTournament: SyncedModelObject, SyncedStorable {
try container.encode(self.showTeamsInProg, forKey: ._showTeamsInProg) try container.encode(self.showTeamsInProg, forKey: ._showTeamsInProg)
try container.encode(self.clubMemberFeeDeduction, forKey: ._clubMemberFeeDeduction) try container.encode(self.clubMemberFeeDeduction, forKey: ._clubMemberFeeDeduction)
try container.encode(self.unregisterDeltaInHours, forKey: ._unregisterDeltaInHours) try container.encode(self.unregisterDeltaInHours, forKey: ._unregisterDeltaInHours)
try container.encode(self.currencyCode, forKey: ._currencyCode)
try super.encode(to: encoder) try super.encode(to: encoder)
} }
@ -608,6 +614,7 @@ public class BaseTournament: SyncedModelObject, SyncedStorable {
self.showTeamsInProg = tournament.showTeamsInProg self.showTeamsInProg = tournament.showTeamsInProg
self.clubMemberFeeDeduction = tournament.clubMemberFeeDeduction self.clubMemberFeeDeduction = tournament.clubMemberFeeDeduction
self.unregisterDeltaInHours = tournament.unregisterDeltaInHours self.unregisterDeltaInHours = tournament.unregisterDeltaInHours
self.currencyCode = tournament.currencyCode
} }
public static func parentRelationships() -> [Relationship] { public static func parentRelationships() -> [Relationship] {

@ -140,6 +140,21 @@
"name": "clubMember", "name": "clubMember",
"type": "Bool", "type": "Bool",
"defaultValue": "false" "defaultValue": "false"
},
{
"name": "contactName",
"type": "String",
"optional": true
},
{
"name": "contactPhoneNumber",
"type": "String",
"optional": true
},
{
"name": "contactEmail",
"type": "String",
"optional": true
} }
] ]
} }

@ -367,6 +367,11 @@
"name": "unregisterDeltaInHours", "name": "unregisterDeltaInHours",
"type": "Int", "type": "Int",
"defaultValue": "24" "defaultValue": "24"
},
{
"name": "currencyCode",
"type": "String",
"optional": true
} }
] ]
} }

@ -50,6 +50,15 @@ final public class Match: BaseMatch, SideStorable {
plannedStartDate = startDate plannedStartDate = startDate
} }
public func updateStartDate(_ date: Date?, keepPlannedStartDate: Bool) {
DataStore.shared.resetOngoingCache()
let cachedPlannedStartDate = self.plannedStartDate
self.startDate = date
if keepPlannedStartDate {
self.plannedStartDate = cachedPlannedStartDate
}
}
// MARK: - // MARK: -
public func setMatchName(_ serverName: String?) { public func setMatchName(_ serverName: String?) {
@ -210,7 +219,7 @@ defer {
} }
public func cleanScheduleAndSave(_ targetStartDate: Date? = nil) { public func cleanScheduleAndSave(_ targetStartDate: Date? = nil) {
startDate = targetStartDate ?? startDate updateStartDate(targetStartDate, keepPlannedStartDate: true)
confirmed = false confirmed = false
endDate = nil endDate = nil
followingMatch()?.cleanScheduleAndSave(nil) followingMatch()?.cleanScheduleAndSave(nil)
@ -555,7 +564,7 @@ defer {
updateFollowingMatchTeamScore() updateFollowingMatchTeamScore()
} }
public func setUnfinishedScore(fromMatchDescriptor matchDescriptor: MatchDescriptor) { public func setUnfinishedScore(fromMatchDescriptor matchDescriptor: MatchDescriptor, walkoutPosition: TeamPosition?) {
updateScore(fromMatchDescriptor: matchDescriptor) updateScore(fromMatchDescriptor: matchDescriptor)
if endDate == nil { if endDate == nil {
endDate = Date() endDate = Date()
@ -565,7 +574,41 @@ defer {
} else if let startDate, let endDate, startDate >= endDate { } else if let startDate, let endDate, startDate >= endDate {
self.startDate = endDate.addingTimeInterval(Double(-getDuration()*60)) self.startDate = endDate.addingTimeInterval(Double(-getDuration()*60))
} }
if let walkoutPosition {
let teamOne = team(walkoutPosition.otherTeam)
let teamTwo = team(walkoutPosition)
teamOne?.hasArrived()
teamTwo?.hasArrived()
teamOne?.resetRestingTime()
teamTwo?.resetRestingTime()
winningTeamId = teamOne?.id
losingTeamId = teamTwo?.id
let teamScoreWalkout = teamScore(walkoutPosition) ?? TeamScore(match: id, team: team(walkoutPosition))
teamScoreWalkout.walkOut = 0
let teamScoreWinning = teamScore(walkoutPosition.otherTeam) ?? TeamScore(match: id, team: team(walkoutPosition.otherTeam))
teamScoreWinning.walkOut = nil
self.tournamentStore?.teamScores.addOrUpdate(contentOfs: [teamScoreWalkout, teamScoreWinning])
}
confirmed = true confirmed = true
groupStageObject?.updateGroupStageState()
roundObject?.updateTournamentState()
if let tournament = currentTournament(), let endDate, let startDate {
if endDate.isEarlierThan(tournament.startDate) {
tournament.startDate = startDate
}
do {
try DataStore.shared.tournaments.addOrUpdate(instance: tournament)
} catch {
Logger.error(error)
}
}
updateFollowingMatchTeamScore()
} }
public func setScore(fromMatchDescriptor matchDescriptor: MatchDescriptor) { public func setScore(fromMatchDescriptor matchDescriptor: MatchDescriptor) {
@ -609,8 +652,10 @@ defer {
public func updateScore(fromMatchDescriptor matchDescriptor: MatchDescriptor) { public func updateScore(fromMatchDescriptor matchDescriptor: MatchDescriptor) {
let teamScoreOne = teamScore(.one) ?? TeamScore(match: id, team: team(.one)) let teamScoreOne = teamScore(.one) ?? TeamScore(match: id, team: team(.one))
teamScoreOne.walkOut = nil
teamScoreOne.score = matchDescriptor.teamOneScores.joined(separator: ",") teamScoreOne.score = matchDescriptor.teamOneScores.joined(separator: ",")
let teamScoreTwo = teamScore(.two) ?? TeamScore(match: id, team: team(.two)) let teamScoreTwo = teamScore(.two) ?? TeamScore(match: id, team: team(.two))
teamScoreTwo.walkOut = nil
teamScoreTwo.score = matchDescriptor.teamTwoScores.joined(separator: ",") teamScoreTwo.score = matchDescriptor.teamTwoScores.joined(separator: ",")
if matchDescriptor.teamOneScores.last?.contains("-") == true && matchDescriptor.teamTwoScores.last?.contains("-") == false { if matchDescriptor.teamOneScores.last?.contains("-") == true && matchDescriptor.teamTwoScores.last?.contains("-") == false {
@ -668,7 +713,7 @@ defer {
public func validateMatch(fromStartDate: Date, toEndDate: Date, fieldSetup: MatchFieldSetup, forced: Bool = false) { public func validateMatch(fromStartDate: Date, toEndDate: Date, fieldSetup: MatchFieldSetup, forced: Bool = false) {
if hasEnded() == false { if hasEnded() == false {
startDate = fromStartDate updateStartDate(fromStartDate, keepPlannedStartDate: true)
switch fieldSetup { switch fieldSetup {
case .fullRandom: case .fullRandom:
@ -685,7 +730,7 @@ defer {
} }
} else { } else {
startDate = fromStartDate updateStartDate(fromStartDate, keepPlannedStartDate: true)
endDate = toEndDate endDate = toEndDate
} }

@ -138,7 +138,6 @@ final public class MatchScheduler: BaseMatchScheduler, SideStorable {
} }
public func groupStageDispatcher(groupStages: [GroupStage], startingDate: Date) -> GroupStageMatchDispatcher { public func groupStageDispatcher(groupStages: [GroupStage], startingDate: Date) -> GroupStageMatchDispatcher {
let _groupStages = groupStages let _groupStages = groupStages
// Get the maximum count of matches in any group // Get the maximum count of matches in any group
@ -839,7 +838,7 @@ final public class MatchScheduler: BaseMatchScheduler, SideStorable {
} }
if tournament.dayDuration > 1 && (lastDate.timeOfDay == .evening || errorFormat) { if tournament.dayDuration > 1 && (lastDate.timeOfDay == .evening || lastDate.timeOfDay == .night || errorFormat) {
bracketStartDate = lastDate.tomorrowAtNine bracketStartDate = lastDate.tomorrowAtNine
} }

@ -35,9 +35,9 @@ public enum PlayerPaymentType: Int, CaseIterable, Identifiable, Codable {
case .cash: case .cash:
return "Cash" return "Cash"
case .lydia: case .lydia:
return "Lydia" return "SumUp"
case .paylib: case .paylib:
return "Paylib" return "Wero"
case .bankTransfer: case .bankTransfer:
return "Virement" return "Virement"
case .clubHouse: case .clubHouse:

@ -119,7 +119,9 @@ final public class PlayerRegistration: BasePlayerRegistration, SideStorable {
public func playerLabel(_ displayStyle: DisplayStyle = .wide) -> String { public func playerLabel(_ displayStyle: DisplayStyle = .wide) -> String {
switch displayStyle { switch displayStyle {
case .wide, .title: case .title:
return firstName.trimmed.capitalized + " " + lastName.trimmed.capitalized
case .wide:
return lastName.trimmed.capitalized + " " + firstName.trimmed.capitalized return lastName.trimmed.capitalized + " " + firstName.trimmed.capitalized
case .short: case .short:
let names = lastName.components(separatedBy: .whitespaces) let names = lastName.components(separatedBy: .whitespaces)
@ -170,7 +172,7 @@ final public class PlayerRegistration: BasePlayerRegistration, SideStorable {
let currentRank = rank ?? maleUnranked let currentRank = rank ?? maleUnranked
switch tournament.tournamentCategory { switch tournament.tournamentCategory {
case .men: case .men:
let addon = PlayerRegistration.addon(for: currentRank, manMax: maleUnranked, womanMax: femaleUnranked) let addon = tournament.addon(for: currentRank, manMax: maleUnranked, womanMax: femaleUnranked)
computedRank = isMalePlayer() ? currentRank : currentRank + addon computedRank = isMalePlayer() ? currentRank : currentRank + addon
default: default:
computedRank = currentRank computedRank = currentRank
@ -279,16 +281,6 @@ final public class PlayerRegistration: BasePlayerRegistration, SideStorable {
} }
} }
public static func addon(for playerRank: Int, manMax: Int, womanMax: Int) -> Int {
switch playerRank {
case 0: return 0
case womanMax: return manMax - womanMax
case manMax: return 0
default:
return TournamentCategory.femaleInMaleAssimilationAddition(playerRank)
}
}
func insertOnServer() { func insertOnServer() {
self.tournamentStore?.playerRegistrations.writeChangeAndInsertOnServer(instance: self) self.tournamentStore?.playerRegistrations.writeChangeAndInsertOnServer(instance: self)
} }

@ -183,11 +183,11 @@ final public class TeamRegistration: BaseTeamRegistration, SideStorable {
} }
public func getPhoneNumbers() -> [String] { public func getPhoneNumbers() -> [String] {
return players().compactMap { $0.phoneNumber }.filter({ $0.isEmpty == false }) return players().flatMap { [$0.phoneNumber, $0.contactPhoneNumber].compacted() }.filter({ $0.isEmpty == false })
} }
public func getMail() -> [String] { public func getMail() -> [String] {
let mails = players().compactMap({ $0.email }) let mails = players().flatMap({ [$0.email, $0.contactEmail].compacted() })
return mails return mails
} }
@ -255,10 +255,10 @@ final public class TeamRegistration: BaseTeamRegistration, SideStorable {
separator: twoLines ? "\n" : " \(separator) ") separator: twoLines ? "\n" : " \(separator) ")
} }
public func teamLabelRanked(displayRank: Bool, displayTeamName: Bool) -> String { public func teamLabelRanked(displayStyle: DisplayStyle = .wide, displayRank: Bool, displayTeamName: Bool) -> String {
[ [
displayTeamName ? name : nil, displayRank ? seedIndex() : nil, displayTeamName ? name : nil, displayRank ? seedIndex() : nil,
displayTeamName ? (name == nil ? teamLabel() : name) : teamLabel(), displayTeamName ? (name == nil ? teamLabel(displayStyle) : name) : teamLabel(displayStyle),
].compactMap({ $0 }).joined(separator: " ") ].compactMap({ $0 }).joined(separator: " ")
} }

@ -1236,7 +1236,7 @@ defer {
public var entryFeeMessage: String { public var entryFeeMessage: String {
if let entryFee { if let entryFee {
let message: String = "Inscription : \(entryFee.formatted(.currency(code: Locale.defaultCurrency()))) par joueur." let message: String = "Inscription : \(entryFee.formatted(.currency(code: self.defaultCurrency()))) par joueur."
return [message, self._paymentMethodMessage()].compactMap { $0 }.joined(separator: "\n") return [message, self._paymentMethodMessage()].compactMap { $0 }.joined(separator: "\n")
} else { } else {
return "Inscription : gratuite." return "Inscription : gratuite."
@ -2155,6 +2155,45 @@ defer {
return .open return .open
} }
public func getPaymentStatus() -> PaymentStatus {
if enableOnlinePayment == false {
return .notEnabled
}
// 1. Check if Stripe account is configured. This is the most fundamental requirement.
if (stripeAccountId == nil || stripeAccountId!.isEmpty) && isCorporateTournament == false {
return .notConfigured
}
// 2. Check if online payment is mandatory. This determines the main branch of statuses.
if onlinePaymentIsMandatory {
// Payment is mandatory. Now check refund options.
if enableOnlinePaymentRefund {
let now = Date()
if let refundDate = refundDateLimit {
// Refund is enabled and a date limit is set. Check if the date has passed.
if now < refundDate {
return .mandatoryRefundEnabled // Mandatory, refund is currently possible
} else {
return .mandatoryRefundEnded // Mandatory, refund period has ended
}
} else {
// Refund is enabled but no specific date limit is set. Assume it's currently enabled.
return .mandatoryRefundEnabled
}
} else {
// Payment is mandatory, but refunds are explicitly not enabled.
return .mandatoryNoRefund
}
} else {
// Payment is not mandatory, meaning it's optional.
// Features like `clubMemberFeeDeduction` or `enableTimeToConfirm`
// would apply to this optional payment, but don't change its
// overall 'optionalPayment' status for this summary function.
return .optionalPayment
}
}
// MARK: - Status // MARK: - Status
public func shouldTournamentBeOver() async -> Bool { public func shouldTournamentBeOver() async -> Bool {
if hasEnded() { if hasEnded() {
@ -2332,6 +2371,54 @@ defer {
groupStages().sorted(by: \.computedStartDateForSorting).first?.startDate groupStages().sorted(by: \.computedStartDateForSorting).first?.startDate
} }
public func defaultCurrency() -> String {
if let currencyCode = self.currencyCode {
return currencyCode
} else {
return Locale.defaultCurrency()
}
}
public func addon(for playerRank: Int, manMax: Int, womanMax: Int) -> Int {
switch playerRank {
case 0: return 0
case womanMax: return manMax - womanMax
case manMax: return 0
default:
return TournamentCategory.femaleInMaleAssimilationAddition(playerRank, seasonYear: self.startDate.seasonYear())
}
}
public func coachingIsAuthorized() -> Bool {
switch startDate.seasonYear() {
case 2026:
return true
default:
return tournamentLevel.coachingIsAuthorized
}
}
public func minimumNumberOfTeams() -> Int {
return federalTournamentAge.minimumNumberOfTeams(inCategory: tournamentCategory, andInLevel: tournamentLevel)
}
public func removeAllDates() {
let allMatches = allMatches()
allMatches.forEach({
$0.startDate = nil
$0.confirmed = false
})
self.tournamentStore?.matches.addOrUpdate(contentOfs: allMatches)
let allGroupStages = groupStages()
allGroupStages.forEach({ $0.startDate = nil })
self.tournamentStore?.groupStages.addOrUpdate(contentOfs: allGroupStages)
let allRounds = allRounds()
allRounds.forEach({ $0.startDate = nil })
self.tournamentStore?.rounds.addOrUpdate(contentOfs: allRounds)
}
// MARK: - // MARK: -
func insertOnServer() throws { func insertOnServer() throws {

@ -37,6 +37,14 @@ public enum TimeOfDay {
public extension Date { public extension Date {
func seasonYear() -> Int {
if self.monthInt >= 9 {
return self.yearInt + 1
}
return self.yearInt
}
func withoutSeconds() -> Date { func withoutSeconds() -> Date {
let calendar = Calendar.current let calendar = Calendar.current
return calendar.date(bySettingHour: calendar.component(.hour, from: self), return calendar.date(bySettingHour: calendar.component(.hour, from: self),
@ -206,6 +214,10 @@ public extension Date {
Calendar.current.component(.day, from: self) Calendar.current.component(.day, from: self)
} }
var hourInt: Int {
Calendar.current.component(.hour, from: self)
}
var startOfDay: Date { var startOfDay: Date {
Calendar.current.startOfDay(for: self) Calendar.current.startOfDay(for: self)
} }
@ -265,3 +277,15 @@ public extension Date {
return calendar.date(bySetting: .minute, value: 0, of: self)!.withoutSeconds() return calendar.date(bySetting: .minute, value: 0, of: self)!.withoutSeconds()
} }
} }
public extension Date {
var startOfCurrentMonth: Date {
let calendar = Calendar.current
return calendar.dateInterval(of: .month, for: self)?.start ?? self
}
func addingMonths(_ months: Int) -> Date {
let calendar = Calendar.current
return calendar.date(byAdding: .month, value: months, to: self) ?? self
}
}

@ -143,6 +143,24 @@ public extension URL {
return nil return nil
} }
func fftImportingAnonymous() -> Int? {
// Read the contents of the file
guard let fileContents = try? String(contentsOfFile: path(), encoding: .utf8) else {
return nil
}
// Split the contents by newline characters
let lines = fileContents.components(separatedBy: .newlines)
if let line = lines.first(where: {
$0.hasPrefix("anonymous-players:")
}) {
return Int(line.replacingOccurrences(of: "anonymous-players:", with: ""))
}
return nil
}
func getUnrankedValue() -> Int? { func getUnrankedValue() -> Int? {
// Read the contents of the file // Read the contents of the file
guard let fileContents = try? String(contentsOfFile: path(), encoding: .utf8) else { guard let fileContents = try? String(contentsOfFile: path(), encoding: .utf8) else {

@ -16,13 +16,43 @@ public enum NetworkManagerError: LocalizedError {
case messageNotSent //no network no error case messageNotSent //no network no error
case fileNotModified case fileNotModified
case fileNotDownloaded(Int) case fileNotDownloaded(Int)
case noDataReceived // New: If data is empty or nil
case htmlDecodingFailed // New: If String(data: data, encoding: .utf8) fails
case formBuildIdPrefixNotFound // New: If the prefix regex doesn't match
case formBuildIdSuffixNotFound // New: If the suffix regex doesn't match
case formBuildIdExtractionFailed // New: General parsing failure if other specific errors don't cover it
case apiError(String) // ADDED: General API error with a descriptive message
public var errorDescription: String? { public var errorDescription: String? {
switch self { switch self {
case .maintenance: case .maintenance:
return "Le site de la FFT est en maintenance" return "Le site de la FFT est en maintenance."
default: case .fileNotYetAvailable:
return String(describing: self) return "Le fichier n'est pas encore disponible."
case .mailFailed:
return "L'envoi de l'e-mail a échoué."
case .mailNotSent:
return "L'e-mail n'a pas été envoyé (pas de réseau ou autre)."
case .messageFailed:
return "L'envoi du message a échoué."
case .messageNotSent:
return "Le message n'a pas été envoyé (pas de réseau ou autre)."
case .fileNotModified:
return "Le fichier n'a pas été modifié."
case .fileNotDownloaded(let statusCode):
return "Le fichier n'a pas pu être téléchargé. Code d'état : \(statusCode)."
case .noDataReceived:
return "Aucune donnée n'a été reçue du serveur."
case .htmlDecodingFailed:
return "Échec du décodage de la réponse HTML."
case .formBuildIdPrefixNotFound:
return "Impossible de trouver le début de l'ID du formulaire (form_build_id) dans la page."
case .formBuildIdSuffixNotFound:
return "Impossible de trouver la fin de l'ID du formulaire (form_build_id) dans la page."
case .formBuildIdExtractionFailed:
return "Échec général de l'extraction de l'ID du formulaire (form_build_id)."
case .apiError(let message):
return "Erreur API: \(message)"
} }
} }
} }

@ -166,6 +166,7 @@ public enum TournamentDifficulty {
public enum FederalTournamentAge: Int, Hashable, Codable, CaseIterable, Identifiable { public enum FederalTournamentAge: Int, Hashable, Codable, CaseIterable, Identifiable {
case unlisted = 0 case unlisted = 0
case a09_10 = 100
case a11_12 = 120 case a11_12 = 120
case a13_14 = 140 case a13_14 = 140
case a15_16 = 160 case a15_16 = 160
@ -184,6 +185,8 @@ public enum FederalTournamentAge: Int, Hashable, Codable, CaseIterable, Identifi
switch self { switch self {
case .unlisted: case .unlisted:
return (nil, nil) return (nil, nil)
case .a09_10:
return (year - 10, year - 9)
case .a11_12: case .a11_12:
return (year - 12, year - 11) return (year - 12, year - 11)
case .a13_14: case .a13_14:
@ -205,6 +208,8 @@ public enum FederalTournamentAge: Int, Hashable, Codable, CaseIterable, Identifi
switch self { switch self {
case .unlisted: case .unlisted:
return "Animation" return "Animation"
case .a09_10:
return "09/10 ans"
case .a11_12: case .a11_12:
return "11/12 ans" return "11/12 ans"
case .a13_14: case .a13_14:
@ -241,6 +246,8 @@ public enum FederalTournamentAge: Int, Hashable, Codable, CaseIterable, Identifi
public var order: Int { public var order: Int {
switch self { switch self {
case .unlisted: case .unlisted:
return 8
case .a09_10:
return 7 return 7
case .a11_12: case .a11_12:
return 6 return 6
@ -263,6 +270,8 @@ public enum FederalTournamentAge: Int, Hashable, Codable, CaseIterable, Identifi
switch self { switch self {
case .unlisted: case .unlisted:
return displayStyle == .title ? "Aucune" : "" return displayStyle == .title ? "Aucune" : ""
case .a09_10:
return "U10"
case .a11_12: case .a11_12:
return "U12" return "U12"
case .a13_14: case .a13_14:
@ -289,14 +298,16 @@ public enum FederalTournamentAge: Int, Hashable, Codable, CaseIterable, Identifi
switch self { switch self {
case .unlisted: case .unlisted:
return true return true
case .a09_10:
return age < 11
case .a11_12: case .a11_12:
return age < 13 return age < 13
case .a13_14: case .a13_14:
return age < 15 return age < 15
case .a15_16: case .a15_16:
return age < 17 return age < 17 && age >= 11
case .a17_18: case .a17_18:
return age < 19 return age < 19 && age >= 11
case .senior: case .senior:
return age >= 11 return age >= 11
case .a45: case .a45:
@ -310,6 +321,8 @@ public enum FederalTournamentAge: Int, Hashable, Codable, CaseIterable, Identifi
switch self { switch self {
case .unlisted: case .unlisted:
return false return false
case .a09_10:
return true
case .a11_12: case .a11_12:
return true return true
case .a13_14: case .a13_14:
@ -326,6 +339,42 @@ public enum FederalTournamentAge: Int, Hashable, Codable, CaseIterable, Identifi
return false return false
} }
} }
func minimumNumberOfTeams(inCategory category: TournamentCategory, andInLevel level: TournamentLevel) -> Int {
if self.isChildCategory() {
if level == .p250, category == .men {
return 8
}
return 4
} else {
switch level {
case .p25, .p100, .p250:
if category == .women {
return 4
}
if level == .p100 {
return 8
}
return 12
case .p500:
if category == .women {
return 8
}
return 16
case .p1000:
if category == .women {
return 16
}
return 24
case .p1500, .p2000:
return 24
default:
return -1
}
}
}
} }
public enum TournamentLevel: Int, Hashable, Codable, CaseIterable, Identifiable { public enum TournamentLevel: Int, Hashable, Codable, CaseIterable, Identifiable {
@ -432,7 +481,12 @@ public enum TournamentLevel: Int, Hashable, Codable, CaseIterable, Identifiable
} }
} }
public func minimumPlayerRank(category: TournamentCategory, ageCategory: FederalTournamentAge) -> Int { public func minimumPlayerRank(category: TournamentCategory, ageCategory: FederalTournamentAge, seasonYear: Int) -> Int {
if seasonYear == 2026, category == .mix {
return 0
}
switch self { switch self {
case .p25: case .p25:
switch ageCategory { switch ageCategory {
@ -876,7 +930,10 @@ public enum TournamentCategory: Int, Hashable, Codable, CaseIterable, Identifiab
} }
} }
public static func femaleInMaleAssimilationAddition(_ rank: Int) -> Int { public static func femaleInMaleAssimilationAddition(_ rank: Int, seasonYear: Int?) -> Int {
switch seasonYear {
case .some(let year):
if year < 2026 {
switch rank { switch rank {
case 1...10: return 400 case 1...10: return 400
case 11...30: return 1000 case 11...30: return 1000
@ -891,6 +948,26 @@ public enum TournamentCategory: Int, Hashable, Codable, CaseIterable, Identifiab
return 50000 return 50000
} }
} }
case .none:
break
}
switch rank {
case 1...10: return 400
case 11...30: return 1000
case 31...60: return 2000
case 61...100: return 3500
case 101...200: return 10000
case 201...500: return 15000
case 501...1000: return 25000
case 1001...2000: return 35000
case 2001...3000: return 45000
case 3001...5000: return 55000
case 5001...10000: return 70000
default:
return 90000
}
}
public static func mostRecent(inTournaments tournaments: [Tournament]) -> Self { public static func mostRecent(inTournaments tournaments: [Tournament]) -> Self {
return tournaments.first?.tournamentCategory ?? .men return tournaments.first?.tournamentCategory ?? .men
@ -1499,7 +1576,7 @@ public enum MatchFormat: Int, Hashable, Codable, CaseIterable, Identifiable {
public var isFederal: Bool { public var isFederal: Bool {
switch self { switch self {
case .megaTie, .twoSetsOfSuperTie, .singleSet, .singleSetDecisivePoint, .singleSetOfFourGames, .singleSetOfFourGamesDecisivePoint: case .megaTie, .twoSetsOfSuperTie, .singleSet, .singleSetDecisivePoint, .singleSetOfFourGamesDecisivePoint:
return false return false
default: default:
return true return true
@ -1525,11 +1602,11 @@ public enum MatchFormat: Int, Hashable, Codable, CaseIterable, Identifiable {
case .twoSetsOfSuperTie: case .twoSetsOfSuperTie:
return "G" return "G"
case .megaTie: case .megaTie:
return "F" return "H"
case .singleSet: case .singleSet:
return "H1" return "I1"
case .singleSetDecisivePoint: case .singleSetDecisivePoint:
return "H2" return "I2"
case .twoSetsDecisivePoint: case .twoSetsDecisivePoint:
return "A2" return "A2"
case .twoSetsDecisivePointSuperTie: case .twoSetsDecisivePointSuperTie:
@ -1539,9 +1616,9 @@ public enum MatchFormat: Int, Hashable, Codable, CaseIterable, Identifiable {
case .nineGamesDecisivePoint: case .nineGamesDecisivePoint:
return "D2" return "D2"
case .singleSetOfFourGames: case .singleSetOfFourGames:
return "I1" return "F1"
case .singleSetOfFourGamesDecisivePoint: case .singleSetOfFourGamesDecisivePoint:
return "I2" return "F2"
} }
} }

Loading…
Cancel
Save