Skip to content

Commit

Permalink
Merge pull request activemerchant#213 from ZenCocoon/signatures
Browse files Browse the repository at this point in the history
Ogone: Add support for all signature procedures. Helped by @rymai
  • Loading branch information
jduff committed Nov 14, 2011
2 parents 39b13cd + 47a0423 commit cb18c13
Show file tree
Hide file tree
Showing 4 changed files with 230 additions and 86 deletions.
1 change: 1 addition & 0 deletions lib/active_merchant.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
require 'active_support/core_ext/string/inflections'
require 'active_support/core_ext/hash/indifferent_access'
require 'active_support/core_ext/hash/conversions'
require 'active_support/core_ext/object/conversions'
require 'active_support/core_ext/class/attribute'
require 'active_support/core_ext/class/attribute_accessors'
require 'active_support/core_ext/class/delegating_attributes'
Expand Down
143 changes: 90 additions & 53 deletions lib/active_merchant/billing/gateways/ogone.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# coding: utf-8
require 'rexml/document'

module ActiveMerchant #:nodoc:
Expand All @@ -8,27 +9,32 @@ module Billing #:nodoc:
# communication between Ogone systems and your e-commerce website.
#
# This implementation follows the specification provided in the DirectLink integration
# guide version 2.4 (December 2008), available here:
# guide version 4.2.0 (26 October 2011), available here:
# https://secure.ogone.com/ncol/Ogone_DirectLink_EN.pdf
#
# It also features aliases, which allow to store/unstore credit cards, as specified in
# the Alias Manager Option guide version 2.2 available here:
# the Alias Manager Option guide version 3.2.0 (26 October 2011) available here:
# https://secure.ogone.com/ncol/Ogone_Alias_EN.pdf
#
# It was last tested on Release 04.79 of Ogone e-Commerce (dated 11/02/2009).
# It was last tested on Release 4.89 of Ogone DirectLink + AliasManager (26 October 2011).
#
# For any questions or comments, please contact Nicolas Jacobeus ([email protected]).
# For any questions or comments, please contact one of the following:
# - Nicolas Jacobeus ([email protected]),
# - Sébastien Grosjean ([email protected]),
# - Rémy Coutable ([email protected]).
#
# == Example use:
# == Usage
#
# gateway = ActiveMerchant::Billing::OgoneGateway.new(
# :login => "my_ogone_psp_id",
# :user => "my_ogone_user_id",
# :password => "my_ogone_pswd",
# :signature => "my_ogone_sha1_signature" # extra security, only if you configured your Ogone environment so
# :login => "my_ogone_psp_id",
# :user => "my_ogone_user_id",
# :password => "my_ogone_pswd",
# :signature => "my_ogone_sha_signature", # Only if you configured your Ogone environment so.
# :signature_encryptor => "sha512", # Can be "sha1" (default), "sha256" or "sha512".
# # Must be the same as the one configured in your Ogone account.
# )
#
# # set up credit card obj as in main ActiveMerchant example
# # set up credit card object as in main ActiveMerchant example
# creditcard = ActiveMerchant::Billing::CreditCard.new(
# :type => 'visa',
# :number => '4242424242424242',
Expand All @@ -47,18 +53,21 @@ module Billing #:nodoc:
# puts response.message # Retrieve the message returned by Ogone
# puts response.authorization # Retrieve the unique transaction ID returned by Ogone
#
# To use the alias feature, simply add :alias in the options hash:
# == Alias feature
#
# gateway.purchase(1000, creditcard, :order_id => "1", :alias => "myawesomecustomer") # associates the alias to that creditcard
# gateway.purchase(2000, nil, :order_id => "2", :alias => "myawesomecustomer") # don't need to know the creditcard for subsequent orders
# To use the alias feature, simply add :store in the options hash:
#
# # Associate the alias to that credit card
# gateway.purchase(1000, creditcard, :order_id => "1", :store => "myawesomecustomer")
#
# # You can use the alias instead of the credit card for subsequent orders
# gateway.purchase(2000, "myawesomecustomer", :order_id => "2")
#
class OgoneGateway < Gateway

URLS = {
:test => { :order => 'https://secure.ogone.com/ncol/test/orderdirect.asp',
:maintenance => 'https://secure.ogone.com/ncol/test/maintenancedirect.asp' },
:production => { :order => 'https://secure.ogone.com/ncol/prod/orderdirect.asp',
:maintenance => 'https://secure.ogone.com/ncol/prod/maintenancedirect.asp' }
:order => 'https://secure.ogone.com/ncol/%s/orderdirect.asp',
:maintenance => 'https://secure.ogone.com/ncol/%s/maintenancedirect.asp'
}

CVV_MAPPING = { 'OK' => 'M',
Expand All @@ -68,8 +77,12 @@ class OgoneGateway < Gateway
AVS_MAPPING = { 'OK' => 'M',
'KO' => 'N',
'NO' => 'R' }

SUCCESS_MESSAGE = "The transaction was successful"

OGONE_NO_SIGNATURE_DEPRECATION_MESSAGE = "Signature usage will be required from a future release of ActiveMerchant's Ogone Gateway. Please update your Ogone account to use it."
OGONE_LOW_ENCRYPTION_DEPRECATION_MESSAGE = "SHA512 signature encryptor will be required from a future release of ActiveMerchant's Ogone Gateway. Please update your Ogone account to use it."

self.supported_countries = ['BE', 'DE', 'FR', 'NL', 'AT', 'CH']
# also supports Airplus and UATP
self.supported_cardtypes = [:visa, :master, :american_express, :diners_club, :discover, :jcb, :maestro]
Expand Down Expand Up @@ -108,7 +121,7 @@ def purchase(money, payment_source, options = {})

# Complete a previously authorized transaction.
def capture(money, authorization, options = {})
post = {}
post = {}
add_authorization(post, reference_from(authorization))
add_invoice(post, options)
add_customer_data(post, options)
Expand All @@ -124,7 +137,7 @@ def void(identification, options = {})
end

# Credit the specified account by a specific amount.
def credit(money, identification_or_credit_card, options = {})
def credit(money, identification_or_credit_card, options = {})
if reference_transaction?(identification_or_credit_card)
deprecated CREDIT_DEPRECATION_MESSAGE
# Referenced credit: refund of a settled transaction
Expand All @@ -133,34 +146,35 @@ def credit(money, identification_or_credit_card, options = {})
perform_non_referenced_credit(money, identification_or_credit_card, options)
end
end

# Refund of a settled transaction
def refund(money, reference, options = {})
def refund(money, reference, options = {})
perform_reference_credit(money, reference, options)
end

def test?
@options[:test] || super
end

private

def reference_from(authorization)
authorization.split(";").first
end

def reference_transaction?(identifier)
return false unless identifier.is_a?(String)
return false unless identifier.is_a?(String)
reference, action = identifier.split(";")
!action.nil?
end

def perform_reference_credit(money, payment_target, options = {})
post = {}
add_authorization(post, reference_from(payment_target))
add_money(post, money, options)
commit('RFD', post)
commit('RFD', post)
end

def perform_non_referenced_credit(money, payment_target, options = {})
# Non-referenced credit: acts like a reverse purchase
post = {}
Expand All @@ -171,7 +185,7 @@ def perform_non_referenced_credit(money, payment_target, options = {})
add_money(post, money, options)
commit('RFD', post)
end

def add_payment_source(post, payment_source, options)
if payment_source.is_a?(String)
add_alias(post, payment_source)
Expand All @@ -180,18 +194,18 @@ def add_payment_source(post, payment_source, options)
add_alias(post, options[:store])
add_creditcard(post, payment_source)
end
end
end

def add_eci(post, eci)
add_pair post, 'ECI', eci
end

def add_alias(post, _alias)
add_pair post, 'ALIAS', _alias
add_pair post, 'ALIAS', _alias
end

def add_authorization(post, authorization)
add_pair post, 'PAYID', authorization
add_pair post, 'PAYID', authorization
end

def add_money(post, money, options)
Expand Down Expand Up @@ -226,35 +240,39 @@ def add_creditcard(post, creditcard)
end

def parse(body)
xml = REXML::Document.new(body)
convert_attributes_to_hash(xml.root.attributes)
xml_root = REXML::Document.new(body).root
convert_attributes_to_hash(xml_root.attributes)
end

def commit(action, parameters)
add_pair parameters, 'PSPID', @options[:login]
add_pair parameters, 'USERID', @options[:user]
add_pair parameters, 'PSWD', @options[:password]
url = URLS[test? ? :test : :production][parameters['PAYID'] ? :maintenance : :order ]
add_pair parameters, 'PSPID', @options[:login]
add_pair parameters, 'USERID', @options[:user]
add_pair parameters, 'PSWD', @options[:password]

url = URLS[parameters['PAYID'] ? :maintenance : :order] % [test? ? "test" : "prod"]
response = parse(ssl_post(url, post_data(action, parameters)))
options = { :authorization => [response["PAYID"], action].join(";"),
:test => test?,
:avs_result => { :code => AVS_MAPPING[response["AAVCheck"]] },
:cvv_result => CVV_MAPPING[response["CVCCheck"]] }

options = {
:authorization => [response["PAYID"], action].join(";"),
:test => test?,
:avs_result => { :code => AVS_MAPPING[response["AAVCheck"]] },
:cvv_result => CVV_MAPPING[response["CVCCheck"]]
}
Response.new(successful?(response), message_from(response), response, options)
end

def successful?(response)
response["NCERROR"] == "0"
end

def message_from(response)
if successful?(response)
if successful?(response)
SUCCESS_MESSAGE
else
format_error_message(response["NCERRORPLUS"])
end
end

def format_error_message(message)
raw_message = message.to_s.strip
case raw_message
Expand All @@ -268,16 +286,35 @@ def format_error_message(message)
end

def post_data(action, parameters = {})
add_pair parameters, 'Operation' , action
if @options[:signature] # the user wants a SHA-1 signature
string = ['orderID','amount','currency','CARDNO','PSPID','Operation','ALIAS'].map{|s|parameters[s]}.join + @options[:signature]
add_pair parameters, 'SHASign' , Digest::SHA1.hexdigest(string)
add_pair parameters, 'Operation', action
@options[:signature] ? add_signature(parameters) : deprecated(OGONE_NO_SIGNATURE_DEPRECATION_MESSAGE)
parameters.to_query
end

def add_signature(parameters)
deprecated(OGONE_LOW_ENCRYPTION_DEPRECATION_MESSAGE) unless @options[:signature_encryptor] == 'sha512'

sha_encryptor = case @options[:signature_encryptor]
when 'sha256'
Digest::SHA256
when 'sha512'
Digest::SHA512
else
Digest::SHA1
end

string_to_digest = if @options[:signature_encryptor]
parameters.sort { |a, b| a[0].upcase <=> b[0].upcase }.map { |k, v| "#{k.upcase}=#{v}" }.join(@options[:signature])
else
%w[orderID amount currency CARDNO PSPID Operation ALIAS].map { |key| parameters[key] }.join
end
parameters.collect { |key, value| "#{key}=#{CGI.escape(value.to_s)}" }.join("&")
string_to_digest << @options[:signature]

add_pair parameters, 'SHASign', sha_encryptor.hexdigest(string_to_digest).upcase
end

def add_pair(post, key, value, options = {})
post[key] = value if !value.blank? || options[:required]
def add_pair(post, key, value)
post[key] = value if !value.blank?
end

def convert_attributes_to_hash(rexml_attributes)
Expand Down
53 changes: 46 additions & 7 deletions test/remote/gateways/remote_ogone_test.rb
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
# coding: utf-8
require 'test_helper'

class RemoteOgoneTest < Test::Unit::TestCase

def setup
@gateway = OgoneGateway.new(fixtures(:ogone))
@amount = 100
@credit_card = credit_card('4000100011112224')
@declined_card = credit_card('1111111111111111')
@credit_card = credit_card('4000100011112224')
@declined_card = credit_card('1111111111111111')
@options = {
:order_id => generate_unique_id[0...30],
:billing_address => address,
Expand All @@ -19,15 +20,54 @@ def test_successful_purchase
assert_success response
assert_equal OgoneGateway::SUCCESS_MESSAGE, response.message
end


def test_successful_purchase_with_utf8_encoding_1
assert response = @gateway.purchase(@amount, credit_card('4000100011112224', :first_name => "Rémy", :last_name => "Fröåïør"), @options)
assert_success response
assert_equal OgoneGateway::SUCCESS_MESSAGE, response.message
end

def test_successful_purchase_with_utf8_encoding_2
assert response = @gateway.purchase(@amount, credit_card('4000100011112224', :first_name => "ワタシ", :last_name => "ёжзийклмнопрсуфхцч"), @options)
assert_success response
assert_equal OgoneGateway::SUCCESS_MESSAGE, response.message
end

# NOTE: You have to set the "Hash algorithm" to "SHA-1" in the "Technical information"->"Global security parameters"
# section of your account admin on https://secure.ogone.com/ncol/test/frame_ogone.asp before running this test
def test_successful_purchase_with_signature_encryptor_to_sha1
gateway = OgoneGateway.new(fixtures(:ogone).merge(:signature_encryptor => 'sha1'))
assert response = gateway.purchase(@amount, @credit_card, @options)
assert_success response
assert_equal OgoneGateway::SUCCESS_MESSAGE, response.message
end

# NOTE: You have to set the "Hash algorithm" to "SHA-256" in the "Technical information"->"Global security parameters"
# section of your account admin on https://secure.ogone.com/ncol/test/frame_ogone.asp before running this test
def test_successful_purchase_with_signature_encryptor_to_sha256
gateway = OgoneGateway.new(fixtures(:ogone).merge(:signature_encryptor => 'sha256'))
assert response = gateway.purchase(@amount, @credit_card, @options)
assert_success response
assert_equal OgoneGateway::SUCCESS_MESSAGE, response.message
end

# NOTE: You have to set the "Hash algorithm" to "SHA-512" in the "Technical information"->"Global security parameters"
# section of your account admin on https://secure.ogone.com/ncol/test/frame_ogone.asp before running this test
def test_successful_purchase_with_signature_encryptor_to_sha512
gateway = OgoneGateway.new(fixtures(:ogone).merge(:signature_encryptor => 'sha512'))
assert response = gateway.purchase(@amount, @credit_card, @options)
assert_success response
assert_equal OgoneGateway::SUCCESS_MESSAGE, response.message
end

def test_successful_with_non_numeric_order_id
@options[:order_id] = "##{@options[:order_id][0...26]}.12"
assert response = @gateway.purchase(@amount, @credit_card, @options)
assert_success response
assert_equal OgoneGateway::SUCCESS_MESSAGE, response.message
end

def test_successful_purchase_without_order_id
def test_successful_purchase_without_explicit_order_id
@options.delete(:order_id)
assert response = @gateway.purchase(@amount, @credit_card, @options)
assert_success response
Expand All @@ -45,7 +85,7 @@ def test_authorize_and_capture
assert_success auth
assert_equal OgoneGateway::SUCCESS_MESSAGE, auth.message
assert auth.authorization
assert capture = @gateway.capture(@amount, auth.authorization)
assert capture = @gateway.capture(@amount, auth.authorization, @options)
assert_success capture
end

Expand Down Expand Up @@ -109,7 +149,6 @@ def test_invalid_login
)
assert response = gateway.purchase(@amount, @credit_card, @options)
assert_failure response
assert_equal 'No pspid', response.message
assert_equal 'Some of the data entered is incorrect. please retry.', response.message
end

end
Loading

0 comments on commit cb18c13

Please sign in to comment.