Skip to content

Releases: rameerez/wallets

v0.2.0

03 May 22:34
43afc41

Choose a tag to compare

Highlights

This release fixes two bugs in Wallets.configuration.allow_negative_balance handling and pins the documented edge-case behavior with explicit tests + inline comments. Default-config users (the flag stays false) see no behavior change.

🐞 Wallet#transfer_to now honors allow_negative_balance

Previously the flag was half-applied: direct wallet.debit honored it via apply_debit, but wallet.transfer_to (the canonical user-to-user primitive) had its own balance pre-check that always rejected transfers below zero — regardless of the flag. Apps using the flag for a "convenience overdraft" (ride-fare apps where passengers briefly go negative until rewards land, family wallets with shared spend, telecom plans with bridging credit) had to monkey-patch the gem.

What's new:

  • transfer_to now goes through when the flag is on, driving the source wallet negative the same way debit already could.
  • When the transfer drives the source below zero AND the caller is using the default :preserve expiration policy, the policy automatically falls back to :none for that transfer. There are no positive source buckets to inherit expirations from for the deficit portion. The receiver gets a single evergreen credit. Explicit :fixed (with expires_at:) and explicit :none are honored unchanged.
  • The :insufficient_balance callback no longer fires for successful overdraft transfers. It still fires (and the transfer still raises Wallets::InsufficientBalance) when the flag is off and the transfer is rejected.

🐞 :balance_depleted callback fires on positive→non-positive crossings

Previously fired only on exact zero. With allow_negative_balance = true, a single debit could take a wallet from +100 to -50 — skipping zero entirely — and the callback was silently missing. Widened the condition from balance.zero? to !balance.positive?. With the flag off, balances can't go below zero, so the new condition collapses to "exactly zero" and existing callers see no behavior change.

📌 Documented behavior, now pinned by tests

Audit pass added explicit coverage and inline comments for the subtleties that were correct already but easy to break:

  • Compounding debits accumulate unbacked_amount per row (gem doesn't spread debt).
  • A credit on a negative-balance wallet does not auto-allocate against existing unbacked debits. Both ledger entries persist; balance reconciles them. FIFO consumption on the next debit pulls from the new credit, not the old debt.
  • has_enough_balance? keeps strict semantics under the flag — it answers "do you have it on hand?", not "would the gem accept this debit?". Overdraft is a deliberate caller choice.
  • Flipping allow_negative_balance OFF while wallets are below zero leaves them un-saveable until you flip it back on (the model's balance >= 0 validation is gated on the flag, and refresh_cached_balance! calls save! on every credit/debit). The flag is meant to be a stable config decision, not a runtime toggle.
  • :low_balance_reached is one-shot per crossing — going from -50 to -80 doesn't re-fire because the wallet was already low; coming back up to +200 then dipping low again does fire.
  • Concurrent overdraft debits on the same wallet serialize through with_lock (FOR UPDATE); concurrent transfers serialize through lock_wallet_pair!.

The README gains a Negative balances and overdraft section covering all of the above.

Compatibility

  • The gem default for allow_negative_balance is still false. Apps that haven't opted in see no behavior change.
  • Apps that previously caught Wallets::InsufficientBalance from transfer_to after flipping the flag on (i.e. were aware of the half-applied behavior and relied on it as a guardrail) need to know transfers will now go through. Layer an app-level floor policy in front of transfer_to if you want a cap — the gem flag is binary by design.
  • Successful overdraft transfers no longer fire :insufficient_balance. Apps that hooked the callback to nudge users to top up after a failed attempt will still see those events when the flag is off (or when their own policy refuses); they will correctly stop seeing them for transfers that actually went through.

Test coverage

22 new tests across transfer_test.rb, wallet_test.rb, wallet_callbacks_test.rb. Existing tests covering the old "transfers always reject below zero" and "depleted = exactly zero" semantics are amended to reflect the new contracts.

Run Result
bundle exec rake test 92 / 338 assertions, all green
gemfiles/rails_7.2.gemfile 92 / 338, all green
gemfiles/rails_8.1.gemfile 92 / 338, all green
Postgres + MySQL + SQLite (3.3, 3.4, 4.0) on CI all green

What's Changed

What's Changed

  • Negative-balance correctness: fix transfer_to + :balance_depleted, audit + pin documented behavior by @rameerez in #3

Full Changelog: v0.1.0...v0.2.0

v0.1.0

18 Mar 03:19

Choose a tag to compare

Initial Release

  • Multi-asset wallets per owner via has_walletsuser.wallet(:usd), user.wallet(:gems), etc.
  • Append-only transaction ledger with credit, debit, and transfer_to APIs
  • FIFO allocation for expiring balances — oldest credits consumed first
  • Transfer expiration policies: :preserve (default), :none, :fixed
  • Transfers split into multiple inbound legs when consuming buckets with different expirations
  • Embeddability hooks for other gems to reuse the ledger core with custom tables/config/callbacks
  • Idempotent create_for_owner! with race condition handling
  • Row-level locking to prevent double-spending
  • Balance snapshots on every transaction for reconciliation
  • Rich metadata support on wallets, transactions, and transfers
  • Lifecycle callbacks: on_balance_credited, on_balance_debited, on_transfer_completed, etc.
  • Install generator with migrations and initializer
  • Rails 6.1+ support (tested through Rails 8.x)

What's Changed

  • Add embeddability hooks and transfer expiration policies (v0.2.0) by @rameerez in #1
  • Make create_for_owner! idempotent and race-safe by @rameerez in #2

New Contributors

Full Changelog: https://github.com/rameerez/wallets/commits/v0.1.0