solidus_stripe 4.4.1 → 5.0.0.alpha.1

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -3,30 +3,137 @@
3
3
  module SolidusStripe
4
4
  module Generators
5
5
  class InstallGenerator < Rails::Generators::Base
6
- class_option :auto_run_migrations, type: :boolean, default: false
6
+ class_option :migrations, type: :boolean, default: true
7
+ class_option :core, type: :boolean, default: true
8
+ class_option :backend, type: :boolean, default: true
9
+ class_option :starter_frontend, type: :boolean, default: true
7
10
 
8
- def add_javascripts
9
- append_file 'vendor/assets/javascripts/spree/frontend/all.js', "//= require spree/frontend/solidus_stripe\n"
11
+ class_option :migrate, type: :boolean, default: true
12
+ class_option :load_seeds, type: :boolean, default: true
13
+ class_option :watch, type: :boolean, default: false, hide: true
14
+ class_option :sync, type: :boolean, default: false, hide: true
15
+
16
+ # This is only used to run all-specs during development and CI, regular installation limits
17
+ # installed specs to frontend, which are the ones related to code copied to the target application.
18
+ class_option :specs, type: :string, enum: %w[all frontend], default: 'frontend', hide: true
19
+
20
+ source_root File.expand_path('templates', __dir__)
21
+
22
+ def install_migrations
23
+ return if options[:sync]
24
+
25
+ say_status :install, "[#{engine.engine_name}] migrations", :blue
26
+ shell.indent do
27
+ rake 'railties:install:migrations FROM=solidus_stripe'
28
+ run 'bin/rails db:migrate' if options[:migrate]
29
+ end
30
+ end
31
+
32
+ def install_solidus_core_support
33
+ support_code_for(:core) do
34
+ directory 'config/initializers', 'config/initializers'
35
+ route "mount SolidusStripe::Engine, at: '/solidus_stripe'"
36
+ end
37
+ end
38
+
39
+ def install_solidus_backend_support
40
+ support_code_for(:backend) do
41
+ append_file(
42
+ 'vendor/assets/javascripts/spree/backend/all.js',
43
+ "//= require spree/backend/solidus_stripe\n"
44
+ )
45
+ inject_into_file(
46
+ 'vendor/assets/stylesheets/spree/backend/all.css',
47
+ " *= require spree/backend/solidus_stripe\n",
48
+ before: %r{\*/},
49
+ verbose: true,
50
+ )
51
+ end
10
52
  end
11
53
 
12
- def add_migrations
13
- run 'bin/rails railties:install:migrations FROM=solidus_stripe'
54
+ def install_solidus_starter_frontend_support
55
+ support_code_for(:starter_frontend) do
56
+ directory 'app', 'app'
57
+ inject_into_file(
58
+ 'app/assets/stylesheets/solidus_starter_frontend.css',
59
+ " *= require spree/frontend/solidus_stripe\n",
60
+ before: %r{\*/},
61
+ verbose: true,
62
+ )
63
+
64
+ spec_paths =
65
+ case options[:specs]
66
+ when 'all' then %w[spec]
67
+ when 'frontend'
68
+ %w[
69
+ spec/solidus_stripe_spec_helper.rb
70
+ spec/system/frontend
71
+ spec/support
72
+ ]
73
+ end
74
+
75
+ spec_paths.each do |path|
76
+ if engine.root.join(path).directory?
77
+ directory engine.root.join(path), path
78
+ else
79
+ template engine.root.join(path), path
80
+ end
81
+ end
82
+
83
+ run 'bin/importmap pin @stripe/stripe-js'
84
+ end
14
85
  end
15
86
 
16
- def run_migrations
17
- run_migrations = options[:auto_run_migrations] || ENV['CI'] || ['', 'y', 'Y'].include?(ask('Would you like to run the migrations now? [Y/n]')) # rubocop:disable Layout/LineLength
18
- if run_migrations
19
- run 'bin/rails db:migrate'
87
+ def load_seeds
88
+ if options[:migrate] && options[:load_seeds]
89
+ say_status :load, "seed data", :blue
90
+ append_file "db/seeds.rb", <<~RUBY
91
+ #{engine.name}.load_seed
92
+ RUBY
93
+ engine.load_seed
20
94
  else
21
- puts 'Skipping bin/rails db:migrate, don\'t forget to run it!' # rubocop:disable Rails/Output
95
+ say_status :skip, "loading seed data", :blue
96
+ end
97
+ end
98
+
99
+ def watch
100
+ return unless options[:watch]
101
+
102
+ glob = "#{::SolidusStripe::Engine.root}/{app,lib,config}"
103
+ say_status :watch, "starting watcher... #{glob}", :cyan
104
+
105
+ require 'listen'
106
+ listener = Listen.to(*Dir[glob], relative: true) do |*changes|
107
+ say_status :watch, "changed: #{changes.flatten.join(', ')}", :cyan
108
+ shell.indent do
109
+ install_solidus_core_support
110
+ install_solidus_backend_support
111
+ install_solidus_starter_frontend_support
112
+ end
113
+ say_status :watch, "update completed", :cyan
22
114
  end
115
+ listener.start
116
+ sleep
117
+ rescue Interrupt
118
+ say_status :watch, "stopping watcher...", :cyan
119
+ listener.stop
120
+ rescue LoadError
121
+ say_status :error, 'in order for the --watch option to work you need the "listen" gem in your Gemfile', :red
23
122
  end
24
123
 
25
- def populate_seed_data
26
- return unless options.auto_run_seeds?
124
+ private
125
+
126
+ def support_code_for(component_name, &block)
127
+ if options[component_name]
128
+ say_status :install, "[#{engine.engine_name}] solidus_#{component_name}", :blue
129
+ shell.indent(&block)
130
+ else
131
+ say_status :skip, "[#{engine.engine_name}] solidus_#{component_name}", :blue
132
+ end
133
+ end
27
134
 
28
- say_status :loading, 'stripe seed data'
29
- rake('db:seed:solidus_stripe')
135
+ def engine
136
+ ::SolidusStripe::Engine
30
137
  end
31
138
  end
32
139
  end
@@ -0,0 +1,13 @@
1
+ /*
2
+ Placeholder manifest file.
3
+ the installer will append this file to the app vendored assets here: 'vendor/assets/stylesheets/spree/frontend/all.css'
4
+ */
5
+
6
+ .solidus-stripe-payment {
7
+ padding: 1em 0;
8
+ }
9
+
10
+ [type="submit"]:disabled {
11
+ opacity: 0.5;
12
+ cursor: not-allowed;
13
+ }
@@ -0,0 +1,39 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+ import { loadStripe } from "@stripe/stripe-js"
3
+
4
+ export default class extends Controller {
5
+ static values = {
6
+ clientSecret: String,
7
+ publishableKey: String,
8
+ returnUrl: String,
9
+ errorBaseUrl: String,
10
+ }
11
+
12
+ async connect() {
13
+ this.stripe = await loadStripe(this.publishableKeyValue)
14
+ }
15
+
16
+ // action
17
+ async confirm(e) {
18
+ // Bail out if not on the confirm method form.
19
+ if (e.target !== this.element.form) return
20
+
21
+ e.preventDefault()
22
+
23
+ const { error } = await this.stripe.confirmPayment({
24
+ clientSecret: this.clientSecretValue,
25
+ confirmParams: { return_url: this.returnUrlValue },
26
+ })
27
+
28
+ if (error) {
29
+ // This point will only be reached if there is an immediate error when
30
+ // confirming the payment. Show error to your customer.
31
+ const messageParam = `error_message=${encodeURIComponent(error.message)}`
32
+ location.href = `${this.errorBaseUrlValue}&${messageParam}`
33
+ } else {
34
+ // Your customer will be redirected to your `return_url`. For some payment
35
+ // methods like iDEAL, your customer will be redirected to an intermediate
36
+ // site first to authorize the payment, then redirected to the `return_url`.
37
+ }
38
+ }
39
+ }
@@ -0,0 +1,89 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+ import { loadStripe } from "@stripe/stripe-js"
3
+
4
+ export default class extends Controller {
5
+ static values = {
6
+ clientSecret: String,
7
+ publishableKey: String,
8
+ paymentElementOptions: Object,
9
+
10
+ // For now we don't have a controller to interact with
11
+ // and we can't use outlets, so we fallback on acquiring selectors.
12
+ submitSelector: String,
13
+ radioSelector: String,
14
+ }
15
+
16
+ static targets = ["paymentElement", "message", "paymentMethodInput"]
17
+
18
+ get submitOutletElement() {
19
+ return document.querySelector(this.submitSelectorValue)
20
+ }
21
+
22
+ get radioOutletElement() {
23
+ return document.querySelector(this.radioSelectorValue)
24
+ }
25
+
26
+ async connect() {
27
+ this.stripe = await loadStripe(this.publishableKeyValue)
28
+ this.setupPaymentElement()
29
+ }
30
+
31
+ // @action
32
+ async handleSubmit(e) {
33
+ // Bail out if not on the payment method form.
34
+ if (e.target !== this.radioOutletElement.form) return
35
+
36
+ // Bail out if the current payment method is not selected.
37
+ if (!this.radioOutletElement.checked) return
38
+
39
+ e.preventDefault()
40
+
41
+ this.setLoading(true)
42
+
43
+ // Trigger form validation and wallet collection
44
+ const { error: submitError } = await this.elements.submit()
45
+
46
+ if (submitError) {
47
+ this.messageTarget.textContent = submitError.message
48
+ this.setLoading(false)
49
+ return
50
+ }
51
+
52
+ // Create the PaymentMethod using the details collected by the Payment Element
53
+ const { error, paymentMethod } = await this.stripe.createPaymentMethod({
54
+ elements: this.elements,
55
+ params: { billing_details: {} },
56
+ })
57
+
58
+ if (error) {
59
+ if (error.type === "card_error" || error.type === "validation_error") {
60
+ this.messageTarget.textContent = error.message
61
+ } else {
62
+ this.messageTarget.textContent = "An unexpected error occurred."
63
+ }
64
+ this.setLoading(false)
65
+ return
66
+ }
67
+
68
+ this.setLoading(false)
69
+ this.paymentMethodInputTarget.value = paymentMethod.id
70
+ this.paymentMethodInputTarget.form.submit()
71
+ }
72
+
73
+ setupPaymentElement() {
74
+ this.elements = this.stripe.elements(this.paymentElementOptionsValue)
75
+ this.paymentElement = this.elements.create("payment", { layout: "tabs" })
76
+ this.paymentElementTarget.innerHTML = "" // Remove child nodes used for loading
77
+ this.paymentElement.mount(this.paymentElementTarget)
78
+ }
79
+
80
+ setLoading(isLoading) {
81
+ const element = this.submitOutletElement
82
+
83
+ if (isLoading) {
84
+ element.setAttribute("disabled", "")
85
+ } else {
86
+ element.removeAttribute("disabled")
87
+ }
88
+ }
89
+ }
@@ -0,0 +1,16 @@
1
+ <%
2
+ stripe_payment_method = wallet_payment_source.payment_source.stripe_payment_method
3
+
4
+ # https://stripe.com/docs/api/payment_methods/object#payment_method_object-type
5
+ partial_base = "checkouts/existing_payment/stripe"
6
+ payment_type = stripe_payment_method.type
7
+
8
+ # Fallback on the default partial if a specialized partial is not available.
9
+ payment_type = 'default' if lookup_context.find_all("#{partial_base}/_#{payment_type}").none?
10
+ %>
11
+
12
+ <div>
13
+ <label>
14
+ <%= render "#{partial_base}/#{payment_type}", stripe_payment_method: stripe_payment_method %>
15
+ </label>
16
+ </div>
@@ -0,0 +1,8 @@
1
+ <% card = stripe_payment_method.card %>
2
+ <%#
3
+ Add credit card logos:
4
+ https://support.stripe.com/questions/where-to-find-logos-for-accepted-credit-card-types
5
+ %>
6
+ 💳 <%= card.brand %>
7
+ <span>****&nbsp;****&nbsp;****&nbsp;<%= card.last4 %></span>
8
+ (<%= t 'spree.expiration' %>: <%= card.exp_month %>/<%= card.exp_year %>)
@@ -0,0 +1,7 @@
1
+ <% logger.error(
2
+ %{Can't find a partial for payments with type #{payment_type} } +
3
+ %{please add a partial named "#{partial_base}/_#{payment_type}.html.erb".}
4
+ ) %>
5
+
6
+ <%= stripe_payment_method.type.humanize %>
7
+ (<%= time_ago_in_words Time.at(stripe_payment_method.created) %>)
@@ -0,0 +1,39 @@
1
+ <% payment_element_options = {
2
+ mode: 'payment',
3
+ amount: SolidusStripe::MoneyToStripeAmountConverter.to_stripe_amount(
4
+ current_order.display_order_total_after_store_credit.money.fractional,
5
+ current_order.currency,
6
+ ),
7
+ currency: current_order.currency.downcase,
8
+ paymentMethodCreation: 'manual',
9
+ captureMethod: payment_method.auto_capture? ? 'automatic' : 'manual',
10
+ # Fully customizable with appearance API.
11
+ # https://stripe.com/docs/elements/appearance-api
12
+ # appearance: {},
13
+ } %>
14
+
15
+ <div
16
+ class="solidus-stripe-payment"
17
+ data-controller="solidus-stripe-payment"
18
+ data-solidus-stripe-payment-publishable-key-value="<%= payment_method.preferred_publishable_key %>"
19
+ data-solidus-stripe-payment-payment-element-options-value="<%= payment_element_options.to_json %>"
20
+ data-solidus-stripe-payment-radio-selector-value="#order_payments_attributes__payment_method_id_<%= payment_method.id %>"
21
+ data-solidus-stripe-payment-submit-selector-value="#checkout_form_payment [type='submit']"
22
+ data-action="submit@window->solidus-stripe-payment#handleSubmit"
23
+ >
24
+ <input
25
+ type="hidden"
26
+ name="payment_source[<%= payment_method.id %>][stripe_payment_method_id]"
27
+ value=""
28
+ data-solidus-stripe-payment-target="paymentMethodInput"
29
+ >
30
+
31
+ <div data-solidus-stripe-payment-target="paymentElement">
32
+ <!-- Elements will create form elements here -->
33
+ <em><%= t('solidus_stripe.loading') %></em>
34
+ </div>
35
+
36
+ <div data-solidus-stripe-payment-target="message">
37
+ <!-- Display error/notification messages to your customers here -->
38
+ </div>
39
+ </div>
@@ -0,0 +1,20 @@
1
+ <% stripe_payment_method = payment.source.stripe_payment_method %>
2
+ <%= render(
3
+ "checkouts/existing_payment/stripe/#{stripe_payment_method.type}",
4
+ stripe_payment_method: stripe_payment_method
5
+ ) %>
6
+
7
+ <% if current_order&.confirm? %>
8
+ <% stripe_intent =
9
+ SolidusStripe::PaymentIntent.prepare_for_payment(payment).stripe_intent %>
10
+
11
+ <input
12
+ type="hidden"
13
+ data-controller="solidus-stripe-confirm"
14
+ data-solidus-stripe-confirm-publishable-key-value="<%= payment.payment_method.preferred_publishable_key %>"
15
+ data-solidus-stripe-confirm-client-secret-value="<%= stripe_intent.client_secret %>"
16
+ data-solidus-stripe-confirm-return-url-value="<%= solidus_stripe.after_confirmation_url(payment.payment_method.slug) %>"
17
+ data-solidus-stripe-confirm-error-base-url-value="<%= solidus_stripe.after_confirmation_url(payment.payment_method.slug, payment_intent: stripe_intent.id) %>"
18
+ data-action="submit@window->solidus-stripe-confirm#confirm"
19
+ >
20
+ <% end %>
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ SolidusStripe.configure do |config|
4
+ # List of webhook events you want to handle.
5
+ # For instance, if you want to handle the `payment_intent.succeeded` event,
6
+ # you should add it to the list below. A corresponding
7
+ # `:"stripe.payment_intent.succeeded"` event will be published in `Spree::Bus`
8
+ # whenever a `payment_intent.succeeded` event is received from Stripe.
9
+ # config.webhook_events = %i[payment_intent.succeeded]
10
+ #
11
+ # Number of seconds while a webhook event is valid after its creation.
12
+ # Defaults to the same value as Stripe's default.
13
+ # config.webhook_signature_tolerance = 150
14
+ #
15
+ # Name of the `Spree::RefundReason` used for Stripe-generated refunds.
16
+ # Defaults to {SolidusStripe::DEFAULT_STRIPE_REFUND_REASON_NAME}. If you
17
+ # change it, make sure that the corresponding `Spree::RefundReason` exists in
18
+ # the database with that name.
19
+ # config.refund_reason_name = "Stripe refund"
20
+ end
21
+
22
+ if ENV['SOLIDUS_STRIPE_API_KEY']
23
+ Spree::Config.static_model_preferences.add(
24
+ 'SolidusStripe::PaymentMethod',
25
+ 'solidus_stripe_env_credentials',
26
+ api_key: ENV.fetch('SOLIDUS_STRIPE_API_KEY'),
27
+ publishable_key: ENV.fetch('SOLIDUS_STRIPE_PUBLISHABLE_KEY'),
28
+ test_mode: ENV.fetch('SOLIDUS_STRIPE_API_KEY').start_with?('sk_test_'),
29
+ webhook_endpoint_signing_secret: ENV.fetch('SOLIDUS_STRIPE_WEBHOOK_SIGNING_SECRET')
30
+ )
31
+ end
@@ -1,10 +1,31 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "stripe/webhook"
4
+ require "solidus_stripe/seeds"
5
+
3
6
  module SolidusStripe
4
7
  class Configuration
5
- # Define here the settings for this extension, e.g.:
6
- #
7
- # attr_accessor :my_setting
8
+ # @!attribute [rw] webhook_events
9
+ # @return [Array<Symbol>] stripe events to handle. You also need to
10
+ # register them in the Stripe dashboard. For an event `:foo`, a matching
11
+ # `:"stripe.foo"` event will be registered in `Spree::Bus`.
12
+ attr_accessor :webhook_events
13
+
14
+ # @!attribute [rw] webhook_signature_tolerance
15
+ # @return [Integer] number of seconds while a webhook event is valid after
16
+ # its creation. Defaults to `Stripe::Webhook::DEFAULT_TOLERANCE`.
17
+ attr_accessor :webhook_signature_tolerance
18
+
19
+ # @!attribute [rw] refund_reason_name
20
+ # @return [String] `Spree::RefundReason#name` used for Stripe-generated
21
+ # refunds. Defaults to {SolidusStripe::Seeds::DEFAULT_STRIPE_REFUND_REASON_NAME}.
22
+ attr_accessor :refund_reason_name
23
+
24
+ def initialize
25
+ @webhook_events = []
26
+ @webhook_signature_tolerance = Stripe::Webhook::DEFAULT_TOLERANCE
27
+ @refund_reason_name = Seeds::DEFAULT_STRIPE_REFUND_REASON_NAME
28
+ end
8
29
  end
9
30
 
10
31
  class << self
@@ -7,17 +7,30 @@ module SolidusStripe
7
7
  class Engine < Rails::Engine
8
8
  include SolidusSupport::EngineExtensions
9
9
 
10
- isolate_namespace ::Spree
11
-
10
+ isolate_namespace SolidusStripe
12
11
  engine_name 'solidus_stripe'
13
12
 
13
+ initializer "solidus_stripe.add_payment_method", after: "spree.register.payment_methods" do |app|
14
+ app.config.spree.payment_methods << 'SolidusStripe::PaymentMethod'
15
+
16
+ ::Spree::PermittedAttributes.source_attributes.prepend :stripe_payment_method_id
17
+ end
18
+
19
+ initializer "solidus_stripe.pub_sub", after: "spree.core.pub_sub" do |app|
20
+ require "solidus_stripe/webhook/event"
21
+ app.reloader.to_prepare do
22
+ SolidusStripe::Webhook::Event.register(
23
+ user_events: SolidusStripe.configuration.webhook_events,
24
+ bus: Spree::Bus
25
+ )
26
+ SolidusStripe::Webhook::PaymentIntentSubscriber.new.subscribe_to(Spree::Bus)
27
+ SolidusStripe::Webhook::ChargeSubscriber.new.subscribe_to(Spree::Bus)
28
+ end
29
+ end
30
+
14
31
  # use rspec for tests
15
32
  config.generators do |g|
16
33
  g.test_framework :rspec
17
34
  end
18
-
19
- initializer "spree.payment_method.add_stripe_credit_card", after: "spree.register.payment_methods" do |app|
20
- app.config.spree.payment_methods << "Spree::PaymentMethod::StripeCreditCard"
21
- end
22
35
  end
23
36
  end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Helpers to interoperate amounts between Solidus and Stripe.
4
+ #
5
+ # Solidus will provide a "fractional" amount, that is specific for each currency
6
+ # following the configuration defined in the Money gem.
7
+ #
8
+ # Stripe uses the "smallest currency unit", (e.g., 100 cents to charge $1.00 or
9
+ # 100 to charge ¥100, a zero-decimal currency).
10
+ #
11
+ # @see https://stripe.com/docs/currencies#zero-decimal
12
+ #
13
+ # We need to ensure the fractional amount is considering the same number of decimals.
14
+ module SolidusStripe::MoneyToStripeAmountConverter
15
+ extend ActiveSupport::Concern
16
+ extend self
17
+
18
+ # @api private
19
+ ZERO_DECIMAL_CURRENCIES = %w[
20
+ BIF
21
+ CLP
22
+ DJF
23
+ GNF
24
+ JPY
25
+ KMF
26
+ KRW
27
+ MGA
28
+ PYG
29
+ RWF
30
+ UGX
31
+ VND
32
+ VUV
33
+ XAF
34
+ XOF
35
+ XPF
36
+ ].freeze
37
+
38
+ # @api private
39
+ THREE_DECIMAL_CURRENCIES = %w[
40
+ BHD
41
+ JOD
42
+ KWD
43
+ OMR
44
+ TND
45
+ ].freeze
46
+
47
+ # @api private
48
+ #
49
+ # Special currencies that are represented in cents but should be
50
+ # divisible by 100, thus making them integer only.
51
+ DIVISIBLE_BY_100 = %w[
52
+ HUF
53
+ TWD
54
+ UGX
55
+ ].freeze
56
+
57
+ def to_stripe_amount(fractional, currency)
58
+ solidus_subunit_to_unit, stripe_subunit_to_unit = subunit_to_unit(currency)
59
+
60
+ if stripe_subunit_to_unit == solidus_subunit_to_unit
61
+ fractional
62
+ else
63
+ (fractional / solidus_subunit_to_unit.to_d) * stripe_subunit_to_unit.to_d
64
+ end
65
+ end
66
+
67
+ def to_solidus_amount(fractional, currency)
68
+ solidus_subunit_to_unit, stripe_subunit_to_unit = subunit_to_unit(currency)
69
+
70
+ if stripe_subunit_to_unit == solidus_subunit_to_unit
71
+ fractional
72
+ else
73
+ (fractional / stripe_subunit_to_unit.to_d) * solidus_subunit_to_unit.to_d
74
+ end
75
+ end
76
+
77
+ # Transforms a decimal amount into a subunit amount
78
+ #
79
+ # @param amount [BigDecimal]
80
+ # @param currency [String]
81
+ # @return [Integer]
82
+ def solidus_decimal_to_subunit(amount, currency)
83
+ Money.from_amount(amount, currency).fractional
84
+ end
85
+
86
+ # Transforms a subunit amount into a decimal amount
87
+ #
88
+ # @param amount [Integer]
89
+ # @param currency [String]
90
+ # @return [BigDecimal]
91
+ def solidus_subunit_to_decimal(amount, currency)
92
+ Money.new(amount, currency).to_d
93
+ end
94
+
95
+ private
96
+
97
+ def subunit_to_unit(currency)
98
+ solidus_subunit_to_unit = ::Money::Currency.new(currency).subunit_to_unit
99
+ stripe_subunit_to_unit =
100
+ case currency.to_s.upcase
101
+ when *ZERO_DECIMAL_CURRENCIES then 1
102
+ when *THREE_DECIMAL_CURRENCIES then 1000
103
+ when *DIVISIBLE_BY_100 then 100
104
+ else 100
105
+ end
106
+
107
+ [solidus_subunit_to_unit, stripe_subunit_to_unit]
108
+ end
109
+ end