solidus_stripe 3.0.0 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.rubocop.yml +5 -0
  4. data/CHANGELOG.md +55 -0
  5. data/Gemfile +7 -0
  6. data/LICENSE +2 -2
  7. data/README.md +69 -11
  8. data/Rakefile +1 -1
  9. data/app/assets/javascripts/spree/frontend/solidus_stripe/stripe-cart-page-checkout.js +42 -9
  10. data/app/assets/javascripts/spree/frontend/solidus_stripe/stripe-elements.js +61 -25
  11. data/app/assets/javascripts/spree/frontend/solidus_stripe/stripe-payment-intents.js +4 -2
  12. data/app/assets/javascripts/spree/frontend/solidus_stripe/stripe-payment-request-button-shared.js +56 -19
  13. data/app/assets/javascripts/spree/frontend/solidus_stripe/stripe-payment.js +7 -1
  14. data/app/controllers/solidus_stripe/intents_controller.rb +42 -28
  15. data/app/decorators/models/spree/order_update_attributes_decorator.rb +39 -0
  16. data/app/decorators/models/spree/payment_decorator.rb +11 -0
  17. data/app/decorators/models/spree/refund_decorator.rb +9 -0
  18. data/app/models/solidus_stripe/address_from_params_service.rb +5 -2
  19. data/app/models/solidus_stripe/create_intents_payment_service.rb +113 -0
  20. data/app/models/spree/payment_method/stripe_credit_card.rb +19 -9
  21. data/bin/r +13 -0
  22. data/bin/rake +7 -0
  23. data/bin/sandbox +84 -0
  24. data/bin/sandbox_rails +18 -0
  25. data/bin/setup +1 -1
  26. data/config/routes.rb +4 -1
  27. data/lib/generators/solidus_stripe/install/install_generator.rb +7 -3
  28. data/lib/solidus_stripe/engine.rb +2 -2
  29. data/lib/solidus_stripe/factories.rb +4 -0
  30. data/lib/solidus_stripe/version.rb +1 -1
  31. data/lib/views/frontend/spree/checkout/payment/v3/_form_elements.html.erb +0 -1
  32. data/solidus_stripe.gemspec +34 -37
  33. data/spec/features/stripe_checkout_spec.rb +159 -64
  34. data/spec/models/solidus_stripe/address_from_params_service_spec.rb +19 -5
  35. data/spec/models/solidus_stripe/create_intents_payment_service_spec.rb +127 -0
  36. data/spec/models/spree/payment_method/stripe_credit_card_spec.rb +44 -1
  37. data/spec/spec_helper.rb +4 -1
  38. metadata +22 -12
  39. data/LICENSE.md +0 -26
@@ -83,36 +83,73 @@
83
83
  this.showError(response.error);
84
84
  this.completePaymentRequest(payment, 'fail');
85
85
  } else if (response.requires_action) {
86
- this.stripe.handleCardAction(
87
- response.stripe_payment_intent_client_secret
88
- ).then(this.onIntentsClientSecret.bind(this));
86
+ var clientSecret = response.stripe_payment_intent_client_secret;
87
+ var onConfirmCardPayment = this.onConfirmCardPayment.bind(this);
88
+
89
+ this.stripe.confirmCardPayment(
90
+ clientSecret,
91
+ {payment_method: payment.paymentMethod.id},
92
+ {handleActions: false}
93
+ ).then(function(confirmResult) {
94
+ onConfirmCardPayment(confirmResult, payment, clientSecret)
95
+ });
89
96
  } else {
90
- this.completePaymentRequest(payment, 'success');
91
- this.submitPayment(payment);
97
+ this.completePayment(payment, response.stripe_payment_intent_id)
92
98
  }
93
99
  },
94
100
 
95
- onIntentsClientSecret: function(result) {
96
- if (result.error) {
97
- this.showError(result.error);
101
+ onConfirmCardPayment: function(confirmResult, payment, clientSecret) {
102
+ onStripeResponse = function(response, payment) {
103
+ if (response.error) {
104
+ this.showError(response.error);
105
+ } else {
106
+ this.completePayment(payment, response.paymentIntent.id)
107
+ }
108
+ }.bind(this);
109
+
110
+ if (confirmResult.error) {
111
+ this.completePaymentRequest(payment, 'fail');
112
+ this.showError(confirmResult.error);
98
113
  } else {
99
- fetch('/stripe/confirm_intents', {
100
- method: 'POST',
101
- headers: { 'Content-Type': 'application/json' },
102
- body: JSON.stringify({
103
- spree_payment_method_id: this.config.id,
104
- stripe_payment_intent_id: result.paymentIntent.id,
105
- authenticity_token: this.authToken
106
- })
107
- }).then(function(confirmResult) {
108
- return confirmResult.json();
109
- }).then(this.handleServerResponse.bind(this));
114
+ this.completePaymentRequest(payment, 'success');
115
+ this.stripe.confirmCardPayment(clientSecret).then(function(response) {
116
+ onStripeResponse(response, payment);
117
+ });
110
118
  }
111
119
  },
112
120
 
121
+ completePayment: function(payment, stripePaymentIntentId) {
122
+ var onCreateBackendPayment = function (response) {
123
+ if (response.error) {
124
+ this.completePaymentRequest(payment, 'fail');
125
+ this.showError(response.error);
126
+ } else {
127
+ this.completePaymentRequest(payment, 'success');
128
+ this.submitPayment(payment);
129
+ }
130
+ }.bind(this);
131
+
132
+ fetch('/stripe/create_payment', {
133
+ method: 'POST',
134
+ headers: { 'Content-Type': 'application/json' },
135
+ body: JSON.stringify({
136
+ form_data: this.form ? this.form.serialize() : payment.shippingAddress,
137
+ spree_payment_method_id: this.config.id,
138
+ stripe_payment_intent_id: stripePaymentIntentId,
139
+ authenticity_token: this.authToken
140
+ })
141
+ }).then(function(solidusPaymentResponse) {
142
+ return solidusPaymentResponse.json();
143
+ }).then(onCreateBackendPayment)
144
+ },
145
+
113
146
  completePaymentRequest: function(payment, state) {
114
147
  if (payment && typeof payment.complete === 'function') {
115
148
  payment.complete(state);
149
+ if (state === 'fail') {
150
+ // restart the button (required in order to force address choice)
151
+ new SolidusStripe.CartPageCheckout().init();
152
+ }
116
153
  }
117
154
  }
118
155
  };
@@ -6,5 +6,11 @@ SolidusStripe.Payment = function() {
6
6
  this.authToken = $('meta[name="csrf-token"]').attr('content');
7
7
 
8
8
  this.stripe = Stripe(this.config.publishable_key);
9
- this.elements = this.stripe.elements({locale: 'en'});
9
+ this.elements = this.stripe.elements(this.elementsBaseOptions());
10
+ };
11
+
12
+ SolidusStripe.Payment.prototype.elementsBaseOptions = function () {
13
+ return {
14
+ locale: 'en'
15
+ };
10
16
  };
@@ -4,27 +4,23 @@ module SolidusStripe
4
4
  class IntentsController < Spree::BaseController
5
5
  include Spree::Core::ControllerHelpers::Order
6
6
 
7
- def confirm
8
- begin
9
- if params[:stripe_payment_method_id].present?
10
- intent = stripe.create_intent(
11
- (current_order.total * 100).to_i,
12
- params[:stripe_payment_method_id],
13
- currency: current_order.currency,
14
- confirmation_method: 'manual',
15
- confirm: true,
16
- setup_future_usage: 'on_session',
17
- metadata: { order_id: current_order.id }
18
- )
19
- elsif params[:stripe_payment_intent_id].present?
20
- intent = stripe.confirm_intent(params[:stripe_payment_intent_id], nil)
21
- end
22
- rescue Stripe::CardError => e
23
- render json: { error: e.message }, status: 500
24
- return
25
- end
7
+ def create_intent
8
+ @intent = create_payment_intent
9
+ generate_payment_response
10
+ end
11
+
12
+ def create_payment
13
+ create_payment_service = SolidusStripe::CreateIntentsPaymentService.new(
14
+ params[:stripe_payment_intent_id],
15
+ stripe,
16
+ self
17
+ )
26
18
 
27
- generate_payment_response(intent)
19
+ if create_payment_service.call
20
+ render json: { success: true }
21
+ else
22
+ render json: { error: "Could not create payment" }, status: 500
23
+ end
28
24
  end
29
25
 
30
26
  private
@@ -33,20 +29,38 @@ module SolidusStripe
33
29
  @stripe ||= Spree::PaymentMethod::StripeCreditCard.find(params[:spree_payment_method_id])
34
30
  end
35
31
 
36
- def generate_payment_response(intent)
37
- response = intent.params
32
+ def generate_payment_response
33
+ response = @intent.params
38
34
  # Note that if your API version is before 2019-02-11, 'requires_action'
39
35
  # appears as 'requires_source_action'.
40
36
  if %w[requires_source_action requires_action].include?(response['status']) && response['next_action']['type'] == 'use_stripe_sdk'
41
- render json: {
42
- requires_action: true,
43
- stripe_payment_intent_client_secret: response['client_secret']
44
- }
45
- elsif response['status'] == 'succeeded'
46
- render json: { success: true }
37
+ render json: {
38
+ requires_action: true,
39
+ stripe_payment_intent_client_secret: response['client_secret']
40
+ }
41
+ elsif response['status'] == 'requires_capture'
42
+ render json: {
43
+ success: true,
44
+ requires_capture: true,
45
+ stripe_payment_intent_id: response['id']
46
+ }
47
47
  else
48
48
  render json: { error: response['error']['message'] }, status: 500
49
49
  end
50
50
  end
51
+
52
+ def create_payment_intent
53
+ stripe.create_intent(
54
+ (current_order.total * 100).to_i,
55
+ params[:stripe_payment_method_id],
56
+ description: "Solidus Order ID: #{current_order.number} (pending)",
57
+ currency: current_order.currency,
58
+ confirmation_method: 'automatic',
59
+ capture_method: 'manual',
60
+ confirm: true,
61
+ setup_future_usage: 'off_session',
62
+ metadata: { order_id: current_order.id }
63
+ )
64
+ end
51
65
  end
52
66
  end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spree
4
+ module OrderUpdateAttributesDecorator
5
+ def assign_payments_attributes
6
+ return if payments_attributes.empty?
7
+ return if adding_new_stripe_payment_intents_card?
8
+
9
+ stripe_intents_pending_payments.each(&:void_transaction!)
10
+
11
+ super
12
+ end
13
+
14
+ private
15
+
16
+ def adding_new_stripe_payment_intents_card?
17
+ paying_with_stripe_intents? && stripe_intents_pending_payments.any?
18
+ end
19
+
20
+ def stripe_intents_pending_payments
21
+ @stripe_intents_pending_payments ||= order.payments.valid.select do |payment|
22
+ payment_method = payment.payment_method
23
+ payment.pending? && stripe_intents?(payment_method)
24
+ end
25
+ end
26
+
27
+ def paying_with_stripe_intents?
28
+ if id = payments_attributes.first&.dig(:payment_method_id)
29
+ stripe_intents?(Spree::PaymentMethod.find(id))
30
+ end
31
+ end
32
+
33
+ def stripe_intents?(payment_method)
34
+ payment_method.respond_to?(:v3_intents?) && payment_method.v3_intents?
35
+ end
36
+
37
+ ::Spree::OrderUpdateAttributes.prepend(self)
38
+ end
39
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spree
4
+ module PaymentDecorator
5
+ def gateway_order_identifier
6
+ gateway_order_id
7
+ end
8
+
9
+ ::Spree::Payment.prepend(self)
10
+ end
11
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spree
4
+ module RefundDecorator
5
+ attr_reader :response
6
+
7
+ Spree::Refund.prepend(self)
8
+ end
9
+ end
@@ -4,7 +4,7 @@ module SolidusStripe
4
4
  class AddressFromParamsService
5
5
  attr_reader :address_params, :user
6
6
 
7
- def initialize(address_params, user)
7
+ def initialize(address_params, user = nil)
8
8
  @address_params, @user = address_params, user
9
9
  end
10
10
 
@@ -43,7 +43,10 @@ module SolidusStripe
43
43
  end
44
44
 
45
45
  def state
46
- @state ||= country.states.find_by_abbr(address_params[:region])
46
+ @state ||= begin
47
+ region = address_params[:region]
48
+ country.states.find_by(abbr: region) || country.states.find_by(name: region)
49
+ end
47
50
  end
48
51
 
49
52
  def default_attributes
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusStripe
4
+ class CreateIntentsPaymentService
5
+ attr_reader :intent_id, :stripe, :controller
6
+
7
+ delegate :request, :current_order, :params, to: :controller
8
+
9
+ def initialize(intent_id, stripe, controller)
10
+ @intent_id, @stripe, @controller = intent_id, stripe, controller
11
+ end
12
+
13
+ def call
14
+ invalidate_previous_payment_intents_payments
15
+ if (payment = create_payment)
16
+ description = "Solidus Order ID: #{payment.gateway_order_identifier}"
17
+ stripe.update_intent(nil, intent_id, nil, description: description)
18
+ true
19
+ else
20
+ invalidate_current_payment_intent
21
+ false
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def intent
28
+ @intent ||= stripe.show_intent(intent_id, {})
29
+ end
30
+
31
+ def invalidate_current_payment_intent
32
+ stripe.cancel(intent_id)
33
+ end
34
+
35
+ def invalidate_previous_payment_intents_payments
36
+ if stripe.v3_intents?
37
+ current_order.payments.pending.where(payment_method: stripe).each(&:void_transaction!)
38
+ end
39
+ end
40
+
41
+ def create_payment
42
+ Spree::OrderUpdateAttributes.new(
43
+ current_order,
44
+ payment_params,
45
+ request_env: request.headers.env
46
+ ).apply
47
+
48
+ created_payment = Spree::Payment.find_by(response_code: intent_id)
49
+ created_payment&.tap { |payment| payment.update!(state: :pending) }
50
+ end
51
+
52
+ def payment_params
53
+ {
54
+ payments_attributes: [{
55
+ payment_method_id: stripe.id,
56
+ amount: current_order.total,
57
+ response_code: intent_id,
58
+ source_attributes: {
59
+ month: intent_card['exp_month'],
60
+ year: intent_card['exp_year'],
61
+ cc_type: intent_card['brand'],
62
+ last_digits: intent_card['last4'],
63
+ gateway_payment_profile_id: intent_customer_profile,
64
+ name: card_holder_name || address_full_name,
65
+ address_attributes: address_attributes
66
+ }
67
+ }]
68
+ }
69
+ end
70
+
71
+ def intent_card
72
+ intent_data['payment_method_details']['card']
73
+ end
74
+
75
+ def intent_customer_profile
76
+ intent.params['payment_method']
77
+ end
78
+
79
+ def card_holder_name
80
+ (html_payment_source_data['name'] || intent_data['billing_details']['name']).presence
81
+ end
82
+
83
+ def intent_data
84
+ intent.params['charges']['data'][0]
85
+ end
86
+
87
+ def form_data
88
+ params[:form_data]
89
+ end
90
+
91
+ def html_payment_source_data
92
+ if form_data.is_a?(String)
93
+ data = Rack::Utils.parse_nested_query(form_data)
94
+ data['payment_source'][stripe.id.to_s]
95
+ else
96
+ {}
97
+ end
98
+ end
99
+
100
+ def address_attributes
101
+ html_payment_source_data['address_attributes'] || SolidusStripe::AddressFromParamsService.new(form_data).call.attributes
102
+ end
103
+
104
+ def address_full_name
105
+ current_order.bill_address&.full_name || form_data[:recipient]
106
+ end
107
+
108
+ def update_stripe_payment_description
109
+ description = "Solidus Order ID: #{payment.gateway_order_identifier}"
110
+ stripe.update_intent(nil, intent_id, nil, description: description)
111
+ end
112
+ end
113
+ end
@@ -15,6 +15,8 @@ module Spree
15
15
  'Visa' => 'visa'
16
16
  }
17
17
 
18
+ delegate :create_intent, :update_intent, :confirm_intent, :show_intent, to: :gateway
19
+
18
20
  def stripe_config(order)
19
21
  {
20
22
  id: id,
@@ -59,14 +61,6 @@ module Spree
59
61
  true
60
62
  end
61
63
 
62
- def create_intent(*args)
63
- gateway.create_intent(*args)
64
- end
65
-
66
- def confirm_intent(*args)
67
- gateway.confirm_intent(*args)
68
- end
69
-
70
64
  def purchase(money, creditcard, transaction_options)
71
65
  gateway.purchase(*options_for_purchase_or_auth(money, creditcard, transaction_options))
72
66
  end
@@ -87,6 +81,21 @@ module Spree
87
81
  gateway.void(response_code, {})
88
82
  end
89
83
 
84
+ def payment_intents_refund_reason
85
+ Spree::RefundReason.where(name: Spree::Payment::Cancellation::DEFAULT_REASON).first_or_create
86
+ end
87
+
88
+ def try_void(payment)
89
+ if v3_intents? && payment.completed?
90
+ payment.refunds.create!(
91
+ amount: payment.credit_allowed,
92
+ reason: payment_intents_refund_reason
93
+ ).response
94
+ else
95
+ payment.void_transaction!
96
+ end
97
+ end
98
+
90
99
  def cancel(response_code)
91
100
  gateway.void(response_code, {})
92
101
  end
@@ -140,8 +149,9 @@ module Spree
140
149
 
141
150
  def options_for_purchase_or_auth(money, creditcard, transaction_options)
142
151
  options = {}
143
- options[:description] = "Spree Order ID: #{transaction_options[:order_id]}"
152
+ options[:description] = "Solidus Order ID: #{transaction_options[:order_id]}"
144
153
  options[:currency] = transaction_options[:currency]
154
+ options[:off_session] = true if v3_intents?
145
155
 
146
156
  if customer = creditcard.gateway_customer_profile_id
147
157
  options[:customer] = customer