A Short Love Story about Adyen and Service Objects

Photo of Marcin Szmigiel

Marcin Szmigiel

Aug 10, 2017 • 14 min read
money-pile-795450-edited.jpg

Some time ago, I got assigned to the enigmatic task of analyzing and implementing the integration with a new payment provider for our client’s Austrian services.

The requirement was straightforward: the new payment service should be Adyen, as Adyen supports SEPA payments. Here is the story about how it all played out.

Wait, what?

tl;dr: below is some information about the project and the problem. For more technical stuff just scroll down until you see text in a fixed-width font.

Project

The project I implemented this functionality for is called Lemonfrog. It is a set of web apps aimed at pairing users, not only in the dating sense. We can break down Lemonfrog’s services into two main groups:

  • Care services: they connect people looking for a job with people looking for help, e.g. tutor24 (students and tutors) or homeservice24 (all kinds of housework).
  • Dating services, e.g. singlemitkind (for single parents).

Though Lemonfrog itself is based in Switzerland, its services are also present in Austria and Germany, where it also has a large user base.

The main source of income for our client are premium accounts – some types of users require a premium account to be able to contact others. The variety of services and their target users requires more than one type of payment, so next to the most popular payments providers (i.e. Stripe and PayPal) we also have options such as Invoice by email/letter. Remember, not everybody knows how to use a credit card.

Problem

The biggest problem for a Switzerland-based application about targeting Austria is that the Austrian currency is the euro, and the Swiss currency is the franc. The solution to this issue is called the Single Euro Payments Area, the European Union’s payment-integration initiative for simplification of bank transfers denominated in euros.

To put it more simply, thanks to SEPA (both Austria and Switzerland are its members) people from Austria can pay people in Switzerland easily and with smaller costs.

Adyen

The payment solutions provider which fit the requirement of supporting SEPA perfectly is Adyen. It offers many payment methods from around the world (including methods specific for one country and more global ones, like the SEPA bank transfer, as well).

How?

Ok, so now when we knew what the problem was and what tool we should use, the time came to analyze how to use that tool to solve the problem. After a few hours of research, I found only one out-of-the-box solution (the gem called adyen) that could be potentially helpful, but considering our needs and the fact that that gem didn’t seem to be well supported (the last commit was from almost half a year before) we decided that it would better to write our own solution.

There’s no need to delve deeper into the specifics of Adyen, what it provides or how it works – all this information is available in the developer’s documentation. In this article, I will focus only on some suggestions about how to implement the integration with Adyen in your app from the developer’s point of view.

During the ‘investigative’ part of my work, I tried to write down all necessary information:

  • We needed to store three values for each of our services: account name, skin code and HMAC key.
  • The payment flow could be divided into three main actions:
    • Creating and sending a payment request.
    • Receiving and handling the response when the user comes back to our service after completing the payment.
    • Handling notifications about payment status changes.
  • Creating and sending a payment request includes preparing correct request parameters that are in accordance with Adyen’s documentation and a signature for our request.

I decided to split each action into separate service classes – one service object would then be responsible for one action. If you want to know more about service objects, you can find an article about this approach here.

Charge request

To create and successfully send a charge request to Adyen, we need to perform three steps:

  • Build a list of parameters based on the payment data.
  • Sign those parameters.
  • Build request URI based on those params and the environment in which app is currently running.

From our payments controller’s point of view, we only need the URI to which we should redirect the user when an Adyen payment method is chosen. This will be handled by ChargeRequestService:


module Adyen
  class ChargeRequestService
    attr_reader :params

    def initialize(payment, service)
      @params = RequestParamsService.new(payment, service).call
    end

    def call
      request_url + "?" + URI.encode_www_form(params)
    end

    private

    def env
      Rails.env.production? ? "live" : "test"
    end

    def request_url
      "https://#{env}.adyen.com/hpp/select.shtml"
    end
  end
end

Its constructor takes two objects as arguments: one with payment data, the other one with data related to the current service and its configuration. Then, it delegates the building parameters to our RequestParamsService. Based on the @params hash and the current app environment, it builds and returns a URI to Adyen’s live or test endpoint.

Building and signing params

Now, we will have a closer look at how the parameters are built and signed.


module Adyen
  class RequestParamsService
    PARAMETERS = %w(... merchant_account ...).freeze

    attr_reader :params

    def initialize(payment, service)
      @service = service
      @payment = payment
      @params = build_params
    end

    def call
      params["merchantSig"] = build_signature
      params
    end

    private

    attr_reader :service, :payment

    def build_params
      PARAMETERS.each_with_object({}) { |param, hash| hash[param.camelize(:lower)] = send(param) }
    end

    def build_signature
      SignatureService.new(service.adyen_hmac_key, params).call
    end

    def merchant_account
      service.adyen_account_name || GLOBAL_ADYEN_ACCOUNT
    end
  end
end

In the service responsible for building our parameters, we are using some values defined in constants. The most important one is called PARAMETERS, because it includes the list of all the parameters (in the alphabetical order) that we want to include in the generated hash. The names are written in snake_case, as each name has its own private method responsible for generating its value, but Adyen requires our keys to be written in lowerCamelCase. That’s why we need to call .camelize(:lower) in our build_params method.

Parameters signature

Before returning a hash with the parameters, we want to generate their signature by calling SignatureService with the HMAC key generated through the Adyen panel and saved as adyen_hmac_key.


module Adyen
  class SignatureService
    def initialize(key, params)
      @key = Array(key).pack("H*")
      @params = params
    end

    def call
      calculate_signature
    end

    private

    attr_reader :key, :params

    def calculate_signature
      Base64.strict_encode64(hmac)
    end

    def hmac
      digest = OpenSSL::Digest.new("sha256")
      OpenSSL::HMAC.digest(digest, key, params_string)
    end

    def keys_string
      params.keys.join(":")
    end

    def params_string
      "#{keys_string}:#{values_string}"
    end

    def values_string
      params.values.map { |v| v.gsub(":", "\\:").gsub("\\", "\\\\") }.join(":")
    end
  end
end

Here is what the signature generation algorithm should look like according to the documentation:

  1. Sort hash alphabetically by keys.
  2. Escape colons and backslashes in values with \: and \\ respectively.
  3. Concatenate keys with :, then do the same for values and join both strings (also with :).
  4. Convert the HMAC key to its binary representation – it’s considered a hexadecimal value.
  5. Calculate the HMAC with the signing string from step 3 using SHA-256.
  6. Encode results using the Base64 encoding scheme.

On its dashboard, Adyen provides a form to test if a generated signature is correct for given values.

Handling Adyen’s response

After the payment goes through, Adyen redirects the user back to our page and sends us some information about the results of the process. We will handle these responses in a separate service.


module Adyen
  class HandleResponseService
    attr_reader :params, :payment

    def initialize(params)
      @params = params
      @payment = Payment.find(payment_id)
    end

    def call
      payment.update_attributes(
        state: fetch_state,
        external_id: fetch_external_id
      )
    end

    private

    def fetch_external_id
      params["pspReference"]
    end

    def fetch_state
      params["authResult"]
    end

    def payment_id
      # Here you can get payment_id for example from “merchantReference”
    end
  end
end

First, we want to find the payment in our database based on the received data. The next step is to update this payment’s status. We also want to save the external ID of the payment – it might prove useful for identifying payments.

Responding to changes: handling notifications

SEPA payments are not as fast as payments with a credit card or PayPal – in the SEPA, a payment needs about one working day to be authorized and settled. This makes receiving and reacting to notifications from Adyen crucial for our case. Here is the service responsible for that.


module Adyen
  class HandleNotificationService
    attr_reader :notifications

    def initialize(params)
      @notifications = params["notificationItems"]
    end

    def call
      notifications.each do |item|
        @current_item = item["NotificationRequestItem"]
        handle_notification_item
      end
    end

    private

    attr_reader :current_item

    def handle_notification_item
      return false unless payment
      if successful?
        send "handle_#{event}"
      else
        Rollbar.info(failure_reason_message)
      end
    rescue ::NoMethodError
      Rollbar.info(not_implemented_for_message)
    end

    def event
      current_item["eventCode"].downcase
    end

    def handle_authorisation
      # Code for handling authorisation event
    end

    def handle_cancellation
      # Code for handling cancellation event
    end

    def successful?
      current_item["success"] == "true"
    end
  end
end

Adyen notifications come as an array of notificationItems – we want to iterate through each of those items and handle each notification event. This is done in the handle_notification_item method. First, we need to check if the payment is found for the given data. Next, we should check whether the notification says that the event was successful. If it wasn’t successful, we can somehow store the data about the failure (send it to Rollbar for example).

Handling events is managed by handle_[event_name] methods, which are dynamically called - in those methods we can implement the code responsible for doing what we want to do in case of this specific event (e.g. for handle_authorisation, we can update the payment state to PAID and trigger a mailer which will inform the user that the payment has been approved). We also want to store information about unknown events (there are plenty of event codes which Adyen can potentially send, but only a few will be useful for us) – in the case of NoMethodError we send the information to Rollbar.

Summary

Adyen can be very useful for your client from the business perspective. It’s good to know that such a tool exists and know how to use it. Unfortunately, there is neither documentation meant for Ruby developers nor any up-to-date out-of-the-box solution. Hopefully, this article will help you with implementing the integration with Adyen in your app.

Photo of Marcin Szmigiel

More posts by this author

Marcin Szmigiel

Marcin has a mix of scientific and humanistic mind, that’s why he chose to study Human-Computer...
How to build products fast?  We've just answered the question in our Digital Acceleration Editorial  Sign up to get access

We're Netguru!

At Netguru we specialize in designing, building, shipping and scaling beautiful, usable products with blazing-fast efficiency
Let's talk business!

Trusted by:

  • Vector-5
  • Babbel logo
  • Merc logo
  • Ikea logo
  • Volkswagen logo
  • UBS_Home