solidus_stripe 4.4.1 → 5.0.0.alpha.1

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 (153) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +92 -52
  3. data/.gitignore +3 -0
  4. data/.rubocop.yml +94 -4
  5. data/.yardopts +1 -0
  6. data/Gemfile +10 -30
  7. data/LICENSE +2 -2
  8. data/Procfile.dev +3 -0
  9. data/README.md +145 -215
  10. data/Rakefile +5 -44
  11. data/app/assets/javascripts/spree/backend/solidus_stripe.js +2 -0
  12. data/app/assets/stylesheets/spree/backend/solidus_stripe.css +4 -0
  13. data/app/controllers/solidus_stripe/intents_controller.rb +36 -52
  14. data/app/controllers/solidus_stripe/webhooks_controller.rb +28 -0
  15. data/app/models/concerns/solidus_stripe/log_entries.rb +31 -0
  16. data/app/models/solidus_stripe/customer.rb +21 -0
  17. data/app/models/solidus_stripe/gateway.rb +231 -0
  18. data/app/models/solidus_stripe/payment_intent.rb +111 -0
  19. data/app/models/solidus_stripe/payment_method.rb +106 -0
  20. data/app/models/solidus_stripe/payment_source.rb +31 -0
  21. data/app/models/solidus_stripe/slug_entry.rb +20 -0
  22. data/app/models/solidus_stripe.rb +7 -0
  23. data/app/subscribers/solidus_stripe/webhook/charge_subscriber.rb +28 -0
  24. data/app/subscribers/solidus_stripe/webhook/payment_intent_subscriber.rb +112 -0
  25. data/app/views/spree/admin/payments/source_forms/_stripe.html.erb +29 -0
  26. data/app/views/spree/admin/payments/source_forms/existing_payment/_stripe.html.erb +14 -0
  27. data/app/views/spree/admin/payments/source_forms/existing_payment/stripe/_card.html.erb +8 -0
  28. data/app/views/spree/admin/payments/source_forms/existing_payment/stripe/_default.html.erb +7 -0
  29. data/app/views/spree/admin/payments/source_views/_stripe.html.erb +15 -0
  30. data/app/views/spree/api/payments/source_views/_stripe.json.jbuilder +8 -0
  31. data/bin/dev +13 -0
  32. data/bin/dummy-app +29 -0
  33. data/bin/rails +38 -3
  34. data/bin/rails-dummy-app +3 -0
  35. data/bin/rails-engine +1 -11
  36. data/bin/rails-new +55 -0
  37. data/bin/rails-sandbox +1 -14
  38. data/bin/rspec +10 -0
  39. data/bin/sandbox +12 -74
  40. data/bin/setup +1 -0
  41. data/bin/update-migrations +56 -0
  42. data/codecov.yml +12 -0
  43. data/config/locales/en.yml +16 -1
  44. data/config/routes.rb +5 -11
  45. data/coverage.rb +42 -0
  46. data/db/migrate/20230109183332_create_solidus_stripe_payment_sources.rb +10 -0
  47. data/db/migrate/20230303154931_create_solidus_stripe_setup_intent.rb +10 -0
  48. data/db/migrate/20230306105520_create_solidus_stripe_payment_intents.rb +10 -0
  49. data/db/migrate/20230308122414_create_solidus_stripe_webhook_endpoints.rb +10 -0
  50. data/db/migrate/20230310152615_add_payment_method_reference_to_stripe_intents.rb +6 -0
  51. data/db/migrate/20230310171444_normalize_stripe_intent_id_attributes.rb +6 -0
  52. data/db/migrate/20230313150008_create_solidus_stripe_customers.rb +16 -0
  53. data/db/migrate/20230323154931_drop_solidus_stripe_setup_intent.rb +13 -0
  54. data/db/migrate/20230403094916_rename_webhook_endpoint_to_payment_method_slug_entries.rb +5 -0
  55. data/db/seeds.rb +6 -24
  56. data/lib/generators/solidus_stripe/install/install_generator.rb +121 -14
  57. data/lib/generators/solidus_stripe/install/templates/app/assets/stylesheets/spree/frontend/solidus_stripe.css +13 -0
  58. data/lib/generators/solidus_stripe/install/templates/app/javascript/controllers/solidus_stripe_confirm_controller.js +39 -0
  59. data/lib/generators/solidus_stripe/install/templates/app/javascript/controllers/solidus_stripe_payment_controller.js +89 -0
  60. data/lib/generators/solidus_stripe/install/templates/app/views/checkouts/existing_payment/_stripe.html.erb +16 -0
  61. data/lib/generators/solidus_stripe/install/templates/app/views/checkouts/existing_payment/stripe/_card.html.erb +8 -0
  62. data/lib/generators/solidus_stripe/install/templates/app/views/checkouts/existing_payment/stripe/_default.html.erb +7 -0
  63. data/lib/generators/solidus_stripe/install/templates/app/views/checkouts/payment/_stripe.html.erb +39 -0
  64. data/lib/generators/solidus_stripe/install/templates/app/views/orders/payment_info/_stripe.html.erb +20 -0
  65. data/lib/generators/solidus_stripe/install/templates/config/initializers/solidus_stripe.rb +31 -0
  66. data/lib/solidus_stripe/configuration.rb +24 -3
  67. data/lib/solidus_stripe/engine.rb +19 -6
  68. data/lib/solidus_stripe/money_to_stripe_amount_converter.rb +109 -0
  69. data/lib/solidus_stripe/refunds_synchronizer.rb +96 -0
  70. data/lib/solidus_stripe/seeds.rb +19 -0
  71. data/lib/solidus_stripe/testing_support/factories.rb +153 -0
  72. data/lib/solidus_stripe/version.rb +1 -1
  73. data/lib/solidus_stripe/webhook/event.rb +90 -0
  74. data/lib/solidus_stripe.rb +0 -2
  75. data/solidus_stripe.gemspec +29 -6
  76. data/spec/lib/solidus_stripe/configuration_spec.rb +21 -0
  77. data/spec/lib/solidus_stripe/money_to_stripe_amount_converter_spec.rb +133 -0
  78. data/spec/lib/solidus_stripe/refunds_synchronizer_spec.rb +238 -0
  79. data/spec/lib/solidus_stripe/seeds_spec.rb +43 -0
  80. data/spec/lib/solidus_stripe/webhook/event_spec.rb +134 -0
  81. data/spec/models/concerns/solidus_stripe/log_entries_spec.rb +54 -0
  82. data/spec/models/solidus_stripe/customer_spec.rb +47 -0
  83. data/spec/models/solidus_stripe/gateway_spec.rb +283 -0
  84. data/spec/models/solidus_stripe/payment_intent_spec.rb +17 -0
  85. data/spec/models/solidus_stripe/payment_method_spec.rb +137 -0
  86. data/spec/models/solidus_stripe/payment_source_spec.rb +25 -0
  87. data/spec/requests/solidus_stripe/intents_controller_spec.rb +29 -0
  88. data/spec/requests/solidus_stripe/webhooks_controller/charge/refunded_spec.rb +31 -0
  89. data/spec/requests/solidus_stripe/webhooks_controller/payment_intent/canceled_spec.rb +23 -0
  90. data/spec/requests/solidus_stripe/webhooks_controller/payment_intent/payment_failed_spec.rb +23 -0
  91. data/spec/requests/solidus_stripe/webhooks_controller/payment_intent/succeeded_spec.rb +29 -0
  92. data/spec/requests/solidus_stripe/webhooks_controller_spec.rb +52 -0
  93. data/spec/solidus_stripe_spec_helper.rb +10 -0
  94. data/spec/subscribers/solidus_stripe/webhook/charge_subscriber_spec.rb +33 -0
  95. data/spec/subscribers/solidus_stripe/webhook/payment_intent_subscriber_spec.rb +297 -0
  96. data/spec/support/solidus_stripe/backend_test_helper.rb +210 -0
  97. data/spec/support/solidus_stripe/checkout_test_helper.rb +339 -0
  98. data/spec/support/solidus_stripe/factories.rb +5 -0
  99. data/spec/support/solidus_stripe/webhook/data_fixtures.rb +106 -0
  100. data/spec/support/solidus_stripe/webhook/event_with_context_factory.rb +82 -0
  101. data/spec/support/solidus_stripe/webhook/request_helper.rb +32 -0
  102. data/spec/system/backend/solidus_stripe/orders/payments_spec.rb +119 -0
  103. data/spec/system/frontend/.keep +0 -0
  104. data/spec/system/frontend/solidus_stripe/checkout_spec.rb +187 -0
  105. data/tmp/.keep +0 -0
  106. metadata +202 -78
  107. data/.rubocop_todo.yml +0 -298
  108. data/.travis.yml +0 -28
  109. data/app/assets/javascripts/spree/frontend/solidus_stripe/stripe-cart-page-checkout.js +0 -122
  110. data/app/assets/javascripts/spree/frontend/solidus_stripe/stripe-elements.js +0 -148
  111. data/app/assets/javascripts/spree/frontend/solidus_stripe/stripe-init.js +0 -20
  112. data/app/assets/javascripts/spree/frontend/solidus_stripe/stripe-payment-intents.js +0 -84
  113. data/app/assets/javascripts/spree/frontend/solidus_stripe/stripe-payment-request-button-shared.js +0 -160
  114. data/app/assets/javascripts/spree/frontend/solidus_stripe/stripe-payment.js +0 -16
  115. data/app/assets/javascripts/spree/frontend/solidus_stripe.js +0 -6
  116. data/app/controllers/solidus_stripe/payment_request_controller.rb +0 -52
  117. data/app/controllers/spree/stripe_controller.rb +0 -13
  118. data/app/decorators/models/spree/order_update_attributes_decorator.rb +0 -39
  119. data/app/decorators/models/spree/payment_decorator.rb +0 -11
  120. data/app/decorators/models/spree/refund_decorator.rb +0 -9
  121. data/app/models/solidus_stripe/address_from_params_service.rb +0 -72
  122. data/app/models/solidus_stripe/create_intents_payment_service.rb +0 -114
  123. data/app/models/solidus_stripe/prepare_order_for_payment_service.rb +0 -46
  124. data/app/models/solidus_stripe/shipping_rates_service.rb +0 -46
  125. data/app/models/spree/payment_method/stripe_credit_card.rb +0 -230
  126. data/bin/r +0 -13
  127. data/bin/sandbox_rails +0 -18
  128. data/db/migrate/20181010123508_update_stripe_payment_method_type_to_credit_card.rb +0 -21
  129. data/lib/assets/stylesheets/spree/frontend/solidus_stripe.scss +0 -11
  130. data/lib/solidus_stripe/testing_support/card_input_helper.rb +0 -34
  131. data/lib/tasks/solidus_stripe/db/seed.rake +0 -14
  132. data/lib/views/api/spree/api/payments/source_views/_stripe.json.jbuilder +0 -3
  133. data/lib/views/backend/spree/admin/log_entries/_stripe.html.erb +0 -28
  134. data/lib/views/backend/spree/admin/payments/source_forms/_stripe.html.erb +0 -1
  135. data/lib/views/backend/spree/admin/payments/source_views/_stripe.html.erb +0 -1
  136. data/lib/views/frontend/spree/checkout/existing_payment/_stripe.html.erb +0 -1
  137. data/lib/views/frontend/spree/checkout/payment/_stripe.html.erb +0 -8
  138. data/lib/views/frontend/spree/checkout/payment/v2/_javascript.html.erb +0 -78
  139. data/lib/views/frontend/spree/checkout/payment/v3/_elements.html.erb +0 -1
  140. data/lib/views/frontend/spree/checkout/payment/v3/_form_elements.html.erb +0 -40
  141. data/lib/views/frontend/spree/checkout/payment/v3/_intents.html.erb +0 -1
  142. data/lib/views/frontend/spree/checkout/payment/v3/_stripe.html.erb +0 -2
  143. data/lib/views/frontend/spree/orders/_stripe_payment_request_button.html.erb +0 -14
  144. data/spec/features/stripe_checkout_spec.rb +0 -486
  145. data/spec/models/solidus_stripe/address_from_params_service_spec.rb +0 -87
  146. data/spec/models/solidus_stripe/create_intents_payment_service_spec.rb +0 -127
  147. data/spec/models/solidus_stripe/prepare_order_for_payment_service_spec.rb +0 -65
  148. data/spec/models/solidus_stripe/shipping_rates_service_spec.rb +0 -54
  149. data/spec/models/spree/payment_method/stripe_credit_card_spec.rb +0 -354
  150. data/spec/requests/payment_requests_spec.rb +0 -152
  151. data/spec/solidus_frontend_app_template.rb +0 -17
  152. data/spec/spec_helper.rb +0 -37
  153. data/spec/support/solidus_address_helper.rb +0 -15
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "solidus_stripe/money_to_stripe_amount_converter"
4
+
5
+ module SolidusStripe
6
+ # Synchronizes refunds from Stripe to Solidus.
7
+ #
8
+ # For our use case, Stripe has two ways to inform us about refunds initiated
9
+ # on their side:
10
+ #
11
+ # 1. The `charge.refunded` webhook event, which is triggered when a refund is
12
+ # explicitly created.
13
+ # 2. The `payment_intent.succeeded` webhook event, which is triggered when a
14
+ # payment intent is captured. If the payment intent is captured for less than
15
+ # the full amount, a refund is automatically created for the remaining amount.
16
+ #
17
+ # In both cases, Stripe doesn't tell us which refund was recently created, so
18
+ # we need to fetch all the refunds for the payment intent and check if any of
19
+ # them is missing on Solidus. We're using the `transaction_id` field on
20
+ # `Spree::Refund` to match refunds against Stripe refunds ids. We could think
21
+ # about only syncing the single refund not present on Solidus, but we need to
22
+ # acknowledge concurrent partial refunds.
23
+ #
24
+ # The `Spree::RefundReason` with `SolidusStripe::Config.refund_reason_name`
25
+ # as name is used as created refunds' reason.
26
+ #
27
+ # Besides, we need to account for refunds created from Solidus admin panel,
28
+ # which calls the Stripe API. In this case, we need to avoid syncing the
29
+ # refund back to Solidus on the subsequent webhook, otherwise we would end up
30
+ # with duplicate records. We're marking those refunds with a metadata field on
31
+ # Stripe, so we can filter them out (see {Gateway#credit}).
32
+ class RefundsSynchronizer
33
+ include MoneyToStripeAmountConverter
34
+
35
+ # Metadata key used to mark refunds that shouldn't be synced back to Solidus.
36
+ # @return [Symbol]
37
+ SKIP_SYNC_METADATA_KEY = :solidus_skip_sync
38
+
39
+ # Metadata value used to mark refunds that shouldn't be synced back to Solidus.
40
+ # @return [String]
41
+ SKIP_SYNC_METADATA_VALUE = 'true'
42
+
43
+ # @param payment_method [SolidusStripe::PaymentMethod]
44
+ def initialize(payment_method)
45
+ @payment_method = payment_method
46
+ end
47
+
48
+ # @param payment_intent_id [String]
49
+ def call(payment_intent_id)
50
+ payment = Spree::Payment.find_by!(response_code: payment_intent_id)
51
+
52
+ stripe_refunds(payment_intent_id)
53
+ .select(&method(:stripe_refund_needs_sync?))
54
+ .map(
55
+ &method(:create_refund).curry[payment]
56
+ )
57
+ end
58
+
59
+ private
60
+
61
+ def stripe_refunds(payment_intent_id)
62
+ @payment_method.gateway.request do
63
+ Stripe::Refund.list(payment_intent: payment_intent_id).data
64
+ end
65
+ end
66
+
67
+ def stripe_refund_needs_sync?(refund)
68
+ originated_outside_solidus = refund.metadata[SKIP_SYNC_METADATA_KEY] != SKIP_SYNC_METADATA_VALUE
69
+ not_already_synced = Spree::Refund.find_by(transaction_id: refund.id).nil?
70
+
71
+ originated_outside_solidus && not_already_synced
72
+ end
73
+
74
+ def create_refund(payment, refund)
75
+ Spree::Refund.create!(
76
+ payment: payment,
77
+ amount: refund_decimal_amount(refund),
78
+ transaction_id: refund.id,
79
+ reason: SolidusStripe::PaymentMethod.refund_reason
80
+ ).tap(&method(:log_refund).curry[payment])
81
+ end
82
+
83
+ def log_refund(payment, refund)
84
+ SolidusStripe::LogEntries.payment_log(
85
+ payment,
86
+ success: true,
87
+ message: "Payment was refunded after Stripe event (#{refund.money})"
88
+ )
89
+ end
90
+
91
+ def refund_decimal_amount(refund)
92
+ to_solidus_amount(refund.amount, refund.currency)
93
+ .then { |amount| solidus_subunit_to_decimal(amount, refund.currency) }
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,19 @@
1
+ # frozen-string-literal: true
2
+
3
+ module SolidusStripe
4
+ # Seed data to seamlessly integrate Solidus with Stripe.
5
+ module Seeds
6
+ # Default name for the `Spree::RefundReason` used for Stripe refunds.
7
+ #
8
+ # Changed via {SolidusStripe::Configuration#refund_reason_name}.
9
+ #
10
+ # @return [String]
11
+ DEFAULT_STRIPE_REFUND_REASON_NAME = "Refund generated from Stripe"
12
+
13
+ def self.refund_reasons
14
+ Spree::RefundReason.find_or_create_by(
15
+ name: DEFAULT_STRIPE_REFUND_REASON_NAME
16
+ ) { _1.mutable = false }
17
+ end
18
+ end
19
+ end
@@ -1,4 +1,157 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  FactoryBot.define do
4
+ factory :stripe_payment_method, class: 'SolidusStripe::PaymentMethod' do
5
+ type { "SolidusStripe::PaymentMethod" }
6
+ name { "Stripe Payment Method" }
7
+ preferences {
8
+ {
9
+ api_key: ENV['SOLIDUS_STRIPE_API_KEY'] ||
10
+ "sk_dummy_#{SecureRandom.hex(24)}",
11
+ publishable_key: ENV['SOLIDUS_STRIPE_PUBLISHABLE_KEY'] ||
12
+ "pk_dummy_#{SecureRandom.hex(24)}",
13
+ webhook_endpoint_signing_secret: ENV['SOLIDUS_STRIPE_WEBHOOK_SIGNING_SECRET'] ||
14
+ "whsec_dummy_#{SecureRandom.hex(32)}"
15
+ }
16
+ }
17
+ end
18
+
19
+ factory :stripe_payment_source, class: 'SolidusStripe::PaymentSource' do
20
+ association :payment_method, factory: :stripe_payment_method
21
+ stripe_payment_method_id { "pm_#{SecureRandom.uuid.delete('-')}" }
22
+ end
23
+
24
+ factory :stripe_payment, parent: :payment do
25
+ association :payment_method, factory: :stripe_payment_method
26
+ amount { order.outstanding_balance }
27
+ response_code { "pi_#{SecureRandom.uuid.delete('-')}" }
28
+ state { 'checkout' }
29
+
30
+ transient do
31
+ stripe_payment_method_id { "pm_#{SecureRandom.uuid.delete('-')}" }
32
+ end
33
+
34
+ source {
35
+ create(
36
+ :stripe_payment_source,
37
+ payment_method: payment_method,
38
+ stripe_payment_method_id: stripe_payment_method_id
39
+ )
40
+ }
41
+
42
+ trait :authorized do
43
+ state { 'pending' }
44
+
45
+ after(:create) do |payment, _evaluator|
46
+ create(
47
+ :stripe_payment_log_entry,
48
+ :authorize,
49
+ source: payment
50
+ )
51
+ end
52
+ end
53
+
54
+ trait :captured do
55
+ state { 'completed' }
56
+
57
+ after(:create) do |payment, _evaluator|
58
+ create(
59
+ :stripe_payment_log_entry,
60
+ :autocapture,
61
+ source: payment
62
+ )
63
+ end
64
+ end
65
+ end
66
+
67
+ factory :stripe_payment_intent, class: 'SolidusStripe::PaymentIntent' do
68
+ association :order
69
+ association :payment_method, factory: :stripe_payment_method
70
+ stripe_intent_id { "pm_#{SecureRandom.uuid.delete('-')}" }
71
+ end
72
+
73
+ factory :stripe_slug_entry, class: 'SolidusStripe::SlugEntry' do
74
+ association :payment_method, factory: :stripe_payment_method
75
+ slug { SecureRandom.hex(16) }
76
+ end
77
+
78
+ factory :stripe_customer, class: 'SolidusStripe::Customer' do
79
+ association :payment_method, factory: :stripe_payment_method
80
+ association :source, factory: :user
81
+ stripe_id { "cus_#{SecureRandom.uuid.delete('-')}" }
82
+
83
+ trait :guest do
84
+ association :source, factory: :order, email: 'guest@example.com', user: nil
85
+ end
86
+ end
87
+
88
+ factory :stripe_payment_log_entry, class: 'Spree::LogEntry' do
89
+ transient do
90
+ success { true }
91
+ message { nil }
92
+ response_code { source.response_code }
93
+ data { nil }
94
+ end
95
+
96
+ source { create(:payment) }
97
+
98
+ trait :authorize do
99
+ message { 'PaymentIntent was confirmed successfully' }
100
+ end
101
+
102
+ trait :capture do
103
+ message { 'Payment captured successfully' }
104
+ end
105
+
106
+ trait :autocapture do
107
+ message { 'PaymentIntent was confirmed and captured successfully' }
108
+ end
109
+
110
+ trait :void do
111
+ message { 'PaymentIntent was canceled successfully' }
112
+ end
113
+
114
+ trait :refund do
115
+ message { 'PaymentIntent was refunded successfully' }
116
+ end
117
+
118
+ trait :fail do
119
+ success { false }
120
+ message { 'PaymentIntent operation failed with a generic error' }
121
+ end
122
+
123
+ details {
124
+ YAML.safe_dump(
125
+ SolidusStripe::LogEntries.build_payment_log(
126
+ success: success,
127
+ message: message,
128
+ response_code: response_code,
129
+ data: data
130
+ ),
131
+ permitted_classes: Spree::LogEntry.permitted_classes,
132
+ aliases: Spree::Config.log_entry_allow_aliases
133
+ )
134
+ }
135
+ end
136
+
137
+ factory :order_with_stripe_payment, parent: :order do
138
+ transient do
139
+ amount { 10 }
140
+ payment_method { build(:stripe_payment_method) }
141
+ stripe_payment_method_id { "pm_#{SecureRandom.uuid.delete('-')}" }
142
+ end
143
+
144
+ line_items { [build(:line_item, price: amount)] }
145
+
146
+ after(:create) do |order, evaluator|
147
+ build(
148
+ :stripe_payment,
149
+ amount: evaluator.amount,
150
+ order: order,
151
+ payment_method: evaluator.payment_method,
152
+ stripe_payment_method_id: evaluator.stripe_payment_method_id
153
+ )
154
+ order.recalculate
155
+ end
156
+ end
4
157
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SolidusStripe
4
- VERSION = '4.4.1'
4
+ VERSION = '5.0.0.alpha.1'
5
5
  end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusStripe
4
+ module Webhook
5
+ # Omnes event wrapping a Stripe event for a given payment method.
6
+ #
7
+ # All unknown methods are delegated to the wrapped Stripe event.
8
+ #
9
+ # @see https://www.rubydoc.info/gems/stripe/Stripe/Event
10
+ class Event
11
+ include Omnes::Event
12
+
13
+ PREFIX = "stripe."
14
+ private_constant :PREFIX
15
+
16
+ CORE_EVENTS = Set[*%i[
17
+ payment_intent.succeeded
18
+ payment_intent.payment_failed
19
+ payment_intent.canceled
20
+ charge.refunded
21
+ ]].freeze
22
+ private_constant :CORE_EVENTS
23
+
24
+ # @api private
25
+ class << self
26
+ def from_request(payload:, signature_header:, slug:, tolerance: default_tolerance)
27
+ payment_method = SolidusStripe::PaymentMethod.with_slug(slug).first!
28
+ stripe_event = Stripe::Webhook.construct_event(
29
+ payload,
30
+ signature_header,
31
+ payment_method.preferred_webhook_endpoint_signing_secret,
32
+ tolerance: tolerance
33
+ )
34
+ new(stripe_event: stripe_event, spree_payment_method: payment_method)
35
+ rescue ActiveRecord::RecordNotFound, Stripe::SignatureVerificationError, JSON::ParserError
36
+ nil
37
+ end
38
+
39
+ def register(user_events:, bus:, core_events: CORE_EVENTS)
40
+ (core_events + user_events).each do |event|
41
+ bus.register(:"stripe.#{event}")
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ def default_tolerance
48
+ SolidusStripe.configuration.webhook_signature_tolerance
49
+ end
50
+ end
51
+
52
+ # @api private
53
+ attr_reader :omnes_event_name
54
+
55
+ # @attr_reader [SolidusStripe::PaymentMethod]
56
+ attr_reader :spree_payment_method
57
+
58
+ # @api private
59
+ def initialize(stripe_event:, spree_payment_method:)
60
+ @stripe_event = stripe_event
61
+ @spree_payment_method = spree_payment_method
62
+ @omnes_event_name = :"#{PREFIX}#{stripe_event.type}"
63
+ end
64
+
65
+ # Serializable representation of the event.
66
+ #
67
+ # Ready to be consumed by async Omnes adapters, like
68
+ # `Omnes::Subscriber::Adapter::ActiveJob` or
69
+ # `Omnes::Subscriber::Adapter::Sidekiq`.
70
+ #
71
+ # @return [Hash<String, Object>]
72
+ def payload
73
+ {
74
+ "stripe_event" => @stripe_event.as_json,
75
+ "spree_payment_method_id" => @spree_payment_method.id
76
+ }
77
+ end
78
+
79
+ private
80
+
81
+ def method_missing(method_name, ...)
82
+ @stripe_event.send(method_name, ...)
83
+ end
84
+
85
+ def respond_to_missing?(...)
86
+ @stripe_event.respond_to?(...)
87
+ end
88
+ end
89
+ end
90
+ end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'active_merchant'
4
-
5
3
  require 'solidus_stripe/configuration'
6
4
  require 'solidus_stripe/version'
7
5
  require 'solidus_stripe/engine'
@@ -15,9 +15,9 @@ Gem::Specification.new do |spec|
15
15
 
16
16
  spec.metadata['homepage_uri'] = spec.homepage
17
17
  spec.metadata['source_code_uri'] = 'https://github.com/solidusio/solidus_stripe'
18
- spec.metadata['changelog_uri'] = 'https://github.com/solidusio/solidus_stripe/blob/master/CHANGELOG.md'
18
+ spec.metadata['changelog_uri'] = 'https://github.com/solidusio/solidus_stripe/releases'
19
19
 
20
- spec.required_ruby_version = Gem::Requirement.new('>= 2.5')
20
+ spec.required_ruby_version = Gem::Requirement.new('>= 3.0')
21
21
 
22
22
  # Specify which files should be added to the gem when it is released.
23
23
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
@@ -29,10 +29,33 @@ Gem::Specification.new do |spec|
29
29
  spec.executables = files.grep(%r{^exe/}) { |f| File.basename(f) }
30
30
  spec.require_paths = ["lib"]
31
31
 
32
- spec.add_dependency 'solidus_core', ['>= 3', '< 5']
32
+ spec.add_dependency 'solidus_core', ['>= 3.2', '< 4']
33
33
  spec.add_dependency 'solidus_support', '~> 0.8'
34
- spec.add_dependency 'activemerchant', '>= 1.105'
35
- spec.add_dependency 'rexml'
36
34
 
37
- spec.add_development_dependency 'solidus_dev_support', '~> 2.5'
35
+ spec.add_dependency 'stripe', '~> 8.0'
36
+
37
+ spec.add_development_dependency 'redcarpet' # used by yard
38
+ spec.add_development_dependency 'rubocop', '~> 1.0'
39
+ spec.add_development_dependency 'rubocop-performance', '~> 1.5'
40
+ spec.add_development_dependency 'rubocop-rails', '~> 2.3'
41
+ spec.add_development_dependency 'rubocop-rspec', '~> 2.0'
42
+ spec.add_development_dependency 'yard'
43
+
44
+ spec.post_install_message = <<~TXT
45
+ ------------------------------------------------------------------------
46
+ ⚠️ **WARNING: `solidus_stripe` & `solidus_frontend** ⚠️
47
+ ------------------------------------------------------------------------
48
+
49
+ If you need support for `solidus_frontend` please add `< 5`
50
+ as a version requirement in your gemfile:
51
+
52
+ `gem 'solidus_stripe', '< 5'`
53
+
54
+ or if your tracking the github version please switch to
55
+ the `v4` branch:
56
+
57
+ `gem 'solidus_stripe', github: 'solidusio/solidus_stripe', branch: 'v4'`
58
+
59
+ ------------------------------------------------------------------------
60
+ TXT
38
61
  end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "solidus_stripe_spec_helper"
4
+
5
+ RSpec.describe SolidusStripe::Configuration do
6
+ describe "#initialize" do
7
+ it "defaults webhook events to empty" do
8
+ expect(described_class.new.webhook_events).to eq([])
9
+ end
10
+
11
+ it "defaults webhook signature tolerance to stripe's default" do
12
+ expect(described_class.new.webhook_signature_tolerance).to eq(Stripe::Webhook::DEFAULT_TOLERANCE)
13
+ end
14
+
15
+ it "defaults refund reason name to the one in seeds" do
16
+ expect(
17
+ described_class.new.refund_reason_name
18
+ ).to eq(SolidusStripe::Seeds::DEFAULT_STRIPE_REFUND_REASON_NAME)
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'solidus_stripe_spec_helper'
4
+
5
+ # rubocop:disable Style/NumericLiterals
6
+ RSpec.describe SolidusStripe::MoneyToStripeAmountConverter do
7
+ describe '.to_stripe_amount' do
8
+ let(:custom_message) do
9
+ ->(actual, expected, currency) { "Expected #{currency} to be #{expected} but got #{actual}" }
10
+ end
11
+
12
+ context 'with zero decimal currencies' do
13
+ it 'returns the same unit amount' do
14
+ (described_class::ZERO_DECIMAL_CURRENCIES - %w[MGA]).each do |currency|
15
+ actual = described_class.to_stripe_amount(12345, currency).to_i
16
+
17
+ expected = 12345
18
+ expect(actual).to eq(expected), custom_message.call(actual, expected, currency)
19
+ end
20
+ end
21
+ end
22
+
23
+ context 'with three decimal currencies' do
24
+ it 'returns the same subunit amount' do
25
+ described_class::THREE_DECIMAL_CURRENCIES.each do |currency|
26
+ actual = described_class.to_stripe_amount(12_345, currency).to_i
27
+
28
+ expected = 12_345
29
+ expect(actual).to eq(expected), custom_message.call(actual, expected, currency)
30
+ end
31
+ end
32
+ end
33
+
34
+ context 'with special cases' do
35
+ it 'returns the amount divided by 5 (iraimbilanja to ariary) when currency is MGA' do
36
+ expect(described_class.to_stripe_amount(255, 'MGA').to_i).to eq(51)
37
+ end
38
+
39
+ it 'returns the amount as a two decimal currency when currency is HUF' do
40
+ expect(described_class.to_stripe_amount(1_2345, 'HUF').to_i).to eq(12_345_00)
41
+ end
42
+
43
+ it 'returns the same amount as a zero decimal currency when currency is UGX' do
44
+ expect(described_class.to_stripe_amount(12_3450_00, 'UGX').to_i).to eq(123_450_00)
45
+ end
46
+
47
+ it 'returns the same amount as a zero decimal currency when currency is TWD' do
48
+ expect(described_class.to_stripe_amount(1_2345_00, 'TWD').to_i).to eq(12_345_00)
49
+ end
50
+ end
51
+
52
+ context 'with default as two decimal currencies' do
53
+ it 'returns the same subunit amount' do
54
+ %w[USD EUR ILS MXN TWD].each do |currency|
55
+ actual = described_class.to_stripe_amount(123_45, currency).to_i
56
+
57
+ expected = 123_45
58
+ expect(actual).to eq(expected), custom_message.call(actual, expected, currency)
59
+ end
60
+ end
61
+ end
62
+ end
63
+
64
+ describe '.to_solidus_amount' do
65
+ let(:custom_message) do
66
+ ->(actual, expected, currency) { "Expected #{currency} to be #{expected} but got #{actual}" }
67
+ end
68
+
69
+ context 'with zero decimal currencies' do
70
+ it 'returns the same unit amount' do
71
+ (described_class::ZERO_DECIMAL_CURRENCIES - %w[MGA]).each do |currency|
72
+ actual = described_class.to_stripe_amount(12345, currency).to_i
73
+
74
+ expected = 12345
75
+ expect(actual).to eq(expected), custom_message.call(actual, expected, currency)
76
+ end
77
+ end
78
+ end
79
+
80
+ context 'with three decimal currencies' do
81
+ it 'returns the same subunit amount' do
82
+ described_class::THREE_DECIMAL_CURRENCIES.each do |currency|
83
+ actual = described_class.to_solidus_amount(12_345, currency).to_i
84
+
85
+ expected = 12_345
86
+ expect(actual).to eq(expected), custom_message.call(actual, expected, currency)
87
+ end
88
+ end
89
+ end
90
+
91
+ context 'with special cases' do
92
+ it 'returns the amount multiplied by 5 (ariary to iraimbilanja) when currency is MGA' do
93
+ expect(described_class.to_solidus_amount(51, 'MGA').to_i).to eq(255)
94
+ end
95
+
96
+ it 'returns the amount without the last two digits when currency is HUF' do
97
+ expect(described_class.to_solidus_amount(12_345_00, 'HUF').to_i).to eq(1_2345)
98
+ end
99
+
100
+ it 'returns the amount as a zero decimal currency when currency is UGX' do
101
+ expect(described_class.to_solidus_amount(123_450_00, 'UGX').to_i).to eq(12_3450_00)
102
+ end
103
+
104
+ it 'returns the amount as a zero decimal currency when currency is TWD' do
105
+ expect(described_class.to_solidus_amount(12_345_00, 'TWD').to_i).to eq(1_2345_00)
106
+ end
107
+ end
108
+
109
+ context 'with default as two decimal currencies' do
110
+ it 'returns the same subunit amount' do
111
+ %w[USD EUR ILS MXN TWD].each do |currency|
112
+ actual = described_class.to_solidus_amount(123_45, currency).to_i
113
+
114
+ expected = 123_45
115
+ expect(actual).to eq(expected), custom_message.call(actual, expected, currency)
116
+ end
117
+ end
118
+ end
119
+ end
120
+
121
+ describe '.solidus_decimal_to_subunit' do
122
+ it 'returns the subunit amount as an integer' do
123
+ expect(described_class.solidus_decimal_to_subunit(100, 'USD')).to be(100_00)
124
+ end
125
+ end
126
+
127
+ describe '.solidus_subunit_to_decimal' do
128
+ it 'returns to the unit amount as a decimal' do
129
+ expect(described_class.solidus_subunit_to_decimal(100_00, 'USD')).to eq(BigDecimal("100"))
130
+ end
131
+ end
132
+ end
133
+ # rubocop:enable Style/NumericLiterals