From b3d45de55c5d62e9adece8b8225702295f1f5a67 Mon Sep 17 00:00:00 2001 From: Hadi Sharghi Date: Thu, 29 Apr 2021 18:48:17 +0430 Subject: [PATCH 1/9] wip --- .../VaporWallet/Migrations/CreateWallet.swift | 15 ++++-- .../VaporWallet/Models/Entities/Wallet.swift | 11 +++-- Sources/VaporWallet/WalletRepository.swift | 2 +- Tests/VaporWalletTests/Model+Test.swift | 48 +++++++++++++++++++ Tests/VaporWalletTests/VaporWalletTests.swift | 33 ++++++++++++- 5 files changed, 101 insertions(+), 8 deletions(-) diff --git a/Sources/VaporWallet/Migrations/CreateWallet.swift b/Sources/VaporWallet/Migrations/CreateWallet.swift index ef9e52d..80d2d18 100644 --- a/Sources/VaporWallet/Migrations/CreateWallet.swift +++ b/Sources/VaporWallet/Migrations/CreateWallet.swift @@ -1,6 +1,7 @@ import Fluent +import SQLKit -public struct CreateWallet: Migration { +public struct CreateWallet: Migration { private var idKey: String public init(foreignKeyColumnName idKey: String = "id") { self.idKey = idKey @@ -10,13 +11,21 @@ public struct CreateWallet: Migration { return database.schema(Wallet.schema) .id() .field("name", .string, .required) - .field("owner_id", .uuid, .required, .references(M.schema, .init(stringLiteral: self.idKey), onDelete: .cascade)) + .field("owner_type", .string, .required) + .field("owner_id", .uuid, .required) .field("balance", .int, .required) .field("decimal_places", .uint8, .required) .field("created_at", .datetime, .required) .field("updated_at", .datetime, .required) .field("deleted_at", .datetime) - .create() + .create().flatMap { _ in + let sqlDB = (database as! SQLDatabase) + return sqlDB + .create(index: "type_idx") + .on(Wallet.schema) + .column("owner_type") + .run() + } } public func revert(on database: Database) -> EventLoopFuture { diff --git a/Sources/VaporWallet/Models/Entities/Wallet.swift b/Sources/VaporWallet/Models/Entities/Wallet.swift index 58c551d..2b9b347 100644 --- a/Sources/VaporWallet/Models/Entities/Wallet.swift +++ b/Sources/VaporWallet/Models/Entities/Wallet.swift @@ -14,13 +14,16 @@ public final class Wallet: Model, Content { @ID(key: .id) public var id: UUID? + + @Field(key: "owner_id") + var owner: UUID + + @Field(key: "owner_type") + var ownerType: String @Field(key: "name") var name: String - @Field(key: "owner_id") - var owner: UUID - @Field(key: "balance") var balance: Int @@ -43,6 +46,7 @@ public final class Wallet: Model, Content { init( id: UUID? = nil, + ownerType: String, ownerID: UUID, name: String = WalletType.default.value, balance: Int = 0, @@ -52,6 +56,7 @@ public final class Wallet: Model, Content { deletedAt: Date? = nil ) { self.id = id + self.ownerType = ownerType self.owner = ownerID self.name = name self.balance = balance diff --git a/Sources/VaporWallet/WalletRepository.swift b/Sources/VaporWallet/WalletRepository.swift index 0ba983b..a69054f 100644 --- a/Sources/VaporWallet/WalletRepository.swift +++ b/Sources/VaporWallet/WalletRepository.swift @@ -31,7 +31,7 @@ public class WalletsRepository { extension WalletsRepository { public func create(type name: WalletType = .default, decimalPlaces: UInt8 = 2) -> EventLoopFuture { - let wallet: Wallet = Wallet(ownerID: self.id, name: name.value, decimalPlaces: decimalPlaces) + let wallet: Wallet = Wallet(ownerType: String(describing: self), ownerID: self.id, name: name.value, decimalPlaces: decimalPlaces) return wallet.save(on: db) } diff --git a/Tests/VaporWalletTests/Model+Test.swift b/Tests/VaporWalletTests/Model+Test.swift index 69d3591..b41d82e 100644 --- a/Tests/VaporWalletTests/Model+Test.swift +++ b/Tests/VaporWalletTests/Model+Test.swift @@ -56,3 +56,51 @@ struct CreateUser: Migration { } + +final class Game: Model { + static let schema = "games" + + @ID(key: .id) + var id: UUID? + + @Field(key: "name") + var name: String + + init() {} + + init( + id: UUID? = nil, + name: String + ) { + self.id = id + self.name = name + } + + public static func create(name: String = "game1", on database: Database) throws -> Game { + let game = Game(name: name) + try game.save(on: database).wait() + return game + } +} + +extension Game: HasWallet { + + static let idKey = \Game.$id + +} + + +struct CreateGame: Migration { + func prepare(on database: Database) -> EventLoopFuture { + return database.schema(Game.schema) + .id() + .field("name", .string, .required) + .create() + } + + func revert(on database: Database) -> EventLoopFuture { + return database.schema(Game.schema).delete() + } +} + + diff --git a/Tests/VaporWalletTests/VaporWalletTests.swift b/Tests/VaporWalletTests/VaporWalletTests.swift index ceee75f..fdcf1e6 100644 --- a/Tests/VaporWalletTests/VaporWalletTests.swift +++ b/Tests/VaporWalletTests/VaporWalletTests.swift @@ -52,6 +52,11 @@ class VaporWalletTests: XCTestCase { XCTAssert(user.username == "user1") } + func testAddGame() throws { + let game = try Game.create(name: "new_game", on: app.db) + XCTAssert(game.name == "new_game") + } + func testUserHasNoDefaultWallet() throws { let userWithNoWallet = try User.create(on: app.db) XCTAssertThrowsError(try userWithNoWallet.walletsRepository(on: app.db).default().wait(), "expected throw") { (error) in @@ -318,6 +323,31 @@ class VaporWalletTests: XCTestCase { } + func testMultiModelWallet() throws { + app.databases.middleware.use(WalletMiddleware()) + app.databases.middleware.use(WalletMiddleware()) + app.databases.middleware.use(WalletTransactionMiddleware()) + + let user = try User.create(username: "user1", on: app.db) + let game = Game(id: user.id, name: "game1") + try game.save(on: app.db).wait() + + let repo1 = user.walletsRepository(on: app.db) + let repo2 = game.walletsRepository(on: app.db) + + try repo1.deposit(amount: 100).wait() + try repo2.deposit(amount: 500).wait() + + var balance1 = try repo1.balance().wait() + var balance2 = try repo2.balance().wait() + + XCTAssertEqual(balance1, 100) + XCTAssertEqual(balance2, 500) + + + + } + @@ -331,7 +361,8 @@ class VaporWalletTests: XCTestCase { private func migrations(_ app: Application) throws { // Initial Migrations app.migrations.add(CreateUser()) - app.migrations.add(CreateWallet()) + app.migrations.add(CreateGame()) + app.migrations.add(CreateWallet()) app.migrations.add(CreateWalletTransaction()) } } From 78aac953b0e1beca04b1fe04bd6c74fb6e47e453 Mon Sep 17 00:00:00 2001 From: Hadi Sharghi Date: Tue, 13 Jun 2023 19:59:07 +0330 Subject: [PATCH 2/9] asyn-await wip --- Sources/VaporWallet/HasWallet.swift | 11 + .../Middlewares/WalletModelMiddleware.swift | 12 + .../WalletTransactionModelMiddleware.swift | 11 + .../VaporWallet/Migrations/CreateWallet.swift | 34 +- .../Migrations/CreateWalletTransaction.swift | 26 ++ .../Models/Entities/WalletTransaction.swift | 5 + Sources/VaporWallet/WalletRepository.swift | 230 ++++++++++- Tests/VaporWalletTests/Model+Test.swift | 31 +- .../VaporWalletAsyncTests.swift | 375 ++++++++++++++++++ Tests/VaporWalletTests/VaporWalletTests.swift | 4 +- 10 files changed, 715 insertions(+), 24 deletions(-) create mode 100644 Tests/VaporWalletTests/VaporWalletAsyncTests.swift diff --git a/Sources/VaporWallet/HasWallet.swift b/Sources/VaporWallet/HasWallet.swift index 3e1018b..26d2d24 100644 --- a/Sources/VaporWallet/HasWallet.swift +++ b/Sources/VaporWallet/HasWallet.swift @@ -26,6 +26,17 @@ extension HasWallet { } extension Wallet { + public func refreshBalanceAsync(on db: Database) async throws -> Double { + let balance = try await self.$transactions + .query(on: db) + .filter(\.$confirmed == true) + .sum(\.$amount) + .get() + self.balance = balance ?? 0 + try await self.update(on: db) + return Double(self.balance) + } + public func refreshBalance(on db: Database) -> EventLoopFuture { self.$transactions .query(on: db) diff --git a/Sources/VaporWallet/Middlewares/WalletModelMiddleware.swift b/Sources/VaporWallet/Middlewares/WalletModelMiddleware.swift index d6346a1..d4dbe8c 100644 --- a/Sources/VaporWallet/Middlewares/WalletModelMiddleware.swift +++ b/Sources/VaporWallet/Middlewares/WalletModelMiddleware.swift @@ -22,3 +22,15 @@ public struct WalletMiddleware: ModelMiddleware { } } } + +public struct AsyncWalletMiddleware: AsyncModelMiddleware { + + public init() {} + + public func create(model: M, on db: Database, next: AnyAsyncModelResponder) async throws { + try await next.create(model, on: db) + db.logger.log(level: .info, "default wallet for user \(model._$idKey) has been created") + let repo = model.walletsRepository(on: db) + try await repo.createAsyc() + } +} diff --git a/Sources/VaporWallet/Middlewares/WalletTransactionModelMiddleware.swift b/Sources/VaporWallet/Middlewares/WalletTransactionModelMiddleware.swift index d8bf1e7..4d32a18 100644 --- a/Sources/VaporWallet/Middlewares/WalletTransactionModelMiddleware.swift +++ b/Sources/VaporWallet/Middlewares/WalletTransactionModelMiddleware.swift @@ -24,3 +24,14 @@ public struct WalletTransactionMiddleware: ModelMiddleware { } } +public struct AsyncWalletTransactionMiddleware: AsyncModelMiddleware { + + public init() {} + + public func create(model: WalletTransaction, on db: Database, next: AnyAsyncModelResponder) async throws { + try await next.create(model, on: db) + let wallet = try await model.$wallet.get(on: db) + _ = try await wallet.refreshBalanceAsync(on: db) + } +} + diff --git a/Sources/VaporWallet/Migrations/CreateWallet.swift b/Sources/VaporWallet/Migrations/CreateWallet.swift index 80d2d18..21e244c 100644 --- a/Sources/VaporWallet/Migrations/CreateWallet.swift +++ b/Sources/VaporWallet/Migrations/CreateWallet.swift @@ -21,7 +21,7 @@ public struct CreateWallet: Migration { .create().flatMap { _ in let sqlDB = (database as! SQLDatabase) return sqlDB - .create(index: "type_idx") + .create(index: "type_idx") .on(Wallet.schema) .column("owner_type") .run() @@ -32,3 +32,35 @@ public struct CreateWallet: Migration { return database.schema(Wallet.schema).delete() } } + +public struct CreateWalletAsync: AsyncMigration { + private var idKey: String + public init(foreignKeyColumnName idKey: String = "id") { + self.idKey = idKey + } + + public func prepare(on database: Database) async throws { + try await database.schema(Wallet.schema) + .id() + .field("name", .string, .required) + .field("owner_type", .string, .required) + .field("owner_id", .uuid, .required) + .field("balance", .int, .required) + .field("decimal_places", .uint8, .required) + .field("created_at", .datetime, .required) + .field("updated_at", .datetime, .required) + .field("deleted_at", .datetime) + .create() + let sqlDB = (database as! SQLDatabase) + try await sqlDB + .create(index: "type_idx") + .on(Wallet.schema) + .column("owner_type") + .run() + } + + + public func revert(on database: Database) async throws { + try await database.schema(Wallet.schema).delete() + } +} diff --git a/Sources/VaporWallet/Migrations/CreateWalletTransaction.swift b/Sources/VaporWallet/Migrations/CreateWalletTransaction.swift index 328ce78..850c17b 100644 --- a/Sources/VaporWallet/Migrations/CreateWalletTransaction.swift +++ b/Sources/VaporWallet/Migrations/CreateWalletTransaction.swift @@ -25,3 +25,29 @@ public struct CreateWalletTransaction: Migration { } } + +public struct CreateWalletTransactionAsync: AsyncMigration { + public init() { } + + public func prepare(on database: Database) async throws { + let transactionType = try await database.enum("type").case("deposit").case("withdraw").create() + + try await database.schema(WalletTransaction.schema) + .id() + .field("wallet_id", .uuid, .required, .references(Wallet.schema, "id", onDelete: .cascade)) + .field("type", transactionType, .required) + .field("amount", .int, .required) + .field("confirmed", .bool, .required) + .field("meta", .json) + .field("created_at", .datetime, .required) + .field("updated_at", .datetime, .required) + .create() + } + + + public func revert(on database: Database) async throws { + let _ = try await database.enum("type").deleteCase("deposit").deleteCase("withdraw").update() + try await database.schema(WalletTransaction.schema).delete() + } + +} diff --git a/Sources/VaporWallet/Models/Entities/WalletTransaction.swift b/Sources/VaporWallet/Models/Entities/WalletTransaction.swift index 387303f..b7eefe1 100644 --- a/Sources/VaporWallet/Models/Entities/WalletTransaction.swift +++ b/Sources/VaporWallet/Models/Entities/WalletTransaction.swift @@ -77,5 +77,10 @@ extension WalletTransaction { return self.update(on: db) } + public func confirmAsync(on db: Database) async throws { + self.confirmed = true + try await self.update(on: db) + } + } diff --git a/Sources/VaporWallet/WalletRepository.swift b/Sources/VaporWallet/WalletRepository.swift index a69054f..37c42bf 100644 --- a/Sources/VaporWallet/WalletRepository.swift +++ b/Sources/VaporWallet/WalletRepository.swift @@ -30,6 +30,54 @@ public class WalletsRepository { /// extension WalletsRepository { + public func createAsyc(type name: WalletType = .default, decimalPlaces: UInt8 = 2) async throws { + let wallet: Wallet = Wallet(ownerType: String(describing: self), ownerID: self.id, name: name.value, decimalPlaces: decimalPlaces) + try await wallet.save(on: db) + } + + public func allAsync() async throws -> [Wallet] { + return try await Wallet + .query(on: self.db) + .filter(\.$owner == self.id) + .all() + } + + public func getAsync(type name: WalletType) async throws -> Wallet { + let wallet = try await Wallet.query(on: db) + .filter(\.$owner == self.id) + .filter(\.$name == name.value) + .first() + guard let wallet = wallet else { + throw WalletError.walletNotFound(name: name.value) + } + return wallet + } + + public func defaultAsync() async throws -> Wallet { + return try await getAsync(type: .default) + } + + public func balanceAsync(type name: WalletType = .default, withUnconfirmed: Bool = false, asDecimal: Bool = false) async throws -> Double { + let wallet = try await getAsync(type: name) + if withUnconfirmed { + let intBalance = try await wallet.$transactions + .query(on: self.db) + .sum(\.$amount) + .get() + + let balance = intBalance == nil ? 0.0 : Double(intBalance!) + + return asDecimal ? balance.toDecimal(with: wallet.decimalPlaces) : balance + } + return asDecimal ? Double(wallet.balance).toDecimal(with: wallet.decimalPlaces) : Double(wallet.balance) + } + + public func refreshBalanceAsync(of walletType: WalletType = .default) async throws -> Double { + let wallet = try await getAsync(type: walletType) + return try await wallet.refreshBalanceAsync(on: self.db) + } + + public func create(type name: WalletType = .default, decimalPlaces: UInt8 = 2) -> EventLoopFuture { let wallet: Wallet = Wallet(ownerType: String(describing: self), ownerID: self.id, name: name.value, decimalPlaces: decimalPlaces) return wallet.save(on: db) @@ -84,6 +132,64 @@ extension WalletsRepository { /// extension WalletsRepository { + public func canWithdrawAsync(from: WalletType = .default, amount: Int) async throws -> Bool { + let wallet = try await getAsync(type: from) + return try await self._canWithdrawAsync(from: wallet, amount: amount) + } + + public func withdrawAsync(from: WalletType = .default, amount: Double, meta: [String: String]? = nil) async throws { + let wallet = try await getAsync(type: from) + let intAmount = Int(amount * pow(10, Double(wallet.decimalPlaces))) + guard try await canWithdrawAsync(from: from, amount: intAmount) else { + throw WalletError.insufficientBalance + } + try await self._withdrawAsync(on: self.db, from: wallet, amount: intAmount, meta: meta) + } + + + public func withdrawAsync(from: WalletType = .default, amount: Int, meta: [String: String]? = nil) async throws { + guard try await canWithdrawAsync(from: from, amount: amount) else { + throw WalletError.insufficientBalance + } + let wallet = try await getAsync(type: from) + try await self._withdrawAsync(on: self.db, from: wallet, amount: amount, meta: meta) + } + + + public func depositAsync(to: WalletType = .default, amount: Double, confirmed: Bool = true, meta: [String: String]? = nil) async throws { + let wallet = try await getAsync(type: to) + let intAmount = Int(amount * pow(10, Double(wallet.decimalPlaces))) + try await self._depositAsync(on: self.db, to: wallet, amount: intAmount, meta: meta) + } + + public func depositAsync(to: WalletType = .default, amount: Int, confirmed: Bool = true, meta: [String: String]? = nil) async throws { + let wallet = try await getAsync(type: to) + try await self._depositAsync(on: self.db, to: wallet, amount: amount, meta: meta) + } + + + public func transaferAsync(from: Wallet, to: Wallet, amount: Int, meta: [String: String]? = nil) async throws { + try await self._transferAsync(from: from, to: to, amount: amount, meta: meta) + } + + public func transaferAsync(from: WalletType, to: Wallet, amount: Int, meta: [String: String]? = nil) async throws { + let fromWallet = try await getAsync(type: from) + try await self._transferAsync(from: fromWallet, to: to, amount: amount, meta: meta) + } + + public func transaferAsync(from: Wallet, to: WalletType, amount: Int, meta: [String: String]? = nil) async throws { + let toWallet = try await getAsync(type: to) + try await self._transferAsync(from: from, to: toWallet, amount: amount, meta: meta) + } + + public func transaferAsync(from: WalletType, to: WalletType, amount: Int, meta: [String: String]? = nil) async throws { + let fromWallet = try await getAsync(type: from) + let toWallet = try await getAsync(type: to) + try await self._transferAsync(from: fromWallet, to: toWallet, amount: amount, meta: meta) + } + + + public func canWithdraw(from: WalletType = .default, amount: Int) -> EventLoopFuture { get(type: from).flatMap { self._canWithdraw(from: $0, amount: amount) } } @@ -94,18 +200,18 @@ extension WalletsRepository { return self._withdraw(on: self.db, from: wallet, amount: intAmount, meta: meta) } } - + public func withdraw(from: WalletType = .default, amount: Int, meta: [String: String]? = nil) -> EventLoopFuture { canWithdraw(from: from, amount: amount) .guard({ $0 == true }, else: WalletError.insufficientBalance) - .flatMap { _ in - self.get(type: from).flatMap { wallet -> EventLoopFuture in - self._withdraw(on: self.db, from: wallet, amount: amount, meta: meta) + .flatMap { _ in + self.get(type: from).flatMap { wallet -> EventLoopFuture in + self._withdraw(on: self.db, from: wallet, amount: amount, meta: meta) + } } - } } - + public func deposit(to: WalletType = .default, amount: Double, confirmed: Bool = true, meta: [String: String]? = nil) -> EventLoopFuture { get(type: to).flatMap { wallet -> EventLoopFuture in let intAmount = Int(amount * pow(10, Double(wallet.decimalPlaces))) @@ -122,18 +228,18 @@ extension WalletsRepository { public func transafer(from: Wallet, to: Wallet, amount: Int, meta: [String: String]? = nil) -> EventLoopFuture { return _canWithdraw(from: from, amount: amount) .guard({ $0 == true }, else: WalletError.insufficientBalance) - .flatMap { _ in - self._transfer(from: from, to: to, amount: amount, meta: meta) - } + .flatMap { _ in + self._transfer(from: from, to: to, amount: amount, meta: meta) + } } public func transfer(from: WalletType, to: Wallet, amount: Int, meta: [String: String]? = nil) -> EventLoopFuture { return get(type: from).flatMap { fromWallet -> EventLoopFuture in self._canWithdraw(from: fromWallet, amount: amount) .guard({ $0 == true }, else: WalletError.insufficientBalance) - .flatMap { _ in - return self._transfer(from: fromWallet, to: to, amount: amount, meta: meta) - } + .flatMap { _ in + return self._transfer(from: fromWallet, to: to, amount: amount, meta: meta) + } } } @@ -142,7 +248,7 @@ extension WalletsRepository { self.transfer(from: from, to: toWallet, amount: amount, meta: meta) } } - + } @@ -150,9 +256,55 @@ extension WalletsRepository { /// Accessing transactions of a wallet and confirming transactions /// extension WalletsRepository { + public func transactionsAsync(type name: WalletType = .default, + paginate: PageRequest = .init(page: 1, per: 10), + sortOrder: DatabaseQuery.Sort.Direction = .descending) async throws -> Page { + let wallet = try await self.getAsync(type: name) + return try await wallet.$transactions + .query(on: self.db) + .sort(\.$createdAt, sortOrder) + .filter(\.$confirmed == true) + .paginate(paginate) + } + + public func unconfirmedTransactionsAsync(type name: WalletType = .default, + paginate: PageRequest = .init(page: 1, per: 10), + sortOrder: DatabaseQuery.Sort.Direction = .descending) async throws -> Page { + let wallet = try await self.getAsync(type: name) + return try await wallet.$transactions + .query(on: self.db) + .sort(\.$createdAt, sortOrder) + .filter(\.$confirmed == false) + .paginate(paginate) + } + + + public func confirmAllAsync(type name: WalletType = .default) async throws -> Double { + let wallet = try await self.getAsync(type: name) + return try await self.db.transaction { database in + try await wallet.$transactions + .query(on: database) + .set(\.$confirmed, to: true) + .update() + return try await wallet.refreshBalanceAsync(on: database) + } + } + + public func confirmAsync(transaction: WalletTransaction, refresh: Bool = true) async throws -> Double { + transaction.confirmed = true + return try await self.db.transaction { database in + try await transaction.update(on: database) + let wallet = try await transaction.$wallet.get(on: database) + return try await wallet.refreshBalanceAsync(on: database) + } + } + + + + public func transactions(type name: WalletType = .default, - paginate: PageRequest = .init(page: 1, per: 10), - sortOrder: DatabaseQuery.Sort.Direction = .descending) -> EventLoopFuture> { + paginate: PageRequest = .init(page: 1, per: 10), + sortOrder: DatabaseQuery.Sort.Direction = .descending) -> EventLoopFuture> { return self.get(type: name).flatMap { $0.$transactions .query(on: self.db) @@ -173,7 +325,7 @@ extension WalletsRepository { .paginate(paginate) } } - + public func confirmAll(type name: WalletType = .default) -> EventLoopFuture { get(type: name).flatMap { (wallet) -> EventLoopFuture in self.db.transaction { (database) -> EventLoopFuture in @@ -183,7 +335,7 @@ extension WalletsRepository { .update() .flatMap { _ -> EventLoopFuture in wallet.refreshBalance(on: database) - } + } } } } @@ -206,6 +358,46 @@ extension WalletsRepository { /// Private methdos /// extension WalletsRepository { + private func _canWithdrawAsync(from: Wallet, amount: Int) async throws -> Bool { + return try await from.refreshBalanceAsync(on: self.db) > Double(amount) + } + + private func _depositAsync(on db: Database, to: Wallet, amount: Int, confirmed: Bool = true, meta: [String: String]? = nil) async throws { + try await db.transaction { database in + var walletTransaction: WalletTransaction + do { + walletTransaction = WalletTransaction(walletID: try to.requireID(), type: .deposit, amount: amount, confirmed: confirmed, meta: meta) + } catch { + throw WalletError.walletNotFound(name: to.name) + } + _ = try await walletTransaction.save(on: database) + } + } + + private func _withdrawAsync(on db: Database, from: Wallet, amount: Int, meta: [String: String]? = nil) async throws { + try await db.transaction { database in + var walletTransaction: WalletTransaction + do { + walletTransaction = WalletTransaction(walletID: try from.requireID(), type: .withdraw, amount: -1 * amount, meta: meta) + } catch { + throw WalletError.walletNotFound(name: from.name) + } + _ = try await walletTransaction.save(on: database) + } + } + + private func _transferAsync(from: Wallet, to: Wallet, amount: Int, meta: [String: String]? = nil) async throws { + try await self.db.transaction { database in + guard try await self._canWithdrawAsync(from: from, amount: amount) else { + throw WalletError.insufficientBalance + } + try await self._withdrawAsync(on: database, from: from, amount: amount, meta: meta) + try await self._depositAsync(on: database, to: to, amount: amount, meta: meta) + _ = try await from.refreshBalanceAsync(on: database) + _ = try await to.refreshBalanceAsync(on: database) + } + } + private func _canWithdraw(from: Wallet, amount: Int) -> EventLoopFuture { from.refreshBalance(on: self.db).map { $0 >= Double(amount) } } @@ -231,7 +423,7 @@ extension WalletsRepository { } } } - + private func _transfer(from: Wallet, to: Wallet, amount: Int, meta: [String: String]? = nil) -> EventLoopFuture { return self.db.transaction { (database) -> EventLoopFuture in return self._withdraw(on: database, from: from, amount: amount, meta: meta).flatMap { _ -> EventLoopFuture in @@ -247,4 +439,4 @@ extension WalletsRepository { } } - + diff --git a/Tests/VaporWalletTests/Model+Test.swift b/Tests/VaporWalletTests/Model+Test.swift index b41d82e..d4b458c 100644 --- a/Tests/VaporWalletTests/Model+Test.swift +++ b/Tests/VaporWalletTests/Model+Test.swift @@ -36,7 +36,7 @@ final class User: Model { } extension User: HasWallet { - + static let idKey = \User.$id } @@ -55,6 +55,19 @@ struct CreateUser: Migration { } } +struct CreateUserAsync: AsyncMigration { + func prepare(on database: Database) async throws { + try await database.schema(User.schema) + .id() + .field("username", .string, .required) + .create() + } + + func revert(on database: Database) async throws { + try await database.schema(User.schema).delete() + } +} + final class Game: Model { @@ -84,7 +97,7 @@ final class Game: Model { } extension Game: HasWallet { - + static let idKey = \Game.$id } @@ -104,3 +117,17 @@ struct CreateGame: Migration { } +struct CreateGameAsync: AsyncMigration { + func prepare(on database: Database) async throws { + try await database.schema(Game.schema) + .id() + .field("name", .string, .required) + .create() + } + + func revert(on database: Database) async throws { + try await database.schema(Game.schema).delete() + } +} + + diff --git a/Tests/VaporWalletTests/VaporWalletAsyncTests.swift b/Tests/VaporWalletTests/VaporWalletAsyncTests.swift new file mode 100644 index 0000000..1187dd6 --- /dev/null +++ b/Tests/VaporWalletTests/VaporWalletAsyncTests.swift @@ -0,0 +1,375 @@ +import Vapor +import XCTest +import Fluent +import FluentSQLiteDriver +import FluentMySQLDriver +@testable import VaporWallet + +class VaporWalletAsyncTests: XCTestCase { + + private var app: Application! + + override func setUp() { + super.setUp() + + app = Application(.testing) + app.logger.logLevel = .debug + app.databases.use(.mysql(hostname: "127.0.0.1", port: 3306, username: "root", password: "hadi2400", database: "vp-test", tlsConfiguration: .none), as: .mysql) + // app.databases.use(.sqlite(.memory), as: .sqlite) + + try! migrations(app) + try! app.autoRevert().wait() + try! app.autoMigrate().wait() + try! resetDB() + + } + + func resetDB() throws { + let db = (app.db as! SQLDatabase) + let query = db.raw(""" + SELECT Concat('DELETE FROM ', TABLE_NAME, ';') as truncate_query FROM INFORMATION_SCHEMA.TABLES where `TABLE_SCHEMA` = 'vp-test' and `TABLE_NAME` not like '_fluent_%'; + """) + + return try query.all().flatMap { results in + return results.compactMap { row in + try? row.decode(column: "truncate_query", as: String.self) + }.map { query in + return (db as! MySQLDatabase).simpleQuery(query).transform(to: ()) + }.flatten(on: self.app.db.eventLoop) + }.wait() + } + + + override func tearDown() { + try! app.autoRevert().wait() + app.shutdown() + + } + + + func testAddUser() async throws { + let user = try await User.create(username: "user1", on: app.db) + XCTAssert(user.username == "user1") + } + + func testAddGame() throws { + let game = try Game.create(name: "new_game", on: app.db) + XCTAssert(game.name == "new_game") + } + + func testUserHasNoDefaultWallet() throws { + let userWithNoWallet = try User.create(on: app.db) + XCTAssertThrowsError(try userWithNoWallet.walletsRepository(on: app.db).default().wait(), "expected throw") { (error) in + XCTAssertEqual(error as! WalletError, WalletError.walletNotFound(name: WalletType.default.value)) + } + } + + + func testUserHasDefaultWallet() throws { + app.databases.middleware.use(WalletMiddleware()) + let userWithDefaultWallet = try User.create(on: app.db) + let defaultWallet = try userWithDefaultWallet.walletsRepository(on: app.db).default().wait() + + XCTAssertEqual(defaultWallet.name, WalletType.default.value) + } + + func testCreateWallet() throws { + let (_, wallets) = setupUserAndWalletsRepo(on: app.db) + try wallets.create(type: .init(name: "savings")).transform(to: ()).wait() + + let userWallets = try wallets.all().wait() + XCTAssertEqual(userWallets.count, 1) + XCTAssertEqual(userWallets.first?.name, "savings") + + } + + func testWalletDeposit() throws { + app.databases.middleware.use(WalletMiddleware()) + let (_, wallets) = setupUserAndWalletsRepo(on: app.db) + + + try! wallets.deposit(amount: 10).wait() + let balance = try wallets.balance().wait() + + XCTAssertEqual(balance, 0) + + let refreshedBalance = try wallets.refreshBalance().wait() + XCTAssertEqual(refreshedBalance, 10) + + } + + func testWalletTransactionMiddleware() throws { + app.databases.middleware.use(WalletMiddleware()) + app.databases.middleware.use(WalletTransactionMiddleware()) + + let user = try User.create(on: app.db) + let walletsRepoWithMiddleware = user.walletsRepository(on: app.db) + + try! walletsRepoWithMiddleware.deposit(amount: 40).wait() + + let balance = try walletsRepoWithMiddleware.balance().wait() + + XCTAssertEqual(balance, 40) + + } + + func testWalletWithdraw() throws { + app.databases.middleware.use(WalletMiddleware()) + app.databases.middleware.use(WalletTransactionMiddleware()) + + let (_, wallets) = setupUserAndWalletsRepo(on: app.db) + + try wallets.deposit(amount: 100).wait() + + var balance = try wallets.balance().wait() + + XCTAssertEqual(balance, 100) + + XCTAssertThrowsError(try wallets.withdraw(amount: 200).wait(), "expected throw") { (error) in + XCTAssertEqual(error as! WalletError, WalletError.insufficientBalance) + } + + try! wallets.withdraw(amount: 50).wait() + + balance = try wallets.balance().wait() + + XCTAssertEqual(balance, 50) + + } + + + func testWalletCanWithdraw() throws { + app.databases.middleware.use(WalletMiddleware()) + app.databases.middleware.use(WalletTransactionMiddleware()) + + let (_, wallets) = setupUserAndWalletsRepo(on: app.db) + + try wallets.deposit(amount: 100).wait() + + XCTAssertTrue(try! wallets.canWithdraw(amount: 100).wait()) + XCTAssertFalse(try! wallets.canWithdraw(amount: 200).wait()) + + } + + func testMultiWallet() throws { + app.databases.middleware.use(WalletTransactionMiddleware()) + app.databases.middleware.use(WalletTransactionMiddleware()) + + let (_, wallets) = setupUserAndWalletsRepo(on: app.db) + + let savingsWallet = WalletType(name: "savings") + let myWallet = WalletType(name: "my-wallet") + let notExistsWallet = WalletType(name: "not-exists") + + try wallets.create(type: myWallet).transform(to: ()).wait() + try wallets.create(type: savingsWallet).transform(to: ()).wait() + + try wallets.deposit(to: myWallet, amount: 100).wait() + try wallets.deposit(to: savingsWallet, amount: 200).wait() + + do { + try wallets.deposit(to: notExistsWallet, amount: 1000).wait() + } catch { + XCTAssertEqual(error as! WalletError, WalletError.walletNotFound(name: "not-exists")) + } + + let balance1 = try wallets.balance(type: myWallet).wait() + let balance2 = try wallets.balance(type: savingsWallet).wait() + + XCTAssertEqual(balance1, 100) + XCTAssertEqual(balance2, 200) + + } + + + func testTransactionMetadata() throws { + app.databases.middleware.use(WalletMiddleware()) + + let (_, wallets) = setupUserAndWalletsRepo(on: app.db) + + try wallets.deposit(amount: 100, meta: ["description": "payment of taxes"]).wait() + + let transaction = try wallets.default().wait() + .$transactions.get(on: app.db).wait() + .first! + + XCTAssertEqual(transaction.meta!["description"] , "payment of taxes") + } + + func testWalletDecimalBalance() throws { + app.databases.middleware.use(WalletTransactionMiddleware()) + + let user = try User.create(on: app.db) + let wallets = user.walletsRepository(on: app.db) + try wallets.create(type: .default, decimalPlaces: 2).wait() + + try wallets.deposit(amount: 100).wait() + + var balance = try wallets.balance().wait() + XCTAssertEqual(balance, 100) + + try wallets.deposit(amount: 1.45).wait() + balance = try wallets.balance().wait() + XCTAssertEqual(balance, 245) + + balance = try wallets.balance(asDecimal: true).wait() + XCTAssertEqual(balance, 2.45) + + + // decmial values will be truncated to wallet's decimalPlace value + try wallets.deposit(amount: 1.555).wait() + balance = try wallets.balance().wait() + XCTAssertEqual(balance, 400) + + } + + + func testConfirmTransaction() throws { + app.databases.middleware.use(WalletMiddleware()) + app.databases.middleware.use(WalletTransactionMiddleware()) + + let (_, wallets) = setupUserAndWalletsRepo(on: app.db) + + try wallets.deposit(amount: 10, confirmed: true).wait() + sleep(1) + try wallets.deposit(amount: 40, confirmed: false).wait() + + var balance = try wallets.balance().wait() + let unconfirmedBalance = try wallets.balance(withUnconfirmed: true).wait() + XCTAssertEqual(balance, 10) + XCTAssertEqual(unconfirmedBalance, 50) + + let transaction = try wallets.unconfirmedTransactions() + .wait() + .items.first! + + balance = try wallets.confirm(transaction: transaction).wait() + + XCTAssertEqual(balance, 50) + + } + + func testConfirmAllTransactionsOfWallet() throws { + app.databases.middleware.use(WalletMiddleware()) + app.databases.middleware.use(WalletTransactionMiddleware()) + + let (_, wallets) = setupUserAndWalletsRepo(on: app.db) + + try wallets.deposit(amount: 10, confirmed: false).wait() + try wallets.deposit(amount: 40, confirmed: false).wait() + + var balance = try wallets.balance().wait() + XCTAssertEqual(balance, 0) + + balance = try wallets.confirmAll().wait() + + XCTAssertEqual(balance, 50) + } + + + func testTransferBetweenAUsersWallets() throws { + app.databases.middleware.use(WalletMiddleware()) + app.databases.middleware.use(WalletTransactionMiddleware()) + + let (_, wallets) = setupUserAndWalletsRepo(on: app.db) + + let wallet1 = WalletType(name: "wallet1") + let wallet2 = WalletType(name: "wallet2") + + try wallets.create(type: wallet1).wait() + try wallets.create(type: wallet2).wait() + + + try wallets.deposit(to: wallet1, amount: 100).wait() + + try wallets.transafer(from: wallet1, to: wallet2, amount: 20).wait() + + let balance1 = try wallets.balance(type: wallet1).wait() + let balance2 = try wallets.balance(type: wallet2).wait() + + XCTAssertEqual(balance1, 80) + XCTAssertEqual(balance2, 20) + + } + + func testTransferBetweenTwoUsersWallets() throws { + app.databases.middleware.use(WalletMiddleware()) + app.databases.middleware.use(WalletTransactionMiddleware()) + + let user1 = try User.create(username: "user1", on: app.db) + let user2 = try User.create(username: "user2", on: app.db) + + let repo1 = user1.walletsRepository(on: app.db) + let repo2 = user2.walletsRepository(on: app.db) + + try repo1.deposit(amount: 100).wait() + + try repo1.transafer(from: try repo1.default().wait(), to: try repo2.default().wait(), amount: 20).wait() + + var balance1 = try repo1.balance().wait() + var balance2 = try repo2.balance().wait() + + XCTAssertEqual(balance1, 80) + XCTAssertEqual(balance2, 20) + + try repo1.transfer(from: .default, to: try repo2.default().wait(), amount: 20).wait() + + balance1 = try repo1.balance().wait() + balance2 = try repo2.balance().wait() + + XCTAssertEqual(balance1, 60) + XCTAssertEqual(balance2, 40) + + } + + + func testMultiModelWallet() throws { + app.databases.middleware.use(WalletMiddleware()) + app.databases.middleware.use(WalletMiddleware()) + app.databases.middleware.use(WalletTransactionMiddleware()) + + let user = try User.create(username: "user1", on: app.db) + let game = Game(id: user.id, name: "game1") + try game.save(on: app.db).wait() + + let repo1 = user.walletsRepository(on: app.db) + let repo2 = game.walletsRepository(on: app.db) + + try repo1.deposit(amount: 100).wait() + try repo2.deposit(amount: 500).wait() + + let balance1 = try repo1.balance().wait() + let balance2 = try repo2.balance().wait() + + XCTAssertEqual(balance1, 100) + XCTAssertEqual(balance2, 500) + + + + } + + + + + private func setupUserAndWalletsRepo(on: Database) -> (User, WalletsRepository) { + let user = try! User.create(on: app.db) + let wallets = user.walletsRepository(on: app.db) + + return (user, wallets) + } + + private func migrations(_ app: Application) throws { + // Initial Migrations + app.migrations.add(CreateUserAsync()) + app.migrations.add(CreateGameAsync()) + app.migrations.add(CreateWalletAsync()) + app.migrations.add(CreateWalletTransactionAsync()) + } +} +// +//extension WalletError: Equatable { +// public static func == (lhs: WalletError, rhs: WalletError) -> Bool { +// return lhs.errorDescription == rhs.errorDescription +// } +// +//} diff --git a/Tests/VaporWalletTests/VaporWalletTests.swift b/Tests/VaporWalletTests/VaporWalletTests.swift index fdcf1e6..cb90f31 100644 --- a/Tests/VaporWalletTests/VaporWalletTests.swift +++ b/Tests/VaporWalletTests/VaporWalletTests.swift @@ -338,8 +338,8 @@ class VaporWalletTests: XCTestCase { try repo1.deposit(amount: 100).wait() try repo2.deposit(amount: 500).wait() - var balance1 = try repo1.balance().wait() - var balance2 = try repo2.balance().wait() + let balance1 = try repo1.balance().wait() + let balance2 = try repo2.balance().wait() XCTAssertEqual(balance1, 100) XCTAssertEqual(balance2, 500) From 5ce632280a54b4468fc298fe161b9edede975010 Mon Sep 17 00:00:00 2001 From: Hadi Sharghi Date: Wed, 14 Jun 2023 19:00:49 +0330 Subject: [PATCH 3/9] comment out legacy codes. switch to async/await --- Package.swift | 2 + Sources/VaporWallet/HasWallet.swift | 28 +- .../Middlewares/WalletModelMiddleware.swift | 30 +- .../WalletTransactionModelMiddleware.swift | 28 +- .../VaporWallet/Migrations/CreateWallet.swift | 8 +- .../Migrations/CreateWalletTransaction.swift | 20 +- .../VaporWallet/Models/Entities/Wallet.swift | 5 + .../Models/Entities/WalletTransaction.swift | 10 +- Sources/VaporWallet/WalletRepository.swift | 449 +++++++++--------- Tests/VaporWalletTests/Model+Test.swift | 66 +-- .../VaporWalletAsyncTests.swift | 375 --------------- Tests/VaporWalletTests/VaporWalletTests.swift | 394 ++++++++------- Tests/VaporWalletTests/XCTest+async.swift | 25 + 13 files changed, 577 insertions(+), 863 deletions(-) delete mode 100644 Tests/VaporWalletTests/VaporWalletAsyncTests.swift create mode 100644 Tests/VaporWalletTests/XCTest+async.swift diff --git a/Package.swift b/Package.swift index a2651ee..dc2629c 100644 --- a/Package.swift +++ b/Package.swift @@ -21,6 +21,7 @@ let package = Package( .package(url: "https://github.com/vapor/fluent.git", from: "4.0.0"), .package(url: "https://github.com/vapor/fluent-sqlite-driver.git", from: "4.0.0"), .package(url: "https://github.com/vapor/fluent-mysql-driver.git", from: "4.0.0"), + .package(url: "https://github.com/vapor/fluent-postgres-driver.git", from: "2.7.2"), ], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. @@ -36,6 +37,7 @@ let package = Package( .product(name: "XCTVapor", package: "vapor"), .product(name: "FluentSQLiteDriver", package: "fluent-sqlite-driver"), .product(name: "FluentMySQLDriver", package: "fluent-mysql-driver"), + .product(name: "FluentPostgresDriver", package: "fluent-postgres-driver"), ]), ] ) diff --git a/Sources/VaporWallet/HasWallet.swift b/Sources/VaporWallet/HasWallet.swift index 26d2d24..f1313e2 100644 --- a/Sources/VaporWallet/HasWallet.swift +++ b/Sources/VaporWallet/HasWallet.swift @@ -36,19 +36,19 @@ extension Wallet { try await self.update(on: db) return Double(self.balance) } - - public func refreshBalance(on db: Database) -> EventLoopFuture { - self.$transactions - .query(on: db) - .filter(\.$confirmed == true) - .sum(\.$amount) - .unwrap(orReplace: 0) - .flatMap { (balance) -> EventLoopFuture in - self.balance = balance - return self.update(on: db).map { - return Double(balance) - } - } - } +// +// public func refreshBalance(on db: Database) -> EventLoopFuture { +// self.$transactions +// .query(on: db) +// .filter(\.$confirmed == true) +// .sum(\.$amount) +// .unwrap(orReplace: 0) +// .flatMap { (balance) -> EventLoopFuture in +// self.balance = balance +// return self.update(on: db).map { +// return Double(balance) +// } +// } +// } } diff --git a/Sources/VaporWallet/Middlewares/WalletModelMiddleware.swift b/Sources/VaporWallet/Middlewares/WalletModelMiddleware.swift index d4dbe8c..1b79a55 100644 --- a/Sources/VaporWallet/Middlewares/WalletModelMiddleware.swift +++ b/Sources/VaporWallet/Middlewares/WalletModelMiddleware.swift @@ -8,20 +8,20 @@ import Vapor import Fluent - -public struct WalletMiddleware: ModelMiddleware { - - public init() {} - - public func create(model: M, on db: Database, next: AnyModelResponder) -> EventLoopFuture { - - // Create `default` wallet when new model is created - return next.create(model, on: db).flatMap { - db.logger.log(level: .info, "default wallet for user \(model._$idKey) has been created") - return model.walletsRepository(on: db).create().transform(to: ()) - } - } -} +// +//public struct WalletMiddleware: ModelMiddleware { +// +// public init() {} +// +// public func create(model: M, on db: Database, next: AnyModelResponder) -> EventLoopFuture { +// +// // Create `default` wallet when new model is created +// return next.create(model, on: db).flatMap { +// db.logger.log(level: .info, "default wallet for user \(model._$idKey) has been created") +// return model.walletsRepository(on: db).create().transform(to: ()) +// } +// } +//} public struct AsyncWalletMiddleware: AsyncModelMiddleware { @@ -31,6 +31,6 @@ public struct AsyncWalletMiddleware: AsyncModelMiddleware { try await next.create(model, on: db) db.logger.log(level: .info, "default wallet for user \(model._$idKey) has been created") let repo = model.walletsRepository(on: db) - try await repo.createAsyc() + try await repo.createAsync() } } diff --git a/Sources/VaporWallet/Middlewares/WalletTransactionModelMiddleware.swift b/Sources/VaporWallet/Middlewares/WalletTransactionModelMiddleware.swift index 4d32a18..a762461 100644 --- a/Sources/VaporWallet/Middlewares/WalletTransactionModelMiddleware.swift +++ b/Sources/VaporWallet/Middlewares/WalletTransactionModelMiddleware.swift @@ -9,20 +9,20 @@ import Vapor import Fluent - -public struct WalletTransactionMiddleware: ModelMiddleware { - - public init() {} - - public func create(model: WalletTransaction, on db: Database, next: AnyModelResponder) -> EventLoopFuture { - return next.create(model, on: db).flatMap { - return model - .$wallet.get(on: db) - .map { $0.refreshBalance(on: db) } - .transform(to: ()) - } - } -} +// +//public struct WalletTransactionMiddleware: ModelMiddleware { +// +// public init() {} +// +// public func create(model: WalletTransaction, on db: Database, next: AnyModelResponder) -> EventLoopFuture { +// return next.create(model, on: db).flatMap { +// return model +// .$wallet.get(on: db) +// .map { $0.refreshBalance(on: db) } +// .transform(to: ()) +// } +// } +//} public struct AsyncWalletTransactionMiddleware: AsyncModelMiddleware { diff --git a/Sources/VaporWallet/Migrations/CreateWallet.swift b/Sources/VaporWallet/Migrations/CreateWallet.swift index 21e244c..8900a2e 100644 --- a/Sources/VaporWallet/Migrations/CreateWallet.swift +++ b/Sources/VaporWallet/Migrations/CreateWallet.swift @@ -13,6 +13,7 @@ public struct CreateWallet: Migration { .field("name", .string, .required) .field("owner_type", .string, .required) .field("owner_id", .uuid, .required) + .field("min_allowed_balance", .int, .required) .field("balance", .int, .required) .field("decimal_places", .uint8, .required) .field("created_at", .datetime, .required) @@ -38,13 +39,14 @@ public struct CreateWalletAsync: AsyncMigration { public init(foreignKeyColumnName idKey: String = "id") { self.idKey = idKey } - + public func prepare(on database: Database) async throws { try await database.schema(Wallet.schema) .id() .field("name", .string, .required) .field("owner_type", .string, .required) .field("owner_id", .uuid, .required) + .field("min_allowed_balance", .int, .required) .field("balance", .int, .required) .field("decimal_places", .uint8, .required) .field("created_at", .datetime, .required) @@ -58,8 +60,8 @@ public struct CreateWalletAsync: AsyncMigration { .column("owner_type") .run() } - - + + public func revert(on database: Database) async throws { try await database.schema(Wallet.schema).delete() } diff --git a/Sources/VaporWallet/Migrations/CreateWalletTransaction.swift b/Sources/VaporWallet/Migrations/CreateWalletTransaction.swift index 850c17b..2465755 100644 --- a/Sources/VaporWallet/Migrations/CreateWalletTransaction.swift +++ b/Sources/VaporWallet/Migrations/CreateWalletTransaction.swift @@ -28,10 +28,13 @@ public struct CreateWalletTransaction: Migration { public struct CreateWalletTransactionAsync: AsyncMigration { public init() { } - + public func prepare(on database: Database) async throws { - let transactionType = try await database.enum("type").case("deposit").case("withdraw").create() - + let transactionType = try await database.enum("type") + .case("deposit") + .case("withdraw") + .create() + try await database.schema(WalletTransaction.schema) .id() .field("wallet_id", .uuid, .required, .references(Wallet.schema, "id", onDelete: .cascade)) @@ -43,11 +46,14 @@ public struct CreateWalletTransactionAsync: AsyncMigration { .field("updated_at", .datetime, .required) .create() } - - + + public func revert(on database: Database) async throws { - let _ = try await database.enum("type").deleteCase("deposit").deleteCase("withdraw").update() + let _ = try await database.enum("type") + .deleteCase("deposit") + .deleteCase("withdraw") + .update() try await database.schema(WalletTransaction.schema).delete() } - + } diff --git a/Sources/VaporWallet/Models/Entities/Wallet.swift b/Sources/VaporWallet/Models/Entities/Wallet.swift index 2b9b347..1bb7691 100644 --- a/Sources/VaporWallet/Models/Entities/Wallet.swift +++ b/Sources/VaporWallet/Models/Entities/Wallet.swift @@ -24,6 +24,9 @@ public final class Wallet: Model, Content { @Field(key: "name") var name: String + @Field(key: "min_allowed_balance") + var minAllowedBalance: Int + @Field(key: "balance") var balance: Int @@ -49,6 +52,7 @@ public final class Wallet: Model, Content { ownerType: String, ownerID: UUID, name: String = WalletType.default.value, + minAllowedBalance: Int = 0, balance: Int = 0, decimalPlaces: UInt8 = 2, createdAt: Date? = nil, @@ -59,6 +63,7 @@ public final class Wallet: Model, Content { self.ownerType = ownerType self.owner = ownerID self.name = name + self.minAllowedBalance = minAllowedBalance self.balance = balance self.decimalPlaces = decimalPlaces self.createdAt = createdAt diff --git a/Sources/VaporWallet/Models/Entities/WalletTransaction.swift b/Sources/VaporWallet/Models/Entities/WalletTransaction.swift index b7eefe1..854810a 100644 --- a/Sources/VaporWallet/Models/Entities/WalletTransaction.swift +++ b/Sources/VaporWallet/Models/Entities/WalletTransaction.swift @@ -71,11 +71,11 @@ extension WalletTransaction { public var isConfirmed: Bool { return self.confirmed } - - public func confirm(on db: Database) -> EventLoopFuture { - self.confirmed = true - return self.update(on: db) - } +// +// public func confirm(on db: Database) -> EventLoopFuture { +// self.confirmed = true +// return self.update(on: db) +// } public func confirmAsync(on db: Database) async throws { self.confirmed = true diff --git a/Sources/VaporWallet/WalletRepository.swift b/Sources/VaporWallet/WalletRepository.swift index 37c42bf..f0cf252 100644 --- a/Sources/VaporWallet/WalletRepository.swift +++ b/Sources/VaporWallet/WalletRepository.swift @@ -19,10 +19,12 @@ public class WalletsRepository { } self.db = db self.id = id + self.type = String(describing: M.self) } - + private var db: Database private var id: M.ID.Value + private var type: String } /// @@ -30,8 +32,12 @@ public class WalletsRepository { /// extension WalletsRepository { - public func createAsyc(type name: WalletType = .default, decimalPlaces: UInt8 = 2) async throws { - let wallet: Wallet = Wallet(ownerType: String(describing: self), ownerID: self.id, name: name.value, decimalPlaces: decimalPlaces) + public func createAsync(type name: WalletType = .default, decimalPlaces: UInt8 = 2, minAllowedBalance: Int = 0) async throws { + let wallet: Wallet = Wallet(ownerType: String(describing: self), + ownerID: self.id, + name: name.value, + minAllowedBalance: minAllowedBalance, + decimalPlaces: decimalPlaces) try await wallet.save(on: db) } @@ -42,19 +48,25 @@ extension WalletsRepository { .all() } - public func getAsync(type name: WalletType) async throws -> Wallet { - let wallet = try await Wallet.query(on: db) + public func getAsync(type name: WalletType, withTransactions: Bool = false) async throws -> Wallet { + var walletQuery = Wallet.query(on: db) .filter(\.$owner == self.id) + .filter(\.$ownerType == self.type) .filter(\.$name == name.value) - .first() + + if (withTransactions) { + walletQuery = walletQuery.with(\.$transactions) + } + let wallet = try await walletQuery.first() + guard let wallet = wallet else { throw WalletError.walletNotFound(name: name.value) } return wallet } - public func defaultAsync() async throws -> Wallet { - return try await getAsync(type: .default) + public func defaultAsync(withTransactions: Bool = false) async throws -> Wallet { + return try await getAsync(type: .default, withTransactions: withTransactions) } public func balanceAsync(type name: WalletType = .default, withUnconfirmed: Bool = false, asDecimal: Bool = false) async throws -> Double { @@ -77,52 +89,52 @@ extension WalletsRepository { return try await wallet.refreshBalanceAsync(on: self.db) } - - public func create(type name: WalletType = .default, decimalPlaces: UInt8 = 2) -> EventLoopFuture { - let wallet: Wallet = Wallet(ownerType: String(describing: self), ownerID: self.id, name: name.value, decimalPlaces: decimalPlaces) - return wallet.save(on: db) - } - - public func all() -> EventLoopFuture<[Wallet]> { - Wallet.query(on: self.db) - .filter(\.$owner == self.id) - .all() - } - - public func get(type name: WalletType) -> EventLoopFuture { - Wallet.query(on: db) - .filter(\.$owner == self.id) - .filter(\.$name == name.value) - .first() - .unwrap(or: WalletError.walletNotFound(name: name.value)) - } - - public func `default`() -> EventLoopFuture { - get(type: .default) - } - - public func balance(type name: WalletType = .default, withUnconfirmed: Bool = false, asDecimal: Bool = false) -> EventLoopFuture { - if withUnconfirmed { - return get(type: name).flatMap { wallet in - wallet.$transactions - .query(on: self.db) - .sum(\.$amount) - .unwrap(orReplace: 0) - .map { (intBalance) -> Double in - return asDecimal ? Double(intBalance).toDecimal(with: wallet.decimalPlaces) : Double(intBalance) - } - } - } - return get(type: name).map { wallet in - return asDecimal ? Double(wallet.balance).toDecimal(with: wallet.decimalPlaces) : Double(wallet.balance) - } - } - - public func refreshBalance(of walletType: WalletType = .default) -> EventLoopFuture { - return get(type: walletType).flatMap { wallet -> EventLoopFuture in - wallet.refreshBalance(on: self.db) - } - } + // + // public func create(type name: WalletType = .default, decimalPlaces: UInt8 = 2) -> EventLoopFuture { + // let wallet: Wallet = Wallet(ownerType: String(describing: self), ownerID: self.id, name: name.value, decimalPlaces: decimalPlaces) + // return wallet.save(on: db) + // } + // + // public func all() -> EventLoopFuture<[Wallet]> { + // Wallet.query(on: self.db) + // .filter(\.$owner == self.id) + // .all() + // } + // + // public func get(type name: WalletType) -> EventLoopFuture { + // Wallet.query(on: db) + // .filter(\.$owner == self.id) + // .filter(\.$name == name.value) + // .first() + // .unwrap(or: WalletError.walletNotFound(name: name.value)) + // } + // + // public func `default`() -> EventLoopFuture { + // get(type: .default) + // } + // + // public func balance(type name: WalletType = .default, withUnconfirmed: Bool = false, asDecimal: Bool = false) -> EventLoopFuture { + // if withUnconfirmed { + // return get(type: name).flatMap { wallet in + // wallet.$transactions + // .query(on: self.db) + // .sum(\.$amount) + // .unwrap(orReplace: 0) + // .map { (intBalance) -> Double in + // return asDecimal ? Double(intBalance).toDecimal(with: wallet.decimalPlaces) : Double(intBalance) + // } + // } + // } + // return get(type: name).map { wallet in + // return asDecimal ? Double(wallet.balance).toDecimal(with: wallet.decimalPlaces) : Double(wallet.balance) + // } + // } + // + // public func refreshBalance(of walletType: WalletType = .default) -> EventLoopFuture { + // return get(type: walletType).flatMap { wallet -> EventLoopFuture in + // wallet.refreshBalance(on: self.db) + // } + // } } @@ -134,7 +146,7 @@ extension WalletsRepository { public func canWithdrawAsync(from: WalletType = .default, amount: Int) async throws -> Bool { let wallet = try await getAsync(type: from) - return try await self._canWithdrawAsync(from: wallet, amount: amount) + return try await self._canWithdrawAsync(on: self.db, from: wallet, amount: amount) } public func withdrawAsync(from: WalletType = .default, amount: Double, meta: [String: String]? = nil) async throws { @@ -159,95 +171,90 @@ extension WalletsRepository { public func depositAsync(to: WalletType = .default, amount: Double, confirmed: Bool = true, meta: [String: String]? = nil) async throws { let wallet = try await getAsync(type: to) let intAmount = Int(amount * pow(10, Double(wallet.decimalPlaces))) - try await self._depositAsync(on: self.db, to: wallet, amount: intAmount, meta: meta) + try await self._depositAsync(on: self.db, to: wallet, amount: intAmount, confirmed: confirmed, meta: meta) } public func depositAsync(to: WalletType = .default, amount: Int, confirmed: Bool = true, meta: [String: String]? = nil) async throws { let wallet = try await getAsync(type: to) - try await self._depositAsync(on: self.db, to: wallet, amount: amount, meta: meta) + try await self._depositAsync(on: self.db, to: wallet, amount: amount, confirmed: confirmed, meta: meta) } - public func transaferAsync(from: Wallet, to: Wallet, amount: Int, meta: [String: String]? = nil) async throws { + public func transferAsync(from: Wallet, to: Wallet, amount: Int, meta: [String: String]? = nil) async throws { try await self._transferAsync(from: from, to: to, amount: amount, meta: meta) } - public func transaferAsync(from: WalletType, to: Wallet, amount: Int, meta: [String: String]? = nil) async throws { + public func transferAsync(from: WalletType, to: Wallet, amount: Int, meta: [String: String]? = nil) async throws { let fromWallet = try await getAsync(type: from) try await self._transferAsync(from: fromWallet, to: to, amount: amount, meta: meta) } - public func transaferAsync(from: Wallet, to: WalletType, amount: Int, meta: [String: String]? = nil) async throws { - let toWallet = try await getAsync(type: to) - try await self._transferAsync(from: from, to: toWallet, amount: amount, meta: meta) - } - - public func transaferAsync(from: WalletType, to: WalletType, amount: Int, meta: [String: String]? = nil) async throws { + public func transferAsync(from: WalletType, to: WalletType, amount: Int, meta: [String: String]? = nil) async throws { let fromWallet = try await getAsync(type: from) let toWallet = try await getAsync(type: to) try await self._transferAsync(from: fromWallet, to: toWallet, amount: amount, meta: meta) } - - public func canWithdraw(from: WalletType = .default, amount: Int) -> EventLoopFuture { - get(type: from).flatMap { self._canWithdraw(from: $0, amount: amount) } - } - - public func withdraw(from: WalletType = .default, amount: Double, meta: [String: String]? = nil) -> EventLoopFuture { - get(type: from).flatMap { wallet -> EventLoopFuture in - let intAmount = Int(amount * pow(10, Double(wallet.decimalPlaces))) - return self._withdraw(on: self.db, from: wallet, amount: intAmount, meta: meta) - } - } - - public func withdraw(from: WalletType = .default, amount: Int, meta: [String: String]? = nil) -> EventLoopFuture { - - canWithdraw(from: from, amount: amount) - .guard({ $0 == true }, else: WalletError.insufficientBalance) - .flatMap { _ in - self.get(type: from).flatMap { wallet -> EventLoopFuture in - self._withdraw(on: self.db, from: wallet, amount: amount, meta: meta) - } - } - } - - public func deposit(to: WalletType = .default, amount: Double, confirmed: Bool = true, meta: [String: String]? = nil) -> EventLoopFuture { - get(type: to).flatMap { wallet -> EventLoopFuture in - let intAmount = Int(amount * pow(10, Double(wallet.decimalPlaces))) - return self._deposit(on: self.db, to: wallet, amount: intAmount, confirmed: confirmed, meta: meta) - } - } - - public func deposit(to: WalletType = .default, amount: Int, confirmed: Bool = true, meta: [String: String]? = nil) -> EventLoopFuture { - get(type: to).flatMap { wallet -> EventLoopFuture in - self._deposit(on: self.db, to: wallet, amount: amount, confirmed: confirmed, meta: meta) - } - } - - public func transafer(from: Wallet, to: Wallet, amount: Int, meta: [String: String]? = nil) -> EventLoopFuture { - return _canWithdraw(from: from, amount: amount) - .guard({ $0 == true }, else: WalletError.insufficientBalance) - .flatMap { _ in - self._transfer(from: from, to: to, amount: amount, meta: meta) - } - } - - public func transfer(from: WalletType, to: Wallet, amount: Int, meta: [String: String]? = nil) -> EventLoopFuture { - return get(type: from).flatMap { fromWallet -> EventLoopFuture in - self._canWithdraw(from: fromWallet, amount: amount) - .guard({ $0 == true }, else: WalletError.insufficientBalance) - .flatMap { _ in - return self._transfer(from: fromWallet, to: to, amount: amount, meta: meta) - } - } - } - - public func transafer(from: WalletType, to: WalletType, amount: Int, meta: [String: String]? = nil) -> EventLoopFuture { - return get(type: to).flatMap { toWallet -> EventLoopFuture in - self.transfer(from: from, to: toWallet, amount: amount, meta: meta) - } - } + // + // public func canWithdraw(from: WalletType = .default, amount: Int) -> EventLoopFuture { + // get(type: from).flatMap { self._canWithdraw(from: $0, amount: amount) } + // } + // + // public func withdraw(from: WalletType = .default, amount: Double, meta: [String: String]? = nil) -> EventLoopFuture { + // get(type: from).flatMap { wallet -> EventLoopFuture in + // let intAmount = Int(amount * pow(10, Double(wallet.decimalPlaces))) + // return self._withdraw(on: self.db, from: wallet, amount: intAmount, meta: meta) + // } + // } + // + // public func withdraw(from: WalletType = .default, amount: Int, meta: [String: String]? = nil) -> EventLoopFuture { + // + // canWithdraw(from: from, amount: amount) + // .guard({ $0 == true }, else: WalletError.insufficientBalance) + // .flatMap { _ in + // self.get(type: from).flatMap { wallet -> EventLoopFuture in + // self._withdraw(on: self.db, from: wallet, amount: amount, meta: meta) + // } + // } + // } + // + // public func deposit(to: WalletType = .default, amount: Double, confirmed: Bool = true, meta: [String: String]? = nil) -> EventLoopFuture { + // get(type: to).flatMap { wallet -> EventLoopFuture in + // let intAmount = Int(amount * pow(10, Double(wallet.decimalPlaces))) + // return self._deposit(on: self.db, to: wallet, amount: intAmount, confirmed: confirmed, meta: meta) + // } + // } + // + // public func deposit(to: WalletType = .default, amount: Int, confirmed: Bool = true, meta: [String: String]? = nil) -> EventLoopFuture { + // get(type: to).flatMap { wallet -> EventLoopFuture in + // self._deposit(on: self.db, to: wallet, amount: amount, confirmed: confirmed, meta: meta) + // } + // } + // + // public func transafer(from: Wallet, to: Wallet, amount: Int, meta: [String: String]? = nil) -> EventLoopFuture { + // return _canWithdraw(from: from, amount: amount) + // .guard({ $0 == true }, else: WalletError.insufficientBalance) + // .flatMap { _ in + // self._transfer(from: from, to: to, amount: amount, meta: meta) + // } + // } + // + // public func transfer(from: WalletType, to: Wallet, amount: Int, meta: [String: String]? = nil) -> EventLoopFuture { + // return get(type: from).flatMap { fromWallet -> EventLoopFuture in + // self._canWithdraw(from: fromWallet, amount: amount) + // .guard({ $0 == true }, else: WalletError.insufficientBalance) + // .flatMap { _ in + // return self._transfer(from: fromWallet, to: to, amount: amount, meta: meta) + // } + // } + // } + // + // public func transafer(from: WalletType, to: WalletType, amount: Int, meta: [String: String]? = nil) -> EventLoopFuture { + // return get(type: to).flatMap { toWallet -> EventLoopFuture in + // self.transfer(from: from, to: toWallet, amount: amount, meta: meta) + // } + // } } @@ -270,7 +277,7 @@ extension WalletsRepository { public func unconfirmedTransactionsAsync(type name: WalletType = .default, paginate: PageRequest = .init(page: 1, per: 10), sortOrder: DatabaseQuery.Sort.Direction = .descending) async throws -> Page { - let wallet = try await self.getAsync(type: name) + let wallet = try await self.getAsync(type: name, withTransactions: true) return try await wallet.$transactions .query(on: self.db) .sort(\.$createdAt, sortOrder) @@ -280,7 +287,7 @@ extension WalletsRepository { public func confirmAllAsync(type name: WalletType = .default) async throws -> Double { - let wallet = try await self.getAsync(type: name) + let wallet = try await self.getAsync(type: name, withTransactions: true) return try await self.db.transaction { database in try await wallet.$transactions .query(on: database) @@ -299,67 +306,67 @@ extension WalletsRepository { } } - - - - public func transactions(type name: WalletType = .default, - paginate: PageRequest = .init(page: 1, per: 10), - sortOrder: DatabaseQuery.Sort.Direction = .descending) -> EventLoopFuture> { - return self.get(type: name).flatMap { - $0.$transactions - .query(on: self.db) - .sort(\.$createdAt, sortOrder) - .filter(\.$confirmed == true) - .paginate(paginate) - } - } - - public func unconfirmedTransactions(type name: WalletType = .default, - paginate: PageRequest = .init(page: 1, per: 10), - sortOrder: DatabaseQuery.Sort.Direction = .descending) -> EventLoopFuture> { - return self.get(type: name).flatMap { - $0.$transactions - .query(on: self.db) - .sort(\.$createdAt, sortOrder) - .filter(\.$confirmed == false) - .paginate(paginate) - } - } - - public func confirmAll(type name: WalletType = .default) -> EventLoopFuture { - get(type: name).flatMap { (wallet) -> EventLoopFuture in - self.db.transaction { (database) -> EventLoopFuture in - wallet.$transactions - .query(on: database) - .set(\.$confirmed, to: true) - .update() - .flatMap { _ -> EventLoopFuture in - wallet.refreshBalance(on: database) - } - } - } - } - - - public func confirm(transaction: WalletTransaction, refresh: Bool = true) -> EventLoopFuture { - transaction.confirmed = true - return self.db.transaction { (database) -> EventLoopFuture in - transaction.update(on: database).flatMap { () -> EventLoopFuture in - transaction.$wallet.get(on: database).flatMap { wallet -> EventLoopFuture in - wallet.refreshBalance(on: database) - } - } - } - } + // + // + // public func transactions(type name: WalletType = .default, + // paginate: PageRequest = .init(page: 1, per: 10), + // sortOrder: DatabaseQuery.Sort.Direction = .descending) -> EventLoopFuture> { + // return self.get(type: name).flatMap { + // $0.$transactions + // .query(on: self.db) + // .sort(\.$createdAt, sortOrder) + // .filter(\.$confirmed == true) + // .paginate(paginate) + // } + // } + // + // public func unconfirmedTransactions(type name: WalletType = .default, + // paginate: PageRequest = .init(page: 1, per: 10), + // sortOrder: DatabaseQuery.Sort.Direction = .descending) -> EventLoopFuture> { + // return self.get(type: name).flatMap { + // $0.$transactions + // .query(on: self.db) + // .sort(\.$createdAt, sortOrder) + // .filter(\.$confirmed == false) + // .paginate(paginate) + // } + // } + // + // public func confirmAll(type name: WalletType = .default) -> EventLoopFuture { + // get(type: name).flatMap { (wallet) -> EventLoopFuture in + // self.db.transaction { (database) -> EventLoopFuture in + // wallet.$transactions + // .query(on: database) + // .set(\.$confirmed, to: true) + // .update() + // .flatMap { _ -> EventLoopFuture in + // wallet.refreshBalance(on: database) + // } + // } + // } + // } + // + // + // public func confirm(transaction: WalletTransaction, refresh: Bool = true) -> EventLoopFuture { + // transaction.confirmed = true + // return self.db.transaction { (database) -> EventLoopFuture in + // transaction.update(on: database).flatMap { () -> EventLoopFuture in + // transaction.$wallet.get(on: database).flatMap { wallet -> EventLoopFuture in + // wallet.refreshBalance(on: database) + // } + // } + // } + // } + // } /// /// Private methdos /// extension WalletsRepository { - private func _canWithdrawAsync(from: Wallet, amount: Int) async throws -> Bool { - return try await from.refreshBalanceAsync(on: self.db) > Double(amount) + private func _canWithdrawAsync(on db: Database, from: Wallet, amount: Int) async throws -> Bool { + return try await from.refreshBalanceAsync(on: db) - Double(amount) >= Double(from.minAllowedBalance) } private func _depositAsync(on db: Database, to: Wallet, amount: Int, confirmed: Bool = true, meta: [String: String]? = nil) async throws { @@ -388,7 +395,7 @@ extension WalletsRepository { private func _transferAsync(from: Wallet, to: Wallet, amount: Int, meta: [String: String]? = nil) async throws { try await self.db.transaction { database in - guard try await self._canWithdrawAsync(from: from, amount: amount) else { + guard try await self._canWithdrawAsync(on: database, from: from, amount: amount) else { throw WalletError.insufficientBalance } try await self._withdrawAsync(on: database, from: from, amount: amount, meta: meta) @@ -397,46 +404,46 @@ extension WalletsRepository { _ = try await to.refreshBalanceAsync(on: database) } } - - private func _canWithdraw(from: Wallet, amount: Int) -> EventLoopFuture { - from.refreshBalance(on: self.db).map { $0 >= Double(amount) } - } - - private func _deposit(on db: Database, to: Wallet, amount: Int, confirmed: Bool = true, meta: [String: String]? = nil) -> EventLoopFuture { - return db.transaction { database -> EventLoopFuture in - do { - return WalletTransaction(walletID: try to.requireID(), type: .deposit, amount: amount, confirmed: confirmed, meta: meta) - .save(on: database) - } catch { - return self.db.eventLoop.makeFailedFuture(WalletError.walletNotFound(name: to.name)) - } - } - } - - private func _withdraw(on db: Database, from: Wallet, amount: Int, meta: [String: String]? = nil) -> EventLoopFuture { - return db.transaction { database -> EventLoopFuture in - do { - return WalletTransaction(walletID: try from.requireID(), type: .withdraw, amount: -1 * amount, meta: meta) - .save(on: database) - } catch { - return database.eventLoop.makeFailedFuture(WalletError.walletNotFound(name: from.name)) - } - } - } - - private func _transfer(from: Wallet, to: Wallet, amount: Int, meta: [String: String]? = nil) -> EventLoopFuture { - return self.db.transaction { (database) -> EventLoopFuture in - return self._withdraw(on: database, from: from, amount: amount, meta: meta).flatMap { _ -> EventLoopFuture in - self._deposit(on: database, to: to, amount: amount, meta: meta).flatMap { _ -> EventLoopFuture in - let refreshFrom = from.refreshBalance(on: database) - let refreshTo = to.refreshBalance(on: database) - return refreshFrom.and(refreshTo).flatMap { (_, _) -> EventLoopFuture in - database.eventLoop.makeSucceededFuture(()) - } - } - } - } - } + // + // private func _canWithdraw(from: Wallet, amount: Int) -> EventLoopFuture { + // from.refreshBalance(on: self.db).map { $0 >= Double(amount) } + // } + // + // private func _deposit(on db: Database, to: Wallet, amount: Int, confirmed: Bool = true, meta: [String: String]? = nil) -> EventLoopFuture { + // return db.transaction { database -> EventLoopFuture in + // do { + // return WalletTransaction(walletID: try to.requireID(), type: .deposit, amount: amount, confirmed: confirmed, meta: meta) + // .save(on: database) + // } catch { + // return self.db.eventLoop.makeFailedFuture(WalletError.walletNotFound(name: to.name)) + // } + // } + // } + // + // private func _withdraw(on db: Database, from: Wallet, amount: Int, meta: [String: String]? = nil) -> EventLoopFuture { + // return db.transaction { database -> EventLoopFuture in + // do { + // return WalletTransaction(walletID: try from.requireID(), type: .withdraw, amount: -1 * amount, meta: meta) + // .save(on: database) + // } catch { + // return database.eventLoop.makeFailedFuture(WalletError.walletNotFound(name: from.name)) + // } + // } + // } + // + // private func _transfer(from: Wallet, to: Wallet, amount: Int, meta: [String: String]? = nil) -> EventLoopFuture { + // return self.db.transaction { (database) -> EventLoopFuture in + // return self._withdraw(on: database, from: from, amount: amount, meta: meta).flatMap { _ -> EventLoopFuture in + // self._deposit(on: database, to: to, amount: amount, meta: meta).flatMap { _ -> EventLoopFuture in + // let refreshFrom = from.refreshBalance(on: database) + // let refreshTo = to.refreshBalance(on: database) + // return refreshFrom.and(refreshTo).flatMap { (_, _) -> EventLoopFuture in + // database.eventLoop.makeSucceededFuture(()) + // } + // } + // } + // } + // } } diff --git a/Tests/VaporWalletTests/Model+Test.swift b/Tests/VaporWalletTests/Model+Test.swift index d4b458c..23f1761 100644 --- a/Tests/VaporWalletTests/Model+Test.swift +++ b/Tests/VaporWalletTests/Model+Test.swift @@ -28,9 +28,9 @@ final class User: Model { self.username = username } - public static func create(username: String = "user1", on database: Database) throws -> User { + public static func create(username: String = "user1", on database: Database) async throws -> User { let user = User(username: username) - try user.save(on: database).wait() + try await user.save(on: database) return user } } @@ -49,24 +49,24 @@ struct CreateUser: Migration { .field("username", .string, .required) .create() } - + func revert(on database: Database) -> EventLoopFuture { return database.schema(User.schema).delete() } } - -struct CreateUserAsync: AsyncMigration { - func prepare(on database: Database) async throws { - try await database.schema(User.schema) - .id() - .field("username", .string, .required) - .create() - } - - func revert(on database: Database) async throws { - try await database.schema(User.schema).delete() - } -} +// +//struct CreateUserAsync: AsyncMigration { +// func prepare(on database: Database) async throws { +// try await database.schema(User.schema) +// .id() +// .field("username", .string, .required) +// .create() +// } +// +// func revert(on database: Database) async throws { +// try await database.schema(User.schema).delete() +// } +//} @@ -89,9 +89,9 @@ final class Game: Model { self.name = name } - public static func create(name: String = "game1", on database: Database) throws -> Game { + public static func create(name: String = "game1", on database: Database) async throws -> Game { let game = Game(name: name) - try game.save(on: database).wait() + try await game.save(on: database) return game } } @@ -110,24 +110,24 @@ struct CreateGame: Migration { .field("name", .string, .required) .create() } - + func revert(on database: Database) -> EventLoopFuture { return database.schema(Game.schema).delete() } } - -struct CreateGameAsync: AsyncMigration { - func prepare(on database: Database) async throws { - try await database.schema(Game.schema) - .id() - .field("name", .string, .required) - .create() - } - - func revert(on database: Database) async throws { - try await database.schema(Game.schema).delete() - } -} - +// +//struct CreateGameAsync: AsyncMigration { +// func prepare(on database: Database) async throws { +// try await database.schema(Game.schema) +// .id() +// .field("name", .string, .required) +// .create() +// } +// +// func revert(on database: Database) async throws { +// try await database.schema(Game.schema).delete() +// } +//} +// diff --git a/Tests/VaporWalletTests/VaporWalletAsyncTests.swift b/Tests/VaporWalletTests/VaporWalletAsyncTests.swift deleted file mode 100644 index 1187dd6..0000000 --- a/Tests/VaporWalletTests/VaporWalletAsyncTests.swift +++ /dev/null @@ -1,375 +0,0 @@ -import Vapor -import XCTest -import Fluent -import FluentSQLiteDriver -import FluentMySQLDriver -@testable import VaporWallet - -class VaporWalletAsyncTests: XCTestCase { - - private var app: Application! - - override func setUp() { - super.setUp() - - app = Application(.testing) - app.logger.logLevel = .debug - app.databases.use(.mysql(hostname: "127.0.0.1", port: 3306, username: "root", password: "hadi2400", database: "vp-test", tlsConfiguration: .none), as: .mysql) - // app.databases.use(.sqlite(.memory), as: .sqlite) - - try! migrations(app) - try! app.autoRevert().wait() - try! app.autoMigrate().wait() - try! resetDB() - - } - - func resetDB() throws { - let db = (app.db as! SQLDatabase) - let query = db.raw(""" - SELECT Concat('DELETE FROM ', TABLE_NAME, ';') as truncate_query FROM INFORMATION_SCHEMA.TABLES where `TABLE_SCHEMA` = 'vp-test' and `TABLE_NAME` not like '_fluent_%'; - """) - - return try query.all().flatMap { results in - return results.compactMap { row in - try? row.decode(column: "truncate_query", as: String.self) - }.map { query in - return (db as! MySQLDatabase).simpleQuery(query).transform(to: ()) - }.flatten(on: self.app.db.eventLoop) - }.wait() - } - - - override func tearDown() { - try! app.autoRevert().wait() - app.shutdown() - - } - - - func testAddUser() async throws { - let user = try await User.create(username: "user1", on: app.db) - XCTAssert(user.username == "user1") - } - - func testAddGame() throws { - let game = try Game.create(name: "new_game", on: app.db) - XCTAssert(game.name == "new_game") - } - - func testUserHasNoDefaultWallet() throws { - let userWithNoWallet = try User.create(on: app.db) - XCTAssertThrowsError(try userWithNoWallet.walletsRepository(on: app.db).default().wait(), "expected throw") { (error) in - XCTAssertEqual(error as! WalletError, WalletError.walletNotFound(name: WalletType.default.value)) - } - } - - - func testUserHasDefaultWallet() throws { - app.databases.middleware.use(WalletMiddleware()) - let userWithDefaultWallet = try User.create(on: app.db) - let defaultWallet = try userWithDefaultWallet.walletsRepository(on: app.db).default().wait() - - XCTAssertEqual(defaultWallet.name, WalletType.default.value) - } - - func testCreateWallet() throws { - let (_, wallets) = setupUserAndWalletsRepo(on: app.db) - try wallets.create(type: .init(name: "savings")).transform(to: ()).wait() - - let userWallets = try wallets.all().wait() - XCTAssertEqual(userWallets.count, 1) - XCTAssertEqual(userWallets.first?.name, "savings") - - } - - func testWalletDeposit() throws { - app.databases.middleware.use(WalletMiddleware()) - let (_, wallets) = setupUserAndWalletsRepo(on: app.db) - - - try! wallets.deposit(amount: 10).wait() - let balance = try wallets.balance().wait() - - XCTAssertEqual(balance, 0) - - let refreshedBalance = try wallets.refreshBalance().wait() - XCTAssertEqual(refreshedBalance, 10) - - } - - func testWalletTransactionMiddleware() throws { - app.databases.middleware.use(WalletMiddleware()) - app.databases.middleware.use(WalletTransactionMiddleware()) - - let user = try User.create(on: app.db) - let walletsRepoWithMiddleware = user.walletsRepository(on: app.db) - - try! walletsRepoWithMiddleware.deposit(amount: 40).wait() - - let balance = try walletsRepoWithMiddleware.balance().wait() - - XCTAssertEqual(balance, 40) - - } - - func testWalletWithdraw() throws { - app.databases.middleware.use(WalletMiddleware()) - app.databases.middleware.use(WalletTransactionMiddleware()) - - let (_, wallets) = setupUserAndWalletsRepo(on: app.db) - - try wallets.deposit(amount: 100).wait() - - var balance = try wallets.balance().wait() - - XCTAssertEqual(balance, 100) - - XCTAssertThrowsError(try wallets.withdraw(amount: 200).wait(), "expected throw") { (error) in - XCTAssertEqual(error as! WalletError, WalletError.insufficientBalance) - } - - try! wallets.withdraw(amount: 50).wait() - - balance = try wallets.balance().wait() - - XCTAssertEqual(balance, 50) - - } - - - func testWalletCanWithdraw() throws { - app.databases.middleware.use(WalletMiddleware()) - app.databases.middleware.use(WalletTransactionMiddleware()) - - let (_, wallets) = setupUserAndWalletsRepo(on: app.db) - - try wallets.deposit(amount: 100).wait() - - XCTAssertTrue(try! wallets.canWithdraw(amount: 100).wait()) - XCTAssertFalse(try! wallets.canWithdraw(amount: 200).wait()) - - } - - func testMultiWallet() throws { - app.databases.middleware.use(WalletTransactionMiddleware()) - app.databases.middleware.use(WalletTransactionMiddleware()) - - let (_, wallets) = setupUserAndWalletsRepo(on: app.db) - - let savingsWallet = WalletType(name: "savings") - let myWallet = WalletType(name: "my-wallet") - let notExistsWallet = WalletType(name: "not-exists") - - try wallets.create(type: myWallet).transform(to: ()).wait() - try wallets.create(type: savingsWallet).transform(to: ()).wait() - - try wallets.deposit(to: myWallet, amount: 100).wait() - try wallets.deposit(to: savingsWallet, amount: 200).wait() - - do { - try wallets.deposit(to: notExistsWallet, amount: 1000).wait() - } catch { - XCTAssertEqual(error as! WalletError, WalletError.walletNotFound(name: "not-exists")) - } - - let balance1 = try wallets.balance(type: myWallet).wait() - let balance2 = try wallets.balance(type: savingsWallet).wait() - - XCTAssertEqual(balance1, 100) - XCTAssertEqual(balance2, 200) - - } - - - func testTransactionMetadata() throws { - app.databases.middleware.use(WalletMiddleware()) - - let (_, wallets) = setupUserAndWalletsRepo(on: app.db) - - try wallets.deposit(amount: 100, meta: ["description": "payment of taxes"]).wait() - - let transaction = try wallets.default().wait() - .$transactions.get(on: app.db).wait() - .first! - - XCTAssertEqual(transaction.meta!["description"] , "payment of taxes") - } - - func testWalletDecimalBalance() throws { - app.databases.middleware.use(WalletTransactionMiddleware()) - - let user = try User.create(on: app.db) - let wallets = user.walletsRepository(on: app.db) - try wallets.create(type: .default, decimalPlaces: 2).wait() - - try wallets.deposit(amount: 100).wait() - - var balance = try wallets.balance().wait() - XCTAssertEqual(balance, 100) - - try wallets.deposit(amount: 1.45).wait() - balance = try wallets.balance().wait() - XCTAssertEqual(balance, 245) - - balance = try wallets.balance(asDecimal: true).wait() - XCTAssertEqual(balance, 2.45) - - - // decmial values will be truncated to wallet's decimalPlace value - try wallets.deposit(amount: 1.555).wait() - balance = try wallets.balance().wait() - XCTAssertEqual(balance, 400) - - } - - - func testConfirmTransaction() throws { - app.databases.middleware.use(WalletMiddleware()) - app.databases.middleware.use(WalletTransactionMiddleware()) - - let (_, wallets) = setupUserAndWalletsRepo(on: app.db) - - try wallets.deposit(amount: 10, confirmed: true).wait() - sleep(1) - try wallets.deposit(amount: 40, confirmed: false).wait() - - var balance = try wallets.balance().wait() - let unconfirmedBalance = try wallets.balance(withUnconfirmed: true).wait() - XCTAssertEqual(balance, 10) - XCTAssertEqual(unconfirmedBalance, 50) - - let transaction = try wallets.unconfirmedTransactions() - .wait() - .items.first! - - balance = try wallets.confirm(transaction: transaction).wait() - - XCTAssertEqual(balance, 50) - - } - - func testConfirmAllTransactionsOfWallet() throws { - app.databases.middleware.use(WalletMiddleware()) - app.databases.middleware.use(WalletTransactionMiddleware()) - - let (_, wallets) = setupUserAndWalletsRepo(on: app.db) - - try wallets.deposit(amount: 10, confirmed: false).wait() - try wallets.deposit(amount: 40, confirmed: false).wait() - - var balance = try wallets.balance().wait() - XCTAssertEqual(balance, 0) - - balance = try wallets.confirmAll().wait() - - XCTAssertEqual(balance, 50) - } - - - func testTransferBetweenAUsersWallets() throws { - app.databases.middleware.use(WalletMiddleware()) - app.databases.middleware.use(WalletTransactionMiddleware()) - - let (_, wallets) = setupUserAndWalletsRepo(on: app.db) - - let wallet1 = WalletType(name: "wallet1") - let wallet2 = WalletType(name: "wallet2") - - try wallets.create(type: wallet1).wait() - try wallets.create(type: wallet2).wait() - - - try wallets.deposit(to: wallet1, amount: 100).wait() - - try wallets.transafer(from: wallet1, to: wallet2, amount: 20).wait() - - let balance1 = try wallets.balance(type: wallet1).wait() - let balance2 = try wallets.balance(type: wallet2).wait() - - XCTAssertEqual(balance1, 80) - XCTAssertEqual(balance2, 20) - - } - - func testTransferBetweenTwoUsersWallets() throws { - app.databases.middleware.use(WalletMiddleware()) - app.databases.middleware.use(WalletTransactionMiddleware()) - - let user1 = try User.create(username: "user1", on: app.db) - let user2 = try User.create(username: "user2", on: app.db) - - let repo1 = user1.walletsRepository(on: app.db) - let repo2 = user2.walletsRepository(on: app.db) - - try repo1.deposit(amount: 100).wait() - - try repo1.transafer(from: try repo1.default().wait(), to: try repo2.default().wait(), amount: 20).wait() - - var balance1 = try repo1.balance().wait() - var balance2 = try repo2.balance().wait() - - XCTAssertEqual(balance1, 80) - XCTAssertEqual(balance2, 20) - - try repo1.transfer(from: .default, to: try repo2.default().wait(), amount: 20).wait() - - balance1 = try repo1.balance().wait() - balance2 = try repo2.balance().wait() - - XCTAssertEqual(balance1, 60) - XCTAssertEqual(balance2, 40) - - } - - - func testMultiModelWallet() throws { - app.databases.middleware.use(WalletMiddleware()) - app.databases.middleware.use(WalletMiddleware()) - app.databases.middleware.use(WalletTransactionMiddleware()) - - let user = try User.create(username: "user1", on: app.db) - let game = Game(id: user.id, name: "game1") - try game.save(on: app.db).wait() - - let repo1 = user.walletsRepository(on: app.db) - let repo2 = game.walletsRepository(on: app.db) - - try repo1.deposit(amount: 100).wait() - try repo2.deposit(amount: 500).wait() - - let balance1 = try repo1.balance().wait() - let balance2 = try repo2.balance().wait() - - XCTAssertEqual(balance1, 100) - XCTAssertEqual(balance2, 500) - - - - } - - - - - private func setupUserAndWalletsRepo(on: Database) -> (User, WalletsRepository) { - let user = try! User.create(on: app.db) - let wallets = user.walletsRepository(on: app.db) - - return (user, wallets) - } - - private func migrations(_ app: Application) throws { - // Initial Migrations - app.migrations.add(CreateUserAsync()) - app.migrations.add(CreateGameAsync()) - app.migrations.add(CreateWalletAsync()) - app.migrations.add(CreateWalletTransactionAsync()) - } -} -// -//extension WalletError: Equatable { -// public static func == (lhs: WalletError, rhs: WalletError) -> Bool { -// return lhs.errorDescription == rhs.errorDescription -// } -// -//} diff --git a/Tests/VaporWalletTests/VaporWalletTests.swift b/Tests/VaporWalletTests/VaporWalletTests.swift index cb90f31..8eafb37 100644 --- a/Tests/VaporWalletTests/VaporWalletTests.swift +++ b/Tests/VaporWalletTests/VaporWalletTests.swift @@ -3,6 +3,8 @@ import XCTest import Fluent import FluentSQLiteDriver import FluentMySQLDriver +import FluentPostgresDriver + @testable import VaporWallet class VaporWalletTests: XCTestCase { @@ -14,13 +16,21 @@ class VaporWalletTests: XCTestCase { app = Application(.testing) app.logger.logLevel = .debug - app.databases.use(.mysql(hostname: "127.0.0.1", port: 3306, username: "root", password: "hadi2400", database: "vp-test", tlsConfiguration: .none), as: .mysql) -// app.databases.use(.sqlite(.memory), as: .sqlite) + +// app.databases.use(.postgres( +// hostname: "localhost", +// port: 5432, +// username: "catgpt", +// password: "catgpt", +// database: "catgpt" +// ), as: .psql) + // app.databases.use(.mysql(hostname: "127.0.0.1", port: 3306, username: "vapor", password: "vapor", database: "vp-test"), as: .mysql) + app.databases.use(.sqlite(.file("catgpt-sqlite-db.sqlite")), as: .sqlite) try! migrations(app) try! app.autoRevert().wait() try! app.autoMigrate().wait() - try! resetDB() + // try! resetDB() } @@ -47,134 +57,152 @@ class VaporWalletTests: XCTestCase { } - func testAddUser() throws { - let user = try User.create(username: "user1", on: app.db) + func testAddUser() async throws { + let user = try await User.create(username: "user1", on: app.db) XCTAssert(user.username == "user1") } - func testAddGame() throws { - let game = try Game.create(name: "new_game", on: app.db) + func testAddGame() async throws { + let game = try await Game.create(name: "new_game", on: app.db) XCTAssert(game.name == "new_game") } - - func testUserHasNoDefaultWallet() throws { - let userWithNoWallet = try User.create(on: app.db) - XCTAssertThrowsError(try userWithNoWallet.walletsRepository(on: app.db).default().wait(), "expected throw") { (error) in + + func testUserHasNoDefaultWallet() async throws { + let userWithNoWallet = try await User.create(on: app.db) + await XCTAssertThrowsError(try await userWithNoWallet.walletsRepository(on: app.db).defaultAsync(), "expected throw") { (error) in XCTAssertEqual(error as! WalletError, WalletError.walletNotFound(name: WalletType.default.value)) } } - - func testUserHasDefaultWallet() throws { - app.databases.middleware.use(WalletMiddleware()) - let userWithDefaultWallet = try User.create(on: app.db) - let defaultWallet = try userWithDefaultWallet.walletsRepository(on: app.db).default().wait() + func testUserHasDefaultWallet() async throws { + app.databases.middleware.use(AsyncWalletMiddleware()) + let userWithDefaultWallet = try await User.create(on: app.db) + let defaultWallet = try await userWithDefaultWallet.walletsRepository(on: app.db).defaultAsync() XCTAssertEqual(defaultWallet.name, WalletType.default.value) } - func testCreateWallet() throws { - let (_, wallets) = setupUserAndWalletsRepo(on: app.db) - try wallets.create(type: .init(name: "savings")).transform(to: ()).wait() + func testCreateWallet() async throws { + let (_, wallets) = try await setupUserAndWalletsRepo(on: app.db) + try await wallets.createAsync(type: .init(name: "savings")) - let userWallets = try wallets.all().wait() + let userWallets = try await wallets.allAsync() XCTAssertEqual(userWallets.count, 1) XCTAssertEqual(userWallets.first?.name, "savings") } - func testWalletDeposit() throws { - app.databases.middleware.use(WalletMiddleware()) - let (_, wallets) = setupUserAndWalletsRepo(on: app.db) + func testWalletDeposit() async throws { + app.databases.middleware.use(AsyncWalletMiddleware()) + let (_, wallets) = try await setupUserAndWalletsRepo(on: app.db) - try! wallets.deposit(amount: 10).wait() - let balance = try wallets.balance().wait() + try! await wallets.depositAsync(amount: 10) + let balance = try await wallets.balanceAsync() XCTAssertEqual(balance, 0) - let refreshedBalance = try wallets.refreshBalance().wait() + let refreshedBalance = try await wallets.refreshBalanceAsync() XCTAssertEqual(refreshedBalance, 10) } - - func testWalletTransactionMiddleware() throws { - app.databases.middleware.use(WalletMiddleware()) - app.databases.middleware.use(WalletTransactionMiddleware()) - - let user = try User.create(on: app.db) + + func testWalletTransactionMiddleware() async throws { + app.databases.middleware.use(AsyncWalletMiddleware()) + app.databases.middleware.use(AsyncWalletTransactionMiddleware()) + + let user = try await User.create(on: app.db) let walletsRepoWithMiddleware = user.walletsRepository(on: app.db) - try! walletsRepoWithMiddleware.deposit(amount: 40).wait() + try! await walletsRepoWithMiddleware.depositAsync(amount: 40) - let balance = try walletsRepoWithMiddleware.balance().wait() + let balance = try await walletsRepoWithMiddleware.balanceAsync() XCTAssertEqual(balance, 40) } - func testWalletWithdraw() throws { - app.databases.middleware.use(WalletMiddleware()) - app.databases.middleware.use(WalletTransactionMiddleware()) + func testWalletWithdraw() async throws { + app.databases.middleware.use(AsyncWalletMiddleware()) + app.databases.middleware.use(AsyncWalletTransactionMiddleware()) - let (_, wallets) = setupUserAndWalletsRepo(on: app.db) + let (_, wallets) = try await setupUserAndWalletsRepo(on: app.db) - try wallets.deposit(amount: 100).wait() + try await wallets.depositAsync(amount: 100) - var balance = try wallets.balance().wait() + var balance = try await wallets.balanceAsync() XCTAssertEqual(balance, 100) - XCTAssertThrowsError(try wallets.withdraw(amount: 200).wait(), "expected throw") { (error) in + await XCTAssertThrowsError(try await wallets.withdrawAsync(amount: 200), "expected throw") { (error) in XCTAssertEqual(error as! WalletError, WalletError.insufficientBalance) } - try! wallets.withdraw(amount: 50).wait() + try! await wallets.withdrawAsync(amount: 50) - balance = try wallets.balance().wait() + balance = try await wallets.balanceAsync() XCTAssertEqual(balance, 50) } - func testWalletCanWithdraw() throws { - app.databases.middleware.use(WalletMiddleware()) - app.databases.middleware.use(WalletTransactionMiddleware()) + func testWalletCanWithdraw() async throws { + app.databases.middleware.use(AsyncWalletMiddleware()) + app.databases.middleware.use(AsyncWalletTransactionMiddleware()) + + let (_, wallets) = try await setupUserAndWalletsRepo(on: app.db) + + try await wallets.depositAsync(amount: 100) + + var can = try! await wallets.canWithdrawAsync(amount: 100) + XCTAssertTrue(can) + can = try! await wallets.canWithdrawAsync(amount: 200) + XCTAssertFalse(can) + + } + + func testWalletCanWithdrawWithMinAllowedBalance() async throws { + app.databases.middleware.use(AsyncWalletMiddleware()) + app.databases.middleware.use(AsyncWalletTransactionMiddleware()) - let (_, wallets) = setupUserAndWalletsRepo(on: app.db) + let (_, wallets) = try await setupUserAndWalletsRepo(on: app.db) + try await wallets.createAsync(type: .init(name: "magical"), minAllowedBalance: -50) - try wallets.deposit(amount: 100).wait() + + try await wallets.depositAsync(to: .init(name: "magical"), amount: 100) - XCTAssertTrue(try! wallets.canWithdraw(amount: 100).wait()) - XCTAssertFalse(try! wallets.canWithdraw(amount: 200).wait()) + var can = try! await wallets.canWithdrawAsync(from: .default, amount: 130) + XCTAssertFalse(can) + can = try! await wallets.canWithdrawAsync(from: .init(name: "magical"), amount: 130) + XCTAssertTrue(can) } - func testMultiWallet() throws { - app.databases.middleware.use(WalletTransactionMiddleware()) - app.databases.middleware.use(WalletTransactionMiddleware()) + func testMultiWallet() async throws { + app.databases.middleware.use(AsyncWalletTransactionMiddleware()) + app.databases.middleware.use(AsyncWalletTransactionMiddleware()) - let (_, wallets) = setupUserAndWalletsRepo(on: app.db) + let (_, wallets) = try await setupUserAndWalletsRepo(on: app.db) let savingsWallet = WalletType(name: "savings") let myWallet = WalletType(name: "my-wallet") let notExistsWallet = WalletType(name: "not-exists") - try wallets.create(type: myWallet).transform(to: ()).wait() - try wallets.create(type: savingsWallet).transform(to: ()).wait() - - try wallets.deposit(to: myWallet, amount: 100).wait() - try wallets.deposit(to: savingsWallet, amount: 200).wait() + try await wallets.createAsync(type: myWallet) + try await wallets.createAsync(type: savingsWallet) + + try await wallets.depositAsync(to: myWallet, amount: 100) + try await wallets.depositAsync(to: savingsWallet, amount: 200) do { - try wallets.deposit(to: notExistsWallet, amount: 1000).wait() + try await wallets.depositAsync(to: notExistsWallet, amount: 1000) } catch { XCTAssertEqual(error as! WalletError, WalletError.walletNotFound(name: "not-exists")) } - let balance1 = try wallets.balance(type: myWallet).wait() - let balance2 = try wallets.balance(type: savingsWallet).wait() + let balance1 = try await wallets.balanceAsync(type: myWallet) + let balance2 = try await wallets.balanceAsync(type: savingsWallet) XCTAssertEqual(balance1, 100) XCTAssertEqual(balance2, 200) @@ -182,182 +210,196 @@ class VaporWalletTests: XCTestCase { } - func testTransactionMetadata() throws { - app.databases.middleware.use(WalletMiddleware()) + func testTransactionMetadata() async throws { + app.databases.middleware.use(AsyncWalletMiddleware()) - let (_, wallets) = setupUserAndWalletsRepo(on: app.db) + let (_, wallets) = try await setupUserAndWalletsRepo(on: app.db) - try wallets.deposit(amount: 100, meta: ["description": "payment of taxes"]).wait() - - let transaction = try wallets.default().wait() - .$transactions.get(on: app.db).wait() + try await wallets.depositAsync(amount: 100, meta: ["description": "tax payments"]) + + let transaction = try await wallets.defaultAsync() + .$transactions.get(on: app.db) .first! - - XCTAssertEqual(transaction.meta!["description"] , "payment of taxes") + + XCTAssertEqual(transaction.meta!["description"] , "tax payments") } - func testWalletDecimalBalance() throws { - app.databases.middleware.use(WalletTransactionMiddleware()) + func testWalletDecimalBalance() async throws { + app.databases.middleware.use(AsyncWalletTransactionMiddleware()) - let user = try User.create(on: app.db) + let user = try await User.create(on: app.db) let wallets = user.walletsRepository(on: app.db) - try wallets.create(type: .default, decimalPlaces: 2).wait() + try await wallets.createAsync(type: .default, decimalPlaces: 2) - try wallets.deposit(amount: 100).wait() - - var balance = try wallets.balance().wait() + try await wallets.depositAsync(amount: 100) + + var balance = try await wallets.balanceAsync() XCTAssertEqual(balance, 100) - - try wallets.deposit(amount: 1.45).wait() - balance = try wallets.balance().wait() + + try await wallets.depositAsync(amount: 1.45) + balance = try await wallets.balanceAsync() XCTAssertEqual(balance, 245) - - balance = try wallets.balance(asDecimal: true).wait() + + balance = try await wallets.balanceAsync(asDecimal: true) XCTAssertEqual(balance, 2.45) - - + + // decmial values will be truncated to wallet's decimalPlace value - try wallets.deposit(amount: 1.555).wait() - balance = try wallets.balance().wait() + try await wallets.depositAsync(amount: 1.555) + balance = try await wallets.balanceAsync() XCTAssertEqual(balance, 400) } - - - func testConfirmTransaction() throws { - app.databases.middleware.use(WalletMiddleware()) - app.databases.middleware.use(WalletTransactionMiddleware()) - let (_, wallets) = setupUserAndWalletsRepo(on: app.db) - - try wallets.deposit(amount: 10, confirmed: true).wait() + + func testConfirmTransaction() async throws { + app.databases.middleware.use(AsyncWalletMiddleware()) + app.databases.middleware.use(AsyncWalletTransactionMiddleware()) + + let (_, wallets) = try await setupUserAndWalletsRepo(on: app.db) + + try await wallets.depositAsync(amount: 10, confirmed: true) sleep(1) - try wallets.deposit(amount: 40, confirmed: false).wait() + try await wallets.depositAsync(amount: 40, confirmed: false) - var balance = try wallets.balance().wait() - let unconfirmedBalance = try wallets.balance(withUnconfirmed: true).wait() + var balance = try await wallets.balanceAsync() + let unconfirmedBalance = try await wallets.balanceAsync(withUnconfirmed: true) XCTAssertEqual(balance, 10) XCTAssertEqual(unconfirmedBalance, 50) - let transaction = try wallets.unconfirmedTransactions() - .wait() - .items.first! + let transaction = try await wallets + .unconfirmedTransactionsAsync() + .items + .first! - balance = try wallets.confirm(transaction: transaction).wait() + balance = try await wallets.confirmAsync(transaction: transaction) XCTAssertEqual(balance, 50) } - - func testConfirmAllTransactionsOfWallet() throws { - app.databases.middleware.use(WalletMiddleware()) - app.databases.middleware.use(WalletTransactionMiddleware()) - let (_, wallets) = setupUserAndWalletsRepo(on: app.db) - - try wallets.deposit(amount: 10, confirmed: false).wait() - try wallets.deposit(amount: 40, confirmed: false).wait() - - var balance = try wallets.balance().wait() + func testConfirmAllTransactionsOfWallet() async throws { + app.databases.middleware.use(AsyncWalletMiddleware()) + app.databases.middleware.use(AsyncWalletTransactionMiddleware()) + + let (_, wallets) = try await setupUserAndWalletsRepo(on: app.db) + + try await wallets.depositAsync(amount: 10, confirmed: false) + try await wallets.depositAsync(amount: 40, confirmed: false) + + var balance = try await wallets.balanceAsync() XCTAssertEqual(balance, 0) - balance = try wallets.confirmAll().wait() + balance = try await wallets.confirmAllAsync() XCTAssertEqual(balance, 50) } - - - func testTransferBetweenAUsersWallets() throws { - app.databases.middleware.use(WalletMiddleware()) - app.databases.middleware.use(WalletTransactionMiddleware()) - - let (_, wallets) = setupUserAndWalletsRepo(on: app.db) - + + + func testTransferBetweenAUsersWallets() async throws { + app.databases.middleware.use(AsyncWalletMiddleware()) + app.databases.middleware.use(AsyncWalletTransactionMiddleware()) + + let (_, wallets) = try await setupUserAndWalletsRepo(on: app.db) + let wallet1 = WalletType(name: "wallet1") let wallet2 = WalletType(name: "wallet2") - - try wallets.create(type: wallet1).wait() - try wallets.create(type: wallet2).wait() - - - try wallets.deposit(to: wallet1, amount: 100).wait() - try wallets.transafer(from: wallet1, to: wallet2, amount: 20).wait() - - let balance1 = try wallets.balance(type: wallet1).wait() - let balance2 = try wallets.balance(type: wallet2).wait() - + try await wallets.createAsync(type: wallet1) + try await wallets.createAsync(type: wallet2) + + + try await wallets.depositAsync(to: wallet1, amount: 100) + + try await wallets.transferAsync(from: wallet1, to: wallet2, amount: 20) + + let balance1 = try await wallets.balanceAsync(type: wallet1) + let balance2 = try await wallets.balanceAsync(type: wallet2) + XCTAssertEqual(balance1, 80) XCTAssertEqual(balance2, 20) } - - func testTransferBetweenTwoUsersWallets() throws { - app.databases.middleware.use(WalletMiddleware()) - app.databases.middleware.use(WalletTransactionMiddleware()) - let user1 = try User.create(username: "user1", on: app.db) - let user2 = try User.create(username: "user2", on: app.db) - + func testTransferBetweenTwoUsersWallets() async throws { + app.databases.middleware.use(AsyncWalletMiddleware()) + app.databases.middleware.use(AsyncWalletTransactionMiddleware()) + + let user1 = try await User.create(username: "user1", on: app.db) + let user2 = try await User.create(username: "user2", on: app.db) + let repo1 = user1.walletsRepository(on: app.db) let repo2 = user2.walletsRepository(on: app.db) - - try repo1.deposit(amount: 100).wait() - - try repo1.transafer(from: try repo1.default().wait(), to: try repo2.default().wait(), amount: 20).wait() - - var balance1 = try repo1.balance().wait() - var balance2 = try repo2.balance().wait() - + + try await repo1.depositAsync(amount: 100) + + try await repo1.transferAsync(from: try await repo1.defaultAsync(), to: try await repo2.defaultAsync(), amount: 20) + + var balance1 = try await repo1.balanceAsync() + var balance2 = try await repo2.balanceAsync() + XCTAssertEqual(balance1, 80) XCTAssertEqual(balance2, 20) - try repo1.transfer(from: .default, to: try repo2.default().wait(), amount: 20).wait() - - balance1 = try repo1.balance().wait() - balance2 = try repo2.balance().wait() - + try await repo1.transferAsync(from: .default, to: try await repo2.defaultAsync(), amount: 20) + + balance1 = try await repo1.balanceAsync() + balance2 = try await repo2.balanceAsync() + XCTAssertEqual(balance1, 60) XCTAssertEqual(balance2, 40) + let savings = WalletType(name: "savings") + try await repo1.createAsync(type: savings) + + try await repo1.transferAsync(from: .default, to: savings, amount: 20) + + balance1 = try await repo1.balanceAsync() + balance2 = try await repo1.balanceAsync(type: savings) + + XCTAssertEqual(balance1, 40) + XCTAssertEqual(balance2, 20) + } - - - func testMultiModelWallet() throws { - app.databases.middleware.use(WalletMiddleware()) - app.databases.middleware.use(WalletMiddleware()) - app.databases.middleware.use(WalletTransactionMiddleware()) - let user = try User.create(username: "user1", on: app.db) + + func testMultiModelWallet() async throws { + app.databases.middleware.use(AsyncWalletMiddleware()) + app.databases.middleware.use(AsyncWalletMiddleware()) + app.databases.middleware.use(AsyncWalletTransactionMiddleware()) + + let user = try await User.create(username: "user1", on: app.db) let game = Game(id: user.id, name: "game1") - try game.save(on: app.db).wait() - + try await game.save(on: app.db) + let repo1 = user.walletsRepository(on: app.db) let repo2 = game.walletsRepository(on: app.db) - try repo1.deposit(amount: 100).wait() - try repo2.deposit(amount: 500).wait() - - let balance1 = try repo1.balance().wait() - let balance2 = try repo2.balance().wait() - - XCTAssertEqual(balance1, 100) - XCTAssertEqual(balance2, 500) + try await repo1.depositAsync(amount: 100) +// try await repo2.depositAsync(amount: 500) +// let userWallet = try await repo1.getAsync(type: .default) +// let gameWallet = try await repo2.getAsync(type: .default) + +// let balance1 = try await repo1.balanceAsync() +// let balance2 = try await repo2.balanceAsync() +// +// XCTAssertEqual(balance1, 100) +// XCTAssertEqual(balance2, 500) } - - - - - private func setupUserAndWalletsRepo(on: Database) -> (User, WalletsRepository) { - let user = try! User.create(on: app.db) + + + + + private func setupUserAndWalletsRepo(on: Database) async throws -> (User, WalletsRepository) { + let user = try! await User.create(on: app.db) let wallets = user.walletsRepository(on: app.db) return (user, wallets) } - + private func migrations(_ app: Application) throws { // Initial Migrations app.migrations.add(CreateUser()) diff --git a/Tests/VaporWalletTests/XCTest+async.swift b/Tests/VaporWalletTests/XCTest+async.swift new file mode 100644 index 0000000..e782e08 --- /dev/null +++ b/Tests/VaporWalletTests/XCTest+async.swift @@ -0,0 +1,25 @@ +// +// File.swift +// +// +// Created by Hadi Sharghi on 6/14/23. +// + +import XCTest + +extension XCTest { + func XCTAssertThrowsError( + _ expression: @autoclosure () async throws -> T, + _ message: @autoclosure () -> String = "", + file: StaticString = #filePath, + line: UInt = #line, + _ errorHandler: (_ error: Error) -> Void = { _ in } + ) async { + do { + _ = try await expression() + XCTFail(message(), file: file, line: line) + } catch { + errorHandler(error) + } + } +} From ff934e55c861885e075fcbf0a89661a4e8a69119 Mon Sep 17 00:00:00 2001 From: Hadi Sharghi Date: Wed, 14 Jun 2023 19:31:36 +0330 Subject: [PATCH 4/9] async migration for transactions table --- .../Migrations/CreateWalletTransaction.swift | 46 +++++++++++-------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/Sources/VaporWallet/Migrations/CreateWalletTransaction.swift b/Sources/VaporWallet/Migrations/CreateWalletTransaction.swift index 2465755..740702c 100644 --- a/Sources/VaporWallet/Migrations/CreateWalletTransaction.swift +++ b/Sources/VaporWallet/Migrations/CreateWalletTransaction.swift @@ -4,37 +4,43 @@ public struct CreateWalletTransaction: Migration { public init() { } public func prepare(on database: Database) -> EventLoopFuture { - return database.enum("type").case("deposit").case("withdraw").create().flatMap { transactionType in - return database.schema(WalletTransaction.schema) - .id() - .field("wallet_id", .uuid, .required, .references(Wallet.schema, "id", onDelete: .cascade)) - .field("type", transactionType, .required) - .field("amount", .int, .required) - .field("confirmed", .bool, .required) - .field("meta", .json) - .field("created_at", .datetime, .required) - .field("updated_at", .datetime, .required) - .create() - } + return database.enum("type") + .case("deposit") + .case("withdraw") + .create().flatMap { transactionType in + return database.schema(WalletTransaction.schema) + .id() + .field("wallet_id", .uuid, .required, .references(Wallet.schema, "id", onDelete: .cascade)) + .field("type", transactionType, .required) + .field("amount", .int, .required) + .field("confirmed", .bool, .required) + .field("meta", .json) + .field("created_at", .datetime, .required) + .field("updated_at", .datetime, .required) + .create() + } } public func revert(on database: Database) -> EventLoopFuture { - return database.enum("type").deleteCase("deposit").deleteCase("withdraw").update().flatMap { _ in - return database.schema(WalletTransaction.schema).delete() - } + return database.enum("type") + .deleteCase("deposit") + .deleteCase("withdraw") + .update().flatMap { _ in + return database.schema(WalletTransaction.schema).delete() + } } } public struct CreateWalletTransactionAsync: AsyncMigration { public init() { } - + public func prepare(on database: Database) async throws { let transactionType = try await database.enum("type") .case("deposit") .case("withdraw") .create() - + try await database.schema(WalletTransaction.schema) .id() .field("wallet_id", .uuid, .required, .references(Wallet.schema, "id", onDelete: .cascade)) @@ -46,8 +52,8 @@ public struct CreateWalletTransactionAsync: AsyncMigration { .field("updated_at", .datetime, .required) .create() } - - + + public func revert(on database: Database) async throws { let _ = try await database.enum("type") .deleteCase("deposit") @@ -55,5 +61,5 @@ public struct CreateWalletTransactionAsync: AsyncMigration { .update() try await database.schema(WalletTransaction.schema).delete() } - + } From b72ce3f2496c1183743902ca23951520a18d3a9e Mon Sep 17 00:00:00 2001 From: Hadi Sharghi Date: Wed, 14 Jun 2023 19:44:27 +0330 Subject: [PATCH 5/9] reading enum type before use --- .../Migrations/CreateWalletTransaction.swift | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/Sources/VaporWallet/Migrations/CreateWalletTransaction.swift b/Sources/VaporWallet/Migrations/CreateWalletTransaction.swift index 740702c..8c13d25 100644 --- a/Sources/VaporWallet/Migrations/CreateWalletTransaction.swift +++ b/Sources/VaporWallet/Migrations/CreateWalletTransaction.swift @@ -36,11 +36,12 @@ public struct CreateWalletTransactionAsync: AsyncMigration { public init() { } public func prepare(on database: Database) async throws { - let transactionType = try await database.enum("type") + _ = try await database.enum("type") .case("deposit") .case("withdraw") .create() - + + let transactionType = try await database.enum("type").read() try await database.schema(WalletTransaction.schema) .id() .field("wallet_id", .uuid, .required, .references(Wallet.schema, "id", onDelete: .cascade)) @@ -55,10 +56,7 @@ public struct CreateWalletTransactionAsync: AsyncMigration { public func revert(on database: Database) async throws { - let _ = try await database.enum("type") - .deleteCase("deposit") - .deleteCase("withdraw") - .update() + let _ = try await database.enum("type").delete() try await database.schema(WalletTransaction.schema).delete() } From b369f769c15826e8cc8efcce373b5242dd839dc6 Mon Sep 17 00:00:00 2001 From: Hadi Sharghi Date: Thu, 15 Jun 2023 17:18:54 +0330 Subject: [PATCH 6/9] fix saving wrong value to owner_type --- Sources/VaporWallet/WalletRepository.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/VaporWallet/WalletRepository.swift b/Sources/VaporWallet/WalletRepository.swift index f0cf252..00fe0dc 100644 --- a/Sources/VaporWallet/WalletRepository.swift +++ b/Sources/VaporWallet/WalletRepository.swift @@ -33,7 +33,7 @@ public class WalletsRepository { extension WalletsRepository { public func createAsync(type name: WalletType = .default, decimalPlaces: UInt8 = 2, minAllowedBalance: Int = 0) async throws { - let wallet: Wallet = Wallet(ownerType: String(describing: self), + let wallet: Wallet = Wallet(ownerType: self.type, ownerID: self.id, name: name.value, minAllowedBalance: minAllowedBalance, @@ -373,7 +373,7 @@ extension WalletsRepository { try await db.transaction { database in var walletTransaction: WalletTransaction do { - walletTransaction = WalletTransaction(walletID: try to.requireID(), type: .deposit, amount: amount, confirmed: confirmed, meta: meta) + walletTransaction = WalletTransaction(walletID: try to.requireID(), transactionType: .deposit, amount: amount, confirmed: confirmed, meta: meta) } catch { throw WalletError.walletNotFound(name: to.name) } @@ -385,7 +385,7 @@ extension WalletsRepository { try await db.transaction { database in var walletTransaction: WalletTransaction do { - walletTransaction = WalletTransaction(walletID: try from.requireID(), type: .withdraw, amount: -1 * amount, meta: meta) + walletTransaction = WalletTransaction(walletID: try from.requireID(), transactionType: .withdraw, amount: -1 * amount, meta: meta) } catch { throw WalletError.walletNotFound(name: from.name) } From a999bb085cb93baf5404673e8ce06ec9a684ffb0 Mon Sep 17 00:00:00 2001 From: Hadi Sharghi Date: Thu, 15 Jun 2023 17:19:20 +0330 Subject: [PATCH 7/9] rename transaction type column to transaction_type --- Sources/VaporWallet/HasWallet.swift | 2 +- .../Migrations/CreateWalletTransaction.swift | 13 ++- .../Models/Entities/WalletTransaction.swift | 17 ++-- Tests/VaporWalletTests/VaporWalletTests.swift | 84 +++++++++++++------ 4 files changed, 77 insertions(+), 39 deletions(-) diff --git a/Sources/VaporWallet/HasWallet.swift b/Sources/VaporWallet/HasWallet.swift index f1313e2..91cde0e 100644 --- a/Sources/VaporWallet/HasWallet.swift +++ b/Sources/VaporWallet/HasWallet.swift @@ -31,7 +31,7 @@ extension Wallet { .query(on: db) .filter(\.$confirmed == true) .sum(\.$amount) - .get() + self.balance = balance ?? 0 try await self.update(on: db) return Double(self.balance) diff --git a/Sources/VaporWallet/Migrations/CreateWalletTransaction.swift b/Sources/VaporWallet/Migrations/CreateWalletTransaction.swift index 8c13d25..6c4843a 100644 --- a/Sources/VaporWallet/Migrations/CreateWalletTransaction.swift +++ b/Sources/VaporWallet/Migrations/CreateWalletTransaction.swift @@ -36,16 +36,19 @@ public struct CreateWalletTransactionAsync: AsyncMigration { public init() { } public func prepare(on database: Database) async throws { - _ = try await database.enum("type") + do { + try await database.enum("transaction_type").delete() + } catch { } + + let transactionType = try await database.enum("transaction_type") .case("deposit") .case("withdraw") .create() - let transactionType = try await database.enum("type").read() try await database.schema(WalletTransaction.schema) .id() .field("wallet_id", .uuid, .required, .references(Wallet.schema, "id", onDelete: .cascade)) - .field("type", transactionType, .required) + .field("transaction_type", transactionType, .required) .field("amount", .int, .required) .field("confirmed", .bool, .required) .field("meta", .json) @@ -56,7 +59,9 @@ public struct CreateWalletTransactionAsync: AsyncMigration { public func revert(on database: Database) async throws { - let _ = try await database.enum("type").delete() + do { + try await database.enum("transaction_type").delete() + } catch { } try await database.schema(WalletTransaction.schema).delete() } diff --git a/Sources/VaporWallet/Models/Entities/WalletTransaction.swift b/Sources/VaporWallet/Models/Entities/WalletTransaction.swift index 854810a..24e69f2 100644 --- a/Sources/VaporWallet/Models/Entities/WalletTransaction.swift +++ b/Sources/VaporWallet/Models/Entities/WalletTransaction.swift @@ -9,13 +9,14 @@ import Vapor import Fluent +enum TransactionType: String, Content { + case deposit, withdraw +} + public final class WalletTransaction: Model { public static let schema = "wallet_transactions" - - enum TransactionType: String, Content { - case deposit, withdraw - } + @ID(key: .id) public var id: UUID? @@ -23,8 +24,8 @@ public final class WalletTransaction: Model { @Parent(key: "wallet_id") var wallet: Wallet - @Enum(key: "type") - var type: TransactionType + @Enum(key: "transaction_type") + var transactionType: TransactionType @Field(key: "amount") var amount: Int @@ -46,7 +47,7 @@ public final class WalletTransaction: Model { init( id: UUID? = nil, walletID: UUID, - type: TransactionType, + transactionType: TransactionType, amount: Int, confirmed: Bool = true, meta: [String: String]? = nil, @@ -55,7 +56,7 @@ public final class WalletTransaction: Model { ) { self.id = id self.$wallet.id = walletID - self.type = type + self.transactionType = transactionType self.amount = amount self.meta = meta self.confirmed = confirmed diff --git a/Tests/VaporWalletTests/VaporWalletTests.swift b/Tests/VaporWalletTests/VaporWalletTests.swift index 8eafb37..d6dde8d 100644 --- a/Tests/VaporWalletTests/VaporWalletTests.swift +++ b/Tests/VaporWalletTests/VaporWalletTests.swift @@ -17,15 +17,15 @@ class VaporWalletTests: XCTestCase { app = Application(.testing) app.logger.logLevel = .debug -// app.databases.use(.postgres( -// hostname: "localhost", -// port: 5432, -// username: "catgpt", -// password: "catgpt", -// database: "catgpt" -// ), as: .psql) - // app.databases.use(.mysql(hostname: "127.0.0.1", port: 3306, username: "vapor", password: "vapor", database: "vp-test"), as: .mysql) - app.databases.use(.sqlite(.file("catgpt-sqlite-db.sqlite")), as: .sqlite) + app.databases.use(.postgres( + hostname: "localhost", + port: 5432, + username: "catgpt", + password: "catgpt", + database: "catgpt" + ), as: .psql) +// app.databases.use(.mysql(hostname: "127.0.0.1", port: 3306, username: "vapor", password: "vapor", database: "vp-test"), as: .mysql) +// app.databases.use(.sqlite(.memory), as: .sqlite) try! migrations(app) try! app.autoRevert().wait() @@ -368,25 +368,57 @@ class VaporWalletTests: XCTestCase { app.databases.middleware.use(AsyncWalletMiddleware()) app.databases.middleware.use(AsyncWalletTransactionMiddleware()) - let user = try await User.create(username: "user1", on: app.db) - let game = Game(id: user.id, name: "game1") - try await game.save(on: app.db) - - let repo1 = user.walletsRepository(on: app.db) - let repo2 = game.walletsRepository(on: app.db) + do { + let user = try await User.create(username: "user1", on: app.db) + let game = Game(id: user.id, name: "game1") + try await game.save(on: app.db) + + let repo1 = user.walletsRepository(on: app.db) + let repo2 = game.walletsRepository(on: app.db) + + try await repo1.depositAsync(amount: 100) + try await repo2.depositAsync(amount: 500) + + let balance1 = try await repo1.balanceAsync() + let balance2 = try await repo2.balanceAsync() + + XCTAssertEqual(balance1, 100) + XCTAssertEqual(balance2, 500) + } catch { + print("error: \(String(reflecting: error))") + } - try await repo1.depositAsync(amount: 100) -// try await repo2.depositAsync(amount: 500) + } -// let userWallet = try await repo1.getAsync(type: .default) -// let gameWallet = try await repo2.getAsync(type: .default) - -// let balance1 = try await repo1.balanceAsync() -// let balance2 = try await repo2.balanceAsync() -// -// XCTAssertEqual(balance1, 100) -// XCTAssertEqual(balance2, 500) + func testMultiModelWalletTransfer() async throws { + app.databases.middleware.use(AsyncWalletMiddleware()) + app.databases.middleware.use(AsyncWalletMiddleware()) + app.databases.middleware.use(AsyncWalletTransactionMiddleware()) + do { + let user = try await User.create(username: "user1", on: app.db) + let game = Game(id: user.id, name: "game1") + try await game.save(on: app.db) + + let repo1 = user.walletsRepository(on: app.db) + let repo2 = game.walletsRepository(on: app.db) + + try await repo1.depositAsync(amount: 100) + try await repo2.depositAsync(amount: 500) + + let userWallet = try await repo1.defaultAsync() + let gameWallet = try await repo2.defaultAsync() + + try await repo1.transferAsync(from: gameWallet, to: userWallet, amount: 100) + + let balance1 = try await repo1.balanceAsync() + let balance2 = try await repo2.balanceAsync() + + XCTAssertEqual(balance1, 200) + XCTAssertEqual(balance2, 400) + } catch { + print("###### error: #########\n\(String(reflecting: error))") + } } @@ -405,7 +437,7 @@ class VaporWalletTests: XCTestCase { app.migrations.add(CreateUser()) app.migrations.add(CreateGame()) app.migrations.add(CreateWallet()) - app.migrations.add(CreateWalletTransaction()) + app.migrations.add(CreateWalletTransactionAsync()) } } From 713b57756185fa4c3501d0d8babaf5ec3323253c Mon Sep 17 00:00:00 2001 From: Hadi Sharghi Date: Sat, 17 Jun 2023 20:55:40 +0330 Subject: [PATCH 8/9] fix aggregates bug for postgres. --- Sources/VaporWallet/HasWallet.swift | 26 +- .../VaporWallet/Migrations/CreateWallet.swift | 64 ++-- .../Migrations/CreateWalletTransaction.swift | 73 ++-- Sources/VaporWallet/WalletRepository.swift | 25 +- Tests/VaporWalletTests/VaporWalletTests.swift | 357 +++++++++--------- 5 files changed, 277 insertions(+), 268 deletions(-) diff --git a/Sources/VaporWallet/HasWallet.swift b/Sources/VaporWallet/HasWallet.swift index 91cde0e..fa29e9d 100644 --- a/Sources/VaporWallet/HasWallet.swift +++ b/Sources/VaporWallet/HasWallet.swift @@ -8,6 +8,7 @@ import Vapor import Fluent +import FluentPostgresDriver public protocol HasWallet: FluentKit.Model { static var idKey: KeyPath> { get } @@ -27,12 +28,27 @@ extension HasWallet { extension Wallet { public func refreshBalanceAsync(on db: Database) async throws -> Double { - let balance = try await self.$transactions - .query(on: db) - .filter(\.$confirmed == true) - .sum(\.$amount) + + var balance: Int + // Temporary workaround for sum and average aggregates on Postgres DB + if let _ = db as? PostgresDatabase { + let balanceOptional = try? await self.$transactions + .query(on: db) + .filter(\.$confirmed == true) + .aggregate(.sum, \.$amount, as: Double.self) + + balance = balanceOptional == nil ? 0 : Int(balanceOptional!) + } else { + let intBalance = try await self.$transactions + .query(on: db) + .filter(\.$confirmed == true) + .sum(\.$amount) + + balance = intBalance ?? 0 + } - self.balance = balance ?? 0 + self.balance = balance + try await self.update(on: db) return Double(self.balance) } diff --git a/Sources/VaporWallet/Migrations/CreateWallet.swift b/Sources/VaporWallet/Migrations/CreateWallet.swift index 8900a2e..0ddca1a 100644 --- a/Sources/VaporWallet/Migrations/CreateWallet.swift +++ b/Sources/VaporWallet/Migrations/CreateWallet.swift @@ -1,38 +1,38 @@ import Fluent import SQLKit -public struct CreateWallet: Migration { - private var idKey: String - public init(foreignKeyColumnName idKey: String = "id") { - self.idKey = idKey - } - - public func prepare(on database: Database) -> EventLoopFuture { - return database.schema(Wallet.schema) - .id() - .field("name", .string, .required) - .field("owner_type", .string, .required) - .field("owner_id", .uuid, .required) - .field("min_allowed_balance", .int, .required) - .field("balance", .int, .required) - .field("decimal_places", .uint8, .required) - .field("created_at", .datetime, .required) - .field("updated_at", .datetime, .required) - .field("deleted_at", .datetime) - .create().flatMap { _ in - let sqlDB = (database as! SQLDatabase) - return sqlDB - .create(index: "type_idx") - .on(Wallet.schema) - .column("owner_type") - .run() - } - } - - public func revert(on database: Database) -> EventLoopFuture { - return database.schema(Wallet.schema).delete() - } -} +//public struct CreateWallet: Migration { +// private var idKey: String +// public init(foreignKeyColumnName idKey: String = "id") { +// self.idKey = idKey +// } +// +// public func prepare(on database: Database) -> EventLoopFuture { +// return database.schema(Wallet.schema) +// .id() +// .field("name", .string, .required) +// .field("owner_type", .string, .required) +// .field("owner_id", .uuid, .required) +// .field("min_allowed_balance", .int, .required) +// .field("balance", .int, .required) +// .field("decimal_places", .uint8, .required) +// .field("created_at", .datetime, .required) +// .field("updated_at", .datetime, .required) +// .field("deleted_at", .datetime) +// .create().flatMap { _ in +// let sqlDB = (database as! SQLDatabase) +// return sqlDB +// .create(index: "type_idx") +// .on(Wallet.schema) +// .column("owner_type") +// .run() +// } +// } +// +// public func revert(on database: Database) -> EventLoopFuture { +// return database.schema(Wallet.schema).delete() +// } +//} public struct CreateWalletAsync: AsyncMigration { private var idKey: String diff --git a/Sources/VaporWallet/Migrations/CreateWalletTransaction.swift b/Sources/VaporWallet/Migrations/CreateWalletTransaction.swift index 6c4843a..e5103d2 100644 --- a/Sources/VaporWallet/Migrations/CreateWalletTransaction.swift +++ b/Sources/VaporWallet/Migrations/CreateWalletTransaction.swift @@ -1,50 +1,47 @@ import Fluent -public struct CreateWalletTransaction: Migration { - public init() { } - - public func prepare(on database: Database) -> EventLoopFuture { - return database.enum("type") - .case("deposit") - .case("withdraw") - .create().flatMap { transactionType in - return database.schema(WalletTransaction.schema) - .id() - .field("wallet_id", .uuid, .required, .references(Wallet.schema, "id", onDelete: .cascade)) - .field("type", transactionType, .required) - .field("amount", .int, .required) - .field("confirmed", .bool, .required) - .field("meta", .json) - .field("created_at", .datetime, .required) - .field("updated_at", .datetime, .required) - .create() - } - } - - public func revert(on database: Database) -> EventLoopFuture { - return database.enum("type") - .deleteCase("deposit") - .deleteCase("withdraw") - .update().flatMap { _ in - return database.schema(WalletTransaction.schema).delete() - } - - } -} +//public struct CreateWalletTransaction: Migration { +// public init() { } +// +// public func prepare(on database: Database) -> EventLoopFuture { +// return database.enum("type") +// .case("deposit") +// .case("withdraw") +// .create().flatMap { transactionType in +// return database.schema(WalletTransaction.schema) +// .id() +// .field("wallet_id", .uuid, .required, .references(Wallet.schema, "id", onDelete: .cascade)) +// .field("type", transactionType, .required) +// .field("amount", .int, .required) +// .field("confirmed", .bool, .required) +// .field("meta", .json) +// .field("created_at", .datetime, .required) +// .field("updated_at", .datetime, .required) +// .create() +// } +// } +// +// public func revert(on database: Database) -> EventLoopFuture { +// return database.enum("type") +// .deleteCase("deposit") +// .deleteCase("withdraw") +// .update().flatMap { _ in +// return database.schema(WalletTransaction.schema).delete() +// } +// +// } +//} public struct CreateWalletTransactionAsync: AsyncMigration { public init() { } public func prepare(on database: Database) async throws { - do { - try await database.enum("transaction_type").delete() - } catch { } - + let transactionType = try await database.enum("transaction_type") .case("deposit") .case("withdraw") .create() - + try await database.schema(WalletTransaction.schema) .id() .field("wallet_id", .uuid, .required, .references(Wallet.schema, "id", onDelete: .cascade)) @@ -59,10 +56,8 @@ public struct CreateWalletTransactionAsync: AsyncMigration { public func revert(on database: Database) async throws { - do { - try await database.enum("transaction_type").delete() - } catch { } try await database.schema(WalletTransaction.schema).delete() + try await database.enum("transaction_type").delete() } } diff --git a/Sources/VaporWallet/WalletRepository.swift b/Sources/VaporWallet/WalletRepository.swift index 00fe0dc..55f9537 100644 --- a/Sources/VaporWallet/WalletRepository.swift +++ b/Sources/VaporWallet/WalletRepository.swift @@ -7,6 +7,7 @@ import Vapor import Fluent +import FluentPostgresDriver /// This calss gives access to wallet methods for a `HasWallet` model. /// Creating multiple wallets, accessing them and getting balance of each wallet, @@ -53,7 +54,7 @@ extension WalletsRepository { .filter(\.$owner == self.id) .filter(\.$ownerType == self.type) .filter(\.$name == name.value) - + if (withTransactions) { walletQuery = walletQuery.with(\.$transactions) } @@ -72,13 +73,21 @@ extension WalletsRepository { public func balanceAsync(type name: WalletType = .default, withUnconfirmed: Bool = false, asDecimal: Bool = false) async throws -> Double { let wallet = try await getAsync(type: name) if withUnconfirmed { - let intBalance = try await wallet.$transactions - .query(on: self.db) - .sum(\.$amount) - .get() - - let balance = intBalance == nil ? 0.0 : Double(intBalance!) - + // (1) Temporary workaround for sum and average aggregates, + var balance: Double + if let _ = self.db as? PostgresDatabase { + let balanceOptional = try? await wallet.$transactions + .query(on: self.db) + .aggregate(.sum, \.$amount, as: Double.self) + + balance = balanceOptional ?? 0.0 + } else { + let intBalance = try await wallet.$transactions + .query(on: self.db) + .sum(\.$amount) + + balance = intBalance == nil ? 0.0 : Double(intBalance!) + } return asDecimal ? balance.toDecimal(with: wallet.decimalPlaces) : balance } return asDecimal ? Double(wallet.balance).toDecimal(with: wallet.decimalPlaces) : Double(wallet.balance) diff --git a/Tests/VaporWalletTests/VaporWalletTests.swift b/Tests/VaporWalletTests/VaporWalletTests.swift index d6dde8d..d93fe1c 100644 --- a/Tests/VaporWalletTests/VaporWalletTests.swift +++ b/Tests/VaporWalletTests/VaporWalletTests.swift @@ -16,16 +16,16 @@ class VaporWalletTests: XCTestCase { app = Application(.testing) app.logger.logLevel = .debug - - app.databases.use(.postgres( - hostname: "localhost", - port: 5432, - username: "catgpt", - password: "catgpt", - database: "catgpt" - ), as: .psql) -// app.databases.use(.mysql(hostname: "127.0.0.1", port: 3306, username: "vapor", password: "vapor", database: "vp-test"), as: .mysql) -// app.databases.use(.sqlite(.memory), as: .sqlite) + + // app.databases.use(.postgres( + // hostname: "localhost", + // port: 5432, + // username: "catgpt", + // password: "catgpt", + // database: "catgpt" + // ), as: .psql) + // app.databases.use(.mysql(hostname: "127.0.0.1", port: 3306, username: "vapor", password: "vapor", database: "vp-test"), as: .mysql) + app.databases.use(.sqlite(.memory), as: .sqlite) try! migrations(app) try! app.autoRevert().wait() @@ -35,25 +35,25 @@ class VaporWalletTests: XCTestCase { } func resetDB() throws { - let db = (app.db as! SQLDatabase) - let query = db.raw(""" + let db = (app.db as! SQLDatabase) + let query = db.raw(""" SELECT Concat('DELETE FROM ', TABLE_NAME, ';') as truncate_query FROM INFORMATION_SCHEMA.TABLES where `TABLE_SCHEMA` = 'vp-test' and `TABLE_NAME` not like '_fluent_%'; """) - - return try query.all().flatMap { results in - return results.compactMap { row in - try? row.decode(column: "truncate_query", as: String.self) - }.map { query in - return (db as! MySQLDatabase).simpleQuery(query).transform(to: ()) - }.flatten(on: self.app.db.eventLoop) - }.wait() - } - + + return try query.all().flatMap { results in + return results.compactMap { row in + try? row.decode(column: "truncate_query", as: String.self) + }.map { query in + return (db as! MySQLDatabase).simpleQuery(query).transform(to: ()) + }.flatten(on: self.app.db.eventLoop) + }.wait() + } + override func tearDown() { try! app.autoRevert().wait() app.shutdown() - + } @@ -66,7 +66,7 @@ class VaporWalletTests: XCTestCase { let game = try await Game.create(name: "new_game", on: app.db) XCTAssert(game.name == "new_game") } - + func testUserHasNoDefaultWallet() async throws { let userWithNoWallet = try await User.create(on: app.db) await XCTAssertThrowsError(try await userWithNoWallet.walletsRepository(on: app.db).defaultAsync(), "expected throw") { (error) in @@ -78,365 +78,354 @@ class VaporWalletTests: XCTestCase { app.databases.middleware.use(AsyncWalletMiddleware()) let userWithDefaultWallet = try await User.create(on: app.db) let defaultWallet = try await userWithDefaultWallet.walletsRepository(on: app.db).defaultAsync() - + XCTAssertEqual(defaultWallet.name, WalletType.default.value) } - + func testCreateWallet() async throws { let (_, wallets) = try await setupUserAndWalletsRepo(on: app.db) try await wallets.createAsync(type: .init(name: "savings")) - + let userWallets = try await wallets.allAsync() XCTAssertEqual(userWallets.count, 1) XCTAssertEqual(userWallets.first?.name, "savings") - + } - + func testWalletDeposit() async throws { app.databases.middleware.use(AsyncWalletMiddleware()) let (_, wallets) = try await setupUserAndWalletsRepo(on: app.db) - - + + try! await wallets.depositAsync(amount: 10) let balance = try await wallets.balanceAsync() - + XCTAssertEqual(balance, 0) - + let refreshedBalance = try await wallets.refreshBalanceAsync() XCTAssertEqual(refreshedBalance, 10) - + } - + func testWalletTransactionMiddleware() async throws { app.databases.middleware.use(AsyncWalletMiddleware()) app.databases.middleware.use(AsyncWalletTransactionMiddleware()) - + let user = try await User.create(on: app.db) let walletsRepoWithMiddleware = user.walletsRepository(on: app.db) - + try! await walletsRepoWithMiddleware.depositAsync(amount: 40) - + let balance = try await walletsRepoWithMiddleware.balanceAsync() - + XCTAssertEqual(balance, 40) - + } - + func testWalletWithdraw() async throws { app.databases.middleware.use(AsyncWalletMiddleware()) app.databases.middleware.use(AsyncWalletTransactionMiddleware()) - + let (_, wallets) = try await setupUserAndWalletsRepo(on: app.db) - + try await wallets.depositAsync(amount: 100) - + var balance = try await wallets.balanceAsync() - + XCTAssertEqual(balance, 100) - + await XCTAssertThrowsError(try await wallets.withdrawAsync(amount: 200), "expected throw") { (error) in XCTAssertEqual(error as! WalletError, WalletError.insufficientBalance) } - + try! await wallets.withdrawAsync(amount: 50) - + balance = try await wallets.balanceAsync() - + XCTAssertEqual(balance, 50) - + } - - + + func testWalletCanWithdraw() async throws { app.databases.middleware.use(AsyncWalletMiddleware()) app.databases.middleware.use(AsyncWalletTransactionMiddleware()) - + let (_, wallets) = try await setupUserAndWalletsRepo(on: app.db) - + try await wallets.depositAsync(amount: 100) - + var can = try! await wallets.canWithdrawAsync(amount: 100) XCTAssertTrue(can) can = try! await wallets.canWithdrawAsync(amount: 200) XCTAssertFalse(can) - + } func testWalletCanWithdrawWithMinAllowedBalance() async throws { app.databases.middleware.use(AsyncWalletMiddleware()) app.databases.middleware.use(AsyncWalletTransactionMiddleware()) - + let (_, wallets) = try await setupUserAndWalletsRepo(on: app.db) try await wallets.createAsync(type: .init(name: "magical"), minAllowedBalance: -50) - try await wallets.depositAsync(to: .init(name: "magical"), amount: 100) - - var can = try! await wallets.canWithdrawAsync(from: .default, amount: 130) + var can = try await wallets.canWithdrawAsync(from: .default, amount: 130) XCTAssertFalse(can) - can = try! await wallets.canWithdrawAsync(from: .init(name: "magical"), amount: 130) + + can = try await wallets.canWithdrawAsync(from: .init(name: "magical"), amount: 130) XCTAssertTrue(can) - } - + func testMultiWallet() async throws { app.databases.middleware.use(AsyncWalletTransactionMiddleware()) app.databases.middleware.use(AsyncWalletTransactionMiddleware()) - + let (_, wallets) = try await setupUserAndWalletsRepo(on: app.db) - + let savingsWallet = WalletType(name: "savings") let myWallet = WalletType(name: "my-wallet") let notExistsWallet = WalletType(name: "not-exists") - + try await wallets.createAsync(type: myWallet) try await wallets.createAsync(type: savingsWallet) - + try await wallets.depositAsync(to: myWallet, amount: 100) try await wallets.depositAsync(to: savingsWallet, amount: 200) - + do { try await wallets.depositAsync(to: notExistsWallet, amount: 1000) } catch { XCTAssertEqual(error as! WalletError, WalletError.walletNotFound(name: "not-exists")) } - + let balance1 = try await wallets.balanceAsync(type: myWallet) let balance2 = try await wallets.balanceAsync(type: savingsWallet) - + XCTAssertEqual(balance1, 100) XCTAssertEqual(balance2, 200) - + } - - + + func testTransactionMetadata() async throws { app.databases.middleware.use(AsyncWalletMiddleware()) - + let (_, wallets) = try await setupUserAndWalletsRepo(on: app.db) - + try await wallets.depositAsync(amount: 100, meta: ["description": "tax payments"]) - + let transaction = try await wallets.defaultAsync() .$transactions.get(on: app.db) .first! - + XCTAssertEqual(transaction.meta!["description"] , "tax payments") } - + func testWalletDecimalBalance() async throws { app.databases.middleware.use(AsyncWalletTransactionMiddleware()) - + let user = try await User.create(on: app.db) let wallets = user.walletsRepository(on: app.db) try await wallets.createAsync(type: .default, decimalPlaces: 2) - + try await wallets.depositAsync(amount: 100) - + var balance = try await wallets.balanceAsync() XCTAssertEqual(balance, 100) - + try await wallets.depositAsync(amount: 1.45) balance = try await wallets.balanceAsync() XCTAssertEqual(balance, 245) - + balance = try await wallets.balanceAsync(asDecimal: true) XCTAssertEqual(balance, 2.45) - - + + // decmial values will be truncated to wallet's decimalPlace value try await wallets.depositAsync(amount: 1.555) balance = try await wallets.balanceAsync() XCTAssertEqual(balance, 400) - + } - - + + func testConfirmTransaction() async throws { app.databases.middleware.use(AsyncWalletMiddleware()) app.databases.middleware.use(AsyncWalletTransactionMiddleware()) - + let (_, wallets) = try await setupUserAndWalletsRepo(on: app.db) - + try await wallets.depositAsync(amount: 10, confirmed: true) sleep(1) try await wallets.depositAsync(amount: 40, confirmed: false) - + var balance = try await wallets.balanceAsync() let unconfirmedBalance = try await wallets.balanceAsync(withUnconfirmed: true) XCTAssertEqual(balance, 10) XCTAssertEqual(unconfirmedBalance, 50) - + let transaction = try await wallets .unconfirmedTransactionsAsync() .items .first! - + balance = try await wallets.confirmAsync(transaction: transaction) - + XCTAssertEqual(balance, 50) - + } - + func testConfirmAllTransactionsOfWallet() async throws { app.databases.middleware.use(AsyncWalletMiddleware()) app.databases.middleware.use(AsyncWalletTransactionMiddleware()) - + let (_, wallets) = try await setupUserAndWalletsRepo(on: app.db) - + try await wallets.depositAsync(amount: 10, confirmed: false) try await wallets.depositAsync(amount: 40, confirmed: false) - + var balance = try await wallets.balanceAsync() XCTAssertEqual(balance, 0) - + balance = try await wallets.confirmAllAsync() - + XCTAssertEqual(balance, 50) } - - + + func testTransferBetweenAUsersWallets() async throws { app.databases.middleware.use(AsyncWalletMiddleware()) app.databases.middleware.use(AsyncWalletTransactionMiddleware()) - + let (_, wallets) = try await setupUserAndWalletsRepo(on: app.db) - + let wallet1 = WalletType(name: "wallet1") let wallet2 = WalletType(name: "wallet2") - + try await wallets.createAsync(type: wallet1) try await wallets.createAsync(type: wallet2) - - + + try await wallets.depositAsync(to: wallet1, amount: 100) - + try await wallets.transferAsync(from: wallet1, to: wallet2, amount: 20) - + let balance1 = try await wallets.balanceAsync(type: wallet1) let balance2 = try await wallets.balanceAsync(type: wallet2) - + XCTAssertEqual(balance1, 80) XCTAssertEqual(balance2, 20) - + } - + func testTransferBetweenTwoUsersWallets() async throws { app.databases.middleware.use(AsyncWalletMiddleware()) app.databases.middleware.use(AsyncWalletTransactionMiddleware()) - + let user1 = try await User.create(username: "user1", on: app.db) let user2 = try await User.create(username: "user2", on: app.db) - + let repo1 = user1.walletsRepository(on: app.db) let repo2 = user2.walletsRepository(on: app.db) - + try await repo1.depositAsync(amount: 100) - + try await repo1.transferAsync(from: try await repo1.defaultAsync(), to: try await repo2.defaultAsync(), amount: 20) - + var balance1 = try await repo1.balanceAsync() var balance2 = try await repo2.balanceAsync() - + XCTAssertEqual(balance1, 80) XCTAssertEqual(balance2, 20) - + try await repo1.transferAsync(from: .default, to: try await repo2.defaultAsync(), amount: 20) - + balance1 = try await repo1.balanceAsync() balance2 = try await repo2.balanceAsync() - + XCTAssertEqual(balance1, 60) XCTAssertEqual(balance2, 40) - + let savings = WalletType(name: "savings") try await repo1.createAsync(type: savings) try await repo1.transferAsync(from: .default, to: savings, amount: 20) - + balance1 = try await repo1.balanceAsync() balance2 = try await repo1.balanceAsync(type: savings) - + XCTAssertEqual(balance1, 40) XCTAssertEqual(balance2, 20) - + } - - + + func testMultiModelWallet() async throws { app.databases.middleware.use(AsyncWalletMiddleware()) app.databases.middleware.use(AsyncWalletMiddleware()) app.databases.middleware.use(AsyncWalletTransactionMiddleware()) - - do { - let user = try await User.create(username: "user1", on: app.db) - let game = Game(id: user.id, name: "game1") - try await game.save(on: app.db) - - let repo1 = user.walletsRepository(on: app.db) - let repo2 = game.walletsRepository(on: app.db) - - try await repo1.depositAsync(amount: 100) - try await repo2.depositAsync(amount: 500) - - let balance1 = try await repo1.balanceAsync() - let balance2 = try await repo2.balanceAsync() - - XCTAssertEqual(balance1, 100) - XCTAssertEqual(balance2, 500) - } catch { - print("error: \(String(reflecting: error))") - } - + + let user = try await User.create(username: "user1", on: app.db) + let game = Game(id: user.id, name: "game1") + try await game.save(on: app.db) + + let repo1 = user.walletsRepository(on: app.db) + let repo2 = game.walletsRepository(on: app.db) + + try await repo1.depositAsync(amount: 100) + try await repo2.depositAsync(amount: 500) + + let balance1 = try await repo1.balanceAsync() + let balance2 = try await repo2.balanceAsync() + + XCTAssertEqual(balance1, 100) + XCTAssertEqual(balance2, 500) + } - + func testMultiModelWalletTransfer() async throws { app.databases.middleware.use(AsyncWalletMiddleware()) app.databases.middleware.use(AsyncWalletMiddleware()) app.databases.middleware.use(AsyncWalletTransactionMiddleware()) - - do { - let user = try await User.create(username: "user1", on: app.db) - let game = Game(id: user.id, name: "game1") - try await game.save(on: app.db) - - let repo1 = user.walletsRepository(on: app.db) - let repo2 = game.walletsRepository(on: app.db) - - try await repo1.depositAsync(amount: 100) - try await repo2.depositAsync(amount: 500) - - let userWallet = try await repo1.defaultAsync() - let gameWallet = try await repo2.defaultAsync() - - try await repo1.transferAsync(from: gameWallet, to: userWallet, amount: 100) - - let balance1 = try await repo1.balanceAsync() - let balance2 = try await repo2.balanceAsync() - - XCTAssertEqual(balance1, 200) - XCTAssertEqual(balance2, 400) - } catch { - print("###### error: #########\n\(String(reflecting: error))") - } - + + let user = try await User.create(username: "user1", on: app.db) + let game = Game(id: user.id, name: "game1") + try await game.save(on: app.db) + + let repo1 = user.walletsRepository(on: app.db) + let repo2 = game.walletsRepository(on: app.db) + + try await repo1.depositAsync(amount: 100) + try await repo2.depositAsync(amount: 500) + + let userWallet = try await repo1.defaultAsync() + let gameWallet = try await repo2.defaultAsync() + + try await repo1.transferAsync(from: gameWallet, to: userWallet, amount: 100) + + let balance1 = try await repo1.balanceAsync() + let balance2 = try await repo2.balanceAsync() + + XCTAssertEqual(balance1, 200) + XCTAssertEqual(balance2, 400) } - - - - + + + + private func setupUserAndWalletsRepo(on: Database) async throws -> (User, WalletsRepository) { let user = try! await User.create(on: app.db) let wallets = user.walletsRepository(on: app.db) - + return (user, wallets) } - + private func migrations(_ app: Application) throws { // Initial Migrations app.migrations.add(CreateUser()) app.migrations.add(CreateGame()) - app.migrations.add(CreateWallet()) + app.migrations.add(CreateWalletAsync()) app.migrations.add(CreateWalletTransactionAsync()) } } @@ -445,5 +434,5 @@ extension WalletError: Equatable { public static func == (lhs: WalletError, rhs: WalletError) -> Bool { return lhs.errorDescription == rhs.errorDescription } - + } From 7b6756e2a154517a9107268a0dff909a114364c8 Mon Sep 17 00:00:00 2001 From: Hadi Sharghi Date: Sat, 17 Jun 2023 21:11:10 +0330 Subject: [PATCH 9/9] uncomment all legacy methods --- Sources/VaporWallet/HasWallet.swift | 37 +- .../Middlewares/WalletModelMiddleware.swift | 26 +- .../WalletTransactionModelMiddleware.swift | 27 +- .../VaporWallet/Migrations/CreateWallet.swift | 64 +-- .../Migrations/CreateWalletTransaction.swift | 63 +-- .../Models/Entities/WalletTransaction.swift | 10 +- Sources/VaporWallet/WalletRepository.swift | 395 +++++++++--------- 7 files changed, 312 insertions(+), 310 deletions(-) diff --git a/Sources/VaporWallet/HasWallet.swift b/Sources/VaporWallet/HasWallet.swift index fa29e9d..cbaf5e7 100644 --- a/Sources/VaporWallet/HasWallet.swift +++ b/Sources/VaporWallet/HasWallet.swift @@ -13,7 +13,7 @@ import FluentPostgresDriver public protocol HasWallet: FluentKit.Model { static var idKey: KeyPath> { get } func walletsRepository(on db: Database) -> WalletsRepository - + } extension HasWallet { @@ -28,7 +28,7 @@ extension HasWallet { extension Wallet { public func refreshBalanceAsync(on db: Database) async throws -> Double { - + var balance: Int // Temporary workaround for sum and average aggregates on Postgres DB if let _ = db as? PostgresDatabase { @@ -36,7 +36,7 @@ extension Wallet { .query(on: db) .filter(\.$confirmed == true) .aggregate(.sum, \.$amount, as: Double.self) - + balance = balanceOptional == nil ? 0 : Int(balanceOptional!) } else { let intBalance = try await self.$transactions @@ -48,23 +48,24 @@ extension Wallet { } self.balance = balance - + try await self.update(on: db) return Double(self.balance) } -// -// public func refreshBalance(on db: Database) -> EventLoopFuture { -// self.$transactions -// .query(on: db) -// .filter(\.$confirmed == true) -// .sum(\.$amount) -// .unwrap(orReplace: 0) -// .flatMap { (balance) -> EventLoopFuture in -// self.balance = balance -// return self.update(on: db).map { -// return Double(balance) -// } -// } -// } + + public func refreshBalance(on db: Database) -> EventLoopFuture { + // Temporary workaround for sum and average aggregates on Postgres DB + self.$transactions + .query(on: db) + .filter(\.$confirmed == true) + .sum(\.$amount) + .unwrap(orReplace: 0) + .flatMap { (balance) -> EventLoopFuture in + self.balance = balance + return self.update(on: db).map { + return Double(balance) + } + } + } } diff --git a/Sources/VaporWallet/Middlewares/WalletModelMiddleware.swift b/Sources/VaporWallet/Middlewares/WalletModelMiddleware.swift index 1b79a55..25bd78d 100644 --- a/Sources/VaporWallet/Middlewares/WalletModelMiddleware.swift +++ b/Sources/VaporWallet/Middlewares/WalletModelMiddleware.swift @@ -9,19 +9,19 @@ import Vapor import Fluent // -//public struct WalletMiddleware: ModelMiddleware { -// -// public init() {} -// -// public func create(model: M, on db: Database, next: AnyModelResponder) -> EventLoopFuture { -// -// // Create `default` wallet when new model is created -// return next.create(model, on: db).flatMap { -// db.logger.log(level: .info, "default wallet for user \(model._$idKey) has been created") -// return model.walletsRepository(on: db).create().transform(to: ()) -// } -// } -//} +public struct WalletMiddleware: ModelMiddleware { + + public init() {} + + public func create(model: M, on db: Database, next: AnyModelResponder) -> EventLoopFuture { + + // Create `default` wallet when new model is created + return next.create(model, on: db).flatMap { + db.logger.log(level: .info, "default wallet for user \(model._$idKey) has been created") + return model.walletsRepository(on: db).create().transform(to: ()) + } + } +} public struct AsyncWalletMiddleware: AsyncModelMiddleware { diff --git a/Sources/VaporWallet/Middlewares/WalletTransactionModelMiddleware.swift b/Sources/VaporWallet/Middlewares/WalletTransactionModelMiddleware.swift index a762461..787ac0e 100644 --- a/Sources/VaporWallet/Middlewares/WalletTransactionModelMiddleware.swift +++ b/Sources/VaporWallet/Middlewares/WalletTransactionModelMiddleware.swift @@ -9,20 +9,19 @@ import Vapor import Fluent -// -//public struct WalletTransactionMiddleware: ModelMiddleware { -// -// public init() {} -// -// public func create(model: WalletTransaction, on db: Database, next: AnyModelResponder) -> EventLoopFuture { -// return next.create(model, on: db).flatMap { -// return model -// .$wallet.get(on: db) -// .map { $0.refreshBalance(on: db) } -// .transform(to: ()) -// } -// } -//} +public struct WalletTransactionMiddleware: ModelMiddleware { + + public init() {} + + public func create(model: WalletTransaction, on db: Database, next: AnyModelResponder) -> EventLoopFuture { + return next.create(model, on: db).flatMap { + return model + .$wallet.get(on: db) + .map { $0.refreshBalance(on: db) } + .transform(to: ()) + } + } +} public struct AsyncWalletTransactionMiddleware: AsyncModelMiddleware { diff --git a/Sources/VaporWallet/Migrations/CreateWallet.swift b/Sources/VaporWallet/Migrations/CreateWallet.swift index 0ddca1a..559d92e 100644 --- a/Sources/VaporWallet/Migrations/CreateWallet.swift +++ b/Sources/VaporWallet/Migrations/CreateWallet.swift @@ -1,38 +1,38 @@ import Fluent import SQLKit -//public struct CreateWallet: Migration { -// private var idKey: String -// public init(foreignKeyColumnName idKey: String = "id") { -// self.idKey = idKey -// } -// -// public func prepare(on database: Database) -> EventLoopFuture { -// return database.schema(Wallet.schema) -// .id() -// .field("name", .string, .required) -// .field("owner_type", .string, .required) -// .field("owner_id", .uuid, .required) -// .field("min_allowed_balance", .int, .required) -// .field("balance", .int, .required) -// .field("decimal_places", .uint8, .required) -// .field("created_at", .datetime, .required) -// .field("updated_at", .datetime, .required) -// .field("deleted_at", .datetime) -// .create().flatMap { _ in -// let sqlDB = (database as! SQLDatabase) -// return sqlDB -// .create(index: "type_idx") -// .on(Wallet.schema) -// .column("owner_type") -// .run() -// } -// } -// -// public func revert(on database: Database) -> EventLoopFuture { -// return database.schema(Wallet.schema).delete() -// } -//} +public struct CreateWallet: Migration { + private var idKey: String + public init(foreignKeyColumnName idKey: String = "id") { + self.idKey = idKey + } + + public func prepare(on database: Database) -> EventLoopFuture { + return database.schema(Wallet.schema) + .id() + .field("name", .string, .required) + .field("owner_type", .string, .required) + .field("owner_id", .uuid, .required) + .field("min_allowed_balance", .int, .required) + .field("balance", .int, .required) + .field("decimal_places", .uint8, .required) + .field("created_at", .datetime, .required) + .field("updated_at", .datetime, .required) + .field("deleted_at", .datetime) + .create().flatMap { _ in + let sqlDB = (database as! SQLDatabase) + return sqlDB + .create(index: "type_idx") + .on(Wallet.schema) + .column("owner_type") + .run() + } + } + + public func revert(on database: Database) -> EventLoopFuture { + return database.schema(Wallet.schema).delete() + } +} public struct CreateWalletAsync: AsyncMigration { private var idKey: String diff --git a/Sources/VaporWallet/Migrations/CreateWalletTransaction.swift b/Sources/VaporWallet/Migrations/CreateWalletTransaction.swift index e5103d2..5903436 100644 --- a/Sources/VaporWallet/Migrations/CreateWalletTransaction.swift +++ b/Sources/VaporWallet/Migrations/CreateWalletTransaction.swift @@ -1,36 +1,37 @@ import Fluent -//public struct CreateWalletTransaction: Migration { -// public init() { } -// -// public func prepare(on database: Database) -> EventLoopFuture { -// return database.enum("type") -// .case("deposit") -// .case("withdraw") -// .create().flatMap { transactionType in -// return database.schema(WalletTransaction.schema) -// .id() -// .field("wallet_id", .uuid, .required, .references(Wallet.schema, "id", onDelete: .cascade)) -// .field("type", transactionType, .required) -// .field("amount", .int, .required) -// .field("confirmed", .bool, .required) -// .field("meta", .json) -// .field("created_at", .datetime, .required) -// .field("updated_at", .datetime, .required) -// .create() -// } -// } -// -// public func revert(on database: Database) -> EventLoopFuture { -// return database.enum("type") -// .deleteCase("deposit") -// .deleteCase("withdraw") -// .update().flatMap { _ in -// return database.schema(WalletTransaction.schema).delete() -// } -// -// } -//} +public struct CreateWalletTransaction: Migration { + public init() { } + + public func prepare(on database: Database) -> EventLoopFuture { + return database.enum("transaction_type") + .case("deposit") + .case("withdraw") + .create().flatMap { transactionType in + return database.schema(WalletTransaction.schema) + .id() + .field("wallet_id", .uuid, .required, .references(Wallet.schema, "id", onDelete: .cascade)) + .field("transaction_type", transactionType, .required) + .field("amount", .int, .required) + .field("confirmed", .bool, .required) + .field("meta", .json) + .field("created_at", .datetime, .required) + .field("updated_at", .datetime, .required) + .create() + } + } + + public func revert(on database: Database) -> EventLoopFuture { + return database.schema(WalletTransaction.schema).delete() + .flatMap { _ in + return database.enum("transaction_type") + .deleteCase("deposit") + .deleteCase("withdraw") + .update() + .transform(to: ()) + } + } +} public struct CreateWalletTransactionAsync: AsyncMigration { public init() { } diff --git a/Sources/VaporWallet/Models/Entities/WalletTransaction.swift b/Sources/VaporWallet/Models/Entities/WalletTransaction.swift index 24e69f2..2cd4da1 100644 --- a/Sources/VaporWallet/Models/Entities/WalletTransaction.swift +++ b/Sources/VaporWallet/Models/Entities/WalletTransaction.swift @@ -72,11 +72,11 @@ extension WalletTransaction { public var isConfirmed: Bool { return self.confirmed } -// -// public func confirm(on db: Database) -> EventLoopFuture { -// self.confirmed = true -// return self.update(on: db) -// } + + public func confirm(on db: Database) -> EventLoopFuture { + self.confirmed = true + return self.update(on: db) + } public func confirmAsync(on db: Database) async throws { self.confirmed = true diff --git a/Sources/VaporWallet/WalletRepository.swift b/Sources/VaporWallet/WalletRepository.swift index 55f9537..d8381bf 100644 --- a/Sources/VaporWallet/WalletRepository.swift +++ b/Sources/VaporWallet/WalletRepository.swift @@ -98,52 +98,53 @@ extension WalletsRepository { return try await wallet.refreshBalanceAsync(on: self.db) } - // - // public func create(type name: WalletType = .default, decimalPlaces: UInt8 = 2) -> EventLoopFuture { - // let wallet: Wallet = Wallet(ownerType: String(describing: self), ownerID: self.id, name: name.value, decimalPlaces: decimalPlaces) - // return wallet.save(on: db) - // } - // - // public func all() -> EventLoopFuture<[Wallet]> { - // Wallet.query(on: self.db) - // .filter(\.$owner == self.id) - // .all() - // } - // - // public func get(type name: WalletType) -> EventLoopFuture { - // Wallet.query(on: db) - // .filter(\.$owner == self.id) - // .filter(\.$name == name.value) - // .first() - // .unwrap(or: WalletError.walletNotFound(name: name.value)) - // } - // - // public func `default`() -> EventLoopFuture { - // get(type: .default) - // } - // - // public func balance(type name: WalletType = .default, withUnconfirmed: Bool = false, asDecimal: Bool = false) -> EventLoopFuture { - // if withUnconfirmed { - // return get(type: name).flatMap { wallet in - // wallet.$transactions - // .query(on: self.db) - // .sum(\.$amount) - // .unwrap(orReplace: 0) - // .map { (intBalance) -> Double in - // return asDecimal ? Double(intBalance).toDecimal(with: wallet.decimalPlaces) : Double(intBalance) - // } - // } - // } - // return get(type: name).map { wallet in - // return asDecimal ? Double(wallet.balance).toDecimal(with: wallet.decimalPlaces) : Double(wallet.balance) - // } - // } - // - // public func refreshBalance(of walletType: WalletType = .default) -> EventLoopFuture { - // return get(type: walletType).flatMap { wallet -> EventLoopFuture in - // wallet.refreshBalance(on: self.db) - // } - // } + + public func create(type name: WalletType = .default, decimalPlaces: UInt8 = 2) -> EventLoopFuture { + let wallet: Wallet = Wallet(ownerType: String(describing: self), ownerID: self.id, name: name.value, decimalPlaces: decimalPlaces) + return wallet.save(on: db) + } + + public func all() -> EventLoopFuture<[Wallet]> { + Wallet.query(on: self.db) + .filter(\.$owner == self.id) + .all() + } + + public func get(type name: WalletType) -> EventLoopFuture { + Wallet.query(on: db) + .filter(\.$owner == self.id) + .filter(\.$ownerType == self.type) + .filter(\.$name == name.value) + .first() + .unwrap(or: WalletError.walletNotFound(name: name.value)) + } + + public func `default`() -> EventLoopFuture { + get(type: .default) + } + + public func balance(type name: WalletType = .default, withUnconfirmed: Bool = false, asDecimal: Bool = false) -> EventLoopFuture { + if withUnconfirmed { + return get(type: name).flatMap { wallet in + wallet.$transactions + .query(on: self.db) + .sum(\.$amount) + .unwrap(orReplace: 0) + .map { (intBalance) -> Double in + return asDecimal ? Double(intBalance).toDecimal(with: wallet.decimalPlaces) : Double(intBalance) + } + } + } + return get(type: name).map { wallet in + return asDecimal ? Double(wallet.balance).toDecimal(with: wallet.decimalPlaces) : Double(wallet.balance) + } + } + + public func refreshBalance(of walletType: WalletType = .default) -> EventLoopFuture { + return get(type: walletType).flatMap { wallet -> EventLoopFuture in + wallet.refreshBalance(on: self.db) + } + } } @@ -205,65 +206,65 @@ extension WalletsRepository { } - // - // public func canWithdraw(from: WalletType = .default, amount: Int) -> EventLoopFuture { - // get(type: from).flatMap { self._canWithdraw(from: $0, amount: amount) } - // } - // - // public func withdraw(from: WalletType = .default, amount: Double, meta: [String: String]? = nil) -> EventLoopFuture { - // get(type: from).flatMap { wallet -> EventLoopFuture in - // let intAmount = Int(amount * pow(10, Double(wallet.decimalPlaces))) - // return self._withdraw(on: self.db, from: wallet, amount: intAmount, meta: meta) - // } - // } - // - // public func withdraw(from: WalletType = .default, amount: Int, meta: [String: String]? = nil) -> EventLoopFuture { - // - // canWithdraw(from: from, amount: amount) - // .guard({ $0 == true }, else: WalletError.insufficientBalance) - // .flatMap { _ in - // self.get(type: from).flatMap { wallet -> EventLoopFuture in - // self._withdraw(on: self.db, from: wallet, amount: amount, meta: meta) - // } - // } - // } - // - // public func deposit(to: WalletType = .default, amount: Double, confirmed: Bool = true, meta: [String: String]? = nil) -> EventLoopFuture { - // get(type: to).flatMap { wallet -> EventLoopFuture in - // let intAmount = Int(amount * pow(10, Double(wallet.decimalPlaces))) - // return self._deposit(on: self.db, to: wallet, amount: intAmount, confirmed: confirmed, meta: meta) - // } - // } - // - // public func deposit(to: WalletType = .default, amount: Int, confirmed: Bool = true, meta: [String: String]? = nil) -> EventLoopFuture { - // get(type: to).flatMap { wallet -> EventLoopFuture in - // self._deposit(on: self.db, to: wallet, amount: amount, confirmed: confirmed, meta: meta) - // } - // } - // - // public func transafer(from: Wallet, to: Wallet, amount: Int, meta: [String: String]? = nil) -> EventLoopFuture { - // return _canWithdraw(from: from, amount: amount) - // .guard({ $0 == true }, else: WalletError.insufficientBalance) - // .flatMap { _ in - // self._transfer(from: from, to: to, amount: amount, meta: meta) - // } - // } - // - // public func transfer(from: WalletType, to: Wallet, amount: Int, meta: [String: String]? = nil) -> EventLoopFuture { - // return get(type: from).flatMap { fromWallet -> EventLoopFuture in - // self._canWithdraw(from: fromWallet, amount: amount) - // .guard({ $0 == true }, else: WalletError.insufficientBalance) - // .flatMap { _ in - // return self._transfer(from: fromWallet, to: to, amount: amount, meta: meta) - // } - // } - // } - // - // public func transafer(from: WalletType, to: WalletType, amount: Int, meta: [String: String]? = nil) -> EventLoopFuture { - // return get(type: to).flatMap { toWallet -> EventLoopFuture in - // self.transfer(from: from, to: toWallet, amount: amount, meta: meta) - // } - // } + + public func canWithdraw(from: WalletType = .default, amount: Int) -> EventLoopFuture { + get(type: from).flatMap { self._canWithdraw(from: $0, amount: amount) } + } + + public func withdraw(from: WalletType = .default, amount: Double, meta: [String: String]? = nil) -> EventLoopFuture { + get(type: from).flatMap { wallet -> EventLoopFuture in + let intAmount = Int(amount * pow(10, Double(wallet.decimalPlaces))) + return self._withdraw(on: self.db, from: wallet, amount: intAmount, meta: meta) + } + } + + public func withdraw(from: WalletType = .default, amount: Int, meta: [String: String]? = nil) -> EventLoopFuture { + + canWithdraw(from: from, amount: amount) + .guard({ $0 == true }, else: WalletError.insufficientBalance) + .flatMap { _ in + self.get(type: from).flatMap { wallet -> EventLoopFuture in + self._withdraw(on: self.db, from: wallet, amount: amount, meta: meta) + } + } + } + + public func deposit(to: WalletType = .default, amount: Double, confirmed: Bool = true, meta: [String: String]? = nil) -> EventLoopFuture { + get(type: to).flatMap { wallet -> EventLoopFuture in + let intAmount = Int(amount * pow(10, Double(wallet.decimalPlaces))) + return self._deposit(on: self.db, to: wallet, amount: intAmount, confirmed: confirmed, meta: meta) + } + } + + public func deposit(to: WalletType = .default, amount: Int, confirmed: Bool = true, meta: [String: String]? = nil) -> EventLoopFuture { + get(type: to).flatMap { wallet -> EventLoopFuture in + self._deposit(on: self.db, to: wallet, amount: amount, confirmed: confirmed, meta: meta) + } + } + + public func transafer(from: Wallet, to: Wallet, amount: Int, meta: [String: String]? = nil) -> EventLoopFuture { + return _canWithdraw(from: from, amount: amount) + .guard({ $0 == true }, else: WalletError.insufficientBalance) + .flatMap { _ in + self._transfer(from: from, to: to, amount: amount, meta: meta) + } + } + + public func transfer(from: WalletType, to: Wallet, amount: Int, meta: [String: String]? = nil) -> EventLoopFuture { + return get(type: from).flatMap { fromWallet -> EventLoopFuture in + self._canWithdraw(from: fromWallet, amount: amount) + .guard({ $0 == true }, else: WalletError.insufficientBalance) + .flatMap { _ in + return self._transfer(from: fromWallet, to: to, amount: amount, meta: meta) + } + } + } + + public func transafer(from: WalletType, to: WalletType, amount: Int, meta: [String: String]? = nil) -> EventLoopFuture { + return get(type: to).flatMap { toWallet -> EventLoopFuture in + self.transfer(from: from, to: toWallet, amount: amount, meta: meta) + } + } } @@ -316,58 +317,58 @@ extension WalletsRepository { } - // - // - // public func transactions(type name: WalletType = .default, - // paginate: PageRequest = .init(page: 1, per: 10), - // sortOrder: DatabaseQuery.Sort.Direction = .descending) -> EventLoopFuture> { - // return self.get(type: name).flatMap { - // $0.$transactions - // .query(on: self.db) - // .sort(\.$createdAt, sortOrder) - // .filter(\.$confirmed == true) - // .paginate(paginate) - // } - // } - // - // public func unconfirmedTransactions(type name: WalletType = .default, - // paginate: PageRequest = .init(page: 1, per: 10), - // sortOrder: DatabaseQuery.Sort.Direction = .descending) -> EventLoopFuture> { - // return self.get(type: name).flatMap { - // $0.$transactions - // .query(on: self.db) - // .sort(\.$createdAt, sortOrder) - // .filter(\.$confirmed == false) - // .paginate(paginate) - // } - // } - // - // public func confirmAll(type name: WalletType = .default) -> EventLoopFuture { - // get(type: name).flatMap { (wallet) -> EventLoopFuture in - // self.db.transaction { (database) -> EventLoopFuture in - // wallet.$transactions - // .query(on: database) - // .set(\.$confirmed, to: true) - // .update() - // .flatMap { _ -> EventLoopFuture in - // wallet.refreshBalance(on: database) - // } - // } - // } - // } - // - // - // public func confirm(transaction: WalletTransaction, refresh: Bool = true) -> EventLoopFuture { - // transaction.confirmed = true - // return self.db.transaction { (database) -> EventLoopFuture in - // transaction.update(on: database).flatMap { () -> EventLoopFuture in - // transaction.$wallet.get(on: database).flatMap { wallet -> EventLoopFuture in - // wallet.refreshBalance(on: database) - // } - // } - // } - // } - // + + + public func transactions(type name: WalletType = .default, + paginate: PageRequest = .init(page: 1, per: 10), + sortOrder: DatabaseQuery.Sort.Direction = .descending) -> EventLoopFuture> { + return self.get(type: name).flatMap { + $0.$transactions + .query(on: self.db) + .sort(\.$createdAt, sortOrder) + .filter(\.$confirmed == true) + .paginate(paginate) + } + } + + public func unconfirmedTransactions(type name: WalletType = .default, + paginate: PageRequest = .init(page: 1, per: 10), + sortOrder: DatabaseQuery.Sort.Direction = .descending) -> EventLoopFuture> { + return self.get(type: name).flatMap { + $0.$transactions + .query(on: self.db) + .sort(\.$createdAt, sortOrder) + .filter(\.$confirmed == false) + .paginate(paginate) + } + } + + public func confirmAll(type name: WalletType = .default) -> EventLoopFuture { + get(type: name).flatMap { (wallet) -> EventLoopFuture in + self.db.transaction { (database) -> EventLoopFuture in + wallet.$transactions + .query(on: database) + .set(\.$confirmed, to: true) + .update() + .flatMap { _ -> EventLoopFuture in + wallet.refreshBalance(on: database) + } + } + } + } + + + public func confirm(transaction: WalletTransaction, refresh: Bool = true) -> EventLoopFuture { + transaction.confirmed = true + return self.db.transaction { (database) -> EventLoopFuture in + transaction.update(on: database).flatMap { () -> EventLoopFuture in + transaction.$wallet.get(on: database).flatMap { wallet -> EventLoopFuture in + wallet.refreshBalance(on: database) + } + } + } + } + } /// @@ -413,46 +414,46 @@ extension WalletsRepository { _ = try await to.refreshBalanceAsync(on: database) } } - // - // private func _canWithdraw(from: Wallet, amount: Int) -> EventLoopFuture { - // from.refreshBalance(on: self.db).map { $0 >= Double(amount) } - // } - // - // private func _deposit(on db: Database, to: Wallet, amount: Int, confirmed: Bool = true, meta: [String: String]? = nil) -> EventLoopFuture { - // return db.transaction { database -> EventLoopFuture in - // do { - // return WalletTransaction(walletID: try to.requireID(), type: .deposit, amount: amount, confirmed: confirmed, meta: meta) - // .save(on: database) - // } catch { - // return self.db.eventLoop.makeFailedFuture(WalletError.walletNotFound(name: to.name)) - // } - // } - // } - // - // private func _withdraw(on db: Database, from: Wallet, amount: Int, meta: [String: String]? = nil) -> EventLoopFuture { - // return db.transaction { database -> EventLoopFuture in - // do { - // return WalletTransaction(walletID: try from.requireID(), type: .withdraw, amount: -1 * amount, meta: meta) - // .save(on: database) - // } catch { - // return database.eventLoop.makeFailedFuture(WalletError.walletNotFound(name: from.name)) - // } - // } - // } - // - // private func _transfer(from: Wallet, to: Wallet, amount: Int, meta: [String: String]? = nil) -> EventLoopFuture { - // return self.db.transaction { (database) -> EventLoopFuture in - // return self._withdraw(on: database, from: from, amount: amount, meta: meta).flatMap { _ -> EventLoopFuture in - // self._deposit(on: database, to: to, amount: amount, meta: meta).flatMap { _ -> EventLoopFuture in - // let refreshFrom = from.refreshBalance(on: database) - // let refreshTo = to.refreshBalance(on: database) - // return refreshFrom.and(refreshTo).flatMap { (_, _) -> EventLoopFuture in - // database.eventLoop.makeSucceededFuture(()) - // } - // } - // } - // } - // } + + private func _canWithdraw(from: Wallet, amount: Int) -> EventLoopFuture { + from.refreshBalance(on: self.db).map { $0 >= Double(amount) } + } + + private func _deposit(on db: Database, to: Wallet, amount: Int, confirmed: Bool = true, meta: [String: String]? = nil) -> EventLoopFuture { + return db.transaction { database -> EventLoopFuture in + do { + return WalletTransaction(walletID: try to.requireID(), transactionType: .deposit, amount: amount, confirmed: confirmed, meta: meta) + .save(on: database) + } catch { + return self.db.eventLoop.makeFailedFuture(WalletError.walletNotFound(name: to.name)) + } + } + } + + private func _withdraw(on db: Database, from: Wallet, amount: Int, meta: [String: String]? = nil) -> EventLoopFuture { + return db.transaction { database -> EventLoopFuture in + do { + return WalletTransaction(walletID: try from.requireID(), transactionType: .withdraw, amount: -1 * amount, meta: meta) + .save(on: database) + } catch { + return database.eventLoop.makeFailedFuture(WalletError.walletNotFound(name: from.name)) + } + } + } + + private func _transfer(from: Wallet, to: Wallet, amount: Int, meta: [String: String]? = nil) -> EventLoopFuture { + return self.db.transaction { (database) -> EventLoopFuture in + return self._withdraw(on: database, from: from, amount: amount, meta: meta).flatMap { _ -> EventLoopFuture in + self._deposit(on: database, to: to, amount: amount, meta: meta).flatMap { _ -> EventLoopFuture in + let refreshFrom = from.refreshBalance(on: database) + let refreshTo = to.refreshBalance(on: database) + return refreshFrom.and(refreshTo).flatMap { (_, _) -> EventLoopFuture in + database.eventLoop.makeSucceededFuture(()) + } + } + } + } + } }