Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Wallet Laravel Package

yemeni-open-source/laravel-wallet

A Laravel package that adds wallet functionality to your application, offering a simple API for managing balances, deposits, withdrawals, and transactions so you can integrate digital wallets into PHP projects with minimal setup.

View on GitHub
Deep Wiki
Context7

Getting Started

This release (v1.2.0) is an early-lifecycle change to a wallet/transaction package, so treat the public API as still settling. The single notable feature is a switch that lets insufficient-wallet operations return false instead of throwing UnacceptedTransactionException.

Minimal setup:

  1. composer require the package, then publish assets:
    php artisan vendor:publish --provider="...\WalletServiceProvider"
    
  2. Run the migrations — the package ships wallet, transaction, and transfer tables. Check them before migrating into an existing DB; you almost certainly want to review column types (balances are stored as integers/minor units, not floats).
  3. Add the trait to your billable model:
    class User extends Authenticatable
    {
        use HasWallet;
    }
    
  4. Create the wallet on first use and confirm the shape of the API:
    $user->wallet;                 // lazy-created
    $user->deposit(100_00);        // amounts in minor units
    $user->balanceInt;             // int
    $user->balance;                // formatted string/decimal
    

Where to look first: config/wallet.php (published) is where the new v1.2.0 behaviour is toggled and where currency/decimal settings live. After that, read the HasWallet trait and the Exceptions/ directory — the exception classes tell you more about the package's failure modes than the docs usually do.

First use case: credit a user's wallet on payment webhook, then debit it when they spend. Wire the debit through withdraw() and decide now whether insufficient funds is an exception or a boolean — because v1.2.0 lets you pick, and mixing the two across a codebase is the main source of bugs in this release.

Implementation Patterns

Pick one failure contract per layer and stick to it. The new feature means withdraw()/forceWithdraw() can either throw UnacceptedTransactionException or return false. Choose per-context:

// Pattern A — exceptional (default, pre-1.2.0 behaviour)
try {
    $user->withdraw($amount);
} catch (UnacceptedTransactionException $e) {
    return back()->withErrors('Insufficient balance.');
}

// Pattern B — boolean (v1.2.0, opt-in via config)
if (! $user->withdraw($amount)) {
    return back()->withErrors('Insufficient balance.');
}

Use the boolean style in controllers and form requests (it reads like validation), and keep the exception style in domain services and jobs, where a rejected transaction should abort the whole unit of work and be retried/dead-lettered. Boolean returns inside a queued job make partial failures invisible.

Wrap money movement in a DB transaction with a locked wallet. Optimistic balance checks are not safe under concurrency:

DB::transaction(function () use ($user, $amount) {
    $wallet = $user->wallet()->lockForUpdate()->first();

    if (! $wallet->withdraw($amount)) {
        throw new \RuntimeException('Insufficient funds'); // roll back
    }
});

Note the deliberate re-throw: when you switch the package to return false, you lose the automatic rollback that a thrown exception gave you. Re-introduce it yourself, or the surrounding transaction will commit with no debit applied.

Use intent-check before mutation for UX, not for correctness. canWithdraw()/balance reads are fine for disabling a button, but always re-check inside the locked transaction — a user with two tabs will race you.

Idempotency for webhooks. Payment webhooks retry. Pass a deterministic meta key per transaction (a provider event ID or client-generated UUID) and guard at your own layer with firstOrCreate on a payments table, keyed on that ID, before calling deposit(). Do not rely on the wallet package to deduplicate a webhook replay for you.

Transfers need both sides locked, and deadlock ordering matters. Lock wallets by ascending ID when moving money between two users:

$ids = collect([$from->wallet->id, $to->wallet->id])->sort()->values();

Testing pattern. Use RefreshDatabase, seed a wallet with deposit() rather than factory-forcing the balance column, and write one test per failure contract. If you flip the config in phpunit.xml via WALLET_THROW_ON_INSUFFICIENT=false, add an explicit test that the exception is not thrown, otherwise the suite silently drifts from production.

Gotchas and Tips

  • The v1.2.0 switch is global, not per-call. Changing the config changes behaviour everywhere at once. Existing catch (UnacceptedTransactionException) blocks become dead code and existing tests that expectException will fail. Grep for the exception class before flipping it.
  • false erases the reason. UnacceptedTransactionException can carry context about why a transaction was unaccepted; a bare boolean cannot. If you need to distinguish "insufficient funds" from "wallet frozen" or "amount invalid", keep the exception path for those operations and use the boolean only on the paths you control.
  • Return types become unions. Anything typed : Transaction around a withdraw() call will now be wrong under static analysis (PHPStan/Psalm will flag Transaction|false). Update signatures deliberately instead of adding @phpstan-ignore.
  • Lost rollback (repeated because it bites). An exception unwinds your DB::transaction; a false return does not. Any side effect you perform after a rejected withdraw() will commit unless you check the return value and bail.
  • Money as floats. Never do arithmetic on balance (the formatted accessor). Use balanceInt / minor units and format only at the presentation boundary. Watch for casts in your own code turning an int balance into a string.
  • Decimal/currency config mismatch. If you change decimal_places or currency after wallets have balances, existing rows are not migrated. Decide this before the first production deposit.
  • Debugging rejections: log around the wallet call (user ID, amount, wallet ID, resulting balance) rather than inside the package. Temporarily re-enabling the exception path is the fastest way to get a stack trace showing exactly which call site was rejected.
  • Extension points: the exception class is bindable/replaceable in the container, and transaction lifecycle events are the right hook for ledger mirrors and analytics — don't pollute controllers with that logic. If you need custom rejection semantics beyond the v1.2.0 boolean, catching the exception at a boundary middleware and converting it to a response is cleaner than a fork.
  • Version caution. v1.2.0 is a minor version with a behavioural toggle. Pin the version in composer.json and read the changelog before 1.3.x — a package this young is likely to move the config key or the return contract again.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
gmostafa/php-graphql-client
xeviant/paystack
snowsoft/faker
meditationsteps/blogetc
animalyoung/laravel-influxdb
onix-solutions/sms-telera
lonely-lights/prosetta
oxygen/data
oxygen/core
jengo/base
xiloryx/log-collector
a21ns1g4ts/filament-stripe
la-php/laravel-admin
artisan-build/community-prompts
vleroy/smart
ayles-software/laravel-australian-public-holidays
cboxdk/laravel-id-spatie
cboxdk/laravel-id-billing
cboxdk/laravel-id-billing-bridge
simple-as-fuck/php-performance-log