solidus_stripe 3.0.0 → 4.1.0

Sign up to get free protection for your applications and to get access to all the features.
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