Last Updated: April 09, 2016
·
993
· Evgenia Karunus

How to create a payment gateway for offsite_payments? (active_merchant)

General flow

  1. 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.
  2. 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 in options 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.
  3. 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.
  4. 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.
  5. 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:
    1. 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
    2. 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.
  6. 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

  1. git clone https://github.com/activemerchant/offsite_payments.git
  2. script/generate integration paypal, that will create a few files and tests for them.
  3. 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
  4. 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['EP
    Sum']. We as developers would appreciate some consistency here.

    This is the purpose of OffsitePayments::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.

  5. 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
  6. 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

Have a fresh tip? Share with Coderwall community!

Post
Post a tip