module ActiveMerchant #:nodoc: module Billing #:nodoc: class PayflowNvpUkGateway < Gateway TEST_URL = 'https://pilot-payflowpro.paypal.com' LIVE_URL = 'https://payflowpro.paypal.com' self.class_inheritable_accessor :partner self.class_inheritable_accessor :timeout # Enable safe retry of failed connections # Payflow is safe to retry because retried transactions use the same # X-VPS-Request-ID header. If a transaction is detected as a duplicate # only the original transaction data will be used by Payflow, and the # subsequent Responses will have a :duplicate parameter set in the params # hash. self.retry_safe = true # The countries the gateway supports merchants from as 2 digit ISO country codes self.supported_countries = ['GB'] # The card types supported by the payment gateway self.supported_cardtypes = [:visa, :master, :american_express, :discover, :solo, :switch] # The homepage URL of the gateway self.homepage_url = 'https://www.paypal.com/uk/cgi-bin/webscr?cmd=_wp-pro-overview-outside' # The name of the gateway self.display_name = 'PayPal Website Payments Pro (UK) [NVP API]' self.default_currency = 'GBP' self.partner = 'PayPalUk' self.money_format = :dollars self.timeout = 30 TRANSACTIONS = { :purchase => "S", :authorization => "A", :capture => "D", :void => "V", :credit => "C", :recurring => "R" } PERIODS = { :daily => "EDAY", :weekly => "WEEK", :biweekly => "BIWK", :semimonthly => "SMMO", :quadweekly => "FRWK", :monthly => "MONT", :quarterly => "QTER", :semiyearly => "SMYR", :yearly => "YEAR" } RECURRING_ACTIONS = { :create => "A", :modify => "M", :deactivate => "C", :reactivate => "R", :inquiry => "I" } # Creates a new PayflowNvpUkGateway # # The gateway requires that a valid login and password be passed # in the +options+ hash. # # ==== Parameters # # * options # * :login - Your Payflow login # * :password - Your Payflow password # * :vendor - Your vendor ID. If not present then the login is used def initialize(options = {}) requires!(options, :login, :password) @options = options @options[:partner] = partner if @options[:partner].blank? @options[:vendor] = options[:login] if @options[:vendor].blank? super end # Return the url of the gateway def gateway_url return test? ? TEST_URL : LIVE_URL end # Is the gateway in test mode? def test? @options[:test] || super end # Performs an authorization transaction. This is required due to Visa and # Mastercard restrictions, in that a customer's card cannot be charged # until the goods have been dispatched. An authorization allows an amout # to be reserved on a customer's card, bringing down the available # balance or credit, but they are not actually charged until a subsequent # capture step actually captures the funds. # # ==== Parameters # # * money - The amount to be charged as an integer value in cents. # * creditcard - The CreditCard object to be used as a funding source for the transaction. # * options - A hash of optional parameters # * :order_id - A unique reference for this order (maximum of 127 characters). # * :email - The customer's email address # * :customer - A unique reference for the customer (maximum of 12 characters). # * :ip - The customer's IP address # * :currency - The currency of the transaction. If present must be one of { AUD, CAD, EUR, JPY, GBP or USD }. If omitted the default currency is used. # * :billing_address - The customer's billing address as a hash of address information. # * :address1 - The billing address street # * :city - The billing address city # * :state - The billing address state # * :country - The 2 digit ISO billing address country code # * :zip - The billing address zip code # * :shipping_address - The customer's shipping address as a hash of address information. # * :address1 - The shipping address street # * :city - The shipping address city # * :state - The shipping address state code # * :country - The 2 digit ISO shipping address country code # * :zip - The shipping address zip code def authorize(money, creditcard, options = {}) post = {} add_invoice(post, options) add_creditcard(post, creditcard) add_currency(post, money, options) add_address(post, options[:billing_address] || options[:address]) add_address(post, options[:shipping_address], "shipto") add_customer_data(post, options) commit(TRANSACTIONS[:authorize], money, post) end # A purchase transaction authorizes and captures in a single hit. We can only # do this for transactions where you provide immediate fulfillment of products or services. # # ==== Parameters # # * money - The amount to be charged as an integer value in cents. # * creditcard - The CreditCard object to be used as a funding source for the transaction. # * options - A hash of optional parameters # * :order_id - A unique reference for this order (maximum of 127 characters). # * :email - The customer's email address # * :customer - A unique reference for the customer (maximum of 12 characters). # * :ip - The customer's IP address # * :currency - The currency of the transaction. If present must be one of { AUD, CAD, EUR, JPY, GBP or USD }. If ommitted the default currency is used. # * :billing_address - The customer's billing address as a hash of address information. # * :address1 - The billing address street # * :city - The billing address city # * :state - The billing address state # * :country - The 2 digit ISO billing address country code # * :zip - The billing address zip code # * :shipping_address - The customer's shipping address as a hash of address information. # * :address1 - The shipping address street # * :city - The shipping address city # * :state - The shipping address state code # * :country - The 2 digit ISO shipping address country code # * :zip - The shipping address zip code def purchase(money, creditcard, options = {}) post = {} add_invoice(post, options) add_creditcard(post, creditcard) add_currency(post, money, options) add_address(post, options[:billing_address] || options[:address]) add_address(post, options[:shipping_address], "shipto") add_customer_data(post, options) commit(TRANSACTIONS[:purchase], money, post) end # Captures authorized funds. # # ==== Parameters # # * money - The amount to be authorized as an integer value in cents. Payflow does support changing the captured amount, so whatever is passed here will be captured. # * authorization - The authorization reference string returned by the original transaction's Response#authorization. # * options - not currently used. def capture(money, authorization, options = {}) post = {} post[:origid] = authorization commit(TRANSACTIONS[:capture], money, post) end # Voids an authorization or delayed capture # # ==== Parameters # # * authorization - The authorization reference string returned by the original transaction's Response#authorization. # * options - Not currently used. def void(authorization, options = {}) post = {} post[:origid] = authorization commit(TRANSACTIONS[:void], nil, post) end # Process a refund to a customer. # # ==== Parameters # * money - The amount to be credited as an integer value in cents. # * authorization_or_card - The CreditCard you want to refund to OR the PayPal PNRef of a previous transaction. It depends on the settings in your PayPal account as to whether non referenced credits are permitted. The default is that they are not. # * options - not currently used def credit(money, authorization_or_card, options = {}) post = {} if authorization_or_card.is_a?(String) # perform a referenced credit post[:origid] = authorization else # perform an unreferenced credit add_creditcard(post, creditcard) end commit(TRANSACTIONS[:credit], money, post) end # Create or modify a recurring profile. # # ==== Parameters # # * money - The amount that the recurring profile is to be set up for as an integer value in cents. # * creditcard - The CreditCard object to be used as a funding source for the recurring profile. # * options - A hash of parameters (some optional). # * :profile_id - If present then we are modifying an existing profile, and this :profile_id identifies the profile we want to amend. If not present then we are creating a new recurring payments profile. # * :starting_at - Takes a Date, Time or string in MMDDYYYY format. The date must be in the future. # * :name - The name of the customer to be billed. If omitted the name from the creditcard is used. # * :periodicity - The frequency that the recurring payments will occur at. Can be one of: [:daily, :weekly, :biweekly (every 2 weeks), :semimonthly (twice every month), :quadweekly (once every 4 weeks), :monthly (every month on the same date as the first payment), :quarterly (every 3 months on the same date as the first payment), :semiyearly (every 6 months on the same date as the first payment), :yearly. # * :payments - Integer value describing the number of payments to be made. If set to 0 then profile will continue until terminated # * :comment - Optional description of the goods or service being purchased # * :max_failed_payments - The number of payments that are allowed to fail before PayPal suspends the profile. Defaults to 0 which means PayPal will never suspend the profile until the term is completed. PayPal will keep attempting to process failed payments. # * :currency - The currency of the transaction. If present must be one of { AUD, CAD, EUR, JPY, GBP or USD }. If omitted the default currency is used. # * :description - The description of the profile. Required for the profile to be created successfully. def recurring(money, creditcard, options = {}) post = {} add_creditcard(post, creditcard) add_currency(post, money, options) add_address(post, options[:billing_address] || options[:address]) add_address(post, options[:shipping_address], "shipto") add_customer_data(post, options) add_recurring_info(post, creditcard, options) commit(TRANSACTIONS[:recurring], money, post) end # Inquire about the status of a previously created recurring profile. # # ==== Parameters # # * profile_id - the id of the recurring profile we want to get the details of. # * options - not currently used def recurring_inquiry(profile_id, options = {}) post = {} post[:action] = RECURRING_ACTIONS[:inquiry] post[:origprofileid] = profile_id.to_s commit(TRANSACTIONS[:recurring], nil, post) end # Cancel a recurring profile. # # ==== Parameters # # * profile_id - the id of the recurring profile to cancel def cancel_recurring(profile_id) post = {} post[:action] = RECURRING_ACTIONS[:deactivate] post[:origprofileid] = profile_id.to_s commit(TRANSACTIONS[:recurring], nil, post) end private def add_customer_data(post, options) post[:email] = options[:email].to_s if options.has_key?(:email) post[:custref] = options[:customer].to_s.slice(0,12) if options[:customer] post[:custip] = options[:ip].to_s if options[:ip] end # NOTE : If you pass in any of the ship-to address parameters such as SHIPTOCITY or # SHIPTOSTATE, you must pass in the complete set (that is, SHIPTOSTREET, # SHIPTOCITY, SHIPTOSTATE, SHIPTOCOUNTRY, and SHIPTOZIP). def add_address(post, address, prefix = '') unless address.blank? or address.values.blank? post[prefix+"street"] = address[:address1].to_s post[prefix+"city"] = address[:city].to_s post[prefix+"state"] = address[:state].blank? ? 'n/a' : address[:state] post[prefix+"country"] = address[:country].to_s post[prefix+"zip"] = address[:zip].to_s.sub(/\s+/,'') end end def add_invoice(post, options) post[:invnum] = (options[:invoice] || options[:order_id]).to_s.slice(0,127) if options.has_key?(:invoice) || options.has_key?(:order_id) end def add_creditcard(post, creditcard) unless creditcard.nil? post[:acct] = creditcard.number post[:cvv2] = creditcard.verification_value post[:expdate] = expdate(creditcard) post[:name] = creditcard.name end end def add_currency(post, money, options) post[:currency] = options[:currency] || currency(money) end def add_recurring_info(post, creditcard, options) if options.has_key?(:profile_id) post[:action] = RECURRING_ACTIONS[:modify] post[:origprofileid] = options[:profile_id] else post[:action] = RECURRING_ACTIONS[:create] end post[:start] = format_date(options[:starting_at]) unless options[:starting_at].nil? post[:term] = options[:payments] unless options[:payments].nil? post[:payperiod] = PERIODS[options[:periodicity]] unless options[:periodicity].nil? post[:desc] = options[:comment] unless options[:comment].nil? post[:maxfailpayments] = options[:max_failed_payments] unless options[:max_failed_payments].nil? post[:profilename] = (options[:name] || creditcard.name).to_s.slice(0,128) unless options[:name].nil? && creditcard.nil? post[:desc] = options[:description] unless options[:description].nil? post[:optionaltrxamt] = options[:optionaltrxamt] unless options[:optionaltrxamt].nil? post[:failedinitamtaction] = options[:failedinitamtaction] unless options[:failedinitamtaction].nil? end def format_date(time) case time when Time, Date then time.strftime("%m%d%Y") else time.to_s end end def expdate(creditcard) year = sprintf("%.04i", creditcard.year.to_i) month = sprintf("%.02i", creditcard.month.to_i) "#{month}#{year[-2..-1]}" end def parse(body) results = {} body.split(/&/).each do |pair| key,val = pair.split(/=/) results[key] = val end results end def build_request(parameters, action = nil) post = {} post[:user] = @options[:login] post[:pwd] = @options[:password] post[:partner] = @options[:partner] post[:vendor] = @options[:vendor] post[:trxtype] = action if action post[:tender] = "C" post[:verbosity] = "MEDIUM" request = post.merge(parameters).sort { |a, b| a[0].to_s <=> b[0].to_s }.map { |pair| "#{pair[0].to_s.upcase}=#{CGI.escape(pair[1].to_s)}" }.join("&") #.map { |kv_pair| "#{kv_pair[0].to_s.upcase}=#{CGI.escape(kv_pair[1].to_s)}" }.join("&") request end def build_headers(content_length) { "Content-Type" => "text/namevalue", "Content-Length" => content_length.to_s, "X-VPS-Client-Timeout" => timeout.to_s, "X-VPS-VIT-Integration-Product" => "ActiveMerchant", "X-VPS-VIT-Integration-Version" => ActiveMerchant::VERSION, "X-VPS-VIT-Runtime-Version" => RUBY_VERSION, "X-VPS-Request-ID" => Utils.generate_unique_id } end def commit(action, money, parameters) parameters[:amt] = amount(money) if money request = build_request(parameters, action) headers = build_headers(request.size) response = parse(ssl_post(test? ? TEST_URL : LIVE_URL, request, headers)) Response.new(response["RESULT"] == "0", response["RESPMSG"], response, :authorization => response["PNREF"], :test => test?, :cvv_result => response["PROCCVV2"], :avs_result => { :code => response["PROCAVS"], :postal_match => response["AVSZIP"], :street_match => response["AVSADDR"] } ) end end end end