Unfortunately, ActiveMerchant does not support recurring payments with PayPal Website Payments Pro (US) out of the box.
In this tutorial, we will add support for recurring payments to ActiveMerchant. Before we begin, let’s list what we’re using here:
- ActiveMerchant version 1.7.0
- PayPal Website Payments Pro (with Recurring Payments Enabled)
- Rails version 2.3.8
- Ruby v1.8.7 (patchlevel 299)
Note: You do not need to have an active PayPal Website Payments Pro account to test your code. Instead, create a free PayPal Sandbox (http://sandbox.paypal.com) account.
Click ‘Sign Up Now’ on the PayPal Sandbox site (http://developer.paypal.com) to create a new PayPal Sandbox account. Once you’ve registered, log in to view the PayPal Sandbox main menu.
Click ‘Manually Create Accounts’ to create a new Website Payments Pro (US) test account with recurring payments enabled. Note: You cannot use the ‘preconfigured accounts’ generators, as they do not enable recurring payments. Click ‘Get Started’ under the section labeled ‘Business’.
Select ‘Website Payments Pro’ from the drop down menu and click ‘Continue’ twice.
You should see ‘Apply for Website Payments Pro’. Select ‘Sign Up for a New PayPal Business Account’ and click ‘Sign Up’.
Fill out the form with dummy data. It doesn’t matter what you use here, as it is only for the test account, but make sure to remember the email and password. When finished, click ‘Agree and Continue’.
Continue to fill out the form with dummy data. You can enter any number in the ‘Federal Tax ID’ field, as long as it’s not all zeros. I used ‘11-3873926’ and it worked fine. No need to include any of the ‘optional’ fields, in ssn I entered 111-45-4444 and it worked fine.
Select ‘Recurring Payments’ checkbox on the ‘Products and optional services’ page and then click ‘Continue’.
Fill out the form with dummy data. The credit card information should be pre-populated automatically. Enter ‘000’ (three zeros) for the ‘CSC’ code and click ‘Submit Application’.
and then click continue to my account.
Click on the ‘PayPal’ logo to return to the Sandbox main page. Click ‘Test Email’ and look for an email to the dummy email address you used when you created the test account.
Copy the URL listed under ‘To activate your account, just confirm your email address.’ and paste it into a new browser window. Enter your password (The one you used when you created the test account.) and submit the form.
To verify account, click on ‘Test Accounts’ on the PayPal Sandbox main page. Select the account that you just created and click ‘Enter Sandbox Test Site’.
In the Sandbox Test Site, click the link ‘Get Verified’ on the right side of the page, then click ‘Link Bank Account’. The bank account numbers should be pre-populated by default. Enter a dummy ‘Bank Name’ and click ‘Continue’.
Enter dummy values in both of the ‘Deposit Amounts’ fields and click ‘Continue’ twice. I used ‘$0.25’ and ‘$0.35’ and it worked fine.
Retrieving API Credentials for PayPal Test Account
From the PayPal Sandbox main page, click on ‘Test Accounts’. Select the account that you just created and click ‘Enter Sandbox Test Site’.
Click ‘Profile’ on the Sandbox Test Site main menu, then click ‘Request API Credentials’. Click ‘Set up PayPal API credentials and permissions’ under ‘Option 1’.
Click ‘Request API Credentials’ under ‘Option 2’. Select ‘Request API Signature’ and click ‘Agree and Submit’. You will see your new API credentials, click ‘Done’ and close the window.
From the PayPal Sandbox main page, click ‘API Credentials’. You should now see a new set of API credentials with a ‘API username’, ‘API password’, and ‘Signature’. You will use these credentials when you setup ActiveMerchant.
Now install ActiveMerchant.
I won’t go into detail here how to install the ActiveMerchant plugin. There are many good tutorials available, but I suggest starting with ActiveMerchant’s GitHub (http://github.com/Shopify/active_merchant) and documentation (http://www.activemerchant.org). A couple key points to consider when installing ActiveMerchant are as follows: 1. The API credentials retrieved for the PayPal test account should be copied to the ActiveMerchant configuration placed in your development.rb file as follows:
# Setup ActiveMerchant for development mode with Paypal Sandbox
config.after_initialize do
ActiveMerchant::Billing::Base.mode = :test
::GATEWAY = ActiveMerchant::Billing::PaypalGateway.new(
:login => '[Add PayPal API login here]',
:password => '[Add PayPal API password here]',
:signature => '[Add PayPal API signature here]'
)
end
Once you have a live PayPal Website Payments Pro (US) account, the API credentials should be placed in your production.rb file as follows:
# Setup ActiveMerchant for production mode with Paypal Website Payments Pro
config.after_initialize do
ActiveMerchant::Billing::Base.mode = :production
::GATEWAY = ActiveMerchant::Billing::PaypalGateway.new(
:login => '[Add PayPal API login here]',
:password => '[Add PayPal API password here]',
:signature => '[Add PayPal API signature here]'
)
end
Create a wrapper for the PayPal recurring API resources
As shown on the PayPal API Reference, there are four API resources for managing recurring payments:
- CreateRecurringPaymentsProfile
- UpdateRecurringPaymentsProfile
- GetRecurringPaymentsProfileDetails
- ManageRecurringPaymentsProfileStatus
PayPal manages recurring payments by creating ‘payment profiles’ that include details about the recurring payment, including the periodicity, amount, payment source, etc. We need to create a wrapper for each of these resources so we can easily interact with the PayPal API to create, update, suspend, and cancel payment profiles.
Create a file named ‘paypal_recurring_payments.rb’ in the following location:
/vendor/plugins/active_merchant/lib/active_merchant/billing/gateways/paypal/
and if you’re using a gem then in this path
[path where ruby is installed]/lib/ruby/gems/1.8/gems/active_merchant-1.5.2/lib/active_merchant/billing/gateways/paypal/
- Add the following code to the ‘paypal_recurring_payments.rb’ file:
module ActiveMerchant #:nodoc:
module Billing #:nodoc:
class PaypalGateway < Gateway
RECURRING_ACTIONS = Set.new([:add, :modify, :inquiry, :suspend, :reactivate, :cancel])
# Creates a recurring profile
def create_recurring_profile(*args) #:nodoc:
request = build_recurring_request(:add, *args)
commit('CreateRecurringPaymentsProfile', request)
end
# Updates a recurring profile
def update_recurring_profile(*args) #:nodoc:
request = build_recurring_request(:modify, *args)
commit('UpdateRecurringPaymentsProfile', request)
end
# Retrieves information about a recurring profile
def get_recurring_profile(*args) #:nodoc:
request = build_recurring_request(:inquiry, *args)
commit('GetRecurringPaymentsProfileDetails', request)
end
# Suspends a recurring profile
def suspend_recurring_profile(*args) #:nodoc:
request = build_recurring_request(:suspend, *args)
commit('ManageRecurringPaymentsProfileStatus', request)
end
# Reactivates a previously suspended recurring profile
def reactivate_recurring_profile(*args) #:nodoc:
request = build_recurring_request(:reactivate, *args)
commit('ManageRecurringPaymentsProfileStatus', request)
end
# Cancels an existing recurring profile
def cancel_recurring_profile(*args) #:nodoc:
request = build_recurring_request(:cancel, *args)
commit('ManageRecurringPaymentsProfileStatus', request)
end
private
def build_recurring_request(action, *args) #:nodoc:
unless RECURRING_ACTIONS.include?(action)
raise ArgumentError, "Invalid Recurring Profile Action: #{action}"
end
xml = Builder::XmlMarkup.new :indent => 2
ns2 = 'n2:'
profile_id = args.first[:profile_id] unless args.first[:profile_id].blank?
note = args.first[:note] unless args.first[:note].blank?
credit_card = args.first[:credit_card] unless args.first[:credit_card].blank?
recurring = args.first[:recurring] unless args.first[:recurring].blank?
initial = args.first[:initial] unless args.first[:initial].blank?
trial = args.first[:trial] unless args.first[:trial].blank?
currency = args.first[:currency] unless args.first[:currency].blank?
if [:add].include?(action)
xml.tag!('CreateRecurringPaymentsProfileReq', 'xmlns' => PAYPAL_NAMESPACE) do
xml.tag!('CreateRecurringPaymentsProfileRequest', 'xmlns:n2' => EBAY_NAMESPACE) do
xml.tag!(ns2 + 'Version', API_VERSION)
xml.tag!(ns2 + 'CreateRecurringPaymentsProfileRequestDetails') do
xml.tag!(ns2 + 'CreditCard') do
xml.tag!(ns2 + 'CreditCardType', credit_card[:type].capitalize) # Required
xml.tag!(ns2 + 'CreditCardNumber', credit_card[:number]) # Required
xml.tag!(ns2 + 'ExpMonth', format(credit_card[:month], :two_digits)) # Required
xml.tag!(ns2 + 'ExpYear', format(credit_card[:year], :four_digits)) # Required
xml.tag!(ns2 + 'CVV2', credit_card[:verification_value]) # Required
if ['switch', 'solo'].include?(credit_card[:type].downcase) # Needed for Maestro/Switch and Solo cards only
xml.tag!(ns2 + 'StartMonth', format(credit_card[:start_month], :two_digits)) unless credit_card[:start_month].blank? # Required if Switch or Solo, unless Issue Number provided
xml.tag!(ns2 + 'StartYear', format(credit_card[:start_year], :four_digits)) unless credit_card[:start_year].blank? # Required if Switch or Solo, unless Issue Number provided
xml.tag!(ns2 + 'IssueNumber', format(credit_card[:issue_number], :two_digits)) unless credit_card[:issue_number].blank? # Required if Switch or Solo, unless Issue Number provided
end
xml.tag!(ns2 + 'CardOwner') do
xml.tag!(ns2 + 'PayerName') do
xml.tag!(ns2 + 'FirstName', credit_card[:first_name]) # Required
xml.tag!(ns2 + 'LastName', credit_card[:last_name]) # Required
end
xml.tag!(ns2 + 'Payer', credit_card[:email]) # Required
xml.tag!(ns2 + 'Address') do
xml.tag!(ns2 + 'Street1', credit_card[:street_1]) # Required
xml.tag!(ns2 + 'Street2', credit_card[:street_2]) unless credit_card[:street_2].blank? # Optional
xml.tag!(ns2 + 'CityName', credit_card[:city]) # Required
xml.tag!(ns2 + 'StateOrProvince', credit_card[:state]) # Required
xml.tag!(ns2 + 'PostalCode', credit_card[:zip]) # Required
xml.tag!(ns2 + 'Country', credit_card[:country]) # Required
end
end
end
xml.tag!(ns2 + 'RecurringPaymentsProfileDetails') do
xml.tag!(ns2 + 'BillingStartDate', recurring[:billing_start_date]) # Required
end
xml.tag!(ns2 + 'ScheduleDetails') do
xml.tag!(ns2 + 'Description', recurring[:description]) # Required
frequency, period = get_pay_period(recurring[:periodicity])
xml.tag!(ns2 + 'PaymentPeriod') do
xml.tag!(ns2 + 'BillingPeriod', period) # Required
xml.tag!(ns2 + 'BillingFrequency', frequency.to_s) # Required
xml.tag!(ns2 + 'Amount', amount(recurring[:amount]), 'currencyID' => currency) # Required
xml.tag!(ns2 + 'TotalBillingCycles', recurring[:total_billing_cycles]) unless recurring[:total_billing_cycles].blank? # Optional
end
xml.tag!(ns2 + 'MaxFailedPayments', recurring[:max_failed_payments]) unless recurring[:max_failed_payments].blank? # Optional
xml.tag!(ns2 + 'AutoBillOutstandingAmount', recurring[:auto_bill_outstanding_amount] ? 'AddToNextBilling' : 'NoAutoBill') # Required
unless trial.blank? # Optional
frequency, period = get_pay_period(trial[:periodicity])
xml.tag!(ns2 + 'TrialPeriod') do
xml.tag!(ns2 + 'BillingPeriod', period) # Required
xml.tag!(ns2 + 'BillingFrequency', frequency.to_s) # Required
xml.tag!(ns2 + 'Amount', amount(trial[:amount]), 'currencyID' => currency) # Required
xml.tag!(ns2 + 'TotalBillingCycles', trial[:total_billing_cycles]) # Required
end
end
unless initial.blank? # Optional
xml.tag!(ns2 + 'ActivationDetails') do
xml.tag!(ns2 + 'InitialAmount', amount(initial[:amount]), 'currencyID' => currency) # Required
xml.tag!(ns2 + 'FailedInitialAmountAction', initial[:failure_action]) unless initial[:failure_action].blank? # Optional
end
end
end
end
end
end
elsif [:modify].include?(action)
xml.tag!('UpdateRecurringPaymentsProfileReq', 'xmlns' => PAYPAL_NAMESPACE) do
xml.tag!('UpdateRecurringPaymentsProfileRequest', 'xmlns:n2' => EBAY_NAMESPACE) do
xml.tag!(ns2 + 'Version', API_VERSION)
xml.tag!(ns2 + 'UpdateRecurringPaymentsProfileRequestDetails') do
xml.tag!(ns2 + 'ProfileID', profile_id) # Required
xml.tag!(ns2 + 'Note', note) unless note.blank? # Optional
unless credit_card.blank? # Optional
xml.tag!(ns2 + 'CreditCard') do
xml.tag!(ns2 + 'CreditCardType', credit_card[:type].capitalize) # Required
xml.tag!(ns2 + 'CreditCardNumber', credit_card[:number]) # Required
xml.tag!(ns2 + 'ExpMonth', format(credit_card[:month], :two_digits)) # Required
xml.tag!(ns2 + 'ExpYear', format(credit_card[:year], :four_digits)) # Required
xml.tag!(ns2 + 'CVV2', credit_card[:verification_value]) # Required
if ['switch', 'solo'].include?(credit_card[:type].downcase) # Needed for Maestro/Switch and Solo cards only
xml.tag!(ns2 + 'StartMonth', format(credit_card[:start_month], :two_digits)) unless credit_card[:start_month].blank? # Required if Switch or Solo, unless Issue Number provided
xml.tag!(ns2 + 'StartYear', format(credit_card[:start_year], :four_digits)) unless credit_card[:start_year].blank? # Required if Switch or Solo, unless Issue Number provided
xml.tag!(ns2 + 'IssueNumber', format(credit_card[:issue_number], :two_digits)) unless credit_card[:issue_number].blank? # Required if Switch or Solo, unless Issue Number provided
end
xml.tag!(ns2 + 'CardOwner') do
xml.tag!(ns2 + 'PayerName') do
xml.tag!(ns2 + 'FirstName', credit_card[:first_name]) # Required
xml.tag!(ns2 + 'LastName', credit_card[:last_name]) # Required
end
xml.tag!(ns2 + 'Payer', credit_card[:email]) unless credit_card[:email].blank? # Optional
xml.tag!(ns2 + 'Address') do # Required
xml.tag!(ns2 + 'Street1', credit_card[:street_1]) # Required
xml.tag!(ns2 + 'Street2', credit_card[:street_2]) unless credit_card[:street_2].blank? # Optional
xml.tag!(ns2 + 'CityName', credit_card[:city]) # Required
xml.tag!(ns2 + 'StateOrProvince', credit_card[:state]) # Required
xml.tag!(ns2 + 'PostalCode', credit_card[:zip]) # Required
xml.tag!(ns2 + 'Country', credit_card[:country]) # Required
end
end
end
end
unless recurring.blank? # Optional
xml.tag!(ns2 + 'PaymentPeriod') do
xml.tag!(ns2 + 'Amount', amount(recurring[:amount]), 'currencyID' => currency) unless recurring[:amount].blank? # Optional
xml.tag!(ns2 + 'TotalBillingCycles', recurring[:total_billing_cycles]) unless recurring[:total_billing_cycles].blank? # Optional
end
end
unless trial.blank? # Optional
xml.tag!(ns2 + 'TrialPeriod') do
xml.tag!(ns2 + 'Amount', amount(trial[:amount]), 'currencyID' => currency) unless trial[:amount].blank? # Optional
xml.tag!(ns2 + 'TotalBillingCycles', trial[:total_billing_cycles]) unless trial[:total_billing_cycles].blank? # Optional
end
end
end
end
end
elsif [:suspend, :reactivate, :cancel].include?(action)
xml.tag!('ManageRecurringPaymentsProfileStatusReq', 'xmlns' => PAYPAL_NAMESPACE) do
xml.tag!('ManageRecurringPaymentsProfileStatusRequest', 'xmlns:n2' => EBAY_NAMESPACE) do
xml.tag!(ns2 + 'Version', API_VERSION)
xml.tag!(ns2 + 'ManageRecurringPaymentsProfileStatusRequestDetails') do
raise ArgumentError, 'Invalid Request: Missing Profile ID' if profile_id.blank?
xml.tag!(ns2 + 'ProfileID', profile_id) # Required
xml.tag!(ns2 + 'Note', note) unless note.blank? # Optional
raise ArgumentError, 'Invalid Request: Unrecognized Action' unless ['suspend', 'reactivate', 'cancel'].include?(action.to_s)
xml.tag!(ns2 + 'Action', action.to_s.capitalize) # Required
end
end
end
elsif [:inquiry].include?(action)
xml.tag!('GetRecurringPaymentsProfileDetailsReq', 'xmlns' => PAYPAL_NAMESPACE) do
xml.tag!('GetRecurringPaymentsProfileDetailsRequest', 'xmlns:n2' => EBAY_NAMESPACE) do
xml.tag!(ns2 + 'Version', API_VERSION)
raise ArgumentError, 'Invalid Request: Missing Profile ID' if profile_id.blank?
xml.tag!('ProfileID', profile_id) # Required
end
end
end
end
def get_pay_period(period) #:nodoc:
case period
when :daily then [1, 'Day']
when :weekly then [1, 'Week']
when :biweekly then [2, 'Week']
when :semimonthly then [1, 'SemiMonth']
when :quadweekly then [4, 'Week']
when :monthly then [1, 'Month']
when :quarterly then [3, 'Month']
when :semiyearly then [6, 'Month']
when :yearly then [1, 'Year']
end
end
end
end
end
Note: I haven’t wrapped 100% of PayPal’s API. I’ve only wrapped the resources that I needed for my particular application, which was the bare minimum. You should read the PayPal API Documentation to see what additional resources are available. You can modify the ‘paypal_recurring_payments.rb’ file as needed to fit your project.
Open ‘paypal.rb’ (located in …/gateways/) and add the noted line of code to the top of the file as shown here:
require File.dirname(FILE) + ‘/paypal/paypal_common_api’ require File.dirname(FILE) + ‘/paypal_express’ require File.dirname(FILE) + ‘/paypal/paypal_recurring_payments’ # Add this line
module ActiveMerchant #:nodoc:
module Billing #:nodoc: class PaypalGateway < Gateway include PaypalCommonAPI
Restart your local server if you have it running (plugins are only reloaded on server-startup)
Test your PayPal API wrapper by creating a recurring payment
Create a new controller called ‘payments_controller.rb’
In the ‘new’ action, add the following code:
credit_card = {
:type => '[cc type, e.g. 'visa']', :number => '[cc number, from test account]', :verification_value => '123', :month => '[expiration month, from test account]', :year => '[expiration year, from test account]', :first_name => '[first name, can be anything]', :last_name => '[last name, can be anything]', :street_1 => '[street address, can be anything]', :city => '[city, can be anything]', :state => '[state, can be anything]', :country => '[use 'US' if you used a U.S. state code above]', :zip => '[zip code, can be anything]', :email => '[payor email address, can be anything]'
}
# I have the ‘billing_start_date’ set to one-month from today, but # this can be set to any date as long as it’s not in the past recurring = {
:description => '[text description of the transaction]', :billing_start_date => Time.now.advance(:months => 1).iso8601, :periodicity => :monthly, :amount => [amount in cents], :auto_bill_outstanding_amount => true
}
# Initial payment is optional, read the PayPal API docs to learn more initial = {
:amount => [amount in cents]
}
# Trial is optional, read the PayPal API docs to learn more trial = {
:periodicity => :monthly, :total_billing_cycles => 1, :amount => [amount in cents]
}
currency = ‘USD’
@response = GATEWAY.create_recurring_profile(
:credit_card => credit_card, :address => address, :recurring => recurring, :initial => initial, :trial => trial, :currency => currency)
Note: You shouldn’t use real credit card information for testing purposes. PayPal will provide you with dummy credit cards if you create a new buyer test account in the PayPal Sandbox. You can use the create ‘Preconfigured’ account option to get new buyer accounts.
Add ‘map.resources :payments’ to your project’s routes.rb file for the new resource
Add a new view for the resource in ‘views/payments/’ named ‘new.html.erb’ with the following code included in the file, so we can read the response from PayPal:
<%=h @response.message %>
In your browser, navigate to the resource at http://localhost:3000/payments/new and wait for the PayPal response. If everything worked correctly, you should see ‘Success’.
Log into your PayPal Sandbox and view the recent activity of the Website Payments Pro (US) test account. You should see that a new recurring payments profile has been created. If you do, congrats! You’ve just created your first PayPal recurring payment profile.
You can use the ‘update_recurring_profile’ and ‘get_recurring_profile’ methods (see paypal_recurring_payments.rb) to update and retrieve profile information. Take note of which fields are required in the PayPal API Documentation. I’ve also tried to label them in the ‘paypal_recurring_payments.rb’ file.
Note: This tutorial has been taken from matthodan’s blog. Click here to see the original post
Can we have Recurring Payments with ActiveMerchant & PayPal Web Payments Pro?