wallets 0.4.1 → 0.4.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 06f03b26d96187aae331606ce523b158ad3303d3a0298cefdaf673d27f466bac
4
- data.tar.gz: ecdb3cebac2c0b5ed8a83d14cf7a98e1b046ee59f3837bc2683bf3b2c0f8fe18
3
+ metadata.gz: d628d500b3ad1dfb236b6181fe30f976ae450aa9d4b8d8f5eb609969c6d59e66
4
+ data.tar.gz: 7c91562327a82f56429055df8f5b40e5644a226b7a83bfde9ed3cb68628faec0
5
5
  SHA512:
6
- metadata.gz: b8041ad7293df22cf7a159bcc76b4c8186a2019be95250da10b7d9fe1760dcc4f06dd517a0c454c2ab229eb433036ab34c548a895b9ba176936266103477844a
7
- data.tar.gz: c258acf79ced27d6e6acc0311bea40dec74683cc27bd2bd1ab246b7dd111600419d80608677c622207df3b1c8ae58a796296ac6d27b4fe3b9043e9f4ec192f06
6
+ metadata.gz: 8dd70a121851414b83efe8b1db52fff694a9c5b8a624135413d19c0e47d2a641538464d23879f3bbece40dacb87983cbe8af9f6ff6ed54d129127ef256de81c4
7
+ data.tar.gz: 903e0a69db10cf20c2502ff86a5914ffcbad2713cee1ae6e6f8591c53b52bae9fdfc61f61defe79130be9b5f9a496f849a6f6cbbaea2e5977e34a6e34751b82f
data/CHANGELOG.md CHANGED
@@ -1,3 +1,9 @@
1
+ ## [0.4.2] - 2026-10-18
2
+
3
+ ### Fixed
4
+
5
+ - **A rolled-back command no longer poisons the wallet or hold instance it wrote.** Every command starts by locking its row, and Rails refuses to lock a record with unsaved changes. When a command's savepoint or the caller's transaction was rolled back after the command had written the row (a block that spends inside a command and is then refused, a `transaction { wallet.debit(...); raise }` the caller rescues), Rails kept the cached `balance`, `ledger_revision` and `updated_at` the command had written in memory as unsaved changes, and the next command on the same wallet instance raised `RuntimeError: Locking a record with unpersisted changes is not supported` instead of running; a rolled-back release or capture did the same to its hold. The columns a command writes are the ledger's, not the caller's, and the lock reads the row again anyway, so `lock!` discards them first. A change the caller made to any other attribute is still refused, as Rails intends. Found by installing usage_credits 1.0.0 into a fresh app; its `spend_credits_on` is the first shape.
6
+
1
7
  ## [0.4.1] - 2026-10-18
2
8
 
3
9
  ### Documentation
@@ -124,6 +124,22 @@ module Wallets
124
124
  # nothing. Why it ended is recorded next to why it began, never over it.
125
125
  #
126
126
  # hold.release!(reason: "cleared_by_operator", metadata: {actor_id: operator.id})
127
+ # The columns a release or a capture writes on the hold row; the caller
128
+ # never sets them after the hold is placed.
129
+ COMMAND_WRITTEN_COLUMNS = %w[released_at release_reason release_metadata capture_transaction_id updated_at].freeze
130
+ private_constant :COMMAND_WRITTEN_COLUMNS
131
+
132
+ # A release or a capture locks the hold row, and Rails refuses to lock a
133
+ # record with unsaved changes: a rolled-back release or capture leaves
134
+ # what it wrote in memory as unsaved changes, so retrying it on the same
135
+ # instance would raise. Those columns are the command's, and the lock
136
+ # reads the row again, so they are discarded first (see
137
+ # `Wallets::WalletBase#lock!`).
138
+ def lock!(lock = true)
139
+ restore_attributes(changed & COMMAND_WRITTEN_COLUMNS) if persisted?
140
+ super
141
+ end
142
+
127
143
  def release!(reason: nil, metadata: {})
128
144
  raise InvalidHold, "Only a saved hold can be released" unless persisted?
129
145
 
@@ -66,6 +66,25 @@ module Wallets
66
66
  ARRIVAL_HOLD_OPTIONS = %i[category until metadata].freeze
67
67
  private_constant :ARRIVAL_HOLD_OPTIONS
68
68
 
69
+ # The columns the ledger itself writes on the wallet row
70
+ # (`refresh_cached_balance!`); the caller never sets them.
71
+ LEDGER_WRITTEN_COLUMNS = %w[balance ledger_revision updated_at].freeze
72
+ private_constant :LEDGER_WRITTEN_COLUMNS
73
+
74
+ # Every command starts by locking the wallet row, and Rails refuses to
75
+ # lock a record with unsaved changes. A rolled-back command leaves
76
+ # exactly that behind: the cached balance, the revision and `updated_at`
77
+ # it had written stay in memory, marked as unsaved changes, once its
78
+ # savepoint or the caller's transaction is rolled back, and the next
79
+ # command on the same instance would raise. Those columns are the
80
+ # ledger's, not the caller's, and the lock reads the row again anyway,
81
+ # so they are discarded first. A change to any other attribute is the
82
+ # caller's, and Rails' refusal stands.
83
+ def lock!(lock = true)
84
+ restore_attributes(changed & LEDGER_WRITTEN_COLUMNS) if persisted?
85
+ super
86
+ end
87
+
69
88
  self.table_suffix = "wallets"
70
89
 
71
90
  # =========================================
@@ -1642,12 +1661,13 @@ module Wallets
1642
1661
  # the balance did not move, because `touch` writes nothing under
1643
1662
  # `no_touching`.
1644
1663
  #
1645
- # `update!` rather than assigning and then saving: if the command's
1646
- # savepoint rolls back afterwards, Active Record leaves the wallet with no
1647
- # pending changes, so its next command can lock it. It is clean, not
1648
- # restored: it keeps the rolled-back values in memory until that lock
1649
- # reloads the row. Assigning first would leave them pending instead, and
1650
- # no later command could lock the wallet.
1664
+ # `update!` rather than assigning and then saving, so nothing sits pending
1665
+ # between commands. A rollback can still leave these columns pending:
1666
+ # once the real transaction, or a savepoint the caller opened, rolls
1667
+ # back, Active Record restores the values this write left in memory as
1668
+ # unsaved changes. `lock!` discards exactly those (LEDGER_WRITTEN_COLUMNS)
1669
+ # before locking, so the next command can lock the wallet and reads the
1670
+ # row again.
1651
1671
  def refresh_cached_balance!
1652
1672
  available = current_balance
1653
1673
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Wallets
4
- VERSION = "0.4.1"
4
+ VERSION = "0.4.2"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: wallets
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.1
4
+ version: 0.4.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - rameerez