Skip to content

v0.2.0

Latest

Choose a tag to compare

@rameerez rameerez released this 03 May 22:34
43afc41

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