This package is inspired by Laravel-Wallet
In your Package.swift
file, add the following
.package(url: "https://github.com/hsharghi/vapor-wallet.git", from: "1.0")
.target(name: "App", dependencies: [
.product(name: "Vapor", package: "vapor"),
.product(name: "VaporWallet", package: "vapor-wallet")
])
Simply conform any Model
to HasWallet protocol and now you model has a virtual wallet.
final class User: Model {
static let schema = "users"
@ID(key: .id)
var id: UUID?
...
}
extension User: HasWallet {
static let idKey = \User.$id
}
In configure add migrations
import VaporWallet
public func configure(_ app: Application) throws {
...
app.migrations.add(CreateWallet())
app.migrations.add(CreateWalletTransaction())
...
Now User
instances can have access to a wallet.
let repo = user.walletsRepository(on: db)
try await repo.create()
try await repo.deposit(amount: 100)
try await repo.withdraw(amount: 20, ["description": "paid for some cool stuff"])
If you want a default wallet to be automatically created when a model is saved to database you can use the provided database middleware with the package:
app.databases.middleware.use(WalletMiddleware<User>())
Now for every User
model saved to database, a default
wallet will be created.
Wallet balance is not automatically refreshed on every transaction by default. You need to refresh balance to get the updated balance of the wallet.
try await repo.create()
try await repo.deposit(amount: 100)
let balance = try await repo.balance()
// balance is Double(0)
let refreshedBalance = try await repo.refreshBalance()
// refreshedBalance is Double(100)
It is recommended to add the provided database middleware to auto-refresh wallet balance with each transaction (deposit/withdraw).
app.databases.middleware.use(WalletTransactionMiddleware())
try await repo.deposit(amount: 100)
let balance = try await repo.balance()
// balance is allways up-to-date
Deposit to a wallet can be unconfirmed. It will not calculated in wallet balance. you can confirm it later by accessing the transaction's confirm
method.
After confirming transaction(s), wallets balance is automatically refreshed (unless use autoRefresh: false
parameter). But if you confirm a transaction manually by running fluent queries, you need to call refreshBalance()
method to update the wallet's balance.
try await repo.deposit(amount: 100, confirmed: false)
// balance is 0
let unconfirmedBalance = try await wallets.balance(withUnconfirmed: true)
// unconfirmedBalance is 100
// OR
try await repo.confirmAll()
// now balance is 100
// manuallty confirm transactions
try await wallet.$transactions
.query(on: db)
.set(\.$confirmed, to: true)
.update()
// balance still is 0
try await repo.refreshBalance()
// now balance is 100
Any model conformed to HasWallet
can have multiple wallets.
let savingsWallet = WalletType(name: "savings")
let myWallet = WalletType(name: "my-wallet")
try await repo.create(type: myWallet)
try await repo.create(type: savingsWallet)
try await repo.deposit(to: myWallet, amount: 100)
try await repo.deposit(to: savingsWallet, amount: 15)
try await repo.withdraw(from: myWallet, amount: 25)
try await repo.balance(type: myWallet)
// balance is 75
Funds can be transfered between wallets of same user or different users. Transfering funds between wallets of a single user can be done with wallet types,
try await repo.transfer(from: myWallet, to: savingsWallet, amount: 10)
But transfering funds to a wallet of another user requires to get the wallet model first, then transfer the fund to it.
let repo1 = try await user1.walletsRepository(on: db)
let repo2 = try await user2.walletsRepository(on: db)
let walletUser2 = try await repo2.default()
try await repo1.transfer(from: .default, to: walletUser2, amount: 10)
// this will transfer 10 from user1's default wallet to user2's default wallet
All transaction amounts and wallet balances are stored as Integer
values. But balance is allways returned as Double
. So if you like you can get wallet balance as a decimal value based on decimal places of the wallet.
Deposit and withdraw amounts can be both Integer
or Double
, but at the end both will be stored as Integer
try await repo.create(type: .default, decimalPlaces: 2)
try await repo.deposit(amount: 100)
let balance = try await repo.balance()
// balance is Double(100)
try await repo.deposit(amount: 1.45)
let balance = try await repo.balance()
// balance is Double(245) 100+145
let decimalBalance = try await repo.balance(asDecimal: true)
// Double(2.45)
All fractional amounts in transactions will be truncated to decimalPlaces
of the wallet. Default value when creating a wallet is 2.
try await repo.create(type: .default, decimalPlaces: 2)
try await repo.deposit(amount: 1.555)
let balance = try await repo.balance()
// balance is 155 not 155.5 and not 1555
When creating a wallet, default minimum allowed balance is set to 0, so the wallet balance can not be negative. Any positive or negative value can be set as minimum allowed balance, so you can force a wallet to allways have a minumum balance or even let the wallet to have negative balance.
try await repo.create(minAllowedBalance: -50)
try await repo.deposite(amount: 100)
try await repo.withdraw(amount: 130)
let balance = try await repo.balance()
// balance is -20
You can empty a wallet to zero or minimum allowed balance value which has been set when creating the wallet.
try await repo.create(minAllowedBalance: -50)
try await repo.deposite(amount: 100)
try await repo.empty(strategy: .toZero)
// balance is 0
try await repo.empty(strategy: .toMinAllowed)
// balance is -50
- Using
HasWallet
protocol is limited to models withUUID
primary key. - Transfering funds between wallets with different
decimalPlaces
values have unknown result.