Skip to content

Commit

Permalink
Sage (US): Support store/unstore of cards
Browse files Browse the repository at this point in the history
Closes #1115.
  • Loading branch information
rwdaigle authored and ntalbott committed Apr 14, 2014
1 parent 737b4ec commit 53dc5f8
Show file tree
Hide file tree
Showing 5 changed files with 378 additions and 1 deletion.
1 change: 1 addition & 0 deletions CHANGELOG
Expand Up @@ -10,6 +10,7 @@
* Iridium: Add more currencies [bslobodin]
* iDeal: Add Mollie iDeal offsite implementation [wvanbergen, maartenvg]
* Spreedly: Add ip, description and gateway_specific_fields [faizalzakaria]
* Sage (US): Support store/unstore of cards. [rwdaigle]

== Version 1.42.7 (March 18, 2014)

Expand Down
25 changes: 24 additions & 1 deletion lib/active_merchant/billing/gateways/sage.rb
@@ -1,5 +1,6 @@
require File.dirname(__FILE__) + '/sage/sage_bankcard'
require File.dirname(__FILE__) + '/sage/sage_virtual_check'
require File.dirname(__FILE__) + '/sage/sage_vault'

module ActiveMerchant #:nodoc:
module Billing #:nodoc:
Expand Down Expand Up @@ -138,15 +139,37 @@ def refund(money, source, options = {})
end
end

# Stores a credit card in the Sage vault.
#
# ==== Parameters
#
# * <tt>credit_card</tt> - The CreditCard object to be stored.
def store(credit_card, options = {})
vault.store(credit_card, options)
end

# Deletes a stored card from the Sage vault.
#
# ==== Parameters
#
# * <tt>identification</tt> - The 'GUID' identifying the stored card.
def unstore(identification, options = {})
vault.unstore(identification, options)
end

private

def bankcard
@bankcard ||= SageBankcardGateway.new(@options)
end

def virtual_check
@virtual_check ||= SageVirtualCheckGateway.new(@options)
end

def vault
@vault ||= SageVaultGateway.new(@options)
end
end
end
end

149 changes: 149 additions & 0 deletions lib/active_merchant/billing/gateways/sage/sage_vault.rb
@@ -0,0 +1,149 @@
module ActiveMerchant #:nodoc:
module Billing #:nodoc:
class SageVaultGateway < Gateway #:nodoc:
self.live_url = 'https://www.sagepayments.net/web_services/wsVault/wsVault.asmx'

def initialize(options = {})
requires!(options, :login, :password)
super
end

def store(credit_card, options = {})
request = build_store_request(credit_card, options)
commit(:store, request)
end

def unstore(identification, options = {})
request = build_unstore_request(identification, options)
commit(:unstore, request)
end

private

# A valid request example, since the Sage docs have none:
#
# <?xml version="1.0" encoding="UTF-8" ?>
# <SOAP-ENV:Envelope
# xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"
# xmlns:ns1="https://www.sagepayments.net/web_services/wsVault/wsVault">
# <SOAP-ENV:Body>
# <ns1:INSERT_CREDIT_CARD_DATA>
# <ns1:M_ID>279277516172</ns1:M_ID>
# <ns1:M_KEY>O3I8G2H8V6A3</ns1:M_KEY>
# <ns1:CARDNUMBER>4111111111111111</ns1:CARDNUMBER>
# <ns1:EXPIRATION_DATE>0915</ns1:EXPIRATION_DATE>
# </ns1:INSERT_CREDIT_CARD_DATA>
# </SOAP-ENV:Body>
# </SOAP-ENV:Envelope>
def build_store_request(credit_card, options)
xml = Builder::XmlMarkup.new
add_credit_card(xml, credit_card, options)
xml.target!
end

def build_unstore_request(identification, options)
xml = Builder::XmlMarkup.new
add_identification(xml, identification, options)
xml.target!
end

def add_customer_data(xml)
xml.tag! 'ns1:M_ID', @options[:login]
xml.tag! 'ns1:M_KEY', @options[:password]
end

def add_credit_card(xml, credit_card, options)
xml.tag! 'ns1:CARDNUMBER', credit_card.number
xml.tag! 'ns1:EXPIRATION_DATE', exp_date(credit_card)
end

def add_identification(xml, identification, options)
xml.tag! 'ns1:GUID', identification
end

def exp_date(credit_card)
year = sprintf("%.4i", credit_card.year)
month = sprintf("%.2i", credit_card.month)

"#{month}#{year[-2..-1]}"
end

def commit(action, request)
response = parse(ssl_post(live_url,
build_soap_request(action, request),
build_headers(action))
)

case action
when :store
success = response[:success] == 'true'
message = response[:message].downcase.capitalize if response[:message]
when :unstore
success = response[:delete_data_result] == 'true'
message = success ? 'Succeeded' : 'Failed'
end

Response.new(success, message, response,
authorization: response[:guid]
)
end

ENVELOPE_NAMESPACES = {
'xmlns:SOAP-ENV' => "http://schemas.xmlsoap.org/soap/envelope/",
'xmlns:ns1' => "https://www.sagepayments.net/web_services/wsVault/wsVault"
}

ACTION_ELEMENTS = {
store: 'INSERT_CREDIT_CARD_DATA',
unstore: 'DELETE_DATA'
}

def build_soap_request(action, body)
xml = Builder::XmlMarkup.new

xml.instruct!
xml.tag! 'SOAP-ENV:Envelope', ENVELOPE_NAMESPACES do
xml.tag! 'SOAP-ENV:Body' do
xml.tag! "ns1:#{ACTION_ELEMENTS[action]}" do
add_customer_data(xml)
xml << body
end
end
end
xml.target!
end

SOAP_ACTIONS = {
store: 'https://www.sagepayments.net/web_services/wsVault/wsVault/INSERT_CREDIT_CARD_DATA',
unstore: 'https://www.sagepayments.net/web_services/wsVault/wsVault/DELETE_DATA'
}

def build_headers(action)
{
"SOAPAction" => SOAP_ACTIONS[action],
"Content-Type" => "text/xml; charset=utf-8"
}
end

def parse(body)
response = {}
hashify_xml!(body, response)
response
end

def hashify_xml!(xml, response)
xml = REXML::Document.new(xml)

# Store
xml.elements.each("//Table1/*") do |node|
response[node.name.underscore.to_sym] = node.text
end

# Unstore
xml.elements.each("//DELETE_DATAResponse/*") do |node|
response[node.name.underscore.to_sym] = node.text
end
end
end
end
end
46 changes: 46 additions & 0 deletions test/remote/gateways/remote_sage_vault_test.rb
@@ -0,0 +1,46 @@
require 'test_helper'

class RemoteSageVaultTest < Test::Unit::TestCase

def setup
@gateway = SageVaultGateway.new(fixtures(:sage))

@visa = credit_card("4111111111111111")
@mastercard = credit_card("5499740000000057")
@discover = credit_card("6011000993026909")
@amex = credit_card("371449635392376")

@declined_card = credit_card('4000')

@options = { }
end

def test_store_visa
assert response = @gateway.store(@visa, @options)
assert_success response
assert auth = response.authorization,
"Store card authorization should not be nil"
assert_not_nil response.message
end

def test_failed_store
assert response = @gateway.store(@declined_card, @options)
assert_failure response
assert_nil response.authorization
end

def test_unstore_visa
assert auth = @gateway.store(@visa, @options).authorization,
"Unstore card authorization should not be nil"
assert response = @gateway.unstore(auth, @options)
assert_success response
end

def test_failed_unstore_visa
assert auth = @gateway.store(@visa, @options).authorization,
"Unstore card authorization should not be nil"
assert response = @gateway.unstore(auth, @options)
assert_success response
end

end

0 comments on commit 53dc5f8

Please sign in to comment.