Skip to main content

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)

TypePurposeRefundableExpiry
Paid credits paid_creditsPurchased stored valueYesNone
Bonus credits bonus_creditsPromotions and goodwill grantsNoPer lot
Points pointsActivity rewardsNoPer 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-integrity to 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:

SettingDescription
Points and bonus expiryDefault number of days for new lots. 0 means no expiry. An empty value inherits the parent or default.
My Wallet pageShows or hides the member self-service page.
Manual adjustment step-upRequires 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, and WalletLotExpired
  • 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.

EventRepresentative payload
WalletCreditedCredit transaction
WalletDebitedDebit transaction
WalletRefundedOriginal and refund transactions
WalletLotExpiredExpiry 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.
Delivery boundary

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.