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.
## Getting Started
### Minimal Setup
1. **Installation**:
```bash
composer require bavix/laravel-wallet
Publish the config file (if needed):
php artisan vendor:publish --provider="Bavix\Wallet\WalletServiceProvider"
Model Integration:
Add the 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 and withdraw funds:
$user = User::first();
$user->deposit(100); // Deposit 100 units
$user->withdraw(20); // Withdraw 20 units
config/wallet.php for settings like:
precision (default: 2 for decimal places).currency (default: USD).decimal_class (supports float, string, or Brick\Math\BigDecimal).Basic Transactions:
deposit(), withdraw(), and forceWithdraw() for direct balance adjustments.$user->deposit(50, ['description' => 'Top-up']);
$user->withdraw(10, ['description' => 'Payment']);
Purchases:
CanPay trait for customers and ProductInterface/ProductLimitedInterface for items:
// User model (Customer)
use Bavix\Wallet\Traits\CanPay;
use Bavix\Wallet\Interfaces\Customer;
class User extends Model implements Customer
{
use CanPay;
}
// Item model (Product)
use Bavix\Wallet\Interfaces\ProductInterface;
class Item extends Model implements ProductInterface
{
use HasWallet;
public function getAmountProduct(Customer $customer): int|string
{
return 100;
}
}
$user->pay($item); // Throws exception if insufficient balance
$user->safePay($item); // Returns bool (false if failed)
Refunds and Checks:
$user->refund($item);
app(\Bavix\Wallet\External\Api\PurchaseQueryHandlerInterface::class)
->one(\Bavix\Wallet\External\Api\PurchaseQuery::create($user, $item));
Eager Loading:
User::with('wallet')->get(); // Single wallet
User::with('wallets')->get(); // Multi-wallet
Multi-Wallet Support:
HasWallets trait for models needing multiple wallets (e.g., User with USD and EUR wallets).$user->wallets()->where('currency', 'EUR')->first();
Fractional Currency:
HasWalletFloat for decimal precision (e.g., 1.37):
use Bavix\Wallet\Traits\HasWalletFloat;
use Bavix\Wallet\Interfaces\WalletFloat;
class User extends Model implements Wallet, WalletFloat
{
use HasWalletFloat;
}
$user->depositFloat(1.37);
$user->balanceFloat; // 1.37
Custom Purchase Logic:
PurchaseServiceInterface for complex validation (e.g., cart-based purchases).class CustomPurchaseService implements PurchaseServiceInterface
{
public function validate(PurchaseQuery $query): bool
{
// Custom logic (e.g., check cart items)
return true;
}
}
AppServiceProvider:
$this->app->bind(PurchaseServiceInterface::class, CustomPurchaseService::class);
Extensions:
laravel-wallet-swap for exchange rates or laravel-wallet-warmup for balance caching:
composer require bavix/laravel-wallet-swap
Precision Handling:
2 (e.g., 100.00). For fractional currencies, use HasWalletFloat or configure decimal_class in config/wallet.php:
'decimal_class' => \Brick\Math\BigDecimal::class,
BigDecimal for accuracy.Negative Balances:
withdraw() throws an exception if balance is insufficient. Use forceWithdraw() to bypass checks (e.g., for admin actions):
$user->forceWithdraw(1000, ['description' => 'Admin override']);
Purchase Validation:
ProductLimitedInterface requires implementing canBuy(). Forgetting this will cause runtime errors.PurchaseQueryHandlerInterface for centralized purchase checks to avoid N+1 queries.Eager Loading:
// Bad (N+1 queries)
foreach (User::all() as $user) {
$user->balance; // Triggers a query per user
}
// Good
User::with('wallet')->get();
Multi-Wallet Conflicts:
currency fields are unique in the wallets table when using multi-wallet. Duplicate currencies may cause silent failures.Transaction Logs:
config/wallet.php:
'debug' => env('WALLET_DEBUG', false),
storage/logs/wallet.log.Balance Mismatches:
balance vs. balanceInt/balanceFloat if discrepancies arise. Use balanceInt for integer-based systems:
$user->balanceInt; // Always returns integer (e.g., 100 for 1.00)
Extension Conflicts:
php artisan cache:clear
php artisan view:clear
Caching:
laravel-wallet-warmup to cache balances:
composer require bavix/laravel-wallet-warmup
config/wallet.php:
'warmup' => [
'enabled' => true,
'ttl' => 60, // Cache TTL in minutes
],
Database Indexes:
wallets.user_id and wallets.currency are indexed for multi-wallet queries.Batch Operations:
Wallet::batchDeposit() or Wallet::batchWithdraw() for bulk transactions:
Wallet::batchDeposit([$user1, $user2], 100);
Custom Events:
wallet.deposited, wallet.withdrawn):
event(new \Bavix\Wallet\Events\WalletDeposited($user, $amount));
EventServiceProvider:
protected $listen = [
\Bavix\Wallet\Events\WalletDeposited::class => [
\App\Listeners\LogDeposit::class,
],
];
Middleware:
public function handle(Request $request, Closure $next)
{
$user = $request->user();
if ($user->balance < 100) {
abort(403, 'Insufficient balance');
}
return $next($request);
}
API Responses:
return response()->json([
'balance' => $user->balanceFloat,
'currency' => $user->wallet->currency,
]);
Testing:
WalletTestCase for unit tests:
use Bavix\Wallet
How can I help you explore Laravel packages today?