Fix issue with Purchase, add fields to Purchase + convenience methods to encode/decode + fix tests

sync2
Laurent 1 year ago
parent b74c7a3c74
commit cf3cb3ec18
  1. 18
      PadelClub.xcodeproj/project.pbxproj
  2. 8
      PadelClub/Data/Tournament.swift
  3. 49
      PadelClub/Extensions/CodingContainer+Extensions.swift
  4. 2
      PadelClub/Utils/CryptoKey.swift
  5. 15
      PadelClub/Views/Navigation/Umpire/UmpireView.swift
  6. 106
      PadelClub/Views/Tournament/Subscription/Guard.swift
  7. 55
      PadelClub/Views/Tournament/Subscription/Purchase.swift
  8. 69
      PadelClubTests/ServerDataTests.swift
  9. 14
      PadelClubTests/TokenExemptionTests.swift
  10. 10
      PadelClubTests/UserDataTests.swift

@ -30,7 +30,7 @@
C49EF03A2BDFF4600077B5AA /* LeStorage.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = C49EF0372BDFF3000077B5AA /* LeStorage.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
C49EF03C2BE15AF80077B5AA /* String+Crypto.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49EF03B2BE15AF80077B5AA /* String+Crypto.swift */; };
C49EF0422BE23BF50077B5AA /* PaymentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49EF0412BE23BF50077B5AA /* PaymentTests.swift */; };
C49EF0442BE286780077B5AA /* Key.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49EF0432BE286780077B5AA /* Key.swift */; };
C49EF0442BE286780077B5AA /* CryptoKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49EF0432BE286780077B5AA /* CryptoKey.swift */; };
C4A47D5A2B6D383C00ADC637 /* Tournament.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47D592B6D383C00ADC637 /* Tournament.swift */; };
C4A47D5E2B6D38EC00ADC637 /* DataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47D5D2B6D38EC00ADC637 /* DataStore.swift */; };
C4A47D632B6D3D6500ADC637 /* Club.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47D622B6D3D6500ADC637 /* Club.swift */; };
@ -46,6 +46,8 @@
C4A47DB32B86387500ADC637 /* AccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47DB22B86387500ADC637 /* AccountView.swift */; };
C4B3A1552C2581DA0078EAA8 /* Patcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B3A1542C2581DA0078EAA8 /* Patcher.swift */; };
C4C01D982C481C0C0059087C /* CapsuleViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C01D972C481C0C0059087C /* CapsuleViewModifier.swift */; };
C4C33F762C9B1ED4006316DE /* CodingContainer+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C33F752C9B1EC5006316DE /* CodingContainer+Extensions.swift */; };
C4C33F772C9B1ED4006316DE /* CodingContainer+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C33F752C9B1EC5006316DE /* CodingContainer+Extensions.swift */; };
C4EC6F572BE92CAC000CEAB4 /* local.plist in Resources */ = {isa = PBXBuildFile; fileRef = C4EC6F562BE92CAC000CEAB4 /* local.plist */; };
C4EC6F592BE92D88000CEAB4 /* PListReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EC6F582BE92D88000CEAB4 /* PListReader.swift */; };
C4FC2E272C2AABC90021F3BF /* PasswordField.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4FC2E262C2AABC90021F3BF /* PasswordField.swift */; };
@ -290,7 +292,7 @@
FF70FB402C90584900129CC2 /* ClubRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF5D0D882BB4935C005CB568 /* ClubRowView.swift */; };
FF70FB412C90584900129CC2 /* ClubDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1DC5502BAB351300FD8220 /* ClubDetailView.swift */; };
FF70FB422C90584900129CC2 /* GroupStageCallingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF9268022BCE94A30080F940 /* GroupStageCallingView.swift */; };
FF70FB432C90584900129CC2 /* Key.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49EF0432BE286780077B5AA /* Key.swift */; };
FF70FB432C90584900129CC2 /* CryptoKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49EF0432BE286780077B5AA /* CryptoKey.swift */; };
FF70FB442C90584900129CC2 /* CashierSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF11627C2BCF941A000C4809 /* CashierSettingsView.swift */; };
FF70FB452C90584900129CC2 /* LoserRoundScheduleEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFFCDE0D2BCC833600317DEF /* LoserRoundScheduleEditorView.swift */; };
FF70FB462C90584900129CC2 /* Club.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47D622B6D3D6500ADC637 /* Club.swift */; };
@ -609,7 +611,7 @@
C49EF0372BDFF3000077B5AA /* LeStorage.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = LeStorage.framework; sourceTree = BUILT_PRODUCTS_DIR; };
C49EF03B2BE15AF80077B5AA /* String+Crypto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Crypto.swift"; sourceTree = "<group>"; };
C49EF0412BE23BF50077B5AA /* PaymentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentTests.swift; sourceTree = "<group>"; };
C49EF0432BE286780077B5AA /* Key.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Key.swift; sourceTree = "<group>"; };
C49EF0432BE286780077B5AA /* CryptoKey.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CryptoKey.swift; sourceTree = "<group>"; };
C4A47D592B6D383C00ADC637 /* Tournament.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tournament.swift; sourceTree = "<group>"; };
C4A47D5D2B6D38EC00ADC637 /* DataStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataStore.swift; sourceTree = "<group>"; };
C4A47D622B6D3D6500ADC637 /* Club.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Club.swift; sourceTree = "<group>"; };
@ -625,6 +627,7 @@
C4A47DB22B86387500ADC637 /* AccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountView.swift; sourceTree = "<group>"; };
C4B3A1542C2581DA0078EAA8 /* Patcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Patcher.swift; sourceTree = "<group>"; };
C4C01D972C481C0C0059087C /* CapsuleViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CapsuleViewModifier.swift; sourceTree = "<group>"; };
C4C33F752C9B1EC5006316DE /* CodingContainer+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CodingContainer+Extensions.swift"; sourceTree = "<group>"; };
C4EC6F562BE92CAC000CEAB4 /* local.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = local.plist; sourceTree = "<group>"; };
C4EC6F582BE92D88000CEAB4 /* PListReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PListReader.swift; sourceTree = "<group>"; };
C4FC2E262C2AABC90021F3BF /* PasswordField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordField.swift; sourceTree = "<group>"; };
@ -1626,7 +1629,7 @@
FFA6D7842BB0B795003A31F3 /* FileImportManager.swift */,
FF1F4B722BFA00FB000B4573 /* HtmlGenerator.swift */,
FF1F4B732BFA00FC000B4573 /* HtmlService.swift */,
C49EF0432BE286780077B5AA /* Key.swift */,
C49EF0432BE286780077B5AA /* CryptoKey.swift */,
FFC1E1072BAC29FC008D6F59 /* LocationManager.swift */,
FF92680C2BCEE5EA0080F940 /* NetworkMonitor.swift */,
FF8F26352BAD523300650388 /* PadelRule.swift */,
@ -1646,6 +1649,7 @@
children = (
FF6EC90A2B947AC000EA7F5A /* Array+Extensions.swift */,
FF1CBC1C2BB53DC10036DAAB /* Calendar+Extensions.swift */,
C4C33F752C9B1EC5006316DE /* CodingContainer+Extensions.swift */,
FF5D0D732BB41DF8005CB568 /* Color+Extensions.swift */,
FFF8ACDA2B923F48008466FA /* Date+Extensions.swift */,
FF6EC9082B947A5300EA7F5A /* FixedWidthInteger+Extensions.swift */,
@ -2010,7 +2014,7 @@
FF5D0D892BB4935C005CB568 /* ClubRowView.swift in Sources */,
FF1DC5512BAB351300FD8220 /* ClubDetailView.swift in Sources */,
FF9268032BCE94A30080F940 /* GroupStageCallingView.swift in Sources */,
C49EF0442BE286780077B5AA /* Key.swift in Sources */,
C49EF0442BE286780077B5AA /* CryptoKey.swift in Sources */,
FF11627D2BCF941A000C4809 /* CashierSettingsView.swift in Sources */,
FFFCDE0E2BCC833600317DEF /* LoserRoundScheduleEditorView.swift in Sources */,
C4A47D632B6D3D6500ADC637 /* Club.swift in Sources */,
@ -2068,6 +2072,7 @@
FF025AF12BD1AEBD00A86CF8 /* MatchFormatStorageView.swift in Sources */,
FF3F74F62B919E45004CFE0E /* UmpireView.swift in Sources */,
C4A47DAD2B85FCCD00ADC637 /* User.swift in Sources */,
C4C33F762C9B1ED4006316DE /* CodingContainer+Extensions.swift in Sources */,
FF967D012BAEF0B400A9A3BD /* MatchSummaryView.swift in Sources */,
FF8F26452BAE0A3400650388 /* TournamentDurationManagerView.swift in Sources */,
FF1DC5532BAB354A00FD8220 /* MockData.swift in Sources */,
@ -2278,7 +2283,7 @@
FF70FB402C90584900129CC2 /* ClubRowView.swift in Sources */,
FF70FB412C90584900129CC2 /* ClubDetailView.swift in Sources */,
FF70FB422C90584900129CC2 /* GroupStageCallingView.swift in Sources */,
FF70FB432C90584900129CC2 /* Key.swift in Sources */,
FF70FB432C90584900129CC2 /* CryptoKey.swift in Sources */,
FF70FB442C90584900129CC2 /* CashierSettingsView.swift in Sources */,
FF70FB452C90584900129CC2 /* LoserRoundScheduleEditorView.swift in Sources */,
FF70FB462C90584900129CC2 /* Club.swift in Sources */,
@ -2336,6 +2341,7 @@
FF70FB7A2C90584900129CC2 /* MatchFormatStorageView.swift in Sources */,
FF70FB7B2C90584900129CC2 /* UmpireView.swift in Sources */,
FF70FB7C2C90584900129CC2 /* User.swift in Sources */,
C4C33F772C9B1ED4006316DE /* CodingContainer+Extensions.swift in Sources */,
FF70FB7D2C90584900129CC2 /* MatchSummaryView.swift in Sources */,
FF70FB7E2C90584900129CC2 /* TournamentDurationManagerView.swift in Sources */,
FF70FB7F2C90584900129CC2 /* MockData.swift in Sources */,

@ -198,7 +198,7 @@ final class Tournament : ModelObject, Storable {
if let data {
do {
let decoded: String = try data.decryptData(pass: Key.pass.rawValue)
let decoded: String = try data.decryptData(pass: CryptoKey.pass.rawValue)
let sequence = decoded.compactMap { _numberFormatter.number(from: String($0))?.intValue }
return TournamentPayment(rawValue: sequence[18])
} catch {
@ -212,7 +212,7 @@ final class Tournament : ModelObject, Storable {
let data = try container.decodeIfPresent(Data.self, forKey: ._isCanceled)
if let data {
do {
let decoded: String = try data.decryptData(pass: Key.pass.rawValue)
let decoded: String = try data.decryptData(pass: CryptoKey.pass.rawValue)
let sequence = decoded.compactMap { _numberFormatter.number(from: String($0))?.intValue }
return Bool.decodeInt(sequence[18])
} catch {
@ -321,7 +321,7 @@ final class Tournament : ModelObject, Storable {
let stringCombo: [String] = sequence.map { $0.formatted() }
let joined: String = stringCombo.joined(separator: "")
if let data = joined.data(using: .utf8) {
let encryped: Data = try data.encrypt(pass: Key.pass.rawValue)
let encryped: Data = try data.encrypt(pass: CryptoKey.pass.rawValue)
try container.encodeIfPresent(encryped, forKey: ._payment)
}
@ -337,7 +337,7 @@ final class Tournament : ModelObject, Storable {
let stringCombo: [String] = sequence.map { $0.formatted() }
let joined: String = stringCombo.joined(separator: "")
if let data = joined.data(using: .utf8) {
let encryped: Data = try data.encrypt(pass: Key.pass.rawValue)
let encryped: Data = try data.encrypt(pass: CryptoKey.pass.rawValue)
try container.encode(encryped, forKey: ._isCanceled)
}
}

@ -0,0 +1,49 @@
//
// KeyedEncodingContainer+Extensions.swift
// PadelClub
//
// Created by Laurent Morvillier on 18/09/2024.
//
import Foundation
import LeStorage
extension KeyedDecodingContainer {
func decodeEncrypted(key: Key) throws -> String {
let data = try self.decode(Data.self, forKey: key)
return try data.decryptData(pass: CryptoKey.pass.rawValue)
}
func decodeEncryptedIfPresent(key: Key) throws -> String? {
let data = try self.decodeIfPresent(Data.self, forKey: key)
if let data {
return try data.decryptData(pass: CryptoKey.pass.rawValue)
}
return nil
}
}
extension KeyedEncodingContainer {
mutating func encodeIfPresent<T: Encodable>(_ value: T?, forKey key: Key) throws {
guard let value else {
try self.encodeNil(forKey: key)
return
}
try self.encode(value, forKey: key)
}
mutating func encodeAndEncryptIfPresent(_ value: Data?, forKey key: Key) throws {
guard let value else {
try encodeNil(forKey: key)
return
}
let encryped: Data = try value.encrypt(pass: CryptoKey.pass.rawValue)
try self.encode(encryped, forKey: key)
}
}

@ -7,6 +7,6 @@
import Foundation
enum Key: String {
enum CryptoKey: String {
case pass = "Aa9QDV1G5MP9ijF2FTFasibNbS/Zun4qXrubIL2P+Ik="
}

@ -8,6 +8,7 @@
import SwiftUI
import CoreLocation
import LeStorage
import StoreKit
struct UmpireView: View {
@ -247,18 +248,22 @@ struct AccountRowView: View {
struct ProductIdsView: View {
@State var ids: [String] = []
@State var transactions: [StoreKit.Transaction] = []
var body: some View {
VStack {
List {
LabeledContent("count", value: String(ids.count))
ForEach(self.ids) { id in
Text(id)
LabeledContent("count", value: String(self.transactions.count))
ForEach(self.transactions) { transaction in
if let offerType = transaction.offerType?.rawValue {
LabeledContent(transaction.productID, value: "\(offerType)")
} else {
LabeledContent(transaction.productID, value: "no offer")
}
}
}.onAppear {
Task {
self.ids = await Guard.main.productIds()
self.transactions = Array(Guard.main.purchasedTransactions)
}
}
}

@ -16,7 +16,7 @@ import LeStorage
@Published private(set) var purchasedTransactions = Set<StoreKit.Transaction>()
var currentBestPlan: StoreKit.Transaction? = nil
var currentBestPurchase: Purchase? = nil
var updateListenerTask: Task<Void, Never>? = nil
@ -104,50 +104,64 @@ import LeStorage
func updatePurchasedIdentifiers(_ transaction: StoreKit.Transaction) async {
// Logger.log("\(transaction.productID) > purchase = \(transaction.originalPurchaseDate), exp date= \(transaction.expirationDate), rev date = \(transaction.revocationDate)")
// Logger.log("purchase date = \(transaction.purchaseDate)")
do {
if transaction.revocationDate == nil {
// If the App Store has not revoked the transaction, add it to the list of `purchasedIdentifiers`.
purchasedTransactions.insert(transaction)
try self._addPurchaseIfPossible(transaction: transaction)
// try self._addPurchaseIfPossible(transaction: transaction)
} else {
// If the App Store has revoked this transaction, remove it from the list of `purchasedIdentifiers`.
purchasedTransactions.remove(transaction)
try self._updatePurchaseIfPossible(transaction: transaction)
// try self._updatePurchaseIfPossible(transaction: transaction)
}
try self._updatePurchase(transaction: transaction)
} catch {
Logger.error(error)
}
self._updateBestPlan()
}
fileprivate func _addPurchaseIfPossible(transaction: StoreKit.Transaction) throws {
let purchases = DataStore.shared.purchases
if purchases.hasLoaded {
if self._purchaseById(transaction.originalID) == nil {
let purchase: Purchase = try transaction.purchase()
try purchases.addOrUpdate(instance: purchase)
}
}
}
fileprivate func _updatePurchaseIfPossible(transaction: StoreKit.Transaction) throws {
fileprivate func _updatePurchase(transaction: StoreKit.Transaction) throws {
let purchases = DataStore.shared.purchases
if purchases.hasLoaded {
if let existing: Purchase = self._purchaseById(transaction.originalID) {
existing.revocationDate = transaction.revocationDate
try purchases.addOrUpdate(instance: existing)
}
if let purchase = self._purchaseByTransactionId(transaction.originalID) {
purchase.revocationDate = transaction.revocationDate
purchase.expirationDate = transaction.expirationDate
purchase.purchaseDate = transaction.purchaseDate
try purchases.addOrUpdate(instance: purchase)
} else {
let purchase: Purchase = try transaction.purchase()
try purchases.addOrUpdate(instance: purchase)
}
}
// fileprivate func _addPurchaseIfPossible(transaction: StoreKit.Transaction) throws {
//
// let purchases = DataStore.shared.purchases
//
// if self._purchaseByTransactionId(transaction.originalID) == nil {
// let purchase: Purchase = try transaction.purchase()
// try purchases.addOrUpdate(instance: purchase)
// }
// }
//
// fileprivate func _updatePurchaseIfPossible(transaction: StoreKit.Transaction) throws {
// let purchases = DataStore.shared.purchases
// if let existing: Purchase = self._purchaseByTransactionId(transaction.originalID) {
// existing.revocationDate = transaction.revocationDate
// try purchases.addOrUpdate(instance: existing)
// }
// }
fileprivate func _purchaseById(_ transactionId: UInt64) -> Purchase? {
fileprivate func _purchaseByTransactionId(_ transactionId: UInt64) -> Purchase? {
let purchases = DataStore.shared.purchases
return purchases.first(where: { $0.identifier == transactionId })
return purchases.first(where: { $0.id == transactionId })
}
func processTransactionResult(_ result: VerificationResult<StoreKit.Transaction>) async throws -> StoreKit.Transaction {
@ -162,24 +176,24 @@ import LeStorage
var currentPlan: StoreItem? {
#if DEBUG
return .monthlyUnlimited
return .monthlyUnlimited
#elseif TESTFLIGHT
return .monthlyUnlimited
return .monthlyUnlimited
#else
if let currentBestPlan = self.currentBestPlan, let plan = StoreItem(rawValue: currentBestPlan.productID) {
return plan
}
return nil
if let currentBestPurchase = self.currentBestPurchase, let plan = StoreItem(rawValue: currentBestPurchase.productId) {
return plan
}
return nil
#endif
}
func userFilteredPurchases() -> [StoreKit.Transaction] {
// Logger.log("self.purchasedTransactions = \(self.purchasedTransactions.count)")
Logger.log("self.purchasedTransactions = \(self.purchasedTransactions.count)")
guard let userId = StoreCenter.main.userId, let currentUserUUID: UUID = UUID(uuidString: userId) else {
return []
}
let userTransactions = self.purchasedTransactions.filter { currentUserUUID == $0.appAccountToken || $0.offerType == .promotional }
let userTransactions = self.purchasedTransactions.filter { currentUserUUID == $0.appAccountToken || $0.offerType == .code }
let now: Date = Date()
// print("now = \(now)")
@ -197,17 +211,22 @@ import LeStorage
/// Update best plan by filtering Apple purchases with registered purchases by the user
fileprivate func _updateBestPlan() {
var purchases: [Purchase] = []
// Make sure the purchase has been done with the logged user
let userPurchases = self.userFilteredPurchases()
let userPurchases = self.userFilteredPurchases().compactMap { try? $0.purchase() }
purchases.append(contentsOf: userPurchases)
if let unlimited = userPurchases.first(where: { $0.productID == StoreItem.monthlyUnlimited.rawValue }) {
self.currentBestPlan = unlimited
} else if let fivePerMonth = userPurchases.first(where: { $0.productID == StoreItem.fivePerMonth.rawValue }) {
self.currentBestPlan = fivePerMonth
} else {
self.currentBestPlan = nil
let validPurchases = DataStore.shared.purchases.filter { $0.isValid() }
Logger.log("valid purchases = \(validPurchases.count)")
purchases.append(contentsOf: validPurchases)
if let purchase = purchases.first(where: { $0.productId == StoreItem.monthlyUnlimited.rawValue }) {
self.currentBestPurchase = purchase
} else if let purchase = purchases.first(where: { $0.productId == StoreItem.fivePerMonth.rawValue }) {
self.currentBestPurchase = purchase
}
}
fileprivate func _purchasedTournamentCount() -> Int {
@ -227,7 +246,7 @@ import LeStorage
case .monthlyUnlimited:
return Tournament.TournamentPayment.unlimited
case .fivePerMonth:
if let purchaseDate = self.currentBestPlan?.originalPurchaseDate {
if let purchaseDate = self.currentBestPurchase?.purchaseDate {
let tournaments = DataStore.shared.tournaments.filter { $0.creationDate > purchaseDate && $0.payment == .subscriptionUnit && $0.isCanceled == false }
if tournaments.count < StoreItem.five {
return Tournament.TournamentPayment.subscriptionUnit
@ -282,11 +301,14 @@ fileprivate extension StoreKit.Transaction {
guard let userId = StoreCenter.main.userId else {
throw StoreError.missingUserId
}
return Purchase(user: userId,
identifier: self.originalID,
transactionId: self.originalID,
purchaseDate: self.purchaseDate,
productId: self.productID,
quantity: self.purchasedQuantity)
quantity: self.purchasedQuantity,
revocationDate: self.revocationDate,
expirationDate: self.expirationDate)
}

@ -14,21 +14,66 @@ class Purchase: ModelObject, Storable {
static func filterByStoreIdentifier() -> Bool { return false }
static var relationshipNames: [String] = []
var id: String = Store.randomId()
var id: UInt64
var user: String
var identifier: UInt64
var purchaseDate: Date
var productId: String
var quantity: Int?
var revocationDate: Date? = nil
var expirationDate: Date? = nil
init(user: String, identifier: UInt64, purchaseDate: Date, productId: String, quantity: Int? = nil, revocationDate: Date? = nil) {
init(user: String, transactionId: UInt64, purchaseDate: Date, productId: String, quantity: Int? = nil, revocationDate: Date? = nil, expirationDate: Date? = nil) {
self.id = transactionId
self.user = user
self.identifier = identifier
self.purchaseDate = purchaseDate
self.productId = productId
self.quantity = quantity
self.revocationDate = revocationDate
self.expirationDate = expirationDate
}
enum CodingKeys: String, CodingKey, CaseIterable {
case id
case user
case purchaseDate
case productId
case quantity
case revocationDate
case expirationDate
}
func isValid() -> Bool {
guard self.revocationDate == nil else {
return false
}
guard let expiration = self.expirationDate else {
return false
}
return expiration > Date()
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.id, forKey: .id)
try container.encodeAndEncryptIfPresent(self.user.data(using: .utf8), forKey: .user)
try container.encode(self.purchaseDate, forKey: .purchaseDate)
try container.encode(self.productId, forKey: .productId)
try container.encodeIfPresent(self.quantity, forKey: .quantity)
try container.encodeIfPresent(self.revocationDate, forKey: .revocationDate)
try container.encodeIfPresent(self.expirationDate, forKey: .expirationDate)
}
required init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decode(UInt64.self, forKey: .id)
self.user = try container.decodeEncrypted(key: .user)
self.purchaseDate = try container.decode(Date.self, forKey: .purchaseDate)
self.productId = try container.decode(String.self, forKey: .productId)
self.quantity = try container.decode(Int.self, forKey: .quantity)
self.revocationDate = try container.decodeIfPresent(Date.self, forKey: .revocationDate)
self.expirationDate = try container.decodeIfPresent(Date.self, forKey: .expirationDate)
}
}

@ -15,7 +15,7 @@ final class ServerDataTests: XCTestCase {
let password: String = "MyPass1234--"
override func setUpWithError() throws {
Store.main.synchronizationApiURL = "http://127.0.0.1:8000/roads/"
StoreCenter.main.synchronizationApiURL = "http://127.0.0.1:8000/roads/"
Task {
do {
try await self.login()
@ -31,7 +31,7 @@ final class ServerDataTests: XCTestCase {
func login() async throws {
// print("LOGIN!")
let _: User = try await Store.main.service().login(username: self.username, password: self.password)
let _: User = try await StoreCenter.main.service().login(username: self.username, password: self.password)
}
func testClub() async throws {
@ -47,7 +47,7 @@ final class ServerDataTests: XCTestCase {
club.phone = "061234567890"
club.courtCount = 3
let inserted_club: Club = try await Store.main.service().post(club)
let inserted_club: Club = try await StoreCenter.main.service().post(club)
assert(inserted_club.name == club.name)
assert(inserted_club.acronym == club.acronym)
assert(inserted_club.zipCode == club.zipCode)
@ -60,13 +60,13 @@ final class ServerDataTests: XCTestCase {
inserted_club.phone = "123456"
let updated_club: Club = try await Store.main.service().put(inserted_club)
let updated_club: Club = try await StoreCenter.main.service().put(inserted_club)
assert(updated_club.phone == inserted_club.phone)
}
func testLogin() async throws {
let user: User = try await Store.main.service().login(username: self.username, password: self.password)
let user: User = try await StoreCenter.main.service().login(username: self.username, password: self.password)
assert(user.username == "test")
}
@ -77,14 +77,14 @@ final class ServerDataTests: XCTestCase {
return
}
let clubs: [Club] = try await Store.main.service().get()
let clubs: [Club] = try await StoreCenter.main.service().get()
guard let clubId = clubs.first?.id else {
assertionFailure("missing club in database")
return
}
let event = Event(creator: userId, club: clubId, name: "Roland Garros", tenupId: "abc")
let e = try await Store.main.service().post(event)
let e = try await StoreCenter.main.service().post(event)
assert(e.name == event.name)
assert(e.tenupId == event.tenupId)
@ -93,14 +93,14 @@ final class ServerDataTests: XCTestCase {
func testTournament() async throws {
let event: [Event] = try await Store.main.service().get()
let event: [Event] = try await StoreCenter.main.service().get()
guard let eventId = event.first?.id else {
assertionFailure("missing event in database")
return
}
let tournament = Tournament(event: eventId, name: "RG Homme", startDate: Date(), endDate: nil, creationDate: Date(), isPrivate: false, groupStageFormat: MatchFormat.megaTie, roundFormat: MatchFormat.nineGames, loserRoundFormat: MatchFormat.nineGamesDecisivePoint, groupStageSortMode: GroupStageOrderingMode.snake, groupStageCount: 2, rankSourceDate: Date(), dayDuration: 5, teamCount: 3, teamSorting: TeamSortingType.rank, federalCategory: TournamentCategory.mix, federalLevelCategory: TournamentLevel.p1000, federalAgeCategory: FederalTournamentAge.a45, closedRegistrationDate: Date(), groupStageAdditionalQualified: 4, courtCount: 9, prioritizeClubMembers: true, qualifiedPerGroupStage: 1, teamsPerGroupStage: 2, entryFee: 30.0, additionalEstimationDuration: 5, isDeleted: true, publishTeams: true, publishSummons: true, publishGroupStages: true, publishBrackets: true, shouldVerifyBracket: true, shouldVerifyGroupStage: true, hideTeamsWeight: true, publishTournament: true, hidePointsEarned: true, publishRankings: true)
let t = try await Store.main.service().post(tournament)
let t = try await StoreCenter.main.service().post(tournament)
assert(t.event == tournament.event)
assert(t.name == tournament.name)
@ -143,14 +143,14 @@ final class ServerDataTests: XCTestCase {
func testGroupStage() async throws {
let tournament: [Tournament] = try await Store.main.service().get()
let tournament: [Tournament] = try await StoreCenter.main.service().get()
guard let tournamentId = tournament.first?.id else {
assertionFailure("missing tournament in database")
return
}
let groupStage = GroupStage(tournament: tournamentId, index: 2, size: 3, matchFormat: MatchFormat.nineGames, startDate: Date(), name: "Yeah!")
let gs: GroupStage = try await Store.main.service().post(groupStage)
let gs: GroupStage = try await StoreCenter.main.service().post(groupStage)
assert(gs.tournament == groupStage.tournament)
assert(gs.name == groupStage.name)
@ -163,16 +163,16 @@ final class ServerDataTests: XCTestCase {
func testRound() async throws {
let tournament: [Tournament] = try await Store.main.service().get()
let tournament: [Tournament] = try await StoreCenter.main.service().get()
guard let tournamentId = tournament.first?.id else {
assertionFailure("missing tournament in database")
return
}
let rounds: [Round] = try await Store.main.service().get()
let rounds: [Round] = try await StoreCenter.main.service().get()
let parentRoundId = rounds.first?.id
let round = Round(tournament: tournamentId, index: 1, parent: parentRoundId, matchFormat: MatchFormat.nineGames, startDate: Date())
let r: Round = try await Store.main.service().post(round)
let r: Round = try await StoreCenter.main.service().post(round)
assert(r.tournament == round.tournament)
assert(r.index == round.index)
@ -184,12 +184,12 @@ final class ServerDataTests: XCTestCase {
func testTeamRegistration() async throws {
let tournament: [Tournament] = try await Store.main.service().get()
let tournament: [Tournament] = try await StoreCenter.main.service().get()
guard let tournamentId = tournament.first?.id else {
assertionFailure("missing tournament in database")
return
}
let groupStages: [GroupStage] = try await Store.main.service().get()
let groupStages: [GroupStage] = try await StoreCenter.main.service().get()
guard let groupStageId = groupStages.first?.id else {
assertionFailure("missing groupStage in database")
return
@ -197,7 +197,7 @@ final class ServerDataTests: XCTestCase {
let teamRegistration = TeamRegistration(tournament: tournamentId, groupStage: groupStageId, registrationDate: Date(), callDate: Date(), bracketPosition: 1, groupStagePosition: 2, comment: "comment", source: "source", sourceValue: "source V", logo: "logo", name: "Stax", walkOut: true, wildCardBracket: true, wildCardGroupStage: true, weight: 1, lockedWeight: 11, confirmationDate: Date(), qualified: true)
let tr: TeamRegistration = try await Store.main.service().post(teamRegistration)
let tr: TeamRegistration = try await StoreCenter.main.service().post(teamRegistration)
assert(tr.tournament == teamRegistration.tournament)
assert(tr.groupStage == teamRegistration.groupStage)
@ -222,14 +222,14 @@ final class ServerDataTests: XCTestCase {
func testPlayerRegistration() async throws {
let teamRegistrations: [TeamRegistration] = try await Store.main.service().get()
let teamRegistrations: [TeamRegistration] = try await StoreCenter.main.service().get()
guard let teamRegistrationId = teamRegistrations.first?.id else {
assertionFailure("missing teamRegistrations in database")
return
}
let playerRegistration = PlayerRegistration(teamRegistration: teamRegistrationId, firstName: "juan", lastName: "lebron", licenceId: "123", rank: 11, paymentType: PlayerRegistration.PlayerPaymentType.cash, sex: PlayerRegistration.PlayerSexType.male, tournamentPlayed: 2, points: 33, clubName: "le club", ligueName: "la league", assimilation: "ass", phoneNumber: "123123", email: "email@email.com", birthdate: nil, computedRank: 222, source: PlayerRegistration.PlayerDataSource.frenchFederation, hasArrived: true)
let pr: PlayerRegistration = try await Store.main.service().post(playerRegistration)
let pr: PlayerRegistration = try await StoreCenter.main.service().post(playerRegistration)
assert(pr.teamRegistration == playerRegistration.teamRegistration)
assert(pr.firstName == playerRegistration.firstName)
@ -253,16 +253,16 @@ final class ServerDataTests: XCTestCase {
func testMatch() async throws {
let teamRegistrations: [TeamRegistration] = try await Store.main.service().get()
let teamRegistrations: [TeamRegistration] = try await StoreCenter.main.service().get()
guard let teamRegistrationId = teamRegistrations.first?.id else {
assertionFailure("missing teamRegistrations in database")
return
}
let rounds: [Round] = try await Store.main.service().get()
let rounds: [Round] = try await StoreCenter.main.service().get()
let parentRoundId = rounds.first?.id
let match: Match = Match(round: parentRoundId, groupStage: nil, startDate: Date(), endDate: Date(), index: 2, matchFormat: MatchFormat.twoSets, servingTeamId: teamRegistrationId, winningTeamId: teamRegistrationId, losingTeamId: teamRegistrationId, disabled: true, courtIndex: 1, confirmed: true)
let m: Match = try await Store.main.service().post(match)
let m: Match = try await StoreCenter.main.service().post(match)
assert(m.round == match.round)
assert(m.groupStage == match.groupStage)
@ -281,18 +281,18 @@ final class ServerDataTests: XCTestCase {
func testTeamScore() async throws {
let matches: [Match] = try await Store.main.service().get()
let matches: [Match] = try await StoreCenter.main.service().get()
guard let matchId = matches.first?.id else {
assertionFailure("missing match in database")
return
}
let teamRegistrations: [TeamRegistration] = try await Store.main.service().get()
let teamRegistrations: [TeamRegistration] = try await StoreCenter.main.service().get()
guard let teamRegistrationId = teamRegistrations.first?.id else {
assertionFailure("missing teamRegistrations in database")
return
}
let teamScore = TeamScore(match: matchId, teamRegistration: teamRegistrationId, score: "6/6", walkOut: 1, luckyLoser: 1)
let ts: TeamScore = try await Store.main.service().post(teamScore)
let ts: TeamScore = try await StoreCenter.main.service().post(teamScore)
assert(ts.match == teamScore.match)
assert(ts.teamRegistration == teamScore.teamRegistration)
@ -304,14 +304,14 @@ final class ServerDataTests: XCTestCase {
func testCourt() async throws {
let clubs: [Club] = try await Store.main.service().get()
let clubs: [Club] = try await StoreCenter.main.service().get()
guard let clubId = clubs.first?.id else {
assertionFailure("missing club in database")
return
}
let court = Court(index: 1, club: clubId, name: "Philippe Chatrier", exitAllowed: true, indoor: true)
let c: Court = try await Store.main.service().post(court)
let c: Court = try await StoreCenter.main.service().post(court)
assert(c.club == court.club)
assert(c.name == court.name)
@ -323,14 +323,14 @@ final class ServerDataTests: XCTestCase {
func testDateInterval() async throws {
let event: [Event] = try await Store.main.service().get()
let event: [Event] = try await StoreCenter.main.service().get()
guard let eventId = event.first?.id else {
assertionFailure("missing event in database")
return
}
let dateInterval = DateInterval(event: eventId, courtIndex: 1, startDate: Date(), endDate: Date())
let di: PadelClub.DateInterval = try await Store.main.service().post(dateInterval)
let di: PadelClub.DateInterval = try await StoreCenter.main.service().post(dateInterval)
assert(di.event == dateInterval.event)
assert(di.courtIndex == dateInterval.courtIndex)
@ -346,16 +346,19 @@ final class ServerDataTests: XCTestCase {
return
}
let purchase: Purchase = Purchase(user: userId, identifier: 1234, purchaseDate: Date(), productId: "productId", quantity: 3, revocationDate: Date())
let p: Purchase = try await Store.main.service().post(purchase)
let transactionId = UInt64.random(in: 0...100000)
let quantity = Int.random(in: 0...10)
let purchase: Purchase = Purchase(user: userId, transactionId: transactionId, purchaseDate: Date(), productId: "app.padelclub.productId", quantity: quantity, revocationDate: Date(), expirationDate: Date())
let p: Purchase = try await StoreCenter.main.service().post(purchase)
assert(p.id == purchase.id)
assert(p.identifier == purchase.identifier)
assert(p.user == purchase.user)
assert(p.productId == purchase.productId)
assert(p.purchaseDate.formatted() == purchase.purchaseDate.formatted())
assert(p.quantity == purchase.quantity)
assert(p.user == purchase.user)
assert(p.revocationDate?.formatted() == purchase.revocationDate?.formatted())
assert(p.expirationDate?.formatted() == purchase.expirationDate?.formatted())
}

@ -16,8 +16,8 @@ final class TokenExemptionTests: XCTestCase {
let password: String = "MyPass1234--"
override func setUpWithError() throws {
Store.main.synchronizationApiURL = "http://127.0.0.1:8000/roads/"
Store.main.disconnect()
StoreCenter.main.synchronizationApiURL = "http://127.0.0.1:8000/roads/"
StoreCenter.main.disconnect()
}
override func tearDownWithError() throws {
@ -27,15 +27,15 @@ final class TokenExemptionTests: XCTestCase {
func testClubCreation() async throws {
let user = try await self.login()
Store.main.disconnect()
StoreCenter.main.disconnect()
let club: Club = Club(name: "mon club 2", acronym: "MC", phone: "132", code: "456", address: "l'adresse", city: "la ville", zipCode: "13131", latitude: 13.11111, longitude: 1.121212)
let c = try await Store.main.service().post(club)
let c = try await StoreCenter.main.service().post(club)
assert(c.id == club.id)
do {
_ = try await Store.main.service().put(club)
_ = try await StoreCenter.main.service().put(club)
assertionFailure("the request above should fail without an authenticated user")
} catch {
// good stuff
@ -44,13 +44,13 @@ final class TokenExemptionTests: XCTestCase {
let _ = try await self.login()
club.creator = user.id
let uc = try await Store.main.service().put(club)
let uc = try await StoreCenter.main.service().put(club)
assert(uc.creator == user.id)
}
func login() async throws -> User {
let user: User = try await Store.main.service().login(username: self.username, password: self.password)
let user: User = try await StoreCenter.main.service().login(username: self.username, password: self.password)
return user
}

@ -15,7 +15,7 @@ final class UserDataTests: XCTestCase {
let password: String = "MyPass1234--"
override func setUpWithError() throws {
Store.main.synchronizationApiURL = "http://127.0.0.1:8000/roads/"
StoreCenter.main.synchronizationApiURL = "http://127.0.0.1:8000/roads/"
}
override func tearDownWithError() throws {
@ -24,8 +24,8 @@ final class UserDataTests: XCTestCase {
func testUserCreation() async throws {
let userCreationForm = UserCreationForm(user: User.placeHolder(), username: self.username, password: self.password, firstName: "jean", lastName: "coco", email: "laurent@staxriver.com", phone: "0123", country: "France")
let user: User = try await Store.main.service().createAccount(user: userCreationForm)
let userCreationForm = UserCreationForm(user: User.placeHolder(), username: self.username, password: self.password, firstName: "jean", lastName: "coco", email: "test@lolomo.com", phone: "0123", country: "France")
let user: User = try await StoreCenter.main.service().createAccount(user: userCreationForm)
assert(user.username == userCreationForm.username)
assert(user.firstName == userCreationForm.firstName)
@ -37,7 +37,7 @@ final class UserDataTests: XCTestCase {
}
func login() async throws -> User {
let user: User = try await Store.main.service().login(username: self.username, password: self.password)
let user: User = try await StoreCenter.main.service().login(username: self.username, password: self.password)
return user
}
@ -56,7 +56,7 @@ final class UserDataTests: XCTestCase {
user.groupStageMatchFormatPreference = MatchFormat.twoSets
user.loserBracketMatchFormatPreference = MatchFormat.twoSetsOfFourGames
let uu = try await Store.main.service().put(user)
let uu = try await StoreCenter.main.service().put(user)
assert(uu.summonsMessageBody == user.summonsMessageBody)
assert(uu.summonsMessageSignature == user.summonsMessageSignature)

Loading…
Cancel
Save