Leon's notes

Aug 11

Paypal on Rails, gotchas and tricks

With the help of Activemerchant, integrate Paypal into Rails is really easy, yet there are some gotchas you should be aware of.

  1. The original ‘_ext-enter’ for ‘cmd’ is deprecated
  2. If you are to submit multiple line items, the ‘cmd’ value must be ‘_cart’, then you can use item_name_x/quantity_x/amount_x as the line items details
  3. The built in helper in Activemerchant do not provide the multiple line mapping, I’ved add one myself, the line_items method.
    then you can use it in your view like 
    - service.line_items line_items
  4. The shipping cost to whole cart should be set in ‘handling_cart’, instead of ‘shipping’ as suggested in Paypal’s document, this is really bad.

See the comments you can understand how the customized helper works

# We changed this paypal helper provided by activemerchant because:
# 
# 1) change the initializer to meet our need, for example, 
#   to submit mulitple items, the 'cmd' value should be '_cart',
#   see here: https://www.x.com/thread/43442
#   and here: https://www.x.com/docs/DOC-1340
#   the original '_ext-enter' for 'cmd' is deprecated
# 
# 2) add the line_items method to prepare details for each line item
# 
# 3) accroding to https://www.x.com/thread/39507
#   if you want to pass shpping cost for the whole cart, 
#   you should set up the 'handling_cart' instead of 'shipping' as documented
#   mapping :shipping, 'handling_cart'

module ActiveMerchant #:nodoc:
  module Billing #:nodoc:
    module Integrations #:nodoc:
      module Paypal
        class Helper < ActiveMerchant::Billing::Integrations::Helper
          def initialize(order, account, options = {})
            super
            # indecate we are using thirdparty shopping cart
            add_field('cmd', '_cart')
            add_field('upload', '1')


            add_field('no_shipping', '1')
            add_field('no_note', '1')
            add_field('charset', 'utf-8')
            add_field('address_override', '0')
            add_field('bn', application_id.to_s.slice(0,32)) unless application_id.blank?
          end
          
          # pass the shipping cost for whole cart
          mapping :shipping, 'handling_cart'

          # mapping header image
          mapping :cpp_header_image, 'cpp_header_image'

          # add line item details, note the amount_x 
          # should be price instead of line total   
          def line_items(items = [])
            items.each_with_index do |line, index|
              add_field("item_name_#{index+1}", line.item.item_name)
              add_field("amount_#{index+1}", line.price.value_without_unit)
              add_field("quantity_#{index+1}", line.quantity)
            end
          end          

        end
      end
    end
  end
end

Also, you need to create your own payment_service_for( I named it paypal_payment_service_for) to support encrypt data before sending to paypal, because the built in payment_service_for in Activemerchant does not suppor that

require_library_or_gem 'action_pack'

module ActiveMerchant #:nodoc:
  module Billing #:nodoc:
    module Integrations #:nodoc:
      module ActionViewHelper
        def paypal_payment_service_for(order, account, options = {}, &proc)
          raise ArgumentError, "Missing block" unless block_given?

          integration_module = ActiveMerchant::Billing::Integrations.const_get(options.delete(:service).to_s.camelize)

          encrypt = options.delete(:encrypt)

          result = []
          result << form_tag(integration_module.service_url, options.delete(:html) || {})

          service_class = integration_module.const_get('Helper')
          service = service_class.new(order, account, options)

          result << capture(service, &proc)

          if encrypt
            paypal_params = { :cert_id => cert_id }
            service.form_fields.each do |field, value|
              paypal_params.merge!(field => value)
            end

            result << hidden_field_tag(:cmd, "_s_xclick")
            result << "\n"
            result << hidden_field_tag(:encrypted, encrypt_for_paypal(paypal_params))
          else
            service.form_fields.each do |field, value|
              result << hidden_field_tag(field, value)
            end
          end

          result << ''
          result= result.join("\n")

          concat(result.respond_to?(:html_safe) ? result.html_safe : result)
          nil
        end

        # encrypt values
        def encrypt_for_paypal(values)
          unless Settings.payment.paypal.test_mode
            app_cert_pem = File.read("#{Rails.root}/config/paypal/paypal-pubcert.pem")
            app_key_pem = File.read("#{Rails.root}/config/paypal/paypal-prvkey.pem")
            paypal_cert_pem = File.read("#{Rails.root}/config/paypal/paypal-cert.pem")
          else
            app_cert_pem = File.read("#{Rails.root}/config/paypal/paypal-sandbox-pubcert.pem")
            app_key_pem = File.read("#{Rails.root}/config/paypal/paypal-sandbox-prvkey.pem")
            paypal_cert_pem = File.read("#{Rails.root}/config/paypal/paypal-sandbox-cert.pem")
          end

          signed = OpenSSL::PKCS7::sign(OpenSSL::X509::Certificate.new(app_cert_pem), OpenSSL::PKey::RSA.new(app_key_pem, ''), values.map { |k, v| "#{k}=#{v}" }.join("\n"), [], OpenSSL::PKCS7::BINARY)
          OpenSSL::PKCS7::encrypt([OpenSSL::X509::Certificate.new(paypal_cert_pem)], signed.to_der, OpenSSL::Cipher::Cipher::new("DES3"), OpenSSL::PKCS7::BINARY).to_s.gsub("\n", "")
        end

        def cert_id
          Settings.payment.paypal.certificate_id
        end
      end
    end
  end
end