require 'nokogiri'
module ActiveMerchant #:nodoc:
module Billing #:nodoc:
# Public: This gateway allows you to interact with any gateway you've
# created in Spreedly (https://spreedly.com). It's an adapter which can be
# particularly useful if you already have code interacting with
# ActiveMerchant and want to easily take advantage of Spreedly's vault.
class SpreedlyCoreGateway < Gateway
self.live_url = 'https://core.spreedly.com/v1'
self.supported_countries = %w(AD AE AT AU BD BE BG BN CA CH CY CZ DE DK EE EG ES FI FR GB
GI GR HK HU ID IE IL IM IN IS IT JO KW LB LI LK LT LU LV MC
MT MU MV MX MY NL NO NZ OM PH PL PT QA RO SA SE SG SI SK SM
TR TT UM US VA VN ZA)
self.supported_cardtypes = %i[visa master american_express discover]
self.homepage_url = 'https://spreedly.com'
self.display_name = 'Spreedly'
self.money_format = :cents
self.default_currency = 'USD'
# Public: Create a new Spreedly gateway.
#
# options - A hash of options:
# :login - The environment key.
# :password - The access secret.
# :gateway_token - The token of the gateway you've created in
# Spreedly.
def initialize(options = {})
requires!(options, :login, :password, :gateway_token)
super
end
# Public: Run a purchase transaction.
#
# money - The monetary amount of the transaction in cents.
# payment_method - The CreditCard or Check or the Spreedly payment method token.
# options - A hash of options:
# :store - Retain the payment method if the purchase
# succeeds. Defaults to false. (optional)
def purchase(money, payment_method, options = {})
request = build_transaction_request(money, payment_method, options)
commit("gateways/#{options[:gateway_token] || @options[:gateway_token]}/purchase.xml", request)
end
# Public: Run an authorize transaction.
#
# money - The monetary amount of the transaction in cents.
# payment_method - The CreditCard or the Spreedly payment method token.
# options - A hash of options:
# :store - Retain the payment method if the authorize
# succeeds. Defaults to false. (optional)
def authorize(money, payment_method, options = {})
request = build_transaction_request(money, payment_method, options)
commit("gateways/#{@options[:gateway_token]}/authorize.xml", request)
end
def capture(money, authorization, options = {})
request = build_xml_request('transaction') do |doc|
add_invoice(doc, money, options)
end
commit("transactions/#{authorization}/capture.xml", request)
end
def refund(money, authorization, options = {})
request = build_xml_request('transaction') do |doc|
add_invoice(doc, money, options)
add_extra_options(:gateway_specific_fields, doc, options)
end
commit("transactions/#{authorization}/credit.xml", request)
end
def void(authorization, options = {})
commit("transactions/#{authorization}/void.xml", '')
end
# Public: Determine whether a credit card is chargeable card and available for purchases.
#
# payment_method - The CreditCard or the Spreedly payment method token.
# options - A hash of options:
# :store - Retain the payment method if the verify
# succeeds. Defaults to false. (optional)
def verify(payment_method, options = {})
if payment_method.is_a?(String)
verify_with_token(payment_method, options)
else
MultiResponse.run do |r|
r.process { save_card(options[:store], payment_method, options) }
r.process { verify_with_token(r.authorization, options) }
end
end
end
# Public: Store a credit card in the Spreedly vault and retain it.
#
# credit_card - The CreditCard to store
# options - A standard ActiveMerchant options hash
def store(credit_card, options = {})
retain = (options.has_key?(:retain) ? options[:retain] : true)
save_card(retain, credit_card, options)
end
# Public: Redact the CreditCard in Spreedly. This wipes the sensitive
# payment information from the card.
#
# credit_card - The CreditCard to store
# options - A standard ActiveMerchant options hash
def unstore(authorization, options = {})
commit("payment_methods/#{authorization}/redact.xml", '', :put)
end
# Public: Get the transaction with the given token.
def find(transaction_token)
commit("transactions/#{transaction_token}.xml", nil, :get)
end
alias status find
def supports_scrubbing?
true
end
def scrub(transcript)
transcript.
gsub(%r((Authorization: Basic )\w+), '\1[FILTERED]').
gsub(%r(().+()), '\1[FILTERED]\2').
gsub(%r(().+()), '\1[FILTERED]\2').
gsub(%r(().+()), '\1[FILTERED]\2')
end
private
def save_card(retain, credit_card, options)
request = build_xml_request('payment_method') do |doc|
add_credit_card(doc, credit_card, options)
add_extra_options(:data, doc, options)
doc.retained(true) if retain
end
commit('payment_methods.xml', request, :post, :payment_method_token)
end
def purchase_with_token(money, payment_method_token, options)
request = build_transaction_request(money, payment_method_token, options)
commit("gateways/#{options[:gateway_token] || @options[:gateway_token]}/purchase.xml", request)
end
def authorize_with_token(money, payment_method_token, options)
request = build_transaction_request(money, payment_method_token, options)
commit("gateways/#{@options[:gateway_token]}/authorize.xml", request)
end
def verify_with_token(payment_method_token, options)
request = build_transaction_request(nil, payment_method_token, options)
commit("gateways/#{@options[:gateway_token]}/verify.xml", request)
end
def build_transaction_request(money, payment_method, options)
build_xml_request('transaction') do |doc|
add_invoice(doc, money, options)
add_payment_method(doc, payment_method, options)
add_extra_options(:gateway_specific_fields, doc, options)
end
end
def add_invoice(doc, money, options)
doc.amount amount(money) unless money.nil?
doc.currency_code(options[:currency] || currency(money) || default_currency)
doc.order_id(options[:order_id])
doc.ip(options[:ip]) if options[:ip]
doc.description(options[:description]) if options[:description]
doc.merchant_name_descriptor(options[:merchant_name_descriptor]) if options[:merchant_name_descriptor]
doc.merchant_location_descriptor(options[:merchant_location_descriptor]) if options[:merchant_location_descriptor]
end
def add_payment_method(doc, payment_method, options)
doc.retain_on_success(true) if options[:store]
if payment_method.is_a?(String)
doc.payment_method_token(payment_method)
elsif payment_method.is_a?(CreditCard)
add_credit_card(doc, payment_method, options)
elsif payment_method.is_a?(Check)
add_bank_account(doc, payment_method, options)
else
raise TypeError, 'Payment method not supported'
end
end
def add_credit_card(doc, credit_card, options)
doc.credit_card do
doc.number(credit_card.number)
doc.verification_value(credit_card.verification_value)
doc.first_name(credit_card.first_name)
doc.last_name(credit_card.last_name)
doc.month(credit_card.month)
doc.year(credit_card.year)
doc.email(options[:email])
doc.address1(options[:billing_address].try(:[], :address1))
doc.address2(options[:billing_address].try(:[], :address2))
doc.city(options[:billing_address].try(:[], :city))
doc.state(options[:billing_address].try(:[], :state))
doc.zip(options[:billing_address].try(:[], :zip))
doc.country(options[:billing_address].try(:[], :country))
end
end
def add_bank_account(doc, bank_account, options)
doc.bank_account do
doc.first_name(bank_account.first_name)
doc.last_name(bank_account.last_name)
doc.bank_routing_number(bank_account.routing_number)
doc.bank_account_number(bank_account.account_number)
doc.bank_account_type(bank_account.account_type)
doc.bank_account_holder_type(bank_account.account_holder_type)
end
end
def add_extra_options(type, doc, options)
doc.send(type) do
extra_options_to_doc(doc, options[type])
end
end
def extra_options_to_doc(doc, value)
return doc.text value unless value.kind_of? Hash
value.each do |k, v|
doc.send(k) do
extra_options_to_doc(doc, v)
end
end
end
def parse(xml)
response = {}
doc = Nokogiri::XML(xml)
doc.root.xpath('*').each do |node|
if node.elements.empty?
response[node.name.downcase.to_sym] = node.text
else
node.elements.each do |childnode|
childnode_to_response(response, node, childnode)
end
end
end
response
end
def childnode_to_response(response, node, childnode)
node_name = node.name.downcase
childnode_name = childnode.name.downcase
composed_name = "#{node_name}_#{childnode_name}"
childnodes_present = !childnode.elements.empty?
if childnodes_present && composed_name == 'payment_method_data'
response[composed_name.to_sym] = Hash.from_xml(childnode.to_s).values.first
elsif childnodes_present && node_name == 'gateway_specific_response_fields'
response[node_name.to_sym] = {
childnode_name => Hash.from_xml(childnode.to_s).values.first
}
else
response[composed_name.to_sym] = childnode.text
end
end
def build_xml_request(root, &block)
builder = Nokogiri::XML::Builder.new
builder.__send__(root, &block)
builder.to_xml
end
def commit(relative_url, request, method = :post, authorization_field = :token)
begin
raw_response = ssl_request(method, "#{live_url}/#{relative_url}", request, headers)
rescue ResponseError => e
raw_response = e.response.body
end
response_from(raw_response, authorization_field)
end
def response_from(raw_response, authorization_field)
parsed = parse(raw_response)
options = {
authorization: parsed[authorization_field],
test: (parsed[:on_test_gateway] == 'true'),
avs_result: { code: parsed[:response_avs_code] },
cvv_result: parsed[:response_cvv_code]
}
Response.new(parsed[:succeeded] == 'true', parsed[:message] || parsed[:error], parsed, options)
end
def headers
{
'Authorization' => ('Basic ' + Base64.strict_encode64("#{@options[:login]}:#{@options[:password]}").chomp),
'Content-Type' => 'text/xml'
}
end
end
end
end