A Short Love Story about Adyen and Service Objects

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:
- Sort hash alphabetically by keys.
- Escape colons and backslashes in values with \: and \\ respectively.
- Concatenate keys with :, then do the same for values and join both strings (also with :).
- Convert the HMAC key to its binary representation – it’s considered a hexadecimal value.
- Calculate the HMAC with the signing string from step 3 using SHA-256.
- 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.