Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Introduce Web Payment API and split in 2 files: payline_direct_api an…

…d payline_web_api
  • Loading branch information...
commit 3f493021c77505a1ecaf2688fb35c2462a09cf43 1 parent 3d946e4
@samleb samleb authored
View
164 lib/active_merchant/billing/gateways/payline.rb
@@ -1,169 +1,41 @@
-require 'active_merchant/billing/iso_4217_currency_codes'
-require 'savon'
+require 'active_merchant/billing/gateways/payline/payline_common'
+require 'active_merchant/billing/gateways/payline/payline_direct_api'
+require 'active_merchant/billing/gateways/payline/payline_web_api'
module ActiveMerchant
module Billing
- class Payline < Gateway
+ class PaylineGateway < Gateway
+ include PaylineCommon
+ include PaylineDirectAPI
+ include PaylineWebAPI
+
API_VERSION = '4.24'.freeze
- self.display_name = 'Payline DirectPayment'
+ self.display_name = 'Payline'
self.homepage_url = 'http://www.payline.com/'
self.default_currency = 'EUR'
self.money_format = :cents
self.supported_cardtypes = [:visa, :master, :american_express, :diners_club, :jcb]
+ class_attribute :web_live_url, :web_test_url, :instance_writer => false
+
self.live_url = 'https://services.payline.com/V4/services/DirectPaymentAPI'.freeze
self.test_url = 'https://homologation.payline.com/V4/services/DirectPaymentAPI'.freeze
- include ISO4217CurrencyCodes
-
- IMPL_NAMESPACE = 'http://impl.ws.payline.experian.com'.freeze
- OBJ_NAMESPACE = 'http://obj.ws.payline.experian.com'.freeze
-
- LOG_FILTERED_TAGS = %w( number cvx ).freeze
+ self.web_live_url = 'https://services.payline.com/V4/services/WebPaymentAPI/'.freeze
+ self.web_test_url = 'https://homologation.payline.com/V4/services/WebPaymentAPI/'.freeze
- DATE_FORMAT = "%d/%m/%Y %H:%M".freeze
- EXPIRATION_DATE_FORMAT = "%.2d%.2d".freeze
-
- SUCCESS_CODE = '00000'.freeze
-
- ACTION_CODES = {
- :authorization => 100,
- :full_capture => 201
- }.freeze
-
- PAYMENT_MODES = {
- :direct => 'CPT',
- :deffered => 'DIF',
- :installments => 'NX',
- :recurrent => 'REC'
- }.freeze
-
- CARD_BRAND_CODES = Hash.new('CB').update(
- 'visa' => 'VISA',
- 'master' => 'MASTERCARD',
- 'american_express' => 'AMEX',
- 'diners_club' => 'DINERS',
- 'jcb' => 'JCB',
- 'switch' => 'SWITCH',
- 'maestro' => 'MAESTRO'
- ).freeze
+ include ISO4217CurrencyCodes
def initialize(options = {})
+ # FIXME: should not be blank!
requires!(options, :merchant_id, :merchant_access_key, :contract_number)
@options = options
end
-
- def authorize(money, credit_card, options = {})
- requires!(options, :order_id)
- currency = currency_code(options[:currency])
- request :do_authorization do |xml|
- add_payment(xml, money, currency, :authorization)
- add_credit_card(xml, credit_card)
- add_order(xml, money, currency, options)
- end
- end
-
- def capture(money, authorization, options = {})
- request :do_capture do |xml|
- xml.transactionID authorization
- add_payment(xml, money, currency_code(options[:currency]), :full_capture)
- end
- end
-
- protected
- def request(method_name)
- response = savon_client.request(:"#{method_name}_request") do
- soap.namespaces[:xmlns] = IMPL_NAMESPACE
- soap.namespaces[:'xmlns:obj'] = OBJ_NAMESPACE
- xml = Builder::XmlMarkup.new
- yield xml
- soap.body = xml.target!
- end
- if response.success?
- response = response.to_hash[:"#{method_name}_response"]
- message = message_from(response[:result])
- transaction_id = response[:transaction][:id] if String === response[:transaction][:id]
- Response.new(success?(response), message, response, {
- :authorization => transaction_id,
- :test => test?
- })
- else
- fault = response.to_hash[:fault]
- message = "#{fault[:faultcode]} #{fault[:faultstring]}"
- Response.new(false, message, {}, { :test => test? })
- end
- end
-
- def savon_client
- @savon_client ||= Savon.client do
- config.raise_errors = false
- configure_logger(config)
- wsdl.namespace = IMPL_NAMESPACE
- wsdl.endpoint = test? ? test_url : live_url
- http.headers["Authorization"] = basic_authentication_header
- end
- end
-
- def configure_logger(config)
- if logger
- config.logger = logger
- config.logger.filter.push(*LOG_FILTERED_TAGS)
- else
- config.log = false
- end
- end
-
- def basic_authentication_header
- string = [options[:merchant_id], options[:merchant_access_key]].join(':')
- "Basic #{strict_encode64(string)}"
- end
-
- # Base64.strict_encode64 is only available in 1.9 stdlib
- def strict_encode64(string)
- [string].pack('m0')
- end
-
- def add_payment(xml, money, currency, action)
- xml.payment do
- xml.obj :action, ACTION_CODES[action]
- xml.obj :amount, money
- xml.obj :currency, currency
- xml.obj :mode, PAYMENT_MODES[:direct]
- xml.obj :contractNumber, options[:contract_number]
- end
- end
-
- def add_credit_card(xml, card)
- xml.card do
- xml.obj :number, card.number
- xml.obj :type, CARD_BRAND_CODES[card.brand]
- xml.obj :expirationDate, expiration_date(card.month, card.year)
- xml.obj :cvx, card.verification_value
- end
- end
-
- def expiration_date(month, year)
- EXPIRATION_DATE_FORMAT % [month, year.to_s[-2..-1]]
- end
-
- def add_order(xml, money, currency, options)
- xml.order do
- xml.obj :ref, options[:order_id]
- xml.obj :amount, money
- xml.obj :currency, currency
- xml.obj :date, (options[:date] || Time.now).strftime(DATE_FORMAT)
- end
- end
-
- def message_from(result)
- "#{result[:short_message]}: #{result[:long_message]} (code #{result[:code]})}"
- end
-
- def success?(response)
- response[:result][:code] == SUCCESS_CODE
- end
end
+
+ # Backwards-compatibility with 0.0.1
+ Payline = PaylineGateway
end
end
View
228 lib/active_merchant/billing/gateways/payline/payline_common.rb
@@ -0,0 +1,228 @@
+require 'active_merchant/billing/iso_4217_currency_codes'
+require 'savon'
+
+module ActiveMerchant
+ module Billing
+ module PaylineCommon
+ WEB_API_VERSION = '3'.freeze
+
+ IMPL_NAMESPACE = 'http://impl.ws.payline.experian.com'.freeze
+ OBJ_NAMESPACE = 'http://obj.ws.payline.experian.com'.freeze
+
+ LOG_FILTERED_TAGS = %w( number cvx ).freeze
+
+ DATE_FORMAT = "%d/%m/%Y %H:%M".freeze
+
+ SUCCESS_MESSAGES = {
+ # Card & Check
+ "00000" => "Transaction approved",
+ "01001" => "Transaction approved but required a verification by merchant",
+ # Wallet
+ "02500" => "Operation successful",
+ "02501" => "Operation successful but wallet will expire",
+ "02517" => "Cannot disable some wallet(s)",
+ "02520" => "Cannot enable some wallet(s)",
+ # Cancelling & Reauthorizing
+ "02616" => "Error while creating the wallet"
+ }.freeze
+
+ SUCCESS_CODES = SUCCESS_MESSAGES.keys.freeze
+
+ ACTION_CODES = {
+ :authorization => 100,
+ :purchase => 101, # Authorization + Capture
+ :capture => 201
+ }.freeze
+
+ PAYMENT_MODES = {
+ :direct => 'CPT',
+ :deffered => 'DIF',
+ :installments => 'NX',
+ :recurrent => 'REC'
+ }.freeze
+
+ # locale => ISO 639-2 code
+ LANGUAGE_CODES = {
+ 'fr' => 'fre',
+ 'en' => 'eng',
+ 'es' => 'spa',
+ 'it' => 'ita',
+ 'pt' => 'por',
+ 'de' => 'ger',
+ 'nl' => 'dut',
+ 'fi' => 'fin'
+ }.freeze
+
+ RECURRING_FREQUENCIES = {
+ :daily => 10,
+ :weekly => 20,
+ :fortnightly => 30,
+ :monthly => 40,
+ :bimonthly => 50,
+ :quarterly => 60,
+ :semiannual => 70,
+ :yearly => 80,
+ :biannual => 90
+ }.freeze
+
+ protected
+ def request(client, method_name)
+ response = client.request :"#{method_name}_request" do
+ soap.namespaces[:xmlns] = IMPL_NAMESPACE
+ soap.namespaces[:'xmlns:obj'] = OBJ_NAMESPACE
+ xml = Builder::XmlMarkup.new
+ yield xml
+ soap.body = xml.target!
+ end
+ if response.success?
+ response = response.to_hash[:"#{method_name}_response"]
+ build_response(response)
+ else
+ message = (response.soap_fault? ? response.soap_fault : response.http_error).to_s
+ Response.new(false, message, {}, { :test => test? })
+ end
+ end
+
+ def create_savon_client(endpoint)
+ Savon.client do
+ config.raise_errors = false
+ configure_logger(config)
+ wsdl.namespace = IMPL_NAMESPACE
+ wsdl.endpoint = endpoint
+ http.headers["Authorization"] = basic_authentication_header
+ end
+ end
+
+ def configure_logger(config)
+ if logger
+ config.logger = logger
+ config.logger.filter.push(*LOG_FILTERED_TAGS)
+ else
+ config.log = false
+ end
+ end
+
+ def basic_authentication_header
+ string = [options[:merchant_id], options[:merchant_access_key]].join(':')
+ "Basic #{strict_encode64(string)}"
+ end
+
+ # Base64.strict_encode64 is only available in 1.9 stdlib
+ def strict_encode64(string)
+ [string].pack('m0')
+ end
+
+ def build_response(response)
+ message = message_from(result = response[:result])
+ if transaction = response[:transaction]
+ transaction_id = transaction[:id] if String === transaction[:id]
+ end
+ format_response!(response = response.with_indifferent_access)
+ Response.new(success?(result), message, response, {
+ :authorization => transaction_id,
+ :test => test?
+ })
+ end
+
+ def message_from(result)
+ message = result[:short_message]
+ message << ": " << result[:long_message] unless result[:long_message] == message
+ message << " (code #{result[:code]})"
+ message
+ end
+
+ def success?(result)
+ SUCCESS_CODES.include?(result[:code])
+ end
+
+ def contract_number
+ options[:contract_number]
+ end
+
+ def language_code(locale)
+ LANGUAGE_CODES[locale.to_s.downcase] if locale
+ end
+
+ def format_date(time)
+ time.strftime(DATE_FORMAT)
+ end
+
+ def format_boolean(boolean, default = false)
+ case boolean.nil? ? default : boolean
+ when true, 1
+ 1
+ else
+ 0
+ end
+ end
+
+ def format_response!(response)
+ unless response.delete("@xmlns") && response.empty?
+ response.each do |key, value|
+ if Hash === value
+ response[key] = format_response!(value)
+ elsif Array === value
+ value.map! { |el| format_response!(el) }
+ end
+ end
+ end
+ end
+
+ def add_version(xml)
+ xml.version WEB_API_VERSION
+ end
+
+ def add_payment(xml, money, currency, action, mode = nil)
+ xml.payment do
+ xml.obj :action, action_code(action)
+ xml.obj :amount, money
+ xml.obj :currency, currency
+ xml.obj :mode, payment_mode(mode)
+ xml.obj :contractNumber, contract_number
+ end
+ end
+
+ def add_order(xml, money, currency, options)
+ xml.order do
+ xml.obj :ref, options[:order_id]
+ xml.obj :amount, money
+ xml.obj :currency, currency
+ xml.obj :date, format_date(options[:order_date] || Time.now)
+ end
+ end
+
+ def add_buyer(xml, buyer)
+ xml.buyer do
+ xml.obj :walletId, buyer[:wallet_id] if buyer[:wallet_id].present?
+ xml.obj :firstName, buyer[:first_name]
+ xml.obj :lastName, buyer[:last_name]
+ xml.obj :email, buyer[:email] if buyer[:email].present?
+ add_address(xml, buyer[:address]) if buyer[:address].present?
+ xml.obj :ip, buyer[:ip] if buyer[:ip].present?
+ end
+ end
+
+ def add_address(xml, address)
+
+ end
+
+ def action_code(action)
+ action = :purchase if action.blank?
+ if ACTION_CODES.key?(action)
+ ACTION_CODES[action]
+ else
+ action
+ end
+ end
+
+ def payment_mode(mode)
+ mode = :direct if mode.blank?
+ if PAYMENT_MODES.key?(mode)
+ PAYMENT_MODES[mode]
+ else
+ mode
+ end
+ end
+ end
+ end
+end
View
95 lib/active_merchant/billing/gateways/payline/payline_direct_api.rb
@@ -0,0 +1,95 @@
+module ActiveMerchant
+ module Billing
+ module PaylineDirectAPI
+ CARD_BRAND_CODES = Hash.new('CB').update(
+ 'visa' => 'VISA',
+ 'master' => 'MASTERCARD',
+ 'american_express' => 'AMEX',
+ 'diners_club' => 'DINERS',
+ 'jcb' => 'JCB',
+ 'switch' => 'SWITCH',
+ 'maestro' => 'MAESTRO'
+ ).freeze
+
+ EXPIRATION_DATE_FORMAT = "%.2d%.2d".freeze
+
+ def do_authorization(money, credit_card, options = {})
+ requires!(options, :order_id)
+ currency = currency_code(options[:currency])
+ direct_api_request :do_authorization do |xml|
+ add_payment(xml, money, currency, :authorization, options[:mode])
+ add_credit_card(xml, credit_card)
+ add_order(xml, money, currency, options)
+ end
+ end
+ alias_method :authorize, :do_authorization
+
+ def do_capture(money, authorization, options = {})
+ direct_api_request :do_capture do |xml|
+ xml.transactionID authorization
+ add_payment(xml, money, currency_code(options[:currency]), :capture, options[:mode])
+ end
+ end
+ alias_method :capture, :do_capture
+
+ def do_recurrent_wallet_payment(money, wallet_id, options = {})
+ currency = currency_code(options[:currency])
+ direct_api_request :do_recurrent_wallet_payment do |xml|
+ add_version(xml)
+ xml.walletId wallet_id
+ xml.scheduledDate format_date(options[:scheduled_date] || Time.now)
+ add_payment(xml, money, currency, :capture, :recurrent)
+ add_order(xml, money, currency, options)
+ xml.recurring do
+ xml.obj :amount, options[:recurring_amount]
+ xml.obj :billingLeft, options[:terms]
+ xml.obj :billingCycle, PaylineCommon::RECURRING_FREQUENCIES[options[:frequency]]
+ end
+ end
+ end
+
+ def get_recurrent_payment_responses(payment_record_id)
+ response = get_payment_record(payment_record_id)
+ responses = response.params[:billing_record_list][:billing_record]
+ responses.collect { |r| build_response(r) if r[:result] }.compact
+ end
+
+ def get_payment_record(payment_record_id)
+ payment_record_request :get, payment_record_id
+ end
+
+ def disable_payment_record(payment_record_id)
+ payment_record_request :disable, payment_record_id
+ end
+
+ protected
+ def direct_api_request(method_name, &block)
+ request(direct_api_savon_client, method_name, &block)
+ end
+
+ def direct_api_savon_client
+ @direct_api_savon_client ||= create_savon_client(test? ? test_url : live_url)
+ end
+
+ def add_credit_card(xml, card)
+ xml.card do
+ xml.obj :number, card.number
+ xml.obj :type, CARD_BRAND_CODES[card.brand]
+ xml.obj :expirationDate, expiration_date(card.month, card.year)
+ xml.obj :cvx, card.verification_value
+ end
+ end
+
+ def expiration_date(month, year)
+ EXPIRATION_DATE_FORMAT % [month, year.to_s[-2..-1]]
+ end
+
+ def payment_record_request(method_name, payment_record_id)
+ direct_api_request :"#{method_name}_payment_record" do |xml|
+ xml.contractNumber contract_number
+ xml.paymentRecordId payment_record_id
+ end
+ end
+ end
+ end
+end
View
84 lib/active_merchant/billing/gateways/payline/payline_web_api.rb
@@ -0,0 +1,84 @@
+module ActiveMerchant
+ module Billing
+ module PaylineWebAPI
+ SSL = 'SSL'.freeze
+
+ def do_web_payment(money, options = {})
+ currency = currency_code(options[:currency])
+ web_api_request :do_web_payment do |xml|
+ add_version(xml)
+ add_payment(xml, money, currency, options[:action], options[:mode])
+ add_order(xml, money, currency, options)
+ add_buyer(xml, options)
+ add_web_params(xml, options)
+ end
+ end
+ alias_method :setup_purchase, :do_web_payment
+
+ # :wallet_id [String] alpha-numeric 50 chars max
+ # :locale [String, Symbol] ISO 639-1 locale code
+ # :return_url (*)
+ # :cancel_return_url (*)
+ # :notify_url
+ # :custom_payment_page_code
+ # :data [Hash] hash of custom data
+ def create_web_wallet(options = {})
+ web_wallet_request :create, options do |xml|
+ xml.selectedContractList do
+ xml.obj :selectedContract, contract_number
+ end
+ add_buyer(xml, options)
+ xml.updatePersonalDetails format_boolean(options[:update_personal_details])
+ xml.privateDataList do
+ options[:data].to_hash.each do |key, value|
+ xml.obj :key, key
+ xml.obj :value, value
+ end
+ end if options[:data].present?
+ end
+ end
+
+ def update_web_wallet(wallet_id, options = {})
+ web_wallet_request :update, options do |xml|
+ xml.walletId wallet_id
+ xml.updatePaymentDetails format_boolean(options[:update_payment_details], true)
+ xml.updatePersonalDetails format_boolean(options[:update_personal_details])
+ xml.updateOwnerDetails format_boolean(options[:update_owner_details])
+ end
+ end
+
+ def get_web_wallet(token)
+ web_api_request :get_web_wallet do |xml|
+ add_version(xml)
+ xml.token token
+ end
+ end
+
+ protected
+ def web_api_request(method_name, &block)
+ request(web_api_savon_client, method_name, &block)
+ end
+
+ def web_api_savon_client
+ @web_api_savon_client ||= create_savon_client(test? ? web_test_url : web_live_url)
+ end
+
+ def web_wallet_request(method_name, options)
+ web_api_request :"#{method_name}_web_wallet" do |xml|
+ xml.contractNumber contract_number
+ yield xml
+ add_web_params(xml, options)
+ end
+ end
+
+ def add_web_params(xml, options)
+ xml.languageCode language_code(options[:locale])
+ xml.securityMode SSL
+ xml.returnURL options[:return_url]
+ xml.cancelURL options[:cancel_return_url]
+ xml.notificationURL options[:notify_url]
+ xml.customPaymentPageCode options[:custom_payment_page_code]
+ end
+ end
+ end
+end
Please sign in to comment.
Something went wrong with that request. Please try again.