Merge branch 'main' into sync3

sync3
Laurent 1 month ago
commit ac9adad50f
  1. 2
      .gitignore
  2. 16
      PadelClubData/Data/GroupStage.swift
  3. 7
      PadelClubData/Data/MatchScheduler.swift
  4. 45
      PadelClubData/Data/Tournament.swift
  5. 35
      PadelClubData/Extensions/Date+Extensions.swift
  6. 60
      PadelClubData/Subscriptions/Guard.swift
  7. 14
      PadelClubData/Subscriptions/StoreItem.swift
  8. 10
      PadelClubData/Subscriptions/StoreManager.swift
  9. 2
      PadelClubData/ViewModel/PadelRule.swift

2
.gitignore vendored

@ -3,6 +3,8 @@
# #
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
PadelClubDataTests/config.plist
## User settings ## User settings
xcuserdata/ xcuserdata/
.DS_Store .DS_Store

@ -298,7 +298,7 @@ final public class GroupStage: BaseGroupStage, SideStorable {
return playedMatches.filter({ $0.isRunning() }).sorted(by: \.computedStartDateForSorting) return playedMatches.filter({ $0.isRunning() }).sorted(by: \.computedStartDateForSorting)
} }
public func readyMatches(playedMatches: [Match]) -> [Match] { public func readyMatches(playedMatches: [Match], runningMatches: [Match]) -> [Match] {
#if _DEBUG_TIME //DEBUGING TIME #if _DEBUG_TIME //DEBUGING TIME
let start = Date() let start = Date()
defer { defer {
@ -306,7 +306,9 @@ final public class GroupStage: BaseGroupStage, SideStorable {
print("func group stage readyMatches", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) print("func group stage readyMatches", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
} }
#endif #endif
return playedMatches.filter({ $0.isReady() && $0.isRunning() == false && $0.hasEnded() == false }) let playingTeams = runningMatches.flatMap({ $0.teams() }).map({ $0.id })
return playedMatches.filter({ $0.isReady() && $0.isRunning() == false && $0.hasEnded() == false && $0.containsTeamIds(playingTeams) == false })
} }
public func finishedMatches(playedMatches: [Match]) -> [Match] { public func finishedMatches(playedMatches: [Match]) -> [Match] {
@ -617,6 +619,16 @@ final public class GroupStage: BaseGroupStage, SideStorable {
return self._matches().sorted(by: \.computedStartDateForSorting).first?.startDate return self._matches().sorted(by: \.computedStartDateForSorting).first?.startDate
} }
public func removeAllTeams() {
let teams = teams()
teams.forEach { team in
team.groupStagePosition = nil
team.groupStage = nil
self._matches().forEach({ $0.updateTeamScores() })
}
tournamentStore?.teamRegistrations.addOrUpdate(contentOfs: teams)
}
public override func deleteDependencies(store: Store, actionOption: ActionOption) { public override func deleteDependencies(store: Store, actionOption: ActionOption) {
store.deleteDependencies(type: Match.self, actionOption: actionOption) { $0.groupStage == self.id } store.deleteDependencies(type: Match.self, actionOption: actionOption) { $0.groupStage == self.id }
} }

@ -839,8 +839,10 @@ final public class MatchScheduler: BaseMatchScheduler, SideStorable {
if tournament.dayDuration > 1 && (lastDate.timeOfDay == .evening || lastDate.timeOfDay == .night || errorFormat) { if tournament.dayDuration > 1 && (lastDate.timeOfDay == .evening || lastDate.timeOfDay == .night || errorFormat) {
if tournament.groupStageCount > 0 {
bracketStartDate = lastDate.tomorrowAtNine bracketStartDate = lastDate.tomorrowAtNine
} }
}
return updateBracketSchedule(tournament: tournament, fromRoundId: nil, fromMatchId: nil, startDate: bracketStartDate) return updateBracketSchedule(tournament: tournament, fromRoundId: nil, fromMatchId: nil, startDate: bracketStartDate)
} }
@ -894,6 +896,11 @@ extension Match {
return teamIds().contains(id) return teamIds().contains(id)
} }
public func containsTeamIds(_ ids: [String]) -> Bool {
let teamIds = teamIds()
return !Set(ids).isDisjoint(with: teamIds)
}
public func containsTeamIndex(_ id: String) -> Bool { public func containsTeamIndex(_ id: String) -> Bool {
matchUp().contains(id) matchUp().contains(id)
} }

@ -104,6 +104,25 @@ final public class Tournament: BaseTournament {
return self.tournamentStore?.teamRegistrations.count ?? 0 return self.tournamentStore?.teamRegistrations.count ?? 0
} }
public func deleteGroupStage(_ groupStage: GroupStage) {
groupStage.removeAllTeams()
let index = groupStage.index
self.tournamentStore?.groupStages.delete(instance: groupStage)
self.groupStageCount -= 1
let groupStages = self.groupStages()
groupStages.filter({ $0.index > index }).forEach { gs in
gs.index -= 1
}
self.tournamentStore?.groupStages.addOrUpdate(contentOfs: groupStages)
}
public func addGroupStage() {
let groupStage = GroupStage(tournament: id, index: groupStageCount, size: teamsPerGroupStage, format: groupStageFormat)
self.tournamentStore?.groupStages.addOrUpdate(instance: groupStage)
groupStage.buildMatches(keepExistingMatches: false)
self.groupStageCount += 1
}
public func groupStages(atStep step: Int = 0) -> [GroupStage] { public func groupStages(atStep step: Int = 0) -> [GroupStage] {
guard let tournamentStore = self.tournamentStore else { return [] } guard let tournamentStore = self.tournamentStore else { return [] }
let groupStages: [GroupStage] = tournamentStore.groupStages.filter { $0.tournament == self.id && $0.step == step } let groupStages: [GroupStage] = tournamentStore.groupStages.filter { $0.tournament == self.id && $0.step == step }
@ -872,7 +891,7 @@ defer {
return allMatches.filter({ $0.isRunning() && $0.isReady() }).sorted(using: defaultSorting, order: .ascending) return allMatches.filter({ $0.isRunning() && $0.isReady() }).sorted(using: defaultSorting, order: .ascending)
} }
public static func readyMatches(_ allMatches: [Match]) -> [Match] { public static func readyMatches(_ allMatches: [Match], runningMatches: [Match]) -> [Match] {
#if _DEBUG_TIME //DEBUGING TIME #if _DEBUG_TIME //DEBUGING TIME
let start = Date() let start = Date()
defer { defer {
@ -880,7 +899,10 @@ defer {
print("func tournament readyMatches", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) print("func tournament readyMatches", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
} }
#endif #endif
return allMatches.filter({ $0.isReady() && $0.isRunning() == false && $0.hasEnded() == false }).sorted(using: defaultSorting, order: .ascending)
let playingTeams = runningMatches.flatMap({ $0.teams() }).map({ $0.id })
return allMatches.filter({ $0.isReady() && $0.isRunning() == false && $0.hasEnded() == false && $0.containsTeamIds(playingTeams) == false }).sorted(using: defaultSorting, order: .ascending)
} }
public static func matchesLeft(_ allMatches: [Match]) -> [Match] { public static func matchesLeft(_ allMatches: [Match]) -> [Match] {
@ -1698,12 +1720,14 @@ defer {
} }
public func initSettings(templateTournament: Tournament?, overrideTeamCount: Bool = true) { public func initSettings(templateTournament: Tournament?, overrideTeamCount: Bool = true) {
courtCount = eventObject()?.clubObject()?.courtCount ?? 2
setupDefaultPrivateSettings(templateTournament: templateTournament) setupDefaultPrivateSettings(templateTournament: templateTournament)
setupUmpireSettings(defaultTournament: nil) //default is not template, default is for event sharing settings setupUmpireSettings(defaultTournament: nil) //default is not template, default is for event sharing settings
if let templateTournament { if let templateTournament {
setupRegistrationSettings(templateTournament: templateTournament, overrideTeamCount: overrideTeamCount) setupRegistrationSettings(templateTournament: templateTournament, overrideTeamCount: overrideTeamCount)
} }
setupFederalSettings() setupFederalSettings()
customizeUsingPreferences()
} }
public func setupFederalSettings() { public func setupFederalSettings() {
@ -1719,6 +1743,23 @@ defer {
} }
} }
public func customizeUsingPreferences() {
guard let lastTournamentWithSameBuild = DataStore.shared.tournaments.filter({ tournament in
tournament.tournamentLevel == self.tournamentLevel
&& tournament.tournamentCategory == self.tournamentCategory
&& tournament.federalTournamentAge == self.federalTournamentAge
&& tournament.hasEnded() == true
&& tournament.isCanceled == false
&& tournament.isDeleted == false
}).sorted(by: \.endDate!, order: .descending).first else {
return
}
self.entryFee = lastTournamentWithSameBuild.entryFee
self.clubMemberFeeDeduction = lastTournamentWithSameBuild.clubMemberFeeDeduction
}
public func deadline(for type: TournamentDeadlineType) -> Date? { public func deadline(for type: TournamentDeadlineType) -> Date? {
guard [.p500, .p1000, .p1500, .p2000].contains(tournamentLevel) else { return nil } guard [.p500, .p1000, .p1500, .p2000].contains(tournamentLevel) else { return nil }

@ -118,6 +118,14 @@ public extension Date {
} }
} }
var nextDay: Date {
return Calendar.current.date(byAdding: .day, value: 1, to: self)!
}
var weekDay: Int {
Calendar.current.component(.weekday, from: self)
}
func atBeginningOfDay(hourInt: Int = 9) -> Date { func atBeginningOfDay(hourInt: Int = 9) -> Date {
Calendar.current.date(byAdding: .hour, value: hourInt, to: self.startOfDay)! Calendar.current.date(byAdding: .hour, value: hourInt, to: self.startOfDay)!
} }
@ -144,6 +152,28 @@ public extension Date {
return weekdays.map { $0.capitalized } return weekdays.map { $0.capitalized }
}() }()
static var weekdays: [String] = {
let calendar = Calendar.current
// let weekdays = calendar.shortWeekdaySymbols
// return weekdays.map { weekday in
// guard let firstLetter = weekday.first else { return "" }
// return String(firstLetter).capitalized
// }
// Adjusted for the different weekday starts
var weekdays = calendar.weekdaySymbols
if firstDayOfWeek > 1 {
for _ in 1..<firstDayOfWeek {
if let first = weekdays.first {
weekdays.append(first)
weekdays.removeFirst()
}
}
}
return weekdays.map { $0.capitalized }
}()
static var fullMonthNames: [String] = { static var fullMonthNames: [String] = {
let dateFormatter = DateFormatter() let dateFormatter = DateFormatter()
dateFormatter.locale = Locale.current dateFormatter.locale = Locale.current
@ -164,6 +194,11 @@ public extension Date {
return Calendar.current.date(byAdding: .day, value: -1, to: lastDay)! return Calendar.current.date(byAdding: .day, value: -1, to: lastDay)!
} }
var endOfWeek: Date {
let lastDay = Calendar.current.dateInterval(of: .weekOfMonth, for: self)!.end
return Calendar.current.date(byAdding: .day, value: -1, to: lastDay)!
}
var startOfPreviousMonth: Date { var startOfPreviousMonth: Date {
let dayInPreviousMonth = Calendar.current.date(byAdding: .month, value: -1, to: self)! let dayInPreviousMonth = Calendar.current.date(byAdding: .month, value: -1, to: self)!
return dayInPreviousMonth.startOfMonth return dayInPreviousMonth.startOfMonth

@ -30,11 +30,7 @@ import Combine
self.updateListenerTask = self.listenForTransactions() self.updateListenerTask = self.listenForTransactions()
Task { Task {
do { await self.refreshPurchases()
try await self.refreshPurchasedAppleProducts()
} catch {
Logger.error(error)
}
Logger.log("plan = \(String(describing: currentBestPurchase?.productId))") Logger.log("plan = \(String(describing: currentBestPurchase?.productId))")
} }
@ -66,18 +62,45 @@ import Combine
return productIds return productIds
} }
public func refreshPurchasedAppleProducts() async throws { public func refreshPurchases() async {
await _refreshUnfinishedTransactions()
await _refreshPurchasedAppleProducts()
}
fileprivate func _refreshPurchasedAppleProducts() async {
// Iterate through the user's purchased products. // Iterate through the user's purchased products.
for await verificationResult in Transaction.currentEntitlements { for await verificationResult in Transaction.currentEntitlements {
do {
let transaction = try await self.processTransactionResult(verificationResult)
print("processs product id = \(transaction.productID)")
DispatchQueue.main.async {
NotificationCenter.default.post(name: Notification.Name.StoreEventHappened, object: nil)
}
await transaction.finish()
} catch {
Logger.error(error)
}
}
}
public func _refreshUnfinishedTransactions() async {
// Iterate through the user's purchased products.
for await verificationResult in Transaction.unfinished {
do {
let transaction = try await self.processTransactionResult(verificationResult) let transaction = try await self.processTransactionResult(verificationResult)
print("processs product id = \(transaction.productID)") print("processs product id = \(transaction.productID)")
DispatchQueue.main.async { DispatchQueue.main.async {
NotificationCenter.default.post(name: Notification.Name.StoreEventHappened, object: nil) NotificationCenter.default.post(name: Notification.Name.StoreEventHappened, object: nil)
} }
await transaction.finish() await transaction.finish()
} catch {
Logger.error(error)
} }
} }
}
func listenForTransactions() -> Task<Void, Never> { func listenForTransactions() -> Task<Void, Never> {
return Task(priority: .background) { return Task(priority: .background) {
@ -235,13 +258,34 @@ import Combine
let purchases = DataStore.shared.purchases let purchases = DataStore.shared.purchases
let units = purchases.filter { $0.productId == StoreItem.unit.rawValue } let units = purchases.filter { $0.productId == StoreItem.unit.rawValue }
return units.reduce(0) { $0 + ($1.quantity ?? 0) } let units10Pack = purchases.filter { $0.productId == StoreItem.unit10Pack.rawValue }
return units.reduce(0) { $0 + ($1.quantity ?? 0) } + 10 * units10Pack.reduce(0) { $0 + ($1.quantity ?? 0) }
// let units = self.userFilteredPurchases().filter { $0.productID == StoreItem.unit.rawValue } // let units = self.userFilteredPurchases().filter { $0.productID == StoreItem.unit.rawValue }
// return units.reduce(0) { $0 + $1.purchasedQuantity } // return units.reduce(0) { $0 + $1.purchasedQuantity }
} }
public func paymentForNewTournament() -> TournamentPayment? { struct CanCreateResponse: Decodable { var canCreate: Bool }
public func paymentForNewTournament() async -> TournamentPayment? {
if let payment = self.localPaymentForNewTournament() {
return payment
} else if let services = try? StoreCenter.main.service() {
do {
let response: CanCreateResponse = try await services.run(path: "is_granted_unlimited_access/", method: .get, requiresToken: true)
if response.canCreate {
return .unlimited
}
} catch {
Logger.error(error)
}
}
return nil
}
public func localPaymentForNewTournament() -> TournamentPayment? {
switch self.currentPlan { switch self.currentPlan {
case .monthlyUnlimited: case .monthlyUnlimited:

@ -11,6 +11,7 @@ public enum StoreItem: String, Identifiable, CaseIterable {
case monthlyUnlimited = "app.padelclub.tournament.subscription.unlimited" case monthlyUnlimited = "app.padelclub.tournament.subscription.unlimited"
case fivePerMonth = "app.padelclub.tournament.subscription.five.per.month" case fivePerMonth = "app.padelclub.tournament.subscription.five.per.month"
case unit = "app.padelclub.tournament.unit" case unit = "app.padelclub.tournament.unit"
case unit10Pack = "app.padelclub.tournament.unit.10"
#if DEBUG #if DEBUG
public static let five: Int = 2 public static let five: Int = 2
@ -20,18 +21,27 @@ public enum StoreItem: String, Identifiable, CaseIterable {
public var id: String { return self.rawValue } public var id: String { return self.rawValue }
public var summarySystemImage: String {
switch self {
case .monthlyUnlimited: return "infinity.circle.fill"
case .fivePerMonth: return "star.circle.fill"
case .unit, .unit10Pack: return "tennisball.circle.fill"
}
}
public var systemImage: String { public var systemImage: String {
switch self { switch self {
case .monthlyUnlimited: return "infinity.circle.fill" case .monthlyUnlimited: return "infinity.circle.fill"
case .fivePerMonth: return "star.circle.fill" case .fivePerMonth: return "star.circle.fill"
case .unit: return "tennisball.circle.fill" case .unit: return "1.circle.fill"
case .unit10Pack: return "10.circle.fill"
} }
} }
public var isConsumable: Bool { public var isConsumable: Bool {
switch self { switch self {
case .monthlyUnlimited, .fivePerMonth: return false case .monthlyUnlimited, .fivePerMonth: return false
case .unit: return true case .unit, .unit10Pack: return true
} }
} }

@ -53,7 +53,11 @@ public class StoreManager {
var products: [Product] = try await Product.products(for: self._productIdentifiers()) var products: [Product] = try await Product.products(for: self._productIdentifiers())
products = products.sorted { p1, p2 in products = products.sorted { p1, p2 in
if p1.type == p2.type {
return p2.price > p1.price return p2.price > p1.price
} else {
return p2.type.rawValue < p1.type.rawValue
}
} }
Logger.log("products = \(products.count)") Logger.log("products = \(products.count)")
@ -67,12 +71,10 @@ public class StoreManager {
fileprivate func _productIdentifiers() -> [String] { fileprivate func _productIdentifiers() -> [String] {
var items: [StoreItem] = [] var items: [StoreItem] = []
switch Guard.main.currentPlan { switch Guard.main.currentPlan {
case .fivePerMonth:
items = [StoreItem.unit, StoreItem.monthlyUnlimited]
case .monthlyUnlimited: case .monthlyUnlimited:
break items = [StoreItem.unit, StoreItem.unit10Pack]
default: default:
items = StoreItem.allCases items = [StoreItem.unit, StoreItem.unit10Pack, StoreItem.monthlyUnlimited]
} }
return items.map { $0.rawValue } return items.map { $0.rawValue }
} }

@ -25,7 +25,7 @@ enum RankSource: Hashable {
} }
} }
public protocol TournamentBuildHolder: Identifiable { public protocol TournamentBuildHolder: Identifiable, Hashable, Equatable {
var id: String { get } var id: String { get }
var category: TournamentCategory { get } var category: TournamentCategory { get }
var level: TournamentLevel { get } var level: TournamentLevel { get }

Loading…
Cancel
Save