Merge branch 'main'

sync3
Razmig Sarkissian 3 months ago
commit 5ea1518e2e
  1. 30
      PadelClubData/Business.swift
  2. 26
      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. 38
      PadelClubData/Utils/NetworkManagerError.swift
  16. 99
      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 {
case all = -1
case male = 1

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

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

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

@ -140,6 +140,21 @@
"name": "clubMember",
"type": "Bool",
"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",
"type": "Int",
"defaultValue": "24"
},
{
"name": "currencyCode",
"type": "String",
"optional": true
}
]
}

@ -50,6 +50,15 @@ final public class Match: BaseMatch, SideStorable {
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: -
public func setMatchName(_ serverName: String?) {
@ -210,7 +219,7 @@ defer {
}
public func cleanScheduleAndSave(_ targetStartDate: Date? = nil) {
startDate = targetStartDate ?? startDate
updateStartDate(targetStartDate, keepPlannedStartDate: true)
confirmed = false
endDate = nil
followingMatch()?.cleanScheduleAndSave(nil)
@ -555,7 +564,7 @@ defer {
updateFollowingMatchTeamScore()
}
public func setUnfinishedScore(fromMatchDescriptor matchDescriptor: MatchDescriptor) {
public func setUnfinishedScore(fromMatchDescriptor matchDescriptor: MatchDescriptor, walkoutPosition: TeamPosition?) {
updateScore(fromMatchDescriptor: matchDescriptor)
if endDate == nil {
endDate = Date()
@ -565,7 +574,41 @@ defer {
} else if let startDate, let endDate, startDate >= endDate {
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
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) {
@ -609,8 +652,10 @@ defer {
public func updateScore(fromMatchDescriptor matchDescriptor: MatchDescriptor) {
let teamScoreOne = teamScore(.one) ?? TeamScore(match: id, team: team(.one))
teamScoreOne.walkOut = nil
teamScoreOne.score = matchDescriptor.teamOneScores.joined(separator: ",")
let teamScoreTwo = teamScore(.two) ?? TeamScore(match: id, team: team(.two))
teamScoreTwo.walkOut = nil
teamScoreTwo.score = matchDescriptor.teamTwoScores.joined(separator: ",")
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) {
if hasEnded() == false {
startDate = fromStartDate
updateStartDate(fromStartDate, keepPlannedStartDate: true)
switch fieldSetup {
case .fullRandom:
@ -685,7 +730,7 @@ defer {
}
} else {
startDate = fromStartDate
updateStartDate(fromStartDate, keepPlannedStartDate: true)
endDate = toEndDate
}

@ -138,7 +138,6 @@ final public class MatchScheduler: BaseMatchScheduler, SideStorable {
}
public func groupStageDispatcher(groupStages: [GroupStage], startingDate: Date) -> GroupStageMatchDispatcher {
let _groupStages = groupStages
// 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
}

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

@ -119,7 +119,9 @@ final public class PlayerRegistration: BasePlayerRegistration, SideStorable {
public func playerLabel(_ displayStyle: DisplayStyle = .wide) -> String {
switch displayStyle {
case .wide, .title:
case .title:
return firstName.trimmed.capitalized + " " + lastName.trimmed.capitalized
case .wide:
return lastName.trimmed.capitalized + " " + firstName.trimmed.capitalized
case .short:
let names = lastName.components(separatedBy: .whitespaces)
@ -170,7 +172,7 @@ final public class PlayerRegistration: BasePlayerRegistration, SideStorable {
let currentRank = rank ?? maleUnranked
switch tournament.tournamentCategory {
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
default:
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() {
self.tournamentStore?.playerRegistrations.writeChangeAndInsertOnServer(instance: self)
}

@ -183,11 +183,11 @@ final public class TeamRegistration: BaseTeamRegistration, SideStorable {
}
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] {
let mails = players().compactMap({ $0.email })
let mails = players().flatMap({ [$0.email, $0.contactEmail].compacted() })
return mails
}
@ -255,10 +255,10 @@ final public class TeamRegistration: BaseTeamRegistration, SideStorable {
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 ? teamLabel() : name) : teamLabel(),
displayTeamName ? (name == nil ? teamLabel(displayStyle) : name) : teamLabel(displayStyle),
].compactMap({ $0 }).joined(separator: " ")
}

@ -1236,7 +1236,7 @@ defer {
public var entryFeeMessage: String {
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")
} else {
return "Inscription : gratuite."
@ -2154,6 +2154,45 @@ defer {
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
public func shouldTournamentBeOver() async -> Bool {
@ -2332,6 +2371,54 @@ defer {
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: -
func insertOnServer() throws {

@ -37,6 +37,14 @@ public enum TimeOfDay {
public extension Date {
func seasonYear() -> Int {
if self.monthInt >= 9 {
return self.yearInt + 1
}
return self.yearInt
}
func withoutSeconds() -> Date {
let calendar = Calendar.current
return calendar.date(bySettingHour: calendar.component(.hour, from: self),
@ -206,6 +214,10 @@ public extension Date {
Calendar.current.component(.day, from: self)
}
var hourInt: Int {
Calendar.current.component(.hour, from: self)
}
var startOfDay: Date {
Calendar.current.startOfDay(for: self)
}
@ -265,3 +277,15 @@ public extension Date {
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
}
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? {
// Read the contents of the file
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 fileNotModified
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? {
switch self {
case .maintenance:
return "Le site de la FFT est en maintenance"
default:
return String(describing: self)
return "Le site de la FFT est en maintenance."
case .fileNotYetAvailable:
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 {
case unlisted = 0
case a09_10 = 100
case a11_12 = 120
case a13_14 = 140
case a15_16 = 160
@ -184,6 +185,8 @@ public enum FederalTournamentAge: Int, Hashable, Codable, CaseIterable, Identifi
switch self {
case .unlisted:
return (nil, nil)
case .a09_10:
return (year - 10, year - 9)
case .a11_12:
return (year - 12, year - 11)
case .a13_14:
@ -205,6 +208,8 @@ public enum FederalTournamentAge: Int, Hashable, Codable, CaseIterable, Identifi
switch self {
case .unlisted:
return "Animation"
case .a09_10:
return "09/10 ans"
case .a11_12:
return "11/12 ans"
case .a13_14:
@ -241,6 +246,8 @@ public enum FederalTournamentAge: Int, Hashable, Codable, CaseIterable, Identifi
public var order: Int {
switch self {
case .unlisted:
return 8
case .a09_10:
return 7
case .a11_12:
return 6
@ -263,6 +270,8 @@ public enum FederalTournamentAge: Int, Hashable, Codable, CaseIterable, Identifi
switch self {
case .unlisted:
return displayStyle == .title ? "Aucune" : ""
case .a09_10:
return "U10"
case .a11_12:
return "U12"
case .a13_14:
@ -289,14 +298,16 @@ public enum FederalTournamentAge: Int, Hashable, Codable, CaseIterable, Identifi
switch self {
case .unlisted:
return true
case .a09_10:
return age < 11
case .a11_12:
return age < 13
case .a13_14:
return age < 15
case .a15_16:
return age < 17
return age < 17 && age >= 11
case .a17_18:
return age < 19
return age < 19 && age >= 11
case .senior:
return age >= 11
case .a45:
@ -310,6 +321,8 @@ public enum FederalTournamentAge: Int, Hashable, Codable, CaseIterable, Identifi
switch self {
case .unlisted:
return false
case .a09_10:
return true
case .a11_12:
return true
case .a13_14:
@ -326,6 +339,42 @@ public enum FederalTournamentAge: Int, Hashable, Codable, CaseIterable, Identifi
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 {
@ -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 {
case .p25:
switch ageCategory {
@ -876,7 +930,28 @@ 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 {
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
default:
return 50000
}
}
case .none:
break
}
switch rank {
case 1...10: return 400
case 11...30: return 1000
@ -887,8 +962,10 @@ public enum TournamentCategory: Int, Hashable, Codable, CaseIterable, Identifiab
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 50000
return 90000
}
}
@ -1499,7 +1576,7 @@ public enum MatchFormat: Int, Hashable, Codable, CaseIterable, Identifiable {
public var isFederal: Bool {
switch self {
case .megaTie, .twoSetsOfSuperTie, .singleSet, .singleSetDecisivePoint, .singleSetOfFourGames, .singleSetOfFourGamesDecisivePoint:
case .megaTie, .twoSetsOfSuperTie, .singleSet, .singleSetDecisivePoint, .singleSetOfFourGamesDecisivePoint:
return false
default:
return true
@ -1525,11 +1602,11 @@ public enum MatchFormat: Int, Hashable, Codable, CaseIterable, Identifiable {
case .twoSetsOfSuperTie:
return "G"
case .megaTie:
return "F"
return "H"
case .singleSet:
return "H1"
return "I1"
case .singleSetDecisivePoint:
return "H2"
return "I2"
case .twoSetsDecisivePoint:
return "A2"
case .twoSetsDecisivePointSuperTie:
@ -1539,9 +1616,9 @@ public enum MatchFormat: Int, Hashable, Codable, CaseIterable, Identifiable {
case .nineGamesDecisivePoint:
return "D2"
case .singleSetOfFourGames:
return "I1"
return "F1"
case .singleSetOfFourGamesDecisivePoint:
return "I2"
return "F2"
}
}

Loading…
Cancel
Save