wallets 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +18 -0
- data/README.md +128 -43
- data/context7.json +4 -0
- data/lib/generators/wallets/templates/initializer.rb +10 -1
- data/lib/wallets/models/wallet.rb +52 -3
- data/lib/wallets/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: dcdad28fc010051f982f84493c687649cd9f6af970774dedd13ec9ec76a24a1c
|
|
4
|
+
data.tar.gz: ec2ca091ea219646e77503ef8da39a0ce468b7ce63a7b7e46807174b83b3799a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 75ae215cb1c057a6aef302504c12113aa8271dc083c84b3e6635aeee9b2e2f64fef3cad3019ce3513008babd85e0b7dde031abe5f6cdd2ba9986e43e565d7016
|
|
7
|
+
data.tar.gz: e9219a6fe158109c11a54dc30572ca71bb866e190cb8c071cc97f59bfb40e8fb3de3d6448b7a78fa3b1978f6f406a82961526256aed7b5bd5642a11aed579ac3
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,21 @@
|
|
|
1
|
+
## [0.2.0] - 2026-05-03
|
|
2
|
+
|
|
3
|
+
### Fixed
|
|
4
|
+
|
|
5
|
+
- `Wallet#transfer_to` now honors `Wallets.configuration.allow_negative_balance`, matching `Wallet#debit`. Previously the flag was half-applied: direct debits could go below zero, but the canonical transfer primitive (used to move value between users) silently rejected any transfer that would push the source below zero. Apps using the flag for a "convenience overdraft" (e.g. ride-fare apps where passengers may briefly go negative until rewards land) had to monkey-patch the gem to get consistent behavior. ([#3](https://github.com/rameerez/wallets/pull/3))
|
|
6
|
+
- `:balance_depleted` callback now fires when a debit takes a wallet from a positive balance to **zero or lower** (was: exactly zero). Previously, with `allow_negative_balance = true`, a single debit that drove a wallet from e.g. +100 to -50 would skip the callback because the wallet never landed on exact zero. The callback semantic is "ran out of available value", which is at least as true at -50 as at 0. With `allow_negative_balance = false` the behavior is unchanged because balances cannot go below zero.
|
|
7
|
+
|
|
8
|
+
### Changed
|
|
9
|
+
|
|
10
|
+
- When a transfer drives the source wallet below zero AND the caller is using the default `:preserve` expiration policy, the policy automatically falls back to `:none` for that transfer. Rationale: there are no positive source buckets to "preserve" expirations from for the deficit portion, and `build_preserved_transfer_inbound_credit_specs` would otherwise raise `InvalidTransfer`. The inbound credit becomes a single evergreen entry — the only honest representation of "value created without a source bucket". Explicit `:fixed` and `:none` policies are honored unchanged.
|
|
11
|
+
- `:insufficient_balance` callback no longer fires for transfers that succeed under `allow_negative_balance = true`. It still fires (and the transfer still raises `Wallets::InsufficientBalance`) when the flag is off and the transfer is rejected.
|
|
12
|
+
|
|
13
|
+
### Documented
|
|
14
|
+
|
|
15
|
+
- `apply_credit` does NOT auto-allocate a new credit against existing unbacked debits. The FIFO ledger is intentionally append-only — both the unbacked debit and the new credit persist as independent rows; `balance` reconciles them on the fly. Apps that want automatic debt settlement should layer that on in their own service code.
|
|
16
|
+
- `allow_negative_balance` is meant to be a stable config decision, not a runtime toggle. Flipping it OFF while wallets are below zero leaves them un-saveable, since the model's `balance >= 0` validation is gated on the flag and any subsequent `credit` / `debit` calls `refresh_cached_balance!` (which calls `save!`).
|
|
17
|
+
- `has_enough_balance?` keeps strict semantics under `allow_negative_balance = true`: it still answers "does this wallet have enough on hand right now?", returning `false` when the gem would happily complete an overdraft. Overdraft is a deliberate caller choice via `debit` / `transfer_to`, not a query semantic.
|
|
18
|
+
|
|
1
19
|
## [0.1.0] - 2026-03-18
|
|
2
20
|
|
|
3
21
|
Initial release.
|
data/README.md
CHANGED
|
@@ -96,26 +96,53 @@ user.wallet(:gems).credit(5, category: :top_up)
|
|
|
96
96
|
|
|
97
97
|
## Example
|
|
98
98
|
|
|
99
|
+
Here is a more relatable money flow for a home-cleaning app:
|
|
100
|
+
|
|
99
101
|
```ruby
|
|
100
102
|
class User < ApplicationRecord
|
|
101
|
-
has_wallets default_asset: :
|
|
103
|
+
has_wallets default_asset: :eur_cents
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
class Platform < ApplicationRecord
|
|
107
|
+
has_wallets default_asset: :eur_cents
|
|
102
108
|
end
|
|
103
109
|
|
|
104
|
-
|
|
105
|
-
|
|
110
|
+
sara = User.find(1) # customer
|
|
111
|
+
lucia = User.find(2) # cleaner
|
|
112
|
+
platform = Platform.first
|
|
113
|
+
|
|
114
|
+
# Sara books a cleaning and tops up her wallet
|
|
115
|
+
sara.wallet.credit(10_000, category: :top_up, metadata: { source: "card" })
|
|
116
|
+
|
|
117
|
+
# Sara books a 2-hour cleaning for €60.00
|
|
118
|
+
booking_total = 6_000
|
|
119
|
+
platform_fee = 900
|
|
120
|
+
cleaner_payout = booking_total - platform_fee
|
|
106
121
|
|
|
107
|
-
|
|
108
|
-
|
|
122
|
+
# Sara pays the platform the gross booking price
|
|
123
|
+
charge = sara.wallet.transfer_to(
|
|
124
|
+
platform.wallet,
|
|
125
|
+
booking_total,
|
|
126
|
+
category: :cleaning_booking_charge,
|
|
127
|
+
metadata: { booking_id: 42, cleaner_id: lucia.id }
|
|
128
|
+
)
|
|
109
129
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
130
|
+
# The app later pays Lucia the net amount and keeps its fee
|
|
131
|
+
payout = platform.wallet.transfer_to(
|
|
132
|
+
lucia.wallet,
|
|
133
|
+
cleaner_payout,
|
|
134
|
+
category: :cleaner_payout,
|
|
135
|
+
metadata: {
|
|
136
|
+
booking_id: 42,
|
|
137
|
+
customer_id: sara.id,
|
|
138
|
+
gross_transfer_id: charge.id,
|
|
139
|
+
platform_fee: platform_fee
|
|
140
|
+
}
|
|
115
141
|
)
|
|
116
142
|
|
|
117
|
-
|
|
118
|
-
|
|
143
|
+
# Query the linked transfer records later
|
|
144
|
+
charge.outbound_transaction
|
|
145
|
+
payout.inbound_transactions
|
|
119
146
|
```
|
|
120
147
|
|
|
121
148
|
Amounts are always integers. For money, store the smallest unit like cents. For games, store whole resource units.
|
|
@@ -188,6 +215,23 @@ transfer.outbound_transaction
|
|
|
188
215
|
transfer.inbound_transactions
|
|
189
216
|
```
|
|
190
217
|
|
|
218
|
+
`category:` on `transfer_to` describes the **business meaning of the transfer itself**. The ledger entries created underneath are still recorded as `transfer_out` and `transfer_in`, and the original transfer category is mirrored into transaction metadata as `transfer_category`.
|
|
219
|
+
|
|
220
|
+
That means you can query transfers directly for business flows:
|
|
221
|
+
|
|
222
|
+
```ruby
|
|
223
|
+
user.wallet.outgoing_transfers.where(category: :peer_payment)
|
|
224
|
+
user.wallet.incoming_transfers.where(category: :peer_payment)
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Or query the ledger legs when you need transaction-level audit data:
|
|
228
|
+
|
|
229
|
+
```ruby
|
|
230
|
+
user.wallet.history
|
|
231
|
+
.by_category(:transfer_in)
|
|
232
|
+
.where("metadata->>'transfer_category' = ?", "peer_payment")
|
|
233
|
+
```
|
|
234
|
+
|
|
191
235
|
Transfers require both wallets to use the same asset and the same wallet class. `:eur` can move to `:eur`; `:wood` can move to `:wood`; `Wallets::Wallet` cannot transfer directly to `UsageCredits::Wallet`.
|
|
192
236
|
|
|
193
237
|
> [!NOTE]
|
|
@@ -201,6 +245,36 @@ Transfers require both wallets to use the same asset and the same wallet class.
|
|
|
201
245
|
> sender.transfer_to(receiver, 100, expiration_policy: :fixed, expires_at: 30.days.from_now)
|
|
202
246
|
> ```
|
|
203
247
|
|
|
248
|
+
### Negative balances and overdraft
|
|
249
|
+
|
|
250
|
+
By default `wallets` rejects any debit or transfer that would push a wallet below zero. Some apps (ride-fare apps with rewards, family wallets with shared spend, telecom plans with bridging credit, etc.) want to allow a small overdraft so the user experience doesn't hard-stop on a low balance.
|
|
251
|
+
|
|
252
|
+
Flip one flag and it applies consistently to direct `wallet.debit` and to `wallet.transfer_to`:
|
|
253
|
+
|
|
254
|
+
```ruby
|
|
255
|
+
Wallets.configure do |config|
|
|
256
|
+
config.allow_negative_balance = true
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
passenger.wallet(:eur).balance # => 100 (1€)
|
|
260
|
+
passenger.wallet(:eur).transfer_to(driver.wallet(:eur), 300, category: :ride_fare)
|
|
261
|
+
passenger.wallet(:eur).reload.balance # => -200 (-2€)
|
|
262
|
+
driver.wallet(:eur).reload.balance # => 300 (3€)
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
A few things to know:
|
|
266
|
+
|
|
267
|
+
- **Apps own the floor.** The flag is binary — it doesn't cap how negative a wallet can go. If you want a "5€ convenience overdraft", enforce that in your domain code before calling `transfer_to` (e.g. a `WalletPolicy.can_afford?(wallet:, amount:)` service that checks `wallet.balance + MAX_OVERDRAFT >= amount`).
|
|
268
|
+
- **`has_enough_balance?` stays strict.** It answers "does this wallet have enough on hand right now?" — overdraft is a deliberate choice the caller makes by attempting the debit/transfer, not by querying. So `wallet.has_enough_balance?(amount)` returns `false` even when the gem would happily complete an overdraft.
|
|
269
|
+
- **`:preserve` expiration falls back to `:none` when a transfer goes negative.** With `allow_negative_balance = true` and the default `:preserve` policy, transfers that exceed the source's positive buckets can't honestly "preserve" an expiration on the deficit portion (there is no source bucket). The transfer's `expiration_policy` is automatically downgraded to `:none` for that transfer; the receiver gets a single evergreen credit. Explicit `:fixed` (with `expires_at:`) and `:none` are honored as-is.
|
|
270
|
+
- **`:balance_depleted` fires on positive→non-positive crossings**, not on exact zero. A debit that takes a wallet from +100 to -50 in one shot still fires the callback. With `allow_negative_balance = false` the behavior collapses back to "exactly zero" (since balances can't go below zero), so existing callers don't see a change.
|
|
271
|
+
- **`:low_balance_reached` is one-shot per crossing**, regardless of how deep the dip goes. Going from +200 to -100 fires once; the next debit from -100 to -200 does not re-fire because the wallet was already below threshold.
|
|
272
|
+
- **`:insufficient_balance` callback.** Fires only when a debit or transfer is actually rejected (i.e. flag off or your service-layer floor refused). Successful overdraft transfers do not fire it.
|
|
273
|
+
- **Credits don't auto-settle prior debt.** If a wallet is at -50 and you `credit(80)`, the wallet's balance becomes 30 — but the unbacked -50 debit and the +80 credit persist as independent ledger rows. The next debit consumes from the +80 bucket (FIFO) without back-filling the older unbacked debit. The math is consistent; the audit story is "we never settled the original debt with this credit". If you want auto-settlement, do it in your service layer (e.g. when crediting, `wallet.transactions.where("amount < 0").where("ABS(amount) > spent_amount") …` and create allocations explicitly).
|
|
274
|
+
- **System-initiated reversals.** Refunds and payout reversals via `transfer_to` will also go through, even if the recipient's wallet ends up below zero. That's correct: the ledger has to settle, and a negative wallet records a real debt instead of a silent failure. Apps that want to *block* user-initiated overdrafts but *allow* system reversals should keep the floor check in their service layer, not toggle the flag mid-request.
|
|
275
|
+
- **Don't toggle the flag at runtime while wallets are negative.** `allow_negative_balance` is meant to be a stable config decision. The model carries a `balance >= 0` validation gated on the flag; flipping it OFF while wallets sit below zero leaves them un-saveable until you flip it back on (any subsequent `credit` / `debit` calls `refresh_cached_balance!`, which raises a `RecordInvalid`). If you need to turn the flag off, drain or settle any negative wallets first.
|
|
276
|
+
- **Concurrency holds.** `wallet.debit` / `wallet.credit` use `with_lock` (row-level `SELECT … FOR UPDATE`); `wallet.transfer_to` locks both wallets in id order via `lock_wallet_pair!`. Two concurrent overdraft debits on the same wallet serialize cleanly — they don't double-count `previous_balance`.
|
|
277
|
+
|
|
204
278
|
### Expiring balances
|
|
205
279
|
|
|
206
280
|
Credits can expire:
|
|
@@ -223,12 +297,13 @@ Create or edit `config/initializers/wallets.rb`:
|
|
|
223
297
|
Wallets.configure do |config|
|
|
224
298
|
config.default_asset = :coins
|
|
225
299
|
|
|
226
|
-
# Useful for
|
|
300
|
+
# Useful for direct credit/debit events in your app.
|
|
301
|
+
# Transfer business categories live on Wallets::Transfer.category.
|
|
227
302
|
config.additional_categories = %w[
|
|
228
303
|
quest_reward
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
304
|
+
provider_bonus
|
|
305
|
+
payout
|
|
306
|
+
promo_credit
|
|
232
307
|
]
|
|
233
308
|
|
|
234
309
|
config.allow_negative_balance = false
|
|
@@ -414,50 +489,60 @@ player.wallet(:gold).transfer_to(
|
|
|
414
489
|
)
|
|
415
490
|
```
|
|
416
491
|
|
|
417
|
-
###
|
|
492
|
+
### Home services booking app
|
|
418
493
|
|
|
419
|
-
|
|
494
|
+
A home-services app where the platform collects the customer payment, keeps its fee, and later pays the provider:
|
|
420
495
|
|
|
421
496
|
```ruby
|
|
422
497
|
class User < ApplicationRecord
|
|
423
498
|
has_wallets default_asset: :usd_cents
|
|
424
499
|
end
|
|
425
500
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
seller_earnings = order_total - platform_fee
|
|
501
|
+
class Marketplace < ApplicationRecord
|
|
502
|
+
has_wallets default_asset: :usd_cents
|
|
503
|
+
end
|
|
430
504
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
505
|
+
sara = User.find(1) # customer
|
|
506
|
+
lucia = User.find(2) # cleaner
|
|
507
|
+
booking = Booking.find(42)
|
|
508
|
+
marketplace = Marketplace.first
|
|
509
|
+
|
|
510
|
+
# Booking completed
|
|
511
|
+
booking_total = 5000 # $50.00
|
|
512
|
+
platform_fee = (booking_total * 0.10).to_i # 10%
|
|
513
|
+
provider_earnings = booking_total - platform_fee
|
|
514
|
+
|
|
515
|
+
# Customer pays the marketplace
|
|
516
|
+
charge = sara.wallet(:usd_cents).transfer_to(
|
|
517
|
+
marketplace.wallet(:usd_cents),
|
|
518
|
+
booking_total,
|
|
519
|
+
category: :booking_charge,
|
|
434
520
|
metadata: {
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
platform_fee: platform_fee,
|
|
438
|
-
buyer_id: buyer.id
|
|
521
|
+
booking_id: booking.id,
|
|
522
|
+
provider_id: lucia.id
|
|
439
523
|
}
|
|
440
524
|
)
|
|
441
525
|
|
|
442
|
-
#
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
526
|
+
# Marketplace pays the cleaner the net amount
|
|
527
|
+
payout = marketplace.wallet(:usd_cents).transfer_to(
|
|
528
|
+
lucia.wallet(:usd_cents),
|
|
529
|
+
provider_earnings,
|
|
530
|
+
category: :provider_payout,
|
|
531
|
+
metadata: {
|
|
532
|
+
booking_id: booking.id,
|
|
533
|
+
gross_amount: booking_total,
|
|
534
|
+
platform_fee: platform_fee,
|
|
535
|
+
customer_id: sara.id,
|
|
536
|
+
booking_charge_transfer_id: charge.id
|
|
537
|
+
}
|
|
447
538
|
)
|
|
448
539
|
|
|
449
|
-
#
|
|
450
|
-
|
|
451
|
-
|
|
540
|
+
# Cleaner later cashes out to Stripe
|
|
541
|
+
lucia.wallet(:usd_cents).debit(
|
|
542
|
+
lucia.wallet(:usd_cents).balance,
|
|
452
543
|
category: :payout,
|
|
453
544
|
metadata: { stripe_transfer_id: "tr_xxx" }
|
|
454
545
|
)
|
|
455
|
-
|
|
456
|
-
# Transaction history for accounting
|
|
457
|
-
seller.wallet(:usd_cents).history.each do |tx|
|
|
458
|
-
puts "#{tx.created_at}: #{tx.category} #{tx.amount} cents"
|
|
459
|
-
puts " Balance: #{tx.balance_before} → #{tx.balance_after}"
|
|
460
|
-
end
|
|
461
546
|
```
|
|
462
547
|
|
|
463
548
|
### Loyalty programs & Reward points
|
data/context7.json
ADDED
|
@@ -17,6 +17,14 @@ Wallets.configure do |config|
|
|
|
17
17
|
# config.table_prefix = "wallets_"
|
|
18
18
|
|
|
19
19
|
# Set to true only if your domain explicitly supports debt or overdrafts.
|
|
20
|
+
# Applies consistently across `wallet.debit` and `wallet.transfer_to` — a
|
|
21
|
+
# debit or a transfer can drive the source wallet below zero. Cap how
|
|
22
|
+
# negative a wallet can go in your own service layer (the flag is binary).
|
|
23
|
+
#
|
|
24
|
+
# Heads up: this is meant to be a stable config decision, not a runtime
|
|
25
|
+
# toggle. Flipping it OFF while wallets are negative leaves those wallets
|
|
26
|
+
# un-saveable until you flip it back on. Drain or settle negative wallets
|
|
27
|
+
# before disabling.
|
|
20
28
|
#
|
|
21
29
|
# config.allow_negative_balance = false
|
|
22
30
|
|
|
@@ -49,7 +57,8 @@ Wallets.configure do |config|
|
|
|
49
57
|
# on_balance_debited - After value is deducted from a wallet
|
|
50
58
|
# on_transfer_completed - After a transfer between wallets succeeds
|
|
51
59
|
# on_low_balance_reached - When balance drops below the threshold
|
|
52
|
-
# on_balance_depleted - When balance
|
|
60
|
+
# on_balance_depleted - When a debit drives balance from positive
|
|
61
|
+
# to zero or below (one-shot per crossing)
|
|
53
62
|
# on_insufficient_balance - When a debit or transfer is rejected
|
|
54
63
|
#
|
|
55
64
|
# Context object properties (available depending on event):
|
|
@@ -243,11 +243,50 @@ module Wallets
|
|
|
243
243
|
lock_wallet_pair!(other_wallet)
|
|
244
244
|
|
|
245
245
|
previous_balance = balance
|
|
246
|
-
|
|
246
|
+
|
|
247
|
+
# Insufficient-balance gate.
|
|
248
|
+
#
|
|
249
|
+
# `allow_negative_balance` is the single source of truth for whether
|
|
250
|
+
# the gem permits a wallet to drop below zero. Direct `wallet.debit`
|
|
251
|
+
# has always honored it via `apply_debit`; transfers must too, or
|
|
252
|
+
# the flag is half-applied and surprising (debits go through, the
|
|
253
|
+
# canonical transfer primitive doesn't).
|
|
254
|
+
#
|
|
255
|
+
# When the flag is OFF, this is the only chance to refuse cleanly:
|
|
256
|
+
# we want to dispatch `:insufficient_balance` for the observability
|
|
257
|
+
# callback, then raise BEFORE creating the Transfer row. (The later
|
|
258
|
+
# `apply_debit` call would also catch this, but only after a
|
|
259
|
+
# pointless Transfer row was created.)
|
|
260
|
+
#
|
|
261
|
+
# When the flag is ON, we let the transfer through. `apply_debit`
|
|
262
|
+
# below will allocate as many positive buckets as exist; the
|
|
263
|
+
# remaining shortfall lands as an unbacked negative transaction the
|
|
264
|
+
# wallet's `balance` accounting accommodates via
|
|
265
|
+
# `unbacked_negative_balance`.
|
|
266
|
+
if amount > previous_balance && !allow_negative_balance?
|
|
247
267
|
dispatch_insufficient_balance!(amount, previous_balance, metadata)
|
|
248
268
|
raise InsufficientBalance, "Insufficient balance (#{previous_balance} < #{amount})"
|
|
249
269
|
end
|
|
250
270
|
|
|
271
|
+
# When a transfer dips below zero we can't faithfully apply the
|
|
272
|
+
# `:preserve` expiration policy: there are no positive source
|
|
273
|
+
# buckets to inherit expirations from for the deficit portion.
|
|
274
|
+
# `build_preserved_transfer_inbound_credit_specs` would refuse with
|
|
275
|
+
# `InvalidTransfer` on the count mismatch, defeating the very
|
|
276
|
+
# callers who opted into negative balances. The only honest
|
|
277
|
+
# fallback is to collapse the inbound side to a single evergreen
|
|
278
|
+
# credit (`:none`) — value created without a source bucket has no
|
|
279
|
+
# source expiration to preserve.
|
|
280
|
+
#
|
|
281
|
+
# Callers who care about a specific expiration on the inbound side
|
|
282
|
+
# should pass `expiration_policy: :fixed, expires_at: …`. Those are
|
|
283
|
+
# honored unchanged because they don't depend on source-bucket
|
|
284
|
+
# walking. Same for an explicit `:none`.
|
|
285
|
+
if resolved_policy == "preserve" && amount > previous_balance
|
|
286
|
+
resolved_policy = "none"
|
|
287
|
+
inbound_expires_at = nil
|
|
288
|
+
end
|
|
289
|
+
|
|
251
290
|
transfer = transfer_class.create!(
|
|
252
291
|
from_wallet: self,
|
|
253
292
|
to_wallet: other_wallet,
|
|
@@ -582,11 +621,21 @@ module Wallets
|
|
|
582
621
|
)
|
|
583
622
|
end
|
|
584
623
|
|
|
585
|
-
|
|
624
|
+
# `:balance_depleted` fires when the wallet crosses from a positive
|
|
625
|
+
# balance to ZERO OR LOWER on this debit. We use `<= 0` rather than
|
|
626
|
+
# `== 0` so the callback still fires when `allow_negative_balance` is
|
|
627
|
+
# true and a single debit takes the wallet from e.g. +100 to -50
|
|
628
|
+
# (skipping exact zero). With negatives disabled, `balance` cannot
|
|
629
|
+
# go below zero, so `<= 0` collapses to `== 0` and the original
|
|
630
|
+
# "exactly zero" semantic is preserved for default-config users.
|
|
631
|
+
# Either way the callback is one-shot per crossing — going from -50
|
|
632
|
+
# to -80 doesn't re-fire because `previous_balance` was already
|
|
633
|
+
# non-positive.
|
|
634
|
+
if previous_balance.positive? && !balance.positive?
|
|
586
635
|
dispatch_callback(:depleted,
|
|
587
636
|
wallet: self,
|
|
588
637
|
previous_balance: previous_balance,
|
|
589
|
-
new_balance:
|
|
638
|
+
new_balance: balance
|
|
590
639
|
)
|
|
591
640
|
end
|
|
592
641
|
end
|
data/lib/wallets/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: wallets
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- rameerez
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-03
|
|
10
|
+
date: 2026-05-03 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: rails
|
|
@@ -50,6 +50,7 @@ files:
|
|
|
50
50
|
- LICENSE.txt
|
|
51
51
|
- README.md
|
|
52
52
|
- Rakefile
|
|
53
|
+
- context7.json
|
|
53
54
|
- lib/generators/wallets/install_generator.rb
|
|
54
55
|
- lib/generators/wallets/templates/create_wallets_tables.rb.erb
|
|
55
56
|
- lib/generators/wallets/templates/initializer.rb
|