spree_cm_commissioner 2.12.7.pre.pre1 → 2.12.8

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.
Files changed (22) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +1 -1
  3. data/app/jobs/spree_cm_commissioner/inventory_items/bulk_adjust_quantities_job.rb +2 -3
  4. data/app/jobs/spree_cm_commissioner/inventory_items/bulk_adjust_quantities_on_convert_job.rb +2 -3
  5. data/app/jobs/spree_cm_commissioner/inventory_items/bulk_adjust_quantities_on_hold_job.rb +2 -3
  6. data/app/models/spree_cm_commissioner/inventory_item.rb +2 -22
  7. data/app/services/spree_cm_commissioner/inventory_holds/acquire.rb +40 -106
  8. data/app/services/spree_cm_commissioner/inventory_holds/convert.rb +9 -76
  9. data/app/services/spree_cm_commissioner/inventory_holds/release.rb +6 -48
  10. data/app/services/spree_cm_commissioner/inventory_items/adjust_available_quantity.rb +3 -4
  11. data/app/services/spree_cm_commissioner/inventory_items/bulk_adjust_quantities.rb +4 -6
  12. data/app/services/spree_cm_commissioner/inventory_items/bulk_adjust_quantities_by_variant.rb +1 -11
  13. data/app/services/spree_cm_commissioner/inventory_items/bulk_adjust_quantities_on_convert.rb +4 -6
  14. data/app/services/spree_cm_commissioner/inventory_items/bulk_adjust_quantities_on_hold.rb +3 -6
  15. data/app/services/spree_cm_commissioner/inventory_items/move_to_locked.rb +12 -57
  16. data/app/services/spree_cm_commissioner/redis_stock/base.rb +2 -3
  17. data/app/services/spree_cm_commissioner/redis_stock/restock.rb +3 -35
  18. data/app/services/spree_cm_commissioner/redis_stock/unstock.rb +3 -35
  19. data/config/locales/en.yml +0 -2
  20. data/config/locales/km.yml +0 -2
  21. data/lib/spree_cm_commissioner/version.rb +1 -1
  22. metadata +3 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4f4e6fad42b4e85ffc7259488c4594acea3dd6949dfbbd5dca113cff65740f7f
4
- data.tar.gz: 81a670727506c0b79c5b18789c0b88bd042cc7a5b94318d49616251389b88129
3
+ metadata.gz: 742162ce28f156432f76f071c6538e682fecfa8008758acc698d283cb2a3742c
4
+ data.tar.gz: bfafd91f275bc28a16234e3153ee8d83aa5b5040be06acca7850770b2573fe01
5
5
  SHA512:
6
- metadata.gz: 16e72d5ccf30ad000ba4377824ce84a8245104f55835a8e298ca5d93117fdaf06f0a1f6adeef544dae996e5cf527980669129f6230068f7eb6a445d021dcd2b2
7
- data.tar.gz: 1688cbabe227af883abc06175772c009ecf138433ff180a197f668010db60298c49ac113eb33027c9c63bd14a2d7b80e403ab246961b8c3af94381d40763b668
6
+ metadata.gz: f8c8857a35fe48099949496f9e69c0741ca32a18a05fbc05afafdc1cb5cd7feb3394b3b0158cbb27dea96f585e378f9a4a14c0d2fbac1b8cee6e54c8a51be443
7
+ data.tar.gz: d8e013c7a5172fabbfea21e83ac52d5231351ed380960e1308b458ad1e7b6cd960bb2ca7a6204b82448f3d494c17cb3eb3ebb9a5d704228b901c789dacef09f2
data/Gemfile.lock CHANGED
@@ -34,7 +34,7 @@ GIT
34
34
  PATH
35
35
  remote: .
36
36
  specs:
37
- spree_cm_commissioner (2.12.7.pre.pre1)
37
+ spree_cm_commissioner (2.12.8)
38
38
  activerecord-multi-tenant
39
39
  activerecord_json_validator (~> 2.1, >= 2.1.3)
40
40
  aws-sdk-cloudfront
@@ -23,14 +23,13 @@ module SpreeCmCommissioner
23
23
  "line_item:#{options[:line_item_ids].compact.uniq.sort.join(':')}"
24
24
  end
25
25
 
26
- # :line_item_ids, :inventory_id_and_quantities, :caller_source, :correlation_id
26
+ # :line_item_ids, :inventory_id_and_quantities, :caller_source
27
27
  #
28
28
  # :caller_source is a string like "ClassName#method" identifying who enqueued the job.
29
29
  def bulk_adjust_quantities!(options = {})
30
30
  SpreeCmCommissioner::InventoryItems::BulkAdjustQuantities.call!(
31
31
  inventory_id_and_quantities: options[:inventory_id_and_quantities],
32
- caller_source: options[:caller_source],
33
- correlation_id: options[:correlation_id]
32
+ caller_source: options[:caller_source]
34
33
  )
35
34
  end
36
35
  end
@@ -32,15 +32,14 @@ module SpreeCmCommissioner
32
32
  .join(',')
33
33
  end
34
34
 
35
- # :order_id, :inventory_id_and_quantities, :caller_source, :correlation_id
35
+ # :order_id, :inventory_id_and_quantities, :caller_source
36
36
  def bulk_adjust_quantities_on_convert(options = {})
37
37
  raise ArgumentError, 'order_id is required' if options[:order_id].blank?
38
38
  raise ArgumentError, 'inventory_id_and_quantities is required' if options[:inventory_id_and_quantities].blank?
39
39
 
40
40
  SpreeCmCommissioner::InventoryItems::BulkAdjustQuantitiesOnConvert.call(
41
41
  inventory_id_and_quantities: options[:inventory_id_and_quantities],
42
- caller_source: options[:caller_source],
43
- correlation_id: options[:correlation_id]
42
+ caller_source: options[:caller_source]
44
43
  )
45
44
  end
46
45
  end
@@ -38,7 +38,7 @@ module SpreeCmCommissioner
38
38
  .join(',')
39
39
  end
40
40
 
41
- # :order_id, :inventory_id_and_quantities, :caller_source, :correlation_id
41
+ # :order_id, :inventory_id_and_quantities, :caller_source
42
42
  #
43
43
  # :order_id is used for unique idempotency key generation to prevent duplicate
44
44
  # processing, though it's not used in the adjustment itself.
@@ -49,8 +49,7 @@ module SpreeCmCommissioner
49
49
 
50
50
  SpreeCmCommissioner::InventoryItems::BulkAdjustQuantitiesOnHold.call(
51
51
  inventory_id_and_quantities: options[:inventory_id_and_quantities],
52
- caller_source: options[:caller_source],
53
- correlation_id: options[:correlation_id]
52
+ caller_source: options[:caller_source]
54
53
  )
55
54
  end
56
55
  end
@@ -107,10 +107,7 @@ module SpreeCmCommissioner
107
107
  end
108
108
 
109
109
  # This method is only used when admin update stock
110
- def adjust_quantity!(quantity, correlation_id: nil)
111
- correlation_id ||= SecureRandom.uuid
112
- quantity_available_before = redis_quantity_after = nil
113
-
110
+ def adjust_quantity!(quantity)
114
111
  with_lock do
115
112
  # IMPORTANT: Apply quantity changes directly without defensive clamping.
116
113
  # The model validation will catch any attempts to go negative, surfacing bugs
@@ -121,8 +118,6 @@ module SpreeCmCommissioner
121
118
  #
122
119
  # Why? Clamping masks bugs. Validation errors are better than silent data loss.
123
120
  # See: /docs/lessons-learned/inventory-consistency-issues.md#lesson-learned-async-job-validation-strategy
124
- quantity_available_before = quantity_available
125
-
126
121
  self.max_capacity = max_capacity + quantity
127
122
  self.quantity_available = quantity_available + quantity
128
123
  save!
@@ -132,23 +127,8 @@ module SpreeCmCommissioner
132
127
  # high-contention hot path, so keeping it in the same transaction as `save!` means a
133
128
  # Redis failure rolls the DB change back too, instead of leaving the DB and Redis counts
134
129
  # out of sync with nothing to reconcile them.
135
- redis_quantity_after = adjust_quantity_in_redis(quantity)
130
+ adjust_quantity_in_redis(quantity)
136
131
  end
137
-
138
- # Logged after with_lock releases: I/O here would otherwise extend how long the row's
139
- # SELECT ... FOR UPDATE is held under high-traffic contention — same reasoning as the other
140
- # Bulk* adjusters in this gem.
141
- CmAppLogger.log(
142
- label: "#{self.class.name}#adjust_quantity!",
143
- data: {
144
- correlation_id: correlation_id,
145
- inventory_item_id: id,
146
- delta: quantity,
147
- quantity_available_before: quantity_available_before,
148
- quantity_available_after: quantity_available,
149
- redis_quantity_after: redis_quantity_after
150
- }
151
- )
152
132
  end
153
133
 
154
134
  # Stock and hold keys. `locked: true` addresses the withheld pool — see LineItem#locked_stock?,
@@ -13,10 +13,6 @@ module SpreeCmCommissioner
13
13
  AcquireLimitExceededError = Class.new(StandardError)
14
14
 
15
15
  def call(order:, expires_at:)
16
- # One id per acquire attempt, threaded through the Redis-mutation log below and into the
17
- # DB-sync job's own log (enqueue_sync_inventory_on_hold) — greppable end-to-end trail from
18
- # "Redis hold counter changed" to "DB row actually caught up", independent of async timing.
19
- correlation_id = SecureRandom.uuid
20
16
  line_items = order.line_items.select(&:should_hold_inventory?)
21
17
  return success(nil) if line_items.empty?
22
18
 
@@ -33,25 +29,21 @@ module SpreeCmCommissioner
33
29
  raise AcquireLimitExceededError, result.error.to_s unless result.success?
34
30
  end
35
31
 
36
- hold = with_advisory_lock!(order) do
37
- h = acquire_hold_for_order!(order, line_items, expires_at, correlation_id)
32
+ with_advisory_lock!(order) do
33
+ hold = acquire_hold_for_order!(order, line_items, expires_at)
38
34
 
39
- enqueue_release_job(h)
40
- enqueue_sync_inventory_on_hold(order, correlation_id)
35
+ enqueue_release_job(hold)
36
+ enqueue_sync_inventory_on_hold(order)
41
37
 
42
- h
43
- end
44
-
45
- # Logged after the advisory lock releases (see with_advisory_lock!'s ensure) — never while
46
- # held — so a slow log write can't extend how long other requests for this order wait on it.
47
- CmAppLogger.log(
48
- label: "#{self.class.name}#call success",
49
- data: { correlation_id: correlation_id, order_id: order.id, hold_id: hold.id }
50
- )
38
+ CmAppLogger.log(
39
+ label: "#{self.class.name}#call success",
40
+ data: { order_id: order.id, hold_id: hold.id }
41
+ )
51
42
 
52
- success(hold)
43
+ success(hold)
44
+ end
53
45
  rescue StandardError => e
54
- error = { error_type: e.class.name.demodulize, correlation_id: correlation_id, order_id: order.id, message: e.message }
46
+ error = { error_type: e.class.name.demodulize, order_id: order.id, message: e.message }
55
47
  CmAppLogger.error(label: "#{self.class.name}#call failed", data: error)
56
48
  failure(nil, e.message)
57
49
  end
@@ -81,71 +73,45 @@ module SpreeCmCommissioner
81
73
  #
82
74
  # Risk: Redis deduction could orphan if exception raised after Redis commit but before DB.
83
75
  # Accepted—advisory lock ensures single process per order prevents impact.
84
- # AcquireInsufficientStockError (not ActiveRecord::Rollback) is what rolls hold.save! back on
85
- # insufficient stock — any exception, not just Rollback, unwinds a `transaction do` block and
86
- # rolls it back; Rollback is just the one case ActiveRecord swallows instead of re-raising.
87
- # Using our own error class means it still propagates out normally (real transaction or a
88
- # plain `.and_yield` test double alike) so the rescue below can log, then re-raise it.
89
- #
90
- # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
91
- def acquire_hold_for_order!(order, line_items, expires_at, correlation_id)
76
+ def acquire_hold_for_order!(order, line_items, expires_at)
92
77
  hold = SpreeCmCommissioner::InventoryHold.find_or_initialize_by(order: order)
93
78
  return hold if hold.persisted? && hold.active?
94
79
  return hold if hold.persisted? && hold.payment_locked?
95
80
 
96
- redis_rows = nil
97
-
98
- begin
99
- SpreeCmCommissioner::InventoryHold.transaction do
100
- begin
101
- hold.assign_attributes(
102
- user: order.user,
103
- status: :active,
104
- held_at: hold.held_at || Time.current,
105
- expires_at: expires_at,
106
- payment_locked_at: nil,
107
- release_reason: nil,
108
- total_quantity_on_hold: line_items.sum(&:quantity),
109
- ip_address: order.last_ip_address
110
- )
111
-
112
- hold.save!
113
- rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotUnique
114
- raise AcquireHoldSaveError, I18n.t('inventory_hold.acquire.db_save_error')
115
- end
116
-
117
- begin
118
- redis_rows, redis_applied = acquire_redis_stock!(line_items)
119
- rescue Redis::BaseError, Redis::CommandError, IOError
120
- raise AcquireRedisDeductionError, I18n.t('inventory_hold.acquire.redis_error')
121
- end
122
-
123
- raise AcquireInsufficientStockError, I18n.t('inventory_hold.acquire.insufficient_stock') unless redis_applied
81
+ SpreeCmCommissioner::InventoryHold.transaction do
82
+ begin
83
+ hold.assign_attributes(
84
+ user: order.user,
85
+ status: :active,
86
+ held_at: hold.held_at || Time.current,
87
+ expires_at: expires_at,
88
+ payment_locked_at: nil,
89
+ release_reason: nil,
90
+ total_quantity_on_hold: line_items.sum(&:quantity),
91
+ ip_address: order.last_ip_address
92
+ )
93
+
94
+ hold.save!
95
+ rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotUnique
96
+ raise AcquireHoldSaveError, I18n.t('inventory_hold.acquire.db_save_error')
124
97
  end
125
- rescue AcquireInsufficientStockError => e
126
- # Logged after the transaction has already rolled back and released its row lock — never
127
- # while held — so a slow log write can't extend how long other requests wait on this hold.
128
- log_redis_hold_mutation(order: order, hold: hold, correlation_id: correlation_id, rows: redis_rows, applied: false) if redis_rows
129
- raise e
130
- end
131
98
 
132
- # Logged after the transaction commits and releases its row lock — never while held.
133
- log_redis_hold_mutation(order: order, hold: hold, correlation_id: correlation_id, rows: redis_rows, applied: true) if redis_rows
99
+ begin
100
+ positive = acquire_redis_stock!(line_items)
101
+ raise AcquireInsufficientStockError, I18n.t('inventory_hold.acquire.insufficient_stock') unless positive
102
+ rescue Redis::BaseError, Redis::CommandError, IOError
103
+ raise AcquireRedisDeductionError, I18n.t('inventory_hold.acquire.redis_error')
104
+ end
134
105
 
135
- hold
106
+ hold
107
+ end
136
108
  end
137
- # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
138
109
 
139
- # Pure Redis I/O — no logging in here (that happens after acquire_hold_for_order!'s
140
- # transaction closes). Returns [rows, applied] rather than logging directly, so the mutation
141
- # detail survives even on the insufficient-stock path where the caller rolls back and
142
- # re-raises.
143
110
  def acquire_redis_stock!(line_items)
144
111
  stock_keys = []
145
112
  hold_keys = []
146
113
  quantities = []
147
114
  warmable_items = []
148
- locked_flags = []
149
115
 
150
116
  line_items.each do |li|
151
117
  # A locked-stock line reserves from the locked pool's own key pair. The Lua below is
@@ -161,57 +127,26 @@ module SpreeCmCommissioner
161
127
  hold_keys << item.redis_hold_key(locked: locked)
162
128
  quantities << li.quantity
163
129
  warmable_items << item
164
- locked_flags << locked
165
130
  end
166
131
  end
167
132
 
168
- return [nil, true] if hold_keys.empty?
133
+ return true if hold_keys.empty?
169
134
 
170
- # Captured (not discarded) so the mutation log (logged later, after the transaction) can
171
- # report exactly what Redis held a moment before this eval — zero extra Redis round trips,
172
- # the warm already reads it.
173
- cached_items = SpreeCmCommissioner::RedisStock::CachedInventoryItemsBuilder.new(warmable_items).call
135
+ SpreeCmCommissioner::RedisStock::CachedInventoryItemsBuilder.new(warmable_items).call
174
136
  interleaved_keys = stock_keys.zip(hold_keys).flatten
175
137
 
176
138
  result = SpreeCmCommissioner.inventory_redis_pool.with do |redis|
177
139
  redis.eval(acquire_lua_script, keys: interleaved_keys, argv: quantities)
178
140
  end
179
141
 
180
- rows = cached_items.each_with_index.map { |cached_item, i| [cached_item, quantities[i], locked_flags[i]] }
181
- [rows, result.positive?]
182
- end
183
-
184
- # `quantity_on_hold`/`quantity_locked_on_hold` on each cached item is exactly what Redis held
185
- # immediately before this eval (the warm above just read it). `applied: false` means the eval
186
- # rejected the whole batch (insufficient stock) — nothing was written, so there is no "after".
187
- # `rows` is [cached_item, quantity_delta, locked?] per inventory item, in eval order.
188
- def log_redis_hold_mutation(order:, hold:, correlation_id:, rows:, applied:)
189
- CmAppLogger.log(
190
- label: "#{self.class.name}#acquire_redis_stock!",
191
- data: {
192
- correlation_id: correlation_id,
193
- order_id: order.id,
194
- hold_id: hold.id,
195
- applied: applied,
196
- items: rows.map do |cached_item, delta, locked|
197
- before = locked ? cached_item.quantity_locked_on_hold : cached_item.quantity_on_hold
198
- {
199
- inventory_item_id: cached_item.inventory_item_id,
200
- locked: locked,
201
- quantity_on_hold_before: before,
202
- delta: delta,
203
- quantity_on_hold_after: applied ? before + delta : before
204
- }
205
- end
206
- }
207
- )
142
+ result.positive?
208
143
  end
209
144
 
210
145
  def enqueue_release_job(hold)
211
146
  SpreeCmCommissioner::InventoryHolds::ReleaseJob.set(wait_until: hold.expires_at).perform_later(hold_id: hold.id)
212
147
  end
213
148
 
214
- def enqueue_sync_inventory_on_hold(order, correlation_id)
149
+ def enqueue_sync_inventory_on_hold(order)
215
150
  inventory_id_and_quantities = order.line_items.flat_map do |line_item|
216
151
  next [] unless line_item.should_hold_inventory?
217
152
 
@@ -229,7 +164,6 @@ module SpreeCmCommissioner
229
164
  SpreeCmCommissioner::InventoryItems::BulkAdjustQuantitiesOnHoldJob.perform_later(
230
165
  order_id: order.id,
231
166
  caller_source: "#{self.class.name}#enqueue_sync_inventory_on_hold",
232
- correlation_id: correlation_id,
233
167
  inventory_id_and_quantities: inventory_id_and_quantities
234
168
  )
235
169
  end
@@ -4,59 +4,30 @@ module SpreeCmCommissioner
4
4
  prepend ::Spree::ServiceModule::Base
5
5
  extend SpreeCmCommissioner::ServiceModuleThrowable
6
6
 
7
- HoldExpiredError = Class.new(StandardError)
8
-
9
7
  def call(order:)
10
- # One id per conversion attempt, threaded into the Redis-mutation log below and the DB-sync
11
- # job's own log — same purpose as Acquire's correlation_id.
12
- correlation_id = SecureRandom.uuid
13
8
  hold = InventoryHold.find_by(order: order)
14
9
  return success(order) if hold.nil?
15
- return success(order) if hold.converted?
16
- return failure(nil, hold_expired_message) if hold.finalized?
17
-
18
- redis_rows = nil
10
+ return success(order) if hold.finalized?
19
11
 
20
12
  InventoryHold.transaction do
21
13
  hold.lock!
22
14
 
23
- raise HoldExpiredError, hold_expired_message if hold.finalized?
24
-
25
- # Finalized (converted or released) between the check above and acquiring this lock —
26
- # e.g. ReleaseJob won the race, or a concurrent completion attempt for the same order
27
- # converted it first. Strict: fail either way rather than silently treating "someone
28
- # else already converted it" as success — a losing duplicate request should surface as
29
- # a failure, not a false success for work it didn't do.
30
-
31
- hold.update!(status: :converted)
32
- redis_rows = convert_hold_redis!(order)
33
- enqueue_sync_inventory(order, correlation_id)
15
+ unless hold.finalized?
16
+ hold.update!(status: :converted)
17
+ convert_hold_redis!(order)
18
+ enqueue_sync_inventory(order)
19
+ end
34
20
  end
35
21
 
36
- # Logged after the transaction commits and hold.lock! releases its row lock — never while
37
- # held — so a slow log write can't extend how long other requests wait on this hold.
38
- log_redis_convert_mutation(order: order, hold: hold, correlation_id: correlation_id, rows: redis_rows) if redis_rows
39
-
40
22
  success(order)
41
23
  rescue StandardError => e
42
- error = { error_type: e.class.name.demodulize, correlation_id: correlation_id, order_id: order.id, message: e.message }
24
+ error = { error_type: e.class.name.demodulize, order_id: order.id, message: e.message }
43
25
  CmAppLogger.error(label: "#{self.class.name}#call failed", data: error)
44
26
  failure(nil, e.message)
45
27
  end
46
28
 
47
29
  private
48
30
 
49
- # A hold that is `released` (any reason — expiry, payment timeout, cancellation) by the time
50
- # the order tries to complete means its reservation already expired and the unit may have
51
- # been reallocated to someone else. Completion must not silently treat that as "already
52
- # handled" the way an already-`converted` hold correctly is (idempotent re-entry). Failing
53
- # here surfaces as UnableToConvertInventoryError from OrderHolds::Reserve, which rolls back
54
- # the :complete transition — the same rollback path a genuine insufficient-stock failure in
55
- # RedisStock::Unstock already takes.
56
- def hold_expired_message
57
- I18n.t('inventory_hold.convert.hold_expired')
58
- end
59
-
60
31
  # Moves a held unit to a permanent sale by decrementing the hold counter AND the stock
61
32
  # counter together, in one atomic Lua call. These used to be two separate Redis round trips
62
33
  # (hold decrement here, stock decrement in RedisStock::Unstock, called a moment later from
@@ -67,14 +38,11 @@ module SpreeCmCommissioner
67
38
  #
68
39
  # RedisStock::Unstock skips any line item that should_hold_inventory? for exactly this reason
69
40
  # — this method is now the only place their stock key gets decremented.
70
- # Pure Redis I/O — no logging in here (that happens after the caller's transaction closes).
71
- # Returns the rows the mutation log needs, or nil when there was nothing to convert.
72
41
  def convert_hold_redis!(order)
73
42
  stock_keys = []
74
43
  hold_keys = []
75
44
  quantities = []
76
45
  warmable_items = []
77
- locked_flags = []
78
46
 
79
47
  order.line_items.each do |li|
80
48
  next unless li.should_hold_inventory?
@@ -89,51 +57,17 @@ module SpreeCmCommissioner
89
57
  hold_keys << item.redis_hold_key(locked: locked)
90
58
  quantities << li.quantity
91
59
  warmable_items << item
92
- locked_flags << locked
93
60
  end
94
61
  end
95
62
 
96
63
  return if hold_keys.empty?
97
64
 
98
- # Captured (not discarded) — the warm already reads both counters this eval is about to
99
- # decrement, so the mutation log (logged later, after the transaction) needs no extra Redis
100
- # round trip.
101
- cached_items = SpreeCmCommissioner::RedisStock::CachedInventoryItemsBuilder.new(warmable_items).call
65
+ SpreeCmCommissioner::RedisStock::CachedInventoryItemsBuilder.new(warmable_items).call
102
66
 
103
67
  interleaved_keys = stock_keys.zip(hold_keys).flatten
104
68
  SpreeCmCommissioner.inventory_redis_pool.with do |redis|
105
69
  redis.eval(convert_hold_lua_script, keys: interleaved_keys, argv: quantities)
106
70
  end
107
-
108
- cached_items.each_with_index.map { |cached_item, i| [cached_item, quantities[i], locked_flags[i]] }
109
- end
110
-
111
- # This script always applies (no insufficiency check — the unit was already validated and
112
- # reserved at Acquire time), so every item here has a real "after": both the stock and hold
113
- # counters drop by the same delta. `rows` is [cached_item, quantity_delta, locked?] per
114
- # inventory item, in eval order.
115
- def log_redis_convert_mutation(order:, hold:, correlation_id:, rows:)
116
- CmAppLogger.log(
117
- label: "#{self.class.name}#convert_hold_redis!",
118
- data: {
119
- correlation_id: correlation_id,
120
- order_id: order.id,
121
- hold_id: hold.id,
122
- items: rows.map do |cached_item, delta, locked|
123
- stock_before = locked ? cached_item.quantity_locked : cached_item.quantity_available
124
- hold_before = locked ? cached_item.quantity_locked_on_hold : cached_item.quantity_on_hold
125
- {
126
- inventory_item_id: cached_item.inventory_item_id,
127
- locked: locked,
128
- delta: delta,
129
- quantity_available_before: stock_before,
130
- quantity_available_after: stock_before - delta,
131
- quantity_on_hold_before: hold_before,
132
- quantity_on_hold_after: hold_before - delta
133
- }
134
- end
135
- }
136
- )
137
71
  end
138
72
 
139
73
  def convert_hold_lua_script
@@ -152,7 +86,7 @@ module SpreeCmCommissioner
152
86
  # because the hold is gone, quantity_available drops because the unit is permanently sold) via
153
87
  # ONE job that applies both under a single row lock per item — see
154
88
  # InventoryItems::BulkAdjustQuantitiesOnConvert for why this isn't the usual two separate jobs.
155
- def enqueue_sync_inventory(order, correlation_id)
89
+ def enqueue_sync_inventory(order)
156
90
  adjustments = order.line_items.flat_map do |line_item|
157
91
  next [] unless line_item.should_hold_inventory?
158
92
 
@@ -171,7 +105,6 @@ module SpreeCmCommissioner
171
105
  SpreeCmCommissioner::InventoryItems::BulkAdjustQuantitiesOnConvertJob.perform_later(
172
106
  order_id: order.id,
173
107
  caller_source: "#{self.class.name}#enqueue_sync_inventory",
174
- correlation_id: correlation_id,
175
108
  inventory_id_and_quantities: adjustments
176
109
  )
177
110
  end
@@ -9,11 +9,7 @@ module SpreeCmCommissioner
9
9
  def call(hold:, reason: :hold_expired, scheduled_release: false)
10
10
  return success(hold) if hold.finalized?
11
11
 
12
- # One id per release attempt, threaded into the Redis-mutation log below and the DB-sync
13
- # job's own log — same purpose as Acquire's/Convert's correlation_id.
14
- correlation_id = SecureRandom.uuid
15
12
  released = false
16
- redis_rows = nil
17
13
 
18
14
  InventoryHold.transaction do
19
15
  hold.lock!
@@ -27,15 +23,11 @@ module SpreeCmCommissioner
27
23
  next if scheduled_release && hold.expires_at > Time.current
28
24
 
29
25
  hold.update!(status: :released, release_reason: reason)
30
- redis_rows = restock_redis!(hold)
31
- enqueue_sync_inventory_on_hold(hold.order, correlation_id)
26
+ restock_redis!(hold)
27
+ enqueue_sync_inventory_on_hold(hold.order)
32
28
  released = true
33
29
  end
34
30
 
35
- # Logged after the transaction commits and hold.lock! releases its row lock — never while
36
- # held — so a slow log write can't extend how long other requests wait on this hold.
37
- log_redis_release_mutation(hold: hold, correlation_id: correlation_id, rows: redis_rows) if redis_rows
38
-
39
31
  # Notify AFTER the transaction commits (deliver_later must not be enqueued inside an open
40
32
  # transaction), and only when THIS invocation performed the release — the `next` guards above
41
33
  # commit without releasing, so this keeps notifications idempotent across stale/concurrent jobs.
@@ -63,10 +55,8 @@ module SpreeCmCommissioner
63
55
  SpreeCmCommissioner::ReleaseInventoryItemNotificationSender.call(notificable: hold.order)
64
56
  end
65
57
 
66
- # Pure Redis I/O — no logging in here (that happens after the caller's transaction closes).
67
- # Returns the rows the mutation log needs, or nil when there was nothing to release.
68
58
  def restock_redis!(hold)
69
- keys, quantities, warmable_items, locked_flags = hold_keys_and_quantities(hold.order)
59
+ keys, quantities, warmable_items = hold_keys_and_quantities(hold.order)
70
60
  return if keys.empty?
71
61
 
72
62
  release_hold_lua_script = <<~LUA
@@ -78,46 +68,16 @@ module SpreeCmCommissioner
78
68
  return 1
79
69
  LUA
80
70
 
81
- # Captured (not discarded) — the warm already reads the hold counter this eval is about to
82
- # decrement, so the mutation log (logged later, after the transaction) needs no extra Redis
83
- # round trip.
84
- cached_items = SpreeCmCommissioner::RedisStock::CachedInventoryItemsBuilder.new(warmable_items).call
71
+ SpreeCmCommissioner::RedisStock::CachedInventoryItemsBuilder.new(warmable_items).call
85
72
  SpreeCmCommissioner.inventory_redis_pool.with do |redis|
86
73
  redis.eval(release_hold_lua_script, keys: keys, argv: quantities)
87
74
  end
88
-
89
- cached_items.each_with_index.map { |cached_item, i| [cached_item, quantities[i], locked_flags[i]] }
90
- end
91
-
92
- # This script always applies (no insufficiency check — releasing a hold can't oversell), so
93
- # every item here has a real "after". `rows` is [cached_item, quantity_delta, locked?] per
94
- # inventory item, in eval order.
95
- def log_redis_release_mutation(hold:, correlation_id:, rows:)
96
- CmAppLogger.log(
97
- label: "#{self.class.name}#restock_redis!",
98
- data: {
99
- correlation_id: correlation_id,
100
- order_id: hold.order.id,
101
- hold_id: hold.id,
102
- items: rows.map do |cached_item, delta, locked|
103
- before = locked ? cached_item.quantity_locked_on_hold : cached_item.quantity_on_hold
104
- {
105
- inventory_item_id: cached_item.inventory_item_id,
106
- locked: locked,
107
- delta: delta,
108
- quantity_on_hold_before: before,
109
- quantity_on_hold_after: before - delta
110
- }
111
- end
112
- }
113
- )
114
75
  end
115
76
 
116
77
  def hold_keys_and_quantities(order)
117
78
  keys = []
118
79
  quantities = []
119
80
  warmable_items = []
120
- locked_flags = []
121
81
 
122
82
  order.line_items.includes(:variant).find_each do |li|
123
83
  next unless li.should_hold_inventory?
@@ -131,14 +91,13 @@ module SpreeCmCommissioner
131
91
  keys << item.redis_hold_key(locked: locked)
132
92
  quantities << li.quantity
133
93
  warmable_items << item
134
- locked_flags << locked
135
94
  end
136
95
  end
137
96
 
138
- [keys, quantities, warmable_items, locked_flags]
97
+ [keys, quantities, warmable_items]
139
98
  end
140
99
 
141
- def enqueue_sync_inventory_on_hold(order, correlation_id)
100
+ def enqueue_sync_inventory_on_hold(order)
142
101
  inventory_id_and_quantities = order.line_items.flat_map do |line_item|
143
102
  next [] unless line_item.should_hold_inventory?
144
103
 
@@ -157,7 +116,6 @@ module SpreeCmCommissioner
157
116
  SpreeCmCommissioner::InventoryItems::BulkAdjustQuantitiesOnHoldJob.perform_later(
158
117
  order_id: order.id,
159
118
  caller_source: "#{self.class.name}#enqueue_sync_inventory_on_hold",
160
- correlation_id: correlation_id,
161
119
  inventory_id_and_quantities: inventory_id_and_quantities
162
120
  )
163
121
  end
@@ -37,14 +37,14 @@ module SpreeCmCommissioner
37
37
  # thing RedisStock::Unstock and InventoryHolds::Convert do before their own Lua scripts.
38
38
  warm_redis_key!(inventory_item) if warm_redis
39
39
 
40
- before = after = redis_after = nil
40
+ before = after = nil
41
41
 
42
42
  apply = lambda do
43
43
  before = inventory_item.quantity_available
44
44
  after = before + delta
45
45
 
46
46
  inventory_item.update!(quantity_available: after)
47
- redis_after = inventory_item.adjust_quantity_in_redis(delta)
47
+ inventory_item.adjust_quantity_in_redis(delta)
48
48
  end
49
49
 
50
50
  lock ? inventory_item.with_lock(&apply) : apply.call
@@ -59,8 +59,7 @@ module SpreeCmCommissioner
59
59
  inventory_item_id: inventory_item.id,
60
60
  quantity_available_before: before,
61
61
  delta: delta,
62
- quantity_available_after: after,
63
- redis_quantity_after: redis_after
62
+ quantity_available_after: after
64
63
  }
65
64
  )
66
65
 
@@ -4,10 +4,10 @@ module SpreeCmCommissioner
4
4
  prepend ::Spree::ServiceModule::Base
5
5
  extend SpreeCmCommissioner::ServiceModuleThrowable
6
6
 
7
- def call(inventory_id_and_quantities:, caller_source: nil, correlation_id: nil)
7
+ def call(inventory_id_and_quantities:, caller_source: nil)
8
8
  CmAppLogger.log(
9
9
  label: "#{self.class.name}#call started",
10
- data: { correlation_id: correlation_id, caller_source: caller_source, inventory_id_and_quantities: inventory_id_and_quantities }
10
+ data: { caller_source: caller_source, inventory_id_and_quantities: inventory_id_and_quantities }
11
11
  )
12
12
 
13
13
  ActiveRecord::Base.transaction do
@@ -17,7 +17,7 @@ module SpreeCmCommissioner
17
17
  inventory_item = items_by_id[inventory_id]
18
18
  next if inventory_item.nil?
19
19
 
20
- adjust_quantity_available(inventory_item, quantity, caller_source, correlation_id, locked: locked)
20
+ adjust_quantity_available(inventory_item, quantity, caller_source, locked: locked)
21
21
  end
22
22
  end
23
23
 
@@ -38,7 +38,7 @@ module SpreeCmCommissioner
38
38
  end
39
39
 
40
40
  # locked defaults to false so the original three-argument signature keeps working.
41
- def adjust_quantity_available(inventory_item, quantity, caller_source, correlation_id, locked: false)
41
+ def adjust_quantity_available(inventory_item, quantity, caller_source, locked: false)
42
42
  column = locked ? :quantity_locked : :quantity_available
43
43
 
44
44
  # IMPORTANT: Apply the quantity change directly without defensive clamping.
@@ -63,7 +63,6 @@ module SpreeCmCommissioner
63
63
  CmAppLogger.log(
64
64
  label: "#{self.class.name}#adjust_quantity_available",
65
65
  data: {
66
- correlation_id: correlation_id,
67
66
  caller_source: caller_source,
68
67
  inventory_item_id: inventory_item.id,
69
68
  locked: locked,
@@ -77,7 +76,6 @@ module SpreeCmCommissioner
77
76
  CmAppLogger.error(
78
77
  label: "#{self.class.name}#adjust_quantity_available failed",
79
78
  data: {
80
- correlation_id: correlation_id,
81
79
  caller_source: caller_source,
82
80
  inventory_item_id: inventory_item.id,
83
81
  locked: locked,
@@ -5,18 +5,8 @@ module SpreeCmCommissioner
5
5
  extend SpreeCmCommissioner::ServiceModuleThrowable
6
6
 
7
7
  def call(variant:, quantity:)
8
- # One id per batch — every inventory item touched below logs it via
9
- # InventoryItem#adjust_quantity!, tying this whole variant-wide adjustment together in a
10
- # search, same as the other Bulk* services' correlation_id.
11
- correlation_id = SecureRandom.uuid
12
-
13
- CmAppLogger.log(
14
- label: "#{self.class.name}#call started",
15
- data: { correlation_id: correlation_id, variant_id: variant.id, delta: quantity }
16
- )
17
-
18
8
  variant.inventory_items.active.find_each do |inventory_item|
19
- inventory_item.adjust_quantity!(quantity, correlation_id: correlation_id)
9
+ inventory_item.adjust_quantity!(quantity)
20
10
  end
21
11
 
22
12
  success(nil)
@@ -10,10 +10,10 @@ module SpreeCmCommissioner
10
10
  prepend ::Spree::ServiceModule::Base
11
11
  extend SpreeCmCommissioner::ServiceModuleThrowable
12
12
 
13
- def call(inventory_id_and_quantities:, caller_source: nil, correlation_id: nil)
13
+ def call(inventory_id_and_quantities:, caller_source: nil)
14
14
  CmAppLogger.log(
15
15
  label: "#{self.class.name}#call started",
16
- data: { correlation_id: correlation_id, caller_source: caller_source, inventory_id_and_quantities: inventory_id_and_quantities }
16
+ data: { caller_source: caller_source, inventory_id_and_quantities: inventory_id_and_quantities }
17
17
  )
18
18
 
19
19
  ActiveRecord::Base.transaction do
@@ -23,7 +23,7 @@ module SpreeCmCommissioner
23
23
  inventory_item = items_by_id[inventory_id]
24
24
  next if inventory_item.nil?
25
25
 
26
- adjust_quantities(inventory_item, quantity, caller_source, correlation_id, locked: locked)
26
+ adjust_quantities(inventory_item, quantity, caller_source, locked: locked)
27
27
  end
28
28
  end
29
29
 
@@ -40,7 +40,7 @@ module SpreeCmCommissioner
40
40
  .transform_values { |entries| entries.sum { |entry| entry[:quantity] } }
41
41
  end
42
42
 
43
- def adjust_quantities(inventory_item, quantity, caller_source, correlation_id, locked: false)
43
+ def adjust_quantities(inventory_item, quantity, caller_source, locked: false)
44
44
  available_column = locked ? :quantity_locked : :quantity_available
45
45
  hold_column = locked ? :quantity_locked_on_hold : :quantity_on_hold
46
46
 
@@ -60,7 +60,6 @@ module SpreeCmCommissioner
60
60
  CmAppLogger.log(
61
61
  label: "#{self.class.name}#adjust_quantities",
62
62
  data: {
63
- correlation_id: correlation_id,
64
63
  caller_source: caller_source,
65
64
  inventory_item_id: inventory_item.id,
66
65
  locked: locked,
@@ -77,7 +76,6 @@ module SpreeCmCommissioner
77
76
  CmAppLogger.error(
78
77
  label: "#{self.class.name}#adjust_quantities failed",
79
78
  data: {
80
- correlation_id: correlation_id,
81
79
  caller_source: caller_source,
82
80
  inventory_item_id: inventory_item.id,
83
81
  locked: locked,
@@ -4,11 +4,10 @@ module SpreeCmCommissioner
4
4
  prepend ::Spree::ServiceModule::Base
5
5
  extend SpreeCmCommissioner::ServiceModuleThrowable
6
6
 
7
- def call(inventory_id_and_quantities:, caller_source: nil, correlation_id: nil)
7
+ def call(inventory_id_and_quantities:, caller_source: nil)
8
8
  CmAppLogger.log(
9
9
  label: "#{self.class.name}#call started",
10
10
  data: {
11
- correlation_id: correlation_id,
12
11
  caller_source: caller_source,
13
12
  inventory_id_and_quantities: inventory_id_and_quantities
14
13
  }
@@ -21,7 +20,7 @@ module SpreeCmCommissioner
21
20
  inventory_item = items_by_id[inventory_id]
22
21
  next if inventory_item.nil?
23
22
 
24
- adjust_quantity_on_hold(inventory_item, quantity, caller_source, correlation_id, locked: locked)
23
+ adjust_quantity_on_hold(inventory_item, quantity, caller_source, locked: locked)
25
24
  end
26
25
  end
27
26
 
@@ -45,7 +44,7 @@ module SpreeCmCommissioner
45
44
  end
46
45
 
47
46
  # locked defaults to false so the original three-argument signature keeps working.
48
- def adjust_quantity_on_hold(inventory_item, quantity, caller_source, correlation_id, locked: false)
47
+ def adjust_quantity_on_hold(inventory_item, quantity, caller_source, locked: false)
49
48
  column = locked ? :quantity_locked_on_hold : :quantity_on_hold
50
49
 
51
50
  # IMPORTANT: Apply the quantity change directly without defensive clamping.
@@ -70,7 +69,6 @@ module SpreeCmCommissioner
70
69
  CmAppLogger.log(
71
70
  label: "#{self.class.name}#adjust_quantity_on_hold",
72
71
  data: {
73
- correlation_id: correlation_id,
74
72
  caller_source: caller_source,
75
73
  inventory_item_id: inventory_item.id,
76
74
  locked: locked,
@@ -84,7 +82,6 @@ module SpreeCmCommissioner
84
82
  CmAppLogger.error(
85
83
  label: "#{self.class.name}#adjust_quantity_on_hold failed",
86
84
  data: {
87
- correlation_id: correlation_id,
88
85
  caller_source: caller_source,
89
86
  inventory_item_id: inventory_item.id,
90
87
  locked: locked,
@@ -25,54 +25,39 @@ module SpreeCmCommissioner
25
25
  # key could not be warmed and the adjustment would clamp the date to 0 for a year.
26
26
  return failure(nil, :inactive_inventory_item) unless inventory_item.active?
27
27
 
28
- # One id per transfer, tying this call's Redis-mutation log to its DB-value logs below —
29
- # there's no async job on this path (row and Redis are written together), but a shared id
30
- # still makes the two log lines trivially joinable in a search.
31
- correlation_id = SecureRandom.uuid
32
28
  outcome = nil
33
- redis_mutation = nil
34
29
 
35
30
  # requires_new, not a bare with_lock: an ActiveRecord::Rollback raised in a nested transaction
36
31
  # is swallowed without rolling anything back, so a refused transfer would leave the row already
37
32
  # moved. The savepoint makes the rollback real wherever this is called from.
38
33
  inventory_item.transaction(requires_new: true) do
39
34
  inventory_item.lock!
40
- outcome, redis_mutation = transfer!(inventory_item, quantity, caller_source, user)
35
+ outcome = transfer!(inventory_item, quantity, caller_source, user)
41
36
  raise ActiveRecord::Rollback unless outcome == :moved
42
37
  end
43
38
 
44
- # Logged after the transaction commits/rolls back and inventory_item.lock! releases its row
45
- # lock — never while held — so a slow log write can't extend how long other requests wait on
46
- # this item.
47
- log_redis_move_mutation(inventory_item, quantity, redis_mutation, correlation_id) if redis_mutation
48
-
49
39
  return failure(nil, outcome) unless outcome == :moved
50
40
 
51
- log_adjustment(inventory_item, quantity, caller_source, correlation_id)
52
- log(inventory_item, quantity, outcome, correlation_id)
41
+ log(inventory_item, quantity, outcome)
53
42
  success(inventory_item)
54
43
  end
55
44
 
56
45
  private
57
46
 
58
- # Runs inside the locked transaction. Returns [outcome, redis_mutation] — redis_mutation is
59
- # {cached_item:, applied:} once move_stock_in_redis has actually run, else nil. No logging in
60
- # here (that happens in `call`, after the transaction closes).
47
+ # Runs inside the locked transaction. Returns the outcome symbol.
61
48
  def transfer!(inventory_item, quantity, caller_source, user)
62
49
  new_locked = inventory_item.quantity_locked + quantity
63
- return [:quantity_exceeds_lock, nil] if new_locked.negative?
50
+ return :quantity_exceeds_lock if new_locked.negative?
64
51
 
65
52
  # Neither column may go negative. The Redis script below is the authoritative check because it
66
53
  # is the only one that sees holds, but it runs after the row write — so bound the row here or
67
54
  # the update raises RecordInvalid instead of returning a named failure the admin can read.
68
55
  new_available = inventory_item.quantity_available - quantity
69
- return [insufficient_stock_reason(quantity), nil] if new_available.negative?
56
+ return insufficient_stock_reason(quantity) if new_available.negative?
70
57
 
71
58
  # SET-NX both pools from the row before anything is written, so the script below never reads a
72
- # cold key as 0 and moves units that were never there. Captured (not discarded) — this is
73
- # exactly the pre-mutation Redis snapshot the mutation log (logged later, after the
74
- # transaction) needs, no extra round trip.
75
- cached_item = warm_pools(inventory_item)
59
+ # cold key as 0 and moves units that were never there.
60
+ warm_pools(inventory_item)
76
61
 
77
62
  # ORDER MATTERS, and not for style. Redis has no rollback, and InventoryItems::Reset cannot
78
63
  # recompute quantity_locked — there are no rows to derive it from — so a Redis write that
@@ -80,42 +65,14 @@ module SpreeCmCommissioner
80
65
  # it back if the script refuses.
81
66
  inventory_item.update!(quantity_locked: new_locked, quantity_available: new_available)
82
67
 
83
- applied = move_stock_in_redis(inventory_item, quantity)
84
- redis_mutation = { cached_item: cached_item, applied: applied }
85
- return [insufficient_stock_reason(quantity), redis_mutation] unless applied
68
+ return insufficient_stock_reason(quantity) unless move_stock_in_redis(inventory_item, quantity)
86
69
 
87
70
  # Inside the same savepoint as the row update above, so a later failure in this same call would
88
71
  # roll this row back with it — the ledger can never disagree with quantity_locked.
89
72
  create_movement!(inventory_item, quantity, caller_source, user)
73
+ log_adjustment(inventory_item, quantity, caller_source)
90
74
 
91
- [:moved, redis_mutation]
92
- end
93
-
94
- # `applied: false` means the script refused (would drain a pool below what's held) — nothing
95
- # was written, so there is no "after" for either pool.
96
- def log_redis_move_mutation(inventory_item, quantity, redis_mutation, correlation_id)
97
- cached_item = redis_mutation[:cached_item]
98
- applied = redis_mutation[:applied]
99
- locking = quantity.positive?
100
- delta = quantity.abs
101
- from_before = locking ? cached_item.quantity_available : cached_item.quantity_locked
102
- to_before = locking ? cached_item.quantity_locked : cached_item.quantity_available
103
-
104
- CmAppLogger.log(
105
- label: "#{self.class.name}#move_stock_in_redis",
106
- data: {
107
- correlation_id: correlation_id,
108
- inventory_item_id: inventory_item.id,
109
- applied: applied,
110
- from_pool: locking ? 'public' : 'locked',
111
- to_pool: locking ? 'locked' : 'public',
112
- delta: delta,
113
- from_before: from_before,
114
- from_after: applied ? from_before - delta : from_before,
115
- to_before: to_before,
116
- to_after: applied ? to_before + delta : to_before
117
- }
118
- )
75
+ :moved
119
76
  end
120
77
 
121
78
  # The one durable record of this transfer — CmAppLogger below is a log line, not a queryable row.
@@ -185,11 +142,10 @@ module SpreeCmCommissioner
185
142
  # AdjustAvailableQuantity used to write quantity_available and carry this log line. The script
186
143
  # above owns both stock keys now, so the row update is folded into transfer! — but caller_source
187
144
  # is threaded from the admin controller specifically so the audit trail names the action.
188
- def log_adjustment(inventory_item, quantity, caller_source, correlation_id)
145
+ def log_adjustment(inventory_item, quantity, caller_source)
189
146
  CmAppLogger.log(
190
147
  label: "#{self.class.name}#transfer!",
191
148
  data: {
192
- correlation_id: correlation_id,
193
149
  caller_source: caller_source || "#{self.class.name}#transfer!",
194
150
  inventory_item_id: inventory_item.id,
195
151
  quantity_available: inventory_item.quantity_available,
@@ -199,11 +155,10 @@ module SpreeCmCommissioner
199
155
  )
200
156
  end
201
157
 
202
- def log(inventory_item, quantity, outcome, correlation_id)
158
+ def log(inventory_item, quantity, outcome)
203
159
  CmAppLogger.log(
204
160
  label: "#{self.class.name}#call",
205
161
  data: {
206
- correlation_id: correlation_id,
207
162
  inventory_item_id: inventory_item.id,
208
163
  quantity: quantity,
209
164
  quantity_available: inventory_item.quantity_available,
@@ -28,12 +28,11 @@ module SpreeCmCommissioner
28
28
  SpreeCmCommissioner::RedisStock::CachedInventoryItemsBuilder.new(inventory_items).call
29
29
  end
30
30
 
31
- def schedule_sync_inventory(inventory_id_and_quantities, correlation_id:)
31
+ def schedule_sync_inventory(inventory_id_and_quantities)
32
32
  SpreeCmCommissioner::InventoryItems::BulkAdjustQuantitiesJob.perform_later(
33
33
  inventory_id_and_quantities: inventory_id_and_quantities,
34
34
  line_item_ids: @line_item_ids,
35
- caller_source: "#{self.class.name}#schedule_sync_inventory",
36
- correlation_id: correlation_id
35
+ caller_source: "#{self.class.name}#schedule_sync_inventory"
37
36
  )
38
37
  end
39
38
  end
@@ -4,7 +4,6 @@ module SpreeCmCommissioner
4
4
  # override
5
5
  def call(line_item_ids:)
6
6
  @line_item_ids = line_item_ids
7
- correlation_id = SecureRandom.uuid
8
7
 
9
8
  keys = []
10
9
  quantities = []
@@ -29,51 +28,20 @@ module SpreeCmCommissioner
29
28
  end
30
29
  end
31
30
 
32
- # Captured (not discarded) — the warm already reads the stock counter this eval is about to
33
- # increment, so the mutation log below needs no extra Redis round trip.
34
- cached_items = warm_redis_keys!(warmable_items)
31
+ warm_redis_keys!(warmable_items)
35
32
 
36
- applied = restock(keys, quantities, max_capacities)
37
- log_redis_restock_mutation(correlation_id: correlation_id, cached_items: cached_items, quantities: quantities, locked_flags: locked_flags,
38
- applied: applied
39
- )
40
- return failure(nil, :unable_to_restock) unless applied
33
+ return failure(nil, :unable_to_restock) unless restock(keys, quantities, max_capacities)
41
34
 
42
35
  inventory_id_and_quantities = inventory_ids.map.with_index do |inventory_id, i|
43
36
  { inventory_id: inventory_id, quantity: quantities[i], locked: locked_flags[i] }
44
37
  end
45
38
 
46
- schedule_sync_inventory(inventory_id_and_quantities, correlation_id: correlation_id)
39
+ schedule_sync_inventory(inventory_id_and_quantities)
47
40
  success(nil)
48
41
  end
49
42
 
50
43
  private
51
44
 
52
- # `applied: false` means the eval rejected the whole batch (would exceed max_capacity) —
53
- # nothing was written, so there is no "after" for those items.
54
- def log_redis_restock_mutation(correlation_id:, cached_items:, quantities:, locked_flags:, applied:)
55
- CmAppLogger.log(
56
- label: "#{self.class.name}#restock",
57
- data: {
58
- correlation_id: correlation_id,
59
- line_item_ids: @line_item_ids,
60
- applied: applied,
61
- items: cached_items.each_with_index.map do |cached_item, i|
62
- locked = locked_flags[i]
63
- before = locked ? cached_item.quantity_locked : cached_item.quantity_available
64
- delta = quantities[i]
65
- {
66
- inventory_item_id: cached_item.inventory_item_id,
67
- locked: locked,
68
- quantity_available_before: before,
69
- delta: delta,
70
- quantity_available_after: applied ? before + delta : before
71
- }
72
- end
73
- }
74
- )
75
- end
76
-
77
45
  # max_capacity is the ceiling for both pools. For :locked it is loose rather than exact — the
78
46
  # true ceiling would be the size of the lock, which is not tracked — but it is always an upper
79
47
  # bound on what can sit in the locked pool, so it never rejects a legitimate restock. Going
@@ -4,7 +4,6 @@ module SpreeCmCommissioner
4
4
  # override
5
5
  def call(line_item_ids:)
6
6
  @line_item_ids = line_item_ids
7
- correlation_id = SecureRandom.uuid
8
7
 
9
8
  keys = []
10
9
  quantities = []
@@ -31,51 +30,20 @@ module SpreeCmCommissioner
31
30
  end
32
31
  end
33
32
 
34
- # Captured (not discarded) — the warm already reads the stock counter this eval is about to
35
- # decrement, so the mutation log below needs no extra Redis round trip.
36
- cached_items = warm_redis_keys!(warmable_items)
33
+ warm_redis_keys!(warmable_items)
37
34
 
38
- applied = unstock(keys, quantities)
39
- log_redis_unstock_mutation(correlation_id: correlation_id, cached_items: cached_items, quantities: quantities, locked_flags: locked_flags,
40
- applied: applied
41
- )
42
- return failure(nil, :unable_to_unstock) unless applied
35
+ return failure(nil, :unable_to_unstock) unless unstock(keys, quantities)
43
36
 
44
37
  inventory_id_and_quantities = inventory_ids.map.with_index do |inventory_id, i|
45
38
  { inventory_id: inventory_id, quantity: -quantities[i], locked: locked_flags[i] }
46
39
  end
47
40
 
48
- schedule_sync_inventory(inventory_id_and_quantities, correlation_id: correlation_id)
41
+ schedule_sync_inventory(inventory_id_and_quantities)
49
42
  success(nil)
50
43
  end
51
44
 
52
45
  private
53
46
 
54
- # `applied: false` means the eval rejected the whole batch (would go negative) — nothing was
55
- # written, so there is no "after" for those items.
56
- def log_redis_unstock_mutation(correlation_id:, cached_items:, quantities:, locked_flags:, applied:)
57
- CmAppLogger.log(
58
- label: "#{self.class.name}#unstock",
59
- data: {
60
- correlation_id: correlation_id,
61
- line_item_ids: @line_item_ids,
62
- applied: applied,
63
- items: cached_items.each_with_index.map do |cached_item, i|
64
- locked = locked_flags[i]
65
- before = locked ? cached_item.quantity_locked : cached_item.quantity_available
66
- delta = quantities[i]
67
- {
68
- inventory_item_id: cached_item.inventory_item_id,
69
- locked: locked,
70
- quantity_available_before: before,
71
- delta: delta,
72
- quantity_available_after: applied ? before - delta : before
73
- }
74
- end
75
- }
76
- )
77
- end
78
-
79
47
  def unstock(keys, quantities)
80
48
  SpreeCmCommissioner.inventory_redis_pool.with do |redis|
81
49
  redis.eval(unstock_redis_script, keys: keys, argv: quantities)
@@ -1006,8 +1006,6 @@ en:
1006
1006
  exceeded_active_holds: "You've reached the limit of reservations you can hold at once. Please complete or cancel one before reserving more."
1007
1007
  ip_rate_limit: "Too many reservation attempts from your network in a short time. Please wait a while and try again."
1008
1008
  hold_cooldown: "Your previous reservation just expired. Please wait a couple of minutes before reserving again."
1009
- convert:
1010
- hold_expired: "Your reservation expired before payment completed. Please try booking again."
1011
1009
 
1012
1010
  # Displayed as-is on stock-status badges pushed to Firestore (see
1013
1011
  # SpreeCmCommissioner::Products::PublishStockStatusToFirestore) — not read via I18n at request
@@ -605,8 +605,6 @@ km:
605
605
  exceeded_active_holds: "អ្នកកក់ដល់ចំនួនកំណត់អតិបរមាក្នុងពេលតែមួយហើយ។ សូមបញ្ចប់ ឬលុបការកក់មួយ មុនពេលកក់បន្ថែម។"
606
606
  ip_rate_limit: "មានការព្យាយាមកក់ច្រើនពេកពីបណ្តាញរបស់អ្នកក្នុងរយៈពេលខ្លី។ សូមរង់ចាំបន្តិច រួចព្យាយាមម្តងទៀត។"
607
607
  hold_cooldown: "ការកក់មុនរបស់អ្នកទើបតែផុតកំណត់។ សូមរង់ចាំពីរបីនាទី មុនពេលកក់ម្តងទៀត។"
608
- convert:
609
- hold_expired: "ការកក់របស់អ្នកបានផុតកំណត់មុនពេលទូទាត់ប្រាក់បានបញ្ចប់។ សូមព្យាយាមកក់ម្តងទៀត។"
610
608
 
611
609
  stock_status:
612
610
  pending_hold: "តម្រូវការខ្ពស់"
@@ -1,5 +1,5 @@
1
1
  module SpreeCmCommissioner
2
- VERSION = '2.12.7-pre1'.freeze
2
+ VERSION = '2.12.8'.freeze
3
3
 
4
4
  module_function
5
5
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: spree_cm_commissioner
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.12.7.pre.pre1
4
+ version: 2.12.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - You
@@ -3781,9 +3781,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
3781
3781
  version: '2.7'
3782
3782
  required_rubygems_version: !ruby/object:Gem::Requirement
3783
3783
  requirements:
3784
- - - ">"
3784
+ - - ">="
3785
3785
  - !ruby/object:Gem::Version
3786
- version: 1.3.1
3786
+ version: '0'
3787
3787
  requirements:
3788
3788
  - none
3789
3789
  rubygems_version: 3.4.1