diff --git a/PadelClubData/Subscriptions/Guard.swift b/PadelClubData/Subscriptions/Guard.swift index 2ff17e7..2164667 100644 --- a/PadelClubData/Subscriptions/Guard.swift +++ b/PadelClubData/Subscriptions/Guard.swift @@ -258,7 +258,9 @@ import Combine let purchases = DataStore.shared.purchases 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 } // return units.reduce(0) { $0 + $1.purchasedQuantity } diff --git a/PadelClubData/Subscriptions/StoreItem.swift b/PadelClubData/Subscriptions/StoreItem.swift index 868d28c..d9c4bce 100644 --- a/PadelClubData/Subscriptions/StoreItem.swift +++ b/PadelClubData/Subscriptions/StoreItem.swift @@ -11,6 +11,7 @@ public enum StoreItem: String, Identifiable, CaseIterable { case monthlyUnlimited = "app.padelclub.tournament.subscription.unlimited" case fivePerMonth = "app.padelclub.tournament.subscription.five.per.month" case unit = "app.padelclub.tournament.unit" + case unit10Pack = "app.padelclub.tournament.unit.10" #if DEBUG 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 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 { switch self { case .monthlyUnlimited: return "infinity.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 { switch self { case .monthlyUnlimited, .fivePerMonth: return false - case .unit: return true + case .unit, .unit10Pack: return true } } diff --git a/PadelClubData/Subscriptions/StoreManager.swift b/PadelClubData/Subscriptions/StoreManager.swift index 4bf853b..425de9a 100644 --- a/PadelClubData/Subscriptions/StoreManager.swift +++ b/PadelClubData/Subscriptions/StoreManager.swift @@ -53,7 +53,11 @@ public class StoreManager { var products: [Product] = try await Product.products(for: self._productIdentifiers()) products = products.sorted { p1, p2 in - return p2.price > p1.price + if p1.type == p2.type { + return p2.price > p1.price + } else { + return p2.type.rawValue < p1.type.rawValue + } } Logger.log("products = \(products.count)") @@ -68,11 +72,11 @@ public class StoreManager { var items: [StoreItem] = [] switch Guard.main.currentPlan { case .fivePerMonth: - items = [StoreItem.unit, StoreItem.monthlyUnlimited] + items = [StoreItem.unit, StoreItem.unit10Pack, StoreItem.monthlyUnlimited] case .monthlyUnlimited: break default: - items = StoreItem.allCases + items = [StoreItem.unit, StoreItem.unit10Pack, StoreItem.monthlyUnlimited] } return items.map { $0.rawValue } } diff --git a/PadelClubDataTests/PadelClubDataTests.swift b/PadelClubDataTests/PadelClubDataTests.swift deleted file mode 100644 index 18acb93..0000000 --- a/PadelClubDataTests/PadelClubDataTests.swift +++ /dev/null @@ -1,94 +0,0 @@ -// -// PadelClubDataTests.swift -// PadelClubDataTests -// -// Created by Laurent Morvillier on 15/04/2025. -// - -import Testing -@testable import PadelClubData -@testable import LeStorage - -enum TestError: Error { - case notAuthenticated - case sameDeviceId - case missingEvent -} - -struct PadelClubDataTests { - - let username: String = "UserDataTests" - let password: String = "MyPass1234--" - - init() async throws { - StoreCenter.main.configureURLs(secureScheme: false, domain: "127.0.0.1:8000") - StoreCenter.main.tokenKeychain = MockKeychainStore(fileName: "token.json") - try await self.login() - } - - mutating func login() async throws { - let _: CustomUser = try await StoreCenter.main.service().login(username: self.username, password: self.password) - } - - @Test func testAuthentication() { - #expect(StoreCenter.main.isAuthenticated) - } - - @Test func createTournament() async throws { - - guard let userId = StoreCenter.main.userId else { - throw TestError.notAuthenticated - } - - // Cleanup - let events = DataStore.shared.events - try await DataStore.shared.events.deleteAsync(contentOfs: Array(events)) - - try await DataStore.shared.events.loadDataFromServerIfAllowed(clear: true) - #expect(DataStore.shared.events.count == 0) - - try await DataStore.shared.tournaments.loadDataFromServerIfAllowed(clear: true) - #expect(DataStore.shared.tournaments.count == 0) - - // Create - let event: Event = Event(creator: userId, club: nil, name: "test") - try await DataStore.shared.events.addOrUpdateAsync(instance: event) - - let tournament: Tournament = Tournament.fake() - tournament.event = event.id - try await DataStore.shared.tournaments.addOrUpdateAsync(instance: tournament) - - // Test server content - try await DataStore.shared.events.loadDataFromServerIfAllowed(clear: true) - #expect(DataStore.shared.events.count == 1) - - try await DataStore.shared.tournaments.loadDataFromServerIfAllowed(clear: true) - #expect(DataStore.shared.tournaments.count == 1) - - } - - @Test func dualStoreCenter() async throws { - - let secondStoreServer = StoreCenter() - secondStoreServer.configureURLs(secureScheme: false, domain: "127.0.0.1:8000") - secondStoreServer.tokenKeychain = MockKeychainStore(fileName: "token.json") - - let _: CustomUser = try await secondStoreServer.service().login(username: self.username, password: self.password) - - #expect(StoreCenter.main.isAuthenticated) - #expect(secondStoreServer.isAuthenticated) - - } - - @Test func testWebsocketSynchronization() async throws { - - let secondStoreServer = StoreCenter() - secondStoreServer.configureURLs(secureScheme: false, domain: "127.0.0.1:8000") - secondStoreServer.tokenKeychain = MockKeychainStore(fileName: "token.json") - - let events = DataStore.shared.events - try await DataStore.shared.events.deleteAsync(contentOfs: Array(events)) - - } - -} diff --git a/PadelClubDataTests/SyncDataAccessTests.swift b/PadelClubDataTests/SyncDataAccessTests.swift deleted file mode 100644 index c605dbc..0000000 --- a/PadelClubDataTests/SyncDataAccessTests.swift +++ /dev/null @@ -1,327 +0,0 @@ -// -// DataAccessSyncTests.swift -// PadelClubDataTests -// -// Created by Laurent Morvillier on 02/05/2025. -// - -import Testing -@testable import PadelClubData -@testable import LeStorage - -struct SyncDataAccessTests { - - let username1: String = "UserDataTests" - let password1: String = "MyPass1234--" - - let username2: String = "seconduser" - let password2: String = "MyPass1234--" - - var secondStoreCenter: StoreCenter - - init() async throws { - FileManager.default.deleteDirectoryInDocuments(directoryName: "storage") - FileManager.default.deleteDirectoryInDocuments(directoryName: "storage-2") - - self.secondStoreCenter = StoreCenter(directoryName: "storage-2") - self.secondStoreCenter.configureURLs(secureScheme: false, domain: "127.0.0.1:8000", webSockets: false, useSynchronization: true) - self.secondStoreCenter.tokenKeychain = MockKeychainStore(fileName: "storage-2/token.json") - self.secondStoreCenter.deviceKeychain = MockKeychainStore(fileName: "storage-2/device.json") - try self.secondStoreCenter.deviceKeychain.add(value: UUID().uuidString) - - self.secondStoreCenter.classProject = "PadelClubData" - - let token2 = try? self.secondStoreCenter.rawTokenShouldNotBeUsed() - if token2 == nil { - try await self.login(storeCenter: self.secondStoreCenter, username: self.username2, password: self.password2) - } - - StoreCenter.main.configureURLs(secureScheme: false, domain: "127.0.0.1:8000", webSockets: false, useSynchronization: true) - StoreCenter.main.tokenKeychain = MockKeychainStore(fileName: "storage/token.json") - StoreCenter.main.deviceKeychain = MockKeychainStore(fileName: "storage/device.json") - try StoreCenter.main.deviceKeychain.add(value: UUID().uuidString) - StoreCenter.main.classProject = "PadelClubData" - - let token = try? StoreCenter.main.rawTokenShouldNotBeUsed() - if token == nil { - try await self.login(storeCenter: StoreCenter.main, username: self.username1, password: self.password1) - } - } - - mutating func login(storeCenter: StoreCenter, username: String, password: String) async throws { - let _: CustomUser = try await storeCenter.service().login(username: username, password: password) - } - - @Test func testSetup() async throws { - #expect(StoreCenter.main.isAuthenticated) - #expect(self.secondStoreCenter.isAuthenticated) - - guard let userId1 = StoreCenter.main.userId else { - throw TestError.notAuthenticated - } - guard let userId2 = self.secondStoreCenter.userId else { - throw TestError.notAuthenticated - } - #expect(userId1 != userId2) - } - - /// In this test, the first user: - /// - creates an event and a tournament - /// - shares the tournament with a second user - /// - remove the sharing with the second user - /// We test that the data is properly received and removed upon the sharing actions - @Test func testTournamentSharing() async throws { - - guard let userId1 = StoreCenter.main.userId else { - throw TestError.notAuthenticated - } - guard let userId2 = self.secondStoreCenter.userId else { - throw TestError.notAuthenticated - } - - // Setup - let eventColA: SyncedCollection = await StoreCenter.main.mainStore.asyncLoadingSynchronizedCollection() - let tournamentColA: SyncedCollection = await StoreCenter.main.mainStore.asyncLoadingSynchronizedCollection() - let eventColB: SyncedCollection = await self.secondStoreCenter.mainStore.asyncLoadingSynchronizedCollection() - let tournamentColB: SyncedCollection = await self.secondStoreCenter.mainStore.asyncLoadingSynchronizedCollection() - - if let dataAccessCollection = StoreCenter.main.dataAccessCollection { - try await dataAccessCollection.deleteAsync(contentOfs: Array(dataAccessCollection)) - } - - try await eventColA.deleteAsync(contentOfs: Array(eventColA)) - try await tournamentColA.deleteAsync(contentOfs: Array(tournamentColA)) - - let _ = try await self.secondStoreCenter.testSynchronizeOnceAsync() - - #expect(eventColB.count == 0) - #expect(tournamentColB.count == 0) - - // Create - let eventA = Event(creator: userId1) - try await eventColA.addOrUpdateAsync(instance: eventA) - let tournamentA = Tournament(event: eventA.id, name: "P100") - try await tournamentColA.addOrUpdateAsync(instance: tournamentA) - - // Share with user2 - try await StoreCenter.main.setAuthorizedUsersAsync(for: tournamentA, users: [userId2]) - - var dataB = try await self.secondStoreCenter.testSynchronizeOnceAsync() - var syncDataB = try SyncData(data: dataB, storeCenter: self.secondStoreCenter) - #expect(syncDataB.grants.count == 2) - - #expect(eventColB.count == 1) - #expect(tournamentColB.count == 1) - - // Remove sharing from user2 - try await StoreCenter.main.setAuthorizedUsersAsync(for: tournamentA, users: []) - - dataB = try await self.secondStoreCenter.testSynchronizeOnceAsync() - syncDataB = try SyncData(data: dataB, storeCenter: self.secondStoreCenter) - #expect(syncDataB.revocations.count == 1) - #expect(syncDataB.revocationParents.count == 1) - - #expect(eventColB.count == 0) - #expect(tournamentColB.count == 0) - - let dataAccesses: [DataAccess] = try await StoreCenter.main.service().get() - #expect(dataAccesses.count == 0) - } - - /// In this test, the first user: - /// - creates a club and 2 events in that club - /// - shares both events with a second user - /// - removes the sharing of 1 event - /// Here we want to test that the Club instance remains even if one event is removed from the second user - @Test func testSharedRelationship() async throws { - - guard let userId1 = StoreCenter.main.userId else { - throw TestError.notAuthenticated - } - guard let userId2 = self.secondStoreCenter.userId else { - throw TestError.notAuthenticated - } - - // Setup - let eventColA: SyncedCollection = await StoreCenter.main.mainStore.asyncLoadingSynchronizedCollection() - let clubColA: SyncedCollection = await StoreCenter.main.mainStore.asyncLoadingSynchronizedCollection() - let eventColB: SyncedCollection = await self.secondStoreCenter.mainStore.asyncLoadingSynchronizedCollection() - let clubColB: SyncedCollection = await self.secondStoreCenter.mainStore.asyncLoadingSynchronizedCollection() - - if let dataAccessCollection = StoreCenter.main.dataAccessCollection { - try await dataAccessCollection.deleteAsync(contentOfs: Array(dataAccessCollection)) - } - - try await eventColA.deleteAsync(contentOfs: Array(eventColA)) - try await clubColA.deleteAsync(contentOfs: Array(clubColA)) - - let _ = try await self.secondStoreCenter.testSynchronizeOnceAsync() - - #expect(eventColB.count == 0) - #expect(clubColB.count == 0) - - // Create - let clubA = Club(creator: userId1, name: "Club A", acronym: "CA") - try await clubColA.addOrUpdateAsync(instance: clubA) - - let event1A = Event(creator: userId1, club: clubA.id, name: "event 1") - let event2A = Event(creator: userId1, club: clubA.id, name: "event 2") - try await eventColA.addOrUpdateAsync(contentOfs: [event1A, event2A]) - - // Share with user2 - try await StoreCenter.main.setAuthorizedUsersAsync(for: event1A, users: [userId2]) - try await StoreCenter.main.setAuthorizedUsersAsync(for: event2A, users: [userId2]) - - var dataB = try await self.secondStoreCenter.testSynchronizeOnceAsync() - var syncDataB = try SyncData(data: dataB, storeCenter: self.secondStoreCenter) - - #expect(syncDataB.grants.count == 2) - - let clubGrants = syncDataB.grants.first { $0.type == Club.self } - let eventGrants = syncDataB.grants.first { $0.type == Event.self } - #expect(clubGrants?.items.count == 1) - #expect(eventGrants?.items.count == 2) - - #expect(eventColB.count == 2) - #expect(clubColB.count == 1) - - // Remove sharing from user2 - try await StoreCenter.main.setAuthorizedUsersAsync(for: event1A, users: []) - - dataB = try await self.secondStoreCenter.testSynchronizeOnceAsync() - syncDataB = try SyncData(data: dataB, storeCenter: self.secondStoreCenter) - #expect(syncDataB.revocations.count == 1) - #expect(syncDataB.revocationParents.count == 1) - - #expect(eventColB.count == 1) - #expect(clubColB.count == 1) // club remains because used in event2A - } - - /// In this test, the first user: - /// - creates one event and 2 clubs - /// - shares the event with a second user - /// - changes the club on the event - /// Here we want to test that the first Club is removed and the second one is received - @Test func testRelationshipChange() async throws { - - guard let userId1 = StoreCenter.main.userId else { - throw TestError.notAuthenticated - } - guard let userId2 = self.secondStoreCenter.userId else { - throw TestError.notAuthenticated - } - - // Setup - let eventColA: SyncedCollection = await StoreCenter.main.mainStore.asyncLoadingSynchronizedCollection() - let clubColA: SyncedCollection = await StoreCenter.main.mainStore.asyncLoadingSynchronizedCollection() - let eventColB: SyncedCollection = await self.secondStoreCenter.mainStore.asyncLoadingSynchronizedCollection() - let clubColB: SyncedCollection = await self.secondStoreCenter.mainStore.asyncLoadingSynchronizedCollection() - - if let dataAccessCollection = StoreCenter.main.dataAccessCollection { - try await dataAccessCollection.deleteAsync(contentOfs: Array(dataAccessCollection)) - } - - try await eventColA.deleteAsync(contentOfs: Array(eventColA)) - try await clubColA.deleteAsync(contentOfs: Array(clubColA)) - - let _ = try await self.secondStoreCenter.testSynchronizeOnceAsync() - - #expect(eventColB.count == 0) - #expect(clubColB.count == 0) - - // Create - let club1A = Club(creator: userId1, name: "Club 1", acronym: "C1") - try await clubColA.addOrUpdateAsync(instance: club1A) - let club2A = Club(creator: userId1, name: "Club 2", acronym: "C2") - try await clubColA.addOrUpdateAsync(instance: club2A) - - let eventA = Event(creator: userId1, club: club1A.id, name: "event 1") - try await eventColA.addOrUpdateAsync(instance: eventA) - - // Share with user2 - try await StoreCenter.main.setAuthorizedUsersAsync(for: eventA, users: [userId2]) - let _ = try await self.secondStoreCenter.testSynchronizeOnceAsync() - #expect(eventColB.count == 1) - #expect(clubColB.count == 1) - - // Change the club - eventA.club = club2A.id - try await eventColA.addOrUpdateAsync(instance: eventA) - let dataB = try await self.secondStoreCenter.testSynchronizeOnceAsync() - - let syncDataB = try SyncData(data: dataB, storeCenter: self.secondStoreCenter) - - #expect(syncDataB.sharedRelationshipSets.count == 1) - #expect(syncDataB.sharedRelationshipRemovals.count == 1) - - #expect(eventColB.first?.club == club2A.id) - } - - /// In this test, the first user: - /// - creates one event - /// - shares the event with a second user - /// The second user: - /// - changes the club - /// Here we want to test that the first Club is removed and the second one is received - @Test func testRelationshipChangesByAgent() async throws { - - guard let userId1 = StoreCenter.main.userId else { - throw TestError.notAuthenticated - } - guard let userId2 = self.secondStoreCenter.userId else { - throw TestError.notAuthenticated - } - - // Setup - let eventColA: SyncedCollection = await StoreCenter.main.mainStore.asyncLoadingSynchronizedCollection() - let clubColA: SyncedCollection = await StoreCenter.main.mainStore.asyncLoadingSynchronizedCollection() - let eventColB: SyncedCollection = await self.secondStoreCenter.mainStore.asyncLoadingSynchronizedCollection() - let clubColB: SyncedCollection = await self.secondStoreCenter.mainStore.asyncLoadingSynchronizedCollection() - - if let dataAccessCollection = StoreCenter.main.dataAccessCollection { - try await dataAccessCollection.deleteAsync(contentOfs: Array(dataAccessCollection)) - } - - try await eventColA.deleteAsync(contentOfs: Array(eventColA)) - try await clubColA.deleteAsync(contentOfs: Array(clubColA)) - - let _ = try await self.secondStoreCenter.testSynchronizeOnceAsync() - - #expect(eventColB.count == 0) - #expect(clubColB.count == 0) - - // Create - let clubA = Club(creator: userId1, name: "Club 1", acronym: "C1") - try await clubColA.addOrUpdateAsync(instance: clubA) - - let eventA = Event(creator: userId1, club: clubA.id, name: "event 1") - try await eventColA.addOrUpdateAsync(instance: eventA) - - // Share with user2 - try await StoreCenter.main.setAuthorizedUsersAsync(for: eventA, users: [userId2]) - let _ = try await self.secondStoreCenter.testSynchronizeOnceAsync() - - guard let eventB = eventColB.first else { - throw TestError.missingEvent - } - #expect(eventA.id == eventB.id) - #expect(clubColB.count == 1) - - // Second user changes the club - let club2B = Club(creator: userId2, name: "Club 2", acronym: "C2") - try await clubColB.addOrUpdateAsync(instance: club2B) - eventB.club = club2B.id - try await eventColB.addOrUpdateAsync(instance: eventB) - - let dataA = try await StoreCenter.main.testSynchronizeOnceAsync() - let syncDataA = try SyncData(data: dataA, storeCenter: StoreCenter.main) - -// #expect(syncDataA.sharedRelationshipSets.count == 1) -// #expect(syncDataA.sharedRelationshipRemovals.count == 1) - - #expect(eventA.club == club2B.id) - #expect(clubColB.count == 1) - - } - -}