Go Tripod

Ruby on Rails, PayPal Express Recurring Payments using Active Merchant

I recently needed to setup recurring payments through PayPal’s express checkout for a subscription based site I have writen using Ruby on Rails. There is already an excellent framework for interacting with most payment gateways, including PayPal, for Ruby called Active Merchant. Unfortunately recurring payments support in Active Merchant for PayPal Express Checkout is limited to a script pasted into their lighthous bug tracking system. The trouble is that this script only covers creating subscription profiles and also later getting details of that profile, but I needed to be able to suspend and cancel subscriptions profiles as well as make changes to the subscription from my site.

**** UPDATE: ActiveMerchant recently removed the functionality to use PayPal’s NVP API and so this code will no longer work with the latest ActiveMerchant. Raymond Law has kindly ported the code to use the SOAP API and you can find out more information and usage on his blog. ****

Active Merchant is very easy to extend so I have written a Ruby class that can be dropped into /vendor/plugins/active_merchant/billing/gateways/ within your Rails project (assuming you have AM installed as a plugin)

# simple extension to ActiveMerchant for basic support of recurring payments with Express Checkout API
#
# See http://http://www.gotripod.com/2008/09/07/ruby-on-rails-paypal-express-recurring-payments-using-active-merchant/
# for details on getting started with this gateway
#
#
module ActiveMerchant #:nodoc:
  module Billing #:nodoc:
    class PaypalExpressRecurringNvGateway < Gateway
      include PaypalNvCommonAPI

      LIVE_REDIRECT_URL = 'https://www.paypal.com/cgibin/webscr?cmd=_customer-billing-agreement&token='
      TEST_REDIRECT_URL = 'https://www.sandbox.paypal.com/cgi-bin/webscr?cmd=_customer-billing-agreement&token='

      def redirect_url
        test? ? TEST_REDIRECT_URL : LIVE_REDIRECT_URL
      end

      def redirect_url_for(token)
          "#{redirect_url}#{token}"
      end

      def setup_agreement(description, options = {})
        requires!(options, :return_url, :cancel_return_url)
        commit 'SetCustomerBillingAgreement', build_setup_agreement(description, options)
      end

      def get_agreement(token)
        commit 'GetBillingAgreementCustomerDetails', build_get_agreement(token)
      end

      def create_profile(money, token, options = {})
        commit 'CreateRecurringPaymentsProfile', build_create_profile(money, token, options)
      end

      def get_profile_details(profile_id)
        commit 'GetRecurringPaymentsProfileDetails', build_get_profile_details(profile_id)
      end

      def manage_profile(profile_id, action, options = {})
        commit 'ManageRecurringPaymentsProfileStatus', build_manage_profile(profile_id, action, options)
      end

      def update_profile(profile_id, options = {})
        commit 'UpdateRecurringPaymentsProfile', build_update_profile(profile_id, options)
      end

      def bill_outstanding(profile_id, money, options = {})
        commit 'BillOutstandingAmount', build_bill_outstanding(profile_id, money, options)
      end

      private

      def build_setup_agreement(description, options)
        post = {}
        add_pair(post, :billingagreementdescription, description)
        add_pair(post, :returnurl, options[:return_url])
        add_pair(post, :cancelurl, options[:cancel_return_url])
        add_pair(post, :billingtype, "RecurringPayments")
        add_pair(post, :paymenttype, options[:payment_type]) if options[:payment_type]
        add_pair(post, :localecode, options[:locale]) if options[:locale]
        add_pair(post, :billingagreementcustom, options[:custom_code]) if options[:custom_code]
        post
      end

      def build_get_agreement(token)
        post = {}
        add_pair(post, :token, token)
        post
      end

      def build_create_profile(money, token, options)
        post = {}
        add_pair(post, :token, token)
        add_amount(post, money, options)
        add_pair(post, :subscribername, options[:subscriber_name]) if options[:subscriber_name]
        add_pair(post, :initamt, options[:initamt]) if options[:initamt]
        add_pair(post, :failedinitamtaction, options[:failedinitamtaction]) if  options[:failedinitamtaction]
        add_pair(post, :profilestartdate, Time.now.utc.iso8601)
        add_pair(post, :billingperiod, options[:billing_period] ? options[:billing_period] : "Month")
        add_pair(post, :billingfrequency, options[:billing_frequency] ? options[:billing_frequency] : 1)
        add_pair(post, :totalbillingcycles, options[:billing_cycles]) if [:billing_cycles]
        add_pair(post, :profilereference, options[:reference]) if options[:reference]
        add_pair(post, :autobillamt, options[:auto_bill_amt]) if options[:auto_bill_amt]
        add_pair(post, :maxfailedpayments, options[:max_failed_payments]) if options[:max_failed_payments]
        add_pair(post, :shippingamt, amount(options[:shipping_amount]), :allow_blank => false) if options[:shipping_amount]
        add_pair(post, :taxamt, amount(options[:tax_amount]), :allow_blank => false) if options[:tax_amount]
        add_shipping_address(post, options[:shipping_address]) if options[:shipping_address]
        post
      end

      def build_get_profile_details(profile_id)
        post = {}
        add_pair(post, :profileid, profile_id)
        post
      end

      def build_manage_profile(profile_id, action, options)
        post = {}
        add_pair(post, :profileid, profile_id)
        add_pair(post, :action, action)
        add_pair(post, :note, options[:note]) if options[:note]
        post
      end

      def build_update_profile(profile_id, options)
        post = {}
        add_pair(post, :profileid, profile_id)
        add_pair(post, :note, options[:note]) if options[:note]
        add_pair(post, :desc, options[:description]) if options[:description]
        add_pair(post, :subscribername, options[:subscriber_name]) if options[:subscriber_name]
        add_pair(post, :profilereference, options[:reference]) if options[:reference]
        add_pair(post, :additionalbillingcycles, options[:additional_billing_cycles]) if options[:additional_billing_cycles]
        add_amount(post, options[:money], options)
        add_pair(post, :shippingamt, amount(options[:shipping_amount]), :allow_blank => false) if options[:shipping_amount]
        add_pair(post, :autobillamt, options[:auto_bill_amt]) if options[:auto_bill_amt]
        add_pair(post, :maxfailedpayments, options[:max_failed_payments]) if options[:max_failed_payments]
        add_pair(post, :taxamt, amount(options[:tax_amount]), :allow_blank => false) if options[:tax_amount]
        add_shipping_address(post, options[:shipping_address]) if options[:shipping_address]
        post
      end

      def build_bill_outstanding(profile_id, money, options = {})
        post = {}
        add_pair(post, :profileid, profile_id)
        add_amount(post, money, options)
        add_pair(post, :note, options[:note]) if options[:note]
        post
      end

      def build_response(success, message, response, options = {})
        PaypalExpressNvResponse.new(success, message, response, options)
      end

    end
  end
end

With this class installed using Active Merchant to set up a subscription / recurring payment through PayPal Express Checkout is easy. Firstly setup your gateway object:

gw = ActiveMerchant::Billing::PaypalExpressRecurringNvGateway.new( :login => 'PAYPALEMAIL', :password => 'PASSWORD', :signature => 'PAYPALAPISIGNATURE' )

Then make a request to PayPal to setup the recurring payment. At this stage you pass through a description (which is what is shown to the user when they are asked to authorise the subscription so make it descriptive) and you also need to provide URLs on your site, that PayPal should redirect the subscriber back to when they either complete the payment, or alternatively if they choose to cancel.

response = gw.setup_agreement("Subscription £25 per month", :cancel_return_url => "https://mysite.com/cancel", :return_url => "https://mysite.com/complete" )

The request above returns us a token in the response from paypal and at this point we need to redirect the user to PayPal to authorise this subscription. The user will see the description “Subscription £25 per month” as sent in the previous request. We need to redirect the subscriber to PayPal using the following line of ruby:

redirect_to gw.redirect_url_for(response.token)

Once the user has authorised the subscription they are returned to the :return_url we specified earlier, at which point we can create the actual subscription using the following:

response = gw.create_profile(2500, response.token, :currency => "GBP", :reference => "34")

Note: PayPal are really confusing having one API for the US and another for the UK but if you are using PayPal Express (which is free) independently of PayPal Website Payments Pro (Which you need to pay for) the US PayPal Express API works for all countries (apart from Germany I believe) and as you can see above I am passing in the UK currency. I am using the US API and I have a UK PayPal account. Also note that I have passed in a reference (I have an IPN URL setup in my PayPal account – Unfortunately you cannot pass an IPN URL with the request) to be sent in the IPN.

The previous step completes the set up of our Subscription. However if we need to later get information on the subscription or change it, we need to extract the Profile ID from the response as follows:

profile_id = response.params["profileid"]

With this profile_id we can then later use these additional methods that I have included, such as getting details of the subscription profile using:

gw.get_profile_details(profile_id)

Update the subscription using various options (i.e. changing subscription amount shown below):

gw.update_profile(profile_id, :money => 3000, :currency => "GBP")

Manage the subscription, for example cancel it as follows:

gw.manage_profile(profile_id, "Cancel", :note => "Your subscription has been cancelled by us")

And finally bill any outstanding subscription balance:

gw.bill_outstanding(profile_id, 2500, :currency=> "GBP", :note => "£20 Overdue Subscription")

Please note that as of yet this class is not part of Active Merchant, however it has been added to Active Merchants case #17 If you want to use this you will have to add it manually as above.

I recommend reading PayPals Express Checkout Integration Guide and the Name Value Pair API Developer Guide and Reference for more information on what variables can be passed in each request to PayPal.

Sharing is caring:
Avatar of Colin Ramsay, Technical Director
By Colin Ramsay, Technical Director
Filed under: Development insights
Topics:

Got an idea for a project?

Need a website? Web-enabled software to streamline your business? Just some advice?