Spree Custom Gateway

Spree makes it easy to take payments from any Payment Service Provider, and in this post I will briefly walk you through the process of creating and using your own custom gateway.

Custom gateways in Spree encapsulate the operations required to take payments online. This functionality is abstracted out of the order logic, which makes it easy to think about in isolation. Ultimately what we want is to substitute a small subset of functionality in Spree for our own. The way Spree knows whether a transaction was successful or not is that we tell it exactly what happened. We return a Response object (ActiveMerchant::Billing::Response), which holds a state of successful or failed. If we return a successful response object, everything went well and the user has now completed the checkout. If the transaction was not successful, the user will be redirected back to the payment step with an error message where they can try again.

Foopay gateway

#app/models/spree/gateway/foopay.rb

class Spree::Gateway::Foopay < Spree::Gateway
  def provider_class
    Spree::Gateway::Foopay
  end
  def payment_source_class
    Spree::CreditCard
  end

  def method_type
    'foopay'
  end

  def purchase(amount, transaction_details, options = {})
    ActiveMerchant::Billing::Response.new(true, 'success', {}, {})
  end
end

In this case we have stubbed out the response to always be successful. This happens because the first argument is set to true. If it were set to false, it would always indicate to Spree that the payment has failed.

Typically you would look for something like ‘success’ in your xml, json or post data after you have completed communication with the external gateway. Note that the payment_source_class is Spree::CreditCard. We could specify a custom model here if we wanted to add extra attributes to it. The ‘options’ argument passed in contains only a set list of predefined options including:

:email, :customer, :customer_id, :ip, :order_id, :shipping, :tax, :subtotal

A complete list of these options can be seen here.

Register the gateway

We can register Foopay by adding the following code to the spree initializer:

#config/initializers/spree.rb

Rails.application.config.spree.payment_methods << Spree::Gateway::Foopay

Persisting the gateway

We need to persist our gateway, and also add it to our Spree store:

Spree::Gateway::Foopay.create(      
  name: 'Foopay',      
  description: 'My fancy new Foopay!',      
  active: true,      
  environment: 'development')    

Spree::Store.first.payment_methods << payment_method

Frontend

In order to get the data we need from the user, we need to render a form for them to fill in. Looking back at our custom gateway, we specified a ‘method_type’ method. This will be used to look up the partial to be rendered in the payment step. In Spree, we can see this happening in frontend/app/views/spree/checkout/_payment.html.erb.

render partial: "spree/checkout/payment/#{method.method_type}"

Our gateway specified ‘foopay’ as the method_type, so it will try and render the the _foopay partial. Let’s create it at app/views/spree/checkout/payment/_foopay.html.erb

This file is used to customise what is sent through to the checkout controller.

Note: Naming of the form inputs is important. Custom payment values may also need to be added to the permitted attributes list.

Behind the scenes

I decided to take a few notes on the interesting things that happen when the request goes into Spree.

We can see that the payment form points to: /checkout/update/payment

The request hits the .update method on the Spree::Checkout controller. The first code it hits is:

@order.update_from_params(params, permitted_checkout_attributes, request.headers.env) 

This line will update the order with the new data submitted from the form, any associated models will also be updated or created. If this completes successfully, the order tries to advance to the ‘complete’ state.

@order.next

The state machine hooks are triggered.

before_transition to: :complete do |order|
  ...
  order.process_payments!
end

process_payments! is defined in core/app/models/spree/order/payments.rb

Both the payments and checkout modules are mixed into the order object, so all the methods they provide are added directly to it. Next, it finds each of the unprocessed_payments on the order, and runs process! on them.

.process! checks whether the gateway has auto_capture? enabled. If it does, the purchase method is called directly, otherwise authorize is called.

Following the call into the Payment model, we find the following:

after_initialize :build_source

It is within the .build_source method that Foopay is instantiated.

Time to shine

The actual line of code that hands over responsibility from Spree to Foopay looks like this:

response = payment_method.send(action, money.money.cents, source, gateway_options)

action in this case will be ‘purchase’.

In Foopay, our purchase method accepts the following arguments:

  • amount
  • payment source
  • options

The second payment_source argument will be an instance of Spree::CreditCard. The third argument, ‘options’, is a pre-defined list of gateway options, this argument was discussed above. We use the data in these arguments to finish the payment. Our method returns the appropriate response object, and the transaction is complete.

ActiveMerchant::Billing::Response.new(true, 'success', {}, {})

Resources:

https://guides.spreecommerce.com/developer/payments.html
https://github.com/shopify/active_merchant
https://github.com/spree/spree/tree/2-4-stable
http://api.rubyonrails.org/classes/ActionController/Parameters.html

About the Author

Emile Swarts

Senior Software Engineer at Made Tech. All about big beards, beers and text editors from the seventies.

Avatar for Emile Swarts

We are hiring! Find out more about a career at Made Tech.

Download a copy of our new book

Legacy technology is one of the biggest threats to public sector organisations.
Whether you’ve started your journey already or don’t know where to begin, this 160-page book has been written to guide you to define and implement the right approach for your organisation.