How to create a payment gateway for offsite_payments? (active_merchant)
General flow
- User from our site wants to pay for their order using their Paypal account. Our site's payment page returns a few forms for various payment providers, eg Paypal and Stripe. Each requires different fields, which a generated using
OffsitePayments::Integrations::Paypal::Helper.new(order, account, options={})
method. - Payment provider needs to be sure that it's our app indeed sending that payment request. That's why when regitering on Paypal as a merchant, we are getting eg merchant_id (
order
argument) that is used to identify our merchant, and merchant_key (sent inoptions
argument). merchant_key is a secret key you shouldn't share with anyone, only Paypal and merchant must know it to prove each other's authenticity to each other.
It's proved using 'checksums'. We generate a checksum ourselves server-side by inputting to some encrypting function (they differ for each provider) our merchant_key and all the params (well, except checksum correspondingly) in a form we'll be giving our user to click on. Checksum will be sent to Paypal as one of the hidden fields in that form. - Paypal gets our request. Params sent to the payment provider differ, but they'd usually include: params['order_id'] to identify the order and avoid duplicating the payment, params['amount'] to identify the sum of money order costs, params['merchant_id'] to identify the merchant and params['checksum'] to check if we are indeed the merchant we are pretending to be and to determine if we've been changing params. any slight change to params will lead to completely different checksum.
- Paypal has our secret merchant_key. Paypal verifies our authenticity and params consistency by regenerating the checksum (using our merchant_key) and comparing it to the one we sent.
- If everything is okay, Paypal asks the user if they are willing to pay for the order. If user agrees to pay, two things happen:
- user is redirected to the return_url (that's either set in merchant's console manually, or sent with other params), most likely you'll want it to be our server's page congratulating user with successful transaction
- Paypal is sending a GET/POST (depending on provider again) request to our server with params such as params['order_id'] so that we can identify the order that was processed, params['status'] that will tell us if payment was successful, and params[checksum] that Paypal generated using our merchant_key by inputting params Paypal's sending us now (except for params['checksum']) to the same encrypting function we used to encrypt our request to Paypal.
- There is some route on our server (notify_url), eg /payments/paypal (that's, just like return_url, either defined in the params we sent to Paypal, or in our merchant's console on Paypal) that is sitting there waiting for requests from specific payment provider.
That's the route Paypal is using to send us information on how the payment went (point 5, subpoint 2). Now we need to ensure that this is indeed the Paypal who's sending us this data. To verify it, we are using our merchant_key and regenerate a checksum comparing it with the one sent to us in params, just like Paypal did in point 4.
offsite_payments: general structure
module OffsitePayments::Integrations::Paypal
# Module for configuring gateway-wide stuff, for example self.service_url, maybe accessors to Helper and Notification class instances
class Helper < OffsitePayments::Helper
# Class for creating a payment form on our site, that on submit will lead to Paypal, where clicking user will sign in and agree on money withdrawal.
end
class Notification < OffsitePayments::Notification
# Class for handling Paypal's request to our site, telling us how payment went
end
end
offsite_payments: developing a payment gateway
git clone https://github.com/activemerchant/offsite_payments.git
-
script/generate integration paypal
, that will create a few files and tests for them. -
Now we should put general info for Paypal gateway into
OffsitePayments::Integrations::Paypal
module.
For example, we may want to define a URL that our payment form for Paypal will lead to depending on the mode we are working in:mattr_accessor :test_url self.test_url = "https://www.sandbox.paypal.com/cgi-bin/webscr" mattr_accessor :production_url self.production_url = "https://www.paypal.com/cgi-bin/webscr" def self.service_url mode = OffsitePayments.mode case mode when :production self.production_url when :test self.test_url else raise StandardError, "Integration mode set to an invalid value: #{mode}" end end
-
Then we should create a Helper class, that will then be used as
OffsitePayments::Integrations::Paypal::Helper.new order_id, merchant_id, params
, resulting in a hash in form of { fieldname: value } that Paypal with understand.
The point of Active Merchant gem, and Offsite Payments in particular, is standartization of interface of many different payment providers. Paypal expects us to send amount of money we want to withdraw as params['amount'], Doku as params['AMOUNT'], Easypay as params['EPSum']. We as developers would appreciate some consistency here.
This is the purpose ofOffsitePayments::Integrations::Paypal::Helper.mapping
method:class Helper < OffsitePayments::Helper mapping :standard_param_name, 'whatever Paypal decided to want this param to be called' end
Now you may want to create mappings for all parameters required by your payment provider, most popular of predefined are:
:order # unique order id, most providers require it to be unique for every transaction :account # our merchant_id, used to identify us as a merchant :amount # how much you want user to pay :currency # in what currency :notify_url # url on your server, Paypal will send here information on how payment went, so that you can mark your order as paid for :return_url # url user will be redirected to after payment
You can see other standard mapping names in offsite_payments/helper.rb.
-
After creating mappings we should test them with
RUBYOPT=W0 rake test TEST=test/unit/integrations/paypal/paypal_helper_test.rb
(Active Merchant uses Minitest). There is a special method defined in test/test_helper.rb:assert_field
that checks our mappings. This method requires@helper
variable to be set, so our tests may look like something this:class HelperTest < Test::Unit::TestCase include OffsitePayments::Integrations def setup @helper = Paypal::Helper.new 'order-500', 'OurSite748327432', { standard_param_name: 3 } end def test_basic_helper_fields # assert_field('currency', 'USD') is # same as::: assert_equal 'USD', @helper.fields['currency'] :::method assert_field 'ORDER_ID', 'order-500' # it means we have: # mapping :order, 'ORDER_ID' assert_field 'MID', 'OurSite748327432' # it means we have: # mapping account, 'MID' - (it's our merchant_id) assert_field 'whatever Paypal decided to want this param to be called', '3' # it means we have: # mapping :standard_param_name, 'whatever Paypal decided to want this param to be called' end end
-
Every payment gateway may have different solutions for checksum calculation, they usually provide examples in a few languages on how to do it. That would be something along the lines of taking a hash of params that are going to be sent to eg Paypal:
{ 'whatever Paypal decided to want this param to be called' => 'param value' }
, and sort this hash by key's alphabet, concatenate hash values in one string and encrypt it, resulting in a checksum. Now we'd have to tamper with#new
method (please take a look at /offsite_payments/helper.rb to understand it better):class Helper < OffsitePayments::Helper def initialize(order, account, options = {}) merchant_key = options.delete :merchant_key # we may pass our secret key in options to use it later on generating a checksum field device_used = options.delete(:device_used) # custom mapping that's not given by default super self.device_used = device_used # this methods are generated on the fly by using method_missing. basically result of is @helper.fields (or @fields here) #=> { '___device_used's mapping___': device_used, 'whatever Paypal decided to want this param to be called' => 'param value' , ... } self.checksum = Checksum.create(@fields, merchant_key) # in the end of creating a form prepared for submission to Paypal, we have to generate a checksum, for which you can create you own method. It will need to know a param-value hash of what you are submitting (except for the checksum field itself), and your merchant_key. end end