bavix/laravel-wallet
Virtual wallet system for Laravel: manage balances, deposits/withdrawals, transfers, and multi-wallet support with robust transaction history and concurrency safety. Well-tested, benchmarked, and extensible for payments, loyalty points, and in-app credits.
Installation:
composer require bavix/laravel-wallet
Publish the package config (if needed):
php artisan vendor:publish --provider="Bavix\Wallet\WalletServiceProvider"
Basic Model Integration:
Add HasWallet trait and Wallet interface to your model (e.g., User):
use Bavix\Wallet\Traits\HasWallet;
use Bavix\Wallet\Interfaces\Wallet;
class User extends Model implements Wallet
{
use HasWallet;
}
Run migrations:
php artisan migrate
First Use Case: Deposit/withdraw funds:
$user = User::first();
$user->deposit(100); // Deposit 100 units
$user->withdraw(20); // Withdraw 20 units
$user->balance; // Check balance (80)
Wallet Management:
HasWallet for models with one wallet.
User::with('wallet')->get(); // Eager load wallet
HasWallets for models with multiple wallets (e.g., User with Currency).
User::with('wallets')->get();
Transactions:
$user->deposit(50); // Add funds
$user->withdraw(10); // Subtract funds
$user->forceWithdraw(100, ['description' => 'Tax']); // Force withdraw (allows negative balance)
use Bavix\Wallet\Traits\HasWalletFloat;
use Bavix\Wallet\Interfaces\WalletFloat;
class User implements Wallet, WalletFloat
{
use HasWalletFloat;
}
$user->depositFloat(1.99); // Deposit 1.99 units
Purchases:
use Bavix\Wallet\Interfaces\ProductInterface;
class Item implements ProductInterface
{
public function getAmountProduct(Customer $customer): int|string { return 100; }
public function getMetaProduct(): ?array { return ['title' => 'Product']; }
}
$user->pay($item); // Deduct cost from user's balance
use Bavix\Wallet\Interfaces\ProductLimitedInterface;
class Item implements ProductLimitedInterface
{
public function canBuy(Customer $customer, int $quantity = 1): bool { /* Logic */ }
// ... other methods
}
$user->safePay($item); // Attempt purchase (returns bool)
Refunds and Queries:
$user->refund($item); // Refund purchase
app(PurchaseQueryHandlerInterface::class)->one(PurchaseQuery::create($user, $item)); // Check purchase status
Eager Loading:
// Single wallet
User::with('wallet')->get();
// Multi-wallet
User::with('wallets')->get();
Custom Transactions:
Extend the Transaction model or create custom services by implementing TransactionServiceInterface.
Merchant Fees:
Use the HasGift trait to handle discounts or fees:
use Bavix\Wallet\Traits\HasGift;
class User implements Wallet
{
use HasGift;
}
$user->setGift(10); // Apply a 10-unit discount
Shopping Cart:
Override PurchaseServiceInterface to batch-check purchases:
class CartPurchaseService implements PurchaseServiceInterface
{
public function check(PurchaseQuery $query): bool { /* Batch logic */ }
}
Events:
Listen to wallet events (e.g., WalletDeposited, WalletWithdrawn) for notifications or logging:
event(new WalletDeposited($user, $amount));
Testing:
Use the WalletTestCase trait for unit tests:
use Bavix\Wallet\Tests\WalletTestCase;
class UserTest extends WalletTestCase { ... }
Negative Balances:
withdraw() throws an exception if insufficient funds exist.forceWithdraw() to allow negative balances (e.g., for overdrafts or tax deductions).Floating-Point Precision:
HasWalletFloat uses brick/math for accurate decimal arithmetic.balanceFloat; use provided methods (depositFloat, withdrawFloat).Multi-Wallet Conflicts:
walletable_id and walletable_type are correctly set in the wallets table.HasWallets for models with dynamic wallet types (e.g., User + Currency).Purchase Locking:
ProductLimitedInterface requires canBuy() logic. Omit this for unlimited products.PurchaseServiceInterface to avoid N+1 queries.Event Order:
WalletDeposited fire after the balance is updated. Use creating/updating model events for pre-processing.Transaction Logs:
Enable debug mode in config/wallet.php:
'debug' => env('WALLET_DEBUG', false),
Logs will appear in storage/logs/laravel-wallet.log.
Balance Mismatches:
balance vs. balanceInt/balanceFloat consistency.Query Issues:
tap() to inspect PurchaseQuery objects:
tap(PurchaseQuery::create($user, $item), fn($query) => dd($query->toArray()));
Custom Wallets:
Create a new wallet type by extending Wallet and registering it in WalletServiceProvider:
$this->app->bind(WalletInterface::class, CustomWallet::class);
Transaction Services:
Override TransactionServiceInterface for custom logic (e.g., logging, analytics):
class CustomTransactionService implements TransactionServiceInterface
{
public function create(array $data): Transaction { ... }
}
Purchase Validation:
Extend PurchaseQueryHandlerInterface for custom purchase rules:
class CustomPurchaseHandler implements PurchaseQueryHandlerInterface
{
public function one(PurchaseQuery $query): bool { ... }
}
Currency Support:
Use the Currency model to manage multi-currency wallets:
$user->wallets()->attach($currencyId, ['balance' => 100]);
Webhooks: Dispatch events to trigger external services (e.g., Stripe, PayPal):
event(new WalletDeposited($user, $amount))
->each(fn($event) => $this->notifyExternalService($event));
Decimal Places:
Set in config/wallet.php:
'decimal_places' => 2, // For floating-point wallets
Default Currency:
Configure in config/wallet.php:
'default_currency' => 'USD',
Database Schema:
wallets and transactions tables by default.WalletServiceProvider::boot():
Schema::create('custom_wallets', function (Blueprint $table) { ... });
Caching:
Disable caching in config/wallet.php for development:
'cache' => [
'enabled' => env('WALLET_CACHE', false),
],
How can I help you explore Laravel packages today?