Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 19 additions & 43 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
🚧 **WARNING: WORK IN PROGRESS** 🚧
## 🚧 **WARNING** 🚧 Work In Progress

You're looking at the source for `solidus_stripe` v5, which will only support the **starter frontend**
but at the moment **it is not ready to be used**.

Please use [`solidus_stripe` v4 on the corresponding branch](https://github.com/solidusio/solidus_stripe/tree/v4).

🚧 **WARNING: WORK IN PROGRESS** 🚧
## 🚧 **WARNING** 🚧 Supporting `solidus_frontend`

> ⚠️ **WARNING** ⚠️
>
> Please note that at the moment, solidus_stripe only supports integration with a single Stripe account. This means it is not suitable for use in a multi-seller marketplace environment. We are working to add support for multiple Stripe accounts as soon as possible.
If you need support for `solidus_frontend` please add `< 5` as a version requirement in your gemfile:
`gem 'solidus_stripe', '< 5'`
or if your tracking the github version please switch to the `v4` branch:
`gem 'solidus_stripe', git: 'https://github.com/solidusio/solidus_stripe.git', branch: 'v4'`

---

# Solidus Stripe

Expand All @@ -26,17 +29,6 @@ Add solidus_stripe to your Gemfile:
gem 'solidus_stripe'
```

> ⚠️ **WARNING** ⚠️
>
> If you need support for `solidus_frontend` please add `< 5` as a version requirement in your gemfile:
>
> `gem 'solidus_stripe', '< 5'`
>
> or if your tracking the github version please switch to the `v4` branch:
>
> `gem 'solidus_stripe', git: 'https://github.com/solidusio/solidus_stripe.git', branch: 'v4'`
>

Bundle your dependencies and run the installation generator:

```shell
Expand Down Expand Up @@ -165,35 +157,19 @@ The most important difference is that on Stripe a failure is not a final state,

In order to map these concepts SolidusStripe will match states in a slightly unexpected way, as shown below.

Reference: https://stripe.com/docs/payments/intents?intent=payment

![image](https://user-images.githubusercontent.com/1051/217322027-f49081f5-0795-49f4-994e-285a9de5347c.png)

### ⚠️ Warning: Authorization happens before the order is completed
| Stripe PaymentIntent Status | Solidus Payment State |
| --------------------------- | --------------------- |
| requires_payment_method | checkout |
| requires_action | checkout |
| processing | checkout |
| requires_confirmation | checkout |
| requires_capture | pending |
| succeeded | completed |

This setup implies a payment is authorized after the payment information is submitted to Stripe, although the order
still needs to be completed. That can become an issue if a customer abandons the checkout at the `confirm` step and
no action is taken to free up the money on the backend.
Reference:

In order to mitigate this issue, we suggest adapting the frontend by merging the *confirm* and *payment* steps:

1. embed the agreement to the terms of service
2. add order summary to the payment step
3. apply the following patch

```patch
--- a/templates/app/controllers/checkouts_controller.rb
+++ b/templates/app/controllers/checkouts_controller.rb
@@ -48,6 +48,8 @@ def redirect_on_failure
end

def transition_forward
+ @order.next if @order.has_checkout_step?("payment") && @order.payment?
+
if @order.can_complete?
@order.complete
else
```
- https://stripe.com/docs/payments/intents?intent=payment
- https://github.com/solidusio/solidus/blob/master/core/lib/spree/core/state_machines/payment.rb

## Development

Expand Down
63 changes: 19 additions & 44 deletions app/controllers/solidus_stripe/intents_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,59 +8,34 @@ class SolidusStripe::IntentsController < Spree::BaseController
before_action :load_payment_method

def after_confirmation
unless %w[confirm payment].include?(current_order.state.to_s)
redirect_to main_app.checkout_state_path(current_order.state)
return
end

# Check if this is needed. This was added to be sure the order
# in in the right step.
current_order.state = :payment

# TODO: understand how to handle webhooks. At this stage, we might receive a webhook
# with the confirmation of the setup intent. We need to be sure we are not creating
# the payment twice.
# https://stripe.com/docs/payments/intents?intent=setup#setup-intent-webhooks

current_order.next!

case
when params[:setup_intent]
intent = SolidusStripe::SetupIntent.find_by!(
payment_method: @payment_method,
order: current_order,
stripe_intent_id: params[:setup_intent],
)
when params[:payment_intent]
intent = SolidusStripe::PaymentIntent.find_by!(
payment_method: @payment_method,
order: current_order,
stripe_intent_id: params[:payment_intent],
)
else
unless params[:payment_intent]
return head :unprocessable_entity
end

payment = intent.create_payment!(
amount: current_order.total, # TODO: double check, remove store credit?
add_to_wallet: true
)
unless current_order.confirm?
redirect_to main_app.checkout_state_path(current_order.state)
return
end

SolidusStripe::LogEntries.payment_log(
payment,
success: true,
message: "Reached return URL",
data: intent.stripe_intent,
intent = SolidusStripe::PaymentIntent.find_by!(
payment_method: @payment_method,
order: current_order,
stripe_intent_id: params[:payment_intent],
)

if @payment_method.skip_confirm_step?
if intent.process_payment
flash.notice = t('spree.order_processed_successfully')

flash['order_completed'] = true
current_order.complete!
redirect_to main_app.token_order_path(current_order, current_order.guest_token)

redirect_to(
spree_current_user ?
main_app.order_path(current_order) :
main_app.token_order_path(current_order, current_order.guest_token)
)
else
flash[:notice] = t(".intent_status.#{intent.stripe_intent.status}")
redirect_to main_app.checkout_state_path(current_order.state)
flash[:error] = params[:error_message] || t('spree.payment_processing_failed')
redirect_to(main_app.checkout_state_path(:payment))
end
end

Expand Down
76 changes: 0 additions & 76 deletions app/models/solidus_stripe/gateway.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,38 +29,6 @@ def initialize(options)

attr_reader :client

# Authorizes a certain amount on the provided payment source.
#
# We create and confirm the Stripe payment intent in two steps. That's to
# guarantee that we associate a Solidus payment on the creation, and we can
# fetch it after the webhook event is published on confirmation.
#
# The Stripe payment intent id is copied over the Solidus payment
# `response_code` field.
def authorize(amount_in_cents, source, options = {})
Comment thread
kennyadsl marked this conversation as resolved.
check_given_amount_matches_payment_intent(amount_in_cents, options)

stripe_payment_intent = create_confirmed_stripe_payment_intent(
source: source,
payment: options[:originator],
stripe_payment_intent_options: {
capture_method: "manual"
}
)
build_payment_log(
success: true,
message: "PaymentIntent was confirmed successfully",
response_code: stripe_payment_intent.id,
data: stripe_payment_intent
)
rescue Stripe::StripeError => e
build_payment_log(
success: false,
message: e.message,
data: e.response
)
end

# Captures a certain amount from a previously authorized transaction.
#
# @see https://stripe.com/docs/api/payment_intents/capture#capture_payment_intent
Expand All @@ -86,29 +54,6 @@ def capture(amount_in_cents, payment_intent_id, options = {})
)
end

# Authorizes and captures a certain amount on the provided payment source.
#
# See `#authorize` for how the confirmation step is performed.
#
# @todo add support for purchasing custom amounts
def purchase(amount_in_cents, source, options = {})
check_given_amount_matches_payment_intent(amount_in_cents, options)

stripe_payment_intent = create_confirmed_stripe_payment_intent(
source: source,
payment: options[:originator],
stripe_payment_intent_options: {
capture_method: "automatic"
}
)
build_payment_log(
success: true,
message: "PaymentIntent was confirmed and captured successfully",
response_code: stripe_payment_intent.id,
data: stripe_payment_intent,
)
end

# Voids a previously authorized transaction, releasing the funds that are on hold.
def void(payment_intent_id, _options = {})
check_payment_intent_id(payment_intent_id)
Expand Down Expand Up @@ -171,27 +116,6 @@ def request(&block)

private

def create_confirmed_stripe_payment_intent(source:, payment:, stripe_payment_intent_options:)
stripe_payment_method = source.stripe_payment_method

SolidusStripe::PaymentIntent.create_stripe_intent(
payment_method: source.payment_method,
order: payment.order,
stripe_intent_options: stripe_payment_intent_options.merge(
payment_method: stripe_payment_method.id,
customer: stripe_payment_method.customer,
confirm: false
)
).tap do
payment.update_column(:response_code, _1.id) # rubocop:disable Rails/SkipsModelValidations
confirm_stripe_payment_intent(_1.id)
end
end

def confirm_stripe_payment_intent(stripe_payment_intent_id)
request { Stripe::PaymentIntent.confirm(stripe_payment_intent_id) }
end

def capture_stripe_payment_intent(stripe_payment_intent_id)
request { Stripe::PaymentIntent.capture(stripe_payment_intent_id) }
end
Expand Down
78 changes: 54 additions & 24 deletions app/models/solidus_stripe/payment_intent.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,41 +5,70 @@ class PaymentIntent < ApplicationRecord
belongs_to :order, class_name: 'Spree::Order'
belongs_to :payment_method, class_name: 'SolidusStripe::PaymentMethod'

def self.retrieve_stripe_intent(payment_method:, order:)
find_by(payment_method: payment_method, order: order)&.stripe_intent
end
def self.prepare_for_payment(payment)
# Find or create the intent for the payment.
intent =
find_by(payment_method: payment.payment_method, order: payment.order) ||
new(payment_method: payment.payment_method, order: payment.order)
.tap { _1.update!(stripe_intent_id: _1.create_stripe_intent.id) }

def self.create_stripe_intent(payment_method:, order:, stripe_intent_options: {})
instance = new(payment_method: payment_method, order: order)
instance.create_stripe_intent(stripe_intent_options).tap { instance.update!(stripe_intent_id: _1.id) }
end
# Update the intent with the previously acquired payment method.
intent.payment_method.gateway.request {
Stripe::PaymentIntent.update(intent.stripe_intent_id, payment_method: payment.source.stripe_payment_method_id)
}

def stripe_intent
payment_method.gateway.request do
Stripe::PaymentIntent.retrieve(stripe_intent_id)
end
# Attach the payment intent to the payment.
payment.update!(response_code: intent.stripe_intent.id)

intent
end

def create_payment!(amount: order.total, add_to_wallet: false)
payment = order.payments.create!(
state: 'pending',
def process_payment
payment = order.payments.valid.find_by!(
payment_method: payment_method,
amount: amount,
response_code: stripe_intent.id,
source: payment_method.payment_source_class.new(
payment_method: payment_method,
),
)

if add_to_wallet && order.user && stripe_intent.setup_future_usage.present?
payment.source.update!(stripe_payment_method_id: stripe_intent.payment_method)
order.user.wallet.add payment.source
payment.started_processing!

case stripe_intent.status
when 'requires_capture'
payment.pend! unless payment.pending?
successful = true
else
payment.failure!
successful = false
end

payment
SolidusStripe::LogEntries.payment_log(
payment,
success: successful,
message: I18n.t("solidus_stripe.intent_status.#{stripe_intent.status}"),
data: stripe_intent,
)

if successful
order.complete!
order.user.wallet.add(payment.source) if order.user && stripe_intent.setup_future_usage.present?
else
order.payment_failed!
end

successful
end

def stripe_intent
@stripe_intent ||= payment_method.gateway.request do
Stripe::PaymentIntent.retrieve(stripe_intent_id)
end
end

def reload(...)
@stripe_intent = nil
super
end

def create_stripe_intent(stripe_intent_options)
def create_stripe_intent(**stripe_intent_options)
stripe_customer_id = SolidusStripe::Customer.retrieve_or_create_stripe_customer_id(
payment_method: payment_method,
order: order
Expand All @@ -56,7 +85,8 @@ def create_stripe_intent(stripe_intent_options)
setup_future_usage: payment_method.preferred_setup_future_usage.presence,
customer: stripe_customer_id,
metadata: { solidus_order_number: order.number },
}.merge(stripe_intent_options))
**stripe_intent_options,
})
end
end
end
Expand Down
Loading