Wallet β Member Points and Credits Ledger
Wallet is the ledger plugin for member-owned stored value. It separates paid credits, bonus credits, and points according to their accounting and expiry rules. (ADR-107/060, v1.7.5)
| Type | Purpose | Refundable | Expiry |
|---|---|---|---|
Paid credits paid_credits | Purchased stored value | Yes | None |
Bonus credits bonus_credits | Promotions and goodwill grants | No | Per lot |
Points points | Activity rewards | No | Per lot |
Paid and free value are separate accounts because their accounting and legal treatment differ. Spending consumes bonus credits first, starting with the nearest expiry, and then paid credits. Only paid credits are refundable.
Operator Guideβ
Activation β Always Available, Enabled Per Siteβ
Wallet ships with MSK and does not require a separate installation. The Site Settings > Wallet & Vouchers tab is always available. Enable or disable Wallet for each site from Plugin Management. Disabling it hides the administration menu, member page, and wallet checkout for that site. It is enabled by default.
php artisan migrate # wallet_accounts / wallet_transactions / wallet_lots
Developer note: For a project-specific surface, use
app(\App\Core\Base\Plugin\Activation\Contracts\PluginActivationInterface::class)->isEnabledForCurrentScope('wallet'). Administration resources, member routes, and wallet checkout use the same gate.
Administrationβ
- Wallet accounts: Member balances by type, step-up-protected manual credits or debits, and overview widgets for liabilities and value expiring within 30 days
- Wallet transactions: Ledger history with idempotency key, reason, and resulting balance
- Expiry batch:
wallet:expire-lots, scheduled daily at 03:10; add--check-integrityto verify the ledger
Member Pageβ
/user/wallet shows balances by type, value expiring within 30 days, recent transactions, and a voucher redemption link when Voucher is active.
Policy Settingsβ
Configure Wallet per SaaS or Tenant under Site Settings > Wallet & Vouchers > Wallet:
| Setting | Description |
|---|---|
| Points and bonus expiry | Default number of days for new lots. 0 means no expiry. An empty value inherits the parent or default. |
| My Wallet page | Shows or hides the member self-service page. |
| Manual adjustment step-up | Requires reauthentication for manual credits or debits, with a 60β3600 second validity window. |
Resolution order is Tenant settings > SaaS settings > .env > code defaults. Sites without an override keep the existing behavior.
π Saving changes to the step-up policy itself requires reauthentication and creates an audit record.
.env Fallbacksβ
These values apply when the administration page does not define an override.
WALLET_POINTS_EXPIRE_DAYS=365 # 0=no expiry
WALLET_BONUS_EXPIRE_DAYS=180
Developer Guideβ
Use only the Core contracts; do not depend directly on plugin classes:
use App\Core\Base\Wallet\Contracts\WalletInterface;
use App\Core\Base\Wallet\Contracts\WalletSpenderInterface;
// Idempotent credit: requestId is required.
app(WalletInterface::class)->credit($user, WalletTransactionType::PointAward, 100,
requestId: "signup-bonus-{$user->id}", reason: 'signup bonus');
// Checkout debit: bonus credits are consumed before paid credits.
app(WalletSpenderInterface::class)->spend($user, 5000, requestId: "order-{$order->id}");
- Events:
WalletCredited,WalletDebited,WalletRefunded, andWalletLotExpired - Each SaaS owns the rule for when and how much value to award. See
plugins/Wallet/docs/examples/PurchasePointRewarder.php. - See Commerce for full wallet checkout and reversal-based refunds.
Event Delivery Contract (v1.7.5)β
All four Wallet events use a commit-after contract that is atomic with the database transaction.
| Event | Representative payload |
|---|---|
WalletCredited | Credit transaction |
WalletDebited | Debit transaction |
WalletRefunded | Original and refund transactions |
WalletLotExpired | Expiry transaction, lot ID, and expired amount |
- Outside an active transaction, listeners run immediately.
- Inside a transaction, listeners wait for the outermost commit, not an inner savepoint commit.
- If the outermost transaction rolls back, pending listener calls are discarded. This prevents notifications and webhooks from escaping a failed attempt that will be retried.
- Multiple Wallet events from the same transaction keep their existing registration order. Event names and payloads are unchanged.
Commit-after only orders database commit and listener execution. It does not provide exactly-once delivery to external notifications, webhooks, or message brokers, and it is not a transactional outbox. Consumers must use a stable identifier such as the Wallet transaction request_id for idempotency and define their own policy for post-commit listener failures and queue retries.