ruby_reactor 0.3.1 → 0.3.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 +4 -4
- data/README.md +114 -5
- data/documentation/README.md +20 -8
- data/documentation/async_reactors.md +46 -34
- data/documentation/core_concepts.md +75 -61
- data/documentation/examples/inventory_management.md +2 -3
- data/documentation/examples/order_processing.md +92 -77
- data/documentation/examples/payment_processing.md +28 -117
- data/documentation/getting_started.md +112 -94
- data/documentation/interrupts.md +9 -7
- data/documentation/locks_and_semaphores.md +459 -0
- data/documentation/retry_configuration.md +19 -14
- data/documentation/testing.md +182 -0
- data/lib/ruby_reactor/configuration.rb +18 -1
- data/lib/ruby_reactor/dsl/lockable.rb +130 -0
- data/lib/ruby_reactor/executor/result_handler.rb +19 -0
- data/lib/ruby_reactor/executor/step_executor.rb +5 -0
- data/lib/ruby_reactor/executor.rb +145 -2
- data/lib/ruby_reactor/lock.rb +92 -0
- data/lib/ruby_reactor/period.rb +67 -0
- data/lib/ruby_reactor/rate_limit.rb +74 -0
- data/lib/ruby_reactor/reactor.rb +1 -0
- data/lib/ruby_reactor/rspec/matchers.rb +171 -4
- data/lib/ruby_reactor/semaphore.rb +58 -0
- data/lib/ruby_reactor/sidekiq_workers/worker.rb +70 -8
- data/lib/ruby_reactor/storage/redis_adapter.rb +2 -0
- data/lib/ruby_reactor/storage/redis_locking.rb +251 -0
- data/lib/ruby_reactor/version.rb +1 -1
- data/lib/ruby_reactor.rb +49 -0
- metadata +9 -2
|
@@ -198,7 +198,6 @@ class InventoryManagementReactor < RubyReactor::Reactor
|
|
|
198
198
|
argument :request_data, result(:validate_request)
|
|
199
199
|
argument :replenishment_data, result(:check_replenishment)
|
|
200
200
|
|
|
201
|
-
idempotent true
|
|
202
201
|
retries max_attempts: 3, backoff: :linear, base_delay: 5.seconds
|
|
203
202
|
|
|
204
203
|
run do |args, _context|
|
|
@@ -283,7 +282,7 @@ class BulkInventoryReactor < RubyReactor::Reactor
|
|
|
283
282
|
if result.success?
|
|
284
283
|
results << { success: true, operation: op }
|
|
285
284
|
else
|
|
286
|
-
results << { success: false, operation: op, error: result.error.
|
|
285
|
+
results << { success: false, operation: op, error: result.error.to_s }
|
|
287
286
|
end
|
|
288
287
|
rescue => e
|
|
289
288
|
results << { success: false, operation: op, error: e.message }
|
|
@@ -629,7 +628,7 @@ RSpec.describe InventoryManagementReactor do
|
|
|
629
628
|
)
|
|
630
629
|
|
|
631
630
|
expect(result).to be_failure
|
|
632
|
-
expect(result.error
|
|
631
|
+
expect(result.error).to include("Insufficient stock")
|
|
633
632
|
end
|
|
634
633
|
end
|
|
635
634
|
|
|
@@ -46,28 +46,30 @@ class OrderProcessingReactor < RubyReactor::Reactor
|
|
|
46
46
|
# Reactor-level retry defaults
|
|
47
47
|
retry_defaults max_attempts: 3, backoff: :exponential, base_delay: 2.seconds
|
|
48
48
|
|
|
49
|
+
input :order_id do
|
|
50
|
+
required(:order_id).filled(:string)
|
|
51
|
+
end
|
|
52
|
+
|
|
49
53
|
step :validate_order do
|
|
50
|
-
|
|
51
|
-
required(:order_id).filled(:string)
|
|
52
|
-
end
|
|
54
|
+
argument :order_id, input(:order_id)
|
|
53
55
|
|
|
54
|
-
run do |
|
|
55
|
-
order = Order.find_by(id: order_id)
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
run do |args, _ctx|
|
|
57
|
+
order = Order.find_by(id: args[:order_id])
|
|
58
|
+
return Failure("Order not found") unless order
|
|
59
|
+
return Failure("Order already processed") if order.processed?
|
|
60
|
+
return Failure("Order cancelled") if order.cancelled?
|
|
59
61
|
|
|
60
62
|
Success({ order: order })
|
|
61
63
|
end
|
|
62
64
|
end
|
|
63
65
|
|
|
64
66
|
step :check_inventory do
|
|
65
|
-
argument :order, result(:validate_order)
|
|
67
|
+
argument :order, result(:validate_order, :order)
|
|
66
68
|
|
|
67
|
-
run do |
|
|
69
|
+
run do |args, _ctx|
|
|
68
70
|
unavailable_items = []
|
|
69
71
|
|
|
70
|
-
order.items.each do |item|
|
|
72
|
+
args[:order].items.each do |item|
|
|
71
73
|
product = Product.find(item.product_id)
|
|
72
74
|
if product.inventory_count < item.quantity
|
|
73
75
|
unavailable_items << {
|
|
@@ -78,35 +80,37 @@ class OrderProcessingReactor < RubyReactor::Reactor
|
|
|
78
80
|
end
|
|
79
81
|
end
|
|
80
82
|
|
|
81
|
-
|
|
83
|
+
return Failure("Insufficient inventory: #{unavailable_items}") unless unavailable_items.empty?
|
|
82
84
|
|
|
83
85
|
Success({ inventory_checked: true })
|
|
84
86
|
end
|
|
85
87
|
end
|
|
86
88
|
|
|
87
89
|
step :reserve_inventory do
|
|
88
|
-
argument :order, result(:validate_order)
|
|
90
|
+
argument :order, result(:validate_order, :order)
|
|
89
91
|
|
|
90
|
-
run do |
|
|
91
|
-
reservation_id = InventoryService.reserve_items(order.items)
|
|
92
|
-
|
|
92
|
+
run do |args, _ctx|
|
|
93
|
+
reservation_id = InventoryService.reserve_items(args[:order].items)
|
|
94
|
+
return Failure("Inventory reservation failed") unless reservation_id
|
|
93
95
|
|
|
94
96
|
Success({ reservation_id: reservation_id })
|
|
95
97
|
end
|
|
96
98
|
|
|
97
|
-
undo do |
|
|
98
|
-
# Release reservation
|
|
99
|
-
InventoryService.release_reservation(reservation_id) if reservation_id
|
|
99
|
+
undo do |result, _args, _ctx|
|
|
100
|
+
# Release reservation when a later step fails
|
|
101
|
+
InventoryService.release_reservation(result[:reservation_id]) if result[:reservation_id]
|
|
102
|
+
Success()
|
|
100
103
|
end
|
|
101
104
|
end
|
|
102
105
|
|
|
103
106
|
step :process_payment do
|
|
104
|
-
argument :order, result(:validate_order)
|
|
107
|
+
argument :order, result(:validate_order, :order)
|
|
105
108
|
|
|
106
109
|
# Payment processing needs careful retry handling
|
|
107
110
|
retries max_attempts: 2, backoff: :fixed, base_delay: 30.seconds
|
|
108
111
|
|
|
109
|
-
run do |
|
|
112
|
+
run do |args, _ctx|
|
|
113
|
+
order = args[:order]
|
|
110
114
|
payment_result = PaymentService.charge(
|
|
111
115
|
amount: order.total,
|
|
112
116
|
currency: order.currency,
|
|
@@ -114,44 +118,42 @@ class OrderProcessingReactor < RubyReactor::Reactor
|
|
|
114
118
|
description: "Order ##{order.id}"
|
|
115
119
|
)
|
|
116
120
|
|
|
117
|
-
|
|
121
|
+
return Failure("Payment failed: #{payment_result.error}") unless payment_result.success?
|
|
118
122
|
|
|
119
123
|
Success({ payment_id: payment_result.id, payment_amount: order.total })
|
|
120
124
|
end
|
|
121
125
|
|
|
122
|
-
undo do |
|
|
123
|
-
|
|
124
|
-
|
|
126
|
+
undo do |result, _args, _ctx|
|
|
127
|
+
PaymentService.refund(result[:payment_id]) if result[:payment_id]
|
|
128
|
+
Success()
|
|
125
129
|
end
|
|
126
130
|
end
|
|
127
131
|
|
|
128
132
|
step :update_inventory do
|
|
129
|
-
argument :order, result(:validate_order)
|
|
130
|
-
argument :reservation_id, result(:reserve_inventory)
|
|
133
|
+
argument :order, result(:validate_order, :order)
|
|
134
|
+
argument :reservation_id, result(:reserve_inventory, :reservation_id)
|
|
131
135
|
|
|
132
|
-
run do |
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
raise "Inventory update failed" unless success
|
|
136
|
+
run do |args, _ctx|
|
|
137
|
+
success = InventoryService.confirm_reservation(args[:reservation_id])
|
|
138
|
+
return Failure("Inventory update failed") unless success
|
|
136
139
|
|
|
137
140
|
Success({ inventory_updated: true })
|
|
138
141
|
end
|
|
139
142
|
|
|
140
|
-
undo do |
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
InventoryService.restore_from_reservation(reservation_id) if reservation_id
|
|
143
|
+
undo do |_result, args, _ctx|
|
|
144
|
+
InventoryService.restore_from_reservation(args[:reservation_id]) if args[:reservation_id]
|
|
145
|
+
Success()
|
|
144
146
|
end
|
|
145
147
|
end
|
|
146
148
|
|
|
147
149
|
step :update_order_status do
|
|
148
|
-
argument :order, result(:validate_order)
|
|
149
|
-
argument :payment_id, result(:process_payment)
|
|
150
|
+
argument :order, result(:validate_order, :order)
|
|
151
|
+
argument :payment_id, result(:process_payment, :payment_id)
|
|
150
152
|
|
|
151
|
-
run do |
|
|
152
|
-
order.update!(
|
|
153
|
+
run do |args, _ctx|
|
|
154
|
+
args[:order].update!(
|
|
153
155
|
status: :completed,
|
|
154
|
-
payment_id: payment_id,
|
|
156
|
+
payment_id: args[:payment_id],
|
|
155
157
|
processed_at: Time.current
|
|
156
158
|
)
|
|
157
159
|
|
|
@@ -160,23 +162,25 @@ class OrderProcessingReactor < RubyReactor::Reactor
|
|
|
160
162
|
end
|
|
161
163
|
|
|
162
164
|
step :send_confirmation do
|
|
163
|
-
argument :order, result(:validate_order)
|
|
164
|
-
argument :payment_id, result(:process_payment)
|
|
165
|
+
argument :order, result(:validate_order, :order)
|
|
166
|
+
argument :payment_id, result(:process_payment, :payment_id)
|
|
165
167
|
|
|
166
168
|
retries max_attempts: 3, backoff: :linear, base_delay: 10.seconds
|
|
167
169
|
|
|
168
|
-
run do |
|
|
170
|
+
run do |args, _ctx|
|
|
169
171
|
email_result = EmailService.send_order_confirmation(
|
|
170
|
-
to: order.customer.email,
|
|
171
|
-
order: order,
|
|
172
|
-
payment_id: payment_id
|
|
172
|
+
to: args[:order].customer.email,
|
|
173
|
+
order: args[:order],
|
|
174
|
+
payment_id: args[:payment_id]
|
|
173
175
|
)
|
|
174
176
|
|
|
175
|
-
|
|
177
|
+
return Failure("Confirmation email failed") unless email_result.success?
|
|
176
178
|
|
|
177
179
|
Success({ confirmation_sent: true })
|
|
178
180
|
end
|
|
179
181
|
end
|
|
182
|
+
|
|
183
|
+
returns :send_confirmation
|
|
180
184
|
end
|
|
181
185
|
```
|
|
182
186
|
|
|
@@ -187,16 +191,21 @@ end
|
|
|
187
191
|
```ruby
|
|
188
192
|
# Start order processing asynchronously
|
|
189
193
|
async_result = OrderProcessingReactor.run(order_id: 12345)
|
|
194
|
+
async_result.execution_id # UUID for state lookup
|
|
190
195
|
|
|
191
|
-
#
|
|
192
|
-
|
|
193
|
-
|
|
196
|
+
# Reload state later (e.g. from a polling endpoint)
|
|
197
|
+
reactor = OrderProcessingReactor.find(async_result.execution_id)
|
|
198
|
+
case reactor.context.status.to_s
|
|
199
|
+
when "completed"
|
|
194
200
|
puts "Order processed successfully!"
|
|
195
|
-
|
|
196
|
-
puts "Payment ID: #{
|
|
197
|
-
when
|
|
198
|
-
|
|
201
|
+
payment_id = reactor.context.intermediate_results[:process_payment][:payment_id]
|
|
202
|
+
puts "Payment ID: #{payment_id}"
|
|
203
|
+
when "failed"
|
|
204
|
+
failure = reactor.result # RubyReactor::Failure
|
|
205
|
+
puts "Order processing failed at #{failure.step_name}: #{failure.error}"
|
|
199
206
|
# Could trigger manual review process
|
|
207
|
+
when "running"
|
|
208
|
+
puts "Still processing..."
|
|
200
209
|
end
|
|
201
210
|
```
|
|
202
211
|
|
|
@@ -204,14 +213,15 @@ end
|
|
|
204
213
|
|
|
205
214
|
```ruby
|
|
206
215
|
# For testing or immediate processing
|
|
207
|
-
|
|
216
|
+
reactor = OrderProcessingReactor.new
|
|
217
|
+
result = reactor.run(order_id: 12345)
|
|
208
218
|
|
|
209
219
|
if result.success?
|
|
210
220
|
puts "Order completed!"
|
|
211
|
-
puts "Steps completed: #{
|
|
221
|
+
puts "Steps completed: #{reactor.context.intermediate_results.keys}"
|
|
212
222
|
else
|
|
213
|
-
puts "Failed at step: #{result.
|
|
214
|
-
puts "Error: #{result.error
|
|
223
|
+
puts "Failed at step: #{result.step_name}"
|
|
224
|
+
puts "Error: #{result.error}"
|
|
215
225
|
end
|
|
216
226
|
```
|
|
217
227
|
|
|
@@ -256,10 +266,10 @@ RSpec.describe OrderProcessingReactor do
|
|
|
256
266
|
allow(InventoryService).to receive(:confirm_reservation).and_return(true)
|
|
257
267
|
allow(EmailService).to receive(:send_order_confirmation).and_return(successful_email)
|
|
258
268
|
|
|
259
|
-
|
|
269
|
+
subject = test_reactor(OrderProcessingReactor, order_id: order.id)
|
|
260
270
|
|
|
261
|
-
expect(
|
|
262
|
-
expect(
|
|
271
|
+
expect(subject).to be_success
|
|
272
|
+
expect(subject).to have_run_step(:send_confirmation)
|
|
263
273
|
end
|
|
264
274
|
end
|
|
265
275
|
|
|
@@ -271,10 +281,10 @@ RSpec.describe OrderProcessingReactor do
|
|
|
271
281
|
|
|
272
282
|
expect(InventoryService).to receive(:release_reservation).with("res_123")
|
|
273
283
|
|
|
274
|
-
|
|
284
|
+
subject = test_reactor(OrderProcessingReactor, order_id: order.id)
|
|
275
285
|
|
|
276
|
-
expect(
|
|
277
|
-
expect(
|
|
286
|
+
expect(subject).to be_failure
|
|
287
|
+
expect(subject.error).to include("Payment failed")
|
|
278
288
|
end
|
|
279
289
|
end
|
|
280
290
|
end
|
|
@@ -315,15 +325,16 @@ email_delivery_failure_rate
|
|
|
315
325
|
class PartialOrderProcessingReactor < OrderProcessingReactor
|
|
316
326
|
# Override to allow partial fulfillment
|
|
317
327
|
step :check_inventory do
|
|
318
|
-
|
|
319
|
-
|
|
328
|
+
argument :order, result(:validate_order, :order)
|
|
329
|
+
|
|
330
|
+
run do |args, _ctx|
|
|
331
|
+
available_items, unavailable_items = partition_available_items(args[:order].items)
|
|
320
332
|
|
|
321
333
|
if available_items.any? && unavailable_items.any?
|
|
322
|
-
|
|
323
|
-
partial_order = create_partial_order(order, available_items)
|
|
334
|
+
partial_order = create_partial_order(args[:order], available_items)
|
|
324
335
|
Success({ partial_order: partial_order, unavailable_items: unavailable_items })
|
|
325
336
|
elsif available_items.empty?
|
|
326
|
-
|
|
337
|
+
Failure("No items available")
|
|
327
338
|
else
|
|
328
339
|
Success({ inventory_checked: true })
|
|
329
340
|
end
|
|
@@ -336,23 +347,27 @@ end
|
|
|
336
347
|
|
|
337
348
|
```ruby
|
|
338
349
|
class OrderCancellationReactor < RubyReactor::Reactor
|
|
350
|
+
input :order_id do
|
|
351
|
+
required(:order_id).filled(:string)
|
|
352
|
+
end
|
|
353
|
+
|
|
339
354
|
step :load_order do
|
|
340
|
-
|
|
341
|
-
required(:order_id).filled(:string)
|
|
342
|
-
end
|
|
355
|
+
argument :order_id, input(:order_id)
|
|
343
356
|
|
|
344
|
-
run do |
|
|
345
|
-
order = Order.find_by(id: order_id)
|
|
346
|
-
|
|
357
|
+
run do |args, _ctx|
|
|
358
|
+
order = Order.find_by(id: args[:order_id])
|
|
359
|
+
return Failure("Order not found") unless order
|
|
347
360
|
Success({ order: order })
|
|
348
361
|
end
|
|
349
362
|
end
|
|
350
363
|
|
|
351
364
|
step :cancel_order do
|
|
352
|
-
|
|
365
|
+
argument :order, result(:load_order, :order)
|
|
366
|
+
|
|
367
|
+
run do |args, _ctx|
|
|
368
|
+
order = args[:order]
|
|
353
369
|
# Only cancel if not already completed
|
|
354
370
|
if order.completed?
|
|
355
|
-
# Initiate refund and inventory restoration
|
|
356
371
|
PaymentService.refund(order.payment_id)
|
|
357
372
|
InventoryService.restore_order_items(order)
|
|
358
373
|
end
|
|
@@ -46,15 +46,15 @@ class PaymentProcessingReactor < RubyReactor::Reactor
|
|
|
46
46
|
# Payment processing needs careful retry configuration
|
|
47
47
|
retry_defaults max_attempts: 2, backoff: :fixed, base_delay: 30.seconds
|
|
48
48
|
|
|
49
|
-
input :amount
|
|
49
|
+
input :amount do
|
|
50
50
|
required(:amount).filled(:decimal, gt?: 0)
|
|
51
51
|
end
|
|
52
52
|
|
|
53
|
-
input :currency
|
|
53
|
+
input :currency do
|
|
54
54
|
required(:currency).filled(:string, included_in?: ['USD', 'EUR', 'GBP'])
|
|
55
55
|
end
|
|
56
56
|
|
|
57
|
-
input :card_token
|
|
57
|
+
input :card_token do
|
|
58
58
|
required(:card_token).filled(:string, format?: /^tok_/)
|
|
59
59
|
end
|
|
60
60
|
|
|
@@ -119,10 +119,10 @@ class PaymentProcessingReactor < RubyReactor::Reactor
|
|
|
119
119
|
Success({ auth_id: auth_result.id, auth_amount: amount })
|
|
120
120
|
end
|
|
121
121
|
|
|
122
|
-
undo do |
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
122
|
+
undo do |result, _args, _ctx|
|
|
123
|
+
# Void the pre-authorization if a later step fails
|
|
124
|
+
PaymentGateway.void_authorization(result[:auth_id]) if result[:auth_id]
|
|
125
|
+
Success()
|
|
126
126
|
end
|
|
127
127
|
end
|
|
128
128
|
|
|
@@ -148,10 +148,9 @@ class PaymentProcessingReactor < RubyReactor::Reactor
|
|
|
148
148
|
Success({ charge_id: charge_result.id, charged_amount: amount })
|
|
149
149
|
end
|
|
150
150
|
|
|
151
|
-
undo do |
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
PaymentGateway.refund(charge_id) if charge_id
|
|
151
|
+
undo do |result, _args, _ctx|
|
|
152
|
+
PaymentGateway.refund(result[:charge_id]) if result[:charge_id]
|
|
153
|
+
Success()
|
|
155
154
|
end
|
|
156
155
|
end
|
|
157
156
|
|
|
@@ -182,10 +181,9 @@ class PaymentProcessingReactor < RubyReactor::Reactor
|
|
|
182
181
|
{ transaction_id: transaction.id }
|
|
183
182
|
end
|
|
184
183
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
PaymentTransaction.find_by(id: transaction_id)&.update!(status: :refunded)
|
|
184
|
+
undo do |result, _args, _ctx|
|
|
185
|
+
PaymentTransaction.find_by(id: result[:transaction_id])&.update!(status: :refunded)
|
|
186
|
+
Success()
|
|
189
187
|
end
|
|
190
188
|
end
|
|
191
189
|
|
|
@@ -301,7 +299,7 @@ class SubscriptionPaymentReactor < RubyReactor::Reactor
|
|
|
301
299
|
|
|
302
300
|
retry_defaults max_attempts: 3, backoff: :exponential, base_delay: 1.hour
|
|
303
301
|
|
|
304
|
-
input :subscription_id
|
|
302
|
+
input :subscription_id do
|
|
305
303
|
required(:subscription_id).filled(:string)
|
|
306
304
|
end
|
|
307
305
|
|
|
@@ -351,97 +349,9 @@ class SubscriptionPaymentReactor < RubyReactor::Reactor
|
|
|
351
349
|
{ charge_id: charge_result.id }
|
|
352
350
|
end
|
|
353
351
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
PaymentGateway.refund_subscription_charge(charge_id) if charge_id
|
|
358
|
-
end
|
|
359
|
-
end
|
|
360
|
-
|
|
361
|
-
step :update_billing_record do
|
|
362
|
-
argument :validation_data, result(:validate_subscription)
|
|
363
|
-
argument :proration_data, result(:calculate_proration)
|
|
364
|
-
argument :charge_data, result(:charge_subscription)
|
|
365
|
-
|
|
366
|
-
run do |args, _context|
|
|
367
|
-
subscription = args[:validation_data][:subscription]
|
|
368
|
-
charge_id = args[:charge_data][:charge_id]
|
|
369
|
-
billing_period = args[:proration_data][:billing_period]
|
|
370
|
-
|
|
371
|
-
BillingRecord.create!(
|
|
372
|
-
subscription: subscription,
|
|
373
|
-
charge_id: charge_id,
|
|
374
|
-
billing_period: billing_period,
|
|
375
|
-
status: :paid
|
|
376
|
-
)
|
|
377
|
-
|
|
378
|
-
{ billing_recorded: true }
|
|
379
|
-
end
|
|
380
|
-
end
|
|
381
|
-
```
|
|
382
|
-
|
|
383
|
-
### Subscription Payment Processing
|
|
384
|
-
|
|
385
|
-
```ruby
|
|
386
|
-
class SubscriptionPaymentReactor < RubyReactor::Reactor
|
|
387
|
-
async true
|
|
388
|
-
|
|
389
|
-
retry_defaults max_attempts: 3, backoff: :exponential, base_delay: 1.hour
|
|
390
|
-
|
|
391
|
-
input :subscription_id, validate: -> do
|
|
392
|
-
required(:subscription_id).filled(:string)
|
|
393
|
-
end
|
|
394
|
-
|
|
395
|
-
step :validate_subscription do
|
|
396
|
-
validate_args do
|
|
397
|
-
required(:subscription_id).filled(:string)
|
|
398
|
-
end
|
|
399
|
-
|
|
400
|
-
run do |subscription_id:|
|
|
401
|
-
subscription = Subscription.find(subscription_id)
|
|
402
|
-
raise "Subscription not found" unless subscription
|
|
403
|
-
raise "Subscription inactive" unless subscription.active?
|
|
404
|
-
|
|
405
|
-
{ subscription: subscription }
|
|
406
|
-
end
|
|
407
|
-
end
|
|
408
|
-
|
|
409
|
-
step :calculate_proration do
|
|
410
|
-
argument :validation_data, result(:validate_subscription)
|
|
411
|
-
|
|
412
|
-
run do |args, _context|
|
|
413
|
-
subscription = args[:validation_data][:subscription]
|
|
414
|
-
|
|
415
|
-
# Calculate prorated amount for billing period
|
|
416
|
-
proration = BillingService.calculate_proration(subscription)
|
|
417
|
-
{ proration_amount: proration.amount, billing_period: proration.period }
|
|
418
|
-
end
|
|
419
|
-
end
|
|
420
|
-
|
|
421
|
-
step :charge_subscription do
|
|
422
|
-
argument :validation_data, result(:validate_subscription)
|
|
423
|
-
argument :proration_data, result(:calculate_proration)
|
|
424
|
-
|
|
425
|
-
run do |args, _context|
|
|
426
|
-
subscription = args[:validation_data][:subscription]
|
|
427
|
-
proration_amount = args[:proration_data][:proration_amount]
|
|
428
|
-
billing_period = args[:proration_data][:billing_period]
|
|
429
|
-
|
|
430
|
-
charge_result = PaymentGateway.charge_subscription(
|
|
431
|
-
customer_id: subscription.customer.stripe_id,
|
|
432
|
-
amount: proration_amount,
|
|
433
|
-
description: "Subscription #{subscription.id} - #{billing_period}"
|
|
434
|
-
)
|
|
435
|
-
|
|
436
|
-
raise "Subscription charge failed" unless charge_result.success?
|
|
437
|
-
|
|
438
|
-
{ charge_id: charge_result.id }
|
|
439
|
-
end
|
|
440
|
-
|
|
441
|
-
compensate do |args, _context|
|
|
442
|
-
charge_id = args[:validation_data][:charge_id] || args[:charge_id]
|
|
443
|
-
# Refund the subscription charge
|
|
444
|
-
PaymentGateway.refund_subscription_charge(charge_id) if charge_id
|
|
352
|
+
undo do |result, _args, _ctx|
|
|
353
|
+
PaymentGateway.refund_subscription_charge(result[:charge_id]) if result[:charge_id]
|
|
354
|
+
Success()
|
|
445
355
|
end
|
|
446
356
|
end
|
|
447
357
|
|
|
@@ -541,10 +451,10 @@ RSpec.describe PaymentProcessingReactor do
|
|
|
541
451
|
allow(PaymentGateway).to receive(:charge).and_return(successful_charge)
|
|
542
452
|
allow(EmailService).to receive(:send_payment_receipt).and_return(successful_email)
|
|
543
453
|
|
|
544
|
-
|
|
454
|
+
subject = test_reactor(PaymentProcessingReactor, valid_payment_params)
|
|
545
455
|
|
|
546
|
-
expect(
|
|
547
|
-
expect(
|
|
456
|
+
expect(subject).to be_success
|
|
457
|
+
expect(subject.step_result(:charge_payment)[:charge_id]).to be_present
|
|
548
458
|
end
|
|
549
459
|
end
|
|
550
460
|
|
|
@@ -552,10 +462,10 @@ RSpec.describe PaymentProcessingReactor do
|
|
|
552
462
|
it "blocks high-risk payments" do
|
|
553
463
|
allow(FraudDetectionService).to receive(:analyze).and_return(0.9)
|
|
554
464
|
|
|
555
|
-
|
|
465
|
+
subject = test_reactor(PaymentProcessingReactor, valid_payment_params)
|
|
556
466
|
|
|
557
|
-
expect(
|
|
558
|
-
expect(
|
|
467
|
+
expect(subject).to be_failure
|
|
468
|
+
expect(subject.error).to include("High fraud risk")
|
|
559
469
|
end
|
|
560
470
|
end
|
|
561
471
|
|
|
@@ -569,9 +479,10 @@ RSpec.describe PaymentProcessingReactor do
|
|
|
569
479
|
|
|
570
480
|
expect(PaymentGateway).to receive(:pre_authorize).exactly(3).times
|
|
571
481
|
|
|
572
|
-
|
|
482
|
+
subject = test_reactor(PaymentProcessingReactor, valid_payment_params)
|
|
573
483
|
|
|
574
|
-
expect(
|
|
484
|
+
expect(subject).to be_success
|
|
485
|
+
expect(subject).to have_retried_step(:pre_authorize).times(2)
|
|
575
486
|
end
|
|
576
487
|
end
|
|
577
488
|
|
|
@@ -583,9 +494,9 @@ RSpec.describe PaymentProcessingReactor do
|
|
|
583
494
|
|
|
584
495
|
expect(PaymentGateway).to receive(:void_authorization)
|
|
585
496
|
|
|
586
|
-
|
|
497
|
+
subject = test_reactor(PaymentProcessingReactor, valid_payment_params)
|
|
587
498
|
|
|
588
|
-
expect(
|
|
499
|
+
expect(subject).to be_failure
|
|
589
500
|
end
|
|
590
501
|
end
|
|
591
502
|
end
|