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.
- checksums.yaml +4 -4
- data/.circleci/config.yml +92 -52
- data/.gitignore +3 -0
- data/.rubocop.yml +94 -4
- data/.yardopts +1 -0
- data/Gemfile +10 -30
- data/LICENSE +2 -2
- data/Procfile.dev +3 -0
- data/README.md +145 -215
- data/Rakefile +5 -44
- data/app/assets/javascripts/spree/backend/solidus_stripe.js +2 -0
- data/app/assets/stylesheets/spree/backend/solidus_stripe.css +4 -0
- data/app/controllers/solidus_stripe/intents_controller.rb +36 -52
- data/app/controllers/solidus_stripe/webhooks_controller.rb +28 -0
- data/app/models/concerns/solidus_stripe/log_entries.rb +31 -0
- data/app/models/solidus_stripe/customer.rb +21 -0
- data/app/models/solidus_stripe/gateway.rb +231 -0
- data/app/models/solidus_stripe/payment_intent.rb +111 -0
- data/app/models/solidus_stripe/payment_method.rb +106 -0
- data/app/models/solidus_stripe/payment_source.rb +31 -0
- data/app/models/solidus_stripe/slug_entry.rb +20 -0
- data/app/models/solidus_stripe.rb +7 -0
- data/app/subscribers/solidus_stripe/webhook/charge_subscriber.rb +28 -0
- data/app/subscribers/solidus_stripe/webhook/payment_intent_subscriber.rb +112 -0
- data/app/views/spree/admin/payments/source_forms/_stripe.html.erb +29 -0
- data/app/views/spree/admin/payments/source_forms/existing_payment/_stripe.html.erb +14 -0
- data/app/views/spree/admin/payments/source_forms/existing_payment/stripe/_card.html.erb +8 -0
- data/app/views/spree/admin/payments/source_forms/existing_payment/stripe/_default.html.erb +7 -0
- data/app/views/spree/admin/payments/source_views/_stripe.html.erb +15 -0
- data/app/views/spree/api/payments/source_views/_stripe.json.jbuilder +8 -0
- data/bin/dev +13 -0
- data/bin/dummy-app +29 -0
- data/bin/rails +38 -3
- data/bin/rails-dummy-app +3 -0
- data/bin/rails-engine +1 -11
- data/bin/rails-new +55 -0
- data/bin/rails-sandbox +1 -14
- data/bin/rspec +10 -0
- data/bin/sandbox +12 -74
- data/bin/setup +1 -0
- data/bin/update-migrations +56 -0
- data/codecov.yml +12 -0
- data/config/locales/en.yml +16 -1
- data/config/routes.rb +5 -11
- data/coverage.rb +42 -0
- data/db/migrate/20230109183332_create_solidus_stripe_payment_sources.rb +10 -0
- data/db/migrate/20230303154931_create_solidus_stripe_setup_intent.rb +10 -0
- data/db/migrate/20230306105520_create_solidus_stripe_payment_intents.rb +10 -0
- data/db/migrate/20230308122414_create_solidus_stripe_webhook_endpoints.rb +10 -0
- data/db/migrate/20230310152615_add_payment_method_reference_to_stripe_intents.rb +6 -0
- data/db/migrate/20230310171444_normalize_stripe_intent_id_attributes.rb +6 -0
- data/db/migrate/20230313150008_create_solidus_stripe_customers.rb +16 -0
- data/db/migrate/20230323154931_drop_solidus_stripe_setup_intent.rb +13 -0
- data/db/migrate/20230403094916_rename_webhook_endpoint_to_payment_method_slug_entries.rb +5 -0
- data/db/seeds.rb +6 -24
- data/lib/generators/solidus_stripe/install/install_generator.rb +121 -14
- data/lib/generators/solidus_stripe/install/templates/app/assets/stylesheets/spree/frontend/solidus_stripe.css +13 -0
- data/lib/generators/solidus_stripe/install/templates/app/javascript/controllers/solidus_stripe_confirm_controller.js +39 -0
- data/lib/generators/solidus_stripe/install/templates/app/javascript/controllers/solidus_stripe_payment_controller.js +89 -0
- data/lib/generators/solidus_stripe/install/templates/app/views/checkouts/existing_payment/_stripe.html.erb +16 -0
- data/lib/generators/solidus_stripe/install/templates/app/views/checkouts/existing_payment/stripe/_card.html.erb +8 -0
- data/lib/generators/solidus_stripe/install/templates/app/views/checkouts/existing_payment/stripe/_default.html.erb +7 -0
- data/lib/generators/solidus_stripe/install/templates/app/views/checkouts/payment/_stripe.html.erb +39 -0
- data/lib/generators/solidus_stripe/install/templates/app/views/orders/payment_info/_stripe.html.erb +20 -0
- data/lib/generators/solidus_stripe/install/templates/config/initializers/solidus_stripe.rb +31 -0
- data/lib/solidus_stripe/configuration.rb +24 -3
- data/lib/solidus_stripe/engine.rb +19 -6
- data/lib/solidus_stripe/money_to_stripe_amount_converter.rb +109 -0
- data/lib/solidus_stripe/refunds_synchronizer.rb +96 -0
- data/lib/solidus_stripe/seeds.rb +19 -0
- data/lib/solidus_stripe/testing_support/factories.rb +153 -0
- data/lib/solidus_stripe/version.rb +1 -1
- data/lib/solidus_stripe/webhook/event.rb +90 -0
- data/lib/solidus_stripe.rb +0 -2
- data/solidus_stripe.gemspec +29 -6
- data/spec/lib/solidus_stripe/configuration_spec.rb +21 -0
- data/spec/lib/solidus_stripe/money_to_stripe_amount_converter_spec.rb +133 -0
- data/spec/lib/solidus_stripe/refunds_synchronizer_spec.rb +238 -0
- data/spec/lib/solidus_stripe/seeds_spec.rb +43 -0
- data/spec/lib/solidus_stripe/webhook/event_spec.rb +134 -0
- data/spec/models/concerns/solidus_stripe/log_entries_spec.rb +54 -0
- data/spec/models/solidus_stripe/customer_spec.rb +47 -0
- data/spec/models/solidus_stripe/gateway_spec.rb +283 -0
- data/spec/models/solidus_stripe/payment_intent_spec.rb +17 -0
- data/spec/models/solidus_stripe/payment_method_spec.rb +137 -0
- data/spec/models/solidus_stripe/payment_source_spec.rb +25 -0
- data/spec/requests/solidus_stripe/intents_controller_spec.rb +29 -0
- data/spec/requests/solidus_stripe/webhooks_controller/charge/refunded_spec.rb +31 -0
- data/spec/requests/solidus_stripe/webhooks_controller/payment_intent/canceled_spec.rb +23 -0
- data/spec/requests/solidus_stripe/webhooks_controller/payment_intent/payment_failed_spec.rb +23 -0
- data/spec/requests/solidus_stripe/webhooks_controller/payment_intent/succeeded_spec.rb +29 -0
- data/spec/requests/solidus_stripe/webhooks_controller_spec.rb +52 -0
- data/spec/solidus_stripe_spec_helper.rb +10 -0
- data/spec/subscribers/solidus_stripe/webhook/charge_subscriber_spec.rb +33 -0
- data/spec/subscribers/solidus_stripe/webhook/payment_intent_subscriber_spec.rb +297 -0
- data/spec/support/solidus_stripe/backend_test_helper.rb +210 -0
- data/spec/support/solidus_stripe/checkout_test_helper.rb +339 -0
- data/spec/support/solidus_stripe/factories.rb +5 -0
- data/spec/support/solidus_stripe/webhook/data_fixtures.rb +106 -0
- data/spec/support/solidus_stripe/webhook/event_with_context_factory.rb +82 -0
- data/spec/support/solidus_stripe/webhook/request_helper.rb +32 -0
- data/spec/system/backend/solidus_stripe/orders/payments_spec.rb +119 -0
- data/spec/system/frontend/.keep +0 -0
- data/spec/system/frontend/solidus_stripe/checkout_spec.rb +187 -0
- data/tmp/.keep +0 -0
- metadata +202 -78
- data/.rubocop_todo.yml +0 -298
- data/.travis.yml +0 -28
- data/app/assets/javascripts/spree/frontend/solidus_stripe/stripe-cart-page-checkout.js +0 -122
- data/app/assets/javascripts/spree/frontend/solidus_stripe/stripe-elements.js +0 -148
- data/app/assets/javascripts/spree/frontend/solidus_stripe/stripe-init.js +0 -20
- data/app/assets/javascripts/spree/frontend/solidus_stripe/stripe-payment-intents.js +0 -84
- data/app/assets/javascripts/spree/frontend/solidus_stripe/stripe-payment-request-button-shared.js +0 -160
- data/app/assets/javascripts/spree/frontend/solidus_stripe/stripe-payment.js +0 -16
- data/app/assets/javascripts/spree/frontend/solidus_stripe.js +0 -6
- data/app/controllers/solidus_stripe/payment_request_controller.rb +0 -52
- data/app/controllers/spree/stripe_controller.rb +0 -13
- data/app/decorators/models/spree/order_update_attributes_decorator.rb +0 -39
- data/app/decorators/models/spree/payment_decorator.rb +0 -11
- data/app/decorators/models/spree/refund_decorator.rb +0 -9
- data/app/models/solidus_stripe/address_from_params_service.rb +0 -72
- data/app/models/solidus_stripe/create_intents_payment_service.rb +0 -114
- data/app/models/solidus_stripe/prepare_order_for_payment_service.rb +0 -46
- data/app/models/solidus_stripe/shipping_rates_service.rb +0 -46
- data/app/models/spree/payment_method/stripe_credit_card.rb +0 -230
- data/bin/r +0 -13
- data/bin/sandbox_rails +0 -18
- data/db/migrate/20181010123508_update_stripe_payment_method_type_to_credit_card.rb +0 -21
- data/lib/assets/stylesheets/spree/frontend/solidus_stripe.scss +0 -11
- data/lib/solidus_stripe/testing_support/card_input_helper.rb +0 -34
- data/lib/tasks/solidus_stripe/db/seed.rake +0 -14
- data/lib/views/api/spree/api/payments/source_views/_stripe.json.jbuilder +0 -3
- data/lib/views/backend/spree/admin/log_entries/_stripe.html.erb +0 -28
- data/lib/views/backend/spree/admin/payments/source_forms/_stripe.html.erb +0 -1
- data/lib/views/backend/spree/admin/payments/source_views/_stripe.html.erb +0 -1
- data/lib/views/frontend/spree/checkout/existing_payment/_stripe.html.erb +0 -1
- data/lib/views/frontend/spree/checkout/payment/_stripe.html.erb +0 -8
- data/lib/views/frontend/spree/checkout/payment/v2/_javascript.html.erb +0 -78
- data/lib/views/frontend/spree/checkout/payment/v3/_elements.html.erb +0 -1
- data/lib/views/frontend/spree/checkout/payment/v3/_form_elements.html.erb +0 -40
- data/lib/views/frontend/spree/checkout/payment/v3/_intents.html.erb +0 -1
- data/lib/views/frontend/spree/checkout/payment/v3/_stripe.html.erb +0 -2
- data/lib/views/frontend/spree/orders/_stripe_payment_request_button.html.erb +0 -14
- data/spec/features/stripe_checkout_spec.rb +0 -486
- data/spec/models/solidus_stripe/address_from_params_service_spec.rb +0 -87
- data/spec/models/solidus_stripe/create_intents_payment_service_spec.rb +0 -127
- data/spec/models/solidus_stripe/prepare_order_for_payment_service_spec.rb +0 -65
- data/spec/models/solidus_stripe/shipping_rates_service_spec.rb +0 -54
- data/spec/models/spree/payment_method/stripe_credit_card_spec.rb +0 -354
- data/spec/requests/payment_requests_spec.rb +0 -152
- data/spec/solidus_frontend_app_template.rb +0 -17
- data/spec/spec_helper.rb +0 -37
- 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
|
|
@@ -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
|
data/lib/solidus_stripe.rb
CHANGED
data/solidus_stripe.gemspec
CHANGED
|
@@ -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/
|
|
18
|
+
spec.metadata['changelog_uri'] = 'https://github.com/solidusio/solidus_stripe/releases'
|
|
19
19
|
|
|
20
|
-
spec.required_ruby_version = Gem::Requirement.new('>=
|
|
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', '<
|
|
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.
|
|
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
|