ruby_reactor 0.1.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.
Files changed (75) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +98 -0
  4. data/CODE_OF_CONDUCT.md +84 -0
  5. data/README.md +570 -0
  6. data/Rakefile +12 -0
  7. data/documentation/DAG.md +457 -0
  8. data/documentation/README.md +123 -0
  9. data/documentation/async_reactors.md +369 -0
  10. data/documentation/composition.md +199 -0
  11. data/documentation/core_concepts.md +662 -0
  12. data/documentation/data_pipelines.md +224 -0
  13. data/documentation/examples/inventory_management.md +749 -0
  14. data/documentation/examples/order_processing.md +365 -0
  15. data/documentation/examples/payment_processing.md +654 -0
  16. data/documentation/getting_started.md +224 -0
  17. data/documentation/retry_configuration.md +357 -0
  18. data/lib/ruby_reactor/async_router.rb +91 -0
  19. data/lib/ruby_reactor/configuration.rb +41 -0
  20. data/lib/ruby_reactor/context.rb +169 -0
  21. data/lib/ruby_reactor/context_serializer.rb +164 -0
  22. data/lib/ruby_reactor/dependency_graph.rb +126 -0
  23. data/lib/ruby_reactor/dsl/compose_builder.rb +86 -0
  24. data/lib/ruby_reactor/dsl/map_builder.rb +112 -0
  25. data/lib/ruby_reactor/dsl/reactor.rb +151 -0
  26. data/lib/ruby_reactor/dsl/step_builder.rb +177 -0
  27. data/lib/ruby_reactor/dsl/template_helpers.rb +36 -0
  28. data/lib/ruby_reactor/dsl/validation_helpers.rb +35 -0
  29. data/lib/ruby_reactor/error/base.rb +16 -0
  30. data/lib/ruby_reactor/error/compensation_error.rb +8 -0
  31. data/lib/ruby_reactor/error/context_too_large_error.rb +11 -0
  32. data/lib/ruby_reactor/error/dependency_error.rb +8 -0
  33. data/lib/ruby_reactor/error/deserialization_error.rb +11 -0
  34. data/lib/ruby_reactor/error/input_validation_error.rb +29 -0
  35. data/lib/ruby_reactor/error/schema_version_error.rb +11 -0
  36. data/lib/ruby_reactor/error/step_failure_error.rb +18 -0
  37. data/lib/ruby_reactor/error/undo_error.rb +8 -0
  38. data/lib/ruby_reactor/error/validation_error.rb +8 -0
  39. data/lib/ruby_reactor/executor/compensation_manager.rb +79 -0
  40. data/lib/ruby_reactor/executor/graph_manager.rb +41 -0
  41. data/lib/ruby_reactor/executor/input_validator.rb +39 -0
  42. data/lib/ruby_reactor/executor/result_handler.rb +103 -0
  43. data/lib/ruby_reactor/executor/retry_manager.rb +156 -0
  44. data/lib/ruby_reactor/executor/step_executor.rb +319 -0
  45. data/lib/ruby_reactor/executor.rb +123 -0
  46. data/lib/ruby_reactor/map/collector.rb +65 -0
  47. data/lib/ruby_reactor/map/element_executor.rb +154 -0
  48. data/lib/ruby_reactor/map/execution.rb +60 -0
  49. data/lib/ruby_reactor/map/helpers.rb +67 -0
  50. data/lib/ruby_reactor/max_retries_exhausted_failure.rb +19 -0
  51. data/lib/ruby_reactor/reactor.rb +75 -0
  52. data/lib/ruby_reactor/retry_context.rb +92 -0
  53. data/lib/ruby_reactor/retry_queued_result.rb +26 -0
  54. data/lib/ruby_reactor/sidekiq_workers/map_collector_worker.rb +13 -0
  55. data/lib/ruby_reactor/sidekiq_workers/map_element_worker.rb +13 -0
  56. data/lib/ruby_reactor/sidekiq_workers/map_execution_worker.rb +15 -0
  57. data/lib/ruby_reactor/sidekiq_workers/worker.rb +55 -0
  58. data/lib/ruby_reactor/step/compose_step.rb +107 -0
  59. data/lib/ruby_reactor/step/map_step.rb +234 -0
  60. data/lib/ruby_reactor/step.rb +33 -0
  61. data/lib/ruby_reactor/storage/adapter.rb +51 -0
  62. data/lib/ruby_reactor/storage/configuration.rb +15 -0
  63. data/lib/ruby_reactor/storage/redis_adapter.rb +140 -0
  64. data/lib/ruby_reactor/template/base.rb +15 -0
  65. data/lib/ruby_reactor/template/element.rb +25 -0
  66. data/lib/ruby_reactor/template/input.rb +48 -0
  67. data/lib/ruby_reactor/template/result.rb +48 -0
  68. data/lib/ruby_reactor/template/value.rb +22 -0
  69. data/lib/ruby_reactor/validation/base.rb +26 -0
  70. data/lib/ruby_reactor/validation/input_validator.rb +62 -0
  71. data/lib/ruby_reactor/validation/schema_builder.rb +17 -0
  72. data/lib/ruby_reactor/version.rb +5 -0
  73. data/lib/ruby_reactor.rb +159 -0
  74. data/sig/ruby_reactor.rbs +4 -0
  75. metadata +178 -0
@@ -0,0 +1,654 @@
1
+ # Payment Processing Reactor Example
2
+
3
+ This example shows a payment processing workflow with fraud detection, multiple payment attempts, and comprehensive error handling.
4
+
5
+ ## Overview
6
+
7
+ The PaymentProcessingReactor handles complex payment scenarios:
8
+
9
+ 1. **Validate Payment**: Check payment details and amounts
10
+ 2. **Fraud Check**: Run fraud detection algorithms
11
+ 3. **Pre-authorization**: Pre-authorize the payment
12
+ 4. **Final Charge**: Complete the payment
13
+ 5. **Record Transaction**: Store payment details
14
+ 6. **Send Receipt**: Email payment confirmation
15
+
16
+ ### Payment Processing Workflow
17
+
18
+ ```mermaid
19
+ graph TD
20
+ A[Payment Request] --> B[validate_payment]
21
+ B --> C{Valid<br/>Payment?}
22
+ C -->|No| D[Fail: Validation Error]
23
+ C -->|Yes| E[fraud_detection]
24
+ E --> F{Fraud<br/>Score OK?}
25
+ F -->|No| G[Fail: High Fraud Risk]
26
+ F -->|Yes| H[pre_authorize]
27
+ H --> I{Pre-auth<br/>Success?}
28
+ I -->|No| J[Retry with Backoff]
29
+ J --> H
30
+ I -->|Yes| K[charge_payment]
31
+ K --> L{Charge<br/>Success?}
32
+ L -->|No| M[Compensate: Void Pre-auth]
33
+ L -->|Yes| N[record_transaction]
34
+ N --> O[send_receipt]
35
+ O --> P[Success: Payment Complete]
36
+
37
+ J -->|Max Retries| Q[Fail: Pre-auth Failed]
38
+ ```
39
+
40
+ ## Implementation
41
+
42
+ ```ruby
43
+ class PaymentProcessingReactor < RubyReactor::Reactor
44
+ async true
45
+
46
+ # Payment processing needs careful retry configuration
47
+ retry_defaults max_attempts: 2, backoff: :fixed, base_delay: 30.seconds
48
+
49
+ input :amount, validate: -> do
50
+ required(:amount).filled(:decimal, gt?: 0)
51
+ end
52
+
53
+ input :currency, validate: -> do
54
+ required(:currency).filled(:string, included_in?: ['USD', 'EUR', 'GBP'])
55
+ end
56
+
57
+ input :card_token, validate: -> do
58
+ required(:card_token).filled(:string, format?: /^tok_/)
59
+ end
60
+
61
+ step :validate_payment do
62
+ argument :amount, input(:amount)
63
+ argument :currency, input(:currency)
64
+ argument :card_token, input(:card_token)
65
+
66
+ run do |args, _context|
67
+ amount = args[:amount]
68
+ currency = args[:currency]
69
+ card_token = args[:card_token]
70
+
71
+ Success({ validated_amount: amount, validated_currency: currency })
72
+ end
73
+ end
74
+
75
+ step :fraud_detection do
76
+ argument :validation_data, result(:validate_payment)
77
+ argument :amount, input(:amount)
78
+ argument :card_token, input(:card_token)
79
+
80
+ run do |args, _context|
81
+ amount = args[:amount]
82
+ card_token = args[:card_token]
83
+
84
+ fraud_score = FraudDetectionService.analyze(
85
+ amount: amount,
86
+ card_token: card_token,
87
+ # Additional context...
88
+ )
89
+
90
+ if fraud_score > 0.8
91
+ raise "High fraud risk detected (score: #{fraud_score})"
92
+ end
93
+
94
+ Success({ fraud_score: fraud_score, fraud_check_passed: true })
95
+ end
96
+ end
97
+
98
+ step :pre_authorize do
99
+ argument :fraud_data, result(:fraud_detection)
100
+ argument :amount, input(:amount)
101
+ argument :currency, input(:currency)
102
+ argument :card_token, input(:card_token)
103
+
104
+ retries max_attempts: 3, backoff: :exponential, base_delay: 5.seconds
105
+
106
+ run do |args, _context|
107
+ amount = args[:amount]
108
+ currency = args[:currency]
109
+ card_token = args[:card_token]
110
+
111
+ auth_result = PaymentGateway.pre_authorize(
112
+ amount: amount,
113
+ currency: currency,
114
+ card_token: card_token
115
+ )
116
+
117
+ raise "Pre-authorization failed: #{auth_result.error}" unless auth_result.success?
118
+
119
+ Success({ auth_id: auth_result.id, auth_amount: amount })
120
+ end
121
+
122
+ undo do |args, _context|
123
+ auth_id = args[:fraud_data][:auth_id] || args[:auth_id]
124
+ # Void the pre-authorization
125
+ PaymentGateway.void_authorization(auth_id) if auth_id
126
+ end
127
+ end
128
+
129
+ step :charge_payment do
130
+ argument :auth_data, result(:pre_authorize)
131
+
132
+ # Final charge - critical operation
133
+ retries max_attempts: 3, backoff: :fixed, base_delay: 60.seconds
134
+
135
+ run do |args, _context|
136
+ auth_id = args[:auth_data][:auth_id]
137
+ amount = args[:auth_data][:auth_amount]
138
+ currency = args[:currency]
139
+
140
+ charge_result = PaymentGateway.charge(
141
+ auth_id: auth_id,
142
+ amount: amount,
143
+ currency: currency
144
+ )
145
+
146
+ raise "Charge failed: #{charge_result.error}" unless charge_result.success?
147
+
148
+ Success({ charge_id: charge_result.id, charged_amount: amount })
149
+ end
150
+
151
+ undo do |args, _context|
152
+ charge_id = args[:auth_data][:charge_id] || args[:charge_id]
153
+ # Refund the charge
154
+ PaymentGateway.refund(charge_id) if charge_id
155
+ end
156
+ end
157
+
158
+ step :record_transaction do
159
+ argument :charge_data, result(:charge_payment)
160
+ argument :fraud_data, result(:fraud_detection)
161
+ argument :amount, input(:amount)
162
+ argument :currency, input(:currency)
163
+ argument :card_token, input(:card_token)
164
+
165
+ run do |args, _context|
166
+ charge_id = args[:charge_data][:charge_id]
167
+ amount = args[:amount]
168
+ currency = args[:currency]
169
+ card_token = args[:card_token]
170
+ fraud_score = args[:fraud_data][:fraud_score]
171
+
172
+ transaction = PaymentTransaction.create!(
173
+ charge_id: charge_id,
174
+ amount: amount,
175
+ currency: currency,
176
+ card_token: card_token,
177
+ fraud_score: fraud_score,
178
+ status: :completed,
179
+ processed_at: Time.current
180
+ )
181
+
182
+ { transaction_id: transaction.id }
183
+ end
184
+
185
+ compensate do |args, _context|
186
+ transaction_id = args[:charge_data][:transaction_id] || args[:transaction_id]
187
+ # Mark transaction as failed/refunded
188
+ PaymentTransaction.find_by(id: transaction_id)&.update!(status: :refunded)
189
+ end
190
+ end
191
+
192
+ step :send_receipt do
193
+ argument :transaction_data, result(:record_transaction)
194
+ argument :amount, input(:amount)
195
+ argument :currency, input(:currency)
196
+
197
+ retries max_attempts: 3, backoff: :linear, base_delay: 10.seconds
198
+
199
+ run do |args, _context|
200
+ transaction_id = args[:transaction_data][:transaction_id]
201
+ amount = args[:amount]
202
+ currency = args[:currency]
203
+
204
+ transaction = PaymentTransaction.find(transaction_id)
205
+ customer = transaction.customer
206
+
207
+ receipt_result = EmailService.send_payment_receipt(
208
+ to: customer.email,
209
+ transaction: transaction,
210
+ amount: amount,
211
+ currency: currency
212
+ )
213
+
214
+ raise "Receipt delivery failed" unless receipt_result.success?
215
+
216
+ { receipt_sent: true }
217
+ end
218
+ end
219
+ end
220
+ ```
221
+
222
+ ## Advanced Payment Scenarios
223
+
224
+ ### Multi-Attempt Payment Processing
225
+
226
+ ```ruby
227
+ class MultiAttemptPaymentReactor < RubyReactor::Reactor
228
+ async true
229
+
230
+ retry_defaults max_attempts: 3, backoff: :exponential, base_delay: 10.seconds
231
+
232
+ step :attempt_primary_card do
233
+ argument :order, input(:order)
234
+
235
+ run do |args, _context|
236
+ order = args[:order]
237
+
238
+ result = process_payment_with_card(order, order.primary_card)
239
+ if result.success?
240
+ { payment_result: result, card_used: :primary }
241
+ else
242
+ { payment_failed: true, primary_card_failed: true }
243
+ end
244
+ end
245
+ end
246
+
247
+ step :attempt_backup_card do
248
+ argument :primary_attempt, result(:attempt_primary_card)
249
+ argument :order, input(:order)
250
+
251
+ run do |args, _context|
252
+ order = args[:order]
253
+ primary_card_failed = args[:primary_attempt][:primary_card_failed]
254
+
255
+ return { skipped: true } unless primary_card_failed
256
+
257
+ result = process_payment_with_card(order, order.backup_card)
258
+ if result.success?
259
+ { payment_result: result, card_used: :backup }
260
+ else
261
+ raise "All payment attempts failed"
262
+ end
263
+ end
264
+ end
265
+
266
+ step :process_successful_payment do
267
+ argument :attempt_result, result(:attempt_backup_card)
268
+
269
+ run do |args, _context|
270
+ payment_result = args[:attempt_result][:payment_result]
271
+ card_used = args[:attempt_result][:card_used]
272
+
273
+ # Record successful payment
274
+ PaymentRecord.create!(
275
+ charge_id: payment_result.id,
276
+ card_used: card_used,
277
+ amount: payment_result.amount
278
+ )
279
+
280
+ { payment_recorded: true }
281
+ end
282
+ end
283
+
284
+ private
285
+
286
+ def process_payment_with_card(order, card)
287
+ PaymentGateway.charge(
288
+ amount: order.total,
289
+ card_token: card.token,
290
+ description: "Order ##{order.id}"
291
+ )
292
+ end
293
+ end
294
+ ```
295
+
296
+ ### Subscription Payment Processing
297
+
298
+ ```ruby
299
+ class SubscriptionPaymentReactor < RubyReactor::Reactor
300
+ async true
301
+
302
+ retry_defaults max_attempts: 3, backoff: :exponential, base_delay: 1.hour
303
+
304
+ input :subscription_id, validate: -> do
305
+ required(:subscription_id).filled(:string)
306
+ end
307
+
308
+ step :validate_subscription do
309
+ argument :subscription_id, input(:subscription_id)
310
+
311
+ run do |args, _context|
312
+ subscription_id = args[:subscription_id]
313
+
314
+ subscription = Subscription.find(subscription_id)
315
+ raise "Subscription not found" unless subscription
316
+ raise "Subscription inactive" unless subscription.active?
317
+
318
+ { subscription: subscription }
319
+ end
320
+ end
321
+
322
+ step :calculate_proration do
323
+ argument :validation_data, result(:validate_subscription)
324
+
325
+ run do |args, _context|
326
+ subscription = args[:validation_data][:subscription]
327
+
328
+ # Calculate prorated amount for billing period
329
+ proration = BillingService.calculate_proration(subscription)
330
+ { proration_amount: proration.amount, billing_period: proration.period }
331
+ end
332
+ end
333
+
334
+ step :charge_subscription do
335
+ argument :validation_data, result(:validate_subscription)
336
+ argument :proration_data, result(:calculate_proration)
337
+
338
+ run do |args, _context|
339
+ subscription = args[:validation_data][:subscription]
340
+ proration_amount = args[:proration_data][:proration_amount]
341
+ billing_period = args[:proration_data][:billing_period]
342
+
343
+ charge_result = PaymentGateway.charge_subscription(
344
+ customer_id: subscription.customer.stripe_id,
345
+ amount: proration_amount,
346
+ description: "Subscription #{subscription.id} - #{billing_period}"
347
+ )
348
+
349
+ raise "Subscription charge failed" unless charge_result.success?
350
+
351
+ { charge_id: charge_result.id }
352
+ end
353
+
354
+ compensate do |args, _context|
355
+ charge_id = args[:validation_data][:charge_id] || args[:charge_id]
356
+ # Refund the subscription charge
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
445
+ end
446
+ end
447
+
448
+ step :update_billing_record do
449
+ argument :validation_data, result(:validate_subscription)
450
+ argument :proration_data, result(:calculate_proration)
451
+ argument :charge_data, result(:charge_subscription)
452
+
453
+ run do |args, _context|
454
+ subscription = args[:validation_data][:subscription]
455
+ charge_id = args[:charge_data][:charge_id]
456
+ billing_period = args[:proration_data][:billing_period]
457
+
458
+ BillingRecord.create!(
459
+ subscription: subscription,
460
+ charge_id: charge_id,
461
+ billing_period: billing_period,
462
+ status: :paid
463
+ )
464
+
465
+ { billing_recorded: true }
466
+ end
467
+ end
468
+ end
469
+ ```
470
+
471
+ ## Error Handling Patterns
472
+
473
+ ### Payment Gateway Timeouts
474
+
475
+ ```ruby
476
+ class TimeoutHandlingReactor < RubyReactor::Reactor
477
+ step :handle_timeout do
478
+ retries max_attempts: 5, backoff: :exponential, base_delay: 2.seconds
479
+
480
+ run do
481
+ begin
482
+ Timeout.timeout(10.seconds) do
483
+ PaymentGateway.charge(amount: 100, card_token: token)
484
+ end
485
+ rescue Timeout::Error
486
+ raise PaymentTimeoutError.new("Gateway timeout")
487
+ end
488
+ end
489
+ end
490
+ end
491
+
492
+ class PaymentTimeoutError < StandardError
493
+ def retryable?
494
+ true
495
+ end
496
+ end
497
+ ```
498
+
499
+ ### Insufficient Funds Handling
500
+
501
+ ```ruby
502
+ class InsufficientFundsReactor < RubyReactor::Reactor
503
+ step :handle_insufficient_funds do
504
+ run do
505
+ result = PaymentGateway.charge(amount: 100, card_token: token)
506
+
507
+ if result.error_code == 'insufficient_funds'
508
+ # Trigger alternative payment flow
509
+ notify_customer_insufficient_funds
510
+ raise NonRetryablePaymentError.new("Insufficient funds")
511
+ end
512
+
513
+ result
514
+ end
515
+ end
516
+ end
517
+
518
+ class NonRetryablePaymentError < StandardError
519
+ def retryable?
520
+ false
521
+ end
522
+ end
523
+ ```
524
+
525
+ ## Testing Payment Reactors
526
+
527
+ ```ruby
528
+ RSpec.describe PaymentProcessingReactor do
529
+ let(:valid_payment_params) do
530
+ {
531
+ amount: 100.00,
532
+ currency: 'USD',
533
+ card_token: 'tok_1234567890'
534
+ }
535
+ end
536
+
537
+ context "successful payment" do
538
+ it "completes all payment steps" do
539
+ allow(FraudDetectionService).to receive(:analyze).and_return(0.3)
540
+ allow(PaymentGateway).to receive(:pre_authorize).and_return(successful_auth)
541
+ allow(PaymentGateway).to receive(:charge).and_return(successful_charge)
542
+ allow(EmailService).to receive(:send_payment_receipt).and_return(successful_email)
543
+
544
+ result = PaymentProcessingReactor.run(valid_payment_params)
545
+
546
+ expect(result).to be_success
547
+ expect(result.step_results[:charge_payment][:charge_id]).to be_present
548
+ end
549
+ end
550
+
551
+ context "fraud detection" do
552
+ it "blocks high-risk payments" do
553
+ allow(FraudDetectionService).to receive(:analyze).and_return(0.9)
554
+
555
+ result = PaymentProcessingReactor.run(valid_payment_params)
556
+
557
+ expect(result).to be_failure
558
+ expect(result.error.message).to include("High fraud risk")
559
+ end
560
+ end
561
+
562
+ context "payment gateway failure" do
563
+ it "retries pre-authorization" do
564
+ allow(FraudDetectionService).to receive(:analyze).and_return(0.3)
565
+ allow(PaymentGateway).to receive(:pre_authorize)
566
+ .and_raise(PaymentGateway::TimeoutError)
567
+ .and_raise(PaymentGateway::TimeoutError)
568
+ .and_return(successful_auth)
569
+
570
+ expect(PaymentGateway).to receive(:pre_authorize).exactly(3).times
571
+
572
+ result = PaymentProcessingReactor.run(valid_payment_params)
573
+
574
+ expect(result).to be_success
575
+ end
576
+ end
577
+
578
+ context "compensation" do
579
+ it "refunds on final charge failure" do
580
+ allow(FraudDetectionService).to receive(:analyze).and_return(0.3)
581
+ allow(PaymentGateway).to receive(:pre_authorize).and_return(successful_auth)
582
+ allow(PaymentGateway).to receive(:charge).and_raise(PaymentGateway::DeclineError)
583
+
584
+ expect(PaymentGateway).to receive(:void_authorization)
585
+
586
+ result = PaymentProcessingReactor.run(valid_payment_params)
587
+
588
+ expect(result).to be_failure
589
+ end
590
+ end
591
+ end
592
+ ```
593
+
594
+ ## Monitoring and Alerting
595
+
596
+ ```ruby
597
+ # Key metrics for payment processing
598
+ PAYMENT_METRICS = {
599
+ payment_success_rate: "Percentage of successful payments",
600
+ fraud_detection_rate: "Percentage of payments flagged as fraud",
601
+ average_payment_time: "Average time to process payment",
602
+ retry_rate: "Percentage of payments requiring retries",
603
+ chargeback_rate: "Rate of payment chargebacks"
604
+ }
605
+
606
+ # Alerts
607
+ PAYMENT_ALERTS = {
608
+ high_failure_rate: "Payment success rate below 95%",
609
+ high_fraud_rate: "Fraud detection rate above 10%",
610
+ slow_processing: "Average payment time above 30 seconds"
611
+ }
612
+ ```
613
+
614
+ ## Security Considerations
615
+
616
+ 1. **PCI Compliance**: Never log full card details
617
+ 2. **Tokenization**: Use payment provider tokens, not raw card data
618
+ 3. **Idempotency Keys**: Prevent duplicate charges
619
+ 4. **Rate Limiting**: Implement per-customer rate limits
620
+ 5. **Audit Logging**: Log all payment attempts (without sensitive data)
621
+
622
+ ## Performance Optimization
623
+
624
+ ### Connection Pooling
625
+
626
+ ```ruby
627
+ # Configure payment gateway connection pooling
628
+ PaymentGateway.configure do |config|
629
+ config.connection_pool_size = 10
630
+ config.connection_timeout = 5.seconds
631
+ end
632
+ ```
633
+
634
+ ### Caching
635
+
636
+ ```ruby
637
+ # Cache fraud detection models
638
+ Rails.cache.fetch("fraud_model_#{Date.today}", expires_in: 1.day) do
639
+ FraudDetectionService.load_model
640
+ end
641
+ ```
642
+
643
+ ### Async Processing
644
+
645
+ ```ruby
646
+ # Use async for non-critical steps
647
+ step :send_receipt do
648
+ async true # Don't block payment completion on email delivery
649
+
650
+ run do
651
+ # Email sending logic
652
+ end
653
+ end
654
+ ```