Vendored the activemerchant gem to help with testing

We needed a bogus paypal gateway as per
http://infotrope.net/2013/05/31/testing-paypal-express-with-activemerchants-bogusgateway-and-how-to-make-it-work/

Tests now pass usefully.
This commit is contained in:
Skud
2013-05-31 12:15:23 +10:00
parent cbca8c19a5
commit 08c3ec7dec
327 changed files with 52355 additions and 18 deletions

View File

@@ -9,7 +9,14 @@ gem 'haml'
gem 'cancan'
gem 'activemerchant'
# vendored activemerchant for testing- needed for bogus paypal
# gateway monkeypatch
gem 'activemerchant', '1.33.0',
:path => 'vendor/gems/activemerchant-1.33.0',
:require => 'active_merchant'
gem 'active_utils', '1.0.5',
:path => 'vendor/gems/active_utils-1.0.5'
# Bundle edge Rails instead:
# gem 'rails', :git => 'git://github.com/rails/rails.git'

View File

@@ -1,3 +1,15 @@
PATH
remote: vendor/gems/active_utils-1.0.5
specs:
active_utils (1.0.5)
activesupport (>= 2.3.11)
i18n
PATH
remote: vendor/gems/activemerchant-1.33.0
specs:
activemerchant (1.33.0)
GEM
remote: https://rubygems.org/
specs:
@@ -14,17 +26,6 @@ GEM
rack-cache (~> 1.2)
rack-test (~> 0.6.1)
sprockets (~> 2.2.1)
active_utils (1.0.5)
activesupport (>= 2.3.11)
i18n
activemerchant (1.32.1)
active_utils (>= 1.0.2)
activesupport (>= 2.3.14)
builder (>= 2.0.0)
i18n
json (>= 1.5.1)
money
nokogiri
activemodel (3.2.13)
activesupport (= 3.2.13)
builder (~> 3.0.0)
@@ -127,8 +128,6 @@ GEM
mime-types (~> 1.16)
treetop (~> 1.4.8)
mime-types (1.21)
money (5.1.1)
i18n (~> 0.6.0)
multi_json (1.7.1)
net-scp (1.1.0)
net-ssh (>= 2.6.5)
@@ -245,7 +244,8 @@ PLATFORMS
ruby
DEPENDENCIES
activemerchant
active_utils (= 1.0.5)!
activemerchant (= 1.33.0)!
bluecloth
bootstrap-datepicker-rails
bundler (>= 1.1.5)

View File

@@ -51,7 +51,12 @@ class OrdersController < ApplicationController
@order.completed_at = Time.zone.now
@order.save
@order.record_paypal_details(params[:token])
if (params[:token])
@order.record_paypal_details(params[:token])
else
flash[:alert] = "PayPal didn't return a token for your order. Please notify support."
end
@order.update_account # apply paid account benefits, etc.
respond_to do |format|

View File

@@ -48,8 +48,8 @@ Growstuff::Application.configure do
config.after_initialize do
ActiveMerchant::Billing::Base.mode = :test
::STANDARD_GATEWAY = ActiveMerchant::Billing::BogusGateway.new
::EXPRESS_GATEWAY = ActiveMerchant::Billing::BogusGateway.new
::STANDARD_GATEWAY = ActiveMerchant::Billing::PaypalBogusGateway.new
::EXPRESS_GATEWAY = ActiveMerchant::Billing::PaypalBogusGateway.new
end
end

View File

@@ -32,6 +32,17 @@ describe OrdersController do
end
end
describe "GET checkout" do
it "redirects to Paypal" do
member = FactoryGirl.create(:member)
sign_in member
order = Order.create!(:member_id => member.id)
get :checkout, {:id => order.to_param}
response.status.should eq 302
response.redirect_url.should match /paypal\.com/
end
end
describe "GET complete" do
it "assigns the requested order as @order" do
member = FactoryGirl.create(:member)

View File

@@ -0,0 +1,5 @@
pkg/*
*.gem
.bundle
.DS_Store
Gemfile.lock

View File

@@ -0,0 +1,3 @@
source "http://rubygems.org"
gemspec

View File

@@ -0,0 +1,20 @@
Copyright (c) 2011 Shopify
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -0,0 +1,15 @@
# Active Utils
Active Utils extracts commonly used modules and classes used by [Active Merchant](http://github.com/Shopify/active_merchant), [Active Shipping](http://github.com/Shopify/active_shipping), and [Active Fulfillment](http://github.com/Shopify/active_fulfillment).
### Includes
* Connection - base class for making HTTP requests
* Country - find countries mapped by name, country codes, and numeric values
* Error - common error classes used throughout the Active projects
* PostData - helper class for managing required fields that are to be POST-ed
* PostsData - making SSL HTTP requests
* RequiresParameters - helper method to ensure the required parameters are passed in
* Utils - common utils such as uid generator
* Validateable - module used for making models validateable
* NetworkConnectionRetries - module for retrying network connections when connection errors occur

13
vendor/gems/active_utils-1.0.5/Rakefile vendored Normal file
View File

@@ -0,0 +1,13 @@
require 'bundler'
Bundler::GemHelper.install_tasks
require 'rake/testtask'
Rake::TestTask.new(:test) do |t|
t.pattern = 'test/unit/**/*_test.rb'
t.ruby_opts << '-rubygems'
t.libs << 'test'
t.verbose = true
end
task :default => "test"

View File

@@ -0,0 +1,26 @@
# -*- encoding: utf-8 -*-
$:.push File.expand_path("../lib", __FILE__)
require "active_utils/version"
Gem::Specification.new do |s|
s.name = "active_utils"
s.version = ActiveUtils::VERSION
s.platform = Gem::Platform::RUBY
s.authors = ["Shopify"]
s.email = ["developers@jadedpixel.com"]
s.homepage = "http://github.com/shopify/active_utils"
s.summary = %q{Common utils used by active_merchant, active_fulfillment, and active_shipping}
s.rubyforge_project = "active_utils"
s.add_dependency('activesupport', '>= 2.3.11')
s.add_dependency('i18n')
s.add_development_dependency('rake')
s.add_development_dependency('mocha')
s.files = `git ls-files`.split("\n")
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
s.require_paths = ["lib"]
end

View File

@@ -0,0 +1,20 @@
require 'active_support/core_ext/hash/indifferent_access'
require 'active_support/core_ext/hash/conversions'
require 'active_support/core_ext/class/attribute'
module ActiveMerchant
autoload :NetworkConnectionRetries, 'active_utils/common/network_connection_retries'
autoload :Connection, 'active_utils/common/connection'
autoload :Country, 'active_utils/common/country'
autoload :CountryCode, 'active_utils/common/country'
autoload :ActiveMerchantError, 'active_utils/common/error'
autoload :ConnectionError, 'active_utils/common/error'
autoload :RetriableConnectionError, 'active_utils/common/error'
autoload :ResponseError, 'active_utils/common/error'
autoload :ClientCertificateError, 'active_utils/common/error'
autoload :PostData, 'active_utils/common/post_data'
autoload :PostsData, 'active_utils/common/posts_data'
autoload :RequiresParameters, 'active_utils/common/requires_parameters'
autoload :Utils, 'active_utils/common/utils'
autoload :Validateable, 'active_utils/common/validateable'
end

View File

@@ -0,0 +1,147 @@
require 'uri'
require 'net/http'
require 'net/https'
require 'benchmark'
module ActiveMerchant
class Connection
include NetworkConnectionRetries
MAX_RETRIES = 3
OPEN_TIMEOUT = 60
READ_TIMEOUT = 60
VERIFY_PEER = true
RETRY_SAFE = false
RUBY_184_POST_HEADERS = { "Content-Type" => "application/x-www-form-urlencoded" }
attr_accessor :endpoint
attr_accessor :open_timeout
attr_accessor :read_timeout
attr_accessor :verify_peer
attr_accessor :pem
attr_accessor :pem_password
attr_accessor :wiredump_device
attr_accessor :logger
attr_accessor :tag
attr_accessor :ignore_http_status
def initialize(endpoint)
@endpoint = endpoint.is_a?(URI) ? endpoint : URI.parse(endpoint)
@open_timeout = OPEN_TIMEOUT
@read_timeout = READ_TIMEOUT
@retry_safe = RETRY_SAFE
@verify_peer = VERIFY_PEER
@ignore_http_status = false
end
def request(method, body, headers = {})
retry_exceptions(:max_retries => MAX_RETRIES, :logger => logger, :tag => tag) do
begin
info "#{method.to_s.upcase} #{endpoint}", tag
result = nil
realtime = Benchmark.realtime do
result = case method
when :get
raise ArgumentError, "GET requests do not support a request body" if body
http.get(endpoint.request_uri, headers)
when :post
debug body
http.post(endpoint.request_uri, body, RUBY_184_POST_HEADERS.merge(headers))
when :put
debug body
http.put(endpoint.request_uri, body, headers)
when :delete
# It's kind of ambiguous whether the RFC allows bodies
# for DELETE requests. But Net::HTTP's delete method
# very unambiguously does not.
raise ArgumentError, "DELETE requests do not support a request body" if body
http.delete(endpoint.request_uri, headers)
else
raise ArgumentError, "Unsupported request method #{method.to_s.upcase}"
end
end
info "--> %d %s (%d %.4fs)" % [result.code, result.message, result.body ? result.body.length : 0, realtime], tag
debug result.body
result
end
end
end
private
def http
http = Net::HTTP.new(endpoint.host, endpoint.port)
configure_debugging(http)
configure_timeouts(http)
configure_ssl(http)
configure_cert(http)
http
end
def configure_debugging(http)
http.set_debug_output(wiredump_device)
end
def configure_timeouts(http)
http.open_timeout = open_timeout
http.read_timeout = read_timeout
end
def configure_ssl(http)
return unless endpoint.scheme == "https"
http.use_ssl = true
if verify_peer
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
http.ca_file = File.dirname(__FILE__) + '/../../certs/cacert.pem'
else
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
end
end
def configure_cert(http)
return if pem.blank?
http.cert = OpenSSL::X509::Certificate.new(pem)
if pem_password
http.key = OpenSSL::PKey::RSA.new(pem, pem_password)
else
http.key = OpenSSL::PKey::RSA.new(pem)
end
end
def handle_response(response)
if @ignore_http_status then
return response.body
else
case response.code.to_i
when 200...300
response.body
else
raise ResponseError.new(response)
end
end
end
def debug(message, tag = nil)
log(:debug, message, tag)
end
def info(message, tag = nil)
log(:info, message, tag)
end
def error(message, tag = nil)
log(:error, message, tag)
end
def log(level, message, tag)
message = "[#{tag}] #{message}" if tag
logger.send(level, message) if logger
end
end
end

View File

@@ -0,0 +1,328 @@
#!ruby19
# encoding: utf-8
module ActiveMerchant #:nodoc:
class InvalidCountryCodeError < StandardError
end
class CountryCodeFormatError < StandardError
end
class CountryCode
attr_reader :value, :format
def initialize(value)
@value = value.to_s.upcase
detect_format
end
def to_s
value
end
private
def detect_format
case @value
when /^[[:alpha:]]{2}$/
@format = :alpha2
when /^[[:alpha:]]{3}$/
@format = :alpha3
when /^[[:digit:]]{3}$/
@format = :numeric
else
raise CountryCodeFormatError, "The country code is not formatted correctly #{@value}"
end
end
end
class Country
include RequiresParameters
attr_reader :name
def initialize(options = {})
requires!(options, :name, :alpha2, :alpha3, :numeric)
@name = options.delete(:name)
@codes = options.collect{|k,v| CountryCode.new(v)}
end
def code(format)
@codes.detect{|c| c.format == format}
end
def ==(other)
(@name == other.name)
end
alias eql? ==
def hash
@name.hash
end
def to_s
@name
end
COUNTRIES = [
{ :alpha2 => 'AF', :name => 'Afghanistan', :alpha3 => 'AFG', :numeric => '004' },
{ :alpha2 => 'AL', :name => 'Albania', :alpha3 => 'ALB', :numeric => '008' },
{ :alpha2 => 'DZ', :name => 'Algeria', :alpha3 => 'DZA', :numeric => '012' },
{ :alpha2 => 'AS', :name => 'American Samoa', :alpha3 => 'ASM', :numeric => '016' },
{ :alpha2 => 'AD', :name => 'Andorra', :alpha3 => 'AND', :numeric => '020' },
{ :alpha2 => 'AO', :name => 'Angola', :alpha3 => 'AGO', :numeric => '024' },
{ :alpha2 => 'AI', :name => 'Anguilla', :alpha3 => 'AIA', :numeric => '660' },
{ :alpha2 => 'AG', :name => 'Antigua and Barbuda', :alpha3 => 'ATG', :numeric => '028' },
{ :alpha2 => 'AR', :name => 'Argentina', :alpha3 => 'ARG', :numeric => '032' },
{ :alpha2 => 'AM', :name => 'Armenia', :alpha3 => 'ARM', :numeric => '051' },
{ :alpha2 => 'AW', :name => 'Aruba', :alpha3 => 'ABW', :numeric => '533' },
{ :alpha2 => 'AU', :name => 'Australia', :alpha3 => 'AUS', :numeric => '036' },
{ :alpha2 => 'AT', :name => 'Austria', :alpha3 => 'AUT', :numeric => '040' },
{ :alpha2 => 'AZ', :name => 'Azerbaijan', :alpha3 => 'AZE', :numeric => '031' },
{ :alpha2 => 'BS', :name => 'Bahamas', :alpha3 => 'BHS', :numeric => '044' },
{ :alpha2 => 'BH', :name => 'Bahrain', :alpha3 => 'BHR', :numeric => '048' },
{ :alpha2 => 'BD', :name => 'Bangladesh', :alpha3 => 'BGD', :numeric => '050' },
{ :alpha2 => 'BB', :name => 'Barbados', :alpha3 => 'BRB', :numeric => '052' },
{ :alpha2 => 'BY', :name => 'Belarus', :alpha3 => 'BLR', :numeric => '112' },
{ :alpha2 => 'BE', :name => 'Belgium', :alpha3 => 'BEL', :numeric => '056' },
{ :alpha2 => 'BZ', :name => 'Belize', :alpha3 => 'BLZ', :numeric => '084' },
{ :alpha2 => 'BJ', :name => 'Benin', :alpha3 => 'BEN', :numeric => '204' },
{ :alpha2 => 'BM', :name => 'Bermuda', :alpha3 => 'BMU', :numeric => '060' },
{ :alpha2 => 'BT', :name => 'Bhutan', :alpha3 => 'BTN', :numeric => '064' },
{ :alpha2 => 'BO', :name => 'Bolivia', :alpha3 => 'BOL', :numeric => '068' },
{ :alpha2 => 'BA', :name => 'Bosnia and Herzegovina', :alpha3 => 'BIH', :numeric => '070' },
{ :alpha2 => 'BW', :name => 'Botswana', :alpha3 => 'BWA', :numeric => '072' },
{ :alpha2 => 'BV', :name => 'Bouvet Island', :alpha3 => 'BVD', :numeric => '074' },
{ :alpha2 => 'BR', :name => 'Brazil', :alpha3 => 'BRA', :numeric => '076' },
{ :alpha2 => 'IO', :name => 'British Indian Ocean Territory', :alpha3 => 'IOT', :numeric => '086' },
{ :alpha2 => 'BN', :name => 'Brunei Darussalam', :alpha3 => 'BRN', :numeric => '096' },
{ :alpha2 => 'BG', :name => 'Bulgaria', :alpha3 => 'BGR', :numeric => '100' },
{ :alpha2 => 'BF', :name => 'Burkina Faso', :alpha3 => 'BFA', :numeric => '854' },
{ :alpha2 => 'BI', :name => 'Burundi', :alpha3 => 'BDI', :numeric => '108' },
{ :alpha2 => 'KH', :name => 'Cambodia', :alpha3 => 'KHM', :numeric => '116' },
{ :alpha2 => 'CM', :name => 'Cameroon', :alpha3 => 'CMR', :numeric => '120' },
{ :alpha2 => 'CA', :name => 'Canada', :alpha3 => 'CAN', :numeric => '124' },
{ :alpha2 => 'CV', :name => 'Cape Verde', :alpha3 => 'CPV', :numeric => '132' },
{ :alpha2 => 'KY', :name => 'Cayman Islands', :alpha3 => 'CYM', :numeric => '136' },
{ :alpha2 => 'CF', :name => 'Central African Republic', :alpha3 => 'CAF', :numeric => '140' },
{ :alpha2 => 'TD', :name => 'Chad', :alpha3 => 'TCD', :numeric => '148' },
{ :alpha2 => 'CL', :name => 'Chile', :alpha3 => 'CHL', :numeric => '152' },
{ :alpha2 => 'CN', :name => 'China', :alpha3 => 'CHN', :numeric => '156' },
{ :alpha2 => 'CX', :name => 'Christmas Island', :alpha3 => 'CXR', :numeric => '162' },
{ :alpha2 => 'CC', :name => 'Cocos (Keeling) Islands', :alpha3 => 'CCK', :numeric => '166' },
{ :alpha2 => 'CO', :name => 'Colombia', :alpha3 => 'COL', :numeric => '170' },
{ :alpha2 => 'KM', :name => 'Comoros', :alpha3 => 'COM', :numeric => '174' },
{ :alpha2 => 'CG', :name => 'Congo', :alpha3 => 'COG', :numeric => '178' },
{ :alpha2 => 'CD', :name => 'Congo, the Democratic Republic of the', :alpha3 => 'COD', :numeric => '180' },
{ :alpha2 => 'CK', :name => 'Cook Islands', :alpha3 => 'COK', :numeric => '184' },
{ :alpha2 => 'CR', :name => 'Costa Rica', :alpha3 => 'CRI', :numeric => '188' },
{ :alpha2 => 'CI', :name => 'Cote D\'Ivoire', :alpha3 => 'CIV', :numeric => '384' },
{ :alpha2 => 'HR', :name => 'Croatia', :alpha3 => 'HRV', :numeric => '191' },
{ :alpha2 => 'CU', :name => 'Cuba', :alpha3 => 'CUB', :numeric => '192' },
{ :alpha2 => 'CY', :name => 'Cyprus', :alpha3 => 'CYP', :numeric => '196' },
{ :alpha2 => 'CZ', :name => 'Czech Republic', :alpha3 => 'CZE', :numeric => '203' },
{ :alpha2 => 'DK', :name => 'Denmark', :alpha3 => 'DNK', :numeric => '208' },
{ :alpha2 => 'DJ', :name => 'Djibouti', :alpha3 => 'DJI', :numeric => '262' },
{ :alpha2 => 'DM', :name => 'Dominica', :alpha3 => 'DMA', :numeric => '212' },
{ :alpha2 => 'DO', :name => 'Dominican Republic', :alpha3 => 'DOM', :numeric => '214' },
{ :alpha2 => 'EC', :name => 'Ecuador', :alpha3 => 'ECU', :numeric => '218' },
{ :alpha2 => 'EG', :name => 'Egypt', :alpha3 => 'EGY', :numeric => '818' },
{ :alpha2 => 'SV', :name => 'El Salvador', :alpha3 => 'SLV', :numeric => '222' },
{ :alpha2 => 'GQ', :name => 'Equatorial Guinea', :alpha3 => 'GNQ', :numeric => '226' },
{ :alpha2 => 'ER', :name => 'Eritrea', :alpha3 => 'ERI', :numeric => '232' },
{ :alpha2 => 'EE', :name => 'Estonia', :alpha3 => 'EST', :numeric => '233' },
{ :alpha2 => 'ET', :name => 'Ethiopia', :alpha3 => 'ETH', :numeric => '231' },
{ :alpha2 => 'FK', :name => 'Falkland Islands (Malvinas)', :alpha3 => 'FLK', :numeric => '238' },
{ :alpha2 => 'FO', :name => 'Faroe Islands', :alpha3 => 'FRO', :numeric => '234' },
{ :alpha2 => 'FJ', :name => 'Fiji', :alpha3 => 'FJI', :numeric => '242' },
{ :alpha2 => 'FI', :name => 'Finland', :alpha3 => 'FIN', :numeric => '246' },
{ :alpha2 => 'FR', :name => 'France', :alpha3 => 'FRA', :numeric => '250' },
{ :alpha2 => 'GF', :name => 'French Guiana', :alpha3 => 'GUF', :numeric => '254' },
{ :alpha2 => 'PF', :name => 'French Polynesia', :alpha3 => 'PYF', :numeric => '258' },
{ :alpha2 => 'TF', :name => 'French Southern Territories', :alpha3 => 'ATF', :numeric => '260' },
{ :alpha2 => 'GA', :name => 'Gabon', :alpha3 => 'GAB', :numeric => '266' },
{ :alpha2 => 'GM', :name => 'Gambia', :alpha3 => 'GMB', :numeric => '270' },
{ :alpha2 => 'GE', :name => 'Georgia', :alpha3 => 'GEO', :numeric => '268' },
{ :alpha2 => 'DE', :name => 'Germany', :alpha3 => 'DEU', :numeric => '276' },
{ :alpha2 => 'GH', :name => 'Ghana', :alpha3 => 'GHA', :numeric => '288' },
{ :alpha2 => 'GI', :name => 'Gibraltar', :alpha3 => 'GIB', :numeric => '292' },
{ :alpha2 => 'GR', :name => 'Greece', :alpha3 => 'GRC', :numeric => '300' },
{ :alpha2 => 'GL', :name => 'Greenland', :alpha3 => 'GRL', :numeric => '304' },
{ :alpha2 => 'GD', :name => 'Grenada', :alpha3 => 'GRD', :numeric => '308' },
{ :alpha2 => 'GP', :name => 'Guadeloupe', :alpha3 => 'GLP', :numeric => '312' },
{ :alpha2 => 'GU', :name => 'Guam', :alpha3 => 'GUM', :numeric => '316' },
{ :alpha2 => 'GT', :name => 'Guatemala', :alpha3 => 'GTM', :numeric => '320' },
{ :alpha2 => 'GG', :name => 'Guernsey', :alpha3 => 'GGY', :numeric => '831' },
{ :alpha2 => 'GN', :name => 'Guinea', :alpha3 => 'GIN', :numeric => '324' },
{ :alpha2 => 'GW', :name => 'Guinea-Bissau', :alpha3 => 'GNB', :numeric => '624' },
{ :alpha2 => 'GY', :name => 'Guyana', :alpha3 => 'GUY', :numeric => '328' },
{ :alpha2 => 'HT', :name => 'Haiti', :alpha3 => 'HTI', :numeric => '332' },
{ :alpha2 => 'HM', :name => 'Heard Island And Mcdonald Islands', :alpha3 => 'HMD', :numeric => '334' },
{ :alpha2 => 'VA', :name => 'Holy See (Vatican City State)', :alpha3 => 'VAT', :numeric => '336' },
{ :alpha2 => 'HN', :name => 'Honduras', :alpha3 => 'HND', :numeric => '340' },
{ :alpha2 => 'HK', :name => 'Hong Kong', :alpha3 => 'HKG', :numeric => '344' },
{ :alpha2 => 'HU', :name => 'Hungary', :alpha3 => 'HUN', :numeric => '348' },
{ :alpha2 => 'IS', :name => 'Iceland', :alpha3 => 'ISL', :numeric => '352' },
{ :alpha2 => 'IN', :name => 'India', :alpha3 => 'IND', :numeric => '356' },
{ :alpha2 => 'ID', :name => 'Indonesia', :alpha3 => 'IDN', :numeric => '360' },
{ :alpha2 => 'IR', :name => 'Iran, Islamic Republic of', :alpha3 => 'IRN', :numeric => '364' },
{ :alpha2 => 'IQ', :name => 'Iraq', :alpha3 => 'IRQ', :numeric => '368' },
{ :alpha2 => 'IE', :name => 'Ireland', :alpha3 => 'IRL', :numeric => '372' },
{ :alpha2 => 'IM', :name => 'Isle Of Man', :alpha3 => 'IMN', :numeric => '833' },
{ :alpha2 => 'IL', :name => 'Israel', :alpha3 => 'ISR', :numeric => '376' },
{ :alpha2 => 'IT', :name => 'Italy', :alpha3 => 'ITA', :numeric => '380' },
{ :alpha2 => 'JM', :name => 'Jamaica', :alpha3 => 'JAM', :numeric => '388' },
{ :alpha2 => 'JP', :name => 'Japan', :alpha3 => 'JPN', :numeric => '392' },
{ :alpha2 => 'JE', :name => 'Jersey', :alpha3 => 'JEY', :numeric => '832' },
{ :alpha2 => 'JO', :name => 'Jordan', :alpha3 => 'JOR', :numeric => '400' },
{ :alpha2 => 'KZ', :name => 'Kazakhstan', :alpha3 => 'KAZ', :numeric => '398' },
{ :alpha2 => 'KE', :name => 'Kenya', :alpha3 => 'KEN', :numeric => '404' },
{ :alpha2 => 'KI', :name => 'Kiribati', :alpha3 => 'KIR', :numeric => '296' },
{ :alpha2 => 'KP', :name => 'Korea, Democratic People\'s Republic of', :alpha3 => 'PRK', :numeric => '408' },
{ :alpha2 => 'KR', :name => 'Korea, Republic of', :alpha3 => 'KOR', :numeric => '410' },
{ :alpha2 => 'KW', :name => 'Kuwait', :alpha3 => 'KWT', :numeric => '414' },
{ :alpha2 => 'KG', :name => 'Kyrgyzstan', :alpha3 => 'KGZ', :numeric => '417' },
{ :alpha2 => 'LA', :name => 'Lao People\'s Democratic Republic', :alpha3 => 'LAO', :numeric => '418' },
{ :alpha2 => 'LV', :name => 'Latvia', :alpha3 => 'LVA', :numeric => '428' },
{ :alpha2 => 'LB', :name => 'Lebanon', :alpha3 => 'LBN', :numeric => '422' },
{ :alpha2 => 'LS', :name => 'Lesotho', :alpha3 => 'LSO', :numeric => '426' },
{ :alpha2 => 'LR', :name => 'Liberia', :alpha3 => 'LBR', :numeric => '430' },
{ :alpha2 => 'LY', :name => 'Libyan Arab Jamahiriya', :alpha3 => 'LBY', :numeric => '434' },
{ :alpha2 => 'LI', :name => 'Liechtenstein', :alpha3 => 'LIE', :numeric => '438' },
{ :alpha2 => 'LT', :name => 'Lithuania', :alpha3 => 'LTU', :numeric => '440' },
{ :alpha2 => 'LU', :name => 'Luxembourg', :alpha3 => 'LUX', :numeric => '442' },
{ :alpha2 => 'MO', :name => 'Macao', :alpha3 => 'MAC', :numeric => '446' },
{ :alpha2 => 'MK', :name => 'Macedonia, the Former Yugoslav Republic of', :alpha3 => 'MKD', :numeric => '807' },
{ :alpha2 => 'MG', :name => 'Madagascar', :alpha3 => 'MDG', :numeric => '450' },
{ :alpha2 => 'MW', :name => 'Malawi', :alpha3 => 'MWI', :numeric => '454' },
{ :alpha2 => 'MY', :name => 'Malaysia', :alpha3 => 'MYS', :numeric => '458' },
{ :alpha2 => 'MV', :name => 'Maldives', :alpha3 => 'MDV', :numeric => '462' },
{ :alpha2 => 'ML', :name => 'Mali', :alpha3 => 'MLI', :numeric => '466' },
{ :alpha2 => 'MT', :name => 'Malta', :alpha3 => 'MLT', :numeric => '470' },
{ :alpha2 => 'MH', :name => 'Marshall Islands', :alpha3 => 'MHL', :numeric => '584' },
{ :alpha2 => 'MQ', :name => 'Martinique', :alpha3 => 'MTQ', :numeric => '474' },
{ :alpha2 => 'MR', :name => 'Mauritania', :alpha3 => 'MRT', :numeric => '478' },
{ :alpha2 => 'MU', :name => 'Mauritius', :alpha3 => 'MUS', :numeric => '480' },
{ :alpha2 => 'YT', :name => 'Mayotte', :alpha3 => 'MYT', :numeric => '175' },
{ :alpha2 => 'MX', :name => 'Mexico', :alpha3 => 'MEX', :numeric => '484' },
{ :alpha2 => 'FM', :name => 'Micronesia, Federated States of', :alpha3 => 'FSM', :numeric => '583' },
{ :alpha2 => 'MD', :name => 'Moldova, Republic of', :alpha3 => 'MDA', :numeric => '498' },
{ :alpha2 => 'MC', :name => 'Monaco', :alpha3 => 'MCO', :numeric => '492' },
{ :alpha2 => 'MN', :name => 'Mongolia', :alpha3 => 'MNG', :numeric => '496' },
{ :alpha2 => 'ME', :name => 'Montenegro', :alpha3 => 'MNE', :numeric => '499' },
{ :alpha2 => 'MS', :name => 'Montserrat', :alpha3 => 'MSR', :numeric => '500' },
{ :alpha2 => 'MA', :name => 'Morocco', :alpha3 => 'MAR', :numeric => '504' },
{ :alpha2 => 'MZ', :name => 'Mozambique', :alpha3 => 'MOZ', :numeric => '508' },
{ :alpha2 => 'MM', :name => 'Myanmar', :alpha3 => 'MMR', :numeric => '104' },
{ :alpha2 => 'NA', :name => 'Namibia', :alpha3 => 'NAM', :numeric => '516' },
{ :alpha2 => 'NR', :name => 'Nauru', :alpha3 => 'NRU', :numeric => '520' },
{ :alpha2 => 'NP', :name => 'Nepal', :alpha3 => 'NPL', :numeric => '524' },
{ :alpha2 => 'NL', :name => 'Netherlands', :alpha3 => 'NLD', :numeric => '528' },
{ :alpha2 => 'AN', :name => 'Netherlands Antilles', :alpha3 => 'ANT', :numeric => '530' },
{ :alpha2 => 'NC', :name => 'New Caledonia', :alpha3 => 'NCL', :numeric => '540' },
{ :alpha2 => 'NZ', :name => 'New Zealand', :alpha3 => 'NZL', :numeric => '554' },
{ :alpha2 => 'NI', :name => 'Nicaragua', :alpha3 => 'NIC', :numeric => '558' },
{ :alpha2 => 'NE', :name => 'Niger', :alpha3 => 'NER', :numeric => '562' },
{ :alpha2 => 'NG', :name => 'Nigeria', :alpha3 => 'NGA', :numeric => '566' },
{ :alpha2 => 'NU', :name => 'Niue', :alpha3 => 'NIU', :numeric => '570' },
{ :alpha2 => 'NF', :name => 'Norfolk Island', :alpha3 => 'NFK', :numeric => '574' },
{ :alpha2 => 'MP', :name => 'Northern Mariana Islands', :alpha3 => 'MNP', :numeric => '580' },
{ :alpha2 => 'NO', :name => 'Norway', :alpha3 => 'NOR', :numeric => '578' },
{ :alpha2 => 'OM', :name => 'Oman', :alpha3 => 'OMN', :numeric => '512' },
{ :alpha2 => 'PK', :name => 'Pakistan', :alpha3 => 'PAK', :numeric => '586' },
{ :alpha2 => 'PW', :name => 'Palau', :alpha3 => 'PLW', :numeric => '585' },
{ :alpha2 => 'PS', :name => 'Palestinian Territory, Occupied', :alpha3 => 'PSE', :numeric => '275' },
{ :alpha2 => 'PA', :name => 'Panama', :alpha3 => 'PAN', :numeric => '591' },
{ :alpha2 => 'PG', :name => 'Papua New Guinea', :alpha3 => 'PNG', :numeric => '598' },
{ :alpha2 => 'PY', :name => 'Paraguay', :alpha3 => 'PRY', :numeric => '600' },
{ :alpha2 => 'PE', :name => 'Peru', :alpha3 => 'PER', :numeric => '604' },
{ :alpha2 => 'PH', :name => 'Philippines', :alpha3 => 'PHL', :numeric => '608' },
{ :alpha2 => 'PN', :name => 'Pitcairn', :alpha3 => 'PCN', :numeric => '612' },
{ :alpha2 => 'PL', :name => 'Poland', :alpha3 => 'POL', :numeric => '616' },
{ :alpha2 => 'PT', :name => 'Portugal', :alpha3 => 'PRT', :numeric => '620' },
{ :alpha2 => 'PR', :name => 'Puerto Rico', :alpha3 => 'PRI', :numeric => '630' },
{ :alpha2 => 'QA', :name => 'Qatar', :alpha3 => 'QAT', :numeric => '634' },
{ :alpha2 => 'RE', :name => 'Reunion', :alpha3 => 'REU', :numeric => '638' },
{ :alpha2 => 'RO', :name => 'Romania', :alpha3 => 'ROM', :numeric => '642' },
{ :alpha2 => 'RU', :name => 'Russian Federation', :alpha3 => 'RUS', :numeric => '643' },
{ :alpha2 => 'RW', :name => 'Rwanda', :alpha3 => 'RWA', :numeric => '646' },
{ :alpha2 => 'BL', :name => 'Saint Barthélemy', :alpha3 => 'BLM', :numeric => '652' },
{ :alpha2 => 'SH', :name => 'Saint Helena', :alpha3 => 'SHN', :numeric => '654' },
{ :alpha2 => 'KN', :name => 'Saint Kitts and Nevis', :alpha3 => 'KNA', :numeric => '659' },
{ :alpha2 => 'LC', :name => 'Saint Lucia', :alpha3 => 'LCA', :numeric => '662' },
{ :alpha2 => 'MF', :name => 'Saint Martin (French part)', :alpha3 => 'MAF', :numeric => '663' },
{ :alpha2 => 'PM', :name => 'Saint Pierre and Miquelon', :alpha3 => 'SPM', :numeric => '666' },
{ :alpha2 => 'VC', :name => 'Saint Vincent and the Grenadines', :alpha3 => 'VCT', :numeric => '670' },
{ :alpha2 => 'WS', :name => 'Samoa', :alpha3 => 'WSM', :numeric => '882' },
{ :alpha2 => 'SM', :name => 'San Marino', :alpha3 => 'SMR', :numeric => '674' },
{ :alpha2 => 'ST', :name => 'Sao Tome and Principe', :alpha3 => 'STP', :numeric => '678' },
{ :alpha2 => 'SA', :name => 'Saudi Arabia', :alpha3 => 'SAU', :numeric => '682' },
{ :alpha2 => 'SN', :name => 'Senegal', :alpha3 => 'SEN', :numeric => '686' },
{ :alpha2 => 'RS', :name => 'Serbia', :alpha3 => 'SRB', :numeric => '688' },
{ :alpha2 => 'SC', :name => 'Seychelles', :alpha3 => 'SYC', :numeric => '690' },
{ :alpha2 => 'SL', :name => 'Sierra Leone', :alpha3 => 'SLE', :numeric => '694' },
{ :alpha2 => 'SG', :name => 'Singapore', :alpha3 => 'SGP', :numeric => '702' },
{ :alpha2 => 'SK', :name => 'Slovakia', :alpha3 => 'SVK', :numeric => '703' },
{ :alpha2 => 'SI', :name => 'Slovenia', :alpha3 => 'SVN', :numeric => '705' },
{ :alpha2 => 'SB', :name => 'Solomon Islands', :alpha3 => 'SLB', :numeric => '090' },
{ :alpha2 => 'SO', :name => 'Somalia', :alpha3 => 'SOM', :numeric => '706' },
{ :alpha2 => 'ZA', :name => 'South Africa', :alpha3 => 'ZAF', :numeric => '710' },
{ :alpha2 => 'GS', :name => 'South Georgia and the South Sandwich Islands', :alpha3 => 'SGS', :numeric => '239' },
{ :alpha2 => 'ES', :name => 'Spain', :alpha3 => 'ESP', :numeric => '724' },
{ :alpha2 => 'LK', :name => 'Sri Lanka', :alpha3 => 'LKA', :numeric => '144' },
{ :alpha2 => 'SD', :name => 'Sudan', :alpha3 => 'SDN', :numeric => '736' },
{ :alpha2 => 'SR', :name => 'Suriname', :alpha3 => 'SUR', :numeric => '740' },
{ :alpha2 => 'SJ', :name => 'Svalbard and Jan Mayen', :alpha3 => 'SJM', :numeric => '744' },
{ :alpha2 => 'SZ', :name => 'Swaziland', :alpha3 => 'SWZ', :numeric => '748' },
{ :alpha2 => 'SE', :name => 'Sweden', :alpha3 => 'SWE', :numeric => '752' },
{ :alpha2 => 'CH', :name => 'Switzerland', :alpha3 => 'CHE', :numeric => '756' },
{ :alpha2 => 'SY', :name => 'Syrian Arab Republic', :alpha3 => 'SYR', :numeric => '760' },
{ :alpha2 => 'TW', :name => 'Taiwan, Province of China', :alpha3 => 'TWN', :numeric => '158' },
{ :alpha2 => 'TJ', :name => 'Tajikistan', :alpha3 => 'TJK', :numeric => '762' },
{ :alpha2 => 'TZ', :name => 'Tanzania, United Republic of', :alpha3 => 'TZA', :numeric => '834' },
{ :alpha2 => 'TH', :name => 'Thailand', :alpha3 => 'THA', :numeric => '764' },
{ :alpha2 => 'TL', :name => 'Timor Leste', :alpha3 => 'TLS', :numeric => '626' },
{ :alpha2 => 'TG', :name => 'Togo', :alpha3 => 'TGO', :numeric => '768' },
{ :alpha2 => 'TK', :name => 'Tokelau', :alpha3 => 'TKL', :numeric => '772' },
{ :alpha2 => 'TO', :name => 'Tonga', :alpha3 => 'TON', :numeric => '776' },
{ :alpha2 => 'TT', :name => 'Trinidad and Tobago', :alpha3 => 'TTO', :numeric => '780' },
{ :alpha2 => 'TN', :name => 'Tunisia', :alpha3 => 'TUN', :numeric => '788' },
{ :alpha2 => 'TR', :name => 'Turkey', :alpha3 => 'TUR', :numeric => '792' },
{ :alpha2 => 'TM', :name => 'Turkmenistan', :alpha3 => 'TKM', :numeric => '795' },
{ :alpha2 => 'TC', :name => 'Turks and Caicos Islands', :alpha3 => 'TCA', :numeric => '796' },
{ :alpha2 => 'TV', :name => 'Tuvalu', :alpha3 => 'TUV', :numeric => '798' },
{ :alpha2 => 'UG', :name => 'Uganda', :alpha3 => 'UGA', :numeric => '800' },
{ :alpha2 => 'UA', :name => 'Ukraine', :alpha3 => 'UKR', :numeric => '804' },
{ :alpha2 => 'AE', :name => 'United Arab Emirates', :alpha3 => 'ARE', :numeric => '784' },
{ :alpha2 => 'GB', :name => 'United Kingdom', :alpha3 => 'GBR', :numeric => '826' },
{ :alpha2 => 'US', :name => 'United States', :alpha3 => 'USA', :numeric => '840' },
{ :alpha2 => 'UM', :name => 'United States Minor Outlying Islands', :alpha3 => 'UMI', :numeric => '581' },
{ :alpha2 => 'UY', :name => 'Uruguay', :alpha3 => 'URY', :numeric => '858' },
{ :alpha2 => 'UZ', :name => 'Uzbekistan', :alpha3 => 'UZB', :numeric => '860' },
{ :alpha2 => 'VU', :name => 'Vanuatu', :alpha3 => 'VUT', :numeric => '548' },
{ :alpha2 => 'VE', :name => 'Venezuela', :alpha3 => 'VEN', :numeric => '862' },
{ :alpha2 => 'VN', :name => 'Viet Nam', :alpha3 => 'VNM', :numeric => '704' },
{ :alpha2 => 'VG', :name => 'Virgin Islands, British', :alpha3 => 'VGB', :numeric => '092' },
{ :alpha2 => 'VI', :name => 'Virgin Islands, U.S.', :alpha3 => 'VIR', :numeric => '850' },
{ :alpha2 => 'WF', :name => 'Wallis and Futuna', :alpha3 => 'WLF', :numeric => '876' },
{ :alpha2 => 'EH', :name => 'Western Sahara', :alpha3 => 'ESH', :numeric => '732' },
{ :alpha2 => 'YE', :name => 'Yemen', :alpha3 => 'YEM', :numeric => '887' },
{ :alpha2 => 'ZM', :name => 'Zambia', :alpha3 => 'ZMB', :numeric => '894' },
{ :alpha2 => 'ZW', :name => 'Zimbabwe', :alpha3 => 'ZWE', :numeric => '716' },
{ :alpha2 => 'AX', :name => 'Åland Islands', :alpha3 => 'ALA', :numeric => '248' }
]
def self.find(name)
raise InvalidCountryCodeError, "Cannot lookup country for an empty name" if name.blank?
case name.length
when 2, 3
upcase_name = name.upcase
country_code = CountryCode.new(name)
country = COUNTRIES.detect{|c| c[country_code.format] == upcase_name }
else
country = COUNTRIES.detect{|c| c[:name] == name }
end
raise InvalidCountryCodeError, "No country could be found for the country #{name}" if country.nil?
Country.new(country.dup)
end
end
end

View File

@@ -0,0 +1,26 @@
module ActiveMerchant #:nodoc:
class ActiveMerchantError < StandardError #:nodoc:
end
class ConnectionError < ActiveMerchantError # :nodoc:
end
class RetriableConnectionError < ConnectionError # :nodoc:
end
class ResponseError < ActiveMerchantError # :nodoc:
attr_reader :response
def initialize(response, message = nil)
@response = response
@message = message
end
def to_s
"Failed with #{response.code} #{response.message if response.respond_to?(:message)}"
end
end
class ClientCertificateError < ActiveMerchantError # :nodoc
end
end

View File

@@ -0,0 +1,58 @@
module ActiveMerchant
module NetworkConnectionRetries
DEFAULT_RETRIES = 3
DEFAULT_CONNECTION_ERRORS = {
EOFError => "The remote server dropped the connection",
Errno::ECONNRESET => "The remote server reset the connection",
Timeout::Error => "The connection to the remote server timed out",
Errno::ETIMEDOUT => "The connection to the remote server timed out"
}
def self.included(base)
base.send(:attr_accessor, :retry_safe)
end
def retry_exceptions(options={})
connection_errors = DEFAULT_CONNECTION_ERRORS.merge(options[:connection_exceptions] || {})
retry_network_exceptions(options) do
begin
yield
rescue Errno::ECONNREFUSED => e
raise ActiveMerchant::RetriableConnectionError, "The remote server refused the connection"
rescue OpenSSL::X509::CertificateError => e
NetworkConnectionRetries.log(options[:logger], :error, e.message, options[:tag])
raise ActiveMerchant::ClientCertificateError, "The remote server did not accept the provided SSL certificate"
rescue *connection_errors.keys => e
raise ActiveMerchant::ConnectionError, connection_errors[e.class]
end
end
end
private
def retry_network_exceptions(options = {})
retries = options[:max] || DEFAULT_RETRIES
begin
yield
rescue ActiveMerchant::RetriableConnectionError => e
retries -= 1
retry unless retries.zero?
NetworkConnectionRetries.log(options[:logger], :error, e.message, options[:tag])
raise ActiveMerchant::ConnectionError, e.message
rescue ActiveMerchant::ConnectionError => e
retries -= 1
retry if (options[:retry_safe] || retry_safe) && !retries.zero?
NetworkConnectionRetries.log(options[:logger], :error, e.message, options[:tag])
raise
end
end
def self.log(logger, level, message, tag=nil)
tag ||= self.class.to_s
message = "[#{tag}] #{message}"
logger.send(level, message) if logger
end
end
end

View File

@@ -0,0 +1,24 @@
require 'cgi'
module ActiveMerchant
class PostData < Hash
class_attribute :required_fields, :instance_writer => false
self.required_fields = []
def []=(key, value)
return if value.blank? && !required?(key)
super
end
def to_post_data
collect { |key, value| "#{key}=#{CGI.escape(value.to_s)}" }.join("&")
end
alias_method :to_s, :to_post_data
private
def required?(key)
required_fields.include?(key)
end
end
end

View File

@@ -0,0 +1,69 @@
module ActiveMerchant #:nodoc:
module PostsData #:nodoc:
def self.included(base)
base.superclass_delegating_accessor :ssl_strict
base.ssl_strict = true
base.class_attribute :retry_safe
base.retry_safe = false
base.superclass_delegating_accessor :open_timeout
base.open_timeout = 60
base.superclass_delegating_accessor :read_timeout
base.read_timeout = 60
base.superclass_delegating_accessor :logger
base.superclass_delegating_accessor :wiredump_device
end
def ssl_get(endpoint, headers={})
ssl_request(:get, endpoint, nil, headers)
end
def ssl_post(endpoint, data, headers = {})
ssl_request(:post, endpoint, data, headers)
end
def ssl_request(method, endpoint, data, headers)
handle_response(raw_ssl_request(method, endpoint, data, headers))
end
def raw_ssl_request(method, endpoint, data, headers = {})
logger.warn "#{self.class} using ssl_strict=false, which is insecure" if logger unless ssl_strict
connection = new_connection(endpoint)
connection.open_timeout = open_timeout
connection.read_timeout = read_timeout
connection.retry_safe = retry_safe
connection.verify_peer = ssl_strict
connection.logger = logger
connection.tag = self.class.name
connection.wiredump_device = wiredump_device
connection.pem = @options[:pem] if @options
connection.pem_password = @options[:pem_password] if @options
connection.ignore_http_status = @options[:ignore_http_status] if @options
connection.request(method, data, headers)
end
private
def new_connection(endpoint)
Connection.new(endpoint)
end
def handle_response(response)
case response.code.to_i
when 200...300
response.body
else
raise ResponseError.new(response)
end
end
end
end

View File

@@ -0,0 +1,16 @@
module ActiveMerchant #:nodoc:
module RequiresParameters #:nodoc:
def requires!(hash, *params)
params.each do |param|
if param.is_a?(Array)
raise ArgumentError.new("Missing required parameter: #{param.first}") unless hash.has_key?(param.first)
valid_options = param[1..-1]
raise ArgumentError.new("Parameter: #{param.first} must be one of #{valid_options.to_sentence(:words_connector => 'or')}") unless valid_options.include?(hash[param.first])
else
raise ArgumentError.new("Missing required parameter: #{param}") unless hash.has_key?(param)
end
end
end
end
end

View File

@@ -0,0 +1,20 @@
require 'securerandom'
module ActiveMerchant #:nodoc:
module Utils #:nodoc:
def generate_unique_id
SecureRandom.hex(16)
end
module_function :generate_unique_id
def deprecated(message)
warning = Kernel.caller[1] + message
if respond_to?(:logger) && logger.present?
logger.warn(warning)
else
warn(warning)
end
end
end
end

View File

@@ -0,0 +1,81 @@
module ActiveMerchant #:nodoc:
module Validateable #:nodoc:
def valid?
errors.clear
before_validate if respond_to?(:before_validate, true)
validate if respond_to?(:validate, true)
errors.empty?
end
def initialize(attributes = {})
self.attributes = attributes
end
def errors
@errors ||= Errors.new(self)
end
private
def attributes=(attributes)
unless attributes.nil?
for key, value in attributes
send("#{key}=", value )
end
end
end
# This hash keeps the errors of the object
class Errors < HashWithIndifferentAccess
def initialize(base)
super() { |h, k| h[k] = [] ; h[k] }
@base = base
end
def count
size
end
def empty?
all? { |k, v| v && v.empty? }
end
# returns a specific fields error message.
# if more than one error is available we will only return the first. If no error is available
# we return an empty string
def on(field)
self[field].to_a.first
end
def add(field, error)
self[field] << error
end
def add_to_base(error)
add(:base, error)
end
def each_full
full_messages.each { |msg| yield msg }
end
def full_messages
result = []
self.each do |key, messages|
next if messages.blank?
if key == 'base'
result << "#{messages.first}"
else
result << "#{key.to_s.humanize} #{messages.first}"
end
end
result
end
end
end
end

View File

@@ -0,0 +1,3 @@
module ActiveUtils
VERSION = "1.0.5"
end

View File

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,12 @@
#!/usr/bin/env ruby
$:.unshift(File.dirname(__FILE__) + '/../lib')
require 'rubygems'
require 'bundler'
Bundler.setup
require 'test/unit'
require 'active_utils'
require 'mocha'
include ActiveMerchant

View File

@@ -0,0 +1,149 @@
require 'test_helper'
class ConnectionTest < Test::Unit::TestCase
def setup
@ok = stub(:code => 200, :message => 'OK', :body => 'success')
@endpoint = 'https://example.com/tx.php'
@connection = ActiveMerchant::Connection.new(@endpoint)
@connection.logger = stub(:info => nil, :debug => nil, :error => nil)
end
def test_connection_endpoint_parses_string_to_uri
assert_equal URI.parse(@endpoint), @connection.endpoint
end
def test_connection_endpoint_accepts_uri
endpoint = URI.parse(@endpoint)
connection = ActiveMerchant::Connection.new(endpoint)
assert_equal endpoint, connection.endpoint
end
def test_connection_endpoint_raises_uri_error
assert_raises URI::InvalidURIError do
ActiveMerchant::Connection.new("not a URI")
end
end
def test_successful_get_request
@connection.logger.expects(:info).twice
Net::HTTP.any_instance.expects(:get).with('/tx.php', {}).returns(@ok)
response = @connection.request(:get, nil, {})
assert_equal 'success', response.body
end
def test_successful_post_request
Net::HTTP.any_instance.expects(:post).with('/tx.php', 'data', ActiveMerchant::Connection::RUBY_184_POST_HEADERS).returns(@ok)
response = @connection.request(:post, 'data', {})
assert_equal 'success', response.body
end
def test_successful_put_request
Net::HTTP.any_instance.expects(:put).with('/tx.php', 'data', {}).returns(@ok)
response = @connection.request(:put, 'data', {})
assert_equal 'success', response.body
end
def test_successful_delete_request
Net::HTTP.any_instance.expects(:delete).with('/tx.php', {}).returns(@ok)
response = @connection.request(:delete, nil, {})
assert_equal 'success', response.body
end
def test_get_raises_argument_error_if_passed_data
assert_raise(ArgumentError) do
@connection.request(:get, 'data', {})
end
end
def test_request_raises_when_request_method_not_supported
assert_raise(ArgumentError) do
@connection.request(:head, nil, {})
end
end
def test_default_read_timeout
assert_equal ActiveMerchant::Connection::READ_TIMEOUT, @connection.read_timeout
end
def test_override_read_timeout
@connection.read_timeout = 20
assert_equal 20, @connection.read_timeout
end
def test_default_open_timeout
@connection.open_timeout = 20
assert_equal 20, @connection.open_timeout
end
def test_default_verify_peer
assert_equal ActiveMerchant::Connection::VERIFY_PEER, @connection.verify_peer
end
def test_override_verify_peer
@connection.verify_peer = false
assert_equal false, @connection.verify_peer
end
def test_unrecoverable_exception
@connection.logger.expects(:error).once
Net::HTTP.any_instance.expects(:post).raises(EOFError)
assert_raises(ActiveMerchant::ConnectionError) do
@connection.request(:post, '')
end
end
def test_failure_then_success_with_recoverable_exception
@connection.logger.expects(:error).never
Net::HTTP.any_instance.expects(:post).times(2).raises(Errno::ECONNREFUSED).then.returns(@ok)
assert_nothing_raised do
@connection.request(:post, '')
end
end
def test_failure_limit_reached
@connection.logger.expects(:error).once
Net::HTTP.any_instance.expects(:post).times(ActiveMerchant::Connection::MAX_RETRIES).raises(Errno::ECONNREFUSED)
assert_raises(ActiveMerchant::ConnectionError) do
@connection.request(:post, '')
end
end
def test_failure_then_success_with_retry_safe_enabled
Net::HTTP.any_instance.expects(:post).times(2).raises(EOFError).then.returns(@ok)
@connection.retry_safe = true
assert_nothing_raised do
@connection.request(:post, '')
end
end
def test_mixture_of_failures_with_retry_safe_enabled
Net::HTTP.any_instance.expects(:post).times(3).raises(Errno::ECONNRESET).
raises(Errno::ECONNREFUSED).
raises(EOFError)
@connection.retry_safe = true
assert_raises(ActiveMerchant::ConnectionError) do
@connection.request(:post, '')
end
end
def test_failure_with_ssl_certificate
@connection.logger.expects(:error).once
Net::HTTP.any_instance.expects(:post).raises(OpenSSL::X509::CertificateError)
assert_raises(ActiveMerchant::ClientCertificateError) do
@connection.request(:post, '')
end
end
end

View File

@@ -0,0 +1,31 @@
require 'test_helper'
class CountryCodeTest < Test::Unit::TestCase
def test_alpha2_country_code
code = CountryCode.new('CA')
assert_equal 'CA', code.value
assert_equal 'CA', code.to_s
assert_equal :alpha2, code.format
end
def test_lower_alpha2_country_code
code = CountryCode.new('ca')
assert_equal 'CA', code.value
assert_equal 'CA', code.to_s
assert_equal :alpha2, code.format
end
def test_alpha2_country_code
code = CountryCode.new('CAN')
assert_equal :alpha3, code.format
end
def test_numeric_code
code = CountryCode.new('004')
assert_equal :numeric, code.format
end
def test_invalid_code_format
assert_raise(CountryCodeFormatError){ CountryCode.new('Canada') }
end
end

View File

@@ -0,0 +1,68 @@
require 'test_helper'
class CountryTest < Test::Unit::TestCase
def test_country_from_hash
country = Country.new(:name => 'Canada', :alpha2 => 'CA', :alpha3 => 'CAN', :numeric => '124')
assert_equal 'CA', country.code(:alpha2).value
assert_equal 'CAN', country.code(:alpha3).value
assert_equal '124', country.code(:numeric).value
assert_equal 'Canada', country.to_s
end
def test_country_for_alpha2_code
country = Country.find('CA')
assert_equal 'CA', country.code(:alpha2).value
assert_equal 'CAN', country.code(:alpha3).value
assert_equal '124', country.code(:numeric).value
assert_equal 'Canada', country.to_s
end
def test_country_for_alpha3_code
country = Country.find('CAN')
assert_equal 'Canada', country.to_s
end
def test_country_for_numeric_code
country = Country.find('124')
assert_equal 'Canada', country.to_s
end
def test_find_country_by_name
country = Country.find('Canada')
assert_equal 'Canada', country.to_s
end
def test_find_unknown_country_name
assert_raise(InvalidCountryCodeError) do
Country.find('Asskickistan')
end
end
def test_find_australia
country = Country.find('AU')
assert_equal 'AU', country.code(:alpha2).value
country = Country.find('Australia')
assert_equal 'AU', country.code(:alpha2).value
end
def test_find_united_kingdom
country = Country.find('GB')
assert_equal 'GB', country.code(:alpha2).value
country = Country.find('United Kingdom')
assert_equal 'GB', country.code(:alpha2).value
end
def test_raise_on_nil_name
assert_raise(InvalidCountryCodeError) do
Country.find(nil)
end
end
def test_country_names_are_alphabetized
country_names = Country::COUNTRIES.map { | each | each[:name] }
assert_equal(country_names.sort, country_names)
end
end

View File

@@ -0,0 +1,127 @@
require 'test_helper'
require 'openssl'
require 'net/http'
class NetworkConnectionRetriesTest < Test::Unit::TestCase
class MyNewError < StandardError
end
include NetworkConnectionRetries
def setup
@logger = stubs(:logger)
@requester = stubs(:requester)
@ok = stub(:code => 200, :message => 'OK', :body => 'success')
end
def test_unrecoverable_exception
assert_raises(ActiveMerchant::ConnectionError) do
retry_exceptions do
raise EOFError
end
end
end
def test_unrecoverable_exception_logged_if_logger_provided
@logger.expects(:error).once
assert_raises(ActiveMerchant::ConnectionError) do
retry_exceptions :logger => @logger do
raise EOFError
end
end
end
def test_failure_then_success_with_recoverable_exception
@requester.expects(:post).times(2).raises(Errno::ECONNREFUSED).then.returns(@ok)
assert_nothing_raised do
retry_exceptions do
@requester.post
end
end
end
def test_failure_limit_reached
@requester.expects(:post).times(ActiveMerchant::NetworkConnectionRetries::DEFAULT_RETRIES).raises(Errno::ECONNREFUSED)
assert_raises(ActiveMerchant::ConnectionError) do
retry_exceptions do
@requester.post
end
end
end
def test_failure_limit_reached_logs_final_error
@logger.expects(:error).once
@requester.expects(:post).times(ActiveMerchant::NetworkConnectionRetries::DEFAULT_RETRIES).raises(Errno::ECONNREFUSED)
assert_raises(ActiveMerchant::ConnectionError) do
retry_exceptions(:logger => @logger) do
@requester.post
end
end
end
def test_failure_then_success_with_retry_safe_enabled
@requester.expects(:post).times(2).raises(EOFError).then.returns(@ok)
assert_nothing_raised do
retry_exceptions :retry_safe => true do
@requester.post
end
end
end
def test_mixture_of_failures_with_retry_safe_enabled
@requester.expects(:post).times(3).raises(Errno::ECONNRESET).
raises(Errno::ECONNREFUSED).
raises(EOFError)
assert_raises(ActiveMerchant::ConnectionError) do
retry_exceptions :retry_safe => true do
@requester.post
end
end
end
def test_failure_with_ssl_certificate
@requester.expects(:post).raises(OpenSSL::X509::CertificateError)
assert_raises(ActiveMerchant::ClientCertificateError) do
retry_exceptions do
@requester.post
end
end
end
def test_failure_with_ssl_certificate_logs_error_if_logger_specified
@logger.expects(:error).once
@requester.expects(:post).raises(OpenSSL::X509::CertificateError)
assert_raises(ActiveMerchant::ClientCertificateError) do
retry_exceptions :logger => @logger do
@requester.post
end
end
end
def test_failure_with_additional_exceptions_specified
@requester.expects(:post).raises(MyNewError)
assert_raises(ActiveMerchant::ConnectionError) do
retry_exceptions :connection_exceptions => {MyNewError => "my message"} do
@requester.post
end
end
end
def test_failure_without_additional_exceptions_specified
@requester.expects(:post).raises(MyNewError)
assert_raises(MyNewError) do
retry_exceptions do
@requester.post
end
end
end
end

View File

@@ -0,0 +1,50 @@
require 'test_helper'
class MyPost < ActiveMerchant::PostData
self.required_fields = [ :ccnumber, :ccexp, :firstname, :lastname, :username, :password, :order_id, :key, :time ]
end
class PostDataTest < Test::Unit::TestCase
def teardown
ActiveMerchant::PostData.required_fields = []
end
def test_element_assignment
name = 'Cody Fauser'
post = ActiveMerchant::PostData.new
post[:name] = name
assert_equal name, post[:name]
end
def test_ignore_blank_fields
post = ActiveMerchant::PostData.new
assert_equal 0, post.keys.size
post[:name] = ''
assert_equal 0, post.keys.size
post[:name] = nil
assert_equal 0, post.keys.size
end
def test_dont_ignore_required_blank_fields
ActiveMerchant::PostData.required_fields = [ :name ]
post = ActiveMerchant::PostData.new
assert_equal 0, post.keys.size
post[:name] = ''
assert_equal 1, post.keys.size
assert_equal '', post[:name]
post[:name] = nil
assert_equal 1, post.keys.size
assert_nil post[:name]
end
def test_subclass
post = MyPost.new
assert_equal [ :ccnumber, :ccexp, :firstname, :lastname, :username, :password, :order_id, :key, :time ], post.required_fields
end
end

View File

@@ -0,0 +1,35 @@
require 'test_helper'
require 'active_support/core_ext/class'
class PostsDataTest < Test::Unit::TestCase
class SSLPoster
include PostsData
attr_accessor :logger
end
def setup
@poster = SSLPoster.new
end
def test_logger_warns_if_ssl_strict_disabled
@poster.logger = stub()
@poster.logger.expects(:warn).with("PostsDataTest::SSLPoster using ssl_strict=false, which is insecure")
Connection.any_instance.stubs(:request)
SSLPoster.ssl_strict = false
@poster.raw_ssl_request(:post, "https://shopify.com", "", {})
end
def test_logger_no_warning_if_ssl_strict_enabled
@poster.logger = stub()
@poster.logger.stubs(:warn).never
Connection.any_instance.stubs(:request)
SSLPoster.ssl_strict = true
@poster.raw_ssl_request(:post, "https://shopify.com", "", {})
end
end

View File

@@ -0,0 +1,7 @@
require 'test_helper'
class UtilsTest < Test::Unit::TestCase
def test_unique_id_should_be_32_chars_and_alphanumeric
assert_match /^\w{32}$/, ActiveMerchant::Utils.generate_unique_id
end
end

View File

@@ -0,0 +1,59 @@
require 'test_helper'
class Dood
include ActiveMerchant::Validateable
attr_accessor :name, :email, :country
def validate
errors.add "name", "cannot be empty" if name.blank?
errors.add "email", "cannot be empty" if email.blank?
errors.add_to_base "The country cannot be blank" if country.blank?
end
end
class ValidateableTest < Test::Unit::TestCase
def setup
@dood = Dood.new
end
def test_validation
assert ! @dood.valid?
assert ! @dood.errors.empty?
end
def test_assigns
@dood = Dood.new(:name => "tobi", :email => "tobi@neech.de", :country => 'DE')
assert_equal "tobi", @dood.name
assert_equal "tobi@neech.de", @dood.email
assert @dood.valid?
end
def test_multiple_calls
@dood.name = "tobi"
assert !@dood.valid?
@dood.email = "tobi@neech.de"
assert !@dood.valid?
@dood.country = 'DE'
assert @dood.valid?
end
def test_messages
@dood.valid?
assert_equal "cannot be empty", @dood.errors.on('name')
assert_equal "cannot be empty", @dood.errors.on('email')
assert_equal nil, @dood.errors.on('doesnt_exist')
end
def test_full_messages
@dood.valid?
assert_equal ["Email cannot be empty", "Name cannot be empty", "The country cannot be blank"], @dood.errors.full_messages.sort
end
end

View File

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,402 @@
Protx Gateway
* Contributed by shiftx (Vincent)
Verifi Gateway
* Contributed by Paul Hepworth on 2007-05-12.
* Portions of Verifi Gateway Copyright (c) 2007 Paul Hepworth
Plug 'N Pay Gateway
* Contributed by Ryan Norbauer
PayJunction Gateway
* Contributed by Matt Sanders
E-xact Gateway
* Contributed by James Edward Gray II
Linkpoint Gateway
* Portions of the LinkPoint Gateway by Ryan Heneise
eWay Gateway
* Originally contributed by Lucas Carlson (mailto:lucas@rufy.com)
* Managed Payments support by Jason Stirk with improvements by Keith Pitt
CardStream Gateway
* Portions of the Cardstream gateway by Jonah Fox and Thomas Nichols
CyberSource Gateway
* Contributed by Matt Margolis (matt@mattmargolis.net)
NetRegistry Gateway
* Originally contributed by George Ogata (mailto: george.ogata@gmail.com)
DataCash Gateway (March 2, 2007)
* MoneySpyder, http://moneyspyder.co.uk and E-consultancy, http://www.e-consultancy.com
PSL Card Gateway (March 27, 2007)
* MoneySpyder, http://moneyspyder.co.uk
Viaklix Gateway (Sep 3, 2007)
* Originally contributed by Sal Scotto
Braintree Gateway (Sep 4, 2007)
* Originally contributed by Michael J. Mangino
* Portions of the BrainTree gateway by Jeremy Voorhis
Concord Efsnet Gateway (Sep 7, 2007)
* Originally contributed by snacktime
SecurePayTech Gateway (Oct 23, 2007)
* Originally contributed by Jasper Bryant-Greene
SkipJack Gateway (Nov 29, 2007)
* Originally contributed by Bill Bereza - http://atomicobject.com
HiTRUST Gateway (Dec 10, 2007)
* Jaded Pixel
Payflow NV Gateway (Mar 03, 2008)
* Greg Furmanek
PaypalNVGateway (Apr 12, 2008)
* Greg Furmanek
Beanstream (May 13, 2008)
* Created by xiaobozz ( xiaobozzz at gmail dot com )
* Secure Profiles support by Forrest Zeisler (http://github.com/forrest)
Sage (June, 2008)
* Cody
Modern Payments (June 13, 2008)
* Initial implementation by Jeremy Nicoll
* Additional portions by Cody Fauser
Wirecard Gateway (June 30, 2008)
* Initial implementation by Soleone
Transax Gateway (May 3, 2009)
* Mike Mangino
Merchant E-Solutions Gateway (May 3, 2009)
* Zac Williams, Robby Russell
Instapay Gateway (May 3, 2009)
* Thomas Rideout
Iridium Gateway (June 13, 2009)
* Phil Smy
MerchantWARE (July 7, 2009)
* Cody Fauser
FirstPay (July 24, 2009)
* Phil R
Ogone (July 20, 2009)
* Nicolas Jacobeus
Elavon (August 09, 2009)
* Jesse Storimer
JetPay (September 29, 2009)
* Phil Ripperger, Peter Williams
SallieMae (October 2, 2009)
* iamjwc
Netaxept (February 08, 2010)
* Nathaniel Talbott
Garanti (May 05, 2010)
* Selem Delul (moon@mac.home)
Braintree Blue Gateway (May 19th, 2010)
* Braintree (code@getbraintree.com)
Inspire Gateway (September 27, 2010)
* ryan r. smith
SecureNet Gateway (September 27, 2010)
* Kal
PayboxDirect Gateway (September 27, 2010)
* Donald Piret <donald@donaldpiret.com>
SagePay Form Offsite Gateway (October 14, 2010)
* Adrian Irving-Beer
DirecPay Gateway (October 14, 2010)
* Soleone
ePay Gateway (November 23, 2010)
* Original code by ePay (epay.dk)
* Refactored by Jonathan Rudenberg
iDEAL/Rabobank Gateway (January 10, 2011)
* Original code by Soemirno Kartosoewito
* Refactored by Cody Fauser
* Refactored and updated by Jonathan Rudenberg
Quantum Gateway
* Joshua Lippiner
* Refactored by Nathaniel Talbott
BluePay Gateway
* Mel Sleight
* Refactored by Nathaniel Talbott
Valitor Integration (January 2011)
* Nathaniel Talbott
* Sponsored by Sævar Öfjörð Magnússon
Barclays ePDQ
* Original code by Rob W (rfwatson)
* Refactored by Nathaniel Talbott
Federated Canada
* Bob Larrick (deathbob)
NMI
* Nathaniel Talbott (ntalbott)
QBMS
* Will Glozer (wg)
WorldPay Integration (Feb 17, 2011)
* Original code by Unknown from this patch: https://jadedpixel.lighthouseapp.com/projects/11599/tickets/3-patch-integration-support-for-worldpay-uk
* Refactored by Soleone
WorldPay Gateway
* Original code by Amit kumar (ask4amit@gmail.com)
* Refactored by Nathaniel Talbott (ntalbott)
Orbital Paymentech Gateway (July, 2009)
* Sam Vincent - http://ecommerce.versapay.com
DIRECTebanking - Payment Network AG (May, 2011)
* Gerwin Brunner (Vilango)
Stripe
* Ross Boucher (boucher)
Paystation (July, 2011)
* Nik Wakelin (nikz)
ePaymentPlans offsite gatway (June, 2011)
* Roberto Miranda (robertomiranda)
Optimal Payments (August, 2011)
* Jamie Macey (jamie)
CardSave (August, 2011)
* Tom Crinson (MrJaba)
Dwolla (September, 2011)
* James Armstead (armsteadj1)
Samurai (November, 2011)
* Joshua Krall (jkrall)
CertoDirect Gateway (February, 2012)
* Aleksei Gusev (hron)
Authorize.Net SIM Integration (February, 2012)
* Roger Pack (rdp)
* Nick Rogers (courtland)
NAB Transact (AU) Gateway (February, 2012)
* Tom Meier (tommeier)
iTransact XML Gateway (March, 2012)
* Kevin Motschiedler (motske)
Robokassa Integration (March, 2012)
* Vasiliy Ermolovich (nashby)
Moneris US Gateway (March, 2012)
* Michael Wood (eddanger)
Dotpay Integration (March, 2012)
* Przemysław Ciąćka (kacperix)
Vindicia gateway (April 2012)
* Steven Davidovitz (steved555)
MiGS gateway (April 2012)
* Michael Noack (mnoack)
* Justin Jones (nagash)
ePay integration (April 2012)
* Michael (ePay)
Litle gateway (May 2012)
* Gregory Drake (GregDrake)
Fat Zebra gateway (June 2012)
* Matthew Savage (amasses)
Metrics Global gateway (June 2012)
* Dan Knox (DanKnox)
EasyPay integration (July 2012)
* Vasiliy Ermolovich (nashby)
PayGateXML gateway (July 2012)
* bryan (rubyisbeautiful)
PayWay gateway (July 2012)
* Ben Zhang (BenZhang)
First Data integration (July 2012)
* Nick Rogers (courtland)
WebPay integration (July 2012)
* Vasiliy Ermolovich (nashby)
Suomen Maksuturva integration (July 2012)
* Antti Akonniemi (akonan)
Paxum integration (July 2012)
* Stanislav Mekhonoshin (Mehonoshin)
Balanced gateway (July 2012)
* Marshall Jones (mjallday)
PayFast integration (October 2012)
* Vasiliy Ermolovich (nashby)
A1Agregator (November 2012)
* Roman Ivanilov (england)
Liqpay integration (November 2012)
* beorc
eWay Rapid 3.0 gateway (December 2012)
* Nathaniel Talbott (ntalbott)
FirstData Global Gateway e4 (December 2012)
* Chris Sheppard (frobcode)
Spreedly Core gateway (December 2012)
* Duff OMelia (duff)
Pin gateway (February 2013)
* Myles Eftos (madpilot)
Merchant Warrior (February 2013)
* Ben Bruscella (benbruscella)
* Дмитрий Василец (pronix)
* Kirill Shirinkin (Fodoj)
* Nathaniel Talbott (ntalbott)
Paymill (February 2013)
* Duff O'Melia (duff)
EVO Canada (February 2013)
* Alex Dunae (alexdunae)
Finansbank WebPOS (March 2013)
* scamurcuoglu
Cardstream Modern (March 2013)
* Vincens (ExxKA)
Transnational (May 2013)
* Ben VandenBos (bvandenbos)

View File

@@ -0,0 +1,20 @@
Copyright (c) 2005-2010 Tobias Luetke
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -0,0 +1,221 @@
# Active Merchant
Active Merchant is an extraction from the e-commerce system [Shopify](http://www.shopify.com).
Shopify's requirements for a simple and unified API to access dozens of different payment
gateways with very different internal APIs was the chief principle in designing the library.
It was developed for usage in Ruby on Rails web applications and integrates seamlessly
as a Rails plugin, but it also works excellently as a stand alone Ruby library.
Active Merchant has been in production use since June 2006 and is now used in most modern
Ruby applications which deal with financial transactions. It is maintained by the
[Shopify](http://www.shopify.com) and [Spreedly](https://spreedly.com) teams, with much help
from an ever-growing set of contributors.
See [GettingStarted.md](GettingStarted.md) if you want to learn more about using Active Merchant in your
applications.
## Installation
### From Git
You can check out the latest source from git:
git clone git://github.com/Shopify/active_merchant.git
### From RubyGems
Installation from RubyGems:
gem install activemerchant
Or, if you're using Bundler, just add the following to your Gemfile:
gem 'activemerchant'
## Usage
This simple example demonstrates how a purchase can be made using a person's
credit card details.
require 'rubygems'
require 'active_merchant'
# Use the TrustCommerce test servers
ActiveMerchant::Billing::Base.mode = :test
gateway = ActiveMerchant::Billing::TrustCommerceGateway.new(
:login => 'TestMerchant',
:password => 'password')
# ActiveMerchant accepts all amounts as Integer values in cents
amount = 1000 # $10.00
# The card verification value is also known as CVV2, CVC2, or CID
credit_card = ActiveMerchant::Billing::CreditCard.new(
:first_name => 'Bob',
:last_name => 'Bobsen',
:number => '4242424242424242',
:month => '8',
:year => Time.now.year+1,
:verification_value => '000')
# Validating the card automatically detects the card type
if credit_card.valid?
# Capture $10 from the credit card
response = gateway.purchase(amount, credit_card)
if response.success?
puts "Successfully charged $#{sprintf("%.2f", amount / 100)} to the credit card #{credit_card.display_number}"
else
raise StandardError, response.message
end
end
For more in-depth documentation and tutorials, see [GettingStarted.md](GettingStarted.md) and the
[API documentation](http://rubydoc.info/github/Shopify/active_merchant/master/file/README.md).
## Supported Direct Payment Gateways
The [ActiveMerchant Wiki](http://github.com/Shopify/active_merchant/wikis) contains a [table of features supported by each gateway](http://github.com/Shopify/active_merchant/wikis/gatewayfeaturematrix).
* [Authorize.Net](http://www.authorize.net/) - US
* [Authorize.Net CIM](http://www.authorize.net/) - US
* [Balanced](https://www.balancedpayments.com/) - US
* [Banwire](https://www.banwire.com/) - MX
* [Barclays ePDQ](http://www.barclaycard.co.uk/business/accepting-payments/epdq-mpi/) - UK
* [Beanstream.com](http://www.beanstream.com/) - CA
* [BluePay](http://www.bluepay.com/) - US
* [Braintree](http://www.braintreepaymentsolutions.com) - US
* [CardStream](http://www.cardstream.com/) - UK
* [CertoDirect](http://www.certodirect.com/) - BE, BG, CZ, DK, DE, EE, IE, EL, ES, FR, IT, CY, LV, LT, LU, HU, MT, NL, AT, PL, PT, RO, SI, SK, FI, SE, UK
* [CyberSource](http://www.cybersource.com) - US
* [DataCash](http://www.datacash.com/) - UK
* [Efsnet](http://www.concordefsnet.com/) - US
* [Elavon MyVirtualMerchant](http://www.elavon.com) - US, CA
* [ePay](http://www.epay.dk/) - DK, SE, NO
* [EVO Canada](http://www.evocanada.com/) - CA
* [eWAY](http://www.eway.com.au/) - AU
* [eWay Rapid 3.0](http://www.eway.com.au/) - AU
* [E-xact](http://www.e-xact.com) - CA, US
* [Fat Zebra](https://www.fatzebra.com.au) - AU
* [Federated Canada](http://www.federatedcanada.com/) - CA
* [Finansbank WebPOS](https://www.fbwebpos.com/) - US, TR
* [FirstData Global Gateway e4](http://www.firstdata.com) - CA, US
* [FirstPay](http://www.first-pay.com) - US
* [Garanti Sanal POS](https://ccpos.garanti.com.tr/ccRaporlar/garanti/ccReports) - US, TR
* [HDFC](http://www.hdfcbank.com/sme/sme-details/merchant-services/guzh6m0i) - IN
* [Inspire](http://www.inspiregateway.com) - US
* [InstaPay](http://www.instapayllc.com) - US
* [Iridium](http://www.iridiumcorp.co.uk/) - UK, ES
* [iTransact](http://www.itransact.com/) - US
* [JetPay](http://www.jetpay.com) - US
* [LinkPoint](http://www.linkpoint.com/) - US
* [Litle](http://www.litle.com/) - US
* [Merchant e-Solutions](http://merchante-solutions.com/) - US
* [MerchantWare](http://merchantwarehouse.com/merchantware) - US
* [Merchant Warrior] (http://merchantwarrior.com) - AU
* [Mercury](http://www.mercurypay.com) - US
* [MasterCard Internet Gateway Service (MiGS)](http://mastercard.com/mastercardsps) - AU, AE, BD, BN, EG, HK, ID, IN, JO, KW, LB, LK, MU, MV, MY, NZ, OM, PH, QA, SA, SG, TT, VN
* [Modern Payments](http://www.modpay.com) - US
* [Moneris](http://www.moneris.com/) - CA
* [Moneris US](http://www.monerisusa.com/) - US
* [NABTransact](http://www.nab.com.au/nabtransact/) - AU
* [NELiX TransaX Gateway](http://www.nelixtransax.com) - US
* [Netaxept](http://www.betalingsterminal.no/Netthandel-forside) - NO, DK, SE, FI
* [NETbilling](http://www.netbilling.com) - US
* [NetPay](http://www.netpay.com.mx) - MX
* [NetRegistry](http://www.netregistry.com.au) - AU
* [NMI](http://nmi.com/) - US
* [Ogone DirectLink](http://www.ogone.com) - BE, DE, FR, NL, AT, CH
* [Optimal Payments](http://www.optimalpayments.com/) - CA, US, UK
* [Orbital Paymentech](http://chasepaymentech.com/) - CA, US, UK, GB
* [PayBox Direct](http://www.paybox.com) - FR
* [PayFast](https://www.payfast.co.za/) - ZA
* [PayGate PayXML](http://paygate.co.za/) - US, ZA
* [PayJunction](http://www.payjunction.com/) - US
* [PaymentExpress](http://www.paymentexpress.com/) - AU, MY, NZ, SG, ZA, UK, US
* [PAYMILL](https://www.paymill.com) - AD, AT, BE, CH, CY, CZ, DE, DK, EE, ES, FI, FO, FR, GB, GR, HU, IE, IL, IS, IT, LI, LT, LU, LV, MT, NL, NO, PL, PT, SE, SI, SK, TR, VA
* [PayPal Express Checkout](https://www.paypal.com/cgi-bin/webscr?cmd=xpt/merchant/ExpressCheckoutIntro-outside) - US, CA, SG, AU
* [PayPal Payflow Pro](https://www.paypal.com/cgi-bin/webscr?cmd=_payflow-pro-overview-outside) - US, CA, SG, AU
* [PayPal Website Payments Pro (UK)](https://www.paypal.com/uk/cgi-bin/webscr?cmd=_wp-pro-overview-outside) - UK
* [PayPal Website Payments Pro (CA)](https://www.paypal.com/cgi-bin/webscr?cmd=_wp-pro-overview-outside) - CA
* [PayPal Express Checkout](https://www.paypal.com/cgi-bin/webscr?cmd=xpt/merchant/ExpressCheckoutIntro-outside) - US
* [PayPal Website Payments Pro (US)](https://www.paypal.com/cgi-bin/webscr?cmd=_wp-pro-overview-outside) - US
* [PaySecure](http://www.commsecure.com.au/paysecure.shtml) - AU
* [PayWay](https://www.payway.com.au) - AU
* [Pin](http://www.pin.net.au/) - AU
* [Plug'n Pay](http://www.plugnpay.com/) - US
* [Psigate](http://www.psigate.com/) - CA
* [PSL Payment Solutions](http://www.paymentsolutionsltd.com/) - UK
* [Quantum](http://www.quantumgateway.com) - US
* [QuickBooks Merchant Services](http://payments.intuit.com/) - US
* [Quickpay](http://quickpay.dk/) - DK, SE
* [Rabobank Nederland](http://www.rabobank.nl/) - NL
* [Realex](http://www.realexpayments.com/) - IE, UK
* [Redsys](http://www.redsys.es) - ES
* [SagePay](http://www.sagepay.com) - UK
* [Sage Payment Solutions](http://www.sagepayments.com) - US, CA
* [Sallie Mae](http://www.salliemae.com) - US
* [SecureNet](http://www.securenet.com) - US
* [SecurePay](http://securepay.com.au) - AU
* [SecurePay](http://www.securepay.com/) - US
* [SecurePayTech](http://www.securepaytech.com/) - NZ
* [SkipJack](http://www.skipjack.com/) - US, CA
* [Spreedly Core](https://spreedlycore.com/) - 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
* [Stripe](https://stripe.com/) - US
* [TransFirst](http://www.transfirst.com/) - US
* [Transnational](http://www.tnbci.com/) - US
* [TrustCommerce](http://www.trustcommerce.com/) - US
* [USA ePay](http://www.usaepay.com/) - US
* [Verifi](http://www.verifi.com/) - US
* [ViaKLIX](http://viaklix.com) - US
* [Vindica](http://www.vindicia.com/) - US, CA, UK, AU, MX, BR, DE, KR, CN, HK
* [WebPay](https://webpay.jp/) - JP
* [Wirecard](http://www.wirecard.com) - DE
* [WorldPay](http://www.worldpay.com) - AU, HK, UK, US
## Supported Offsite Payment Gateways
* [2 Checkout](http://www.2checkout.com)
* [A1Agregator](http://a1agregator.ru/) - RU
* [Authorize.Net SIM](http://developer.authorize.net/api/sim/) - US
* [Banca Sella GestPay](https://www.sella.it/banca/ecommerce/gestpay/gestpay.jsp)
* [Chronopay](http://www.chronopay.com)
* [DirecPay](http://www.timesofmoney.com/direcpay/jsp/home.jsp)
* [Direct-eBanking / sofortueberweisung.de by Payment-Networks AG](https://www.payment-network.com/deb_com_en/merchantarea/home) - DE, AT, CH, BE, UK, NL
* [Dotpay](http://dotpay.pl)
* [Dwolla](https://www.dwolla.com/default.aspx)
* [ePay](http://www.epay.dk/epay-payment-solutions/)
* [First Data](https://firstdata.zendesk.com/entries/407522-first-data-global-gateway-e4sm-payment-pages-integration-manual)
* [HiTRUST](http://www.hitrust.com.hk/)
* [Moneybookers](http://www.moneybookers.com)
* [Nochex](http://www.nochex.com)
* [Paxum](https://www.paxum.com/)
* [PayPal Website Payments Standard](https://www.paypal.com/cgi-bin/webscr?cmd#_wp-standard-overview-outside)
* [Paysbuy](https://www.paysbuy.com/) - TH
* [RBK Money](https://rbkmoney.ru/) - RU
* [Robokassa](http://robokassa.ru/) - RU
* [SagePay Form](http://www.sagepay.com/products_services/sage_pay_go/integration/form)
* [Suomen Maksuturva](https://www.maksuturva.fi/services/vendor_services/integration_guidelines.html)
* [Valitor](http://www.valitor.is/) - IS
* [Verkkomaksut](http://www.verkkomaksut.fi) - FI
* [WebMoney](http://www.webmoney.ru) - RU
* [WebPay](http://webpay.by/)
* [WorldPay](http://www.worldpay.com)
## Contributing
The source code is hosted at [GitHub](http://github.com/Shopify/active_merchant), and can be fetched using:
git clone git://github.com/Shopify/active_merchant.git
Please see the [ActiveMerchant Guide to Contributing](http://github.com/Shopify/active_merchant/wikis/contributing) for
information on adding a new gateway to ActiveMerchant.
Please don't touch the CHANGELOG in your pull requests, we'll add the appropriate CHANGELOG entries
at release time.
[![Build Status](https://secure.travis-ci.org/Shopify/active_merchant.png)](http://travis-ci.org/Shopify/active_merchant)
[![Code Climate](https://codeclimate.com/badge.png)](https://codeclimate.com/github/Shopify/active_merchant)

View File

@@ -0,0 +1,20 @@
-----BEGIN CERTIFICATE-----
MIIDNjCCAh6gAwIBAgIBADANBgkqhkiG9w0BAQUFADBBMRMwEQYDVQQDDApjb2R5
ZmF1c2VyMRUwEwYKCZImiZPyLGQBGRYFZ21haWwxEzARBgoJkiaJk/IsZAEZFgNj
b20wHhcNMDcwMjIyMTcyMTI3WhcNMDgwMjIyMTcyMTI3WjBBMRMwEQYDVQQDDApj
b2R5ZmF1c2VyMRUwEwYKCZImiZPyLGQBGRYFZ21haWwxEzARBgoJkiaJk/IsZAEZ
FgNjb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC6T4Iqt5iWvAlU
iXI6L8UO0URQhIC65X/gJ9hL/x4lwSl/ckVm/R/bPrJGmifT+YooFv824N3y/TIX
25o/lZtRj1TUZJK4OCb0aVzosQVxBHSe6rLmxO8cItNTMOM9wn3thaITFrTa1DOQ
O3wqEjvW2L6VMozVfK1MfjL9IGgy0rCnl+2g4Gh4jDDpkLfnMG5CWI6cTCf3C1ye
ytOpWgi0XpOEy8nQWcFmt/KCQ/kFfzBo4QxqJi54b80842EyvzWT9OB7Oew/CXZG
F2yIHtiYxonz6N09vvSzq4CvEuisoUFLKZnktndxMEBKwJU3XeSHAbuS7ix40OKO
WKuI54fHAgMBAAGjOTA3MAkGA1UdEwQCMAAwCwYDVR0PBAQDAgSwMB0GA1UdDgQW
BBR9QQpefI3oDCAxiqJW/3Gg6jI6qjANBgkqhkiG9w0BAQUFAAOCAQEAs0lX26O+
HpyMp7WL+SgZuM8k76AjfOHuKajl2GEn3S8pWYGpsa0xu07HtehJhKLiavrfUYeE
qlFtyYMUyOh6/1S2vfkH6VqjX7mWjoi7XKHW/99fkMS40B5SbN+ypAUst+6c5R84
w390mjtLHpdDE6WQYhS6bFvBN53vK6jG3DLyCJc0K9uMQ7gdHWoxq7RnG92ncQpT
ThpRA+fky5Xt2Q63YJDnJpkYAz79QIama1enSnd4jslKzSl89JS2luq/zioPe/Us
hbyalWR1+HrhgPoSPq7nk+s2FQUBJ9UZFK1lgMzho/4fZgzJwbu+cO8SNuaLS/bj
hPaSTyVU0yCSnw==
-----END CERTIFICATE-----

View File

@@ -0,0 +1,63 @@
#--
# Copyright (c) 2005-2010 Tobias Luetke
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#++
require 'active_support'
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'
require 'active_support/core_ext/module/attribute_accessors'
begin
require 'active_support/base64'
unless defined?(Base64)
Base64 = ActiveSupport::Base64
end
unless Base64.respond_to?(:strict_encode64)
def Base64.strict_encode64(v)
ActiveSupport::Base64.encode64s(v)
end
end
rescue LoadError
require 'base64'
end
require 'securerandom'
require 'builder'
require 'cgi'
require 'rexml/document'
require 'active_utils'
require 'active_merchant/billing'
require 'active_merchant/version'
module ActiveMerchant #:nodoc:
module Billing #:nodoc:
autoload :Integrations, 'active_merchant/billing/integrations'
end
end

View File

@@ -0,0 +1,9 @@
require 'active_merchant/billing/avs_result'
require 'active_merchant/billing/cvv_result'
require 'active_merchant/billing/credit_card_methods'
require 'active_merchant/billing/credit_card_formatting'
require 'active_merchant/billing/credit_card'
require 'active_merchant/billing/base'
require 'active_merchant/billing/check'
require 'active_merchant/billing/response'
require 'active_merchant/billing/gateways'

View File

@@ -0,0 +1,98 @@
#!ruby19
# encoding: utf-8
module ActiveMerchant
module Billing
# Implements the Address Verification System
# https://www.wellsfargo.com/downloads/pdf/biz/merchant/visa_avs.pdf
# http://en.wikipedia.org/wiki/Address_Verification_System
# http://apps.cybersource.com/library/documentation/dev_guides/CC_Svcs_IG/html/app_avs_cvn_codes.htm#app_AVS_CVN_codes_7891_48375
# http://imgserver.skipjack.com/imgServer/5293710/AVS%20and%20CVV2.pdf
# http://www.emsecommerce.net/avs_cvv2_response_codes.htm
class AVSResult
MESSAGES = {
'A' => 'Street address matches, but 5-digit and 9-digit postal code do not match.',
'B' => 'Street address matches, but postal code not verified.',
'C' => 'Street address and postal code do not match.',
'D' => 'Street address and postal code match.',
'E' => 'AVS data is invalid or AVS is not allowed for this card type.',
'F' => 'Card member\'s name does not match, but billing postal code matches.',
'G' => 'Non-U.S. issuing bank does not support AVS.',
'H' => 'Card member\'s name does not match. Street address and postal code match.',
'I' => 'Address not verified.',
'J' => 'Card member\'s name, billing address, and postal code match. Shipping information verified and chargeback protection guaranteed through the Fraud Protection Program.',
'K' => 'Card member\'s name matches but billing address and billing postal code do not match.',
'L' => 'Card member\'s name and billing postal code match, but billing address does not match.',
'M' => 'Street address and postal code match.',
'N' => 'Street address and postal code do not match.',
'O' => 'Card member\'s name and billing address match, but billing postal code does not match.',
'P' => 'Postal code matches, but street address not verified.',
'Q' => 'Card member\'s name, billing address, and postal code match. Shipping information verified but chargeback protection not guaranteed.',
'R' => 'System unavailable.',
'S' => 'U.S.-issuing bank does not support AVS.',
'T' => 'Card member\'s name does not match, but street address matches.',
'U' => 'Address information unavailable.',
'V' => 'Card member\'s name, billing address, and billing postal code match.',
'W' => 'Street address does not match, but 9-digit postal code matches.',
'X' => 'Street address and 9-digit postal code match.',
'Y' => 'Street address and 5-digit postal code match.',
'Z' => 'Street address does not match, but 5-digit postal code matches.'
}
# Map vendor's AVS result code to a postal match code
POSTAL_MATCH_CODE = {
'Y' => %w( D H F H J L M P Q V W X Y Z ),
'N' => %w( A C K N O ),
'X' => %w( G S ),
nil => %w( B E I R T U )
}.inject({}) do |map, (type, codes)|
codes.each { |code| map[code] = type }
map
end
# Map vendor's AVS result code to a street match code
STREET_MATCH_CODE = {
'Y' => %w( A B D H J M O Q T V X Y ),
'N' => %w( C K L N W Z ),
'X' => %w( G S ),
nil => %w( E F I P R U )
}.inject({}) do |map, (type, codes)|
codes.each { |code| map[code] = type }
map
end
attr_reader :code, :message, :street_match, :postal_match
def self.messages
MESSAGES
end
def initialize(attrs)
attrs ||= {}
@code = attrs[:code].upcase unless attrs[:code].blank?
@message = self.class.messages[code]
if attrs[:street_match].blank?
@street_match = STREET_MATCH_CODE[code]
else
@street_match = attrs[:street_match].upcase
end
if attrs[:postal_match].blank?
@postal_match = POSTAL_MATCH_CODE[code]
else
@postal_match = attrs[:postal_match].upcase
end
end
def to_hash
{ 'code' => code,
'message' => message,
'street_match' => street_match,
'postal_match' => postal_match
}
end
end
end
end

View File

@@ -0,0 +1,56 @@
module ActiveMerchant #:nodoc:
module Billing #:nodoc:
module Base
# Set ActiveMerchant gateways in test mode.
#
# ActiveMerchant::Billing::Base.gateway_mode = :test
mattr_accessor :gateway_mode
# Set ActiveMerchant integrations in test mode.
#
# ActiveMerchant::Billing::Base.integration_mode = :test
mattr_accessor :integration_mode
# Set both the mode of both the gateways and integrations
# at once
mattr_reader :mode
def self.mode=(mode)
@@mode = mode
self.gateway_mode = mode
self.integration_mode = mode
end
self.mode = :production
# Return the matching gateway for the provider
# * <tt>bogus</tt>: BogusGateway - Does nothing (for testing)
# * <tt>moneris</tt>: MonerisGateway
# * <tt>authorize_net</tt>: AuthorizeNetGateway
# * <tt>trust_commerce</tt>: TrustCommerceGateway
#
# ActiveMerchant::Billing::Base.gateway('moneris').new
def self.gateway(name)
Billing.const_get("#{name.to_s.downcase}_gateway".camelize)
end
# Return the matching integration module
# You can then get the notification from the module
# * <tt>bogus</tt>: Bogus - Does nothing (for testing)
# * <tt>chronopay</tt>: Chronopay
# * <tt>paypal</tt>: Paypal
#
# chronopay = ActiveMerchant::Billing::Base.integration('chronopay')
# notification = chronopay.notification(raw_post)
#
def self.integration(name)
Billing::Integrations.const_get("#{name.to_s.downcase}".camelize)
end
# A check to see if we're in test mode
def self.test?
self.gateway_mode == :test
end
end
end
end

View File

@@ -0,0 +1,69 @@
module ActiveMerchant #:nodoc:
module Billing #:nodoc:
# The Check object is a plain old Ruby object, similar to CreditCard. It supports validation
# of necessary attributes such as checkholder's name, routing and account numbers, but it is
# not backed by any database.
#
# You may use Check in place of CreditCard with any gateway that supports it.
class Check
include Validateable
attr_accessor :first_name, :last_name,
:bank_name, :routing_number, :account_number,
:account_holder_type, :account_type, :number
# Used for Canadian bank accounts
attr_accessor :institution_number, :transit_number
def name
@name ||= "#{@first_name} #{@last_name}".strip
end
def name=(value)
return if value.blank?
@name = value
segments = value.split(' ')
@last_name = segments.pop
@first_name = segments.join(' ')
end
def validate
[:name, :routing_number, :account_number].each do |attr|
errors.add(attr, "cannot be empty") if self.send(attr).blank?
end
errors.add(:routing_number, "is invalid") unless valid_routing_number?
errors.add(:account_holder_type, "must be personal or business") if
!account_holder_type.blank? && !%w[business personal].include?(account_holder_type.to_s)
errors.add(:account_type, "must be checking or savings") if
!account_type.blank? && !%w[checking savings].include?(account_type.to_s)
end
def type
'check'
end
# Routing numbers may be validated by calculating a checksum and dividing it by 10. The
# formula is:
# (3(d1 + d4 + d7) + 7(d2 + d5 + d8) + 1(d3 + d6 + d9))mod 10 = 0
# See http://en.wikipedia.org/wiki/Routing_transit_number#Internal_checksums
def valid_routing_number?
d = routing_number.to_s.split('').map(&:to_i).select { |d| (0..9).include?(d) }
case d.size
when 9 then
checksum = ((3 * (d[0] + d[3] + d[6])) +
(7 * (d[1] + d[4] + d[7])) +
(d[2] + d[5] + d[8])) % 10
case checksum
when 0 then true
else false
end
else false
end
end
end
end
end

View File

@@ -0,0 +1,278 @@
require 'time'
require 'date'
require 'active_merchant/billing/expiry_date'
module ActiveMerchant #:nodoc:
module Billing #:nodoc:
# A +CreditCard+ object represents a physical credit card, and is capable of validating the various
# data associated with these.
#
# At the moment, the following credit card types are supported:
#
# * Visa
# * MasterCard
# * Discover
# * American Express
# * Diner's Club
# * JCB
# * Switch
# * Solo
# * Dankort
# * Maestro
# * Forbrugsforeningen
# * Laser
#
# For testing purposes, use the 'bogus' credit card brand. This skips the vast majority of
# validations, allowing you to focus on your core concerns until you're ready to be more concerned
# with the details of particular credit cards or your gateway.
#
# == Testing With CreditCard
# Often when testing we don't care about the particulars of a given card brand. When using the 'test'
# mode in your {Gateway}, there are six different valid card numbers: 1, 2, 3, 'success', 'fail',
# and 'error'.
#
# For details, see {CreditCardMethods::ClassMethods#valid_number?}
#
# == Example Usage
# cc = CreditCard.new(
# :first_name => 'Steve',
# :last_name => 'Smith',
# :month => '9',
# :year => '2010',
# :brand => 'visa',
# :number => '4242424242424242'
# )
#
# cc.valid? # => true
# cc.display_number # => XXXX-XXXX-XXXX-4242
#
class CreditCard
include CreditCardMethods
include Validateable
cattr_accessor :require_verification_value
self.require_verification_value = true
# Returns or sets the credit card number.
#
# @return [String]
attr_accessor :number
# Returns or sets the expiry month for the card.
#
# @return [Integer]
attr_accessor :month
# Returns or sets the expiry year for the card.
#
# @return [Integer]
attr_accessor :year
# Returns or sets the credit card brand.
#
# Valid card types are
#
# * +'visa'+
# * +'master'+
# * +'discover'+
# * +'american_express'+
# * +'diners_club'+
# * +'jcb'+
# * +'switch'+
# * +'solo'+
# * +'dankort'+
# * +'maestro'+
# * +'forbrugsforeningen'+
# * +'laser'+
#
# Or, if you wish to test your implementation, +'bogus'+.
#
# @return (String) the credit card brand
attr_accessor :brand
# Returns or sets the first name of the card holder.
#
# @return [String]
attr_accessor :first_name
# Returns or sets the last name of the card holder.
#
# @return [String]
attr_accessor :last_name
# Required for Switch / Solo cards
attr_accessor :start_month, :start_year, :issue_number
# Returns or sets the card verification value.
#
# This attribute is optional but recommended. The verification value is
# a {card security code}[http://en.wikipedia.org/wiki/Card_security_code]. If provided,
# the gateway will attempt to validate the value.
#
# @return [String] the verification value
attr_accessor :verification_value
def type
self.class.deprecated "CreditCard#type is deprecated and will be removed from a future release of ActiveMerchant. Please use CreditCard#brand instead."
brand
end
def type=(value)
self.class.deprecated "CreditCard#type is deprecated and will be removed from a future release of ActiveMerchant. Please use CreditCard#brand instead."
self.brand = value
end
# Provides proxy access to an expiry date object
#
# @return [ExpiryDate]
def expiry_date
ExpiryDate.new(@month, @year)
end
# Returns whether the credit card has expired.
#
# @return +true+ if the card has expired, +false+ otherwise
def expired?
expiry_date.expired?
end
# Returns whether either the +first_name+ or the +last_name+ attributes has been set.
def name?
first_name? || last_name?
end
# Returns whether the +first_name+ attribute has been set.
def first_name?
@first_name.present?
end
# Returns whether the +last_name+ attribute has been set.
def last_name?
@last_name.present?
end
# Returns the full name of the card holder.
#
# @return [String] the full name of the card holder
def name
[@first_name, @last_name].compact.join(' ')
end
def name=(full_name)
names = full_name.split
self.last_name = names.pop
self.first_name = names.join(" ")
end
def verification_value?
!@verification_value.blank?
end
# Returns a display-friendly version of the card number.
#
# All but the last 4 numbers are replaced with an "X", and hyphens are
# inserted in order to improve legibility.
#
# @example
# credit_card = CreditCard.new(:number => "2132542376824338")
# credit_card.display_number # "XXXX-XXXX-XXXX-4338"
#
# @return [String] a display-friendly version of the card number
def display_number
self.class.mask(number)
end
def first_digits
self.class.first_digits(number)
end
def last_digits
self.class.last_digits(number)
end
# Validates the credit card details.
#
# Any validation errors are added to the {#errors} attribute.
def validate
validate_essential_attributes
# Bogus card is pretty much for testing purposes. Lets just skip these extra tests if its used
return if brand == 'bogus'
validate_card_brand
validate_card_number
validate_verification_value
validate_switch_or_solo_attributes
end
def self.requires_verification_value?
require_verification_value
end
private
def before_validate #:nodoc:
self.month = month.to_i
self.year = year.to_i
self.start_month = start_month.to_i unless start_month.nil?
self.start_year = start_year.to_i unless start_year.nil?
self.number = number.to_s.gsub(/[^\d]/, "")
self.brand.downcase! if brand.respond_to?(:downcase)
self.brand = self.class.brand?(number) if brand.blank?
end
def validate_card_number #:nodoc:
if number.blank?
errors.add :number, "is required"
elsif !CreditCard.valid_number?(number)
errors.add :number, "is not a valid credit card number"
end
unless errors.on(:number) || errors.on(:brand)
errors.add :brand, "does not match the card number" unless CreditCard.matching_brand?(number, brand)
end
end
def validate_card_brand #:nodoc:
errors.add :brand, "is required" if brand.blank? && number.present?
errors.add :brand, "is invalid" unless brand.blank? || CreditCard.card_companies.keys.include?(brand)
end
alias_method :validate_card_type, :validate_card_brand
def validate_essential_attributes #:nodoc:
errors.add :first_name, "cannot be empty" if @first_name.blank?
errors.add :last_name, "cannot be empty" if @last_name.blank?
if @month.to_i.zero? || @year.to_i.zero?
errors.add :month, "is required" if @month.to_i.zero?
errors.add :year, "is required" if @year.to_i.zero?
else
errors.add :month, "is not a valid month" unless valid_month?(@month)
errors.add :year, "expired" if expired?
errors.add :year, "is not a valid year" unless expired? || valid_expiry_year?(@year)
end
end
def validate_switch_or_solo_attributes #:nodoc:
if %w[switch solo].include?(brand)
unless valid_month?(@start_month) && valid_start_year?(@start_year) || valid_issue_number?(@issue_number)
if @issue_number.blank?
errors.add :start_month, "is invalid" unless valid_month?(@start_month)
errors.add :start_year, "is invalid" unless valid_start_year?(@start_year)
errors.add :issue_number, "cannot be empty"
else
errors.add :issue_number, "is invalid" unless valid_issue_number?(@issue_number)
end
end
end
end
def validate_verification_value #:nodoc:
if CreditCard.requires_verification_value?
errors.add :verification_value, "is required" unless verification_value?
end
end
end
end
end

View File

@@ -0,0 +1,21 @@
module ActiveMerchant #:nodoc:
module Billing #:nodoc:
module CreditCardFormatting
# This method is used to format numerical information pertaining to credit cards.
#
# format(2005, :two_digits) # => "05"
# format(05, :four_digits) # => "0005"
def format(number, option)
return '' if number.blank?
case option
when :two_digits ; sprintf("%.2i", number.to_i)[-2..-1]
when :four_digits ; sprintf("%.4i", number.to_i)[-4..-1]
else number
end
end
end
end
end

View File

@@ -0,0 +1,143 @@
module ActiveMerchant #:nodoc:
module Billing #:nodoc:
# Convenience methods that can be included into a custom Credit Card object, such as an ActiveRecord based Credit Card object.
module CreditCardMethods
CARD_COMPANIES = {
'visa' => /^4\d{12}(\d{3})?$/,
'master' => /^(5[1-5]\d{4}|677189)\d{10}$/,
'discover' => /^(6011|65\d{2}|64[4-9]\d)\d{12}|(62\d{14})$/,
'american_express' => /^3[47]\d{13}$/,
'diners_club' => /^3(0[0-5]|[68]\d)\d{11}$/,
'jcb' => /^35(28|29|[3-8]\d)\d{12}$/,
'switch' => /^6759\d{12}(\d{2,3})?$/,
'solo' => /^6767\d{12}(\d{2,3})?$/,
'dankort' => /^5019\d{12}$/,
'maestro' => /^(5[06-8]|6\d)\d{10,17}$/,
'forbrugsforeningen' => /^600722\d{10}$/,
'laser' => /^(6304|6706|6709|6771(?!89))\d{8}(\d{4}|\d{6,7})?$/
}
def self.included(base)
base.extend(ClassMethods)
end
def valid_month?(month)
(1..12).include?(month.to_i)
end
def valid_expiry_year?(year)
(Time.now.year..Time.now.year + 20).include?(year.to_i)
end
def valid_start_year?(year)
year.to_s =~ /^\d{4}$/ && year.to_i > 1987
end
def valid_issue_number?(number)
number.to_s =~ /^\d{1,2}$/
end
module ClassMethods
# Returns true if it validates. Optionally, you can pass a card brand as an argument and
# make sure it is of the correct brand.
#
# References:
# - http://perl.about.com/compute/perl/library/nosearch/P073000.htm
# - http://www.beachnet.com/~hstiles/cardtype.html
def valid_number?(number)
valid_test_mode_card_number?(number) ||
valid_card_number_length?(number) &&
valid_checksum?(number)
end
# Regular expressions for the known card companies.
#
# References:
# - http://en.wikipedia.org/wiki/Credit_card_number
# - http://www.barclaycardbusiness.co.uk/information_zone/processing/bin_rules.html
def card_companies
CARD_COMPANIES
end
# Returns a string containing the brand of card from the list of known information below.
# Need to check the cards in a particular order, as there is some overlap of the allowable ranges
#--
# TODO Refactor this method. We basically need to tighten up the Maestro Regexp.
#
# Right now the Maestro regexp overlaps with the MasterCard regexp (IIRC). If we can tighten
# things up, we can boil this whole thing down to something like...
#
# def brand?(number)
# return 'visa' if valid_test_mode_card_number?(number)
# card_companies.find([nil]) { |brand, regexp| number =~ regexp }.first.dup
# end
#
def brand?(number)
return 'bogus' if valid_test_mode_card_number?(number)
card_companies.reject { |c,p| c == 'maestro' }.each do |company, pattern|
return company.dup if number =~ pattern
end
return 'maestro' if number =~ card_companies['maestro']
return nil
end
def type?(number)
deprecated "CreditCard#type? is deprecated and will be removed from a future release of ActiveMerchant. Please use CreditCard#brand? instead."
brand?(number)
end
def first_digits(number)
number.to_s.slice(0,6)
end
def last_digits(number)
number.to_s.length <= 4 ? number : number.to_s.slice(-4..-1)
end
def mask(number)
"XXXX-XXXX-XXXX-#{last_digits(number)}"
end
# Checks to see if the calculated brand matches the specified brand
def matching_brand?(number, brand)
brand?(number) == brand
end
def matching_type?(number, brand)
deprecated "CreditCard#matching_type? is deprecated and will be removed from a future release of ActiveMerchant. Please use CreditCard#matching_brand? instead."
matching_brand?(number, brand)
end
def deprecated(message)
warn(Kernel.caller[1] + message)
end
private
def valid_card_number_length?(number) #:nodoc:
number.to_s.length >= 12
end
def valid_test_mode_card_number?(number) #:nodoc:
ActiveMerchant::Billing::Base.test? &&
%w[1 2 3 success failure error].include?(number.to_s)
end
# Checks the validity of a card number by use of the the Luhn Algorithm.
# Please see http://en.wikipedia.org/wiki/Luhn_algorithm for details.
def valid_checksum?(number) #:nodoc:
sum = 0
for i in 0..number.length
weight = number[-1 * (i + 2), 1].to_i * (2 - (i % 2))
sum += (weight < 10) ? weight : weight - 9
end
(number[-1,1].to_i == (10 - sum % 10) % 10)
end
end
end
end
end

View File

@@ -0,0 +1,38 @@
module ActiveMerchant
module Billing
# Result of the Card Verification Value check
# http://www.bbbonline.org/eExport/doc/MerchantGuide_cvv2.pdf
# Check additional codes from cybersource website
class CVVResult
MESSAGES = {
'D' => 'Suspicious transaction',
'I' => 'Failed data validation check',
'M' => 'Match',
'N' => 'No Match',
'P' => 'Not Processed',
'S' => 'Should have been present',
'U' => 'Issuer unable to process request',
'X' => 'Card does not support verification'
}
def self.messages
MESSAGES
end
attr_reader :code, :message
def initialize(code)
@code = code.upcase unless code.blank?
@message = MESSAGES[@code]
end
def to_hash
{
'code' => code,
'message' => message
}
end
end
end
end

View File

@@ -0,0 +1,34 @@
require 'date'
module ActiveMerchant
module Billing
class CreditCard
class ExpiryDate #:nodoc:
attr_reader :month, :year
def initialize(month, year)
@month = month.to_i
@year = year.to_i
end
def expired? #:nodoc:
Time.now.utc > expiration
end
def expiration #:nodoc:
begin
Time.utc(year, month, month_days, 23, 59, 59)
rescue ArgumentError
Time.at(0).utc
end
end
private
def month_days
mdays = [nil,31,28,31,30,31,30,31,31,30,31,30,31]
mdays[2] = 29 if Date.leap?(year)
mdays[month]
end
end
end
end
end

View File

@@ -0,0 +1,177 @@
require 'net/http'
require 'net/https'
require 'active_merchant/billing/response'
module ActiveMerchant #:nodoc:
module Billing #:nodoc:
#
# == Description
# The Gateway class is the base class for all ActiveMerchant gateway implementations.
#
# The standard list of gateway functions that most concrete gateway subclasses implement is:
#
# * <tt>purchase(money, creditcard, options = {})</tt>
# * <tt>authorize(money, creditcard, options = {})</tt>
# * <tt>capture(money, authorization, options = {})</tt>
# * <tt>void(identification, options = {})</tt>
# * <tt>credit(money, identification, options = {})</tt>
#
# Some gateways include features for recurring billing
#
# * <tt>recurring(money, creditcard, options = {})</tt>
#
# Some gateways also support features for storing credit cards:
#
# * <tt>store(creditcard, options = {})</tt>
# * <tt>unstore(identification, options = {})</tt>
#
# === Gateway Options
# The options hash consists of the following options:
#
# * <tt>:order_id</tt> - The order number
# * <tt>:ip</tt> - The IP address of the customer making the purchase
# * <tt>:customer</tt> - The name, customer number, or other information that identifies the customer
# * <tt>:invoice</tt> - The invoice number
# * <tt>:merchant</tt> - The name or description of the merchant offering the product
# * <tt>:description</tt> - A description of the transaction
# * <tt>:email</tt> - The email address of the customer
# * <tt>:currency</tt> - The currency of the transaction. Only important when you are using a currency that is not the default with a gateway that supports multiple currencies.
# * <tt>:billing_address</tt> - A hash containing the billing address of the customer.
# * <tt>:shipping_address</tt> - A hash containing the shipping address of the customer.
#
# The <tt>:billing_address</tt>, and <tt>:shipping_address</tt> hashes can have the following keys:
#
# * <tt>:name</tt> - The full name of the customer.
# * <tt>:company</tt> - The company name of the customer.
# * <tt>:address1</tt> - The primary street address of the customer.
# * <tt>:address2</tt> - Additional line of address information.
# * <tt>:city</tt> - The city of the customer.
# * <tt>:state</tt> - The state of the customer. The 2 digit code for US and Canadian addresses. The full name of the state or province for foreign addresses.
# * <tt>:country</tt> - The [ISO 3166-1-alpha-2 code](http://www.iso.org/iso/country_codes/iso_3166_code_lists/english_country_names_and_code_elements.htm) for the customer.
# * <tt>:zip</tt> - The zip or postal code of the customer.
# * <tt>:phone</tt> - The phone number of the customer.
#
# == Implmenting new gateways
#
# See the {ActiveMerchant Guide to Contributing}[https://github.com/Shopify/active_merchant/wiki/Contributing]
#
class Gateway
include PostsData
include RequiresParameters
include CreditCardFormatting
include Utils
DEBIT_CARDS = [ :switch, :solo ]
CURRENCIES_WITHOUT_FRACTIONS = [ 'JPY', 'HUF', 'TWD' ]
CREDIT_DEPRECATION_MESSAGE = "Support for using credit to refund existing transactions is deprecated and will be removed from a future release of ActiveMerchant. Please use the refund method instead."
cattr_reader :implementations
@@implementations = []
def self.inherited(subclass)
super
@@implementations << subclass
end
# The format of the amounts used by the gateway
# :dollars => '12.50'
# :cents => '1250'
class_attribute :money_format
self.money_format = :dollars
# The default currency for the transactions if no currency is provided
class_attribute :default_currency
# The countries of merchants the gateway supports
class_attribute :supported_countries
self.supported_countries = []
# The supported card types for the gateway
class_attribute :supported_cardtypes
self.supported_cardtypes = []
class_attribute :homepage_url
class_attribute :display_name
class_attribute :test_url, :live_url
class_attribute :abstract_class
self.abstract_class = false
# The application making the calls to the gateway
# Useful for things like the PayPal build notation (BN) id fields
superclass_delegating_accessor :application_id
self.application_id = 'ActiveMerchant'
attr_reader :options
# Use this method to check if your gateway of interest supports a credit card of some type
def self.supports?(card_type)
supported_cardtypes.include?(card_type.to_sym)
end
def self.card_brand(source)
result = source.respond_to?(:brand) ? source.brand : source.type
result.to_s.downcase
end
def card_brand(source)
self.class.card_brand(source)
end
# Initialize a new gateway.
#
# See the documentation for the gateway you will be using to make sure there are no other
# required options.
def initialize(options = {})
@options = options
end
# Are we running in test mode?
def test?
(@options.has_key?(:test) ? @options[:test] : Base.test?)
end
private # :nodoc: all
def name
self.class.name.scan(/\:\:(\w+)Gateway/).flatten.first
end
def amount(money)
return nil if money.nil?
cents = if money.respond_to?(:cents)
deprecated "Support for Money objects is deprecated and will be removed from a future release of ActiveMerchant. Please use an Integer value in cents"
money.cents
else
money
end
if money.is_a?(String)
raise ArgumentError, 'money amount must be a positive Integer in cents.'
end
if self.money_format == :cents
cents.to_s
else
sprintf("%.2f", cents.to_f / 100)
end
end
def localized_amount(money, currency)
amount = amount(money)
CURRENCIES_WITHOUT_FRACTIONS.include?(currency.to_s) ? amount.split('.').first : amount
end
def currency(money)
money.respond_to?(:currency) ? money.currency : self.default_currency
end
def requires_start_date_or_issue_number?(credit_card)
return false if card_brand(credit_card).blank?
DEBIT_CARDS.include?(card_brand(credit_card).to_sym)
end
end
end
end

View File

@@ -0,0 +1,17 @@
module ActiveMerchant
module Billing
autoload :Gateway, 'active_merchant/billing/gateway'
Dir[File.dirname(__FILE__) + '/gateways/**/*.rb'].each do |f|
# Get camelized class name
filename = File.basename(f, '.rb')
# Add _gateway suffix
gateway_name = filename + '_gateway'
# Camelize the string to get the class name
gateway_class = gateway_name.camelize
# Register for autoloading
autoload gateway_class, f
end
end
end

View File

@@ -0,0 +1,724 @@
module ActiveMerchant #:nodoc:
module Billing #:nodoc:
# For more information on the Authorize.Net Gateway please visit their {Integration Center}[http://developer.authorize.net/]
#
# The login and password are not the username and password you use to
# login to the Authorize.Net Merchant Interface. Instead, you will
# use the API Login ID as the login and Transaction Key as the
# password.
#
# ==== How to Get Your API Login ID and Transaction Key
#
# 1. Log into the Merchant Interface
# 2. Select Settings from the Main Menu
# 3. Click on API Login ID and Transaction Key in the Security section
# 4. Type in the answer to the secret question configured on setup
# 5. Click Submit
#
# ==== Automated Recurring Billing (ARB)
#
# Automated Recurring Billing (ARB) is an optional service for submitting and managing recurring, or subscription-based, transactions.
#
# To use recurring, update_recurring, cancel_recurring and status_recurring ARB must be enabled for your account.
#
# Information about ARB is available on the {Authorize.Net website}[http://www.authorize.net/solutions/merchantsolutions/merchantservices/automatedrecurringbilling/].
# Information about the ARB API is available at the {Authorize.Net Integration Center}[http://developer.authorize.net/]
class AuthorizeNetGateway < Gateway
API_VERSION = '3.1'
class_attribute :arb_test_url, :arb_live_url
self.test_url = "https://test.authorize.net/gateway/transact.dll"
self.live_url = "https://secure.authorize.net/gateway/transact.dll"
self.arb_test_url = 'https://apitest.authorize.net/xml/v1/request.api'
self.arb_live_url = 'https://api.authorize.net/xml/v1/request.api'
class_attribute :duplicate_window
APPROVED, DECLINED, ERROR, FRAUD_REVIEW = 1, 2, 3, 4
RESPONSE_CODE, RESPONSE_REASON_CODE, RESPONSE_REASON_TEXT = 0, 2, 3
AVS_RESULT_CODE, TRANSACTION_ID, CARD_CODE_RESPONSE_CODE = 5, 6, 38
self.default_currency = 'USD'
self.supported_countries = ['US', 'CA', 'GB']
self.supported_cardtypes = [:visa, :master, :american_express, :discover, :diners_club, :jcb]
self.homepage_url = 'http://www.authorize.net/'
self.display_name = 'Authorize.Net'
CARD_CODE_ERRORS = %w( N S )
AVS_ERRORS = %w( A E N R W Z )
AVS_REASON_CODES = %w(27 45)
AUTHORIZE_NET_ARB_NAMESPACE = 'AnetApi/xml/v1/schema/AnetApiSchema.xsd'
RECURRING_ACTIONS = {
:create => 'ARBCreateSubscription',
:update => 'ARBUpdateSubscription',
:cancel => 'ARBCancelSubscription',
:status => 'ARBGetSubscriptionStatus'
}
# Creates a new AuthorizeNetGateway
#
# The gateway requires that a valid login and password be passed
# in the +options+ hash.
#
# ==== Options
#
# * <tt>:login</tt> -- The Authorize.Net API Login ID (REQUIRED)
# * <tt>:password</tt> -- The Authorize.Net Transaction Key. (REQUIRED)
# * <tt>:test</tt> -- +true+ or +false+. If true, perform transactions against the test server.
# Otherwise, perform transactions against the production server.
def initialize(options = {})
requires!(options, :login, :password)
super
end
# Performs an authorization, which reserves the funds on the customer's credit card, but does not
# charge the card.
#
# ==== Parameters
#
# * <tt>money</tt> -- The amount to be authorized as an Integer value in cents.
# * <tt>paysource</tt> -- The CreditCard or Check details for the transaction.
# * <tt>options</tt> -- A hash of optional parameters.
def authorize(money, paysource, options = {})
post = {}
add_currency_code(post, money, options)
add_invoice(post, options)
add_payment_source(post, paysource, options)
add_address(post, options)
add_customer_data(post, options)
add_duplicate_window(post)
commit('AUTH_ONLY', money, post)
end
# Perform a purchase, which is essentially an authorization and capture in a single operation.
#
# ==== Parameters
#
# * <tt>money</tt> -- The amount to be purchased as an Integer value in cents.
# * <tt>paysource</tt> -- The CreditCard or Check details for the transaction.
# * <tt>options</tt> -- A hash of optional parameters.
def purchase(money, paysource, options = {})
post = {}
add_currency_code(post, money, options)
add_invoice(post, options)
add_payment_source(post, paysource, options)
add_address(post, options)
add_customer_data(post, options)
add_duplicate_window(post)
commit('AUTH_CAPTURE', money, post)
end
# Captures the funds from an authorized transaction.
#
# ==== Parameters
#
# * <tt>money</tt> -- The amount to be captured as an Integer value in cents.
# * <tt>authorization</tt> -- The authorization returned from the previous authorize request.
def capture(money, authorization, options = {})
post = {:trans_id => authorization}
add_customer_data(post, options)
add_invoice(post, options)
commit('PRIOR_AUTH_CAPTURE', money, post)
end
# Void a previous transaction
#
# ==== Parameters
#
# * <tt>authorization</tt> - The authorization returned from the previous authorize request.
def void(authorization, options = {})
post = {:trans_id => authorization}
add_duplicate_window(post)
commit('VOID', nil, post)
end
# Refund a transaction.
#
# This transaction indicates to the gateway that
# money should flow from the merchant to the customer.
#
# ==== Parameters
#
# * <tt>money</tt> -- The amount to be credited to the customer as an Integer value in cents.
# * <tt>identification</tt> -- The ID of the original transaction against which the refund is being issued.
# * <tt>options</tt> -- A hash of parameters.
#
# ==== Options
#
# * <tt>:card_number</tt> -- The credit card number the refund is being issued to. (REQUIRED)
# You can either pass the last four digits of the card number or the full card number.
# * <tt>:first_name</tt> -- The first name of the account being refunded.
# * <tt>:last_name</tt> -- The last name of the account being refunded.
# * <tt>:zip</tt> -- The postal code of the account being refunded.
def refund(money, identification, options = {})
requires!(options, :card_number)
post = { :trans_id => identification,
:card_num => options[:card_number]
}
post[:first_name] = options[:first_name] if options[:first_name]
post[:last_name] = options[:last_name] if options[:last_name]
post[:zip] = options[:zip] if options[:zip]
add_invoice(post, options)
add_duplicate_window(post)
commit('CREDIT', money, post)
end
def credit(money, identification, options = {})
deprecated CREDIT_DEPRECATION_MESSAGE
refund(money, identification, options)
end
# Create a recurring payment.
#
# This transaction creates a new Automated Recurring Billing (ARB) subscription. Your account must have ARB enabled.
#
# ==== Parameters
#
# * <tt>money</tt> -- The amount to be charged to the customer at each interval as an Integer value in cents.
# * <tt>creditcard</tt> -- The CreditCard details for the transaction.
# * <tt>options</tt> -- A hash of parameters.
#
# ==== Options
#
# * <tt>:interval</tt> -- A hash containing information about the interval of time between payments. Must
# contain the keys <tt>:length</tt> and <tt>:unit</tt>. <tt>:unit</tt> can be either <tt>:months</tt> or <tt>:days</tt>.
# If <tt>:unit</tt> is <tt>:months</tt> then <tt>:length</tt> must be an integer between 1 and 12 inclusive.
# If <tt>:unit</tt> is <tt>:days</tt> then <tt>:length</tt> must be an integer between 7 and 365 inclusive.
# For example, to charge the customer once every three months the hash would be
# +:interval => { :unit => :months, :length => 3 }+ (REQUIRED)
# * <tt>:duration</tt> -- A hash containing keys for the <tt>:start_date</tt> the subscription begins (also the date the
# initial billing occurs) and the total number of billing <tt>:occurences</tt> or payments for the subscription. (REQUIRED)
def recurring(money, creditcard, options={})
requires!(options, :interval, :duration, :billing_address)
requires!(options[:interval], :length, [:unit, :days, :months])
requires!(options[:duration], :start_date, :occurrences)
requires!(options[:billing_address], :first_name, :last_name)
options[:credit_card] = creditcard
options[:amount] = money
request = build_recurring_request(:create, options)
recurring_commit(:create, request)
end
# Update a recurring payment's details.
#
# This transaction updates an existing Automated Recurring Billing (ARB) subscription. Your account must have ARB enabled
# and the subscription must have already been created previously by calling +recurring()+. The ability to change certain
# details about a recurring payment is dependent on transaction history and cannot be determined until after calling
# +update_recurring()+. See the ARB XML Guide for such conditions.
#
# ==== Parameters
#
# * <tt>options</tt> -- A hash of parameters.
#
# ==== Options
#
# * <tt>:subscription_id</tt> -- A string containing the <tt>:subscription_id</tt> of the recurring payment already in place
# for a given credit card. (REQUIRED)
def update_recurring(options={})
requires!(options, :subscription_id)
request = build_recurring_request(:update, options)
recurring_commit(:update, request)
end
# Cancel a recurring payment.
#
# This transaction cancels an existing Automated Recurring Billing (ARB) subscription. Your account must have ARB enabled
# and the subscription must have already been created previously by calling recurring()
#
# ==== Parameters
#
# * <tt>subscription_id</tt> -- A string containing the +subscription_id+ of the recurring payment already in place
# for a given credit card. (REQUIRED)
def cancel_recurring(subscription_id)
request = build_recurring_request(:cancel, :subscription_id => subscription_id)
recurring_commit(:cancel, request)
end
# Get Subscription Status of a recurring payment.
#
# This transaction gets the status of an existing Automated Recurring Billing (ARB) subscription. Your account must have ARB enabled.
#
# ==== Parameters
#
# * <tt>subscription_id</tt> -- A string containing the +subscription_id+ of the recurring payment already in place
# for a given credit card. (REQUIRED)
def status_recurring(subscription_id)
request = build_recurring_request(:status, :subscription_id => subscription_id)
recurring_commit(:status, request)
end
private
def commit(action, money, parameters)
parameters[:amount] = amount(money) unless action == 'VOID'
# Only activate the test_request when the :test option is passed in
parameters[:test_request] = @options[:test] ? 'TRUE' : 'FALSE'
url = test? ? self.test_url : self.live_url
data = ssl_post url, post_data(action, parameters)
response = parse(data)
response[:action] = action
message = message_from(response)
# Return the response. The authorization can be taken out of the transaction_id
# Test Mode on/off is something we have to parse from the response text.
# It usually looks something like this
#
# (TESTMODE) Successful Sale
test_mode = test? || message =~ /TESTMODE/
Response.new(success?(response), message, response,
:test => test_mode,
:authorization => response[:transaction_id],
:fraud_review => fraud_review?(response),
:avs_result => { :code => response[:avs_result_code] },
:cvv_result => response[:card_code]
)
end
def success?(response)
response[:response_code] == APPROVED
end
def fraud_review?(response)
response[:response_code] == FRAUD_REVIEW
end
def parse(body)
fields = split(body)
results = {
:response_code => fields[RESPONSE_CODE].to_i,
:response_reason_code => fields[RESPONSE_REASON_CODE],
:response_reason_text => fields[RESPONSE_REASON_TEXT],
:avs_result_code => fields[AVS_RESULT_CODE],
:transaction_id => fields[TRANSACTION_ID],
:card_code => fields[CARD_CODE_RESPONSE_CODE]
}
results
end
def post_data(action, parameters = {})
post = {}
post[:version] = API_VERSION
post[:login] = @options[:login]
post[:tran_key] = @options[:password]
post[:relay_response] = "FALSE"
post[:type] = action
post[:delim_data] = "TRUE"
post[:delim_char] = ","
post[:encap_char] = "$"
post[:solution_ID] = application_id if application_id.present? && application_id != "ActiveMerchant"
request = post.merge(parameters).collect { |key, value| "x_#{key}=#{CGI.escape(value.to_s)}" }.join("&")
request
end
def add_currency_code(post, money, options)
post[:currency_code] = options[:currency] || currency(money)
end
def add_invoice(post, options)
post[:invoice_num] = options[:order_id]
post[:description] = options[:description]
end
def add_creditcard(post, creditcard)
post[:card_num] = creditcard.number
post[:card_code] = creditcard.verification_value if creditcard.verification_value?
post[:exp_date] = expdate(creditcard)
post[:first_name] = creditcard.first_name
post[:last_name] = creditcard.last_name
end
def add_payment_source(params, source, options={})
if card_brand(source) == "check"
add_check(params, source, options)
else
add_creditcard(params, source)
end
end
def add_check(post, check, options)
post[:method] = "ECHECK"
post[:bank_name] = check.bank_name
post[:bank_aba_code] = check.routing_number
post[:bank_acct_num] = check.account_number
post[:bank_acct_type] = check.account_type
post[:echeck_type] = "WEB"
post[:bank_acct_name] = check.name
post[:bank_check_number] = check.number if check.number.present?
post[:recurring_billing] = (options[:recurring] ? "TRUE" : "FALSE")
end
def add_customer_data(post, options)
if options.has_key? :email
post[:email] = options[:email]
post[:email_customer] = false
end
if options.has_key? :customer
post[:cust_id] = options[:customer] if Float(options[:customer]) rescue nil
end
if options.has_key? :ip
post[:customer_ip] = options[:ip]
end
end
# x_duplicate_window won't be sent by default, because sending it changes the response.
# "If this field is present in the request with or without a value, an enhanced duplicate transaction response will be sent."
# (as of 2008-12-30) http://www.authorize.net/support/AIM_guide_SCC.pdf
def add_duplicate_window(post)
unless duplicate_window.nil?
post[:duplicate_window] = duplicate_window
end
end
def add_address(post, options)
if address = options[:billing_address] || options[:address]
post[:address] = address[:address1].to_s
post[:company] = address[:company].to_s
post[:phone] = address[:phone].to_s
post[:zip] = address[:zip].to_s
post[:city] = address[:city].to_s
post[:country] = address[:country].to_s
post[:state] = address[:state].blank? ? 'n/a' : address[:state]
end
if address = options[:shipping_address]
post[:ship_to_first_name] = address[:first_name].to_s
post[:ship_to_last_name] = address[:last_name].to_s
post[:ship_to_address] = address[:address1].to_s
post[:ship_to_company] = address[:company].to_s
post[:ship_to_phone] = address[:phone].to_s
post[:ship_to_zip] = address[:zip].to_s
post[:ship_to_city] = address[:city].to_s
post[:ship_to_country] = address[:country].to_s
post[:ship_to_state] = address[:state].blank? ? 'n/a' : address[:state]
end
end
# Make a ruby type out of the response string
def normalize(field)
case field
when "true" then true
when "false" then false
when "" then nil
when "null" then nil
else field
end
end
def message_from(results)
if results[:response_code] == DECLINED
return CVVResult.messages[ results[:card_code] ] if CARD_CODE_ERRORS.include?(results[:card_code])
if AVS_REASON_CODES.include?(results[:response_reason_code]) && AVS_ERRORS.include?(results[:avs_result_code])
return AVSResult.messages[ results[:avs_result_code] ]
end
end
(results[:response_reason_text] ? results[:response_reason_text].chomp('.') : '')
end
def expdate(creditcard)
year = sprintf("%.4i", creditcard.year)
month = sprintf("%.2i", creditcard.month)
"#{month}#{year[-2..-1]}"
end
def split(response)
response[1..-2].split(/\$,\$/)
end
# ARB
# Builds recurring billing request
def build_recurring_request(action, options = {})
unless RECURRING_ACTIONS.include?(action)
raise StandardError, "Invalid Automated Recurring Billing Action: #{action}"
end
xml = Builder::XmlMarkup.new(:indent => 2)
xml.instruct!(:xml, :version => '1.0', :encoding => 'utf-8')
xml.tag!("#{RECURRING_ACTIONS[action]}Request", :xmlns => AUTHORIZE_NET_ARB_NAMESPACE) do
add_arb_merchant_authentication(xml)
# Merchant-assigned reference ID for the request
xml.tag!('refId', options[:ref_id]) if options[:ref_id]
send("build_arb_#{action}_subscription_request", xml, options)
end
end
# Contains the merchants payment gateway account authentication information
def add_arb_merchant_authentication(xml)
xml.tag!('merchantAuthentication') do
xml.tag!('name', @options[:login])
xml.tag!('transactionKey', @options[:password])
end
end
# Builds body for ARBCreateSubscriptionRequest
def build_arb_create_subscription_request(xml, options)
# Subscription
add_arb_subscription(xml, options)
xml.target!
end
# Builds body for ARBUpdateSubscriptionRequest
def build_arb_update_subscription_request(xml, options)
xml.tag!('subscriptionId', options[:subscription_id])
# Adds Subscription
add_arb_subscription(xml, options)
xml.target!
end
# Builds body for ARBCancelSubscriptionRequest
def build_arb_cancel_subscription_request(xml, options)
xml.tag!('subscriptionId', options[:subscription_id])
xml.target!
end
# Builds body for ARBGetSubscriptionStatusRequest
def build_arb_status_subscription_request(xml, options)
xml.tag!('subscriptionId', options[:subscription_id])
xml.target!
end
# Adds subscription information
def add_arb_subscription(xml, options)
xml.tag!('subscription') do
# Merchant-assigned name for the subscription (optional)
xml.tag!('name', options[:subscription_name]) if options[:subscription_name]
# Contains information about the payment schedule
add_arb_payment_schedule(xml, options)
# The amount to be billed to the customer
# for each payment in the subscription
xml.tag!('amount', amount(options[:amount])) if options[:amount]
if trial = options[:trial]
# The amount to be charged for each payment during a trial period (conditional)
xml.tag!('trialAmount', amount(trial[:amount])) if trial[:amount]
end
# Contains either the customers credit card
# or bank account payment information
add_arb_payment(xml, options)
# Contains order information (optional)
add_arb_order(xml, options)
# Contains information about the customer
add_arb_customer(xml, options)
# Contains the customer's billing address information
add_arb_address(xml, 'billTo', options[:billing_address])
# Contains the customer's shipping address information (optional)
add_arb_address(xml, 'shipTo', options[:shipping_address])
end
end
# Adds information about the interval of time between payments
def add_arb_interval(xml, options)
interval = options[:interval]
return unless interval
xml.tag!('interval') do
# The measurement of time, in association with the Interval Unit,
# that is used to define the frequency of the billing occurrences
xml.tag!('length', interval[:length])
# The unit of time, in association with the Interval Length,
# between each billing occurrence
xml.tag!('unit', interval[:unit].to_s)
end
end
# Adds information about the subscription duration
def add_arb_duration(xml, options)
duration = options[:duration]
return unless duration
# The date the subscription begins
# (also the date the initial billing occurs)
xml.tag!('startDate', duration[:start_date]) if duration[:start_date]
# Number of billing occurrences or payments for the subscription
xml.tag!('totalOccurrences', duration[:occurrences]) if duration[:occurrences]
end
def add_arb_payment_schedule(xml, options)
return unless options[:interval] || options[:duration]
xml.tag!('paymentSchedule') do
# Contains information about the interval of time between payments
add_arb_interval(xml, options)
add_arb_duration(xml, options)
if trial = options[:trial]
# Number of billing occurrences or payments in the trial period (optional)
xml.tag!('trialOccurrences', trial[:occurrences]) if trial[:occurrences]
end
end
end
# Adds customer's credit card or bank account payment information
def add_arb_payment(xml, options)
return unless options[:credit_card] || options[:bank_account]
xml.tag!('payment') do
# Contains the customers credit card information
add_arb_credit_card(xml, options)
# Contains the customers bank account information
add_arb_bank_account(xml, options)
end
end
# Adds customers credit card information
# Note: This element should only be included
# when the payment method is credit card.
def add_arb_credit_card(xml, options)
credit_card = options[:credit_card]
return unless credit_card
xml.tag!('creditCard') do
# The credit card number used for payment of the subscription
xml.tag!('cardNumber', credit_card.number)
# The expiration date of the credit card used for the subscription
xml.tag!('expirationDate', arb_expdate(credit_card))
end
end
# Adds customers bank account information
# Note: This element should only be included
# when the payment method is bank account.
def add_arb_bank_account(xml, options)
bank_account = options[:bank_account]
return unless bank_account
xml.tag!('bankAccount') do
# The type of bank account used for payment of the subscription
xml.tag!('accountType', bank_account[:account_type])
# The routing number of the customers bank
xml.tag!('routingNumber', bank_account[:routing_number])
# The bank account number used for payment of the subscription
xml.tag!('accountNumber', bank_account[:account_number])
# The full name of the individual associated
# with the bank account number
xml.tag!('nameOfAccount', bank_account[:name_of_account])
# The full name of the individual associated
# with the bank account number (optional)
xml.tag!('bankName', bank_account[:bank_name]) if bank_account[:bank_name]
# The type of electronic check transaction used for the subscription
xml.tag!('echeckType', bank_account[:echeck_type])
end
end
# Adds order information (optional)
def add_arb_order(xml, options)
order = options[:order]
return unless order
xml.tag!('order') do
# Merchant-assigned invoice number for the subscription (optional)
xml.tag!('invoiceNumber', order[:invoice_number])
# Description of the subscription (optional)
xml.tag!('description', order[:description])
end
end
# Adds information about the customer
def add_arb_customer(xml, options)
customer = options[:customer]
return unless customer
xml.tag!('customer') do
xml.tag!('type', customer[:type]) if customer[:type]
xml.tag!('id', customer[:id]) if customer[:id]
xml.tag!('email', customer[:email]) if customer[:email]
xml.tag!('phoneNumber', customer[:phone_number]) if customer[:phone_number]
xml.tag!('faxNumber', customer[:fax_number]) if customer[:fax_number]
add_arb_drivers_license(xml, options)
xml.tag!('taxId', customer[:tax_id]) if customer[:tax_id]
end
end
# Adds the customer's driver's license information (conditional)
def add_arb_drivers_license(xml, options)
return unless customer = options[:customer]
return unless drivers_license = customer[:drivers_license]
xml.tag!('driversLicense') do
# The customer's driver's license number
xml.tag!('number', drivers_license[:number])
# The customer's driver's license state
xml.tag!('state', drivers_license[:state])
# The customer's driver's license date of birth
xml.tag!('dateOfBirth', drivers_license[:date_of_birth])
end
end
# Adds address information
def add_arb_address(xml, container_name, address)
return if address.blank?
xml.tag!(container_name) do
xml.tag!('firstName', address[:first_name])
xml.tag!('lastName', address[:last_name])
xml.tag!('company', address[:company])
xml.tag!('address', address[:address1])
xml.tag!('city', address[:city])
xml.tag!('state', address[:state])
xml.tag!('zip', address[:zip])
xml.tag!('country', address[:country])
end
end
def arb_expdate(credit_card)
sprintf('%04d-%02d', credit_card.year, credit_card.month)
end
def recurring_commit(action, request)
url = test? ? arb_test_url : arb_live_url
xml = ssl_post(url, request, "Content-Type" => "text/xml")
response = recurring_parse(action, xml)
message = response[:message] || response[:text]
test_mode = test? || message =~ /Test Mode/
success = response[:result_code] == 'Ok'
Response.new(success, message, response,
:test => test_mode,
:authorization => response[:subscription_id]
)
end
def recurring_parse(action, xml)
response = {}
xml = REXML::Document.new(xml)
root = REXML::XPath.first(xml, "//#{RECURRING_ACTIONS[action]}Response") ||
REXML::XPath.first(xml, "//ErrorResponse")
if root
root.elements.to_a.each do |node|
recurring_parse_element(response, node)
end
end
response
end
def recurring_parse_element(response, node)
if node.has_elements?
node.elements.each{|e| recurring_parse_element(response, e) }
else
response[node.name.underscore.to_sym] = node.text
end
end
end
end
end

View File

@@ -0,0 +1,956 @@
# -*- coding: utf-8 -*-
module ActiveMerchant #:nodoc:
module Billing #:nodoc:
# ==== Customer Information Manager (CIM)
#
# The Authorize.Net Customer Information Manager (CIM) is an optional additional service that allows you to store sensitive payment information on
# Authorize.Net's servers, simplifying payments for returning customers and recurring transactions. It can also help with Payment Card Industry (PCI)
# Data Security Standard compliance, since customer data is no longer stored locally.
#
# To use the AuthorizeNetCimGateway CIM must be enabled for your account.
#
# Information about CIM is available on the {Authorize.Net website}[http://www.authorize.net/solutions/merchantsolutions/merchantservices/cim/].
# Information about the CIM API is available at the {Authorize.Net Integration Center}[http://developer.authorize.net/]
#
# ==== Login and Password
#
# The login and password are not the username and password you use to
# login to the Authorize.Net Merchant Interface. Instead, you will
# use the API Login ID as the login and Transaction Key as the
# password.
#
# ==== How to Get Your API Login ID and Transaction Key
#
# 1. Log into the Merchant Interface
# 2. Select Settings from the Main Menu
# 3. Click on API Login ID and Transaction Key in the Security section
# 4. Type in the answer to the secret question configured on setup
# 5. Click Submit
class AuthorizeNetCimGateway < Gateway
self.test_url = 'https://apitest.authorize.net/xml/v1/request.api'
self.live_url = 'https://api.authorize.net/xml/v1/request.api'
AUTHORIZE_NET_CIM_NAMESPACE = 'AnetApi/xml/v1/schema/AnetApiSchema.xsd'
CIM_ACTIONS = {
:create_customer_profile => 'createCustomerProfile',
:create_customer_payment_profile => 'createCustomerPaymentProfile',
:create_customer_shipping_address => 'createCustomerShippingAddress',
:get_customer_profile => 'getCustomerProfile',
:get_customer_profile_ids => 'getCustomerProfileIds',
:get_customer_payment_profile => 'getCustomerPaymentProfile',
:get_customer_shipping_address => 'getCustomerShippingAddress',
:delete_customer_profile => 'deleteCustomerProfile',
:delete_customer_payment_profile => 'deleteCustomerPaymentProfile',
:delete_customer_shipping_address => 'deleteCustomerShippingAddress',
:update_customer_profile => 'updateCustomerProfile',
:update_customer_payment_profile => 'updateCustomerPaymentProfile',
:update_customer_shipping_address => 'updateCustomerShippingAddress',
:create_customer_profile_transaction => 'createCustomerProfileTransaction',
:validate_customer_payment_profile => 'validateCustomerPaymentProfile'
}
CIM_TRANSACTION_TYPES = {
:auth_capture => 'profileTransAuthCapture',
:auth_only => 'profileTransAuthOnly',
:capture_only => 'profileTransCaptureOnly',
:prior_auth_capture => 'profileTransPriorAuthCapture',
:refund => 'profileTransRefund',
:void => 'profileTransVoid'
}
CIM_VALIDATION_MODES = {
:none => 'none',
:test => 'testMode',
:live => 'liveMode',
:old => 'oldLiveMode'
}
BANK_ACCOUNT_TYPES = {
:checking => 'checking',
:savings => 'savings',
:business_checking => 'businessChecking'
}
ECHECK_TYPES = {
:ccd => 'CCD',
:ppd => 'PPD',
:web => 'WEB'
}
self.homepage_url = 'http://www.authorize.net/'
self.display_name = 'Authorize.Net CIM'
self.supported_countries = ['US']
self.supported_cardtypes = [:visa, :master, :american_express, :discover]
# Creates a new AuthorizeNetCimGateway
#
# The gateway requires that a valid API Login ID and Transaction Key be passed
# in the +options+ hash.
#
# ==== Options
#
# * <tt>:login</tt> -- The Authorize.Net API Login ID (REQUIRED)
# * <tt>:password</tt> -- The Authorize.Net Transaction Key. (REQUIRED)
# * <tt>:test</tt> -- +true+ or +false+. If true, perform transactions against the test server.
# Otherwise, perform transactions against the production server.
# * <tt>:delimiter</tt> -- The delimiter used in the direct response. Default is ',' (comma).
def initialize(options = {})
requires!(options, :login, :password)
super
end
# Creates a new customer profile along with any customer payment profiles and customer shipping addresses
# for the customer profile.
#
# Returns a Response with the Customer Profile ID of the new customer profile in the authorization field.
# It is *CRITICAL* that you save this ID. There is no way to retrieve this through the API. You will not
# be able to create another Customer Profile with the same information.
#
#
#
# ==== Options
#
# * <tt>:profile</tt> -- A hash containing at least one of the CONDITIONAL profile options below (REQUIRED)
#
# ==== Profile
#
# * <tt>:email</tt> -- Email address associated with the customer profile (CONDITIONAL)
# * <tt>:description</tt> -- Description of the customer or customer profile (CONDITIONAL)
# * <tt>:merchant_customer_id</tt> -- Merchant assigned ID for the customer (CONDITIONAL)
# * <tt>:payment_profile</tt> -- A hash containing the elements of the new payment profile (optional)
#
# ==== Payment Profile
#
# * <tt>:payment</tt> -- A hash containing information on payment. Either :credit_card or :bank_account (optional)
def create_customer_profile(options)
requires!(options, :profile)
requires!(options[:profile], :email) unless options[:profile][:merchant_customer_id] || options[:profile][:description]
requires!(options[:profile], :description) unless options[:profile][:email] || options[:profile][:merchant_customer_id]
requires!(options[:profile], :merchant_customer_id) unless options[:profile][:description] || options[:profile][:email]
request = build_request(:create_customer_profile, options)
commit(:create_customer_profile, request)
end
# Creates a new customer payment profile for an existing customer profile.
#
# ==== Options
#
# * <tt>:customer_profile_id</tt> -- The Customer Profile ID of the customer the payment profile will be added to. (REQUIRED)
# * <tt>:payment_profile</tt> -- A hash containing the elements of the new payment profile (REQUIRED)
#
# ==== Payment Profile
#
# * <tt>:payment</tt> -- A hash containing information on payment. Either :credit_card or :bank_account (REQUIRED)
def create_customer_payment_profile(options)
requires!(options, :customer_profile_id)
requires!(options, :payment_profile)
requires!(options[:payment_profile], :payment)
request = build_request(:create_customer_payment_profile, options)
commit(:create_customer_payment_profile, request)
end
# Creates a new customer shipping address for an existing customer profile.
#
# ==== Options
#
# * <tt>:customer_profile_id</tt> -- The Customer Profile ID of the customer the payment profile will be added to. (REQUIRED)
# * <tt>:address</tt> -- A hash containing the elements of the shipping address (REQUIRED)
def create_customer_shipping_address(options)
requires!(options, :customer_profile_id)
requires!(options, :address)
request = build_request(:create_customer_shipping_address, options)
commit(:create_customer_shipping_address, request)
end
# Deletes an existing customer profile along with all associated customer payment profiles and customer shipping addresses.
#
# ==== Options
#
# * <tt>:customer_profile_id</tt> -- The Customer Profile ID of the customer to be deleted. (REQUIRED)
def delete_customer_profile(options)
requires!(options, :customer_profile_id)
request = build_request(:delete_customer_profile, options)
commit(:delete_customer_profile, request)
end
# Deletes a customer payment profile from an existing customer profile.
#
# ==== Options
#
# * <tt>:customer_profile_id</tt> -- The Customer Profile ID of the customer with the payment profile to be deleted. (REQUIRED)
# * <tt>:customer_payment_profile_id</tt> -- The Payment Profile ID of the payment profile to be deleted. (REQUIRED)
def delete_customer_payment_profile(options)
requires!(options, :customer_profile_id)
requires!(options, :customer_payment_profile_id)
request = build_request(:delete_customer_payment_profile, options)
commit(:delete_customer_payment_profile, request)
end
# Deletes a customer shipping address from an existing customer profile.
#
# ==== Options
#
# * <tt>:customer_profile_id</tt> -- The Customer Profile ID of the customer with the payment profile to be deleted. (REQUIRED)
# * <tt>:customer_address_id</tt> -- The Shipping Address ID of the shipping address to be deleted. (REQUIRED)
def delete_customer_shipping_address(options)
requires!(options, :customer_profile_id)
requires!(options, :customer_address_id)
request = build_request(:delete_customer_shipping_address, options)
commit(:delete_customer_shipping_address, request)
end
# Retrieves an existing customer profile along with all the associated customer payment profiles and customer shipping addresses.
#
# Returns a Response whose params hash contains all the profile information.
#
# ==== Options
#
# * <tt>:customer_profile_id</tt> -- The Customer Profile ID of the customer to retrieve. (REQUIRED)
def get_customer_profile(options)
requires!(options, :customer_profile_id)
request = build_request(:get_customer_profile, options)
commit(:get_customer_profile, request)
end
def get_customer_profile_ids(options = {})
request = build_request(:get_customer_profile_ids, options)
commit(:get_customer_profile_ids, request)
end
# Retrieve a customer payment profile for an existing customer profile.
#
# Returns a Response whose params hash contains all the payment profile information. Sensitive information such as credit card
# numbers will be masked.
#
# ==== Options
#
# * <tt>:customer_profile_id</tt> -- The Customer Profile ID of the customer with the payment profile to be retrieved. (REQUIRED)
# * <tt>:customer_payment_profile_id</tt> -- The Payment Profile ID of the payment profile to be retrieved. (REQUIRED)
def get_customer_payment_profile(options)
requires!(options, :customer_profile_id)
requires!(options, :customer_payment_profile_id)
request = build_request(:get_customer_payment_profile, options)
commit(:get_customer_payment_profile, request)
end
# Retrieve a customer shipping address for an existing customer profile.
#
# Returns a Response whose params hash contains all the shipping address information.
#
# ==== Options
#
# * <tt>:customer_profile_id</tt> -- The Customer Profile ID of the customer with the payment profile to be retrieved. (REQUIRED)
# * <tt>:customer_address_id</tt> -- The Shipping Address ID of the shipping address to be retrieved. (REQUIRED)
def get_customer_shipping_address(options)
requires!(options, :customer_profile_id)
requires!(options, :customer_address_id)
request = build_request(:get_customer_shipping_address, options)
commit(:get_customer_shipping_address, request)
end
# Updates an existing customer profile.
#
# Warning: if you do not provide a parameter in the <tt>:payment_profile</tt> hash, it is automatically set to nil at
# Authorize.Net. You will most likely want to first get the profile hash using get_customer_profile and then only change the
# elements you wish to change.
#
# ==== Options
#
# * <tt>:profile</tt> -- A hash containing the values the Customer Profile should be updated to. (REQUIRED)
#
# ==== Profile
#
# * <tt>:customer_profile_id</tt> -- The Customer Profile ID of the customer profile to update. (REQUIRED)
def update_customer_profile(options)
requires!(options, :profile)
requires!(options[:profile], :customer_profile_id)
request = build_request(:update_customer_profile, options)
commit(:update_customer_profile, request)
end
# Updates a customer payment profile for an existing customer profile.
#
# Warning: if you do not provide a parameter in the <tt>:payment_profile</tt> hash, it is automatically set to nil at
# Authorize.Net. You will most likely want to first get the profile hash using get_customer_payment_profile and then only
# change the elements you wish to change.
#
# ==== Options
#
# * <tt>:customer_profile_id</tt> -- The Customer Profile ID of the customer with the payment profile to be updated. (REQUIRED)
# * <tt>:payment_profile</tt> -- A hash containing the values the Customer Payment Profile should be updated to. (REQUIRED)
#
# ==== Payment Profile
#
# * <tt>:customer_payment_profile_id</tt> -- The Customer Payment Profile ID of the Customer Payment Profile to update. (REQUIRED)
def update_customer_payment_profile(options)
requires!(options, :customer_profile_id, :payment_profile)
requires!(options[:payment_profile], :customer_payment_profile_id)
request = build_request(:update_customer_payment_profile, options)
commit(:update_customer_payment_profile, request)
end
# Updates a customer shipping address for an existing customer profile.
#
# Warning: if you do not provide a parameter in the <tt>:address</tt> hash, it is automatically set to nil at
# Authorize.Net. You will most likely want to first get the profile hash using get_customer_shipping_address and then only
# change the elements you wish to change.
#
# ==== Options
#
# * <tt>:customer_profile_id</tt> -- The Customer Profile ID of the customer with the payment profile to be updated. (REQUIRED)
# * <tt>:address</tt> -- A hash containing the values the Customer Shipping Address should be updated to. (REQUIRED)
#
# ==== Address
#
# * <tt>:customer_address_id</tt> -- The Customer Address ID of the Customer Payment Profile to update. (REQUIRED)
def update_customer_shipping_address(options)
requires!(options, :customer_profile_id, :address)
requires!(options[:address], :customer_address_id)
request = build_request(:update_customer_shipping_address, options)
commit(:update_customer_shipping_address, request)
end
# Creates a new payment transaction from an existing customer profile
#
# This is what is used to charge a customer whose information you have stored in a Customer Profile.
#
# Returns a Response object that contains the result of the transaction in <tt>params['direct_response']</tt>
#
# ==== Options
#
# * <tt>:transaction</tt> -- A hash containing information on the transaction that is being requested. (REQUIRED)
#
# ==== Transaction
#
# * <tt>:type</tt> -- The type of transaction. Can be either <tt>:auth_only</tt>, <tt>:capture_only</tt>, <tt>:auth_capture</tt>, <tt>:prior_auth_capture</tt>, <tt>:refund</tt> or <tt>:void</tt>. (REQUIRED)
# * <tt>:amount</tt> -- The amount for the tranaction. Formatted with a decimal. For example "4.95" (CONDITIONAL)
# - :type == :void (NOT USED)
# - :type == :refund (OPTIONAL)
# - :type == (:auth_only, :capture_only, :auth_capture, :prior_auth_capture) (REQUIRED)
#
# * <tt>:customer_profile_id</tt> -- The Customer Profile ID of the customer to use in this transaction. (CONDITIONAL)
# - :type == (:void, :prior_auth_capture) (OPTIONAL)
# - :type == :refund (CONDITIONAL - required if masked information is not being submitted [see below])
# - :type == (:auth_only, :capture_only, :auth_capture) (REQUIRED)
#
# * <tt>:customer_payment_profile_id</tt> -- The Customer Payment Profile ID of the Customer Payment Profile to use in this transaction. (CONDITIONAL)
# - :type == (:void, :prior_auth_capture) (OPTIONAL)
# - :type == :refund (CONDITIONAL - required if masked information is not being submitted [see below])
# - :type == (:auth_only, :capture_only, :auth_capture) (REQUIRED)
#
# * <tt>:trans_id</tt> -- The payment gateway assigned transaction ID of the original transaction (CONDITIONAL):
# - :type = (:void, :refund, :prior_auth_capture) (REQUIRED)
# - :type = (:auth_only, :capture_only, :auth_capture) (NOT USED)
#
# * <tt>:card_code</tt> -- CVV/CCV code (OPTIONAL)
# - :type = (:void, :refund, :prior_auth_capture) (NOT USED)
# - :type = (:auth_only, :capture_only, :auth_capture) (OPTIONAL)
#
# * <tt>:customer_shipping_address_id</tt> -- Payment gateway assigned ID associated with the customer shipping address (CONDITIONAL)
# - :type = (:void, :refund) (OPTIONAL)
# - :type = (:auth_only, :capture_only, :auth_capture) (NOT USED)
# - :type = (:prior_auth_capture) (OPTIONAL)
#
# ==== For :type == :refund only
# * <tt>:credit_card_number_masked</tt> -- (CONDITIONAL - requied for credit card refunds is :customer_profile_id AND :customer_payment_profile_id are missing)
# * <tt>:bank_routing_number_masked && :bank_account_number_masked</tt> -- (CONDITIONAL - requied for electronic check refunds is :customer_profile_id AND :customer_payment_profile_id are missing) (NOT ABLE TO TEST - I keep getting "ACH transactions are not accepted by this merchant." when trying to make a payment and, until that's possible I can't refund (wiseleyb@gmail.com))
def create_customer_profile_transaction(options)
requires!(options, :transaction)
requires!(options[:transaction], :type)
case options[:transaction][:type]
when :void
requires!(options[:transaction], :trans_id)
when :refund
requires!(options[:transaction], :trans_id) &&
(
(options[:transaction][:customer_profile_id] && options[:transaction][:customer_payment_profile_id]) ||
options[:transaction][:credit_card_number_masked] ||
(options[:transaction][:bank_routing_number_masked] && options[:transaction][:bank_account_number_masked])
)
when :prior_auth_capture
requires!(options[:transaction], :amount, :trans_id)
else
requires!(options[:transaction], :amount, :customer_profile_id, :customer_payment_profile_id)
end
request = build_request(:create_customer_profile_transaction, options)
commit(:create_customer_profile_transaction, request)
end
# Creates a new payment transaction for refund from an existing customer profile
#
# This is what is used to refund a transaction you have stored in a Customer Profile.
#
# Returns a Response object that contains the result of the transaction in <tt>params['direct_response']</tt>
#
# ==== Options
#
# * <tt>:transaction</tt> -- A hash containing information on the transaction that is being requested. (REQUIRED)
#
# ==== Transaction
#
# * <tt>:amount</tt> -- The total amount to be refunded (REQUIRED)
#
# * <tt>:customer_profile_id</tt> -- The Customer Profile ID of the customer to use in this transaction. (CONDITIONAL :customer_payment_profile_id must be included if used)
# * <tt>:customer_payment_profile_id</tt> -- The Customer Payment Profile ID of the Customer Payment Profile to use in this transaction. (CONDITIONAL :customer_profile_id must be included if used)
#
# * <tt>:credit_card_number_masked</tt> -- Four Xs follwed by the last four digits of the credit card (CONDITIONAL - used if customer_profile_id and customer_payment_profile_id aren't given)
#
# * <tt>:bank_routing_number_masked</tt> -- The last four gidits of the routing number to be refunded (CONDITIONAL - must be used with :bank_account_number_masked)
# * <tt>:bank_account_number_masked</tt> -- The last four digis of the bank account number to be refunded, Ex. XXXX1234 (CONDITIONAL - must be used with :bank_routing_number_masked)
#
# * <tt>:tax</tt> - A hash containing tax information for the refund (OPTIONAL - <tt>:amount</tt>, <tt>:name</tt> (31 characters), <tt>:description</tt> (255 characters))
# * <tt>:duty</tt> - A hash containting duty information for the refund (OPTIONAL - <tt>:amount</tt>, <tt>:name</tt> (31 characters), <tt>:description</tt> (255 characters))
# * <tt>:shipping</tt> - A hash containing shipping information for the refund (OPTIONAL - <tt>:amount</tt>, <tt>:name</tt> (31 characters), <tt>:description</tt> (255 characters))
def create_customer_profile_transaction_for_refund(options)
requires!(options, :transaction)
options[:transaction][:type] = :refund
requires!(options[:transaction], :trans_id)
requires!(options[:transaction], :amount)
request = build_request(:create_customer_profile_transaction, options)
commit(:create_customer_profile_transaction, request)
end
# Creates a new payment transaction for void from an existing customer profile
#
# This is what is used to void a transaction you have stored in a Customer Profile.
#
# Returns a Response object that contains the result of the transaction in <tt>params['direct_response']</tt>
#
# ==== Options
#
# * <tt>:transaction</tt> -- A hash containing information on the transaction that is being requested. (REQUIRED)
#
# ==== Transaction
#
# * <tt>:trans_id</tt> -- The payment gateway assigned transaction id of the original transaction. (REQUIRED)
# * <tt>:customer_profile_id</tt> -- The Customer Profile ID of the customer to use in this transaction.
# * <tt>:customer_payment_profile_id</tt> -- The Customer Payment Profile ID of the Customer Payment Profile to use in this transaction.
# * <tt>:customer_shipping_address_id</tt> -- Payment gateway assigned ID associated with the customer shipping address.
def create_customer_profile_transaction_for_void(options)
requires!(options, :transaction)
options[:transaction][:type] = :void
requires!(options[:transaction], :trans_id)
request = build_request(:create_customer_profile_transaction, options)
commit(:create_customer_profile_transaction, request)
end
# Verifies an existing customer payment profile by generating a test transaction
#
# Returns a Response object that contains the result of the transaction in <tt>params['direct_response']</tt>
#
# ==== Options
#
# * <tt>:customer_profile_id</tt> -- The Customer Profile ID of the customer to use in this transaction. (REQUIRED)
# * <tt>:customer_payment_profile_id</tt> -- The Customer Payment Profile ID of the Customer Payment Profile to be verified. (REQUIRED)
# * <tt>:customer_address_id</tt> -- The Customer Address ID of the Customer Shipping Address to be verified. (OPTIONAL)
# * <tt>:card_code</tt> -- If the payment profile is a credit card, the CCV/CVV code to validate with (OPTIONAL)
# * <tt>:validation_mode</tt> -- <tt>:live</tt> or <tt>:test</tt> In Test Mode, only field validation is performed. (REQUIRED
# In Live Mode, a transaction is generated and submitted to the processor with the amount of $0.01. If successful, the transaction is immediately voided. (REQUIRED)
def validate_customer_payment_profile(options)
requires!(options, :customer_profile_id, :customer_payment_profile_id, :validation_mode)
request = build_request(:validate_customer_payment_profile, options)
commit(:validate_customer_payment_profile, request)
end
private
def expdate(credit_card)
if credit_card.year.present? && credit_card.month.present?
sprintf('%04d-%02d', credit_card.year, credit_card.month)
else
'XXXX'
end
end
def build_request(action, options = {})
unless CIM_ACTIONS.include?(action)
raise StandardError, "Invalid Customer Information Manager Action: #{action}"
end
xml = Builder::XmlMarkup.new(:indent => 2)
xml.instruct!(:xml, :version => '1.0', :encoding => 'utf-8')
xml.tag!("#{CIM_ACTIONS[action]}Request", :xmlns => AUTHORIZE_NET_CIM_NAMESPACE) do
add_merchant_authentication(xml)
# Merchant-assigned reference ID for the request
xml.tag!('refId', options[:ref_id]) if options[:ref_id]
# Order options
add_order(xml, options[:order]) if options[:order]
send("build_#{action}_request", xml, options)
end
end
# Contains the merchants payment gateway account authentication information
def add_merchant_authentication(xml)
xml.tag!('merchantAuthentication') do
xml.tag!('name', @options[:login])
xml.tag!('transactionKey', @options[:password])
end
end
def build_create_customer_profile_request(xml, options)
add_profile(xml, options[:profile])
xml.tag!('validationMode', CIM_VALIDATION_MODES[options[:validation_mode]]) if options[:validation_mode]
if options.has_key?(:payment_profile)
xml.tag!('paymentProfile') do
add_payment_profile(xml, options[:payment_profile])
end
end
xml.target!
end
def build_create_customer_payment_profile_request(xml, options)
xml.tag!('customerProfileId', options[:customer_profile_id])
xml.tag!('paymentProfile') do
add_payment_profile(xml, options[:payment_profile])
end
xml.tag!('validationMode', CIM_VALIDATION_MODES[options[:validation_mode]]) if options[:validation_mode]
xml.target!
end
def build_create_customer_shipping_address_request(xml, options)
xml.tag!('customerProfileId', options[:customer_profile_id])
xml.tag!('address') do
add_address(xml, options[:address])
end
xml.target!
end
def build_delete_customer_profile_request(xml, options)
xml.tag!('customerProfileId', options[:customer_profile_id])
xml.target!
end
def build_delete_customer_payment_profile_request(xml, options)
xml.tag!('customerProfileId', options[:customer_profile_id])
xml.tag!('customerPaymentProfileId', options[:customer_payment_profile_id])
xml.target!
end
def build_delete_customer_shipping_address_request(xml, options)
xml.tag!('customerProfileId', options[:customer_profile_id])
xml.tag!('customerAddressId', options[:customer_address_id])
xml.target!
end
def build_get_customer_profile_request(xml, options)
xml.tag!('customerProfileId', options[:customer_profile_id])
xml.target!
end
def build_get_customer_profile_ids_request(xml, options)
xml.target!
end
def build_get_customer_payment_profile_request(xml, options)
xml.tag!('customerProfileId', options[:customer_profile_id])
xml.tag!('customerPaymentProfileId', options[:customer_payment_profile_id])
xml.target!
end
def build_get_customer_shipping_address_request(xml, options)
xml.tag!('customerProfileId', options[:customer_profile_id])
xml.tag!('customerAddressId', options[:customer_address_id])
xml.target!
end
def build_update_customer_profile_request(xml, options)
add_profile(xml, options[:profile], true)
xml.target!
end
def build_update_customer_payment_profile_request(xml, options)
xml.tag!('customerProfileId', options[:customer_profile_id])
xml.tag!('paymentProfile') do
add_payment_profile(xml, options[:payment_profile])
end
xml.tag!('validationMode', CIM_VALIDATION_MODES[options[:validation_mode]]) if options[:validation_mode]
xml.target!
end
def build_update_customer_shipping_address_request(xml, options)
xml.tag!('customerProfileId', options[:customer_profile_id])
xml.tag!('address') do
add_address(xml, options[:address])
end
xml.target!
end
def build_create_customer_profile_transaction_request(xml, options)
options[:extra_options] ||= {}
options[:extra_options].merge!('x_test_request' => 'TRUE') if @options[:test]
add_transaction(xml, options[:transaction])
tag_unless_blank(xml, 'extraOptions', format_extra_options(options[:extra_options]))
xml.target!
end
def build_validate_customer_payment_profile_request(xml, options)
xml.tag!('customerProfileId', options[:customer_profile_id])
xml.tag!('customerPaymentProfileId', options[:customer_payment_profile_id])
xml.tag!('customerShippingAddressId', options[:customer_address_id]) if options[:customer_address_id]
tag_unless_blank(xml, 'cardCode', options[:card_code])
xml.tag!('validationMode', CIM_VALIDATION_MODES[options[:validation_mode]]) if options[:validation_mode]
xml.target!
end
# :merchant_customer_id (Optional)
# :description (Optional)
# :email (Optional)
# :payment_profiles (Optional)
def add_profile(xml, profile, update = false)
xml.tag!('profile') do
# Merchant assigned ID for the customer. Up to 20 characters. (optional)
xml.tag!('merchantCustomerId', profile[:merchant_customer_id]) if profile[:merchant_customer_id]
# Description of the customer. Up to 255 Characters (optional)
xml.tag!('description', profile[:description]) if profile[:description]
# Email Address for the customer. Up to 255 Characters (optional)
xml.tag!('email', profile[:email]) if profile[:email]
if update
xml.tag!('customerProfileId', profile[:customer_profile_id])
else
add_payment_profiles(xml, profile[:payment_profiles]) if profile[:payment_profiles]
add_ship_to_list(xml, profile[:ship_to_list]) if profile[:ship_to_list]
end
end
end
def add_transaction(xml, transaction)
unless CIM_TRANSACTION_TYPES.include?(transaction[:type])
raise StandardError, "Invalid Customer Information Manager Transaction Type: #{transaction[:type]}"
end
xml.tag!('transaction') do
xml.tag!(CIM_TRANSACTION_TYPES[transaction[:type]]) do
# The amount to be billed to the customer
case transaction[:type]
when :void
tag_unless_blank(xml,'customerProfileId', transaction[:customer_profile_id])
tag_unless_blank(xml,'customerPaymentProfileId', transaction[:customer_payment_profile_id])
tag_unless_blank(xml,'customerShippingAddressId', transaction[:customer_shipping_address_id])
xml.tag!('transId', transaction[:trans_id])
when :refund
#TODO - add lineItems field
xml.tag!('amount', transaction[:amount])
tag_unless_blank(xml, 'customerProfileId', transaction[:customer_profile_id])
tag_unless_blank(xml, 'customerPaymentProfileId', transaction[:customer_payment_profile_id])
tag_unless_blank(xml, 'customerShippingAddressId', transaction[:customer_shipping_address_id])
tag_unless_blank(xml, 'creditCardNumberMasked', transaction[:credit_card_number_masked])
tag_unless_blank(xml, 'bankRoutingNumberMasked', transaction[:bank_routing_number_masked])
tag_unless_blank(xml, 'bankAccountNumberMasked', transaction[:bank_account_number_masked])
xml.tag!('transId', transaction[:trans_id])
add_tax(xml, transaction[:tax]) if transaction[:tax]
add_duty(xml, transaction[:duty]) if transaction[:duty]
add_shipping(xml, transaction[:shipping]) if transaction[:shipping]
when :prior_auth_capture
xml.tag!('amount', transaction[:amount])
xml.tag!('transId', transaction[:trans_id])
else
xml.tag!('amount', transaction[:amount])
xml.tag!('customerProfileId', transaction[:customer_profile_id])
xml.tag!('customerPaymentProfileId', transaction[:customer_payment_profile_id])
xml.tag!('approvalCode', transaction[:approval_code]) if transaction[:type] == :capture_only
end
add_order(xml, transaction[:order]) if transaction[:order].present?
unless [:void,:refund,:prior_auth_capture].include?(transaction[:type])
tag_unless_blank(xml, 'cardCode', transaction[:card_code])
end
end
end
end
def add_tax(xml, tax)
xml.tag!('tax') do
xml.tag!('amount', tax[:amount]) if tax[:amount]
xml.tag!('name', tax[:name]) if tax[:name]
xml.tag!('description', tax[:description]) if tax[:description]
end
end
def add_duty(xml, duty)
xml.tag!('duty') do
xml.tag!('amount', duty[:amount]) if duty[:amount]
xml.tag!('name', duty[:name]) if duty[:name]
xml.tag!('description', duty[:description]) if duty[:description]
end
end
def add_shipping(xml, shipping)
xml.tag!('shipping') do
xml.tag!('amount', shipping[:amount]) if shipping[:amount]
xml.tag!('name', shipping[:name]) if shipping[:name]
xml.tag!('description', shipping[:description]) if shipping[:description]
end
end
def add_order(xml, order)
xml.tag!('order') do
xml.tag!('invoiceNumber', order[:invoice_number]) if order[:invoice_number]
xml.tag!('description', order[:description]) if order[:description]
xml.tag!('purchaseOrderNumber', order[:purchase_order_number]) if order[:purchase_order_number]
end
end
def add_payment_profiles(xml, payment_profiles)
xml.tag!('paymentProfiles') do
add_payment_profile(xml, payment_profiles)
end
end
# :customer_type => 'individual or business', # Optional
# :bill_to => @address,
# :payment => @payment
def add_payment_profile(xml, payment_profile)
# 'individual' or 'business' (optional)
xml.tag!('customerType', payment_profile[:customer_type]) if payment_profile[:customer_type]
if payment_profile[:bill_to]
xml.tag!('billTo') do
add_address(xml, payment_profile[:bill_to])
end
end
if payment_profile[:payment]
xml.tag!('payment') do
add_credit_card(xml, payment_profile[:payment][:credit_card]) if payment_profile[:payment].has_key?(:credit_card)
add_bank_account(xml, payment_profile[:payment][:bank_account]) if payment_profile[:payment].has_key?(:bank_account)
add_drivers_license(xml, payment_profile[:payment][:drivers_license]) if payment_profile[:payment].has_key?(:drivers_license)
# This element is only required for Wells Fargo SecureSource eCheck.Net merchants
# The customer's Social Security Number or Tax ID
xml.tag!('taxId', payment_profile[:payment]) if payment_profile[:payment].has_key?(:tax_id)
end
end
xml.tag!('customerPaymentProfileId', payment_profile[:customer_payment_profile_id]) if payment_profile[:customer_payment_profile_id]
end
def add_ship_to_list(xml, ship_to_list)
xml.tag!('shipToList') do
add_address(xml, ship_to_list)
end
end
def add_address(xml, address)
xml.tag!('firstName', address[:first_name])
xml.tag!('lastName', address[:last_name])
xml.tag!('company', address[:company])
xml.tag!('address', address[:address1]) if address[:address1]
xml.tag!('address', address[:address]) if address[:address]
xml.tag!('city', address[:city])
xml.tag!('state', address[:state])
xml.tag!('zip', address[:zip])
xml.tag!('country', address[:country])
xml.tag!('phoneNumber', address[:phone_number]) if address[:phone_number]
xml.tag!('faxNumber', address[:fax_number]) if address[:fax_number]
xml.tag!('customerAddressId', address[:customer_address_id]) if address[:customer_address_id]
end
# Adds customers credit card information
# Note: This element should only be included
# when the payment method is credit card.
def add_credit_card(xml, credit_card)
return unless credit_card
xml.tag!('creditCard') do
# The credit card number used for payment of the subscription
xml.tag!('cardNumber', credit_card.number)
# The expiration date of the credit card used for the subscription
xml.tag!('expirationDate', expdate(credit_card))
# Note that Authorize.net does not save CVV codes as part of the
# payment profile. Any transactions/validations after the payment
# profile is created that wish to use CVV verification must pass
# the CVV code to authorize.net again.
xml.tag!('cardCode', credit_card.verification_value) if credit_card.verification_value?
end
end
# Adds customers bank account information
# Note: This element should only be included
# when the payment method is bank account.
def add_bank_account(xml, bank_account)
raise StandardError, "Invalid Bank Account Type: #{bank_account[:account_type]}" unless BANK_ACCOUNT_TYPES.include?(bank_account[:account_type])
raise StandardError, "Invalid eCheck Type: #{bank_account[:echeck_type]}" unless ECHECK_TYPES.include?(bank_account[:echeck_type])
xml.tag!('bankAccount') do
# The type of bank account
xml.tag!('accountType', BANK_ACCOUNT_TYPES[bank_account[:account_type]])
# The routing number of the customers bank
xml.tag!('routingNumber', bank_account[:routing_number])
# The bank account number
xml.tag!('accountNumber', bank_account[:account_number])
# The full name of the individual associated
# with the bank account number
xml.tag!('nameOnAccount', bank_account[:name_on_account])
# The type of electronic check transaction
xml.tag!('echeckType', ECHECK_TYPES[bank_account[:echeck_type]])
# The full name of the individual associated
# with the bank account number (optional)
xml.tag!('bankName', bank_account[:bank_name]) if bank_account[:bank_name]
end
end
# Adds customers driver's license information
# Note: This element is only required for
# Wells Fargo SecureSource eCheck.Net merchants
def add_drivers_license(xml, drivers_license)
xml.tag!('driversLicense') do
# The state of the customer's driver's license
# A valid two character state code
xml.tag!('state', drivers_license[:state])
# The customers driver's license number
xml.tag!('number', drivers_license[:number])
# The date of birth listed on the customer's driver's license
# YYYY-MM-DD
xml.tag!('dateOfBirth', drivers_license[:date_of_birth])
end
end
def commit(action, request)
url = test? ? test_url : live_url
xml = ssl_post(url, request, "Content-Type" => "text/xml")
response_params = parse(action, xml)
message = response_params['messages']['message']['text']
test_mode = test? || message =~ /Test Mode/
success = response_params['messages']['result_code'] == 'Ok'
response_params['direct_response'] = parse_direct_response(response_params['direct_response']) if response_params['direct_response']
transaction_id = response_params['direct_response']['transaction_id'] if response_params['direct_response']
Response.new(success, message, response_params,
:test => test_mode,
:authorization => transaction_id || response_params['customer_profile_id'] || (response_params['profile'] ? response_params['profile']['customer_profile_id'] : nil)
)
end
def tag_unless_blank(xml, tag_name, data)
xml.tag!(tag_name, data) unless data.blank? || data.nil?
end
def format_extra_options(options)
options.map{ |k, v| "#{k}=#{v}" }.join(',') unless options.nil?
end
def parse_direct_response(params)
delimiter = @options[:delimiter] || ','
direct_response = {'raw' => params}
direct_response_fields = params.split(delimiter)
direct_response.merge(
{
'response_code' => direct_response_fields[0],
'response_subcode' => direct_response_fields[1],
'response_reason_code' => direct_response_fields[2],
'message' => direct_response_fields[3],
'approval_code' => direct_response_fields[4],
'avs_response' => direct_response_fields[5],
'transaction_id' => direct_response_fields[6],
'invoice_number' => direct_response_fields[7],
'order_description' => direct_response_fields[8],
'amount' => direct_response_fields[9],
'method' => direct_response_fields[10],
'transaction_type' => direct_response_fields[11],
'customer_id' => direct_response_fields[12],
'first_name' => direct_response_fields[13],
'last_name' => direct_response_fields[14],
'company' => direct_response_fields[15],
'address' => direct_response_fields[16],
'city' => direct_response_fields[17],
'state' => direct_response_fields[18],
'zip_code' => direct_response_fields[19],
'country' => direct_response_fields[20],
'phone' => direct_response_fields[21],
'fax' => direct_response_fields[22],
'email_address' => direct_response_fields[23],
'ship_to_first_name' => direct_response_fields[24],
'ship_to_last_name' => direct_response_fields[25],
'ship_to_company' => direct_response_fields[26],
'ship_to_address' => direct_response_fields[27],
'ship_to_city' => direct_response_fields[28],
'ship_to_state' => direct_response_fields[29],
'ship_to_zip_code' => direct_response_fields[30],
'ship_to_country' => direct_response_fields[31],
'tax' => direct_response_fields[32],
'duty' => direct_response_fields[33],
'freight' => direct_response_fields[34],
'tax_exempt' => direct_response_fields[35],
'purchase_order_number' => direct_response_fields[36],
'md5_hash' => direct_response_fields[37],
'card_code' => direct_response_fields[38],
'cardholder_authentication_verification_response' => direct_response_fields[39],
# The following direct response fields are only available in version 3.1 of the
# transaction response. Check your merchant account settings for details.
'account_number' => direct_response_fields[50] || '',
'card_type' => direct_response_fields[51] || '',
'split_tender_id' => direct_response_fields[52] || '',
'requested_amount' => direct_response_fields[53] || '',
'balance_on_card' => direct_response_fields[54] || '',
}
)
end
def parse(action, xml)
xml = REXML::Document.new(xml)
root = REXML::XPath.first(xml, "//#{CIM_ACTIONS[action]}Response") ||
REXML::XPath.first(xml, "//ErrorResponse")
if root
response = parse_element(root)
end
response
end
def parse_element(node)
if node.has_elements?
response = {}
node.elements.each{ |e|
key = e.name.underscore
value = parse_element(e)
if response.has_key?(key)
if response[key].is_a?(Array)
response[key].push(value)
else
response[key] = [response[key], value]
end
else
response[key] = parse_element(e)
end
}
else
response = node.text
end
response
end
end
end
end

View File

@@ -0,0 +1,467 @@
require 'json'
module ActiveMerchant #:nodoc:
module Billing #:nodoc:
# For more information on Balanced visit https://www.balancedpayments.com
# or visit #balanced on irc.freenode.net
#
# Instantiate a instance of BalancedGateway by passing through your
# Balanced API key secret.
#
# ==== To obtain an API key of your own
#
# 1. Visit https://www.balancedpayments.com
# 2. Click "Get started"
# 3. The next screen will give you a test API key of your own
# 4. When you're ready to generate a production API key click the "Go
# live" button on the Balanced dashboard and fill in your marketplace
# details.
#
# ==== Overview
#
# Balanced provides a RESTful API, all entities within Balanced are
# represented by their respective URIs, these are returned in the
# `authorization` parameter of the Active Merchant Response object.
#
# All Response objects will contain a hash property called `params` which
# holds the raw JSON dictionary returned by Balanced. You can find
# properties about the operation performed and the object that represents
# it within this hash.
#
# All operations within Balanced are tied to an account, as such, when you
# perform an `authorization` or a `capture` with a new credit card you
# must ensure you also pass the `:email` property within the `options`
# parameter.
#
# For more details about Balanced's API visit:
# https://www.balancedpayments.com/docs
#
# ==== Terminology & Transaction Flow
#
# * An `authorization` operation will return a Hold URI. An `authorization`
# within Balanced is valid until the `expires_at` property. You can see the
# exact date of the expiry on the Response object by inspecting the
# property `response.params['expires_at']`. The resulting Hold may be
# `capture`d or `void`ed at any time before the `expires_at` date for
# any amount up to the full amount of the original `authorization`.
# * A `capture` operation will return a Debit URI. You must pass the URI of
# the previously performed `authorization`
# * A `purchase` will create a Hold and Debit in a single operation and
# return the URI of the resulting Debit.
# * A `void` operation must be performed on an existing `authorization`
# and will result in releasing the funds reserved by the
# `authorization`.
# * The `refund` operation must be performed on a previously captured
# Debit URI. You may refund any fraction of the original amount of the
# debit up to the original total.
#
class BalancedGateway < Gateway
VERSION = '1.0.0'
TEST_URL = LIVE_URL = 'https://api.balancedpayments.com'
# The countries the gateway supports merchants from as 2 digit ISO
# country codes
self.supported_countries = ['US']
self.supported_cardtypes = [:visa, :master, :american_express, :discover]
self.homepage_url = 'https://www.balancedpayments.com/'
self.display_name = 'Balanced'
self.money_format = :cents
class Error < StandardError
attr_reader :response
def initialize(response, msg=nil)
@response = response
super(msg || response['description'])
end
end
class CardDeclined < Error
end
# Creates a new BalancedGateway
#
# The gateway requires that a valid api_key be passed in the +options+
# hash.
#
# ==== Options
#
# * <tt>:login</tt> -- The Balanced API Secret (REQUIRED)
def initialize(options = {})
requires!(options, :login)
super
initialize_marketplace(options[:marketplace] || load_marketplace)
end
# Performs an authorization (Hold in Balanced nonclementure), which
# reserves the funds on the customer's credit card, but does not charge
# the card. An authorization is valid until the `expires_at` field in
# the params Hash passes. See `response.params['expires_at']`. The exact
# amount of time until an authorization expires depends on the card
# issuer.
#
# If you pass a previously tokenized `credit_card` URI the only other
# parameter required is `money`. If you pass `credit_card` as a hash of
# credit card information you must also pass `options` with a `:email`
# entry.
#
# ==== Parameters
#
# * <tt>money</tt> -- The amount to be authorized as an Integer value in cents.
# * <tt>credit_card</tt> -- A hash of credit card details for this
# transaction or the URI of a card previously stored in Balanced.
# * <tt>options</tt> -- A hash of optional parameters.
#
# ==== Options
#
# If you are passing a new credit card you must pass one of these two
# parameters
#
# * <tt>email</tt> -- the email address of user associated with this
# purchase.
# * <tt>account_uri</tt> -- `account_uri` is the URI of an existing
# Balanced account.
def authorize(money, credit_card, options = {})
if credit_card.respond_to?(:number)
requires!(options, :email) unless options[:account_uri]
end
post = {}
post[:amount] = money
post[:description] = options[:description]
create_or_find_account(post, options)
add_credit_card(post, credit_card, options)
add_address(credit_card, options)
create_transaction(:post, @holds_uri, post)
rescue Error => ex
failed_response(ex.response)
end
# Perform a purchase, which is an authorization and capture in a single
# operation.
#
# ==== Parameters
#
# * <tt>money</tt> -- The amount to be purchased as an Integer value in cents.
# * <tt>credit_card</tt> -- A hash of credit card details for this
# transaction or the URI of a card previously stored in Balanced.
# * <tt>options</tt> -- A hash of optional parameters.
#
# ==== Options
#
# If you are passing a new credit card you must pass one of these two
# parameters
#
# * <tt>email</tt> -- the email address of user associated with this
# purchase.
# * <tt>account_uri</tt> -- `account_uri` is the URI of an existing
# Balanced account.
def purchase(money, credit_card, options = {})
if credit_card.respond_to?('number')
requires!(options, :email) unless options[:account_uri]
end
post = {}
post[:amount] = money
post[:description] = options[:description]
create_or_find_account(post, options)
add_credit_card(post, credit_card, options)
add_address(credit_card, options)
create_transaction(:post, @debits_uri, post)
rescue Error => ex
failed_response(ex.response)
end
# Captures the funds from an authorized transaction (Hold).
#
# ==== Parameters
#
# * <tt>money</tt> -- The amount to be captured as an Integer value in
# cents. If omitted the full amount of the original authorization
# transaction will be captured.
# * <tt>authorization</tt> -- The uri of an authorization returned from
# an authorize request.
#
# ==== Options
#
# * <tt>description</tt> -- A string that will be displayed on the
# Balanced dashboard
def capture(money, authorization, options = {})
post = {}
post[:hold_uri] = authorization
post[:amount] = money if money
post[:description] = options[:description] if options[:description]
post[:on_behalf_of_uri] = options[:on_behalf_of_uri] if options[:on_behalf_of_uri]
create_transaction(:post, @debits_uri, post)
rescue Error => ex
failed_response(ex.response)
end
# Void a previous authorization (Hold)
#
# ==== Parameters
#
# * <tt>authorization</tt> -- The uri of the authorization returned from
# an `authorize` request.
def void(authorization)
post = {}
post[:is_void] = true
create_transaction(:put, authorization, post)
rescue Error => ex
failed_response(ex.response)
end
# Refund a transaction.
#
# Returns the money debited from a card to the card from the
# marketplace's escrow balance.
#
# ==== Parameters
#
# * <tt>debit_uri</tt> -- The uri of the original transaction against
# which the refund is being issued.
# * <tt>options</tt> -- A hash of parameters.
#
# ==== Options
#
# * <tt>`:amount`<tt> -- specify an amount if you want to perform a
# partial refund. This value will default to the total amount of the
# debit that has not been refunded so far.
def refund(amount, debit_uri = "deprecated", options = {})
if(debit_uri == "deprecated" || debit_uri.kind_of?(Hash))
deprecated "Calling the refund method without an amount parameter is deprecated and will be removed in a future version."
return refund(options[:amount], amount, options)
end
requires!(debit_uri)
post = {}
post[:debit_uri] = debit_uri
post[:amount] = amount
post[:description] = options[:description]
create_transaction(:post, @refunds_uri, post)
rescue Error => ex
failed_response(ex.response)
end
# Stores a card and email address
#
# ==== Parameters
#
# * <tt>credit_card</tt> --
def store(credit_card, options = {})
requires!(options, :email)
post = {}
account_uri = create_or_find_account(post, options)
if credit_card.respond_to? :number
add_credit_card(post, credit_card, options)
else
associate_card_to_account(account_uri, credit_card)
credit_card
end
rescue Error => ex
failed_response(ex.response)
end
private
# Load URIs for this marketplace by inspecting the marketplace object
# returned from the uri. http://en.wikipedia.org/wiki/HATEOAS
def load_marketplace
response = http_request(:get, '/v1/marketplaces')
if error?(response)
raise Error.new(response, 'Invalid login credentials supplied')
end
response['items'][0]
end
def initialize_marketplace(marketplace)
@marketplace_uri = marketplace['uri']
@holds_uri = marketplace['holds_uri']
@debits_uri = marketplace['debits_uri']
@cards_uri = marketplace['cards_uri']
@accounts_uri = marketplace['accounts_uri']
@refunds_uri = marketplace['refunds_uri']
end
def create_or_find_account(post, options)
account_uri = nil
if options.has_key? :account_uri
account_uri = options[:account_uri]
end
if account_uri == nil
post[:email_address] = options[:email]
# create an account
response = http_request(:post, @accounts_uri, post)
if response.has_key? 'uri'
account_uri = response['uri']
elsif error?(response)
# lookup account from Balanced, account_uri should be in the
# exception in a dictionary called extras
account_uri = response['extras']['account_uri']
end
end
post[:account_uri] = account_uri
account_uri
end
def add_address(credit_card, options)
return unless credit_card.kind_of?(Hash)
if address = options[:billing_address] || options[:address]
credit_card[:street_address] = address[:address1] if address[:address1]
credit_card[:street_address] += ' ' + address[:address2] if address[:address2]
credit_card[:postal_code] = address[:zip] if address[:zip]
credit_card[:country] = address[:country] if address[:country]
end
end
def add_credit_card(post, credit_card, options)
if credit_card.respond_to? :number
card = {}
card[:card_number] = credit_card.number
card[:expiration_month] = credit_card.month
card[:expiration_year] = credit_card.year
card[:security_code] = credit_card.verification_value if credit_card.verification_value?
card[:name] = credit_card.name if credit_card.name
add_address(card, options)
response = http_request(:post, @cards_uri, card)
if error?(response)
raise CardDeclined, response
end
card_uri = response['uri']
associate_card_to_account(post[:account_uri], card_uri)
post[:card_uri] = card_uri
elsif credit_card.kind_of?(String)
post[:card_uri] = credit_card
end
post[:card_uri]
end
def associate_card_to_account(account_uri, card_uri)
http_request(:put, account_uri, :card_uri => card_uri)
end
def http_request(method, url, parameters={}, meta={})
begin
if method == :get
raw_response = ssl_get(LIVE_URL + url, headers(meta))
else
raw_response = ssl_request(method,
LIVE_URL + url,
post_data(parameters),
headers(meta))
end
parse(raw_response)
rescue ResponseError => e
raw_response = e.response.body
response_error(raw_response)
rescue JSON::ParserError
json_error(raw_response)
end
end
def create_transaction(method, url, parameters, meta={})
response = http_request(method, url, parameters, meta)
success = !error?(response)
Response.new(success,
(success ? "Transaction approved" : response["description"]),
response,
:test => (@marketplace_uri.index("TEST") ? true : false),
:authorization => response["uri"]
)
end
def failed_response(response)
is_test = false
if @marketplace_uri
is_test = (@marketplace_uri.index("TEST") ? true : false)
end
Response.new(false,
response["description"],
response,
:test => is_test
)
end
def parse(body)
JSON.parse(body)
end
def response_error(raw_response)
begin
parse(raw_response)
rescue JSON::ParserError
json_error(raw_response)
end
end
def json_error(raw_response)
msg = 'Invalid response received from the Balanced API. Please contact support@balancedpayments.com if you continue to receive this message.'
msg += " (The raw response returned by the API was #{raw_response.inspect})"
{
"error" => {
"message" => msg
}
}
end
def error?(response)
response.key?('status_code')
end
def post_data(params)
return nil unless params
params.map do |key, value|
next if value.blank?
if value.is_a?(Hash)
h = {}
value.each do |k, v|
h["#{key}[#{k}]"] = v unless v.blank?
end
post_data(h)
else
"#{key}=#{CGI.escape(value.to_s)}"
end
end.compact.join("&")
end
def headers(meta={})
@@ua ||= JSON.dump({
:bindings_version => ActiveMerchant::VERSION,
:lang => 'ruby',
:lang_version => "#{RUBY_VERSION} p#{RUBY_PATCHLEVEL} (#{RUBY_RELEASE_DATE})",
:lib_version => BalancedGateway::VERSION,
:platform => RUBY_PLATFORM,
:publisher => 'active_merchant'
})
{
"Authorization" => "Basic " + Base64.encode64(@options[:login].to_s + ":").strip,
"User-Agent" => "Balanced/v1 ActiveMerchantBindings/#{ActiveMerchant::VERSION}",
"X-Balanced-User-Agent" => @@ua,
}
end
end
end
end

View File

@@ -0,0 +1,105 @@
module ActiveMerchant #:nodoc:
module Billing #:nodoc:
class BanwireGateway < Gateway
URL = 'https://banwire.com/api.pago_pro'
self.supported_countries = ['MX']
self.supported_cardtypes = [:visa, :master, :american_express]
self.homepage_url = 'http://www.banwire.com/'
self.display_name = 'Banwire'
def initialize(options = {})
requires!(options, :login)
super
end
def purchase(money, creditcard, options = {})
post = {}
add_response_type(post)
add_customer_data(post, options)
add_order_data(post, options)
add_creditcard(post, creditcard)
add_address(post, creditcard, options)
add_customer_data(post, options)
add_amount(post, money, options)
commit(money, post)
end
private
def add_response_type(post)
post[:response_format] = "JSON"
end
def add_customer_data(post, options)
post[:user] = @options[:login]
post[:phone] = options[:billing_address][:phone]
post[:mail] = options[:email]
end
def add_order_data(post, options)
post[:reference] = options[:order_id]
post[:concept] = options[:description]
end
def add_address(post, creditcard, options)
post[:address] = options[:billing_address][:address1]
post[:post_code] = options[:billing_address][:zip]
end
def add_creditcard(post, creditcard)
post[:card_num] = creditcard.number
post[:card_name] = creditcard.name
post[:card_type] = card_brand(creditcard)
post[:card_exp] = "#{sprintf("%02d", creditcard.month)}/#{"#{creditcard.year}"[-2, 2]}"
post[:card_ccv2] = creditcard.verification_value
end
def add_amount(post, money, options)
post[:ammount] = amount(money)
post[:currency] = options[:currency]
end
def card_brand(card)
brand = super
({"master" => "mastercard", "american_express" => "amex"}[brand] || brand)
end
def parse(body)
JSON.parse(body)
end
def commit(money, parameters)
raw_response = ssl_post(URL, post_data(parameters))
begin
response = parse(raw_response)
rescue JSON::ParserError
response = json_error(raw_response)
end
Response.new(success?(response),
response["message"],
response,
:test => test?,
:authorization => response["code_auth"])
end
def success?(response)
(response["response"] == "ok")
end
def post_data(parameters = {})
parameters.collect { |key, value| "#{key}=#{CGI.escape(value.to_s)}" }.join("&")
end
def json_error(raw_response)
msg = 'Invalid response received from the Banwire API. Please contact Banwire support if you continue to receive this message.'
msg += " (The raw response returned by the API was #{raw_response.inspect})"
{
"message" => msg
}
end
end
end
end

View File

@@ -0,0 +1,314 @@
module ActiveMerchant #:nodoc:
module Billing #:nodoc:
class BarclaysEpdqGateway < Gateway
self.test_url = 'https://secure2.mde.epdq.co.uk:11500'
self.live_url = 'https://secure2.epdq.co.uk:11500'
self.supported_countries = ['GB']
self.default_currency = 'GBP'
self.supported_cardtypes = [:visa, :master, :american_express, :maestro, :switch ]
self.money_format = :cents
self.homepage_url = 'http://www.barclaycard.co.uk/business/accepting-payments/epdq-mpi/'
self.display_name = 'Barclays ePDQ'
def initialize(options = {})
requires!(options, :login, :password, :client_id)
super
end
def authorize(money, creditcard, options = {})
document = Document.new(self, @options) do
add_order_form(options[:order_id]) do
add_consumer(options) do
add_creditcard(creditcard)
end
add_transaction(:PreAuth, money)
end
end
commit(document)
end
def purchase(money, creditcard, options = {})
# disable fraud checks if this is a repeat order:
if options[:payment_number] && (options[:payment_number] > 1)
no_fraud = true
else
no_fraud = options[:no_fraud]
end
document = Document.new(self, @options, :no_fraud => no_fraud) do
add_order_form(options[:order_id], options[:group_id]) do
add_consumer(options) do
add_creditcard(creditcard)
end
add_transaction(:Auth, money, options)
end
end
commit(document)
end
# authorization is your unique order ID, not the authorization
# code returned by ePDQ
def capture(money, authorization, options = {})
document = Document.new(self, @options) do
add_order_form(authorization) do
add_transaction(:PostAuth, money)
end
end
commit(document)
end
# authorization is your unique order ID, not the authorization
# code returned by ePDQ
def credit(money, creditcard_or_authorization, options = {})
if creditcard_or_authorization.is_a?(String)
deprecated CREDIT_DEPRECATION_MESSAGE
refund(money, creditcard_or_authorization, options)
else
credit_new_order(money, creditcard_or_authorization, options)
end
end
def refund(money, authorization, options = {})
credit_existing_order(money, authorization, options)
end
def void(authorization, options = {})
document = Document.new(self, @options) do
add_order_form(authorization) do
add_transaction(:Void)
end
end
commit(document)
end
private
def credit_new_order(money, creditcard, options)
document = Document.new(self, @options) do
add_order_form do
add_consumer(options) do
add_creditcard(creditcard)
end
add_transaction(:Credit, money)
end
end
commit(document)
end
def credit_existing_order(money, authorization, options)
order_id, _ = authorization.split(":")
document = Document.new(self, @options) do
add_order_form(order_id) do
add_transaction(:Credit, money)
end
end
commit(document)
end
def parse(body)
parser = Parser.new(body)
response = parser.parse
Response.new(response[:success], response[:message], response,
:test => test?,
:authorization => response[:authorization],
:avs_result => response[:avsresponse],
:cvv_result => response[:cvv_result],
:order_id => response[:order_id],
:raw_response => response[:raw_response]
)
end
def commit(document)
url = (test? ? self.test_url : self.live_url)
data = ssl_post(url, document.to_xml)
parse(data)
end
class Parser
def initialize(response)
@response = response
end
def parse
require 'iconv' unless String.method_defined?(:encode)
if String.method_defined?(:encode)
doc = REXML::Document.new(@response.encode("UTF-8", "ISO-8859-1"))
else
ic = Iconv.new('UTF-8', 'ISO-8859-1')
doc = REXML::Document.new(ic.iconv(@response))
end
auth_type = find(doc, "//Transaction/Type").to_s
message = find(doc, "//Message/Text")
if message.blank?
message = find(doc, "//Transaction/CardProcResp/CcReturnMsg")
end
case auth_type
when 'Credit', 'Void'
success = find(doc, "//CcReturnMsg") == "Approved."
else
success = find(doc, "//Transaction/AuthCode").present?
end
{
:success => success,
:message => message,
:transaction_id => find(doc, "//Transaction/Id"),
:avs_result => find(doc, "//Transaction/AvsRespCode"),
:cvv_result => find(doc, "//Transaction/Cvv2Resp"),
:authorization => find(doc, "//OrderFormDoc/Id"),
:raw_response => @response
}
end
def find(doc, xpath)
REXML::XPath.first(doc, xpath).try(:text)
end
end
class Document
attr_reader :type, :xml
PAYMENT_INTERVALS = {
:days => 'D',
:months => 'M'
}
EPDQ_CARD_TYPES = {
:visa => 1,
:master => 2,
:switch => 9,
:maestro => 10,
}
def initialize(gateway, options = {}, document_options = {}, &block)
@gateway = gateway
@options = options
@document_options = document_options
@xml = Builder::XmlMarkup.new(:indent => 2)
build(&block)
end
def to_xml
@xml.target!
end
def build(&block)
xml.instruct!(:xml, :version => '1.0')
xml.EngineDocList do
xml.DocVersion "1.0"
xml.EngineDoc do
xml.ContentType "OrderFormDoc"
xml.User do
xml.Name(@options[:login])
xml.Password(@options[:password])
xml.ClientId({ :DataType => "S32" }, @options[:client_id])
end
xml.Instructions do
if @document_options[:no_fraud]
xml.Pipeline "PaymentNoFraud"
else
xml.Pipeline "Payment"
end
end
instance_eval(&block)
end
end
end
def add_order_form(order_id=nil, group_id=nil, &block)
xml.OrderFormDoc do
xml.Mode 'P'
xml.Id(order_id) if order_id
xml.GroupId(group_id) if group_id
instance_eval(&block)
end
end
def add_consumer(options=nil, &block)
xml.Consumer do
if options
xml.Email(options[:email]) if options[:email]
billing_address = options[:billing_address] || options[:address]
if billing_address
xml.BillTo do
xml.Location do
xml.Address do
xml.Street1 billing_address[:address1]
xml.Street2 billing_address[:address2]
xml.City billing_address[:city]
xml.StateProv billing_address[:state]
xml.PostalCode billing_address[:zip]
xml.Country billing_address[:country_code]
end
end
end
end
end
instance_eval(&block)
end
end
def add_creditcard(creditcard)
xml.PaymentMech do
xml.CreditCard do
xml.Type({ :DataType => 'S32' }, EPDQ_CARD_TYPES[creditcard.brand.to_sym])
xml.Number creditcard.number
xml.Expires({ :DataType => 'ExpirationDate', :Locale => 826 }, format_expiry_date(creditcard))
if creditcard.verification_value.present?
xml.Cvv2Indicator 1
xml.Cvv2Val creditcard.verification_value
else
xml.Cvv2Indicator 5
end
xml.IssueNum(creditcard.issue_number) if creditcard.issue_number.present?
end
end
end
def add_transaction(auth_type, amount = nil, options = {})
@auth_type = auth_type
xml.Transaction do
xml.Type @auth_type.to_s
if options[:payment_number] && options[:payment_number] > 1
xml.CardholderPresentCode({ :DataType => 'S32' }, 8)
else
xml.CardholderPresentCode({ :DataType => 'S32' }, 7)
end
if options[:payment_number]
xml.PaymentNumber({ :DataType => 'S32' }, options[:payment_number])
end
if options[:total_payments]
xml.TotalNumberPayments({ :DataType => 'S32' }, options[:total_payments])
end
if amount
xml.CurrentTotals do
xml.Totals do
xml.Total({ :DataType => 'Money', :Currency => 826 }, amount)
end
end
end
end
end
# date must be formatted MM/YY
def format_expiry_date(creditcard)
month_str = "%02d" % creditcard.month
if match = creditcard.year.to_s.match(/^\d{2}(\d{2})$/)
year_str = "%02d" % match[1].to_i
else
year_str = "%02d" % creditcard.year
end
"#{month_str}/#{year_str}"
end
end
end
end
end

View File

@@ -0,0 +1,169 @@
require File.dirname(__FILE__) + '/beanstream/beanstream_core'
module ActiveMerchant #:nodoc:
module Billing #:nodoc:
# This class implements the Canadian {Beanstream}[http://www.beanstream.com] payment gateway.
# It is also named TD Canada Trust Online Mart payment gateway.
# To learn more about the specification of Beanstream gateway, please read the OM_Direct_Interface_API.pdf,
# which you can get from your Beanstream account or get from me by email.
#
# == Supported transaction types by Beanstream:
# * +P+ - Purchase
# * +PA+ - Pre Authorization
# * +PAC+ - Pre Authorization Completion
#
# == Secure Payment Profiles:
# BeanStream supports payment profiles (vaults). This allows you to store cc information with BeanStream and process subsequent transactions with a customer id.
# Secure Payment Profiles must be enabled on your account (must be done over the phone).
# Your API Access Passcode must be set in Administration => account settings => order settings.
# To learn more about storing credit cards with the Beanstream gateway, please read the BEAN_Payment_Profiles.pdf (I had to phone BeanStream to request it.)
#
# == Notes
# * Adding of order products information is not implemented.
# * Ensure that country and province data is provided as a code such as "CA", "US", "QC".
# * login is the Beanstream merchant ID, username and password should be enabled in your Beanstream account and passed in using the <tt>:user</tt> and <tt>:password</tt> options.
# * Test your app with your true merchant id and test credit card information provided in the api pdf document.
# * Beanstream does not allow Payment Profiles to be deleted with their API. The accounts are 'closed', but have to be deleted manually.
#
# Example authorization (Beanstream PA transaction type):
#
# twenty = 2000
# gateway = BeanstreamGateway.new(
# :login => '100200000',
# :user => 'xiaobozz',
# :password => 'password'
# )
#
# credit_card = CreditCard.new(
# :number => '4030000010001234',
# :month => 8,
# :year => 2011,
# :first_name => 'xiaobo',
# :last_name => 'zzz',
# :verification_value => 137
# )
# response = gateway.authorize(twenty, credit_card,
# :order_id => '1234',
# :billing_address => {
# :name => 'xiaobo zzz',
# :phone => '555-555-5555',
# :address1 => '1234 Levesque St.',
# :address2 => 'Apt B',
# :city => 'Montreal',
# :state => 'QC',
# :country => 'CA',
# :zip => 'H2C1X8'
# },
# :email => 'xiaobozzz@example.com',
# :subtotal => 800,
# :shipping => 100,
# :tax1 => 100,
# :tax2 => 100,
# :custom => 'reference one'
# )
class BeanstreamGateway < Gateway
include BeanstreamCore
def authorize(money, source, options = {})
post = {}
add_amount(post, money)
add_invoice(post, options)
add_source(post, source)
add_address(post, options)
add_transaction_type(post, :authorization)
add_customer_ip(post, options)
commit(post)
end
def purchase(money, source, options = {})
post = {}
add_amount(post, money)
add_invoice(post, options)
add_source(post, source)
add_address(post, options)
add_transaction_type(post, purchase_action(source))
add_customer_ip(post, options)
commit(post)
end
def void(authorization, options = {})
reference, amount, type = split_auth(authorization)
post = {}
add_reference(post, reference)
add_original_amount(post, amount)
add_transaction_type(post, void_action(type))
commit(post)
end
def recurring(money, source, options = {})
post = {}
add_amount(post, money)
add_invoice(post, options)
add_credit_card(post, source)
add_address(post, options)
add_transaction_type(post, purchase_action(source))
add_recurring_type(post, options)
commit(post)
end
def update_recurring(amount, source, options = {})
post = {}
add_recurring_amount(post, amount)
add_recurring_invoice(post, options)
add_credit_card(post, source)
add_address(post, options)
add_recurring_operation_type(post, :update)
add_recurring_service(post, options)
recurring_commit(post)
end
def cancel_recurring(options = {})
post = {}
add_recurring_operation_type(post, :cancel)
add_recurring_service(post, options)
recurring_commit(post)
end
def interac
@interac ||= BeanstreamInteracGateway.new(@options)
end
# To match the other stored-value gateways, like TrustCommerce,
# store and unstore need to be defined
def store(credit_card, options = {})
post = {}
add_address(post, options)
add_credit_card(post, credit_card)
add_secure_profile_variables(post,options)
commit(post, true)
end
#can't actually delete a secure profile with the supplicaed API. This function sets the status of the profile to closed (C).
#Closed profiles will have to removed manually.
def delete(vault_id)
update(vault_id, false, {:status => "C"})
end
alias_method :unstore, :delete
# Update the values (such as CC expiration) stored at
# the gateway. The CC number must be supplied in the
# CreditCard object.
def update(vault_id, credit_card, options = {})
post = {}
add_address(post, options)
add_credit_card(post, credit_card)
options.merge!({:vault_id => vault_id, :operation => secure_profile_action(:modify)})
add_secure_profile_variables(post,options)
commit(post, true)
end
private
def build_response(*args)
Response.new(*args)
end
end
end
end

View File

@@ -0,0 +1,393 @@
module ActiveMerchant #:nodoc:
module Billing #:nodoc:
module BeanstreamCore
RECURRING_URL = 'https://www.beanstream.com/scripts/recurring_billing.asp'
SECURE_PROFILE_URL = 'https://www.beanstream.com/scripts/payment_profile.asp'
SP_SERVICE_VERSION = '1.1'
TRANSACTIONS = {
:authorization => 'PA',
:purchase => 'P',
:capture => 'PAC',
:refund => 'R',
:void => 'VP',
:check_purchase => 'D',
:check_refund => 'C',
:void_purchase => 'VP',
:void_refund => 'VR'
}
PROFILE_OPERATIONS = {
:new => 'N',
:modify => 'M'
}
CVD_CODES = {
'1' => 'M',
'2' => 'N',
'3' => 'I',
'4' => 'S',
'5' => 'U',
'6' => 'P'
}
AVS_CODES = {
'0' => 'R',
'5' => 'I',
'9' => 'I'
}
PERIODS = {
:days => 'D',
:weeks => 'W',
:months => 'M',
:years => 'Y'
}
PERIODICITIES = {
:daily => [:days, 1],
:weekly => [:weeks, 1],
:biweekly => [:weeks, 2],
:monthly => [:months, 1],
:bimonthly => [:months, 2],
:yearly => [:years, 1]
}
RECURRING_OPERATION = {
:update => 'M',
:cancel => 'C'
}
def self.included(base)
base.default_currency = 'CAD'
# The countries the gateway supports merchants from as 2 digit ISO country codes
base.supported_countries = ['CA']
# The card types supported by the payment gateway
base.supported_cardtypes = [:visa, :master, :american_express, :discover, :diners_club, :jcb]
# The homepage URL of the gateway
base.homepage_url = 'http://www.beanstream.com/'
base.live_url = 'https://www.beanstream.com/scripts/process_transaction.asp'
# The name of the gateway
base.display_name = 'Beanstream.com'
end
# Only <tt>:login</tt> is required by default,
# which is the merchant's merchant ID. If you'd like to perform void,
# capture or refund transactions then you'll also need to add a username
# and password to your account under administration -> account settings ->
# order settings -> Use username/password validation
def initialize(options = {})
requires!(options, :login)
super
end
def capture(money, authorization, options = {})
reference, amount, type = split_auth(authorization)
post = {}
add_amount(post, money)
add_reference(post, reference)
add_transaction_type(post, :capture)
commit(post)
end
def refund(money, source, options = {})
post = {}
reference, amount, type = split_auth(source)
add_reference(post, reference)
add_transaction_type(post, refund_action(type))
add_amount(post, money)
commit(post)
end
def credit(money, source, options = {})
deprecated Gateway::CREDIT_DEPRECATION_MESSAGE
refund(money, source, options)
end
private
def purchase_action(source)
if source.is_a?(Check)
:check_purchase
else
:purchase
end
end
def add_customer_ip(post, options)
post[:customerIP] = options[:ip] if options[:ip]
end
def void_action(original_transaction_type)
(original_transaction_type == TRANSACTIONS[:refund]) ? :void_refund : :void_purchase
end
def refund_action(type)
(type == TRANSACTIONS[:check_purchase]) ? :check_refund : :refund
end
def secure_profile_action(type)
PROFILE_OPERATIONS[type] || PROFILE_OPERATIONS[:new]
end
def split_auth(string)
string.split(";")
end
def add_amount(post, money)
post[:trnAmount] = amount(money)
end
def add_original_amount(post, amount)
post[:trnAmount] = amount
end
def add_reference(post, reference)
post[:adjId] = reference
end
def add_address(post, options)
prepare_address_for_non_american_countries(options)
if billing_address = options[:billing_address] || options[:address]
post[:ordName] = billing_address[:name]
post[:ordEmailAddress] = options[:email]
post[:ordPhoneNumber] = billing_address[:phone]
post[:ordAddress1] = billing_address[:address1]
post[:ordAddress2] = billing_address[:address2]
post[:ordCity] = billing_address[:city]
post[:ordProvince] = billing_address[:state]
post[:ordPostalCode] = billing_address[:zip]
post[:ordCountry] = billing_address[:country]
end
if shipping_address = options[:shipping_address]
post[:shipName] = shipping_address[:name]
post[:shipEmailAddress] = options[:email]
post[:shipPhoneNumber] = shipping_address[:phone]
post[:shipAddress1] = shipping_address[:address1]
post[:shipAddress2] = shipping_address[:address2]
post[:shipCity] = shipping_address[:city]
post[:shipProvince] = shipping_address[:state]
post[:shipPostalCode] = shipping_address[:zip]
post[:shipCountry] = shipping_address[:country]
post[:shippingMethod] = shipping_address[:shipping_method]
post[:deliveryEstimate] = shipping_address[:delivery_estimate]
end
end
def prepare_address_for_non_american_countries(options)
[ options[:billing_address], options[:shipping_address] ].compact.each do |address|
unless ['US', 'CA'].include?(address[:country])
address[:state] = '--'
address[:zip] = '000000' unless address[:zip]
end
end
end
def add_invoice(post, options)
post[:trnOrderNumber] = options[:order_id]
post[:trnComments] = options[:description]
post[:ordItemPrice] = amount(options[:subtotal])
post[:ordShippingPrice] = amount(options[:shipping])
post[:ordTax1Price] = amount(options[:tax1] || options[:tax])
post[:ordTax2Price] = amount(options[:tax2])
post[:ref1] = options[:custom]
end
def add_credit_card(post, credit_card)
if credit_card
post[:trnCardOwner] = credit_card.name
post[:trnCardNumber] = credit_card.number
post[:trnExpMonth] = format(credit_card.month, :two_digits)
post[:trnExpYear] = format(credit_card.year, :two_digits)
post[:trnCardCvd] = credit_card.verification_value
end
end
def add_check(post, check)
# The institution number of the consumers financial institution. Required for Canadian dollar EFT transactions.
post[:institutionNumber] = check.institution_number
# The bank transit number of the consumers bank account. Required for Canadian dollar EFT transactions.
post[:transitNumber] = check.transit_number
# The routing number of the consumers bank account. Required for US dollar EFT transactions.
post[:routingNumber] = check.routing_number
# The account number of the consumers bank account. Required for both Canadian and US dollar EFT transactions.
post[:accountNumber] = check.account_number
end
def add_secure_profile_variables(post, options = {})
post[:serviceVersion] = SP_SERVICE_VERSION
post[:responseFormat] = 'QS'
post[:cardValidation] = (options[:cardValidation].to_i == 1) || '0'
post[:operationType] = options[:operationType] || options[:operation] || secure_profile_action(:new)
post[:customerCode] = options[:billing_id] || options[:vault_id] || false
post[:status] = options[:status]
end
def add_recurring_amount(post, money)
post[:amount] = amount(money)
end
def add_recurring_invoice(post, options)
post[:rbApplyTax1] = options[:apply_tax1]
post[:rbApplyTax2] = options[:apply_tax2]
end
def add_recurring_operation_type(post, operation)
post[:operationType] = RECURRING_OPERATION[operation]
end
def add_recurring_service(post, options)
post[:serviceVersion] = '1.0'
post[:merchantId] = @options[:login]
post[:passCode] = @options[:recurring_api_key]
post[:rbAccountId] = options[:account_id]
end
def add_recurring_type(post, options)
# XXX requires!
post[:trnRecurring] = 1
period, increment = interval(options)
post[:rbBillingPeriod] = PERIODS[period]
post[:rbBillingIncrement] = increment
if options.include? :start_date
post[:rbCharge] = 0
post[:rbFirstBilling] = options[:start_date].strftime('%m%d%Y')
end
if count = options[:occurrences] || options[:payments]
post[:rbExpiry] = (options[:start_date] || Date.current).advance(period => count).strftime('%m%d%Y')
end
end
def interval(options)
if options.include? :periodicity
requires!(options, [:periodicity, *PERIODICITIES.keys])
PERIODICITIES[options[:periodicity]]
elsif options.include? :interval
interval = options[:interval]
if interval.respond_to? :parts
parts = interval.parts
raise ArgumentError.new("Cannot recur with mixed interval (#{interval}). Use only one of: days, weeks, months or years") if parts.length > 1
parts.first
elsif interval.kind_of? Hash
requires!(interval, :unit)
unit, length = interval.values_at(:unit, :length)
length ||= 1
[unit, length]
end
end
end
def parse(body)
results = {}
if !body.nil?
body.split(/&/).each do |pair|
key, val = pair.split(/\=/)
results[key.to_sym] = val.nil? ? nil : CGI.unescape(val)
end
end
# Clean up the message text if there is any
if results[:messageText]
results[:messageText].gsub!(/<LI>/, "")
results[:messageText].gsub!(/(\.)?<br>/, ". ")
results[:messageText].strip!
end
results
end
def recurring_parse(data)
REXML::Document.new(data).root.elements.to_a.inject({}) do |response, element|
response[element.name.to_sym] = element.text
response
end
end
def commit(params, use_profile_api = false)
post(post_data(params,use_profile_api),use_profile_api)
end
def recurring_commit(params)
recurring_post(post_data(params, false))
end
def post(data, use_profile_api=nil)
response = parse(ssl_post((use_profile_api ? SECURE_PROFILE_URL : self.live_url), data))
response[:customer_vault_id] = response[:customerCode] if response[:customerCode]
build_response(success?(response), message_from(response), response,
:test => test? || response[:authCode] == "TEST",
:authorization => authorization_from(response),
:cvv_result => CVD_CODES[response[:cvdId]],
:avs_result => { :code => (AVS_CODES.include? response[:avsId]) ? AVS_CODES[response[:avsId]] : response[:avsId] }
)
end
def recurring_post(data)
response = recurring_parse(ssl_post(RECURRING_URL, data))
build_response(recurring_success?(response), recurring_message_from(response), response)
end
def authorization_from(response)
"#{response[:trnId]};#{response[:trnAmount]};#{response[:trnType]}"
end
def message_from(response)
response[:messageText] || response[:responseMessage]
end
def recurring_message_from(response)
response[:message]
end
def success?(response)
response[:responseType] == 'R' || response[:trnApproved] == '1' || response[:responseCode] == '1'
end
def recurring_success?(response)
response[:code] == '1'
end
def add_source(post, source)
if source.is_a?(String) or source.is_a?(Integer)
post[:customerCode] = source
else
card_brand(source) == "check" ? add_check(post, source) : add_credit_card(post, source)
end
end
def add_transaction_type(post, action)
post[:trnType] = TRANSACTIONS[action]
end
def post_data(params, use_profile_api)
params[:requestType] = 'BACKEND'
if use_profile_api
params[:merchantId] = @options[:login]
params[:passCode] = @options[:secure_profile_api_key]
else
params[:username] = @options[:user] if @options[:user]
params[:password] = @options[:password] if @options[:password]
params[:merchant_id] = @options[:login]
end
params[:vbvEnabled] = '0'
params[:scEnabled] = '0'
params.reject{|k, v| v.blank?}.collect { |key, value| "#{key}=#{CGI.escape(value.to_s)}" }.join("&")
end
end
end
end

View File

@@ -0,0 +1,54 @@
require File.dirname(__FILE__) + '/beanstream/beanstream_core'
module ActiveMerchant #:nodoc:
module Billing #:nodoc:
class BeanstreamInteracResponse < Response
def redirect
params['pageContents']
end
end
class BeanstreamInteracGateway < Gateway
include BeanstreamCore
# Confirm a transaction posted back from the bank to Beanstream.
# Confirming a transaction does not require any credentials,
# and in an application with many merchants sharing a funded
# URL the application may not yet know which merchant the
# post back is for until the response of the confirmation is
# received, which contains the order number.
def self.confirm(transaction)
gateway = new(:login => '')
gateway.confirm(transaction)
end
def purchase(money, options = {})
post = {}
add_amount(post, money)
add_invoice(post, options)
add_address(post, options)
add_interac_details(post, options)
add_transaction_type(post, :purchase)
commit(post)
end
# Confirm a transaction posted back from the bank to Beanstream.
def confirm(transaction)
post(transaction)
end
private
def add_interac_details(post, options)
address = options[:billing_address] || options[:address] || {}
post[:trnCardOwner] = address[:name]
post[:paymentMethod] = 'IO'
end
def build_response(*args)
BeanstreamInteracResponse.new(*args)
end
end
end
end

View File

@@ -0,0 +1,503 @@
require 'digest/md5'
module ActiveMerchant #:nodoc:
module Billing #:nodoc:
class BluePayGateway < Gateway
class_attribute :rebilling_url, :ignore_http_status
self.live_url = 'https://secure.bluepay.com/interfaces/bp20post'
self.rebilling_url = 'https://secure.bluepay.com/interfaces/bp20rebadmin'
self.ignore_http_status = true
CARD_CODE_ERRORS = %w( N S )
AVS_ERRORS = %w( A E N R W Z )
AVS_REASON_CODES = %w(27 45)
FRAUD_REVIEW_STATUSES = %w( E 0 )
FIELD_MAP = {
'TRANS_ID' => :transaction_id,
'STATUS' => :response_code,
'AVS' => :avs_result_code,
'CVV2'=> :card_code,
'AUTH_CODE' => :authorization,
'MESSAGE' => :message,
'REBID' => :rebid,
'TRANS_TYPE' => :trans_type,
'PAYMENT_ACCOUNT_MASK' => :acct_mask,
'CARD_TYPE' => :card_type,
}
REBILL_FIELD_MAP = {
'REBILL_ID' => :rebill_id,
'ACCOUNT_ID'=> :account_id,
'USER_ID' => :user_id,
'TEMPLATE_ID' => :template_id,
'STATUS' => :status,
'CREATION_DATE' => :creation_date,
'NEXT_DATE' => :next_date,
'LAST_DATE' => :last_date,
'SCHED_EXPR' => :schedule,
'CYCLES_REMAIN' => :cycles_remain,
'REB_AMOUNT' => :rebill_amount,
'NEXT_AMOUNT' => :next_amount,
'USUAL_DATE' => :undoc_usual_date, # Not found in the bp20rebadmin API doc.
}
self.supported_countries = ['US']
self.supported_cardtypes = [:visa, :master, :american_express, :discover, :diners_club, :jcb]
self.homepage_url = 'http://www.bluepay.com/'
self.display_name = 'BluePay'
self.money_format = :dollars
# Creates a new BluepayGateway
#
# The gateway requires that a valid Account ID and Secret Key be passed
# in the +options+ hash.
#
# ==== Options
#
# * <tt>:account_id</tt> -- The BluePay gateway Account ID (REQUIRED)
# * <tt>:secret_key</tt> -- The BluePay gateway Secret Key (REQUIRED)
# * <tt>:test</tt> -- set to true for TEST mode or false for LIVE mode
def initialize(options = {})
requires!(options, :login, :password)
super
end
# Performs an authorization, which reserves the funds on the customer's credit card. This does not actually take funds from the customer
# This is referred to an AUTH transaction in BluePay
#
# ==== Parameters
#
# * <tt>money</tt> -- The amount to be authorized as an Integer value in cents.
# * <tt>payment_object</tt> -- This can either be one of three things:
# A CreditCard object,
# A Check object,
# or a token. The token is called the Master ID. This is a unique transaction ID returned from a previous transaction. This token associates all the stored information for a previous transaction.
# * <tt>options</tt> -- A hash of optional parameters.
def authorize(money, payment_object, options = {})
post = {}
add_payment_method(post, payment_object)
add_invoice(post, options)
add_address(post, options)
add_customer_data(post, options)
add_rebill(post, options) if options[:rebill]
add_duplicate_override(post, options)
post[:TRANS_TYPE] = 'AUTH'
commit('AUTH_ONLY', money, post)
end
# Perform a purchase, which is essentially an authorization and capture in a single operation.
# This is referred to a SALE transaction in BluePay
#
# ==== Parameters
#
# * <tt>money</tt> -- The amount to be purchased as an Integer value in cents.
# * <tt>payment_object</tt> -- This can either be one of three things:
# A CreditCard object,
# A Check object,
# or a token. The token is called the Master ID. This is a unique transaction ID returned from a previous transaction. This token associates all the stored information for a previous transaction.
# * <tt>options</tt> -- A hash of optional parameters.,
def purchase(money, payment_object, options = {})
post = {}
add_payment_method(post, payment_object)
add_invoice(post, options)
add_address(post, options)
add_customer_data(post, options)
add_rebill(post, options) if options[:rebill]
add_duplicate_override(post, options)
post[:TRANS_TYPE] = 'SALE'
commit('AUTH_CAPTURE', money, post)
end
# Captures the funds from an authorize transaction.
# This is referred to a CAPTURE transaction in BluePay
#
# ==== Parameters
#
# * <tt>money</tt> -- The amount to be captured as an Integer value in cents.
# * <tt>identification</tt> -- The Master ID, or token, returned from the previous authorize transaction.
def capture(money, identification, options = {})
post = {}
add_address(post, options)
add_customer_data(post, options)
post[:MASTER_ID] = identification
post[:TRANS_TYPE] = 'CAPTURE'
commit('PRIOR_AUTH_CAPTURE', money, post)
end
# Void a previous transaction
# This is referred to a VOID transaction in BluePay
#
# ==== Parameters
#
# * <tt>identification</tt> - The Master ID, or token, returned from a previous authorize transaction.
def void(identification, options = {})
post = {}
post[:MASTER_ID] = identification
post[:TRANS_TYPE] = 'VOID'
commit('VOID', nil, post)
end
# Performs a credit.
#
# This transaction indicates that money should flow from the merchant to the customer.
#
# ==== Parameters
#
# * <tt>money</tt> -- The amount to be credited to the customer as an Integer value in cents.
# * <tt>payment_object</tt> -- This can either be one of three things:
# A CreditCard object,
# A Check object,
# or a token. The token is called the Master ID. This is a unique transaction ID returned from a previous transaction. This token associates all the stored information for a previous transaction.
# If the payment_object is a token, then the transaction type will reverse a previous capture or purchase transaction, returning the funds to the customer. If the amount is nil, a full credit will be processed. This is referred to a REFUND transaction in BluePay.
# If the payment_object is either a CreditCard or Check object, then the transaction type will be an unmatched credit placing funds in the specified account. This is referred to a CREDIT transaction in BluePay.
# * <tt>options</tt> -- A hash of parameters.
def refund(money, identification, options = {})
if(identification && !identification.kind_of?(String))
deprecated "refund should only be used to refund a referenced transaction"
return credit(money, identification, options)
end
post = {}
post[:PAYMENT_ACCOUNT] = ''
post[:MASTER_ID] = identification
post[:TRANS_TYPE] = 'REFUND'
post[:NAME1] = (options[:first_name] ? options[:first_name] : "")
post[:NAME2] = options[:last_name] if options[:last_name]
post[:ZIP] = options[:zip] if options[:zip]
add_invoice(post, options)
add_address(post, options)
add_customer_data(post, options)
commit('CREDIT', money, post)
end
def credit(money, payment_object, options = {})
if(payment_object && payment_object.kind_of?(String))
deprecated "credit should only be used to credit a payment method"
return refund(money, payment_object, options)
end
post = {}
post[:PAYMENT_ACCOUNT] = ''
add_payment_method(post, payment_object)
post[:TRANS_TYPE] = 'CREDIT'
post[:NAME1] = (options[:first_name] ? options[:first_name] : "")
post[:NAME2] = options[:last_name] if options[:last_name]
post[:ZIP] = options[:zip] if options[:zip]
add_invoice(post, options)
add_address(post, options)
add_customer_data(post, options)
commit('CREDIT', money, post)
end
# Create a new recurring payment.
#
# ==== Parameters
#
# * <tt>money</tt> -- The amount to charge the customer at the time of the recurring payment setup, in cents. Set to zero if you do not want the customer to be charged at this time.
# * <tt>payment_object</tt> -- This can either be one of three things:
# A CreditCard object,
# A Check object,
# or a token. The token is called the Master ID. This is a unique transaction ID returned from a previous transaction. This token associates all the stored information for a previous transaction.
# * <tt>options</tt> -- A hash of optional parameters.,
# ==== Options
#
# * <tt>:rebill_start_date</tt> is a string that tells the gateway when to start the rebill. (REQUIRED)
# Has two valid formats:
# "YYYY-MM-DD HH:MM:SS" Hours, minutes, and seconds are optional.
# "XX UNITS" Relative date as explained below. Marked from the time of the
# transaction (i.e.: 10 DAYS, 1 MONTH, 1 YEAR)
# * <tt>:rebill_expression</tt> is the period of time in-between rebillings. (REQUIRED)
# It uses the same "XX UNITS" format as rebill_start_date, explained above.
# Optional parameters include:
# * <tt>rebill_cycles</tt>: Number of times to rebill. Don't send or set to nil for infinite rebillings (or
# until canceled).
# * <tt>rebill_amount</tt>: Amount to rebill. Defaults to amount of transaction for rebillings.
#
# For example, to charge the customer $19.95 now and then charge $39.95 in 60 days every 3 months for 5 times, the options hash would be as follows:
# :rebill_start_date => '60 DAYS',
# :rebill_expression => '3 MONTHS',
# :rebill_cycles => '5',
# :rebill_amount => '39.95'
# A money object of 1995 cents would be passed into the 'money' parameter.
def recurring(money, payment_object, options = {})
requires!(options, :rebill_start_date, :rebill_expression)
options[:rebill] = true
if money
purchase(money, payment_object, options)
else
authorize(money, payment_object, options)
end
end
# View a recurring payment
#
# This will pull data associated with a current recurring billing
#
# ==== Parameters
#
# * <tt>rebill_id</tt> -- A string containing the rebill_id of the recurring billing that is already active (REQUIRED)
def status_recurring(rebill_id)
post = {}
requires!(rebill_id)
post[:REBILL_ID] = rebill_id
post[:TRANS_TYPE] = 'GET'
commit('rebill', 'nil', post)
end
# Update a recurring payment's details.
#
# This transaction updates an existing recurring billing
#
# ==== Options
#
# * <tt>:rebill_id</tt> -- The 12 digit rebill ID used to update a particular rebilling cycle. (REQUIRED)
# * <tt>:rebill_amount</tt> -- A string containing the new rebilling amount.
# * <tt>:rebill_next_date</tt> -- A string containing the new rebilling next date.
# * <tt>:rebill_expression</tt> -- A string containing the new rebilling expression.
# * <tt>:rebill_cycles</tt> -- A string containing the new rebilling cycles.
# * <tt>:rebill_next_amount</tt> -- A string containing the next rebilling amount to charge the customer. This ONLY affects the next scheduled charge; all other rebillings will continue at the regular (rebill_amount) amount.
# Take a look above at the recurring_payment method for similar examples on how to use.
def update_recurring(options = {})
post = {}
requires!(options, :rebill_id)
post[:REBILL_ID] = options[:rebill_id]
post[:TRANS_TYPE] = 'SET'
post[:REB_AMOUNT] = amount(options[:rebill_amount]) if options[:rebill_amount]
post[:NEXT_DATE] = options[:rebill_next_date]
post[:REB_EXPR] = options[:rebill_expression]
post[:REB_CYCLES] = options[:rebill_cycles]
post[:NEXT_AMOUNT] = options[:rebill_next_amount]
commit('rebill', 'nil', post)
end
# Cancel a recurring payment.
#
# This transaction cancels an existing recurring billing.
#
# ==== Parameters
#
# * <tt>rebill_id</tt> -- A string containing the rebill_id of the recurring billing that you wish to cancel/stop (REQUIRED)
def cancel_recurring(rebill_id)
post = {}
requires!(rebill_id)
post[:REBILL_ID] = rebill_id
post[:TRANS_TYPE] = 'SET'
post[:STATUS] = 'stopped'
commit('rebill', 'nil', post)
end
private
def commit(action, money, fields)
fields[:AMOUNT] = amount(money) unless(fields[:TRANS_TYPE] == 'VOID' || action == 'rebill')
fields[:MODE] = (test? ? 'TEST' : 'LIVE')
fields[:ACCOUNT_ID] = @options[:login]
if action == 'rebill'
url = rebilling_url
fields[:TAMPER_PROOF_SEAL] = calc_rebill_tps(fields)
else
url = live_url
fields[:TAMPER_PROOF_SEAL] = calc_tps(amount(money), fields)
end
parse(ssl_post(url, post_data(action, fields)))
end
def parse_recurring(response_fields, opts={}) # expected status?
parsed = {}
response_fields.each do |k,v|
mapped_key = REBILL_FIELD_MAP.include?(k) ? REBILL_FIELD_MAP[k] : k
parsed[mapped_key] = v
end
success = parsed[:status] != 'error'
message = parsed[:status]
Response.new(success, message, parsed,
:test => test?,
:authorization => parsed[:rebill_id])
end
def parse(body)
# The bp20api has max one value per form field.
response_fields = Hash[CGI::parse(body).map{|k,v| [k.upcase,v.first]}]
if response_fields.include? "REBILL_ID"
return parse_recurring(response_fields)
end
parsed = {}
response_fields.each do |k,v|
mapped_key = FIELD_MAP.include?(k) ? FIELD_MAP[k] : k
parsed[mapped_key] = v
end
# normalize message
message = message_from(parsed)
success = parsed[:response_code] == '1'
Response.new(success, message, parsed,
:test => test?,
:authorization => (parsed[:rebid] && parsed[:rebid] != '' ? parsed[:rebid] : parsed[:transaction_id]),
:fraud_review => FRAUD_REVIEW_STATUSES.include?(parsed[:response_code]),
:avs_result => { :code => parsed[:avs_result_code] },
:cvv_result => parsed[:card_code]
)
end
def message_from(parsed)
message = parsed[:message]
if(parsed[:response_code].to_i == 2)
if CARD_CODE_ERRORS.include?(parsed[:card_code])
message = CVVResult.messages[parsed[:card_code]]
elsif AVS_ERRORS.include?(parsed[:avs_result_code])
message = AVSResult.messages[ parsed[:avs_result_code] ]
else
message = message.chomp('.')
end
elsif message == "Missing ACCOUNT_ID"
message = "The merchant login ID or password is invalid"
elsif message =~ /Approved/
message = "This transaction has been approved"
elsif message =~ /Expired/
message = "The credit card has expired"
end
message
end
def add_invoice(post, options)
post[:ORDER_ID] = options[:order_id]
post[:INVOICE_ID] = options[:invoice]
post[:invoice_num] = options[:order_id]
post[:MEMO] = options[:description]
post[:description] = options[:description]
end
def add_payment_method(post, payment_object)
post[:MASTER_ID] = ''
case payment_object
when String
post[:MASTER_ID] = payment_object
when Check
add_check(post, payment_object)
else
add_creditcard(post, payment_object)
end
end
def add_creditcard(post, creditcard)
post[:PAYMENT_TYPE] = 'CREDIT'
post[:PAYMENT_ACCOUNT] = creditcard.number
post[:CARD_CVV2] = creditcard.verification_value
post[:CARD_EXPIRE] = expdate(creditcard)
post[:NAME1] = creditcard.first_name
post[:NAME2] = creditcard.last_name
end
CHECK_ACCOUNT_TYPES = {
"checking" => "C",
"savings" => "S"
}
def add_check(post, check)
post[:PAYMENT_TYPE] = 'ACH'
post[:PAYMENT_ACCOUNT] = [CHECK_ACCOUNT_TYPES[check.account_type], check.routing_number, check.account_number].join(":")
post[:NAME1] = check.first_name
post[:NAME2] = check.last_name
end
def add_customer_data(post, options)
post[:EMAIL] = options[:email]
post[:CUSTOM_ID] = options[:customer]
end
def add_duplicate_override(post, options)
post[:DUPLICATE_OVERRIDE] = options[:duplicate_override]
end
def add_address(post, options)
if address = (options[:shipping_address] || options[:billing_address] || options[:address])
post[:NAME1] = address[:first_name]
post[:NAME2] = address[:last_name]
post[:ADDR1] = address[:address1]
post[:ADDR2] = address[:address2]
post[:COMPANY_NAME] = address[:company]
post[:PHONE] = address[:phone]
post[:CITY] = address[:city]
post[:STATE] = (address[:state].blank? ? 'n/a' : address[:state])
post[:ZIP] = address[:zip]
post[:COUNTRY] = address[:country]
end
end
def add_rebill(post, options)
post[:DO_REBILL] = '1'
post[:REB_AMOUNT] = amount(options[:rebill_amount])
post[:REB_FIRST_DATE] = options[:rebill_start_date]
post[:REB_EXPR] = options[:rebill_expression]
post[:REB_CYCLES] = options[:rebill_cycles]
end
def post_data(action, parameters = {})
post = {}
post[:version] = '1'
post[:login] = ''
post[:tran_key] = ''
post[:relay_response] = "FALSE"
post[:type] = action
post[:delim_data] = "TRUE"
post[:delim_char] = ","
post[:encap_char] = "$"
post[:card_num] = '4111111111111111'
post[:exp_date] = '1212'
post[:solution_ID] = application_id if(application_id && application_id != "ActiveMerchant")
post.merge(parameters).collect { |key, value| "#{key}=#{CGI.escape(value.to_s)}" }.join("&")
end
def expdate(creditcard)
year = format(creditcard.year, :two_digits)
month = format(creditcard.month, :two_digits)
"#{month}#{year}"
end
def calc_tps(amount, post)
post[:NAME1] ||= ''
Digest::MD5.hexdigest(
[
@options[:password],
@options[:login],
post[:TRANS_TYPE],
amount,
post[:MASTER_ID],
post[:NAME1],
post[:PAYMENT_ACCOUNT]
].join("")
)
end
def calc_rebill_tps(post)
Digest::MD5.hexdigest(
[
@options[:password],
@options[:login],
post[:TRANS_TYPE],
post[:REBILL_ID]
].join("")
)
end
def handle_response(response)
if ignore_http_status || (200...300).include?(response.code.to_i)
return response.body
end
raise ResponseError.new(response)
end
end
end
end

View File

@@ -0,0 +1,142 @@
module ActiveMerchant #:nodoc:
module Billing #:nodoc:
# Bogus Gateway
class BogusGateway < Gateway
AUTHORIZATION = '53433'
SUCCESS_MESSAGE = "Bogus Gateway: Forced success"
FAILURE_MESSAGE = "Bogus Gateway: Forced failure"
ERROR_MESSAGE = "Bogus Gateway: Use CreditCard number ending in 1 for success, 2 for exception and anything else for error"
CREDIT_ERROR_MESSAGE = "Bogus Gateway: Use CreditCard number ending in 1 for success, 2 for exception and anything else for error"
UNSTORE_ERROR_MESSAGE = "Bogus Gateway: Use trans_id ending in 1 for success, 2 for exception and anything else for error"
CAPTURE_ERROR_MESSAGE = "Bogus Gateway: Use authorization number ending in 1 for exception, 2 for error and anything else for success"
VOID_ERROR_MESSAGE = "Bogus Gateway: Use authorization number ending in 1 for exception, 2 for error and anything else for success"
REFUND_ERROR_MESSAGE = "Bogus Gateway: Use trans_id number ending in 1 for exception, 2 for error and anything else for success"
self.supported_countries = ['US']
self.supported_cardtypes = [:bogus]
self.homepage_url = 'http://example.com'
self.display_name = 'Bogus'
def authorize(money, credit_card_or_reference, options = {})
money = amount(money)
case normalize(credit_card_or_reference)
when /1$/
Response.new(true, SUCCESS_MESSAGE, {:authorized_amount => money}, :test => true, :authorization => AUTHORIZATION )
when /2$/
Response.new(false, FAILURE_MESSAGE, {:authorized_amount => money, :error => FAILURE_MESSAGE }, :test => true)
else
raise Error, ERROR_MESSAGE
end
end
def purchase(money, credit_card_or_reference, options = {})
money = amount(money)
case normalize(credit_card_or_reference)
when /1$/, AUTHORIZATION
Response.new(true, SUCCESS_MESSAGE, {:paid_amount => money}, :test => true, :authorization => AUTHORIZATION)
when /2$/
Response.new(false, FAILURE_MESSAGE, {:paid_amount => money, :error => FAILURE_MESSAGE },:test => true)
else
raise Error, ERROR_MESSAGE
end
end
def recurring(money, credit_card_or_reference, options = {})
money = amount(money)
case normalize(credit_card_or_reference)
when /1$/
Response.new(true, SUCCESS_MESSAGE, {:paid_amount => money}, :test => true)
when /2$/
Response.new(false, FAILURE_MESSAGE, {:paid_amount => money, :error => FAILURE_MESSAGE },:test => true)
else
raise Error, ERROR_MESSAGE
end
end
def credit(money, credit_card_or_reference, options = {})
if credit_card_or_reference.is_a?(String)
deprecated CREDIT_DEPRECATION_MESSAGE
return refund(money, credit_card_or_reference, options)
end
money = amount(money)
case normalize(credit_card_or_reference)
when /1$/
Response.new(true, SUCCESS_MESSAGE, {:paid_amount => money}, :test => true )
when /2$/
Response.new(false, FAILURE_MESSAGE, {:paid_amount => money, :error => FAILURE_MESSAGE }, :test => true)
else
raise Error, CREDIT_ERROR_MESSAGE
end
end
def refund(money, reference, options = {})
money = amount(money)
case reference
when /1$/
raise Error, REFUND_ERROR_MESSAGE
when /2$/
Response.new(false, FAILURE_MESSAGE, {:paid_amount => money, :error => FAILURE_MESSAGE }, :test => true)
else
Response.new(true, SUCCESS_MESSAGE, {:paid_amount => money}, :test => true)
end
end
def capture(money, reference, options = {})
money = amount(money)
case reference
when /1$/
raise Error, CAPTURE_ERROR_MESSAGE
when /2$/
Response.new(false, FAILURE_MESSAGE, {:paid_amount => money, :error => FAILURE_MESSAGE }, :test => true)
else
Response.new(true, SUCCESS_MESSAGE, {:paid_amount => money}, :test => true)
end
end
def void(reference, options = {})
case reference
when /1$/
raise Error, VOID_ERROR_MESSAGE
when /2$/
Response.new(false, FAILURE_MESSAGE, {:authorization => reference, :error => FAILURE_MESSAGE }, :test => true)
else
Response.new(true, SUCCESS_MESSAGE, {:authorization => reference}, :test => true)
end
end
def store(credit_card_or_reference, options = {})
case normalize(credit_card_or_reference)
when /1$/
Response.new(true, SUCCESS_MESSAGE, {:billingid => '1'}, :test => true, :authorization => AUTHORIZATION)
when /2$/
Response.new(false, FAILURE_MESSAGE, {:billingid => nil, :error => FAILURE_MESSAGE }, :test => true)
else
raise Error, ERROR_MESSAGE
end
end
def unstore(reference, options = {})
case reference
when /1$/
Response.new(true, SUCCESS_MESSAGE, {}, :test => true)
when /2$/
Response.new(false, FAILURE_MESSAGE, {:error => FAILURE_MESSAGE },:test => true)
else
raise Error, UNSTORE_ERROR_MESSAGE
end
end
private
def normalize(credit_card_or_reference)
if credit_card_or_reference.respond_to?(:number)
credit_card_or_reference.number
else
credit_card_or_reference.to_s
end
end
end
end
end

View File

@@ -0,0 +1,19 @@
require File.dirname(__FILE__) + '/braintree/braintree_common'
module ActiveMerchant #:nodoc:
module Billing #:nodoc:
class BraintreeGateway < Gateway
include BraintreeCommon
self.abstract_class = true
def self.new(options={})
if options.has_key?(:login)
BraintreeOrangeGateway.new(options)
else
BraintreeBlueGateway.new(options)
end
end
end
end
end

View File

@@ -0,0 +1,9 @@
module BraintreeCommon
def self.included(base)
base.supported_countries = ['US']
base.supported_cardtypes = [:visa, :master, :american_express, :discover, :jcb, :diners_club]
base.homepage_url = 'http://www.braintreepaymentsolutions.com'
base.display_name = 'Braintree'
base.default_currency = 'USD'
end
end

View File

@@ -0,0 +1,401 @@
require File.dirname(__FILE__) + '/braintree/braintree_common'
begin
require "braintree"
rescue LoadError
raise "Could not load the braintree gem. Use `gem install braintree` to install it."
end
raise "Need braintree gem 2.x.y. Run `gem install braintree --version '~>2.0'` to get the correct version." unless Braintree::Version::Major == 2
module ActiveMerchant #:nodoc:
module Billing #:nodoc:
# For more information on the Braintree Gateway please visit their
# {Developer Portal}[https://www.braintreepayments.com/developers]
#
# ==== About this implementation
#
# This implementation leverages the Braintree-authored ruby gem:
# https://github.com/braintree/braintree_ruby
#
# ==== Debugging Information
#
# Setting an ActiveMerchant +wiredump_device+ will automatically
# configure the Braintree logger (via the Braintree gem's
# configuration) when the BraintreeBlueGateway is instantiated.
# Additionally, the log level will be set to +DEBUG+. Therefore,
# all you have to do is set the +wiredump_device+ and you'll
# get your debug output from your HTTP interactions with the
# remote gateway. (Don't enable this in production.)
#
# For example:
#
# ActiveMerchant::Billing::BraintreeBlueGateway.wiredump_device = Logger.new(STDOUT)
# # => #<Logger:0x107d385f8 ...>
#
# Braintree::Configuration.logger
# # => (some other logger, created by default by the gem)
#
# Braintree::Configuration.logger.level
# # => 1 (INFO)
#
# ActiveMerchant::Billing::BraintreeBlueGateway.new(:merchant_id => 'x', :public_key => 'x', :private_key => 'x')
#
# Braintree::Configuration.logger
# # => #<Logger:0x107d385f8 ...>
#
# Braintree::Configuration.logger.level
# # => 0 (DEBUG)
#
# Alternatively, you can avoid setting the +wiredump_device+
# and set +Braintree::Configuration.logger+ and/or
# +Braintree::Configuration.logger.level+ directly.
class BraintreeBlueGateway < Gateway
include BraintreeCommon
self.display_name = 'Braintree (Blue Platform)'
def initialize(options = {})
requires!(options, :merchant_id, :public_key, :private_key)
@merchant_account_id = options[:merchant_account_id]
super
Braintree::Configuration.merchant_id = options[:merchant_id]
Braintree::Configuration.public_key = options[:public_key]
Braintree::Configuration.private_key = options[:private_key]
Braintree::Configuration.environment = (options[:environment] || (test? ? :sandbox : :production)).to_sym
Braintree::Configuration.custom_user_agent = "ActiveMerchant #{ActiveMerchant::VERSION}"
if wiredump_device
Braintree::Configuration.logger = ((Logger === wiredump_device) ? wiredump_device : Logger.new(wiredump_device))
Braintree::Configuration.logger.level = Logger::DEBUG
else
Braintree::Configuration.logger.level = Logger::WARN
end
end
def authorize(money, credit_card_or_vault_id, options = {})
create_transaction(:sale, money, credit_card_or_vault_id, options)
end
def capture(money, authorization, options = {})
commit do
result = Braintree::Transaction.submit_for_settlement(authorization, amount(money).to_s)
Response.new(result.success?, message_from_result(result))
end
end
def purchase(money, credit_card_or_vault_id, options = {})
authorize(money, credit_card_or_vault_id, options.merge(:submit_for_settlement => true))
end
def credit(money, credit_card_or_vault_id, options = {})
create_transaction(:credit, money, credit_card_or_vault_id, options)
end
def refund(*args)
# legacy signature: #refund(transaction_id, options = {})
# new signature: #refund(money, transaction_id, options = {})
money, transaction_id, _ = extract_refund_args(args)
money = amount(money).to_s if money
commit do
result = Braintree::Transaction.refund(transaction_id, money)
Response.new(result.success?, message_from_result(result),
{:braintree_transaction => (transaction_hash(result.transaction) if result.success?)},
{:authorization => (result.transaction.id if result.success?)}
)
end
end
def void(authorization, options = {})
commit do
result = Braintree::Transaction.void(authorization)
Response.new(result.success?, message_from_result(result),
{:braintree_transaction => (transaction_hash(result.transaction) if result.success?)},
{:authorization => (result.transaction.id if result.success?)}
)
end
end
def store(creditcard, options = {})
commit do
parameters = {
:first_name => creditcard.first_name,
:last_name => creditcard.last_name,
:email => options[:email],
:credit_card => {
:number => creditcard.number,
:cvv => creditcard.verification_value,
:expiration_month => creditcard.month.to_s.rjust(2, "0"),
:expiration_year => creditcard.year.to_s
}
}
result = Braintree::Customer.create(merge_credit_card_options(parameters, options))
Response.new(result.success?, message_from_result(result),
{
:braintree_customer => (customer_hash(result.customer) if result.success?),
:customer_vault_id => (result.customer.id if result.success?)
},
:authorization => (result.customer.id if result.success?)
)
end
end
def update(vault_id, creditcard, options = {})
braintree_credit_card = nil
commit do
braintree_credit_card = Braintree::Customer.find(vault_id).credit_cards.detect { |cc| cc.default? }
return Response.new(false, 'Braintree::NotFoundError') if braintree_credit_card.nil?
options.merge!(:update_existing_token => braintree_credit_card.token)
credit_card_params = merge_credit_card_options({
:credit_card => {
:number => creditcard.number,
:cvv => creditcard.verification_value,
:expiration_month => creditcard.month.to_s.rjust(2, "0"),
:expiration_year => creditcard.year.to_s
}
}, options)[:credit_card]
result = Braintree::Customer.update(vault_id,
:first_name => creditcard.first_name,
:last_name => creditcard.last_name,
:email => options[:email],
:credit_card => credit_card_params
)
Response.new(result.success?, message_from_result(result),
:braintree_customer => (customer_hash(Braintree::Customer.find(vault_id)) if result.success?),
:customer_vault_id => (result.customer.id if result.success?)
)
end
end
def unstore(customer_vault_id)
commit do
Braintree::Customer.delete(customer_vault_id)
Response.new(true, "OK")
end
end
alias_method :delete, :unstore
private
def merge_credit_card_options(parameters, options)
valid_options = {}
options.each do |key, value|
valid_options[key] = value if [:update_existing_token, :verify_card, :verification_merchant_account_id].include?(key)
end
parameters[:credit_card] ||= {}
parameters[:credit_card].merge!(:options => valid_options)
parameters[:credit_card][:billing_address] = map_address(options[:billing_address]) if options[:billing_address]
parameters
end
def map_address(address)
return {} if address.nil?
mapped = {
:street_address => address[:address1],
:extended_address => address[:address2],
:company => address[:company],
:locality => address[:city],
:region => address[:state],
:postal_code => address[:zip],
}
if(address[:country] || address[:country_code_alpha2])
mapped[:country_code_alpha2] = (address[:country] || address[:country_code_alpha2])
elsif address[:country_name]
mapped[:country_name] = address[:country_name]
elsif address[:country_code_alpha3]
mapped[:country_code_alpha3] = address[:country_code_alpha3]
elsif address[:country_code_numeric]
mapped[:country_code_numeric] = address[:country_code_numeric]
end
mapped
end
def commit(&block)
yield
rescue Braintree::BraintreeError => ex
Response.new(false, ex.class.to_s)
end
def message_from_result(result)
if result.success?
"OK"
elsif result.errors.size == 0 && result.credit_card_verification
"Processor declined: #{result.credit_card_verification.processor_response_text} (#{result.credit_card_verification.processor_response_code})"
else
result.errors.map { |e| "#{e.message} (#{e.code})" }.join(" ")
end
end
def create_transaction(transaction_type, money, credit_card_or_vault_id, options)
transaction_params = create_transaction_parameters(money, credit_card_or_vault_id, options)
commit do
result = Braintree::Transaction.send(transaction_type, transaction_params)
response_params, response_options, avs_result, cvv_result = {}, {}, {}, {}
if result.success?
response_params[:braintree_transaction] = transaction_hash(result.transaction)
response_params[:customer_vault_id] = result.transaction.customer_details.id
response_options[:authorization] = result.transaction.id
end
if result.transaction
response_options[:avs_result] = {
:code => nil, :message => nil,
:street_match => result.transaction.avs_street_address_response_code,
:postal_match => result.transaction.avs_postal_code_response_code
}
response_options[:cvv_result] = result.transaction.cvv_response_code
if result.transaction.status == "gateway_rejected"
message = "Transaction declined - gateway rejected"
else
message = "#{result.transaction.processor_response_code} #{result.transaction.processor_response_text}"
end
else
message = message_from_result(result)
end
response = Response.new(result.success?, message, response_params, response_options)
response.cvv_result['message'] = ''
response
end
end
def extract_refund_args(args)
options = args.extract_options!
# money, transaction_id, options
if args.length == 1 # legacy signature
return nil, args[0], options
elsif args.length == 2
return args[0], args[1], options
else
raise ArgumentError, "wrong number of arguments (#{args.length} for 2)"
end
end
def customer_hash(customer)
credit_cards = customer.credit_cards.map do |cc|
{
"bin" => cc.bin,
"expiration_date" => cc.expiration_date,
"token" => cc.token,
"last_4" => cc.last_4,
"card_type" => cc.card_type,
"masked_number" => cc.masked_number
}
end
{
"email" => customer.email,
"first_name" => customer.first_name,
"last_name" => customer.last_name,
"credit_cards" => credit_cards,
"id" => customer.id
}
end
def transaction_hash(transaction)
if transaction.vault_customer
vault_customer = {
}
vault_customer["credit_cards"] = transaction.vault_customer.credit_cards.map do |cc|
{
"bin" => cc.bin
}
end
else
vault_customer = nil
end
customer_details = {
"id" => transaction.customer_details.id,
"email" => transaction.customer_details.email
}
billing_details = {
"street_address" => transaction.billing_details.street_address,
"extended_address" => transaction.billing_details.extended_address,
"company" => transaction.billing_details.company,
"locality" => transaction.billing_details.locality,
"region" => transaction.billing_details.region,
"postal_code" => transaction.billing_details.postal_code,
"country_name" => transaction.billing_details.country_name,
}
shipping_details = {
"street_address" => transaction.shipping_details.street_address,
"extended_address" => transaction.shipping_details.extended_address,
"company" => transaction.shipping_details.company,
"locality" => transaction.shipping_details.locality,
"region" => transaction.shipping_details.region,
"postal_code" => transaction.shipping_details.postal_code,
"country_name" => transaction.shipping_details.country_name,
}
credit_card_details = {
"masked_number" => transaction.credit_card_details.masked_number,
"bin" => transaction.credit_card_details.bin,
"last_4" => transaction.credit_card_details.last_4,
"card_type" => transaction.credit_card_details.card_type,
"token" => transaction.credit_card_details.token
}
{
"order_id" => transaction.order_id,
"status" => transaction.status,
"credit_card_details" => credit_card_details,
"customer_details" => customer_details,
"billing_details" => billing_details,
"shipping_details" => shipping_details,
"vault_customer" => vault_customer,
"merchant_account_id" => transaction.merchant_account_id
}
end
def create_transaction_parameters(money, credit_card_or_vault_id, options)
parameters = {
:amount => amount(money).to_s,
:order_id => options[:order_id],
:customer => {
:id => options[:store] == true ? "" : options[:store],
:email => options[:email]
},
:options => {
:store_in_vault => options[:store] ? true : false,
:submit_for_settlement => options[:submit_for_settlement]
}
}
if merchant_account_id = (options[:merchant_account_id] || @merchant_account_id)
parameters[:merchant_account_id] = merchant_account_id
end
if options[:recurring]
parameters[:recurring] = true
end
if credit_card_or_vault_id.is_a?(String) || credit_card_or_vault_id.is_a?(Integer)
parameters[:customer_id] = credit_card_or_vault_id
else
parameters[:customer].merge!(
:first_name => credit_card_or_vault_id.first_name,
:last_name => credit_card_or_vault_id.last_name
)
parameters[:credit_card] = {
:number => credit_card_or_vault_id.number,
:cvv => credit_card_or_vault_id.verification_value,
:expiration_month => credit_card_or_vault_id.month.to_s.rjust(2, "0"),
:expiration_year => credit_card_or_vault_id.year.to_s
}
end
parameters[:billing] = map_address(options[:billing_address]) if options[:billing_address]
parameters[:shipping] = map_address(options[:shipping_address]) if options[:shipping_address]
parameters
end
end
end
end

View File

@@ -0,0 +1,19 @@
require File.dirname(__FILE__) + '/smart_ps.rb'
require File.dirname(__FILE__) + '/braintree/braintree_common'
module ActiveMerchant #:nodoc:
module Billing #:nodoc:
class BraintreeOrangeGateway < SmartPs
include BraintreeCommon
self.display_name = 'Braintree (Orange Platform)'
self.live_url = self.test_url = 'https://secure.braintreepaymentgateway.com/api/transact.php'
def add_processor(post, options)
post[:processor_id] = options[:processor] unless options[:processor].nil?
end
end
end
end

View File

@@ -0,0 +1,23 @@
module ActiveMerchant #:nodoc:
module Billing #:nodoc:
class CardSaveGateway < IridiumGateway
#CardSave lets you handle failovers on payments by providing 3 gateways in case one happens to be down
#URLS = ['https://gw1.cardsaveonlinepayments.com:4430/','https://gw2.cardsaveonlinepayments.com:4430/','https://gw3.cardsaveonlinepayments.com:4430/']
self.money_format = :cents
self.default_currency = 'GBP'
self.supported_cardtypes = [ :visa, :switch, :maestro, :master, :solo, :american_express, :jcb ]
self.supported_countries = [ 'GB' ]
self.homepage_url = 'http://www.cardsave.net/'
self.display_name = 'CardSave'
def initialize(options={})
super
@test_url = 'https://gw1.cardsaveonlinepayments.com:4430/'
@live_url = 'https://gw1.cardsaveonlinepayments.com:4430/'
end
end
end
end

View File

@@ -0,0 +1,225 @@
module ActiveMerchant #:nodoc:
module Billing #:nodoc:
#
# CardStream supports the following credit cards, which are auto-detected by
# the gateway based on the card number used:
# * AM American Express
# * Diners Club
# * Electron
# * JCB
# * UK Maestro
# * Maestro International
# * Mastercard
# * Solo
# * Style
# * Switch
# * Visa Credit
# * Visa Debit
# * Visa Purchasing
#
class CardStreamGateway < Gateway
self.live_url = self.test_url = 'https://gateway.cardstream.com/process.ashx'
self.money_format = :cents
self.default_currency = 'GBP'
self.supported_countries = ['GB']
self.supported_cardtypes = [:visa, :master, :american_express, :diners_club, :discover, :jcb, :maestro, :solo, :switch]
self.homepage_url = 'http://www.cardstream.com/'
self.display_name = 'CardStream'
APPROVED = '00'
CURRENCY_CODES = {
"AUD"=> '036',
"CAD"=> '124',
"CZK"=> '203',
"DKK"=> '208',
"HKD"=> '344',
"ICK"=> '352',
"JPY"=> '392',
"NOK"=> '578',
"SGD"=> '702',
"SEK"=> '752',
"CHF"=> '756',
"GBP"=> '826',
"USD"=> '840',
"EUR"=> '978'
}
TRANSACTIONS = {
:purchase => 'ESALE_KEYED',
:refund => 'EREFUND_KEYED',
:authorization => 'ESALE_KEYED'
}
CVV_CODE = {
'0' => 'U',
'1' => 'P',
'2' => 'M',
'4' => 'N'
}
# 0 - No additional information available.
# 1 - Postcode not checked.
# 2 - Postcode matched.
# 4 - Postcode not matched.
# 8 - Postcode partially matched.
AVS_POSTAL_MATCH = {
"0" => nil,
"1" => nil,
"2" => "Y",
"4" => "N",
"8" => "N"
}
# 0 - No additional information available.
# 1 - Address numeric not checked.
# 2 - Address numeric matched.
# 4 - Address numeric not matched.
# 8 - Address numeric partially matched.
AVS_STREET_MATCH = {
"0" => nil,
"1" => nil,
"2" => "Y",
"4" => "N",
"8" => "N"
}
def initialize(options = {})
requires!(options, :login, :password)
super
end
def purchase(money, credit_card, options = {})
requires!(options, :order_id)
post = {}
add_amount(post, money, options)
add_invoice(post, money, credit_card, options)
add_credit_card(post, credit_card)
add_address(post, options)
add_customer_data(post, options)
commit(:purchase, post)
end
private
def add_amount(post, money, options)
add_pair(post, :Amount, amount(money), :required => true)
add_pair(post, :CurrencyCode, currency_code(options[:currency] || currency(money)), :required => true)
end
def add_customer_data(post, options)
add_pair(post, :BillingEmail, options[:email])
add_pair(post, :BillingPhoneNumber, options[:phone])
end
def add_address(post, options)
address = options[:billing_address] || options[:address]
return if address.nil?
add_pair(post, :BillingStreet, address[:address1])
add_pair(post, :BillingHouseNumber, address[:address2])
add_pair(post, :BillingCity, address[:city])
add_pair(post, :BillingState, address[:state])
add_pair(post, :BillingPostCode, address[:zip])
end
def add_invoice(post, money, credit_card, options)
add_pair(post, :TransactionUnique, options[:order_id], :required => true)
add_pair(post, :OrderDesc, options[:description] || options[:order_id], :required => true)
if [ 'american_express', 'diners_club' ].include?(card_brand(credit_card).to_s)
add_pair(post, :AEIT1Quantity, 1)
add_pair(post, :AEIT1Description, (options[:description] || options[:order_id]).slice(0, 15))
add_pair(post, :AEIT1GrossValue, amount(money))
end
end
def add_credit_card(post, credit_card)
add_pair(post, :CardName, credit_card.name, :required => true)
add_pair(post, :CardNumber, credit_card.number, :required => true)
add_pair(post, :ExpiryDateMM, format(credit_card.month, :two_digits), :required => true)
add_pair(post, :ExpiryDateYY, format(credit_card.year, :two_digits), :required => true)
if requires_start_date_or_issue_number?(credit_card)
add_pair(post, :StartDateMM, format(credit_card.start_month, :two_digits))
add_pair(post, :StartDateYY, format(credit_card.start_year, :two_digits))
add_pair(post, :IssueNumber, credit_card.issue_number)
end
add_pair(post, :CV2, credit_card.verification_value)
end
def commit(action, parameters)
response = parse( ssl_post(self.live_url, post_data(action, parameters)) )
Response.new(response[:response_code] == APPROVED, message_from(response), response,
:test => test?,
:authorization => response[:cross_reference],
:cvv_result => CVV_CODE[ response[:avscv2_response_code].to_s[0, 1] ],
:avs_result => {
:street_match => AVS_STREET_MATCH[ response[:avscv2_response_code].to_s[2, 1] ],
:postal_match => AVS_POSTAL_MATCH[ response[:avscv2_response_code].to_s[1, 1] ]
}
)
end
def message_from(results)
results[:response_code] == APPROVED ? "APPROVED" : results[:message]
end
def post_data(action, parameters = {})
parameters.update(
:MerchantPassword => @options[:password],
:MerchantID => @options[:login],
:MessageType => TRANSACTIONS[action],
:CallBack => "disable",
:DuplicateDelay => "0",
:EchoCardType => "YES",
:EchoAmount => "YES",
:EchoAVSCV2ResponseCode => "YES",
:ReturnAVSCV2Message => "YES",
:CountryCode => '826' # 826 for UK based merchant
)
add_pair(parameters, :Dispatch, action == :authorization ? "LATER" : "NOW")
parameters.collect { |key, value| "VP#{key}=#{CGI.escape(value.to_s)}" }.join("&")
end
# VPCrossReference
# The value in VPCrossReference on a success transaction will contain
# a unique reference that you may use to run future transactions.
# Please note that cross reference transactions must come a static IP
# addressed that has been pre-registered with Cardstream. To
# register an IP address please send it to support@cardstream.com
# with your Cardstream issued merchant ID and it will be added to
# your account.
def parse(body)
result = {}
pairs = body.split("&")
pairs.each do |pair|
a = pair.split("=")
result[a[0].gsub(/^VP/,'').underscore.to_sym] = a[1]
end
result
end
def currency_code(currency)
CURRENCY_CODES[currency]
end
def add_pair(post, key, value, options = {})
post[key] = value if !value.blank? || options[:required]
end
end
end
end

View File

@@ -0,0 +1,155 @@
module ActiveMerchant #:nodoc:
module Billing #:nodoc:
class CardStreamModernGateway < Gateway
self.test_url = self.live_url = 'https://gateway.cardstream.com/direct/'
self.money_format = :cents
self.default_currency = 'GBP'
self.supported_countries = ['GB']
self.supported_cardtypes = [:visa, :master, :american_express, :diners_club, :discover, :jcb, :maestro, :solo, :switch]
self.homepage_url = 'http://www.cardstream.com/'
self.display_name = 'CardStream'
def initialize(options = {})
requires!(options, :login)
if(options[:threeDSRequired])
@threeDSRequired = options[:threeDSRequired]
else
@threeDSRequired = 'N'
end
super
end
def authorize(money, creditcard, options = {})
post = {}
add_amount(post, money, options)
add_invoice(post, creditcard, money, options)
add_creditcard(post, creditcard)
add_address(post, creditcard, options)
add_customer_data(post, options)
commit('PREAUTH', post)
end
def purchase(money, creditcard, options = {})
post = {}
add_amount(post, money, options)
add_invoice(post, creditcard, money, options)
add_creditcard(post, creditcard)
add_address(post, creditcard, options)
add_customer_data(post, options)
commit('SALE', post)
end
def capture(money, authorization, options = {})
post = {}
add_pair(post, :xref, authorization)
add_amount(post, money, options)
commit('SALE', post)
end
def refund(money, authorization, options = {})
post = {}
add_pair(post, :xref, authorization)
add_amount(post, money, options)
commit('REFUND', post)
end
def void(authorization, options = {})
post = {}
add_pair(post, :xref, authorization)
commit('REFUND', post)
end
private
def add_amount(post, money, options)
add_pair(post, :amount, amount(money), :required => true)
add_pair(post, :currencyCode, options[:currency] || self.default_currency)
end
def add_customer_data(post, options)
address = options[:billing_address] || options[:address]
add_pair(post, :customerPostCode, address[:zip])
add_pair(post, :customerEmail, options[:email])
add_pair(post, :customerPhone, options[:phone])
end
def add_address(post, creditcard, options)
address = options[:billing_address] || options[:address]
return if address.nil?
add_pair(post, :customerAddress, address[:address1] + " " + (address[:address2].nil? ? "" : address[:address2]) )
add_pair(post, :customerPostCode, address[:zip])
end
def add_invoice(post, credit_card, money, options)
add_pair(post, :transactionUnique, options[:order_id], :required => true)
add_pair(post, :orderRef, options[:description] || options[:order_id], :required => true)
if [ 'american_express', 'diners_club' ].include?(card_brand(credit_card).to_s)
add_pair(post, :item1Quantity, 1)
add_pair(post, :item1Description, (options[:description] || options[:order_id]).slice(0, 15))
add_pair(post, :item1GrossValue, amount(money))
end
end
def add_creditcard(post, credit_card)
add_pair(post, :customerName, credit_card.name, :required => true)
add_pair(post, :cardNumber, credit_card.number, :required => true)
add_pair(post, :cardExpiryMonth, format(credit_card.month, :two_digits), :required => true)
add_pair(post, :cardExpiryYear, format(credit_card.year, :two_digits), :required => true)
if requires_start_date_or_issue_number?(credit_card)
add_pair(post, :cardStartMonth, format(credit_card.start_month, :two_digits))
add_pair(post, :cardStartYear, format(credit_card.start_year, :two_digits))
add_pair(post, :cardIssueNumber, credit_card.issue_number)
end
add_pair(post, :cardCVV, credit_card.verification_value)
end
def parse(body)
result = {}
pairs = body.split("&")
pairs.each do |pair|
a = pair.split("=")
result[a[0].to_sym] = CGI.unescape(a[1])
end
result
end
def commit(action, parameters)
response = parse( ssl_post(self.live_url, post_data(action, parameters)) )
Response.new(response[:responseCode] == "0",
response[:responseCode] == "0" ? "APPROVED" : response[:responseMessage],
response,
:test => test?,
:authorization => response[:xref],
:avs_result => {
:street_match => response[:addressCheck],
:postal_match => response[:postcodeCheck],
},
:cvv_result => response[:cv2Check]
)
end
def post_data(action, parameters = {})
parameters.update(
:merchantID => @options[:login],
:action => action,
:type => '1', #Ecommerce
:countryCode => self.supported_countries[0],
:threeDSRequired => @threeDSRequired #Disable 3d secure by default
)
parameters.collect { |key, value| "#{key}=#{CGI.escape(value.to_s)}" }.join("&")
end
def add_pair(post, key, value, options = {})
post[key] = value if !value.blank? || options[:required]
end
end
end
end

View File

@@ -0,0 +1,156 @@
module ActiveMerchant #:nodoc:
module Billing #:nodoc:
# CC5 API is used by many banks in Turkey. Extend this base class to provide
# concrete implementations.
class CC5Gateway < Gateway
self.default_currency = 'TRY'
CURRENCY_CODES = {
'TRY' => 949,
'YTL' => 949,
'TRL' => 949,
'TL' => 949,
'USD' => 840,
'EUR' => 978,
'GBP' => 826,
'JPY' => 392
}
def initialize(options = {})
requires!(options, :login, :password, :client_id)
super
end
def purchase(money, creditcard, options = {})
commit(build_sale_request('Auth', money, creditcard, options))
end
def authorize(money, creditcard, options = {})
commit(build_sale_request('PreAuth', money, creditcard, options))
end
def capture(money, authorization, options = {})
commit(build_capture_request(money, authorization, options))
end
protected
def build_sale_request(type, money, creditcard, options = {})
requires!(options, :order_id)
xml = Builder::XmlMarkup.new :indent => 2
xml.tag! 'CC5Request' do
add_login_tags(xml)
xml.tag! 'OrderId', options[:order_id]
xml.tag! 'Type', type
xml.tag! 'Number', creditcard.number
xml.tag! 'Expires', [format(creditcard.month, :two_digits), format(creditcard.year, :two_digits)].join('/')
xml.tag! 'Cvv2Val', creditcard.verification_value
add_amount_tags(money, options, xml)
xml.tag! 'Email', options[:email] if options[:email]
if(address = (options[:billing_address] || options[:address]))
xml.tag! 'BillTo' do
add_address(xml, address)
end
xml.tag! 'ShipTo' do
add_address(xml, address)
end
end
end
xml.target!
end
def build_capture_request(money, authorization, options = {})
xml = Builder::XmlMarkup.new :indent => 2
xml.tag! 'CC5Request' do
add_login_tags(xml)
xml.tag! 'OrderId', authorization
xml.tag! 'Type', 'PostAuth'
add_amount_tags(money, options, xml)
end
end
def add_address(xml, address)
xml.tag! 'Name', normalize(address[:name])
xml.tag! 'Street1', normalize(address[:address1])
xml.tag! 'Street2', normalize(address[:address2]) if address[:address2]
xml.tag! 'City', normalize(address[:city])
xml.tag! 'PostalCode', address[:zip]
xml.tag! 'Country', normalize(address[:country])
xml.tag! 'Company', normalize(address[:company])
xml.tag! 'TelVoice', address[:phone].to_s.gsub(/[^0-9]/, '') if address[:phone]
end
def add_login_tags(xml)
xml.tag! 'Name', @options[:login]
xml.tag! 'Password', @options[:password]
xml.tag! 'ClientId', @options[:client_id]
xml.tag! 'Mode', (test? ? 'T' : 'P')
end
def add_amount_tags(money, options, xml)
xml.tag! 'Total', amount(money)
xml.tag! 'Currency', currency_code(options[:currency] || currency(money))
end
def currency_code(currency)
(CURRENCY_CODES[currency] || CURRENCY_CODES[default_currency])
end
def commit(request)
raw_response = ssl_post((test? ? self.test_url : self.live_url), "DATA=" + request)
response = parse(raw_response)
success = success?(response)
Response.new(
success,
(success ? 'Approved' : "Declined (Reason: #{response[:proc_return_code]} - #{response[:err_msg]})"),
response,
:test => test?,
:authorization => response[:order_id]
)
end
def parse(body)
xml = REXML::Document.new(body)
response = {}
xml.root.elements.to_a.each do |node|
parse_element(response, node)
end
response
end
def parse_element(response, node)
if node.has_elements?
node.elements.each{|element| parse_element(response, element) }
else
response[node.name.underscore.to_sym] = node.text
end
end
def success?(response)
(response[:response] == "Approved")
end
def normalize(text)
return unless text
if ActiveSupport::Inflector.method(:transliterate).arity == -2
ActiveSupport::Inflector.transliterate(text,'')
elsif RUBY_VERSION >= '1.9'
text.gsub(/[^\x00-\x7F]+/, '')
else
ActiveSupport::Inflector.transliterate(text).to_s
end
end
end
end
end

View File

@@ -0,0 +1,277 @@
module ActiveMerchant #:nodoc:
module Billing #:nodoc:
class CertoDirectGateway < Gateway
self.live_url = self.test_url = "https://secure.certodirect.com/gateway/process/v2"
self.supported_countries = [
"BE", "BG", "CZ", "DK", "DE", "EE", "IE", "EL", "ES", "FR",
"IT", "CY", "LV", "LT", "LU", "HU", "MT", "NL", "AT", "PL",
"PT", "RO", "SI", "SK", "FI", "SE", "GB"
]
self.supported_cardtypes = [:visa, :master, :american_express, :discover]
self.homepage_url = 'http://www.certodirect.com/'
self.display_name = 'CertoDirect'
# Creates a new CertoDirectGateway
#
# The gateway requires that a valid login and password be passed
# in the +options+ hash.
#
# ==== Options
#
# * <tt>:login</tt> -- The CertoDirect Shop ID (REQUIRED)
# * <tt>:password</tt> -- The CertoDirect Shop Password. (REQUIRED)
# * <tt>:test</tt> -- +true+ or +false+. If true, perform transactions against the test server.
# Otherwise, perform transactions against the production server.
def initialize(options = {})
requires!(options, :login, :password)
super
end
# Perform a purchase, which is essentially an authorization and capture in a single operation.
#
# ==== Parameters
#
# * <tt>money</tt> -- The amount to be purchased as an Integer value in cents.
# * <tt>credit_card</tt> -- The CreditCard details for the transaction.
# * <tt>options</tt> -- A hash of optional parameters.
def purchase(money, credit_card, options = {})
requires!(options, :email, :currency, :ip, :description)
commit(build_sale_request(money, credit_card, options))
end
# Refund a transaction.
#
# This transaction indicates to the gateway that
# money should flow from the merchant to the customer.
#
# ==== Parameters
#
# * <tt>money</tt> -- The amount to be credited to the customer as an Integer value in cents.
# * <tt>identification</tt> -- The ID of the original order against which the refund is being issued.
# * <tt>options</tt> -- A hash of parameters.
def refund(money, identification, options = {})
requires!(options, :reason)
commit(build_refund_request(money, identification, options))
end
# Performs an authorization, which reserves the funds on the customer's credit card, but does not
# charge the card.
#
# ==== Parameters
#
# * <tt>money</tt> -- The amount to be authorized as an Integer value in cents.
# * <tt>credit_card</tt> -- The CreditCard details for the transaction.
# * <tt>options</tt> -- A hash of optional parameters.
def authorize(money, credit_card, options = {})
requires!(options, :email, :currency, :ip, :description)
commit(build_authorize_request(money, credit_card, options))
end
# Captures the funds from an authorized transaction.
#
# ==== Parameters
#
# * <tt>money</tt> -- The amount to be captured as an Integer value in cents.
# * <tt>identification</tt> -- The authorization returned from the previous authorize request.
def capture(money, identification, options = {})
commit(build_capture_request(money, identification))
end
# Void a previous transaction
#
# ==== Parameters
#
# * <tt>money</tt> -- The amount to be captured as an Integer value in cents.
# * <tt>identification</tt> - The authorization returned from the previous authorize request.
def void(money, identification, options = {})
commit(build_void_request(money, identification))
end
# Create a recurring payment.
#
# ==== Parameters
#
# * <tt>options</tt> -- A hash of parameters.
#
# ==== Options
#
def recurring(identification, options={})
commit(build_recurring_request(identification, options))
end
private
def commit(request_xml)
begin
response = Hash.from_xml(ssl_post(self.live_url, request_xml, headers))
Response.new(success?(response),
message(response),
response,
:test => test?,
:authorization => authorization(response))
rescue ResponseError => e
raise e unless e.response.code == '403'
response = Hash.from_xml(e.response.body)['response']
Response.new(false, message(response), {}, :test => test?)
end
end
def build_sale_request(money, credit_card, options)
build_request_xml('Sale') do |xml|
add_order(xml, money, credit_card, options)
end
end
def build_authorize_request(money, credit_card, options)
build_request_xml('Authorize') do |xml|
add_order(xml, money, credit_card, options)
end
end
def build_refund_request(money, identification, options)
build_request_xml('Refund') do |xml|
add_reference_info(xml, money, identification, options)
xml.tag! 'reason', options[:reason]
end
end
def build_capture_request(money, identification)
build_request_xml('Capture') do |xml|
add_reference_info(xml, money, identification, options)
end
end
def build_void_request(money, identification)
build_request_xml('Void') do |xml|
add_reference_info(xml, money, identification, options)
end
end
def build_recurring_request(identification, options)
build_request_xml('Sale') do |xml|
xml.tag! 'order' do |xml|
xml.tag!('test', 'true') if test?
xml.tag! 'initial_order_id', identification, :type => 'integer'
add_order_details(xml, options[:amount], options) if has_any_order_details_key?(options)
add_address(xml, 'billing_address', options[:billing_address]) if options[:billing_address]
add_address(xml, 'shipping_address', options[:shipping_address]) if options[:shipping_address]
end
end
end
def build_request_xml(type, &block)
xml = Builder::XmlMarkup.new(:indent => 2)
xml.tag! 'transaction' do
xml.tag! 'type', type
yield(xml)
end
xml.target!
end
def add_order(xml, money, credit_card, options)
xml.tag! 'order' do
xml.tag!('test', 'true') if test?
xml.tag!('return_url', options[:return_url]) if options[:return_url]
xml.tag!('cancel_url', options[:cancel_url]) if options[:cancel_url]
xml.tag! 'payment_method_type', 'CreditCard'
xml.tag! 'payment_method' do
xml.tag! 'number', credit_card.number
xml.tag! 'exp_month', "%02i" % credit_card.month
xml.tag! 'exp_year', credit_card.year
xml.tag! 'holder', credit_card.name
xml.tag! 'verification_value', credit_card.verification_value
end
add_order_details(xml, money, options)
add_address(xml, 'billing_address', options[:billing_address]) if options[:billing_address]
add_address(xml, 'shipping_address', options[:shipping_address]) if options[:shipping_address]
end
end
def add_order_details(xml, money, options)
xml.tag! 'details' do
xml.tag!('amount', localized_amount(money, options[:currency]), :type => 'decimal') if money
xml.tag!('currency', options[:currency]) if options[:currency]
xml.tag!('email', options[:email]) if options[:email]
xml.tag!('ip', options[:ip]) if options[:ip]
xml.tag!('shipping', options[:shipping], :type => 'decimal') if options[:shipping]
xml.tag!('description', options[:description]) if options[:description]
end
end
def add_reference_info(xml, money, identification, options)
xml.tag! 'order_id', identification, :type => 'integer'
xml.tag! 'amount', localized_amount(money, options[:currency]), :type => 'decimal'
end
def add_address(xml, address_type, address)
xml.tag! address_type do
xml.tag! 'address', address[:address1]
xml.tag! 'city', address[:city]
xml.tag! 'country', address[:country]
xml.tag! 'first_name', address[:first_name]
xml.tag! 'last_name', address[:last_name]
xml.tag! 'state', address[:state]
xml.tag! 'phone', address[:phone]
xml.tag! 'zip', address[:zip]
end
end
def has_any_order_details_key?(options)
[ :currency, :amount, :email, :ip, :shipping, :description ].any? do |key|
options.has_key?(key)
end
end
def success?(response)
%w(completed forwarding).include?(state(response)) and
status(response) == 'success'
end
def error?(response)
response['errors']
end
def state(response)
response["transaction"].try(:[], "state")
end
def status(response)
response['transaction'].try(:[], 'response').try(:[], 'status')
end
def authorization(response)
error?(response) ? nil : response["transaction"]["order"]['id'].to_s
end
def message(response)
return response['errors'].join('; ') if error?(response)
if state(response) == 'completed'
response["transaction"]["response"]["message"]
else
response['transaction']['message']
end
end
def headers
{ 'authorization' => basic_auth,
'Accept' => 'application/xml',
'Content-Type' => 'application/xml' }
end
def basic_auth
'Basic ' + ["#{@options[:login]}:#{@options[:password]}"].pack('m').delete("\r\n")
end
end
end
end

View File

@@ -0,0 +1,614 @@
module ActiveMerchant #:nodoc:
module Billing #:nodoc:
# See the remote and mocked unit test files for example usage. Pay special
# attention to the contents of the options hash.
#
# Initial setup instructions can be found in
# http://cybersource.com/support_center/implementation/downloads/soap_api/SOAP_toolkits.pdf
#
# Debugging
# If you experience an issue with this gateway be sure to examine the
# transaction information from a general transaction search inside the
# CyberSource Business Center for the full error messages including field
# names.
#
# Important Notes
# * For checks you can purchase and store.
# * AVS and CVV only work against the production server. You will always
# get back X for AVS and no response for CVV against the test server.
# * Nexus is the list of states or provinces where you have a physical
# presence. Nexus is used to calculate tax. Leave blank to tax everyone.
# * If you want to calculate VAT for overseas customers you must supply a
# registration number in the options hash as vat_reg_number.
# * productCode is a value in the line_items hash that is used to tell
# CyberSource what kind of item you are selling. It is used when
# calculating tax/VAT.
# * All transactions use dollar values.
class CyberSourceGateway < Gateway
self.test_url = 'https://ics2wstest.ic3.com/commerce/1.x/transactionProcessor'
self.live_url = 'https://ics2ws.ic3.com/commerce/1.x/transactionProcessor'
XSD_VERSION = "1.69"
# visa, master, american_express, discover
self.supported_cardtypes = [:visa, :master, :american_express, :discover]
self.supported_countries = ['US']
self.default_currency = 'USD'
self.homepage_url = 'http://www.cybersource.com'
self.display_name = 'CyberSource'
# map credit card to the CyberSource expected representation
@@credit_card_codes = {
:visa => '001',
:master => '002',
:american_express => '003',
:discover => '004'
}
# map response codes to something humans can read
@@response_codes = {
:r100 => "Successful transaction",
:r101 => "Request is missing one or more required fields" ,
:r102 => "One or more fields contains invalid data",
:r150 => "General failure",
:r151 => "The request was received but a server time-out occurred",
:r152 => "The request was received, but a service timed out",
:r200 => "The authorization request was approved by the issuing bank but declined by CyberSource because it did not pass the AVS check",
:r201 => "The issuing bank has questions about the request",
:r202 => "Expired card",
:r203 => "General decline of the card",
:r204 => "Insufficient funds in the account",
:r205 => "Stolen or lost card",
:r207 => "Issuing bank unavailable",
:r208 => "Inactive card or card not authorized for card-not-present transactions",
:r209 => "American Express Card Identifiction Digits (CID) did not match",
:r210 => "The card has reached the credit limit",
:r211 => "Invalid card verification number",
:r221 => "The customer matched an entry on the processor's negative file",
:r230 => "The authorization request was approved by the issuing bank but declined by CyberSource because it did not pass the card verification check",
:r231 => "Invalid account number",
:r232 => "The card type is not accepted by the payment processor",
:r233 => "General decline by the processor",
:r234 => "A problem exists with your CyberSource merchant configuration",
:r235 => "The requested amount exceeds the originally authorized amount",
:r236 => "Processor failure",
:r237 => "The authorization has already been reversed",
:r238 => "The authorization has already been captured",
:r239 => "The requested transaction amount must match the previous transaction amount",
:r240 => "The card type sent is invalid or does not correlate with the credit card number",
:r241 => "The request ID is invalid",
:r242 => "You requested a capture, but there is no corresponding, unused authorization record.",
:r243 => "The transaction has already been settled or reversed",
:r244 => "The bank account number failed the validation check",
:r246 => "The capture or credit is not voidable because the capture or credit information has already been submitted to your processor",
:r247 => "You requested a credit for a capture that was previously voided",
:r250 => "The request was received, but a time-out occurred with the payment processor",
:r254 => "Your CyberSource account is prohibited from processing stand-alone refunds",
:r255 => "Your CyberSource account is not configured to process the service in the country you specified"
}
# These are the options that can be used when creating a new CyberSource
# Gateway object.
#
# :login => your username
#
# :password => the transaction key you generated in the Business Center
#
# :test => true sets the gateway to test mode
#
# :vat_reg_number => your VAT registration number
#
# :nexus => "WI CA QC" sets the states/provinces where you have a physical
# presense for tax purposes
#
# :ignore_avs => true don't want to use AVS so continue processing even
# if AVS would have failed
#
# :ignore_cvv => true don't want to use CVV so continue processing even
# if CVV would have failed
def initialize(options = {})
requires!(options, :login, :password)
super
end
# Request an authorization for an amount from CyberSource
#
# You must supply an :order_id in the options hash
def authorize(money, creditcard_or_reference, options = {})
requires!(options, :order_id)
setup_address_hash(options)
commit(build_auth_request(money, creditcard_or_reference, options), options )
end
def auth_reversal(money, identification, options = {})
commit(build_auth_reversal_request(money, identification, options), options)
end
# Capture an authorization that has previously been requested
def capture(money, authorization, options = {})
setup_address_hash(options)
commit(build_capture_request(money, authorization, options), options)
end
# Purchase is an auth followed by a capture
# You must supply an order_id in the options hash
def purchase(money, payment_method_or_reference, options = {})
requires!(options, :order_id)
setup_address_hash(options)
commit(build_purchase_request(money, payment_method_or_reference, options), options)
end
def void(identification, options = {})
commit(build_void_request(identification, options), options)
end
def refund(money, identification, options = {})
commit(build_refund_request(money, identification, options), options)
end
# Adds credit to a subscription (stand alone credit).
def credit(money, reference, options = {})
requires!(options, :order_id)
commit(build_credit_request(money, reference, options), options)
end
# Stores a customer subscription/profile with type "on-demand".
# To charge the card while creating a profile, pass
# options[:setup_fee] => money
def store(payment_method, options = {})
requires!(options, :order_id)
setup_address_hash(options)
commit(build_create_subscription_request(payment_method, options), options)
end
# Updates a customer subscription/profile
def update(reference, creditcard, options = {})
requires!(options, :order_id)
setup_address_hash(options)
commit(build_update_subscription_request(reference, creditcard, options), options)
end
# Removes a customer subscription/profile
def unstore(reference, options = {})
requires!(options, :order_id)
commit(build_delete_subscription_request(reference, options), options)
end
# Retrieves a customer subscription/profile
def retrieve(reference, options = {})
requires!(options, :order_id)
commit(build_retrieve_subscription_request(reference, options), options)
end
# CyberSource requires that you provide line item information for tax
# calculations. If you do not have prices for each item or want to
# simplify the situation then pass in one fake line item that costs the
# subtotal of the order
#
# The line_item hash goes in the options hash and should look like
#
# :line_items => [
# {
# :declared_value => '1',
# :quantity => '2',
# :code => 'default',
# :description => 'Giant Walrus',
# :sku => 'WA323232323232323'
# },
# {
# :declared_value => '6',
# :quantity => '1',
# :code => 'default',
# :description => 'Marble Snowcone',
# :sku => 'FAKE1232132113123'
# }
# ]
#
# This functionality is only supported by this particular gateway may
# be changed at any time
def calculate_tax(creditcard, options)
requires!(options, :line_items)
setup_address_hash(options)
commit(build_tax_calculation_request(creditcard, options), options)
end
private
# Create all address hash key value pairs so that we still function if we
# were only provided with one or two of them
def setup_address_hash(options)
options[:billing_address] = options[:billing_address] || options[:address] || {}
options[:shipping_address] = options[:shipping_address] || {}
end
def build_auth_request(money, creditcard_or_reference, options)
xml = Builder::XmlMarkup.new :indent => 2
add_payment_method_or_subscription(xml, money, creditcard_or_reference, options)
add_auth_service(xml)
add_business_rules_data(xml)
xml.target!
end
def build_tax_calculation_request(creditcard, options)
xml = Builder::XmlMarkup.new :indent => 2
add_address(xml, creditcard, options[:billing_address], options, false)
add_address(xml, creditcard, options[:shipping_address], options, true)
add_line_item_data(xml, options)
add_purchase_data(xml, 0, false, options)
add_tax_service(xml)
add_business_rules_data(xml)
xml.target!
end
def build_capture_request(money, authorization, options)
order_id, request_id, request_token = authorization.split(";")
options[:order_id] = order_id
xml = Builder::XmlMarkup.new :indent => 2
add_purchase_data(xml, money, true, options)
add_capture_service(xml, request_id, request_token)
add_business_rules_data(xml)
xml.target!
end
def build_purchase_request(money, payment_method_or_reference, options)
xml = Builder::XmlMarkup.new :indent => 2
add_payment_method_or_subscription(xml, money, payment_method_or_reference, options)
if !payment_method_or_reference.is_a?(String) && card_brand(payment_method_or_reference) == 'check'
add_check_service(xml)
else
add_purchase_service(xml, options)
add_business_rules_data(xml)
end
xml.target!
end
def build_void_request(identification, options)
order_id, request_id, request_token = identification.split(";")
options[:order_id] = order_id
xml = Builder::XmlMarkup.new :indent => 2
add_void_service(xml, request_id, request_token)
xml.target!
end
def build_auth_reversal_request(money, identification, options)
order_id, request_id, request_token = identification.split(";")
options[:order_id] = order_id
xml = Builder::XmlMarkup.new :indent => 2
add_purchase_data(xml, money, true, options)
add_auth_reversal_service(xml, request_id, request_token)
xml.target!
end
def build_refund_request(money, identification, options)
order_id, request_id, request_token = identification.split(";")
options[:order_id] = order_id
xml = Builder::XmlMarkup.new :indent => 2
add_purchase_data(xml, money, true, options)
add_credit_service(xml, request_id, request_token)
xml.target!
end
def build_credit_request(money, reference, options)
xml = Builder::XmlMarkup.new :indent => 2
add_purchase_data(xml, money, true, options)
add_subscription(xml, options, reference)
add_credit_service(xml)
xml.target!
end
def build_create_subscription_request(payment_method, options)
options[:subscription] = (options[:subscription] || {}).merge(:frequency => "on-demand", :amount => 0, :automatic_renew => false)
xml = Builder::XmlMarkup.new :indent => 2
add_address(xml, payment_method, options[:billing_address], options)
add_purchase_data(xml, options[:setup_fee] || 0, true, options)
if card_brand(payment_method) == 'check'
add_check(xml, payment_method)
add_check_payment_method(xml)
add_check_service(xml, options) if options[:setup_fee]
else
add_creditcard(xml, payment_method)
add_creditcard_payment_method(xml)
add_purchase_service(xml, options) if options[:setup_fee]
end
add_subscription(xml, options)
add_subscription_create_service(xml, options)
add_business_rules_data(xml)
xml.target!
end
def build_update_subscription_request(reference, creditcard, options)
xml = Builder::XmlMarkup.new :indent => 2
add_address(xml, creditcard, options[:billing_address], options) unless options[:billing_address].blank?
add_purchase_data(xml, options[:setup_fee], true, options) unless options[:setup_fee].blank?
add_creditcard(xml, creditcard) if creditcard
add_creditcard_payment_method(xml) if creditcard
add_subscription(xml, options, reference)
add_subscription_update_service(xml, options)
add_business_rules_data(xml)
xml.target!
end
def build_delete_subscription_request(reference, options)
xml = Builder::XmlMarkup.new :indent => 2
add_subscription(xml, options, reference)
add_subscription_delete_service(xml, options)
xml.target!
end
def build_retrieve_subscription_request(reference, options)
xml = Builder::XmlMarkup.new :indent => 2
add_subscription(xml, options, reference)
add_subscription_retrieve_service(xml, options)
xml.target!
end
def add_business_rules_data(xml)
xml.tag! 'businessRules' do
xml.tag!('ignoreAVSResult', 'true') if @options[:ignore_avs]
xml.tag!('ignoreCVResult', 'true') if @options[:ignore_cvv]
end
end
def add_line_item_data(xml, options)
options[:line_items].each_with_index do |value, index|
xml.tag! 'item', {'id' => index} do
xml.tag! 'unitPrice', amount(value[:declared_value])
xml.tag! 'quantity', value[:quantity]
xml.tag! 'productCode', value[:code] || 'shipping_only'
xml.tag! 'productName', value[:description]
xml.tag! 'productSKU', value[:sku]
end
end
end
def add_merchant_data(xml, options)
xml.tag! 'merchantID', @options[:login]
xml.tag! 'merchantReferenceCode', options[:order_id]
xml.tag! 'clientLibrary' ,'Ruby Active Merchant'
xml.tag! 'clientLibraryVersion', VERSION
xml.tag! 'clientEnvironment' , RUBY_PLATFORM
end
def add_purchase_data(xml, money = 0, include_grand_total = false, options={})
xml.tag! 'purchaseTotals' do
xml.tag! 'currency', options[:currency] || currency(money)
xml.tag!('grandTotalAmount', amount(money)) if include_grand_total
end
end
def add_address(xml, payment_method, address, options, shipTo = false)
requires!(options, :email)
xml.tag! shipTo ? 'shipTo' : 'billTo' do
xml.tag! 'firstName', payment_method.first_name if payment_method
xml.tag! 'lastName', payment_method.last_name if payment_method
xml.tag! 'street1', address[:address1]
xml.tag! 'street2', address[:address2] unless address[:address2].blank?
xml.tag! 'city', address[:city]
xml.tag! 'state', address[:state]
xml.tag! 'postalCode', address[:zip]
xml.tag! 'country', address[:country]
xml.tag! 'company', address[:company] unless address[:company].blank?
xml.tag! 'companyTaxID', address[:companyTaxID] unless address[:company_tax_id].blank?
xml.tag! 'phoneNumber', address[:phone_number] unless address[:phone_number].blank?
xml.tag! 'email', options[:email]
xml.tag! 'driversLicenseNumber', options[:drivers_license_number] unless options[:drivers_license_number].blank?
xml.tag! 'driversLicenseState', options[:drivers_license_state] unless options[:drivers_license_state].blank?
end
end
def add_creditcard(xml, creditcard)
xml.tag! 'card' do
xml.tag! 'accountNumber', creditcard.number
xml.tag! 'expirationMonth', format(creditcard.month, :two_digits)
xml.tag! 'expirationYear', format(creditcard.year, :four_digits)
xml.tag!('cvNumber', creditcard.verification_value) unless (@options[:ignore_cvv] || creditcard.verification_value.blank? )
xml.tag! 'cardType', @@credit_card_codes[card_brand(creditcard).to_sym]
end
end
def add_check(xml, check)
xml.tag! 'check' do
xml.tag! 'accountNumber', check.account_number
xml.tag! 'accountType', check.account_type[0]
xml.tag! 'bankTransitNumber', check.routing_number
end
end
def add_tax_service(xml)
xml.tag! 'taxService', {'run' => 'true'} do
xml.tag!('nexus', @options[:nexus]) unless @options[:nexus].blank?
xml.tag!('sellerRegistration', @options[:vat_reg_number]) unless @options[:vat_reg_number].blank?
end
end
def add_auth_service(xml)
xml.tag! 'ccAuthService', {'run' => 'true'}
end
def add_capture_service(xml, request_id, request_token)
xml.tag! 'ccCaptureService', {'run' => 'true'} do
xml.tag! 'authRequestID', request_id
xml.tag! 'authRequestToken', request_token
end
end
def add_purchase_service(xml, options)
xml.tag! 'ccAuthService', {'run' => 'true'}
xml.tag! 'ccCaptureService', {'run' => 'true'}
end
def add_void_service(xml, request_id, request_token)
xml.tag! 'voidService', {'run' => 'true'} do
xml.tag! 'voidRequestID', request_id
xml.tag! 'voidRequestToken', request_token
end
end
def add_auth_reversal_service(xml, request_id, request_token)
xml.tag! 'ccAuthReversalService', {'run' => 'true'} do
xml.tag! 'authRequestID', request_id
xml.tag! 'authRequestToken', request_token
end
end
def add_credit_service(xml, request_id = nil, request_token = nil)
xml.tag! 'ccCreditService', {'run' => 'true'} do
xml.tag! 'captureRequestID', request_id if request_id
xml.tag! 'captureRequestToken', request_token if request_token
end
end
def add_check_service(xml)
xml.tag! 'ecDebitService', {'run' => 'true'}
end
def add_subscription_create_service(xml, options)
xml.tag! 'paySubscriptionCreateService', {'run' => 'true'}
end
def add_subscription_update_service(xml, options)
xml.tag! 'paySubscriptionUpdateService', {'run' => 'true'}
end
def add_subscription_delete_service(xml, options)
xml.tag! 'paySubscriptionDeleteService', {'run' => 'true'}
end
def add_subscription_retrieve_service(xml, options)
xml.tag! 'paySubscriptionRetrieveService', {'run' => 'true'}
end
def add_subscription(xml, options, reference = nil)
options[:subscription] ||= {}
xml.tag! 'recurringSubscriptionInfo' do
if reference
_, subscription_id, _ = reference.split(";")
xml.tag! 'subscriptionID', subscription_id
end
xml.tag! 'status', options[:subscription][:status] if options[:subscription][:status]
xml.tag! 'amount', options[:subscription][:amount] if options[:subscription][:amount]
xml.tag! 'numberOfPayments', options[:subscription][:occurrences] if options[:subscription][:occurrences]
xml.tag! 'automaticRenew', options[:subscription][:automatic_renew] if options[:subscription][:automatic_renew]
xml.tag! 'frequency', options[:subscription][:frequency] if options[:subscription][:frequency]
xml.tag! 'startDate', options[:subscription][:start_date].strftime("%Y%m%d") if options[:subscription][:start_date]
xml.tag! 'endDate', options[:subscription][:end_date].strftime("%Y%m%d") if options[:subscription][:end_date]
xml.tag! 'approvalRequired', options[:subscription][:approval_required] || false
xml.tag! 'event', options[:subscription][:event] if options[:subscription][:event]
xml.tag! 'billPayment', options[:subscription][:bill_payment] if options[:subscription][:bill_payment]
end
end
def add_creditcard_payment_method(xml)
xml.tag! 'subscription' do
xml.tag! 'paymentMethod', "credit card"
end
end
def add_check_payment_method(xml)
xml.tag! 'subscription' do
xml.tag! 'paymentMethod', "check"
end
end
def add_payment_method_or_subscription(xml, money, payment_method_or_reference, options)
if payment_method_or_reference.is_a?(String)
add_purchase_data(xml, money, true, options)
add_subscription(xml, options, payment_method_or_reference)
elsif card_brand(payment_method_or_reference) == 'check'
add_address(xml, payment_method_or_reference, options[:billing_address], options)
add_purchase_data(xml, money, true, options)
add_check(xml, payment_method_or_reference)
else
add_address(xml, payment_method_or_reference, options[:billing_address], options)
add_purchase_data(xml, money, true, options)
add_creditcard(xml, payment_method_or_reference)
end
end
# Where we actually build the full SOAP request using builder
def build_request(body, options)
xml = Builder::XmlMarkup.new :indent => 2
xml.instruct!
xml.tag! 's:Envelope', {'xmlns:s' => 'http://schemas.xmlsoap.org/soap/envelope/'} do
xml.tag! 's:Header' do
xml.tag! 'wsse:Security', {'s:mustUnderstand' => '1', 'xmlns:wsse' => 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd'} do
xml.tag! 'wsse:UsernameToken' do
xml.tag! 'wsse:Username', @options[:login]
xml.tag! 'wsse:Password', @options[:password], 'Type' => 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText'
end
end
end
xml.tag! 's:Body', {'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance', 'xmlns:xsd' => 'http://www.w3.org/2001/XMLSchema'} do
xml.tag! 'requestMessage', {'xmlns' => "urn:schemas-cybersource-com:transaction-data-#{XSD_VERSION}"} do
add_merchant_data(xml, options)
xml << body
end
end
end
xml.target!
end
# Contact CyberSource, make the SOAP request, and parse the reply into a
# Response object
def commit(request, options)
response = parse(ssl_post(test? ? self.test_url : self.live_url, build_request(request, options)))
success = response[:decision] == "ACCEPT"
message = @@response_codes[('r' + response[:reasonCode]).to_sym] rescue response[:message]
authorization = success ? [ options[:order_id], response[:requestID], response[:requestToken] ].compact.join(";") : nil
Response.new(success, message, response,
:test => test?,
:authorization => authorization,
:avs_result => { :code => response[:avsCode] },
:cvv_result => response[:cvCode]
)
end
# Parse the SOAP response
# Technique inspired by the Paypal Gateway
def parse(xml)
reply = {}
xml = REXML::Document.new(xml)
if root = REXML::XPath.first(xml, "//c:replyMessage")
root.elements.to_a.each do |node|
case node.name
when 'c:reasonCode'
reply[:message] = reply(node.text)
else
parse_element(reply, node)
end
end
elsif root = REXML::XPath.first(xml, "//soap:Fault")
parse_element(reply, root)
reply[:message] = "#{reply[:faultcode]}: #{reply[:faultstring]}"
end
return reply
end
def parse_element(reply, node)
if node.has_elements?
node.elements.each{|e| parse_element(reply, e) }
else
if node.parent.name =~ /item/
parent = node.parent.name + (node.parent.attributes["id"] ? "_" + node.parent.attributes["id"] : '')
reply[(parent + '_' + node.name).to_sym] = node.text
else
reply[node.name.to_sym] = node.text
end
end
return reply
end
end
end
end

View File

@@ -0,0 +1,591 @@
module ActiveMerchant
module Billing
class DataCashGateway < Gateway
self.default_currency = 'GBP'
self.supported_countries = ['GB']
# From the DataCash docs; Page 13, the following cards are
# usable:
# American Express, ATM, Carte Blanche, Diners Club, Discover,
# EnRoute, GE Capital, JCB, Laser, Maestro, Mastercard, Solo,
# Switch, Visa, Visa Delta, VISA Electron, Visa Purchasing
#
# Note continuous authority is only supported for :visa, :master and :american_express card types
self.supported_cardtypes = [ :visa, :master, :american_express, :discover, :diners_club, :jcb, :maestro, :switch, :solo, :laser ]
self.homepage_url = 'http://www.datacash.com/'
self.display_name = 'DataCash'
# Datacash server URLs
self.test_url = 'https://testserver.datacash.com/Transaction'
self.live_url = 'https://mars.transaction.datacash.com/Transaction'
# Different Card Transaction Types
AUTH_TYPE = 'auth'
CANCEL_TYPE = 'cancel'
FULFILL_TYPE = 'fulfill'
PRE_TYPE = 'pre'
REFUND_TYPE = 'refund'
TRANSACTION_REFUND_TYPE = 'txn_refund'
# Constant strings for use in the ExtendedPolicy complex element for
# CV2 checks
POLICY_ACCEPT = 'accept'
POLICY_REJECT = 'reject'
# Datacash success code
DATACASH_SUCCESS = '1'
# Creates a new DataCashGateway
#
# The gateway requires that a valid login and password be passed
# in the +options+ hash.
#
# ==== Options
#
# * <tt>:login</tt> -- The Datacash account login.
# * <tt>:password</tt> -- The Datacash account password.
# * <tt>:test => +true+ or +false+</tt> -- Use the test or live Datacash url.
#
def initialize(options = {})
requires!(options, :login, :password)
super
end
# Perform a purchase, which is essentially an authorization and capture in a single operation.
#
# ==== Parameters
# * <tt>money</tt> The amount to be authorized as an Integer value in cents.
# * <tt>authorization_or_credit_card</tt>:: The continuous authority reference or CreditCard details for the transaction.
# * <tt>options</tt> A hash of optional parameters.
# * <tt>:order_id</tt> A unique reference for this order (corresponds to merchantreference in datacash documentation)
# * <tt>:set_up_continuous_authority</tt>
# Set to true to set up a recurring historic transaction account be set up.
# Only supported for :visa, :master and :american_express card types
# See http://www.datacash.com/services/recurring/historic.php for more details of historic transactions.
# * <tt>:address</tt>:: billing address for card
#
# The continuous authority reference will be available in response#params['ca_reference'] if you have requested one
def purchase(money, authorization_or_credit_card, options = {})
requires!(options, :order_id)
if authorization_or_credit_card.is_a?(String)
request = build_purchase_or_authorization_request_with_continuous_authority_reference_request(AUTH_TYPE, money, authorization_or_credit_card, options)
else
request = build_purchase_or_authorization_request_with_credit_card_request(AUTH_TYPE, money, authorization_or_credit_card, options)
end
commit(request)
end
# Performs an authorization, which reserves the funds on the customer's credit card, but does not
# charge the card.
#
# ==== Parameters
#
# * <tt>money</tt> The amount to be authorized as an Integer value in cents.
# * <tt>authorization_or_credit_card</tt>:: The continuous authority reference or CreditCard details for the transaction.
# * <tt>options</tt> A hash of optional parameters.
# * <tt>:order_id</tt> A unique reference for this order (corresponds to merchantreference in datacash documentation)
# * <tt>:set_up_continuous_authority</tt>::
# Set to true to set up a recurring historic transaction account be set up.
# Only supported for :visa, :master and :american_express card types
# See http://www.datacash.com/services/recurring/historic.php for more details of historic transactions.
# * <tt>:address</tt>:: billing address for card
#
# The continuous authority reference will be available in response#params['ca_reference'] if you have requested one
def authorize(money, authorization_or_credit_card, options = {})
requires!(options, :order_id)
if authorization_or_credit_card.is_a?(String)
request = build_purchase_or_authorization_request_with_continuous_authority_reference_request(AUTH_TYPE, money, authorization_or_credit_card, options)
else
request = build_purchase_or_authorization_request_with_credit_card_request(PRE_TYPE, money, authorization_or_credit_card, options)
end
commit(request)
end
# Captures the funds from an authorized transaction.
#
# ==== Parameters
#
# * <tt>money</tt> -- The amount to be captured as anInteger value in cents.
# * <tt>authorization</tt> -- The authorization returned from the previous authorize request.
def capture(money, authorization, options = {})
commit(build_void_or_capture_request(FULFILL_TYPE, money, authorization, options))
end
# Void a previous transaction
#
# ==== Parameters
#
# * <tt>authorization</tt> - The authorization returned from the previous authorize request.
def void(authorization, options = {})
request = build_void_or_capture_request(CANCEL_TYPE, nil, authorization, options)
commit(request)
end
# Refund to a card
#
# ==== Parameters
#
# * <tt>money</tt> The amount to be refunded as an Integer value in cents. Set to nil for a full refund on existing transaction.
# * <tt>reference_or_credit_card</tt> The credit card you want to refund OR the datacash_reference for the existing transaction you are refunding
# * <tt>options</tt> Are ignored when refunding via reference to an existing transaction, otherwise
# * <tt>:order_id</tt> A unique reference for this order (corresponds to merchantreference in datacash documentation)
# * <tt>:address</tt>:: billing address for card
def credit(money, reference_or_credit_card, options = {})
if reference_or_credit_card.is_a?(String)
deprecated CREDIT_DEPRECATION_MESSAGE
refund(money, reference_or_credit_card)
else
request = build_refund_request(money, reference_or_credit_card, options)
commit(request)
end
end
def refund(money, reference, options = {})
commit(build_transaction_refund_request(money, reference))
end
private
# Create the xml document for a 'cancel' or 'fulfill' transaction.
#
# Final XML should look like:
# <Request>
# <Authentication>
# <client>99000001</client>
# <password>******</password>
# </Authentication>
# <Transaction>
# <TxnDetails>
# <amount>25.00</amount>
# </TxnDetails>
# <HistoricTxn>
# <reference>4900200000000001</reference>
# <authcode>A6</authcode>
# <method>fulfill</method>
# </HistoricTxn>
# </Transaction>
# </Request>
#
# Parameters:
# * <tt>type</tt> must be FULFILL_TYPE or CANCEL_TYPE
# * <tt>money</tt> - optional - Integer value in cents
# * <tt>authorization</tt> - the Datacash authorization from a previous succesful authorize transaction
# * <tt>options</tt>
# * <tt>order_id</tt> - A unique reference for the transaction
#
# Returns:
# -Builder xml document
#
def build_void_or_capture_request(type, money, authorization, options)
reference, auth_code, ca_reference = authorization.to_s.split(';')
xml = Builder::XmlMarkup.new :indent => 2
xml.instruct!
xml.tag! :Request do
add_authentication(xml)
xml.tag! :Transaction do
xml.tag! :HistoricTxn do
xml.tag! :reference, reference
xml.tag! :authcode, auth_code
xml.tag! :method, type
end
if money
xml.tag! :TxnDetails do
xml.tag! :merchantreference, format_reference_number(options[:order_id])
xml.tag! :amount, amount(money), :currency => options[:currency] || currency(money)
end
end
end
end
xml.target!
end
# Create the xml document for an 'auth' or 'pre' transaction with a credit card
#
# Final XML should look like:
#
# <Request>
# <Authentication>
# <client>99000000</client>
# <password>*******</password>
# </Authentication>
# <Transaction>
# <TxnDetails>
# <merchantreference>123456</merchantreference>
# <amount currency="EUR">10.00</amount>
# </TxnDetails>
# <CardTxn>
# <Card>
# <pan>4444********1111</pan>
# <expirydate>03/04</expirydate>
# <Cv2Avs>
# <street_address1>Flat 7</street_address1>
# <street_address2>89 Jumble
# Street</street_address2>
# <street_address3>Mytown</street_address3>
# <postcode>AV12FR</postcode>
# <cv2>123</cv2>
# <ExtendedPolicy>
# <cv2_policy notprovided="reject"
# notchecked="accept"
# matched="accept"
# notmatched="reject"
# partialmatch="reject"/>
# <postcode_policy notprovided="reject"
# notchecked="accept"
# matched="accept"
# notmatched="reject"
# partialmatch="accept"/>
# <address_policy notprovided="reject"
# notchecked="accept"
# matched="accept"
# notmatched="reject"
# partialmatch="accept"/>
# </ExtendedPolicy>
# </Cv2Avs>
# </Card>
# <method>auth</method>
# </CardTxn>
# </Transaction>
# </Request>
#
# Parameters:
# -type must be 'auth' or 'pre'
# -money - A money object with the price and currency
# -credit_card - The credit_card details to use
# -options:
# :order_id is the merchant reference number
# :billing_address is the billing address for the cc
# :address is the delivery address
#
# Returns:
# -xml: Builder document containing the markup
#
def build_purchase_or_authorization_request_with_credit_card_request(type, money, credit_card, options)
xml = Builder::XmlMarkup.new :indent => 2
xml.instruct!
xml.tag! :Request do
add_authentication(xml)
xml.tag! :Transaction do
if options[:set_up_continuous_authority]
xml.tag! :ContAuthTxn, :type => 'setup'
end
xml.tag! :CardTxn do
xml.tag! :method, type
add_credit_card(xml, credit_card, options[:billing_address])
end
xml.tag! :TxnDetails do
xml.tag! :merchantreference, format_reference_number(options[:order_id])
xml.tag! :amount, amount(money), :currency => options[:currency] || currency(money)
end
end
end
xml.target!
end
# Create the xml document for an 'auth' or 'pre' transaction with
# continuous authorization
#
# Final XML should look like:
#
# <Request>
# <Transaction>
# <ContAuthTxn type="historic" />
# <TxnDetails>
# <merchantreference>3851231</merchantreference>
# <capturemethod>cont_auth</capturemethod>
# <amount currency="GBP">18.50</amount>
# </TxnDetails>
# <HistoricTxn>
# <reference>4500200040925092</reference>
# <method>auth</method>
# </HistoricTxn>
# </Transaction>
# <Authentication>
# <client>99000001</client>
# <password>mypasswd</password>
# </Authentication>
# </Request>
#
# Parameters:
# -type must be 'auth' or 'pre'
# -money - A money object with the price and currency
# -authorization - The authorization containing a continuous authority reference previously set up on a credit card
# -options:
# :order_id is the merchant reference number
#
# Returns:
# -xml: Builder document containing the markup
#
def build_purchase_or_authorization_request_with_continuous_authority_reference_request(type, money, authorization, options)
reference, auth_code, ca_reference = authorization.to_s.split(';')
raise ArgumentError, "The continuous authority reference is required for continuous authority transactions" if ca_reference.blank?
xml = Builder::XmlMarkup.new :indent => 2
xml.instruct!
xml.tag! :Request do
add_authentication(xml)
xml.tag! :Transaction do
xml.tag! :ContAuthTxn, :type => 'historic'
xml.tag! :HistoricTxn do
xml.tag! :reference, ca_reference
xml.tag! :method, type
end
xml.tag! :TxnDetails do
xml.tag! :merchantreference, format_reference_number(options[:order_id])
xml.tag! :amount, amount(money), :currency => options[:currency] || currency(money)
xml.tag! :capturemethod, 'cont_auth'
end
end
end
xml.target!
end
# Create the xml document for a full or partial refund transaction with
#
# Final XML should look like:
#
# <Request>
# <Authentication>
# <client>99000001</client>
# <password>*******</password>
# </Authentication>
# <Transaction>
# <HistoricTxn>
# <method>txn_refund</method>
# <reference>12345678</reference>
# </HistoricTxn>
# <TxnDetails>
# <amount>10.00</amount>
# </TxnDetails>
# </Transaction>
# </Request>
#
def build_transaction_refund_request(money, reference)
xml = Builder::XmlMarkup.new :indent => 2
xml.instruct!
xml.tag! :Request do
add_authentication(xml)
xml.tag! :Transaction do
xml.tag! :HistoricTxn do
xml.tag! :reference, reference
xml.tag! :method, TRANSACTION_REFUND_TYPE
end
unless money.nil?
xml.tag! :TxnDetails do
xml.tag! :amount, amount(money)
end
end
end
end
xml.target!
end
# Create the xml document for a full or partial refund with
#
# Final XML should look like:
#
# <Request>
# <Authentication>
# <client>99000001</client>
# <password>*****</password>
# </Authentication>
# <Transaction>
# <CardTxn>
# <Card>
# <pan>633300*********1</pan>
# <expirydate>04/06</expirydate>
# <startdate>01/04</startdate>
# </Card>
# <method>refund</method>
# </CardTxn>
# <TxnDetails>
# <merchantreference>1000001</merchantreference>
# <amount currency="GBP">95.99</amount>
# </TxnDetails>
# </Transaction>
# </Request>
def build_refund_request(money, credit_card, options)
xml = Builder::XmlMarkup.new :indent => 2
xml.instruct!
xml.tag! :Request do
add_authentication(xml)
xml.tag! :Transaction do
xml.tag! :CardTxn do
xml.tag! :method, REFUND_TYPE
add_credit_card(xml, credit_card, options[:billing_address])
end
xml.tag! :TxnDetails do
xml.tag! :merchantreference, format_reference_number(options[:order_id])
xml.tag! :amount, amount(money)
end
end
end
xml.target!
end
# Adds the authentication element to the passed builder xml doc
#
# Parameters:
# -xml: Builder document that is being built up
#
# Returns:
# -none: The results is stored in the passed xml document
#
def add_authentication(xml)
xml.tag! :Authentication do
xml.tag! :client, @options[:login]
xml.tag! :password, @options[:password]
end
end
# Add credit_card details to the passed XML Builder doc
#
# Parameters:
# -xml: Builder document that is being built up
# -credit_card: ActiveMerchant::Billing::CreditCard object
# -billing_address: Hash containing all of the billing address details
#
# Returns:
# -none: The results is stored in the passed xml document
#
def add_credit_card(xml, credit_card, address)
xml.tag! :Card do
# DataCash calls the CC number 'pan'
xml.tag! :pan, credit_card.number
xml.tag! :expirydate, format_date(credit_card.month, credit_card.year)
# optional values - for Solo etc
if [ 'switch', 'solo' ].include?(card_brand(credit_card).to_s)
xml.tag! :issuenumber, credit_card.issue_number unless credit_card.issue_number.blank?
if !credit_card.start_month.blank? && !credit_card.start_year.blank?
xml.tag! :startdate, format_date(credit_card.start_month, credit_card.start_year)
end
end
xml.tag! :Cv2Avs do
xml.tag! :cv2, credit_card.verification_value if credit_card.verification_value?
if address
xml.tag! :street_address1, address[:address1] unless address[:address1].blank?
xml.tag! :street_address2, address[:address2] unless address[:address2].blank?
xml.tag! :street_address3, address[:address3] unless address[:address3].blank?
xml.tag! :street_address4, address[:address4] unless address[:address4].blank?
xml.tag! :postcode, address[:zip] unless address[:zip].blank?
end
# The ExtendedPolicy defines what to do when the passed data
# matches, or not...
#
# All of the following elements MUST be present for the
# xml to be valid (or can drop the ExtendedPolicy and use
# a predefined one
xml.tag! :ExtendedPolicy do
xml.tag! :cv2_policy,
:notprovided => POLICY_REJECT,
:notchecked => POLICY_REJECT,
:matched => POLICY_ACCEPT,
:notmatched => POLICY_REJECT,
:partialmatch => POLICY_REJECT
xml.tag! :postcode_policy,
:notprovided => POLICY_ACCEPT,
:notchecked => POLICY_ACCEPT,
:matched => POLICY_ACCEPT,
:notmatched => POLICY_REJECT,
:partialmatch => POLICY_ACCEPT
xml.tag! :address_policy,
:notprovided => POLICY_ACCEPT,
:notchecked => POLICY_ACCEPT,
:matched => POLICY_ACCEPT,
:notmatched => POLICY_REJECT,
:partialmatch => POLICY_ACCEPT
end
end
end
end
# Send the passed data to DataCash for processing
#
# Parameters:
# -request: The XML data that is to be sent to Datacash
#
# Returns:
# - ActiveMerchant::Billing::Response object
#
def commit(request)
response = parse(ssl_post(test? ? self.test_url : self.live_url, request))
Response.new(response[:status] == DATACASH_SUCCESS, response[:reason], response,
:test => test?,
:authorization => "#{response[:datacash_reference]};#{response[:authcode]};#{response[:ca_reference]}"
)
end
# Returns a date string in the format Datacash expects
#
# Parameters:
# -month: integer, the month
# -year: integer, the year
#
# Returns:
# -String: date in MM/YY format
#
def format_date(month, year)
"#{format(month,:two_digits)}/#{format(year, :two_digits)}"
end
# Parse the datacash response and create a Response object
#
# Parameters:
# -body: The XML returned from Datacash
#
# Returns:
# -a hash with all of the values returned in the Datacash XML response
#
def parse(body)
response = {}
xml = REXML::Document.new(body)
root = REXML::XPath.first(xml, "//Response")
root.elements.to_a.each do |node|
parse_element(response, node)
end
response
end
# Parse an xml element
#
# Parameters:
# -response: The hash that the values are being returned in
# -node: The node that is currently being read
#
# Returns:
# - none (results are stored in the passed hash)
def parse_element(response, node)
if node.has_elements?
node.elements.each{|e| parse_element(response, e) }
else
response[node.name.underscore.to_sym] = node.text
end
end
def format_reference_number(number)
number.to_s.gsub(/[^A-Za-z0-9]/, '').rjust(6, "0").first(30)
end
end
end
end

View File

@@ -0,0 +1,230 @@
require 'rexml/document'
module ActiveMerchant #:nodoc:
module Billing #:nodoc:
class EfsnetGateway < Gateway
self.supported_countries = ['US']
self.supported_cardtypes = [:visa, :master, :american_express, :discover]
self.homepage_url = 'http://www.concordefsnet.com/'
self.display_name = 'Efsnet'
self.test_url = 'https://testefsnet.concordebiz.com/efsnet.dll'
self.live_url = 'https://efsnet.concordebiz.com/efsnet.dll'
# login is your Store ID
# password is your Store Key
def initialize(options = {})
requires!(options, :login, :password)
super
end
def authorize(money, creditcard, options = {})
request = build_credit_card_request(money, creditcard, options)
commit(:credit_card_authorize, request)
end
def purchase(money, creditcard, options = {})
request = build_credit_card_request(money, creditcard, options)
commit(:credit_card_charge, request)
end
def capture(money, identification, options = {})
request = build_refund_or_settle_request(money, identification, options)
commit(:credit_card_settle, request)
end
def credit(money, identification_or_credit_card, options = {})
if identification_or_credit_card.is_a?(String)
deprecated CREDIT_DEPRECATION_MESSAGE
# Perform authorization reversal
refund(money, identification_or_credit_card, options)
else
# Perform credit
request = build_credit_card_request(money, identification_or_credit_card, options)
commit(:credit_card_credit, request)
end
end
def refund(money, reference, options = {})
# Perform authorization reversal
request = build_refund_or_settle_request(money, reference, options)
commit(:credit_card_refund, request)
end
def void(identification, options = {})
requires!(options, :order_id)
original_transaction_id, original_transaction_amount = identification.split(";")
commit(:void_transaction, {:reference_number => format_reference_number(options[:order_id]), :transaction_id => original_transaction_id})
end
def voice_authorize(money, authorization_code, creditcard, options = {})
options[:authorization_number] = authorization_code
request = build_credit_card_request(money, creditcard, options)
commit(:credit_card_voice_authorize, request)
end
def force(money, authorization_code, creditcard, options = {})
options[:authorization_number] = authorization_code
request = build_credit_card_request(money, creditcard, options)
commit(:credit_card_capture, request)
end
def system_check
commit(:system_check, {})
end
private
def build_refund_or_settle_request(money, identification, options = {})
original_transaction_id, original_transaction_amount = identification.split(";")
requires!(options, :order_id)
post = {
:reference_number => format_reference_number(options[:order_id]),
:transaction_amount => amount(money),
:original_transaction_amount => original_transaction_amount,
:original_transaction_id => original_transaction_id,
:client_ip_address => options[:ip]
}
end
def build_credit_card_request(money, creditcard, options = {})
requires!(options, :order_id)
post = {
:reference_number => format_reference_number(options[:order_id]),
:authorization_number => options[:authorization_number],
:transaction_amount => amount(money),
:client_ip_address => options[:ip]
}
add_creditcard(post,creditcard)
add_address(post,options)
post
end
def format_reference_number(number)
number.to_s.slice(0,12)
end
def add_address(post,options)
if address = options[:billing_address] || options[:address]
if address[:address2]
post[:billing_address] = address[:address1].to_s << ' ' << address[:address2].to_s
else
post[:billing_address] = address[:address1].to_s
end
post[:billing_city] = address[:city].to_s
post[:billing_state] = address[:state].blank? ? 'n/a' : address[:state]
post[:billing_postal_code] = address[:zip].to_s
post[:billing_country] = address[:country].to_s
end
if address = options[:shipping_address]
if address[:address2]
post[:shipping_address] = address[:address1].to_s << ' ' << address[:address2].to_s
else
post[:shipping_address] = address[:address1].to_s
end
post[:shipping_city] = address[:city].to_s
post[:shipping_state] = address[:state].blank? ? 'n/a' : address[:state]
post[:shipping_postal_code] = address[:zip].to_s
post[:shipping_country] = address[:country].to_s
end
end
def add_creditcard(post, creditcard)
post[:billing_name] = creditcard.name if creditcard.name
post[:account_number] = creditcard.number
post[:card_verification_value] = creditcard.verification_value if creditcard.verification_value?
post[:expiration_month] = sprintf("%.2i", creditcard.month)
post[:expiration_year] = sprintf("%.4i", creditcard.year)[-2..-1]
end
def commit(action, parameters)
response = parse(ssl_post(test? ? self.test_url : self.live_url, post_data(action, parameters), 'Content-Type' => 'text/xml'))
Response.new(success?(response), message_from(response[:result_message]), response,
:test => test?,
:authorization => authorization_from(response, parameters),
:avs_result => { :code => response[:avs_response_code] },
:cvv_result => response[:cvv_response_code]
)
end
def success?(response)
response[:response_code] == '0'
end
def authorization_from(response, params)
[ response[:transaction_id], params[:transaction_amount] ].compact.join(';')
end
def parse(xml)
response = {}
xml = REXML::Document.new(xml)
xml.elements.each('//Reply//TransactionReply/*') do |node|
response[node.name.underscore.to_sym] = normalize(node.text)
end unless xml.root.nil?
response
end
def post_data(action, parameters = {})
xml = REXML::Document.new("<?xml version='1.0' encoding='UTF-8'?>")
root = xml.add_element("Request")
root.attributes["StoreID"] = options[:login]
root.attributes["StoreKey"] = options[:password]
root.attributes["ApplicationID"] = 'ot 1.0'
transaction = root.add_element(action.to_s.camelize)
actions[action].each do |key|
transaction.add_element(key).text = parameters[key.underscore.to_sym] unless parameters[key.underscore.to_sym].blank?
end
xml.to_s
end
def message_from(message)
return 'Unspecified error' if message.blank?
message.gsub(/[^\w]/, ' ').split.join(" ").capitalize
end
# Make a ruby type out of the response string
def normalize(field)
case field
when "true" then true
when "false" then false
when "" then nil
when "null" then nil
else field
end
end
def actions
ACTIONS
end
CREDIT_CARD_FIELDS = %w(AuthorizationNumber ClientIpAddress BillingAddress BillingCity BillingState BillingPostalCode BillingCountry BillingName CardVerificationValue ExpirationMonth ExpirationYear ReferenceNumber TransactionAmount AccountNumber )
ACTIONS = {
:credit_card_authorize => CREDIT_CARD_FIELDS,
:credit_card_charge => CREDIT_CARD_FIELDS,
:credit_card_voice_authorize => CREDIT_CARD_FIELDS,
:credit_card_capture => CREDIT_CARD_FIELDS,
:credit_card_credit => CREDIT_CARD_FIELDS + ["OriginalTransactionAmount"],
:credit_card_refund => %w(ReferenceNumber TransactionAmount OriginalTransactionAmount OriginalTransactionID ClientIpAddress),
:void_transaction => %w(ReferenceNumber TransactionID),
:credit_card_settle => %w(ReferenceNumber TransactionAmount OriginalTransactionAmount OriginalTransactionID ClientIpAddress),
:system_check => %w(SystemCheck),
}
end
end
end

View File

@@ -0,0 +1,312 @@
require File.dirname(__FILE__) + '/viaklix'
module ActiveMerchant #:nodoc:
module Billing #:nodoc:
# = Elavon Virtual Merchant Gateway
#
# == Example use:
#
# gateway = ActiveMerchant::Billing::ElavonGateway.new(
# :login => "my_virtual_merchant_id",
# :password => "my_virtual_merchant_pin",
# :user => "my_virtual_merchant_user_id" # optional
# )
#
# # set up credit card obj as in main ActiveMerchant example
# creditcard = ActiveMerchant::Billing::CreditCard.new(
# :type => 'visa',
# :number => '41111111111111111',
# :month => 10,
# :year => 2011,
# :first_name => 'Bob',
# :last_name => 'Bobsen'
# )
#
# # run request
# response = gateway.purchase(1000, creditcard) # authorize and capture 10 USD
#
# puts response.success? # Check whether the transaction was successful
# puts response.message # Retrieve the message returned by Elavon
# puts response.authorization # Retrieve the unique transaction ID returned by Elavon
#
class ElavonGateway < Gateway
class_attribute :test_url, :live_url, :delimiter, :actions
self.test_url = 'https://demo.myvirtualmerchant.com/VirtualMerchantDemo/process.do'
self.live_url = 'https://www.myvirtualmerchant.com/VirtualMerchant/process.do'
self.display_name = 'Elavon MyVirtualMerchant'
self.supported_countries = ['US', 'CA']
self.supported_cardtypes = [:visa, :master, :american_express, :discover]
self.homepage_url = 'http://www.elavon.com/'
self.delimiter = "\n"
self.actions = {
:purchase => 'CCSALE',
:credit => 'CCCREDIT',
:refund => 'CCRETURN',
:authorize => 'CCAUTHONLY',
:capture => 'CCFORCE',
:void => 'CCVOID'
}
# Initialize the Gateway
#
# The gateway requires that a valid login and password be passed
# in the +options+ hash.
#
# ==== Options
#
# * <tt>:login</tt> -- Merchant ID
# * <tt>:password</tt> -- PIN
# * <tt>:user</tt> -- Specify a subuser of the account (optional)
# * <tt>:test => +true+ or +false+</tt> -- Force test transactions
def initialize(options = {})
requires!(options, :login, :password)
super
end
# Make a purchase
def purchase(money, creditcard, options = {})
form = {}
add_salestax(form, options)
add_invoice(form, options)
add_creditcard(form, creditcard)
add_address(form, options)
add_customer_data(form, options)
add_test_mode(form, options)
commit(:purchase, money, form)
end
# Authorize a credit card for a given amount.
#
# ==== Parameters
# * <tt>money</tt> - The amount to be authorized as an Integer value in cents.
# * <tt>credit_card</tt> - The CreditCard details for the transaction.
# * <tt>options</tt>
# * <tt>:billing_address</tt> - The billing address for the cardholder.
def authorize(money, creditcard, options = {})
form = {}
add_salestax(form, options)
add_invoice(form, options)
add_creditcard(form, creditcard)
add_address(form, options)
add_customer_data(form, options)
add_test_mode(form, options)
commit(:authorize, money, form)
end
# Capture authorized funds from a credit card.
#
# ==== Parameters
# * <tt>money</tt> - The amount to be captured as an Integer value in cents.
# * <tt>authorization</tt> - The approval code returned from the initial authorization.
# * <tt>options</tt>
# * <tt>:credit_card</tt> - The CreditCard details from the initial transaction (required).
def capture(money, authorization, options = {})
requires!(options, :credit_card)
form = {}
add_salestax(form, options)
add_approval_code(form, authorization)
add_invoice(form, options)
add_creditcard(form, options[:credit_card])
add_customer_data(form, options)
add_test_mode(form, options)
commit(:capture, money, form)
end
# Refund a transaction.
#
# This transaction indicates to the gateway that
# money should flow from the merchant to the customer.
#
# ==== Parameters
#
# * <tt>money</tt> -- The amount to be credited to the customer as an Integer value in cents.
# * <tt>identification</tt> -- The ID of the original transaction against which the refund is being issued.
# * <tt>options</tt> -- A hash of parameters.
def refund(money, identification, options = {})
form = {}
add_txn_id(form, identification)
add_test_mode(form, options)
commit(:refund, money, form)
end
# Void a previous transaction
#
# ==== Parameters
#
# * <tt>authorization</tt> - The authorization returned from the previous request.
def void(identification, options = {})
form = {}
add_txn_id(form, identification)
add_test_mode(form, options)
commit(:void, nil, form)
end
# Make a credit to a card. Use the refund method if you'd like to credit using
# previous transaction
#
# ==== Parameters
# * <tt>money</tt> - The amount to be credited as an Integer value in cents.
# * <tt>creditcard</tt> - The credit card to be credited.
# * <tt>options</tt>
def credit(money, creditcard, options = {})
if creditcard.is_a?(String)
raise ArgumentError, "Reference credits are not supported. Please supply the original credit card or use the #refund method."
end
form = {}
add_invoice(form, options)
add_creditcard(form, creditcard)
add_address(form, options)
add_customer_data(form, options)
add_test_mode(form, options)
commit(:credit, money, form)
end
private
def add_invoice(form,options)
form[:invoice_number] = (options[:order_id] || options[:invoice]).to_s.slice(0, 10)
form[:description] = options[:description].to_s.slice(0, 255)
end
def add_approval_code(form, authorization)
form[:approval_code] = authorization.split(';').first
end
def add_txn_id(form, authorization)
form[:txn_id] = authorization.split(';').last
end
def authorization_from(response)
[response['approval_code'], response['txn_id']].join(';')
end
def add_creditcard(form, creditcard)
form[:card_number] = creditcard.number
form[:exp_date] = expdate(creditcard)
if creditcard.verification_value?
add_verification_value(form, creditcard)
end
form[:first_name] = creditcard.first_name.to_s.slice(0, 20)
form[:last_name] = creditcard.last_name.to_s.slice(0, 30)
end
def add_verification_value(form, creditcard)
form[:cvv2cvc2] = creditcard.verification_value
form[:cvv2cvc2_indicator] = '1'
end
def add_customer_data(form, options)
form[:email] = options[:email].to_s.slice(0, 100) unless options[:email].blank?
form[:customer_code] = options[:customer].to_s.slice(0, 10) unless options[:customer].blank?
end
def add_salestax(form, options)
form[:salestax] = options[:tax] if options[:tax].present?
end
def expdate(creditcard)
year = sprintf("%.4i", creditcard.year)
month = sprintf("%.2i", creditcard.month)
"#{month}#{year[2..3]}"
end
def add_address(form,options)
billing_address = options[:billing_address] || options[:address]
if billing_address
form[:avs_address] = billing_address[:address1].to_s.slice(0, 30)
form[:address2] = billing_address[:address2].to_s.slice(0, 30)
form[:avs_zip] = billing_address[:zip].to_s.slice(0, 10)
form[:city] = billing_address[:city].to_s.slice(0, 30)
form[:state] = billing_address[:state].to_s.slice(0, 10)
form[:company] = billing_address[:company].to_s.slice(0, 50)
form[:phone] = billing_address[:phone].to_s.slice(0, 20)
form[:country] = billing_address[:country].to_s.slice(0, 50)
end
if shipping_address = options[:shipping_address]
first_name, last_name = parse_first_and_last_name(shipping_address[:name])
form[:ship_to_first_name] = first_name.to_s.slice(0, 20)
form[:ship_to_last_name] = last_name.to_s.slice(0, 30)
form[:ship_to_address1] = shipping_address[:address1].to_s.slice(0, 30)
form[:ship_to_address2] = shipping_address[:address2].to_s.slice(0, 30)
form[:ship_to_city] = shipping_address[:city].to_s.slice(0, 30)
form[:ship_to_state] = shipping_address[:state].to_s.slice(0, 10)
form[:ship_to_company] = shipping_address[:company].to_s.slice(0, 50)
form[:ship_to_country] = shipping_address[:country].to_s.slice(0, 50)
form[:ship_to_zip] = shipping_address[:zip].to_s.slice(0, 10)
end
end
def parse_first_and_last_name(value)
name = value.to_s.split(' ')
last_name = name.pop || ''
first_name = name.join(' ')
[ first_name, last_name ]
end
def add_test_mode(form, options)
form[:test_mode] = 'TRUE' if options[:test_mode]
end
def message_from(response)
success?(response) ? response['result_message'] : response['errorMessage']
end
def success?(response)
!response.has_key?('errorMessage')
end
def commit(action, money, parameters)
parameters[:amount] = amount(money)
parameters[:transaction_type] = self.actions[action]
response = parse( ssl_post(test? ? self.test_url : self.live_url, post_data(parameters)) )
Response.new(response['result'] == '0', message_from(response), response,
:test => @options[:test] || test?,
:authorization => authorization_from(response),
:avs_result => { :code => response['avs_response'] },
:cvv_result => response['cvv2_response']
)
end
def post_data(parameters)
result = preamble
result.merge!(parameters)
result.collect { |key, value| "ssl_#{key}=#{CGI.escape(value.to_s)}" }.join("&")
end
def preamble
result = {
'merchant_id' => @options[:login],
'pin' => @options[:password],
'show_form' => 'false',
'result_format' => 'ASCII'
}
result['user_id'] = @options[:user] unless @options[:user].blank?
result
end
def parse(msg)
resp = {}
msg.split(self.delimiter).collect{|li|
key, value = li.split("=")
resp[key.strip.gsub(/^ssl_/, '')] = value.to_s.strip
}
resp
end
end
end
end

View File

@@ -0,0 +1,275 @@
module ActiveMerchant #:nodoc:
module Billing #:nodoc:
class EpayGateway < Gateway
API_HOST = 'ssl.ditonlinebetalingssystem.dk'
self.live_url = 'https://' + API_HOST + '/remote/payment'
self.default_currency = 'DKK'
self.money_format = :cents
self.supported_cardtypes = [:dankort, :forbrugsforeningen, :visa, :master,
:american_express, :diners_club, :jcb, :maestro]
self.supported_countries = ['DK', 'SE', 'NO']
self.homepage_url = 'http://epay.dk/'
self.display_name = 'ePay'
CURRENCY_CODES = {
:ADP => '020', :AED => '784', :AFA => '004', :ALL => '008', :AMD => '051',
:ANG => '532', :AOA => '973', :ARS => '032', :AUD => '036', :AWG => '533',
:AZM => '031', :BAM => '977', :BBD => '052', :BDT => '050', :BGL => '100',
:BGN => '975', :BHD => '048', :BIF => '108', :BMD => '060', :BND => '096',
:BOB => '068', :BOV => '984', :BRL => '986', :BSD => '044', :BTN => '064',
:BWP => '072', :BYR => '974', :BZD => '084', :CAD => '124', :CDF => '976',
:CHF => '756', :CLF => '990', :CLP => '152', :CNY => '156', :COP => '170',
:CRC => '188', :CUP => '192', :CVE => '132', :CYP => '196', :CZK => '203',
:DJF => '262', :DKK => '208', :DOP => '214', :DZD => '012', :ECS => '218',
:ECV => '983', :EEK => '233', :EGP => '818', :ERN => '232', :ETB => '230',
:EUR => '978', :FJD => '242', :FKP => '238', :GBP => '826', :GEL => '981',
:GHC => '288', :GIP => '292', :GMD => '270', :GNF => '324', :GTQ => '320',
:GWP => '624', :GYD => '328', :HKD => '344', :HNL => '340', :HRK => '191',
:HTG => '332', :HUF => '348', :IDR => '360', :ILS => '376', :INR => '356',
:IQD => '368', :IRR => '364', :ISK => '352', :JMD => '388', :JOD => '400',
:JPY => '392', :KES => '404', :KGS => '417', :KHR => '116', :KMF => '174',
:KPW => '408', :KRW => '410', :KWD => '414', :KYD => '136', :KZT => '398',
:LAK => '418', :LBP => '422', :LKR => '144', :LRD => '430', :LSL => '426',
:LTL => '440', :LVL => '428', :LYD => '434', :MAD => '504', :MDL => '498',
:MGF => '450', :MKD => '807', :MMK => '104', :MNT => '496', :MOP => '446',
:MRO => '478', :MTL => '470', :MUR => '480', :MVR => '462', :MWK => '454',
:MXN => '484', :MXV => '979', :MYR => '458', :MZM => '508', :NAD => '516',
:NGN => '566', :NIO => '558', :NOK => '578', :NPR => '524', :NZD => '554',
:OMR => '512', :PAB => '590', :PEN => '604', :PGK => '598', :PHP => '608',
:PKR => '586', :PLN => '985', :PYG => '600', :QAR => '634', :ROL => '642',
:RUB => '643', :RUR => '810', :RWF => '646', :SAR => '682', :SBD => '090',
:SCR => '690', :SDD => '736', :SEK => '752', :SGD => '702', :SHP => '654',
:SIT => '705', :SKK => '703', :SLL => '694', :SOS => '706', :SRG => '740',
:STD => '678', :SVC => '222', :SYP => '760', :SZL => '748', :THB => '764',
:TJS => '972', :TMM => '795', :TND => '788', :TOP => '776', :TPE => '626',
:TRL => '792', :TRY => '949', :TTD => '780', :TWD => '901', :TZS => '834',
:UAH => '980', :UGX => '800', :USD => '840', :UYU => '858', :UZS => '860',
:VEB => '862', :VND => '704', :VUV => '548', :XAF => '950', :XCD => '951',
:XOF => '952', :XPF => '953', :YER => '886', :YUM => '891', :ZAR => '710',
:ZMK => '894', :ZWD => '716'
}
# login: merchant number
# password: referrer url (for authorize authentication)
def initialize(options = {})
requires!(options, :login)
super
end
def authorize(money, credit_card_or_reference, options = {})
post = {}
add_amount(post, money, options)
add_invoice(post, options)
add_creditcard_or_reference(post, credit_card_or_reference)
add_instant_capture(post, false)
commit(:authorize, post)
end
def purchase(money, credit_card_or_reference, options = {})
post = {}
add_amount(post, money, options)
add_creditcard_or_reference(post, credit_card_or_reference)
add_invoice(post, options)
add_instant_capture(post, true)
commit(:authorize, post)
end
def capture(money, authorization, options = {})
post = {}
add_reference(post, authorization)
add_amount_without_currency(post, money)
commit(:capture, post)
end
def void(identification, options = {})
post = {}
add_reference(post, identification)
commit(:void, post)
end
def refund(money, identification, options = {})
post = {}
add_amount_without_currency(post, money)
add_reference(post, identification)
commit(:credit, post)
end
def credit(money, identification, options = {})
deprecated CREDIT_DEPRECATION_MESSAGE
refund(money, identification, options)
end
private
def add_amount(post, money, options)
post[:amount] = amount(money)
post[:currency] = CURRENCY_CODES[(options[:currency] || currency(money)).to_sym]
end
def add_amount_without_currency(post, money)
post[:amount] = amount(money)
end
def add_reference(post, identification)
post[:transaction] = identification
end
def add_invoice(post, options)
post[:orderid] = format_order_number(options[:order_id])
end
def add_creditcard(post, credit_card)
post[:cardno] = credit_card.number
post[:cvc] = credit_card.verification_value
post[:expmonth] = credit_card.month
post[:expyear] = credit_card.year
end
def add_creditcard_or_reference(post, credit_card_or_reference)
if credit_card_or_reference.respond_to?(:number)
add_creditcard(post, credit_card_or_reference)
else
add_reference(post, credit_card_or_reference.to_s)
end
end
def add_instant_capture(post, option)
post[:instantcapture] = option ? 1 : 0
end
def commit(action, params)
response = send("do_#{action}", params)
if action == :authorize
Response.new response['accept'].to_i == 1,
response['errortext'],
response,
:test => test?,
:authorization => response['tid']
else
Response.new response['result'] == 'true',
messages(response['epay'], response['pbs']),
response,
:test => test?,
:authorization => params[:transaction]
end
end
def messages(epay, pbs = nil)
response = "ePay: #{epay}"
response << " PBS: #{pbs}" if pbs
return response
end
def soap_post(method, params)
data = xml_builder(params, method)
headers = make_headers(data, method)
REXML::Document.new(ssl_post('https://' + API_HOST + '/remote/payment.asmx', data, headers))
end
def do_authorize(params)
headers = {}
headers['Referer'] = (options[:password] || "activemerchant.org")
response = raw_ssl_request(:post, 'https://' + API_HOST + '/auth/default.aspx', authorize_post_data(params), headers)
# Authorize gives the response back by redirecting with the values in
# the URL query
if location = response['Location']
query = CGI::parse(URI.parse(location.gsub(' ', '%20')).query)
else
return {
'accept' => '0',
'errortext' => 'ePay did not respond as expected. Please try again.',
'response_code' => response.code,
'response_message' => response.message
}
end
result = {}
query.each_pair do |k,v|
result[k] = v.is_a?(Array) && v.size == 1 ? v[0] : v # make values like ['v'] into 'v'
end
result
end
def do_capture(params)
response = soap_post('capture', params)
{
'result' => response.elements['//captureResponse/captureResult'].text,
'pbs' => response.elements['//captureResponse/pbsResponse'].text,
'epay' => response.elements['//captureResponse/epayresponse'].text
}
end
def do_credit(params)
response = soap_post('credit', params)
{
'result' => response.elements['//creditResponse/creditResult'].text,
'pbs' => response.elements['//creditResponse/pbsresponse'].text,
'epay' => response.elements['//creditResponse/epayresponse'].text
}
end
def do_void(params)
response = soap_post('delete', params)
{
'result' => response.elements['//deleteResponse/deleteResult'].text,
'epay' => response.elements['//deleteResponse/epayresponse'].text
}
end
def make_headers(data, soap_call)
{
'Content-Type' => 'text/xml; charset=utf-8',
'Host' => API_HOST,
'Content-Length' => data.size.to_s,
'SOAPAction' => self.live_url + '/' + soap_call
}
end
def xml_builder(params, soap_call)
xml = Builder::XmlMarkup.new(:indent => 2)
xml.instruct!
xml.tag! 'soap:Envelope', { 'xmlns:xsi' => 'http://schemas.xmlsoap.org/soap/envelope/',
'xmlns:xsd' => 'http://www.w3.org/2001/XMLSchema',
'xmlns:soap' => 'http://schemas.xmlsoap.org/soap/envelope/' } do
xml.tag! 'soap:Body' do
xml.tag! soap_call, { 'xmlns' => self.live_url } do
xml.tag! 'merchantnumber', @options[:login]
xml.tag! 'transactionid', params[:transaction]
xml.tag! 'amount', params[:amount].to_s if soap_call != 'delete'
end
end
end
xml.target!
end
def authorize_post_data(params = {})
params[:language] = '2'
params[:cms] = 'activemerchant'
params[:accepturl] = 'https://ssl.ditonlinebetalingssystem.dk/auth/default.aspx?accept=1'
params[:declineurl] = 'https://ssl.ditonlinebetalingssystem.dk/auth/default.aspx?decline=1'
params[:merchantnumber] = @options[:login]
params.collect { |key, value| "#{key}=#{CGI.escape(value.to_s)}" }.join("&")
end
# Limited to 20 digits max
def format_order_number(number)
number.to_s.gsub(/[^\w_]/, '').rjust(4, "0")[0...20]
end
end
end
end

View File

@@ -0,0 +1,308 @@
module ActiveMerchant #:nodoc:
module Billing #:nodoc:
# === EVO Canada payment gateway.
#
# EVO returns two different identifiers for most transactions, the
# +authcode+ and the +transactionid+. Since +transactionid+ is used more
# often (i.e. for {#capture}, {#refund}, {#void} and {#update}) we store it in the
# Response#authorization attribute. The +authcode+ from the merchant
# account is accessible via {Response#params}.
#
# Two different but related response messages are also returned from EVO.
# The message indicated by EVO's <tt>response_code</tt> parameter is returned as
# {Response#message} (Those messages can be seen in the {MESSAGES} hash.)
# The other, shorter message is available via {Response#params}.
#
# It's recommended to save the contents of the {Response#params} in your
# transaction log for future reference.
#
# === Sample Use
#
# gateway = ActiveMerchant::Billing::EvoCaGateway.new(username: 'demo', password: 'password')
#
# response = gateway.authorize(1000, credit_card, options)
#
# puts response.authorization # the transactionid
# puts response.params['authcode'] # the authcode from the merchant account
# puts response.message # the 'pretty' response message
# puts response.params['responsetext'] # the 'terse' response message
#
# gateway.capture(1000, response.authorization)
# gateway.update(response.authorization, shipping_carrier: 'fedex')
# gateway.refund(500, response.authorization)
#
class EvoCaGateway < Gateway
self.test_url = 'https://secure.evoepay.com/api/transact.php'
self.live_url = 'https://secure.evoepay.com/api/transact.php'
self.supported_countries = ['CA']
self.supported_cardtypes = [:visa, :master, :american_express, :jcb, :discover]
self.money_format = :dollars
self.homepage_url = 'http://www.evocanada.com/'
self.display_name = 'EVO Canada'
APPROVED, DECLINED, ERROR = 1, 2, 3
MESSAGES = {
100 => 'Transaction was approved',
200 => 'Transaction was declined by processor',
201 => 'Do not honor',
202 => 'Insufficient funds',
203 => 'Over limit',
204 => 'Transaction not allowed',
220 => 'Incorrect payment data',
221 => 'No such card issuer',
222 => 'No card number on file with issuer',
223 => 'Expired card',
224 => 'Invalid expiration date',
225 => 'Invalid card security code',
240 => 'Call issuer for futher information',
250 => 'Pick up card',
251 => 'Lost card',
252 => 'Stolen card',
253 => 'Fraudulant card',
260 => 'Declined with further instructions available',
261 => 'Declined - stop all recurring payments',
262 => 'Declined - stop this recurring program',
263 => 'Declined - updated cardholder data available',
264 => 'Declined - retry in a few days',
300 => 'Transaction was rejected by gateway',
400 => 'Transaction error returned by processor',
410 => 'Invalid merchant configuration',
411 => 'Merchant account is inactive',
420 => 'Communication error',
421 => 'Communication error with issuer',
430 => 'Duplicate transaction at processor',
440 => 'Processor format error',
441 => 'Invalid transaction information',
460 => 'Processor feature not available',
461 => 'Unsupported card type'
}
# This gateway requires that a valid username and password be passed
# in the +options+ hash.
#
# === Required Options
#
# * <tt>:username</tt>
# * <tt>:password</tt>
def initialize(options = {})
requires!(options, :username, :password)
super
end
# Transaction sales are submitted and immediately flagged for settlement.
# These transactions will automatically be settled.
#
# Payment source can be either a {CreditCard} or {Check}.
#
# === Additional Options
# In addition to the standard options, this gateway supports
#
# * <tt>:tracking_number</tt> - Shipping tracking number
# * <tt>:shipping_carrier</tt> - ups/fedex/dhl/usps
# * <tt>:po_number</tt> - Purchase order
# * <tt>:tax</tt> - Tax amount
# * <tt>:shipping</tt> - Shipping cost
def purchase(money, credit_card_or_check, options = {})
post = {}
add_invoice(post, options)
add_order(post, options)
add_paymentmethod(post, credit_card_or_check)
add_address(post, options)
add_customer_data(post, options)
commit('sale', money, post)
end
# Transaction authorizations are authorized immediately but are not
# flagged for settlement. These transactions must be flagged for
# settlement using the _capture_ transaction type. Authorizations
# typically remain activate for three to seven business days.
#
# Payment source must be a {CreditCard}.
def authorize(money, credit_card, options = {})
post = {}
add_invoice(post, options)
add_order(post, options)
add_paymentmethod(post, credit_card)
add_address(post, options)
add_customer_data(post, options)
commit('auth', money, post)
end
# Transaction captures flag existing _authorizations_ for settlement. Only
# authorizations can be captured. Captures can be submitted for an amount
# equal to or less than the original authorization.
#
# The <tt>authorization</tt> parameter is the transaction ID, retrieved
# from Response#authorization. See EvoCaGateway#purchase for the
# <tt>options</tt>.
def capture(money, authorization, options = {})
post = {
:amount => amount(money),
:transactionid => authorization
}
add_order(post, options)
commit('capture', money, post)
end
# Transaction refunds will reverse a previously settled transaction. If
# the transaction has not been settled, it must be _voided_ instead of
# refunded.
#
# The <tt>identification</tt> parameter is the transaction ID, retrieved
# from {Response#authorization}.
def refund(money, identification)
post = {:transactionid => identification}
commit('refund', money, post)
end
# Transaction credits apply a negative amount to the cardholder's card.
# In most situations credits are disabled as transaction refunds should
# be used instead.
#
# Note that this is different from a {#refund} (which is usually what
# you'll be looking for).
def credit(money, credit_card, options = {})
post = {}
add_invoice(post, options)
add_order(post, options)
add_paymentmethod(post, credit_card)
add_address(post, options)
add_customer_data(post, options)
commit('credit', money, post)
end
# Transaction voids will cancel an existing sale or captured
# authorization. In addition, non-captured authorizations can be voided to
# prevent any future capture. Voids can only occur if the transaction has
# not been settled.
#
# The <tt>identification</tt> parameter is the transaction ID, retrieved
# from {Response#authorization}.
def void(identification)
post = {:transactionid => identification}
commit('void', nil, post)
end
# Transaction updates can be used to update previous transactions with
# specific order information, such as a tracking number and shipping
# carrier. See EvoCaGateway#purchase for <tt>options</tt>.
#
# The <tt>identification</tt> parameter is the transaction ID, retrieved
# from {Response#authorization}.
def update(identification, options)
post = {:transactionid => identification}
add_order(post, options)
commit('update', nil, post)
end
private
def add_customer_data(post, options)
post[:email] = options[:email]
post[:ipaddress] = options[:ip]
end
def add_address(post, options)
if address = options[:billing_address] || options[:address]
post[:firstname] = address[:first_name]
post[:lastname] = address[:last_name]
post[:address1] = address[:address1]
post[:address2] = address[:address2]
post[:company] = address[:company]
post[:phone] = address[:phone]
post[:city] = address[:city]
post[:state] = address[:state]
post[:zip] = address[:zip]
post[:country] = address[:country]
end
if address = options[:shipping_address]
post[:shipping_firstname] = address[:first_name]
post[:shipping_lastname] = address[:last_name]
post[:shipping_address1] = address[:address1]
post[:shipping_address2] = address[:address2]
post[:shipping_company] = address[:company]
post[:shipping_zip] = address[:zip]
post[:shipping_city] = address[:city]
post[:shipping_state] = address[:state]
post[:shipping_country] = address[:country]
end
end
def add_order(post, options)
post[:orderid] = options[:order_id]
post[:tracking_number] = options[:tracking_number]
post[:shipping_carrier] = options[:shipping_carrier]
end
def add_invoice(post, options)
post[:orderdescription] = options[:description]
post[:ponumber] = options[:po_number]
post[:shipping] = amount(options[:shipping])
post[:tax] = amount(options[:tax])
end
def add_paymentmethod(post, payment)
if card_brand(payment)=='check'
post[:payment] = 'check'
post[:checkname] = payment.name
post[:checkaba] = payment.routing_number
post[:checkaccount] = payment.account_number
post[:account_holder_type] = payment.account_holder_type
post[:account_type] = payment.account_type
else
post[:payment] = 'creditcard'
post[:ccnumber] = payment.number
post[:ccexp] = "#{format(payment.month, :two_digits)}#{format(payment.year, :two_digits)}"
post[:cvv] = payment.verification_value
end
end
def parse(body)
fields = {}
CGI::parse(body).each do |k, v|
fields[k.to_s] = v.kind_of?(Array) ? v[0] : v
end
fields
end
def success?(response)
response['response'].to_i == APPROVED
end
def commit(action, money, parameters)
parameters[:amount] = amount(money) unless action == 'void'
data = ssl_post self.live_url, post_data(action, parameters)
response = parse(data)
message = message_from(response)
Response.new(success?(response), message, response,
:test => test?,
:authorization => response['transactionid'],
:avs_result => { :code => response['avsresponse'] },
:cvv_result => response['cvvresponse']
)
end
def message_from(response)
MESSAGES.fetch(response['response_code'].to_i, false) || response['message']
end
def post_data(action, parameters = {})
post = {:type => action}
if test?
post[:username] = 'demo'
post[:password] = 'password'
else
post[:username] = options[:username]
post[:password] = options[:password]
end
post.merge(parameters).collect { |key, value| "#{key}=#{CGI.escape(value.to_s)}" unless value.nil? }.compact.join("&")
end
end
end
end

View File

@@ -0,0 +1,225 @@
require 'rexml/document'
module ActiveMerchant #:nodoc:
module Billing #:nodoc:
# Public: For more information on the Eway Gateway please visit their
# {Developers Area}[http://www.eway.com.au/developers/api/direct-payments]
class EwayGateway < Gateway
self.live_url = 'https://www.eway.com.au'
self.money_format = :cents
self.supported_countries = ['AU']
self.supported_cardtypes = [:visa, :master, :american_express, :diners_club]
self.homepage_url = 'http://www.eway.com.au/'
self.display_name = 'eWAY'
# Public: Create a new Eway Gateway.
# options - A hash of options:
# :login - Your Customer ID.
# :password - Your XML Refund Password that you
# specified on the Eway site. (optional)
def initialize(options = {})
requires!(options, :login)
super
end
def purchase(money, creditcard, options = {})
requires_address!(options)
post = {}
add_creditcard(post, creditcard)
add_address(post, options)
add_customer_id(post)
add_invoice_data(post, options)
add_non_optional_data(post)
add_amount(post, money)
post[:CustomerEmail] = options[:email]
commit(purchase_url(post[:CVN]), money, post)
end
def refund(money, authorization, options={})
post = {}
add_customer_id(post)
add_amount(post, money)
add_non_optional_data(post)
post[:OriginalTrxnNumber] = authorization
post[:RefundPassword] = @options[:password]
post[:CardExpiryMonth] = nil
post[:CardExpiryYear] = nil
commit(refund_url, money, post)
end
private
def requires_address!(options)
raise ArgumentError.new("Missing eWay required parameters: address or billing_address") unless (options.has_key?(:address) or options.has_key?(:billing_address))
end
def add_creditcard(post, creditcard)
post[:CardNumber] = creditcard.number
post[:CardExpiryMonth] = sprintf("%.2i", creditcard.month)
post[:CardExpiryYear] = sprintf("%.4i", creditcard.year)[-2..-1]
post[:CustomerFirstName] = creditcard.first_name
post[:CustomerLastName] = creditcard.last_name
post[:CardHoldersName] = creditcard.name
post[:CVN] = creditcard.verification_value if creditcard.verification_value?
end
def add_address(post, options)
if address = options[:billing_address] || options[:address]
post[:CustomerAddress] = [ address[:address1], address[:address2], address[:city], address[:state], address[:country] ].compact.join(', ')
post[:CustomerPostcode] = address[:zip]
end
end
def add_customer_id(post)
post[:CustomerID] = @options[:login]
end
def add_invoice_data(post, options)
post[:CustomerInvoiceRef] = options[:order_id]
post[:CustomerInvoiceDescription] = options[:description]
end
def add_amount(post, money)
post[:TotalAmount] = amount(money)
end
def add_non_optional_data(post)
post[:Option1] = nil
post[:Option2] = nil
post[:Option3] = nil
post[:TrxnNumber] = nil
end
def commit(url, money, parameters)
raw_response = ssl_post(url, post_data(parameters))
response = parse(raw_response)
Response.new(success?(response),
message_from(response[:ewaytrxnerror]),
response,
:authorization => response[:ewaytrxnnumber],
:test => test?
)
end
def success?(response)
response[:ewaytrxnstatus] == "True"
end
def parse(xml)
response = {}
xml = REXML::Document.new(xml)
xml.elements.each('//ewayResponse/*') do |node|
response[node.name.downcase.to_sym] = normalize(node.text)
end unless xml.root.nil?
response
end
def post_data(parameters = {})
xml = REXML::Document.new
root = xml.add_element("ewaygateway")
parameters.each do |key, value|
root.add_element("eway#{key}").text = value
end
xml.to_s
end
def message_from(message)
return '' if message.blank?
MESSAGES[message[0,2]] || message
end
# Make a ruby type out of the response string
def normalize(field)
case field
when "true" then true
when "false" then false
when "" then nil
when "null" then nil
else field
end
end
def purchase_url(cvn)
suffix = test? ? 'xmltest/testpage.asp' : 'xmlpayment.asp'
gateway_part = cvn ? 'gateway_cvn' : 'gateway'
"#{live_url}/#{gateway_part}/#{suffix}"
end
def refund_url
suffix = test? ? 'xmltest/refund_test.asp' : 'xmlpaymentrefund.asp'
"#{live_url}/gateway/#{suffix}"
end
MESSAGES = {
"00" => "Transaction Approved",
"01" => "Refer to Issuer",
"02" => "Refer to Issuer, special",
"03" => "No Merchant",
"04" => "Pick Up Card",
"05" => "Do Not Honour",
"06" => "Error",
"07" => "Pick Up Card, Special",
"08" => "Honour With Identification",
"09" => "Request In Progress",
"10" => "Approved For Partial Amount",
"11" => "Approved, VIP",
"12" => "Invalid Transaction",
"13" => "Invalid Amount",
"14" => "Invalid Card Number",
"15" => "No Issuer",
"16" => "Approved, Update Track 3",
"19" => "Re-enter Last Transaction",
"21" => "No Action Taken",
"22" => "Suspected Malfunction",
"23" => "Unacceptable Transaction Fee",
"25" => "Unable to Locate Record On File",
"30" => "Format Error",
"31" => "Bank Not Supported By Switch",
"33" => "Expired Card, Capture",
"34" => "Suspected Fraud, Retain Card",
"35" => "Card Acceptor, Contact Acquirer, Retain Card",
"36" => "Restricted Card, Retain Card",
"37" => "Contact Acquirer Security Department, Retain Card",
"38" => "PIN Tries Exceeded, Capture",
"39" => "No Credit Account",
"40" => "Function Not Supported",
"41" => "Lost Card",
"42" => "No Universal Account",
"43" => "Stolen Card",
"44" => "No Investment Account",
"51" => "Insufficient Funds",
"52" => "No Cheque Account",
"53" => "No Savings Account",
"54" => "Expired Card",
"55" => "Incorrect PIN",
"56" => "No Card Record",
"57" => "Function Not Permitted to Cardholder",
"58" => "Function Not Permitted to Terminal",
"59" => "Suspected Fraud",
"60" => "Acceptor Contact Acquirer",
"61" => "Exceeds Withdrawal Limit",
"62" => "Restricted Card",
"63" => "Security Violation",
"64" => "Original Amount Incorrect",
"66" => "Acceptor Contact Acquirer, Security",
"67" => "Capture Card",
"75" => "PIN Tries Exceeded",
"82" => "CVV Validation Error",
"90" => "Cutoff In Progress",
"91" => "Card Issuer Unavailable",
"92" => "Unable To Route Transaction",
"93" => "Cannot Complete, Violation Of The Law",
"94" => "Duplicate Transaction",
"96" => "System Error"
}
end
end
end

View File

@@ -0,0 +1,291 @@
module ActiveMerchant #:nodoc:
module Billing #:nodoc:
class EwayManagedGateway < Gateway
self.test_url = 'https://www.eway.com.au/gateway/ManagedPaymentService/test/managedCreditCardPayment.asmx'
self.live_url = 'https://www.eway.com.au/gateway/ManagedPaymentService/managedCreditCardPayment.asmx'
# The countries the gateway supports merchants from as 2 digit ISO country codes
self.supported_countries = ['AU']
# The card types supported by the payment gateway
self.supported_cardtypes = [:visa, :master]
self.default_currency = 'AUD'
#accepted money format
self.money_format = :cents
# The homepage URL of the gateway
self.homepage_url = 'http://www.eway.com.au/'
# The name of the gateway
self.display_name = 'eWay Managed Payments'
def initialize(options = {})
requires!(options, :login, :username, :password)
# eWay returns 500 code for faults, which AM snaffles.
# So, we tell it to allow them.
options[:ignore_http_status] = true
super
end
# add a new customer CC to your eway account and return unique ManagedCustomerID
# supports storing details required by eway see "add_creditcard" and "add_address"
def store(creditcard, options = {})
post = {}
# Handle our required fields
requires!(options, :billing_address)
# Handle eWay specific required fields.
billing_address = options[:billing_address]
eway_requires!(billing_address)
add_creditcard(post, creditcard)
add_address(post, billing_address)
add_misc_fields(post, options)
commit("CreateCustomer", post)
end
def update(billing_id, creditcard, options={})
post = {}
# Handle our required fields
requires!(options, :billing_address)
# Handle eWay specific required fields.
billing_address = options[:billing_address]
eway_requires!(billing_address)
post[:managedCustomerID]=billing_id
add_creditcard(post, creditcard)
add_address(post, billing_address)
add_misc_fields(post, options)
commit("UpdateCustomer", post)
end
# Process a payment in the given amount against the stored credit card given by billing_id
#
# ==== Parameters
#
# * <tt>money</tt> -- The amount to be purchased as an Integer value in cents.
# * <tt>billing_id</tt> -- The eWay provided card/customer token to charge (managedCustomerID)
# * <tt>options</tt> -- A hash of optional parameters.
#
# ==== Options
#
# * <tt>:order_id</tt> -- The order number, passed to eWay as the "Invoice Reference"
# * <tt>:invoice</tt> -- The invoice number, passed to eWay as the "Invoice Reference" unless :order_id is also given
# * <tt>:description</tt> -- A description of the payment, passed to eWay as the "Invoice Description"
def purchase(money, billing_id, options={})
post = {}
post[:managedCustomerID] = billing_id.to_s
post[:amount]=money
add_invoice(post, options)
commit("ProcessPayment", post)
end
# Get customer's stored credit card details given by billing_id
#
# ==== Parameters
#
# * <tt>billing_id</tt> -- The eWay provided card/customer token to charge (managedCustomerID)
def retrieve(billing_id)
post = {}
post[:managedCustomerID] = billing_id.to_s
commit("QueryCustomer", post)
end
# TODO: eWay API also provides QueryPayment
private
def eway_requires!(hash)
raise ArgumentError.new("Missing eWay required parameter in `billing_address`: title") unless hash.has_key?(:title)
raise ArgumentError.new("Missing eWay required parameter in `billing_address`: country") unless hash.has_key?(:country)
end
def add_address(post, address)
post[:Address] = address[:address1].to_s
post[:Phone] = address[:phone].to_s
post[:PostCode] = address[:zip].to_s
post[:Suburb] = address[:city].to_s
post[:Country] = address[:country].to_s.downcase
post[:State] = address[:state].to_s
post[:Mobile] = address[:mobile].to_s
post[:Fax] = address[:fax].to_s
end
def add_misc_fields(post, options)
post[:CustomerRef]=options[:billing_address][:customer_ref] || options[:customer]
post[:Title]=options[:billing_address][:title]
post[:Company]=options[:billing_address][:company]
post[:JobDesc]=options[:billing_address][:job_desc]
post[:Email]=options[:billing_address][:email] || options[:email]
post[:URL]=options[:billing_address][:url]
post[:Comments]=options[:description]
end
def add_invoice(post, options)
post[:invoiceReference] = options[:order_id] || options[:invoice]
post[:invoiceDescription] = options[:description]
end
# add credit card details to be stored by eway. NOTE eway requires "title" field
def add_creditcard(post, creditcard)
post[:CCNumber] = creditcard.number
post[:CCExpiryMonth] = sprintf("%.2i", creditcard.month)
post[:CCExpiryYear] = sprintf("%.4i", creditcard.year)[-2..-1]
post[:CCNameOnCard] = creditcard.name
post[:FirstName] = creditcard.first_name
post[:LastName] = creditcard.last_name
end
def parse(body)
reply = {}
xml = REXML::Document.new(body)
if root = REXML::XPath.first(xml, "//soap:Fault") then
reply=parse_fault(root)
else
if root = REXML::XPath.first(xml, '//ProcessPaymentResponse/ewayResponse') then
# Successful payment
reply=parse_purchase(root)
else
if root = REXML::XPath.first(xml, '//QueryCustomerResult') then
reply=parse_query_customer(root)
else
if root = REXML::XPath.first(xml, '//CreateCustomerResult') then
reply[:message]='OK'
reply[:CreateCustomerResult]=root.text
reply[:success]=true
else
if root = REXML::XPath.first(xml, '//UpdateCustomerResult') then
if root.text.downcase == 'true' then
reply[:message]='OK'
reply[:success]=true
else
# ERROR: This state should never occur. If there is a problem,
# a soap:Fault will be returned. The presence of this
# element always means a success.
raise StandardError, "Unexpected \"false\" in UpdateCustomerResult"
end
else
# ERROR: This state should never occur currently. We have handled
# responses for all the methods which we support.
raise StandardError, "Unexpected response"
end
end
end
end
end
return reply
end
def parse_fault(node)
reply={}
reply[:message]=REXML::XPath.first(node, '//soap:Reason/soap:Text').text
reply[:success]=false
reply
end
def parse_purchase(node)
reply={}
reply[:message]=REXML::XPath.first(node, '//ewayTrxnError').text
reply[:success]=(REXML::XPath.first(node, '//ewayTrxnStatus').text == 'True')
reply[:auth_code]=REXML::XPath.first(node, '//ewayAuthCode').text
reply[:transaction_number]=REXML::XPath.first(node, '//ewayTrxnNumber').text
reply
end
def parse_query_customer(node)
reply={}
reply[:message]='OK'
reply[:success]=true
reply[:CCNumber]=REXML::XPath.first(node, '//CCNumber').text
reply[:CCName]=REXML::XPath.first(node, '//CCName').text
reply[:CCExpiryMonth]=REXML::XPath.first(node, '//CCExpiryMonth').text
reply[:CCExpiryYear]=REXML::XPath.first(node, '//CCExpiryYear').text
reply
end
def commit(action, post)
raw = begin
ssl_post(test? ? self.test_url : self.live_url, soap_request(post, action), 'Content-Type' => 'application/soap+xml; charset=utf-8')
rescue ResponseError => e
e.response.body
end
response = parse(raw)
EwayResponse.new(response[:success], response[:message], response,
:test => test?,
:authorization => response[:auth_code]
)
end
# Where we build the full SOAP 1.2 request using builder
def soap_request(arguments, action)
# eWay demands all fields be sent, but contain an empty string if blank
post = case action
when 'QueryCustomer'
arguments
when 'ProcessPayment'
default_payment_fields.merge(arguments)
when 'CreateCustomer'
default_customer_fields.merge(arguments)
when 'UpdateCustomer'
default_customer_fields.merge(arguments)
end
xml = Builder::XmlMarkup.new :indent => 2
xml.instruct!
xml.tag! 'soap12:Envelope', {'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance', 'xmlns:xsd' => 'http://www.w3.org/2001/XMLSchema', 'xmlns:soap12' => 'http://www.w3.org/2003/05/soap-envelope'} do
xml.tag! 'soap12:Header' do
xml.tag! 'eWAYHeader', {'xmlns' => 'https://www.eway.com.au/gateway/managedpayment'} do
xml.tag! 'eWAYCustomerID', @options[:login]
xml.tag! 'Username', @options[:username]
xml.tag! 'Password', @options[:password]
end
end
xml.tag! 'soap12:Body' do |x|
x.tag! "#{action}", {'xmlns' => 'https://www.eway.com.au/gateway/managedpayment'} do |y|
post.each do |key, value|
y.tag! "#{key}", "#{value}"
end
end
end
end
xml.target!
end
def default_customer_fields
hash={}
%w( CustomerRef Title FirstName LastName Company JobDesc Email Address Suburb State PostCode Country Phone Mobile Fax URL Comments CCNumber CCNameOnCard CCExpiryMonth CCExpiryYear ).each do |field|
hash[field.to_sym]=''
end
return hash
end
def default_payment_fields
hash={}
%w( managedCustomerID amount invoiceReference invoiceDescription ).each do |field|
hash[field.to_sym]=''
end
return hash
end
class EwayResponse < Response
# add a method to response so we can easily get the eway token "ManagedCustomerID"
def token
@params['CreateCustomerResult']
end
end
end
end
end

View File

@@ -0,0 +1,300 @@
require "nokogiri"
require "cgi"
module ActiveMerchant #:nodoc:
module Billing #:nodoc:
class EwayRapidGateway < Gateway
self.test_url = "https://api.sandbox.ewaypayments.com/"
self.live_url = "https://api.ewaypayments.com/"
self.money_format = :cents
self.supported_countries = ["AU"]
self.supported_cardtypes = [:visa, :master, :american_express, :diners_club]
self.homepage_url = "http://www.eway.com.au/"
self.display_name = "eWAY Rapid 3.0"
self.default_currency = "AUD"
def initialize(options = {})
requires!(options, :login, :password)
super
end
# Public: Run a purchase transaction. Treats the Rapid 3.0 transparent
# redirect as an API endpoint in order to conform to the standard
# ActiveMerchant #purchase API.
#
# amount - The monetary amount of the transaction in cents.
# options - A standard ActiveMerchant options hash:
# :order_id - A merchant-supplied identifier for the
# transaction (optional).
# :description - A merchant-supplied description of the
# transaction (optional).
# :currency - Three letter currency code for the
# transaction (default: "AUD")
# :billing_address - Standard ActiveMerchant address hash
# (optional).
# :shipping_address - Standard ActiveMerchant address hash
# (optional).
# :ip - The ip of the consumer initiating the
# transaction (optional).
# :application_id - A string identifying the application
# submitting the transaction
# (default: "https://github.com/Shopify/active_merchant")
#
# Returns an ActiveMerchant::Billing::Response object
def purchase(amount, payment_method, options={})
MultiResponse.new.tap do |r|
# Rather than follow the redirect, we detect the 302 and capture the
# token out of the Location header in the run_purchase step. But we
# still need a placeholder url to pass to eWay, and that is what
# example.com is used for here.
r.process{setup_purchase(amount, options.merge(:redirect_url => "http://example.com/"))}
r.process{run_purchase(r.authorization, payment_method, r.params["formactionurl"])}
r.process{status(r.authorization)}
end
end
# Public: Acquire the token necessary to run a transparent redirect.
#
# amount - The monetary amount of the transaction in cents.
# options - A supplemented ActiveMerchant options hash:
# :redirect_url - The url to return the customer to after
# the transparent redirect is completed
# (required).
# :order_id - A merchant-supplied identifier for the
# transaction (optional).
# :description - A merchant-supplied description of the
# transaction (optional).
# :currency - Three letter currency code for the
# transaction (default: "AUD")
# :billing_address - Standard ActiveMerchant address hash
# (optional).
# :shipping_address - Standard ActiveMerchant address hash
# (optional).
# :ip - The ip of the consumer initiating the
# transaction (optional).
# :application_id - A string identifying the application
# submitting the transaction
# (default: "https://github.com/Shopify/active_merchant")
#
# Returns an EwayRapidResponse object, which conforms to the
# ActiveMerchant::Billing::Response API, but also exposes #form_url.
def setup_purchase(amount, options={})
requires!(options, :redirect_url)
request = build_xml_request("CreateAccessCodeRequest") do |doc|
add_metadata(doc, options)
add_invoice(doc, amount, options)
add_customer_data(doc, options)
end
commit(url_for("CreateAccessCode"), request)
end
# Public: Retrieve the status of a transaction.
#
# identification - The Eway Rapid 3.0 access code for the transaction
# (returned as the response.authorization by
# #setup_purchase).
#
# Returns an EwayRapidResponse object.
def status(identification)
request = build_xml_request("GetAccessCodeResultRequest") do |doc|
doc.AccessCode identification
end
commit(url_for("GetAccessCodeResult"), request)
end
private
def run_purchase(identification, payment_method, endpoint)
post = {
"accesscode" => identification
}
add_credit_card(post, payment_method)
commit_form(endpoint, build_form_request(post))
end
def add_metadata(doc, options)
doc.RedirectUrl(options[:redirect_url])
doc.CustomerIP options[:ip] if options[:ip]
doc.Method "ProcessPayment"
doc.DeviceID(options[:application_id] || application_id)
end
def add_invoice(doc, money, options)
doc.Payment do
doc.TotalAmount amount(money)
doc.InvoiceReference options[:order_id]
doc.InvoiceDescription options[:description]
currency_code = (options[:currency] || currency(money) || default_currency)
doc.CurrencyCode currency_code
end
end
def add_customer_data(doc, options)
doc.Customer do
add_address(doc, (options[:billing_address] || options[:address]), {:email => options[:email]})
end
doc.ShippingAddress do
add_address(doc, options[:shipping_address], {:skip_company => true})
end
end
def add_address(doc, address, options={})
return unless address
if name = address[:name]
parts = name.split(/\s+/)
doc.FirstName parts.shift if parts.size > 1
doc.LastName parts.join(" ")
end
doc.CompanyName address[:company] unless options[:skip_company]
doc.Street1 address[:address1]
doc.Street2 address[:address2]
doc.City address[:city]
doc.State address[:state]
doc.PostalCode address[:zip]
doc.Country address[:country]
doc.Phone address[:phone]
doc.Fax address[:fax]
doc.Email options[:email]
end
def add_credit_card(post, credit_card)
post["cardname"] = credit_card.name
post["cardnumber"] = credit_card.number
post["cardexpirymonth"] = credit_card.month
post["cardexpiryyear"] = credit_card.year
post["cardcvn"] = credit_card.verification_value
end
def build_xml_request(root)
builder = Nokogiri::XML::Builder.new
builder.__send__(root) do |doc|
yield(doc)
end
builder.to_xml
end
def build_form_request(post)
request = []
post.each do |key, value|
request << "EWAY_#{key.upcase}=#{CGI.escape(value.to_s)}"
end
request.join("&")
end
def url_for(action)
(test? ? test_url : live_url) + action + ".xml"
end
def commit(url, request, form_post=false)
headers = {
"Authorization" => ("Basic " + Base64.strict_encode64(@options[:login].to_s + ":" + @options[:password].to_s).chomp),
"Content-Type" => "text/xml"
}
raw = parse(ssl_post(url, request, headers))
succeeded = success?(raw)
EwayRapidResponse.new(
succeeded,
message_from(succeeded, raw),
raw,
:authorization => authorization_from(raw),
:test => test?,
:avs_result => avs_result_from(raw),
:cvv_result => cvv_result_from(raw)
)
rescue ActiveMerchant::ResponseError => e
return EwayRapidResponse.new(false, e.response.message, {:status_code => e.response.code}, :test => test?)
end
def commit_form(url, request)
http_response = raw_ssl_request(:post, url, request)
success = (http_response.code.to_s == "302")
message = (success ? "Succeeded" : http_response.body)
if success
authorization = CGI.unescape(http_response["Location"].split("=").last)
end
Response.new(success, message, {:location => http_response["Location"]}, :authorization => authorization, :test => test?)
end
def parse(xml)
response = {}
doc = Nokogiri::XML(xml)
doc.root.xpath("*").each do |node|
if (node.elements.size == 0)
response[node.name.downcase.to_sym] = node.text
else
node.elements.each do |childnode|
name = "#{node.name.downcase}_#{childnode.name.downcase}"
response[name.to_sym] = childnode.text
end
end
end unless doc.root.nil?
response
end
def success?(response)
if response[:errors]
false
elsif response[:transactionstatus]
(response[:transactionstatus] == "true")
else
true
end
end
def message_from(succeeded, response)
if response[:errors]
response[:errors]
elsif response[:responsecode]
ActiveMerchant::Billing::EwayGateway::MESSAGES[response[:responsecode]]
elsif response[:responsemessage]
response[:responsemessage]
elsif succeeded
"Succeeded"
else
"Failed"
end
end
def authorization_from(response)
response[:accesscode]
end
def avs_result_from(response)
code = case response[:verification_address]
when "Valid"
"M"
when "Invalid"
"N"
else
"I"
end
{:code => code}
end
def cvv_result_from(response)
case response[:verification_cvn]
when "Valid"
"M"
when "Invalid"
"N"
else
"P"
end
end
class EwayRapidResponse < ActiveMerchant::Billing::Response
def form_url
params["formactionurl"]
end
end
end
end
end

View File

@@ -0,0 +1,218 @@
module ActiveMerchant #:nodoc:
module Billing #:nodoc:
class ExactGateway < Gateway
self.live_url = self.test_url = 'https://secure2.e-xact.com/vplug-in/transaction/rpc-enc/service.asmx'
API_VERSION = "8.5"
TEST_LOGINS = [ {:login => "A00049-01", :password => "test1"},
{:login => "A00427-01", :password => "testus"} ]
TRANSACTIONS = { :sale => "00",
:authorization => "01",
:capture => "32",
:credit => "34" }
ENVELOPE_NAMESPACES = { 'xmlns:xsd' => 'http://www.w3.org/2001/XMLSchema',
'xmlns:env' => 'http://schemas.xmlsoap.org/soap/envelope/',
'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance'
}
SEND_AND_COMMIT_ATTRIBUTES = { 'xmlns:n1' => "http://secure2.e-xact.com/vplug-in/transaction/rpc-enc/Request",
'env:encodingStyle' => 'http://schemas.xmlsoap.org/soap/encoding/'
}
SEND_AND_COMMIT_SOURCE_ATTRIBUTES = { 'xmlns:n2' => 'http://secure2.e-xact.com/vplug-in/transaction/rpc-enc/encodedTypes',
'xsi:type' => 'n2:Transaction'
}
POST_HEADERS = { 'soapAction' => "http://secure2.e-xact.com/vplug-in/transaction/rpc-enc/SendAndCommit",
'Content-Type' => 'text/xml'
}
SUCCESS = "true"
SENSITIVE_FIELDS = [ :verification_str2, :expiry_date, :card_number ]
self.supported_cardtypes = [:visa, :master, :american_express, :jcb, :discover]
self.supported_countries = ['CA', 'US']
self.homepage_url = 'http://www.e-xact.com'
self.display_name = 'E-xact'
def initialize(options = {})
requires!(options, :login, :password)
super
end
def authorize(money, credit_card, options = {})
commit(:authorization, build_sale_or_authorization_request(money, credit_card, options))
end
def purchase(money, credit_card, options = {})
commit(:sale, build_sale_or_authorization_request(money, credit_card, options))
end
def capture(money, authorization, options = {})
commit(:capture, build_capture_or_credit_request(money, authorization, options))
end
def credit(money, authorization, options = {})
deprecated CREDIT_DEPRECATION_MESSAGE
refund(money, authorization, options)
end
def refund(money, authorization, options = {})
commit(:credit, build_capture_or_credit_request(money, authorization, options))
end
private
def build_request(action, body)
xml = Builder::XmlMarkup.new
xml.instruct!
xml.tag! 'env:Envelope', ENVELOPE_NAMESPACES do
xml.tag! 'env:Body' do
xml.tag! 'n1:SendAndCommit', SEND_AND_COMMIT_ATTRIBUTES do
xml.tag! 'SendAndCommitSource', SEND_AND_COMMIT_SOURCE_ATTRIBUTES do
add_credentials(xml)
add_transaction_type(xml, action)
xml << body
end
end
end
end
xml.target!
end
def build_sale_or_authorization_request(money, credit_card, options)
xml = Builder::XmlMarkup.new
add_amount(xml, money)
add_credit_card(xml, credit_card)
add_customer_data(xml, options)
add_invoice(xml, options)
xml.target!
end
def build_capture_or_credit_request(money, identification, options)
xml = Builder::XmlMarkup.new
add_identification(xml, identification)
add_amount(xml, money)
add_customer_data(xml, options)
xml.target!
end
def add_credentials(xml)
xml.tag! 'ExactID', @options[:login]
xml.tag! 'Password', @options[:password]
end
def add_transaction_type(xml, action)
xml.tag! 'Transaction_Type', TRANSACTIONS[action]
end
def add_identification(xml, identification)
authorization_num, transaction_tag = identification.split(';')
xml.tag! 'Authorization_Num', authorization_num
xml.tag! 'Transaction_Tag', transaction_tag
end
def add_amount(xml, money)
xml.tag! 'DollarAmount', amount(money)
end
def add_credit_card(xml, credit_card)
xml.tag! 'Card_Number', credit_card.number
xml.tag! 'Expiry_Date', expdate(credit_card)
xml.tag! 'CardHoldersName', credit_card.name
if credit_card.verification_value?
xml.tag! 'CVD_Presence_Ind', '1'
xml.tag! 'VerificationStr2', credit_card.verification_value
end
end
def add_customer_data(xml, options)
xml.tag! 'Customer_Ref', options[:customer]
xml.tag! 'Client_IP', options[:ip]
xml.tag! 'Client_Email', options[:email]
end
def add_address(xml, options)
if address = options[:billing_address] || options[:address]
xml.tag! 'ZipCode', address[:zip]
end
end
def add_invoice(xml, options)
xml.tag! 'Reference_No', options[:order_id]
xml.tag! 'Reference_3', options[:description]
end
def expdate(credit_card)
"#{format(credit_card.month, :two_digits)}#{format(credit_card.year, :two_digits)}"
end
def commit(action, request)
response = parse(ssl_post(self.live_url, build_request(action, request), POST_HEADERS))
Response.new(successful?(response), message_from(response), response,
:test => test?,
:authorization => authorization_from(response),
:avs_result => { :code => response[:avs] },
:cvv_result => response[:cvv2]
)
end
def successful?(response)
response[:transaction_approved] == SUCCESS
end
def authorization_from(response)
if response[:authorization_num] && response[:transaction_tag]
"#{response[:authorization_num]};#{response[:transaction_tag]}"
else
''
end
end
def message_from(response)
if response[:faultcode] && response[:faultstring]
response[:faultstring]
elsif response[:error_number] != '0'
response[:error_description]
else
result = response[:exact_message] || ''
result << " - #{response[:bank_message]}" unless response[:bank_message].blank?
result
end
end
def parse(xml)
response = {}
xml = REXML::Document.new(xml)
if root = REXML::XPath.first(xml, "//types:TransactionResult")
parse_elements(response, root)
elsif root = REXML::XPath.first(xml, "//soap:Fault")
parse_elements(response, root)
end
response.delete_if{ |k,v| SENSITIVE_FIELDS.include?(k) }
end
def parse_elements(response, root)
root.elements.to_a.each do |node|
response[node.name.gsub(/EXact/, 'Exact').underscore.to_sym] = (node.text || '').strip
end
end
end
end
end

View File

@@ -0,0 +1,152 @@
require 'json'
module ActiveMerchant #:nodoc:
module Billing #:nodoc:
class FatZebraGateway < Gateway
self.live_url = "https://gateway.fatzebra.com.au/v1.0"
self.test_url = "https://gateway.sandbox.fatzebra.com.au/v1.0"
self.supported_countries = ['AU']
self.default_currency = 'AUD'
self.money_format = :cents
self.supported_cardtypes = [:visa, :master, :american_express, :jcb]
self.homepage_url = 'https://www.fatzebra.com.au/'
self.display_name = 'Fat Zebra'
# Setup a new instance of the gateway.
#
# The options hash should include :username and :token
# You can find your username and token at https://dashboard.fatzebra.com.au
# Under the Your Account section
def initialize(options = {})
requires!(options, :username)
requires!(options, :token)
@username = options[:username]
@token = options[:token]
super
end
# To create a purchase on a credit card use:
#
# purchase(money, creditcard , { ... })
#
# To charge a tokenized card
#
# purchase(money, {:token => "abzy87u", :cvv => "123"}, { ... }})
def purchase(money, creditcard, options = {})
post = {}
add_amount(post, money, options)
add_creditcard(post, creditcard, options)
post[:reference] = options[:order_id]
post[:customer_ip] = options[:ip]
commit(:post, 'purchases', post)
end
# Refund a transaction
#
# amount - Integer - the amount to refund
# txn_id - String - the original transaction to be refunded
# reference - String - your transaction reference
def refund(money, txn_id, reference)
post = {}
post[:amount] = money
post[:transaction_id] = txn_id
post[:reference] = reference
commit(:post, "refunds", post)
end
# Tokenize a credit card
def store(creditcard)
post = {}
add_creditcard(post, creditcard)
commit(:post, "credit_cards", post)
end
private
# Add the money details to the request
def add_amount(post, money, options)
post[:amount] = money
end
# Add the credit card details to the request
def add_creditcard(post, creditcard, options = {})
if creditcard.respond_to?(:number)
post[:card_number] = creditcard.number
post[:card_expiry] = "#{creditcard.month}/#{creditcard.year}"
post[:cvv] = creditcard.verification_value if creditcard.verification_value?
post[:card_holder] = creditcard.name if creditcard.name
else
post[:card_token] = creditcard[:token]
post[:cvv] = creditcard[:cvv]
end
end
# Post the data to the gateway
def commit(method, uri, parameters=nil)
raw_response = response = nil
success = false
begin
raw_response = ssl_request(method, get_url(uri), parameters.to_json, headers)
response = parse(raw_response)
success = response["successful"] && (response["response"]["successful"] || response["response"]["token"])
rescue ResponseError => e
if e.response.code == "401"
return Response.new(false, "Invalid Login")
end
raw_response = e.response.body
response = parse(raw_response)
rescue JSON::ParserError
response = json_error(raw_response)
end
message = response["response"]["message"]
unless response["successful"]
# There is an error, so we will show that instead
message = response["errors"].empty? ? "Unknown Error" : response["errors"].join(", ")
end
Response.new(success,
message,
response,
:test => response.has_key?("test") ? response["test"] : false,
:authorization => response["response"]["id"] || response["response"]["token"])
end
# Parse the returned JSON, if parse errors are raised then return a detailed error.
def parse(response)
begin
JSON.parse(response)
rescue JSON::ParserError
msg = 'Invalid JSON response received from Fat Zebra. Please contact support@fatzebra.com.au if you continue to receive this message.'
msg += " (The raw response returned by the API was #{response.inspect})"
{
"successful" => false,
"response" => {},
"errors" => [msg]
}
end
end
# Build the URL based on the AM mode and the URI
def get_url(uri)
base = test? ? self.test_url : self.live_url
base + "/" + uri
end
# Builds the auth and U-A headers for the request
def headers
{
"Authorization" => "Basic " + Base64.strict_encode64(@username.to_s + ":" + @token.to_s).strip,
"User-Agent" => "Fat Zebra v1.0/ActiveMerchant #{ActiveMerchant::VERSION}"
}
end
end
end
end

View File

@@ -0,0 +1,167 @@
module ActiveMerchant #:nodoc:
module Billing #:nodoc:
class FederatedCanadaGateway < Gateway
# Same URL for both test and live, testing is done by using the test username (demo) and password (password).
self.live_url = self.test_url = 'https://secure.federatedgateway.com/api/transact.php'
APPROVED, DECLINED, ERROR = 1, 2, 3
# The countries the gateway supports merchants from as 2 digit ISO country codes
self.supported_countries = ['CA']
self.default_currency = 'CAD'
# The card types supported by the payment gateway
self.supported_cardtypes = [:visa, :master, :american_express, :discover]
# The homepage URL of the gateway
self.homepage_url = 'http://www.federatedcanada.com/'
# The name of the gateway
self.display_name = 'Federated Canada'
def initialize(options = {})
requires!(options, :login, :password)
super
end
def purchase(money, creditcard, options = {})
post = {}
add_invoice(post, options)
add_creditcard(post, creditcard)
add_address(post, options)
add_customer_data(post, options)
commit('sale', money, post)
end
def authorize(money, creditcard, options = {})
post = {}
add_invoice(post, options)
add_creditcard(post, creditcard)
add_address(post, options)
add_customer_data(post, options)
commit('auth', money, post)
end
def capture(money, authorization, options = {})
options[:transactionid] = authorization
commit('capture', money, options)
end
def void(authorization, options = {})
options[:transactionid] = authorization
commit('void', nil, options)
end
def refund(money, authorization, options = {})
commit('refund', money, options.merge(:transactionid => authorization))
end
def credit(money, authorization, options = {})
deprecated CREDIT_DEPRECATION_MESSAGE
refund(money, authorization, options)
end
private
def add_customer_data(post, options)
post[:firstname] = options[:first_name]
post[:lastname] = options[:last_name]
post[:email] = options[:email]
end
def add_address(post, options)
if address = (options[:billing_address] || options[:address])
post[:company] = address[:company]
post[:address1] = address[:address1]
post[:address2] = address[:address2]
post[:city] = address[:city]
post[:state] = address[:state]
post[:zip] = address[:zip]
post[:country] = address[:country]
post[:phone] = address[:phone]
end
if address = options[:shipping_address]
post[:shipping_firstname] = address[:first_name]
post[:shipping_lastname] = address[:last_name]
post[:shipping_company] = address[:company]
post[:shipping_address1] = address[:address1]
post[:shipping_address2] = address[:address2]
post[:shipping_city] = address[:city]
post[:shipping_state] = address[:state]
post[:shipping_zip] = address[:zip]
post[:shipping_country] = address[:country]
post[:shipping_email] = address[:email]
end
end
def add_invoice(post, options)
post[:orderid] = options[:order_id]
post[:orderdescription] = options[:description]
end
def add_creditcard(post, creditcard)
post[:ccnumber] = creditcard.number
post[:ccexp] = expdate(creditcard)
post[:cvv] = creditcard.verification_value
end
def expdate(creditcard)
year = sprintf("%.4i", creditcard.year)
month = sprintf("%.2i", creditcard.month)
"#{month}#{year[-2..-1]}"
end
def parse(body)
body.split('&').inject({}) do |memo, x|
k, v = x.split('=')
memo[k] = v
memo
end
end
def commit(action, money, parameters)
parameters[:amount] = amount(money)
data = ssl_post(self.live_url, post_data(action, parameters))
response = parse(data)
message = message_from(response)
test_mode = test?
Response.new(success?(response), message, response,
:test => test?,
:authorization => response['transactionid'],
:avs_result => {:code => response['avsresponse']},
:cvv_result => response['cvvresponse']
)
end
def success?(response)
response['response'] == '1'
end
def test?
(@options[:login].eql?('demo')) && (@options[:password].eql?('password'))
end
def message_from(response)
case response['response'].to_i
when APPROVED
"Transaction Approved"
when DECLINED
"Transaction Declined"
else
"Error in transaction data or system error"
end
end
def post_data(action, parameters = {})
parameters[:type] = action
parameters[:username] = @options[:login]
parameters[:password] = @options[:password]
parameters.map{|k, v| "#{k}=#{CGI.escape(v.to_s)}"}.join('&')
end
end
end
end

View File

@@ -0,0 +1,22 @@
require File.dirname(__FILE__) + '/cc5'
module ActiveMerchant #:nodoc:
module Billing #:nodoc:
class FinansbankGateway < CC5Gateway
self.live_url = self.test_url = 'https://www.fbwebpos.com/servlet/cc5ApiServer'
# The countries the gateway supports merchants from as 2 digit ISO country codes
self.supported_countries = ['US', 'TR']
# The card types supported by the payment gateway
self.supported_cardtypes = [:visa, :master]
# The homepage URL of the gateway
self.homepage_url = 'https://www.fbwebpos.com/'
# The name of the gateway
self.display_name = 'Finansbank WebPOS'
end
end
end

View File

@@ -0,0 +1,176 @@
module ActiveMerchant #:nodoc:
module Billing #:nodoc:
class FirstPayGateway < Gateway
class FirstPayPostData < PostData
# Fields that will be sent even if they are blank
self.required_fields = [ :action, :amount, :trackid ]
end
# both URLs are IP restricted
self.test_url = 'https://apgcert.first-pay.com/AcqENGIN/SecureCapture'
self.live_url = 'https://acqengin.first-pay.com/AcqENGIN/SecureCapture'
# The countries the gateway supports merchants from as 2 digit ISO country codes
self.supported_countries = ['US']
# The card types supported by the payment gateway
self.supported_cardtypes = [:visa, :master]
# The homepage URL of the gateway
self.homepage_url = 'http://www.first-pay.com'
# The name of the gateway
self.display_name = 'First Pay'
# all transactions are in cents
self.money_format = :cents
ACTIONS = {
'sale' => 1,
'credit' => 2,
'void' => 3
}
def initialize(options = {})
requires!(options, :login, :password)
super
end
def purchase(money, creditcard, options = {})
post = FirstPayPostData.new
add_invoice(post, options)
add_creditcard(post, creditcard)
add_address(post, options)
add_customer_data(post, options)
commit('sale', money, post)
end
def refund(money, reference, options = {})
requires!(options, :credit_card)
post = FirstPayPostData.new
add_invoice(post, options)
add_creditcard(post, options[:credit_card])
add_address(post, options)
add_customer_data(post, options)
add_credit_data(post, reference)
commit('credit', money, post)
end
def credit(money, reference, options = {})
deprecated CREDIT_DEPRECATION_MESSAGE
refund(money, reference, options)
end
def void(money, creditcard, options = {})
post = FirstPayPostData.new
add_creditcard(post, creditcard)
add_void_data(post, options)
add_invoice(post, options)
add_customer_data(post, options)
commit('void', money, post)
end
private
def add_customer_data(post, options)
post[:cardip] = options[:ip]
post[:email] = options[:email]
end
def add_address(post, options)
if billing_address = options[:billing_address] || options[:address]
post[:addr] = billing_address[:address1].to_s + ' ' + billing_address[:address2].to_s
post[:city] = billing_address[:city]
post[:state] = billing_address[:state]
post[:zip] = billing_address[:zip]
post[:country] = billing_address[:country]
end
end
def add_invoice(post, options)
post[:trackid] = rand(Time.now.to_i)
end
def add_creditcard(post, creditcard)
post[:member] = creditcard.first_name.to_s + " " + creditcard.last_name.to_s
post[:card] = creditcard.number
post[:exp] = expdate(creditcard)
end
def expdate(credit_card)
year = sprintf("%.4i", credit_card.year)
month = sprintf("%.2i", credit_card.month)
"#{month}#{year[-2..-1]}"
end
def add_credit_data(post, transaction_id)
post[:transid] = transaction_id
end
def add_void_data(post, options)
post[:transid] = options[:transactionid]
end
def commit(action, money, post)
response = parse( ssl_post(test? ? self.test_url : self.live_url, post_data(action, post, money)) )
Response.new(response[:response] == 'CAPTURED', response[:message], response,
:test => test?,
:authorization => response[:authorization],
:avs_result => { :code => response[:avsresponse] },
:cvv_result => response[:cvvresponse])
end
def parse(body)
response = {}
# check for an error first
if body.include?('!ERROR!')
response[:response] = 'ERROR'
response[:message] = error_message_from(body)
else
# a capture / not captured response will be : delimited
split = body.split(':')
response[:response] = split[0]
# FirstPay docs are worthless. turns out the transactionid is required for credits
# so we need to store that in authorization, not the actual auth.
if response[:response] == 'CAPTURED'
response[:message] = 'CAPTURED'
response[:authorization] = split[9] # actually the transactionid
response[:auth] = split[1]
response[:avsresponse] = split[3]
response[:cvvresponse] = split[17]
else
# NOT CAPTURED response
response[:message] = split[1]
response[:transactionid] = split[9]
end
end
return response
end
def error_message_from(response)
# error messages use this format - '!ERROR! 704-MISSING BASIC DATA TYPE:card, exp, zip, addr, member, amount\n'
response.split("! ")[1].chomp
end
def post_data(action, post, money)
post[:vid] = @options[:login]
post[:password] = @options[:password]
post[:action] = ACTIONS[action]
post[:amount] = amount(money)
return post.to_post_data
end
end
end
end

View File

@@ -0,0 +1,314 @@
module ActiveMerchant #:nodoc:
module Billing #:nodoc:
class FirstdataE4Gateway < Gateway
# TransArmor support requires v11 or lower
self.test_url = "https://api.demo.globalgatewaye4.firstdata.com/transaction/v11"
self.live_url = "https://api.globalgatewaye4.firstdata.com/transaction/v11"
TRANSACTIONS = {
:sale => "00",
:authorization => "01",
:capture => "32",
:void => "33",
:credit => "34",
:store => "05"
}
POST_HEADERS = {
"Accepts" => "application/xml",
"Content-Type" => "application/xml"
}
SUCCESS = "true"
SENSITIVE_FIELDS = [:verification_str2, :expiry_date, :card_number]
self.supported_cardtypes = [:visa, :master, :american_express, :jcb, :discover]
self.supported_countries = ["CA", "US"]
self.default_currency = "USD"
self.homepage_url = "http://www.firstdata.com"
self.display_name = "FirstData Global Gateway e4"
# Create a new FirstdataE4Gateway
#
# The gateway requires that a valid login and password be passed
# in the +options+ hash.
#
# ==== Options
#
# * <tt>:login</tt> -- The EXACT ID. Also known as the Gateway ID.
# (Found in your administration terminal settings)
# * <tt>:password</tt> -- The terminal password (not your account password)
def initialize(options = {})
requires!(options, :login, :password)
@options = options
super
end
def authorize(money, credit_card_or_store_authorization, options = {})
commit(:authorization, build_sale_or_authorization_request(money, credit_card_or_store_authorization, options))
end
def purchase(money, credit_card_or_store_authorization, options = {})
commit(:sale, build_sale_or_authorization_request(money, credit_card_or_store_authorization, options))
end
def capture(money, authorization, options = {})
commit(:capture, build_capture_or_credit_request(money, authorization, options))
end
def void(authorization, options = {})
commit(:void, build_capture_or_credit_request(money_from_authorization(authorization), authorization, options))
end
def refund(money, authorization, options = {})
commit(:credit, build_capture_or_credit_request(money, authorization, options))
end
# Tokenize a credit card with TransArmor
#
# The TransArmor token and other card data necessary for subsequent
# transactions is stored in the response's +authorization+ attribute.
# The authorization string may be passed to +authorize+ and +purchase+
# instead of a +ActiveMerchant::Billing::CreditCard+ instance.
#
# TransArmor support must be explicitly activated on your gateway
# account by FirstData. If your authorization string is empty, contact
# FirstData support for account setup assistance.
#
# === Example
#
# # Generate token
# result = gateway.store(credit_card)
# if result.success?
# my_record.update_attributes(:authorization => result.authorization)
# end
#
# # Use token
# result = gateway.purchase(1000, my_record.authorization)
#
# https://firstdata.zendesk.com/entries/21303361-transarmor-tokenization
def store(credit_card, options = {})
commit(:store, build_store_request(credit_card, options), credit_card)
end
private
def build_request(action, body)
xml = Builder::XmlMarkup.new
xml.instruct!
xml.tag! "Transaction" do
add_credentials(xml)
add_transaction_type(xml, action)
xml << body
end
xml.target!
end
def build_sale_or_authorization_request(money, credit_card_or_store_authorization, options)
xml = Builder::XmlMarkup.new
add_amount(xml, money)
if credit_card_or_store_authorization.is_a? String
add_credit_card_token(xml, credit_card_or_store_authorization)
else
add_credit_card(xml, credit_card_or_store_authorization)
end
add_customer_data(xml, options)
add_invoice(xml, options)
xml.target!
end
def build_capture_or_credit_request(money, identification, options)
xml = Builder::XmlMarkup.new
add_identification(xml, identification)
add_amount(xml, money)
add_customer_data(xml, options)
xml.target!
end
def build_store_request(credit_card, options)
xml = Builder::XmlMarkup.new
add_credit_card(xml, credit_card)
add_customer_data(xml, options)
xml.target!
end
def add_credentials(xml)
xml.tag! "ExactID", @options[:login]
xml.tag! "Password", @options[:password]
end
def add_transaction_type(xml, action)
xml.tag! "Transaction_Type", TRANSACTIONS[action]
end
def add_identification(xml, identification)
authorization_num, transaction_tag, _ = identification.split(";")
xml.tag! "Authorization_Num", authorization_num
xml.tag! "Transaction_Tag", transaction_tag
end
def add_amount(xml, money)
xml.tag! "DollarAmount", amount(money)
end
def add_credit_card(xml, credit_card)
xml.tag! "Card_Number", credit_card.number
xml.tag! "Expiry_Date", expdate(credit_card)
xml.tag! "CardHoldersName", credit_card.name
xml.tag! "CardType", credit_card.brand
if credit_card.verification_value?
xml.tag! "CVD_Presence_Ind", "1"
xml.tag! "VerificationStr2", credit_card.verification_value
end
end
def add_credit_card_token(xml, store_authorization)
params = store_authorization.split(";")
credit_card = CreditCard.new(
:brand => params[1],
:first_name => params[2],
:last_name => params[3],
:month => params[4],
:year => params[5])
xml.tag! "TransarmorToken", params[0]
xml.tag! "Expiry_Date", expdate(credit_card)
xml.tag! "CardHoldersName", credit_card.name
xml.tag! "CardType", credit_card.brand
end
def add_customer_data(xml, options)
xml.tag! "Customer_Ref", options[:customer] if options[:customer]
xml.tag! "Client_IP", options[:ip] if options[:ip]
xml.tag! "Client_Email", options[:email] if options[:email]
end
def add_address(xml, options)
if address = (options[:billing_address] || options[:address])
xml.tag! "ZipCode", address[:zip]
end
end
def add_invoice(xml, options)
xml.tag! "Reference_No", options[:order_id]
xml.tag! "Reference_3", options[:description] if options[:description]
end
def expdate(credit_card)
"#{format(credit_card.month, :two_digits)}#{format(credit_card.year, :two_digits)}"
end
def commit(action, request, credit_card = nil)
url = (test? ? self.test_url : self.live_url)
begin
response = parse(ssl_post(url, build_request(action, request), POST_HEADERS))
rescue ResponseError => e
response = parse_error(e.response)
end
Response.new(successful?(response), message_from(response), response,
:test => test?,
:authorization => response_authorization(action, response, credit_card),
:avs_result => {:code => response[:avs]},
:cvv_result => response[:cvv2]
)
end
def successful?(response)
response[:transaction_approved] == SUCCESS
end
def response_authorization(action, response, credit_card)
if action == :store
store_authorization_from(response, credit_card)
else
authorization_from(response)
end
end
def authorization_from(response)
if response[:authorization_num] && response[:transaction_tag]
[
response[:authorization_num],
response[:transaction_tag],
(response[:dollar_amount].to_f * 100).to_i
].join(";")
else
""
end
end
def store_authorization_from(response, credit_card)
if response[:transarmor_token].present?
[
response[:transarmor_token],
credit_card.brand,
credit_card.first_name,
credit_card.last_name,
credit_card.month,
credit_card.year
].map { |value| value.to_s.gsub(/;/, "") }.join(";")
else
raise StandardError, "TransArmor support is not enabled on your #{display_name} account"
end
end
def money_from_authorization(auth)
_, _, amount = auth.split(/;/, 3)
amount.to_i # return the # of cents, no need to divide
end
def message_from(response)
if(response[:faultcode] && response[:faultstring])
response[:faultstring]
elsif(response[:error_number] && response[:error_number] != "0")
response[:error_description]
else
result = (response[:exact_message] || "")
result << " - #{response[:bank_message]}" if response[:bank_message].present?
result
end
end
def parse_error(error)
{
:transaction_approved => "false",
:error_number => error.code,
:error_description => error.body
}
end
def parse(xml)
response = {}
xml = REXML::Document.new(xml)
if root = REXML::XPath.first(xml, "//TransactionResult")
parse_elements(response, root)
end
response.delete_if{ |k,v| SENSITIVE_FIELDS.include?(k) }
end
def parse_elements(response, root)
root.elements.to_a.each do |node|
response[node.name.gsub(/EXact/, "Exact").underscore.to_sym] = (node.text || "").strip
end
end
end
end
end

View File

@@ -0,0 +1,257 @@
module ActiveMerchant #:nodoc:
module Billing #:nodoc:
class GarantiGateway < Gateway
self.live_url = self.test_url = 'https://sanalposprov.garanti.com.tr/VPServlet'
# The countries the gateway supports merchants from as 2 digit ISO country codes
self.supported_countries = ['US','TR']
# The card types supported by the payment gateway
self.supported_cardtypes = [:visa, :master, :american_express, :discover]
# The homepage URL of the gateway
self.homepage_url = 'https://sanalposweb.garanti.com.tr'
# The name of the gateway
self.display_name = 'Garanti Sanal POS'
self.default_currency = 'TRL'
self.money_format = :cents
CURRENCY_CODES = {
'YTL' => 949,
'TRL' => 949,
'TL' => 949,
'USD' => 840,
'EUR' => 978,
'GBP' => 826,
'JPY' => 392
}
def initialize(options = {})
requires!(options, :login, :password, :terminal_id, :merchant_id)
super
end
def purchase(money, credit_card, options = {})
options = options.merge(:gvp_order_type => "sales")
commit(money, build_sale_request(money, credit_card, options))
end
def authorize(money, credit_card, options = {})
options = options.merge(:gvp_order_type => "preauth")
commit(money, build_authorize_request(money, credit_card, options))
end
def capture(money, ref_id, options = {})
options = options.merge(:gvp_order_type => "postauth")
commit(money, build_capture_request(money, ref_id, options))
end
private
def security_data
rjusted_terminal_id = @options[:terminal_id].to_s.rjust(9, "0")
Digest::SHA1.hexdigest(@options[:password].to_s + rjusted_terminal_id).upcase
end
def generate_hash_data(order_id, terminal_id, credit_card_number, amount, security_data)
data = [order_id, terminal_id, credit_card_number, amount, security_data].join
Digest::SHA1.hexdigest(data).upcase
end
def build_xml_request(money, credit_card, options, &block)
card_number = credit_card.respond_to?(:number) ? credit_card.number : ''
hash_data = generate_hash_data(format_order_id(options[:order_id]), @options[:terminal_id], card_number, amount(money), security_data)
xml = Builder::XmlMarkup.new(:indent => 2)
xml.instruct! :xml, :version => "1.0", :encoding => "UTF-8"
xml.tag! 'GVPSRequest' do
xml.tag! 'Mode', test? ? 'TEST' : 'PROD'
xml.tag! 'Version', 'V0.01'
xml.tag! 'Terminal' do
xml.tag! 'ProvUserID', 'PROVAUT'
xml.tag! 'HashData', hash_data
xml.tag! 'UserID', @options[:login]
xml.tag! 'ID', @options[:terminal_id]
xml.tag! 'MerchantID', @options[:merchant_id]
end
if block_given?
yield xml
else
xml.target!
end
end
end
def build_sale_request(money, credit_card, options)
build_xml_request(money, credit_card, options) do |xml|
add_customer_data(xml, options)
add_order_data(xml, options) do |xml|
add_addresses(xml, options)
end
add_credit_card(xml, credit_card)
add_transaction_data(xml, money, options)
xml.target!
end
end
def build_authorize_request(money, credit_card, options)
build_xml_request(money, credit_card, options) do |xml|
add_customer_data(xml, options)
add_order_data(xml, options) do |xml|
add_addresses(xml, options)
end
add_credit_card(xml, credit_card)
add_transaction_data(xml, money, options)
xml.target!
end
end
def build_capture_request(money, ref_id, options)
options = options.merge(:order_id => ref_id)
build_xml_request(money, ref_id, options) do |xml|
add_customer_data(xml, options)
add_order_data(xml, options)
add_transaction_data(xml, money, options)
xml.target!
end
end
def add_customer_data(xml, options)
xml.tag! 'Customer' do
xml.tag! 'IPAddress', options[:ip] || '1.1.1.1'
xml.tag! 'EmailAddress', options[:email]
end
end
def add_order_data(xml, options, &block)
xml.tag! 'Order' do
xml.tag! 'OrderID', format_order_id(options[:order_id])
xml.tag! 'GroupID'
if block_given?
yield xml
end
end
end
def add_credit_card(xml, credit_card)
xml.tag! 'Card' do
xml.tag! 'Number', credit_card.number
xml.tag! 'ExpireDate', [format_exp(credit_card.month), format_exp(credit_card.year)].join
xml.tag! 'CVV2', credit_card.verification_value
end
end
def format_exp(value)
format(value, :two_digits)
end
# OrderId field must be A-Za-z0-9_ format and max 36 char
def format_order_id(order_id)
order_id.to_s.gsub(/[^A-Za-z0-9_]/, '')[0...36]
end
def add_addresses(xml, options)
xml.tag! 'AddressList' do
if billing_address = options[:billing_address] || options[:address]
xml.tag! 'Address' do
xml.tag! 'Type', 'B'
add_address(xml, billing_address)
end
end
if options[:shipping_address]
xml.tag! 'Address' do
xml.tag! 'Type', 'S'
add_address(xml, options[:shipping_address])
end
end
end
end
def add_address(xml, address)
xml.tag! 'Name', normalize(address[:name])
address_text = address[:address1]
address_text << " #{ address[:address2]}" if address[:address2]
xml.tag! 'Text', normalize(address_text)
xml.tag! 'City', normalize(address[:city])
xml.tag! 'District', normalize(address[:state])
xml.tag! 'PostalCode', address[:zip]
xml.tag! 'Country', normalize(address[:country])
xml.tag! 'Company', normalize(address[:company])
xml.tag! 'PhoneNumber', address[:phone].to_s.gsub(/[^0-9]/, '') if address[:phone]
end
def normalize(text)
return unless text
if ActiveSupport::Inflector.method(:transliterate).arity == -2
ActiveSupport::Inflector.transliterate(text,'')
elsif RUBY_VERSION >= '1.9'
text.gsub(/[^\x00-\x7F]+/, '')
else
ActiveSupport::Inflector.transliterate(text).to_s
end
end
def add_transaction_data(xml, money, options)
xml.tag! 'Transaction' do
xml.tag! 'Type', options[:gvp_order_type]
xml.tag! 'Amount', amount(money)
xml.tag! 'CurrencyCode', currency_code(options[:currency] || currency(money))
xml.tag! 'CardholderPresentCode', 0
end
end
def currency_code(currency)
CURRENCY_CODES[currency] || CURRENCY_CODES[default_currency]
end
def commit(money,request)
raw_response = ssl_post(self.live_url, "data=" + request)
response = parse(raw_response)
success = success?(response)
Response.new(success,
success ? 'Approved' : "Declined (Reason: #{response[:reason_code]} - #{response[:error_msg]} - #{response[:sys_err_msg]})",
response,
:test => test?,
:authorization => response[:order_id])
end
def parse(body)
xml = REXML::Document.new(body)
response = {}
xml.root.elements.to_a.each do |node|
parse_element(response, node)
end
response
end
def parse_element(response, node)
if node.has_elements?
node.elements.each{|element| parse_element(response, element) }
else
response[node.name.underscore.to_sym] = node.text
end
end
def success?(response)
response[:message] == "Approved"
end
end
end
end

View File

@@ -0,0 +1,207 @@
require "nokogiri"
module ActiveMerchant #:nodoc:
module Billing #:nodoc:
class HdfcGateway < Gateway
self.display_name = "HDFC"
self.homepage_url = "http://www.hdfcbank.com/sme/sme-details/merchant-services/guzh6m0i"
self.test_url = "https://securepgtest.fssnet.co.in/pgway/servlet/"
self.live_url = "https://securepg.fssnet.co.in/pgway/servlet/"
self.supported_countries = ["IN"]
self.default_currency = "INR"
self.money_format = :dollars
self.supported_cardtypes = [:visa, :master, :discover, :diners_club]
def initialize(options={})
requires!(options, :login, :password)
super
end
def purchase(amount, payment_method, options={})
post = {}
add_invoice(post, amount, options)
add_payment_method(post, payment_method)
add_customer_data(post, options)
commit("purchase", post)
end
def authorize(amount, payment_method, options={})
post = {}
add_invoice(post, amount, options)
add_payment_method(post, payment_method)
add_customer_data(post, options)
commit("authorize", post)
end
def capture(amount, authorization, options={})
post = {}
add_invoice(post, amount, options)
add_reference(post, authorization)
add_customer_data(post, options)
commit("capture", post)
end
def refund(amount, authorization, options={})
post = {}
add_invoice(post, amount, options)
add_reference(post, authorization)
add_customer_data(post, options)
commit("refund", post)
end
private
CURRENCY_CODES = Hash.new{|h,k| raise ArgumentError.new("Unsupported currency for HDFC: #{k}")}
CURRENCY_CODES["AED"] = "784"
CURRENCY_CODES["AUD"] = "036"
CURRENCY_CODES["CAD"] = "124"
CURRENCY_CODES["EUR"] = "978"
CURRENCY_CODES["GBP"] = "826"
CURRENCY_CODES["INR"] = "356"
CURRENCY_CODES["OMR"] = "512"
CURRENCY_CODES["QAR"] = "634"
CURRENCY_CODES["SGD"] = "702"
CURRENCY_CODES["USD"] = "840"
def add_invoice(post, amount, options)
post[:amt] = amount(amount)
post[:currencycode] = CURRENCY_CODES[options[:currency] || currency(amount)]
post[:trackid] = escape(options[:order_id], 40) if options[:order_id]
post[:udf1] = escape(options[:description]) if options[:description]
post[:eci] = options[:eci] if options[:eci]
end
def add_customer_data(post, options)
post[:udf2] = escape(options[:email]) if options[:email]
if address = (options[:billing_address] || options[:address])
post[:udf3] = escape(address[:phone]) if address[:phone]
post[:udf4] = escape(<<EOA)
#{address[:name]}
#{address[:company]}
#{address[:address1]}
#{address[:address2]}
#{address[:city]} #{address[:state]} #{address[:zip]}
#{address[:country]}
EOA
end
end
def add_payment_method(post, payment_method)
post[:member] = escape(payment_method.name, 30)
post[:card] = escape(payment_method.number)
post[:cvv2] = escape(payment_method.verification_value)
post[:expyear] = format(payment_method.year, :four_digits)
post[:expmonth] = format(payment_method.month, :two_digits)
end
def add_reference(post, authorization)
tranid, member = split_authorization(authorization)
post[:transid] = tranid
post[:member] = member
end
def parse(xml)
response = {}
doc = Nokogiri::XML.fragment(fix_xml(xml))
doc.children.each do |node|
if node.text?
next
elsif (node.elements.size == 0)
response[node.name.downcase.to_sym] = node.text
else
node.elements.each do |childnode|
name = "#{node.name.downcase}_#{childnode.name.downcase}"
response[name.to_sym] = childnode.text
end
end
end
response
end
def fix_xml(xml)
xml.gsub(/&(?!(?:amp|quot|apos|lt|gt);)/, "&amp;")
end
ACTIONS = {
"purchase" => "1",
"refund" => "2",
"authorize" => "4",
"capture" => "5",
}
def commit(action, post)
post[:id] = @options[:login]
post[:password] = @options[:password]
post[:action] = ACTIONS[action] if ACTIONS[action]
raw = parse(ssl_post(url(action), build_request(post)))
succeeded = success_from(raw[:result])
Response.new(
succeeded,
message_from(succeeded, raw),
raw,
:authorization => authorization_from(post, raw),
:test => test?
)
end
def build_request(post)
xml = Builder::XmlMarkup.new :indent => 2
xml.instruct!
post.each do |field, value|
xml.tag!(field, value)
end
xml.target!
end
def url(action)
endpoint = "TranPortalXMLServlet"
(test? ? test_url : live_url) + endpoint
end
def success_from(result)
case result
when "CAPTURED", "APPROVED", "NOT ENROLLED", "ENROLLED"
true
else
false
end
end
def message_from(succeeded, response)
if succeeded
"Succeeded"
else
(response[:error_text] || response[:result] || "Unable to read error message").split("-").last
end
end
def authorization_from(request, response)
[response[:tranid], request[:member]].join("|")
end
def split_authorization(authorization)
tranid, member = authorization.split("|")
[tranid, member]
end
def escape(string, max_length=250)
return "" unless string
if max_length
string = string[0...max_length]
end
string.gsub(/[^A-Za-z0-9 \-_@\.\n]/, '')
end
end
end
end

View File

@@ -0,0 +1,249 @@
require File.dirname(__FILE__) + '/ideal_response'
module ActiveMerchant #:nodoc:
module Billing #:nodoc:
# Implementation contains some simplifications
# - does not support multiple subID per merchant
# - language is fixed to 'nl'
class IdealBaseGateway < Gateway
class_attribute :server_pem, :pem_password, :default_expiration_period
self.default_expiration_period = 'PT10M'
self.default_currency = 'EUR'
self.pem_password = true
self.abstract_class = true
# These constants will never change for most users
AUTHENTICATION_TYPE = 'SHA1_RSA'
LANGUAGE = 'nl'
SUB_ID = '0'
API_VERSION = '1.1.0'
def initialize(options = {})
requires!(options, :login, :password, :pem)
options[:pem_password] = options[:password]
super
end
# Setup transaction. Get redirect_url from response.service_url
def setup_purchase(money, options = {})
requires!(options, :issuer_id, :return_url, :order_id, :currency, :description, :entrance_code)
commit(build_transaction_request(money, options))
end
# Check status of transaction and confirm payment
# transaction_id must be a valid transaction_id from a prior setup.
def capture(transaction, options = {})
options[:transaction_id] = transaction
commit(build_status_request(options))
end
# Get list of issuers from response.issuer_list
def issuers
commit(build_directory_request)
end
private
def url
(test? ? test_url : live_url)
end
def token
if @token.nil?
@token = create_fingerprint(@options[:pem])
end
@token
end
# <?xml version="1.0" encoding="UTF-8"?>
# <AcquirerTrxReq xmlns="http://www.idealdesk.com/Message" version="1.1.0">
# <createDateTimeStamp>2001-12-17T09:30:47.0Z</createDateTimeStamp>
# <Issuer>
# <issuerID>1003</issuerID>
# </Issuer>
# <Merchant>
# <merchantID>000123456</merchantID>
# <subID>0</subID>
# <authentication>passkey</authentication>
# <token>1</token>
# <tokenCode>3823ad872eff23</tokenCode>
# <merchantReturnURL>https://www.mijnwinkel.nl/betaalafhandeling
# </merchantReturnURL>
# </Merchant>
# <Transaction>
# <purchaseID>iDEAL-aankoop 21</purchaseID>
# <amount>5999</amount>
# <currency>EUR</currency>
# <expirationPeriod>PT3M30S</expirationPeriod>
# <language>nl</language>
# <description>Documentensuite</description>
# <entranceCode>D67tyx6rw9IhY71</entranceCode>
# </Transaction>
# </AcquirerTrxReq>
def build_transaction_request(money, options)
date_time_stamp = create_time_stamp
message = date_time_stamp +
options[:issuer_id] +
@options[:login] +
SUB_ID +
options[:return_url] +
options[:order_id] +
money.to_s +
(options[:currency] || currency(money)) +
LANGUAGE +
options[:description] +
options[:entrance_code]
token_code = sign_message(@options[:pem], @options[:password], message)
xml = Builder::XmlMarkup.new(:indent => 2)
xml.instruct!
xml.tag! 'AcquirerTrxReq', 'xmlns' => 'http://www.idealdesk.com/Message', 'version' => API_VERSION do
xml.tag! 'createDateTimeStamp', date_time_stamp
xml.tag! 'Issuer' do
xml.tag! 'issuerID', options[:issuer_id]
end
xml.tag! 'Merchant' do
xml.tag! 'merchantID', @options[:login]
xml.tag! 'subID', SUB_ID
xml.tag! 'authentication', AUTHENTICATION_TYPE
xml.tag! 'token', token
xml.tag! 'tokenCode', token_code
xml.tag! 'merchantReturnURL', options[:return_url]
end
xml.tag! 'Transaction' do
xml.tag! 'purchaseID', options[:order_id]
xml.tag! 'amount', money
xml.tag! 'currency', options[:currency]
xml.tag! 'expirationPeriod', options[:expiration_period] || default_expiration_period
xml.tag! 'language', LANGUAGE
xml.tag! 'description', options[:description]
xml.tag! 'entranceCode', options[:entrance_code]
end
xml.target!
end
end
# <?xml version="1.0" encoding="UTF-8"?>
# <AcquirerStatusReq xmlns="http://www.idealdesk.com/Message" version="1.1.0">
# <createDateTimeStamp>2001-12-17T09:30:47.0Z</createDateTimeStamp>
# <Merchant>
# <merchantID>000123456</merchantID>
# <subID>0</subID>
# <authentication>keyed hash</authentication>
# <token>1</token>
# <tokenCode>3823ad872eff23</tokenCode>
# </Merchant>
# <Transaction>
# <transactionID>0001023456789112</transactionID>
# </Transaction>
# </AcquirerStatusReq>
def build_status_request(options)
datetimestamp = create_time_stamp
message = datetimestamp + @options[:login] + SUB_ID + options[:transaction_id]
tokenCode = sign_message(@options[:pem], @options[:password], message)
xml = Builder::XmlMarkup.new(:indent => 2)
xml.instruct!
xml.tag! 'AcquirerStatusReq', 'xmlns' => 'http://www.idealdesk.com/Message', 'version' => API_VERSION do
xml.tag! 'createDateTimeStamp', datetimestamp
xml.tag! 'Merchant' do
xml.tag! 'merchantID', @options[:login]
xml.tag! 'subID', SUB_ID
xml.tag! 'authentication' , AUTHENTICATION_TYPE
xml.tag! 'token', token
xml.tag! 'tokenCode', tokenCode
end
xml.tag! 'Transaction' do
xml.tag! 'transactionID', options[:transaction_id]
end
end
xml.target!
end
# <?xml version="1.0" encoding="UTF-8"?>
# <DirectoryReq xmlns="http://www.idealdesk.com/Message" version="1.1.0">
# <createDateTimeStamp>2001-12-17T09:30:47.0Z</createDateTimeStamp>
# <Merchant>
# <merchantID>000000001</merchantID>
# <subID>0</subID>
# <authentication>1</authentication>
# <token>hashkey</token>
# <tokenCode>WajqV1a3nDen0be2r196g9FGFF=</tokenCode>
# </Merchant>
# </DirectoryReq>
def build_directory_request
datetimestamp = create_time_stamp
message = datetimestamp + @options[:login] + SUB_ID
tokenCode = sign_message(@options[:pem], @options[:password], message)
xml = Builder::XmlMarkup.new(:indent => 2)
xml.instruct!
xml.tag! 'DirectoryReq', 'xmlns' => 'http://www.idealdesk.com/Message', 'version' => API_VERSION do
xml.tag! 'createDateTimeStamp', datetimestamp
xml.tag! 'Merchant' do
xml.tag! 'merchantID', @options[:login]
xml.tag! 'subID', SUB_ID
xml.tag! 'authentication', AUTHENTICATION_TYPE
xml.tag! 'token', token
xml.tag! 'tokenCode', tokenCode
end
end
xml.target!
end
def commit(request)
raw_response = ssl_post(url, request)
response = Hash.from_xml(raw_response.to_s)
response_type = response.keys[0]
case response_type
when 'AcquirerTrxRes', 'DirectoryRes'
success = true
when 'ErrorRes'
success = false
when 'AcquirerStatusRes'
raise SecurityError, "Message verification failed.", caller unless status_response_verified?(response)
success = (response['AcquirerStatusRes']['Transaction']['status'] == 'Success')
else
raise ArgumentError, "Unknown response type.", caller
end
return IdealResponse.new(success, response.keys[0], response, :test => test?)
end
def create_fingerprint(cert_file)
cert_data = OpenSSL::X509::Certificate.new(cert_file).to_s
cert_data = cert_data.sub(/-----BEGIN CERTIFICATE-----/, '')
cert_data = cert_data.sub(/-----END CERTIFICATE-----/, '')
fingerprint = Base64.decode64(cert_data)
fingerprint = Digest::SHA1.hexdigest(fingerprint)
return fingerprint.upcase
end
def sign_message(private_key_data, password, data)
private_key = OpenSSL::PKey::RSA.new(private_key_data, password)
signature = private_key.sign(OpenSSL::Digest::SHA1.new, data.gsub('\s', ''))
return Base64.encode64(signature).gsub(/\n/, '')
end
def verify_message(cert_file, data, signature)
public_key = OpenSSL::X509::Certificate.new(cert_file).public_key
return public_key.verify(OpenSSL::Digest::SHA1.new, Base64.decode64(signature), data)
end
def status_response_verified?(response)
transaction = response['AcquirerStatusRes']['Transaction']
message = response['AcquirerStatusRes']['createDateTimeStamp'] + transaction['transactionID' ] + transaction['status']
message << transaction['consumerAccountNumber'].to_s
verify_message(server_pem, message, response['AcquirerStatusRes']['Signature']['signatureValue'])
end
def create_time_stamp
Time.now.gmtime.strftime('%Y-%m-%dT%H:%M:%S.000Z')
end
end
end
end

View File

@@ -0,0 +1,13 @@
-----BEGIN CERTIFICATE-----
MIICQDCCAakCBELvbPYwDQYJKoZIhvcNAQEEBQAwZzELMAkGA1UEBhMCREUxDzANBgNVBAgTBkhl
c3NlbjESMBAGA1UEBxMJRnJhbmtmdXJ0MQ4wDAYDVQQKEwVpREVBTDEOMAwGA1UECxMFaURFQUwx
EzARBgNVBAMTCmlERUFMIFJhYm8wHhcNMDUwODAyMTI1NDE0WhcNMTUwNzMxMTI1NDE0WjBnMQsw
CQYDVQQGEwJERTEPMA0GA1UECBMGSGVzc2VuMRIwEAYDVQQHEwlGcmFua2Z1cnQxDjAMBgNVBAoT
BWlERUFMMQ4wDAYDVQQLEwVpREVBTDETMBEGA1UEAxMKaURFQUwgUmFibzCBnzANBgkqhkiG9w0B
AQEFAAOBjQAwgYkCgYEA486iIKVhr8RNjxH+PZ3yTWx/8k2fqDFm8XU8I1Z5esRmPFnXmlgA8cG7
e9AaBPaLoP7Dc8dRQoUO66KMakzGI/WAVdHIJiiKrz8xOcioIgrzPSqec7aqse3J28UraEHkGESJ
7dAW7Pw/shrmpmkzKsunLt6AkXss4W3JUndZUN0CAwEAATANBgkqhkiG9w0BAQQFAAOBgQCGy/FK
Lotp2ZOTtuLMgvDy74eicq/Znv4bLfpglzAPHycRHcHsXuer/lNHyvpKf2gdYe+IFalUW3OJZWIM
jpm4UniJ16RPdgwWVRJEdPr/P7JXMIqD2IEOgujuuTQ7x0VgCf9XrsPsP9ZR5DIPcDDhbrpSE0yF
Do77nwG61xMaGA==
-----END CERTIFICATE-----

View File

@@ -0,0 +1,29 @@
module ActiveMerchant #:nodoc:
module Billing #:nodoc:
class IdealResponse < Response
def issuer_list
list = @params.values[0]['Directory']['Issuer']
case list
when Hash
return [list]
when Array
return list
end
end
def service_url
@params.values[0]['Issuer']['issuerAuthenticationURL']
end
def transaction
@params.values[0]['Transaction']
end
def error
@params.values[0]['Error']
end
end
end
end

View File

@@ -0,0 +1,66 @@
require File.dirname(__FILE__) + '/ideal/ideal_base'
module ActiveMerchant #:nodoc:
module Billing #:nodoc:
# First, make sure you have everything setup correctly and all of your dependencies in place with:
#
# require 'rubygems'
# require 'active_merchant'
#
# ActiveMerchant expects the amounts to be given as an Integer in cents. In this case, 10 EUR becomes 1000.
#
# Create certificates for authentication:
#
# The PEM file expected should contain both the certificate and the generated PEM file.
# Some sample shell commands to generate the certificates:
#
# openssl genrsa -aes128 -out priv.pem -passout pass:[YOUR PASSWORD] 1024
# openssl req -x509 -new -key priv.pem -passin pass:[YOUR PASSWORD] -days 3000 -out cert.cer
# cat cert.cer priv.pem > ideal.pem
#
# Following the steps above, upload cert.cer to the ideal web interface and pass the path of ideal.pem to the :pem option.
#
# Configure the gateway using your iDEAL bank account info and security settings:
#
# Create gateway:
# gateway = ActiveMerchant::Billing::IdealRabobankGateway.new(
# :login => '123456789', # 9 digit merchant number
# :pem => File.read(Rails.root + 'config/ideal.pem'),
# :password => 'password' # password for the PEM key
# )
#
# Get list of issuers to fill selection list on your payment form:
# response = gateway.issuers
# list = response.issuer_list
#
# Request transaction:
#
# options = {
# :issuer_id => '0001',
# :expiration_period => 'PT10M',
# :return_url => 'http://www.return.url',
# :order_id => '1234567890123456',
# :currency => 'EUR',
# :description => 'Een omschrijving',
# :entrance_code => '1234'
# }
#
# response = gateway.setup_purchase(amount, options)
# transaction_id = response.transaction['transactionID']
# redirect_url = response.service_url
#
# Mandatory status request will confirm transaction:
# response = gateway.capture(transaction_id)
#
# Implementation contains some simplifications
# - does not support multiple subID per merchant
# - language is fixed to 'nl'
class IdealRabobankGateway < IdealBaseGateway
class_attribute :test_url, :live_url
self.test_url = 'https://idealtest.rabobank.nl/ideal/iDeal'
self.live_url = 'https://ideal.rabobank.nl/ideal/iDeal'
self.server_pem = File.read(File.dirname(__FILE__) + '/ideal/ideal_rabobank.pem')
end
end
end

View File

@@ -0,0 +1,221 @@
require File.join(File.dirname(__FILE__), '..', 'check.rb')
module ActiveMerchant #:nodoc:
module Billing #:nodoc:
class InspireGateway < Gateway
self.live_url = self.test_url = 'https://secure.inspiregateway.net/api/transact.php'
self.supported_countries = ['US']
self.supported_cardtypes = [:visa, :master, :american_express]
self.homepage_url = 'http://www.inspiregateway.com'
self.display_name = 'Inspire Commerce'
# Creates a new InspireGateway
#
# The gateway requires that a valid login and password be passed
# in the +options+ hash.
#
# ==== Options
#
# * <tt>:login</tt> -- The Inspire Username.
# * <tt>:password</tt> -- The Inspire Passowrd.
# See the Inspire Integration Guide for details. (default: +false+)
def initialize(options = {})
requires!(options, :login, :password)
super
end
# Pass :store => true in the options to store the
# payment info at Inspire Gateway and get a generated
# customer_vault_id in the response.
# Pass :store => some_number_or_string to specify the
# customer_vault_id InspireGateway should use (make sure it's
# unique).
def authorize(money, creditcard, options = {})
post = {}
add_invoice(post, options)
add_payment_source(post, creditcard,options)
add_address(post, creditcard, options)
add_customer_data(post, options)
commit('auth', money, post)
end
def purchase(money, payment_source, options = {})
post = {}
add_invoice(post, options)
add_payment_source(post, payment_source, options)
add_address(post, payment_source, options)
add_customer_data(post, options)
commit('sale', money, post)
end
def capture(money, authorization, options = {})
post ={}
post[:transactionid] = authorization
commit('capture', money, post)
end
def void(authorization, options = {})
post ={}
post[:transactionid] = authorization
commit('void', nil, post)
end
# Update the values (such as CC expiration) stored at
# InspireGateway. The CC number must be supplied in the
# CreditCard object.
def update(vault_id, creditcard, options = {})
post = {}
post[:customer_vault] = "update_customer"
add_customer_vault_id(post, vault_id)
add_creditcard(post, creditcard, options)
add_address(post, creditcard, options)
add_customer_data(post, options)
commit(nil, nil, post)
end
def delete(vault_id)
post = {}
post[:customer_vault] = "delete_customer"
add_customer_vault_id(post, vault_id)
commit(nil, nil, post)
end
# To match the other stored-value gateways, like TrustCommerce,
# store and unstore need to be defined
def store(creditcard, options = {})
billing_id = options.delete(:billing_id).to_s || true
authorize(100, creditcard, options.merge(:store => billing_id))
end
alias_method :unstore, :delete
private
def add_customer_data(post, options)
if options.has_key? :email
post[:email] = options[:email]
end
if options.has_key? :ip
post[:ipaddress] = options[:ip]
end
end
def add_address(post, creditcard, options)
if address = options[:billing_address] || options[:address]
post[:address1] = address[:address1].to_s
post[:address2] = address[:address2].to_s unless address[:address2].blank?
post[:company] = address[:company].to_s
post[:phone] = address[:phone].to_s
post[:zip] = address[:zip].to_s
post[:city] = address[:city].to_s
post[:country] = address[:country].to_s
post[:state] = address[:state].blank? ? 'n/a' : address[:state]
end
end
def add_invoice(post, options)
post[:orderid] = options[:order_id].to_s.gsub(/[^\w.]/, '')
post[:orderdescription] = options[:description]
end
def add_payment_source(params, source, options={})
case determine_funding_source(source)
when :vault then add_customer_vault_id(params, source)
when :credit_card then add_creditcard(params, source, options)
when :check then add_check(params, source)
end
end
def add_customer_vault_id(params,vault_id)
params[:customer_vault_id] = vault_id
end
def add_creditcard(post, creditcard,options)
if options[:store]
post[:customer_vault] = "add_customer"
post[:customer_vault_id] = options[:store] unless options[:store] == true
end
post[:ccnumber] = creditcard.number
post[:cvv] = creditcard.verification_value if creditcard.verification_value?
post[:ccexp] = expdate(creditcard)
post[:firstname] = creditcard.first_name
post[:lastname] = creditcard.last_name
end
def add_check(post, check)
post[:payment] = 'check' # Set transaction to ACH
post[:checkname] = check.name # The name on the customer's Checking Account
post[:checkaba] = check.routing_number # The customer's bank routing number
post[:checkaccount] = check.account_number # The customer's account number
post[:account_holder_type] = check.account_holder_type # The customer's type of ACH account
post[:account_type] = check.account_type # The customer's type of ACH account
end
def parse(body)
results = {}
body.split(/&/).each do |pair|
key,val = pair.split(/=/)
results[key] = val
end
results
end
def commit(action, money, parameters)
parameters[:amount] = amount(money) if money
response = parse( ssl_post(self.live_url, post_data(action,parameters)) )
Response.new(response["response"] == "1", message_from(response), response,
:authorization => response["transactionid"],
:test => test?,
:cvv_result => response["cvvresponse"],
:avs_result => { :code => response["avsresponse"] }
)
end
def expdate(creditcard)
year = sprintf("%.4i", creditcard.year)
month = sprintf("%.2i", creditcard.month)
"#{month}#{year[-2..-1]}"
end
def message_from(response)
case response["responsetext"]
when "SUCCESS","Approved"
"This transaction has been approved"
when "DECLINE"
"This transaction has been declined"
else
response["responsetext"]
end
end
def post_data(action, parameters = {})
post = {}
post[:username] = @options[:login]
post[:password] = @options[:password]
post[:type] = action if action
request = post.merge(parameters).map {|key,value| "#{key}=#{CGI.escape(value.to_s)}"}.join("&")
request
end
def determine_funding_source(source)
case
when source.is_a?(String) then :vault
when CreditCard.card_companies.keys.include?(card_brand(source)) then :credit_card
when card_brand(source) == 'check' then :check
else raise ArgumentError, "Unsupported funding source provided"
end
end
end
end
end

View File

@@ -0,0 +1,163 @@
module ActiveMerchant #:nodoc:
module Billing #:nodoc:
class InstapayGateway < Gateway
self.live_url = 'https://trans.instapaygateway.com/cgi-bin/process.cgi'
# The countries the gateway supports merchants from as 2 digit ISO country codes
self.supported_countries = ['US']
self.money_format = :dollars
self.default_currency = 'USD'
# The card types supported by the payment gateway
self.supported_cardtypes = [:visa, :master, :american_express, :discover]
# The homepage URL of the gateway
self.homepage_url = 'http://www.instapayllc.com'
# The name of the gateway
self.display_name = 'InstaPay'
SUCCESS = "Accepted"
SUCCESS_MESSAGE = "The transaction has been approved"
def initialize(options = {})
requires!(options, :login)
super
end
def authorize(money, creditcard, options = {})
post = {}
post[:authonly] = 1
add_amount(post, money)
add_invoice(post, options)
add_creditcard(post, creditcard)
add_address(post, options)
add_customer_data(post, options)
commit('ns_quicksale_cc', post)
end
def purchase(money, creditcard, options = {})
post = {}
add_amount(post, money)
add_invoice(post, options)
add_creditcard(post, creditcard)
add_address(post, options)
add_customer_data(post, options)
commit('ns_quicksale_cc', post)
end
def capture(money, authorization, options = {})
post = {}
add_amount(post, money)
add_reference(post, authorization)
commit('ns_quicksale_cc', post)
end
private
def add_amount(post, money)
post[:amount] = amount(money)
end
def add_reference(post, reference)
post[:postonly] = reference
end
def add_customer_data(post, options)
post[:ci_email] = options[:email]
post["ci_IP Address"] = options[:ip]
end
def add_address(post, options)
if address = options[:billing_address] || options[:address]
post[:ci_billaddr1] = address[:address1]
post[:ci_billaddr2] = address[:address2]
post[:ci_billcity] = address[:city]
post[:ci_billstate] = address[:state]
post[:ci_billzip] = address[:zip]
post[:ci_billcountry] = address[:country]
post[:ci_phone] = address[:phone]
end
if address = options[:shipping_address]
post[:ci_shipaddr1] = address[:address1]
post[:ci_shipaddr2] = address[:address2]
post[:ci_shipcity] = address[:city]
post[:ci_shipstate] = address[:state]
post[:ci_shipzip] = address[:zip]
post[:ci_shipcountry] = address[:country]
end
end
def add_invoice(post, options)
post[:merchantordernumber] = options[:order_id]
post[:ci_memo] = options[:description]
post[:pocustomerrefid] = options[:invoice]
end
def add_creditcard(post, creditcard)
post[:ccnum] = creditcard.number
post[:expmon] = format(creditcard.month, :two_digits)
post[:cvv2] = creditcard.verification_value if creditcard.verification_value?
post[:expyear] = creditcard.year
post[:ccname] = creditcard.name
end
def parse(body)
results = {}
fields = body.split("\r\n")
response = fields[1].split('=')
response_data = response[1].split(':')
if response[0] == SUCCESS
results[:success] = true
results[:message] = SUCCESS_MESSAGE
results[:transaction_type] = response_data[0]
results[:authorization_code] = response_data[1]
results[:reference_number] = response_data[2]
results[:batch_number] = response_data[3]
results[:transaction_id] = response_data[4]
results[:avs_result] = response_data[5]
results[:authorize_net] = response_data[6]
results[:cvv_result] = response_data[7]
else
results[:success] = false
results[:result] = response_data[0]
results[:response_code] = response_data[1]
results[:message] = response_data[2]
end
fields[1..-1].each do |pair|
key, value = pair.split('=')
results[key] = value
end
results
end
def commit(action, parameters)
data = ssl_post self.live_url, post_data(action, parameters)
response = parse(data)
Response.new(response[:success] , response[:message], response,
:authorization => response[:transaction_id],
:avs_result => { :code => response[:avs_result] },
:cvv_result => response[:cvv_result]
)
end
def post_data(action, parameters = {})
post = {}
post[:acctid] = @options[:login]
if(@options[:password])
post[:merchantpin] = @options[:password]
end
post[:action] = action
request = post.merge(parameters).collect { |key, value| "#{key}=#{CGI.escape(value.to_s)}" }.join("&")
request
end
end
end
end

View File

@@ -0,0 +1,262 @@
module ActiveMerchant #:nodoc:
module Billing #:nodoc:
# For more information on the Iridium Gateway please download the
# documentation from their Merchant Management System.
#
# The login and password are not the username and password you use to
# login to the Iridium Merchant Management System. Instead, you will
# use the API username and password you were issued separately.
class IridiumGateway < Gateway
self.live_url = self.test_url = 'https://gw1.iridiumcorp.net/'
# The countries the gateway supports merchants from as 2 digit ISO country codes
self.supported_countries = ['GB', 'ES']
self.default_currency = 'EUR'
self.money_format = :cents
# The card types supported by the payment gateway
self.supported_cardtypes = [:visa, :master, :american_express, :discover, :maestro, :jcb, :solo, :diners_club]
# The homepage URL of the gateway
self.homepage_url = 'http://www.iridiumcorp.co.uk/'
# The name of the gateway
self.display_name = 'Iridium'
CURRENCY_CODES = {
"AUD" => '036',
"CAD" => '124',
"EUR" => '978',
"GBP" => '826',
"MXN" => '484',
"NZD" => '554',
"USD" => '840',
}
def initialize(options = {})
requires!(options, :login, :password)
super
end
def authorize(money, payment_source, options = {})
setup_address_hash(options)
if payment_source.respond_to?(:number)
commit(build_purchase_request('PREAUTH', money, payment_source, options), options)
else
commit(build_reference_request('PREAUTH', money, payment_source, options), options)
end
end
def purchase(money, payment_source, options = {})
setup_address_hash(options)
if payment_source.respond_to?(:number)
commit(build_purchase_request('SALE', money, payment_source, options), options)
else
commit(build_reference_request('SALE', money, payment_source, options), options)
end
end
def capture(money, authorization, options = {})
commit(build_reference_request('COLLECTION', money, authorization, options), options)
end
def credit(money, authorization, options={})
deprecated CREDIT_DEPRECATION_MESSAGE
refund(money, authorization, options)
end
def refund(money, authorization, options={})
commit(build_reference_request('REFUND', money, authorization, options), options)
end
def void(authorization, options={})
commit(build_reference_request('VOID', nil, authorization, options), options)
end
private
def build_purchase_request(type, money, creditcard, options)
options.merge!(:action => 'CardDetailsTransaction')
build_request(options) do |xml|
add_purchase_data(xml, type, money, options)
add_creditcard(xml, creditcard)
add_customerdetails(xml, creditcard, options[:billing_address], options)
end
end
def build_reference_request(type, money, authorization, options)
options.merge!(:action => 'CrossReferenceTransaction')
order_id, cross_reference, auth_id = authorization.split(";")
build_request(options) do |xml|
if money
details = {'CurrencyCode' => currency_code(options[:currency] || default_currency), 'Amount' => amount(money)}
else
details = {'CurrencyCode' => currency_code(default_currency), 'Amount' => '0'}
end
xml.tag! 'TransactionDetails', details do
xml.tag! 'MessageDetails', {'TransactionType' => type, 'CrossReference' => cross_reference}
xml.tag! 'OrderID', (options[:order_id] || order_id)
end
end
end
def build_request(options)
requires!(options, :action)
xml = Builder::XmlMarkup.new :indent => 2
xml.instruct!(:xml, :version => '1.0', :encoding => 'utf-8')
xml.tag! 'soap:Envelope', { 'xmlns:soap' => 'http://schemas.xmlsoap.org/soap/envelope/',
'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance',
'xmlns:xsd' => 'http://www.w3.org/2001/XMLSchema'} do
xml.tag! 'soap:Body' do
xml.tag! options[:action], {'xmlns' => "https://www.thepaymentgateway.net/"} do
xml.tag! 'PaymentMessage' do
add_merchant_data(xml, options)
yield(xml)
end
end
end
end
xml.target!
end
def setup_address_hash(options)
options[:billing_address] = options[:billing_address] || options[:address] || {}
options[:shipping_address] = options[:shipping_address] || {}
end
def add_purchase_data(xml, type, money, options)
requires!(options, :order_id)
xml.tag! 'TransactionDetails', {'Amount' => amount(money), 'CurrencyCode' => currency_code(options[:currency] || currency(money))} do
xml.tag! 'MessageDetails', {'TransactionType' => type}
xml.tag! 'OrderID', options[:order_id]
xml.tag! 'TransactionControl' do
xml.tag! 'ThreeDSecureOverridePolicy', 'FALSE'
xml.tag! 'EchoAVSCheckResult', 'TRUE'
xml.tag! 'EchoCV2CheckResult', 'TRUE'
end
end
end
def add_customerdetails(xml, creditcard, address, options, shipTo = false)
xml.tag! 'CustomerDetails' do
if address
unless address[:country].blank?
country_code = Country.find(address[:country]).code(:numeric)
end
xml.tag! 'BillingAddress' do
xml.tag! 'Address1', address[:address1]
xml.tag! 'Address2', address[:address2]
xml.tag! 'City', address[:city]
xml.tag! 'State', address[:state]
xml.tag! 'PostCode', address[:zip]
xml.tag! 'CountryCode', country_code if country_code
end
xml.tag! 'PhoneNumber', address[:phone]
end
xml.tag! 'EmailAddress', options[:email]
xml.tag! 'CustomerIPAddress', options[:ip] || "127.0.0.1"
end
end
def add_creditcard(xml, creditcard)
xml.tag! 'CardDetails' do
xml.tag! 'CardName', creditcard.name
xml.tag! 'CV2', creditcard.verification_value if creditcard.verification_value
xml.tag! 'CardNumber', creditcard.number
xml.tag! 'ExpiryDate', { 'Month' => creditcard.month.to_s.rjust(2, "0"), 'Year' => creditcard.year.to_s[/\d\d$/] }
end
end
def add_merchant_data(xml, options)
xml.tag! 'MerchantAuthentication', {"MerchantID" => @options[:login], "Password" => @options[:password]}
end
def commit(request, options)
requires!(options, :action)
response = parse(ssl_post(test? ? self.test_url : self.live_url, request,
{"SOAPAction" => "https://www.thepaymentgateway.net/#{options[:action]}",
"Content-Type" => "text/xml; charset=utf-8" }))
success = response[:transaction_result][:status_code] == "0"
message = response[:transaction_result][:message]
authorization = success ? [ options[:order_id], response[:transaction_output_data][:cross_reference], response[:transaction_output_data][:auth_code] ].compact.join(";") : nil
Response.new(success, message, response,
:test => test?,
:authorization => authorization)
end
def parse(xml)
reply = {}
xml = REXML::Document.new(xml)
if (root = REXML::XPath.first(xml, "//CardDetailsTransactionResponse")) or
(root = REXML::XPath.first(xml, "//CrossReferenceTransactionResponse"))
root.elements.to_a.each do |node|
case node.name
when 'Message'
reply[:message] = reply(node.text)
else
parse_element(reply, node)
end
end
elsif root = REXML::XPath.first(xml, "//soap:Fault")
parse_element(reply, root)
reply[:message] = "#{reply[:faultcode]}: #{reply[:faultstring]}"
end
reply
end
def parse_element(reply, node)
case node.name
when "CrossReferenceTransactionResult"
reply[:transaction_result] = {}
node.attributes.each do |a,b|
reply[:transaction_result][a.underscore.to_sym] = b
end
node.elements.each{|e| parse_element(reply[:transaction_result], e) } if node.has_elements?
when "CardDetailsTransactionResult"
reply[:transaction_result] = {}
node.attributes.each do |a,b|
reply[:transaction_result][a.underscore.to_sym] = b
end
node.elements.each{|e| parse_element(reply[:transaction_result], e) } if node.has_elements?
when "TransactionOutputData"
reply[:transaction_output_data] = {}
node.attributes.each{|a,b| reply[:transaction_output_data][a.underscore.to_sym] = b }
node.elements.each{|e| parse_element(reply[:transaction_output_data], e) } if node.has_elements?
when "CustomVariables"
reply[:custom_variables] = {}
node.attributes.each{|a,b| reply[:custom_variables][a.underscore.to_sym] = b }
node.elements.each{|e| parse_element(reply[:custom_variables], e) } if node.has_elements?
when "GatewayEntryPoints"
reply[:gateway_entry_points] = {}
node.attributes.each{|a,b| reply[:gateway_entry_points][a.underscore.to_sym] = b }
node.elements.each{|e| parse_element(reply[:gateway_entry_points], e) } if node.has_elements?
else
k = node.name.underscore.to_sym
if node.has_elements?
reply[k] = {}
node.elements.each{|e| parse_element(reply[k], e) }
else
if node.has_attributes?
reply[k] = {}
node.attributes.each{|a,b| reply[k][a.underscore.to_sym] = b }
else
reply[k] = node.text
end
end
end
reply
end
def currency_code(currency)
CURRENCY_CODES[currency]
end
end
end
end

View File

@@ -0,0 +1,448 @@
require 'nokogiri'
module ActiveMerchant #:nodoc:
module Billing #:nodoc:
# iTransact, Inc. is an authorized reseller of the PaymentClearing gateway. If your merchant service provider uses PaymentClearing.com to process payments, you can use this module.
#
#
# Please note, the username and API Access Key are not what you use to log into the Merchant Control Panel.
#
# ==== How to get your GatewayID and API Access Key
#
# 1. If you don't already have a Gateway Account, go to http://www.itransact.com/merchant/test.html to sign up.
# 2. Go to http://support.paymentclearing.com and login or register, if necessary.
# 3. Click on "Submit a Ticket."
# 4. Select "Merchant Support" as the department and click "Next"
# 5. Enter *both* your company name and GatewayID. Put "API Access Key" in the subject. In the body, you can request a username, but it may already be in use.
#
# ==== Initialization
#
# Once you have the username, API Access Key, and your GatewayId, you're ready
# to begin. You initialize the Gateway like so:
#
# gateway = ActiveMerchant::Billing::ItransactGateway.new(
# :login => "#{THE_USERNAME}",
# :password => "#{THE_API_ACCESS_KEY}",
# :gateway_id => "#{THE_GATEWAY_ID}"
# )
#
# ==== Important Notes
# 1. Recurring is not implemented
# 1. CreditTransactions are not implemented (these are credits not related to a previously run transaction).
# 1. TransactionStatus is not implemented
#
class ItransactGateway < Gateway
self.live_url = self.test_url = 'https://secure.paymentclearing.com/cgi-bin/rc/xmltrans2.cgi'
# The countries the gateway supports merchants from as 2 digit ISO country codes
self.supported_countries = ['US']
# The card types supported by the payment gateway
self.supported_cardtypes = [:visa, :master, :american_express, :discover]
# The homepage URL of the gateway
self.homepage_url = 'http://www.itransact.com/'
# The name of the gateway
self.display_name = 'iTransact'
#
# Creates a new instance of the iTransact Gateway.
#
# ==== Parameters
# * <tt>options</tt> - A Hash of options
#
# ==== Options Hash
# * <tt>:login</tt> - A String containing your PaymentClearing assigned API Access Username
# * <tt>:password</tt> - A String containing your PaymentClearing assigned API Access Key
# * <tt>:gateway_id</tt> - A String containing your PaymentClearing assigned GatewayID
# * <tt>:test_mode</tt> - <tt>true</tt> or <tt>false</tt>. Run *all* transactions with the 'TestMode' element set to 'TRUE'.
#
def initialize(options = {})
requires!(options, :login, :password, :gateway_id)
super
end
# Performs an authorize transaction. In PaymentClearing's documentation
# this is known as a "PreAuth" transaction.
#
# ==== Parameters
# * <tt>money</tt> - The amount to be captured. Should be an Integer amount in cents.
# * <tt>creditcard</tt> - The CreditCard details for the transaction
# * <tt>options</tt> - A Hash of options
#
# ==== Options Hash
# The standard options apply here (:order_id, :ip, :customer, :invoice, :merchant, :description, :email, :currency, :address, :billing_address, :shipping_address), as well as:
# * <tt>:order_items</tt> - An Array of Hash objects with the keys <tt>:description</tt>, <tt>:cost</tt> (in cents!), and <tt>:quantity</tt>. If this is provided, <tt>:description</tt> and <tt>money</tt> will be ignored.
# * <tt>:vendor_data</tt> - An Array of Hash objects with the keys being the name of the VendorData element and value being the value.
# * <tt>:send_customer_email</tt> - <tt>true</tt> or <tt>false</tt>. Runs the transaction with the 'SendCustomerEmail' element set to 'TRUE' or 'FALSE'.
# * <tt>:send_merchant_email</tt> - <tt>true</tt> or <tt>false</tt>. Runs the transaction with the 'SendMerchantEmail' element set to 'TRUE' or 'FALSE'.
# * <tt>:email_text</tt> - An Array of (up to ten (10)) String objects to be included in emails
# * <tt>:test_mode</tt> - <tt>true</tt> or <tt>false</tt>. Runs the transaction with the 'TestMode' element set to 'TRUE' or 'FALSE'.
#
# ==== Examples
# response = gateway.authorize(1000, creditcard,
# :order_id => '1212', :address => {...}, :email => 'test@test.com',
# :order_items => [
# {:description => 'Line Item 1', :cost => '8.98', :quantity => '6'},
# {:description => 'Line Item 2', :cost => '6.99', :quantity => '4'}
# ],
# :vendor_data => [{'repId' => '1234567'}, {'customerId' => '9886'}],
# :send_customer_email => true,
# :send_merchant_email => true,
# :email_text => ['line1', 'line2', 'line3'],
# :test_mode => true
# )
#
def authorize(money, payment_source, options = {})
payload = Nokogiri::XML::Builder.new do |xml|
xml.AuthTransaction {
xml.Preauth
add_customer_data(xml, payment_source, options)
add_invoice(xml, money, options)
add_payment_source(xml, payment_source)
add_transaction_control(xml, options)
add_vendor_data(xml, options)
}
end.doc
commit(payload)
end
# Performs an authorize and capture in single transaction. In PaymentClearing's
# documentation this is known as an "Auth" or a "Sale" transaction
#
# ==== Parameters
# * <tt>money</tt> - The amount to be captured. Should be <tt>nil</tt> or an Integer amount in cents.
# * <tt>creditcard</tt> - The CreditCard details for the transaction
# * <tt>options</tt> - A Hash of options
#
# ==== Options Hash
# The standard options apply here (:order_id, :ip, :customer, :invoice, :merchant, :description, :email, :currency, :address, :billing_address, :shipping_address), as well as:
# * <tt>:order_items</tt> - An Array of Hash objects with the keys <tt>:description</tt>, <tt>:cost</tt> (in cents!), and <tt>:quantity</tt>. If this is provided, <tt>:description</tt> and <tt>money</tt> will be ignored.
# * <tt>:vendor_data</tt> - An Array of Hash objects with the keys being the name of the VendorData element and value being the value.
# * <tt>:send_customer_email</tt> - <tt>true</tt> or <tt>false</tt>. Runs the transaction with the 'SendCustomerEmail' element set to 'TRUE' or 'FALSE'.
# * <tt>:send_merchant_email</tt> - <tt>true</tt> or <tt>false</tt>. Runs the transaction with the 'SendMerchantEmail' element set to 'TRUE' or 'FALSE'.
# * <tt>:email_text</tt> - An Array of (up to ten (10)) String objects to be included in emails
# * <tt>:test_mode</tt> - <tt>true</tt> or <tt>false</tt>. Runs the transaction with the 'TestMode' element set to 'TRUE' or 'FALSE'.
#
# ==== Examples
# response = gateway.purchase(1000, creditcard,
# :order_id => '1212', :address => {...}, :email => 'test@test.com',
# :order_items => [
# {:description => 'Line Item 1', :cost => '8.98', :quantity => '6'},
# {:description => 'Line Item 2', :cost => '6.99', :quantity => '4'}
# ],
# :vendor_data => [{'repId' => '1234567'}, {'customerId' => '9886'}],
# :send_customer_email => true,
# :send_merchant_email => true,
# :email_text => ['line1', 'line2', 'line3'],
# :test_mode => true
# )
#
def purchase(money, payment_source, options = {})
payload = Nokogiri::XML::Builder.new do |xml|
xml.AuthTransaction {
add_customer_data(xml, payment_source, options)
add_invoice(xml, money, options)
add_payment_source(xml, payment_source)
add_transaction_control(xml, options)
add_vendor_data(xml, options)
}
end.doc
commit(payload)
end
# Captures the funds from an authorize transaction. In PaymentClearing's
# documentation this is known as a "PostAuth" transaction.
#
# ==== Parameters
# * <tt>money</tt> - The amount to be captured. Should be an Integer amount in cents
# * <tt>authorization</tt> - The authorization returned from the previous capture or purchase request
# * <tt>options</tt> - A Hash of options, all are optional.
#
# ==== Options Hash
# The standard options apply here (:order_id, :ip, :customer, :invoice, :merchant, :description, :email, :currency, :address, :billing_address, :shipping_address), as well as:
# * <tt>:vendor_data</tt> - An Array of Hash objects with the keys being the name of the VendorData element and value being the value.
# * <tt>:send_customer_email</tt> - <tt>true</tt> or <tt>false</tt>. Runs the transaction with the 'SendCustomerEmail' element set to 'TRUE' or 'FALSE'.
# * <tt>:send_merchant_email</tt> - <tt>true</tt> or <tt>false</tt>. Runs the transaction with the 'SendMerchantEmail' element set to 'TRUE' or 'FALSE'.
# * <tt>:email_text</tt> - An Array of (up to ten (10)) String objects to be included in emails
# * <tt>:test_mode</tt> - <tt>true</tt> or <tt>false</tt>. Runs the transaction with the 'TestMode' element set to 'TRUE' or 'FALSE'.
#
# ==== Examples
# response = gateway.capture(1000, creditcard,
# :vendor_data => [{'repId' => '1234567'}, {'customerId' => '9886'}],
# :send_customer_email => true,
# :send_merchant_email => true,
# :email_text => ['line1', 'line2', 'line3'],
# :test_mode => true
# )
#
def capture(money, authorization, options = {})
payload = Nokogiri::XML::Builder.new do |xml|
xml.PostAuthTransaction {
xml.OperationXID(authorization)
add_invoice(xml, money, options)
add_transaction_control(xml, options)
add_vendor_data(xml, options)
}
end.doc
commit(payload)
end
# This will reverse a previously run transaction which *has* *not* settled.
#
# ==== Parameters
# * <tt>authorization</tt> - The authorization returned from the previous capture or purchase request
# * <tt>options</tt> - A Hash of options, all are optional
#
# ==== Options Hash
# The standard options (:order_id, :ip, :customer, :invoice, :merchant, :description, :email, :currency, :address, :billing_address, :shipping_address) are ignored.
# * <tt>:vendor_data</tt> - An Array of Hash objects with the keys being the name of the VendorData element and value being the value.
# * <tt>:send_customer_email</tt> - <tt>true</tt> or <tt>false</tt>. Runs the transaction with the 'SendCustomerEmail' element set to 'TRUE' or 'FALSE'.
# * <tt>:send_merchant_email</tt> - <tt>true</tt> or <tt>false</tt>. Runs the transaction with the 'SendMerchantEmail' element set to 'TRUE' or 'FALSE'.
# * <tt>:email_text</tt> - An Array of (up to ten (10)) String objects to be included in emails
# * <tt>:test_mode</tt> - <tt>true</tt> or <tt>false</tt>. Runs the transaction with the 'TestMode' element set to 'TRUE' or 'FALSE'.
#
# ==== Examples
# response = gateway.void('9999999999',
# :vendor_data => [{'repId' => '1234567'}, {'customerId' => '9886'}],
# :send_customer_email => true,
# :send_merchant_email => true,
# :email_text => ['line1', 'line2', 'line3'],
# :test_mode => true
# )
#
def void(authorization, options = {})
payload = Nokogiri::XML::Builder.new do |xml|
xml.VoidTransaction {
xml.OperationXID(authorization)
add_transaction_control(xml, options)
add_vendor_data(xml, options)
}
end.doc
commit(payload)
end
# This will reverse a previously run transaction which *has* settled.
#
# ==== Parameters
# * <tt>money</tt> - The amount to be credited. Should be an Integer amount in cents
# * <tt>authorization</tt> - The authorization returned from the previous capture or purchase request
# * <tt>options</tt> - A Hash of options, all are optional
#
# ==== Options Hash
# The standard options (:order_id, :ip, :customer, :invoice, :merchant, :description, :email, :currency, :address, :billing_address, :shipping_address) are ignored.
# * <tt>:vendor_data</tt> - An Array of Hash objects with the keys being the name of the VendorData element and value being the value.
# * <tt>:send_customer_email</tt> - <tt>true</tt> or <tt>false</tt>. Runs the transaction with the 'SendCustomerEmail' element set to 'TRUE' or 'FALSE'.
# * <tt>:send_merchant_email</tt> - <tt>true</tt> or <tt>false</tt>. Runs the transaction with the 'SendMerchantEmail' element set to 'TRUE' or 'FALSE'.
# * <tt>:email_text</tt> - An Array of (up to ten (10)) String objects to be included in emails
# * <tt>:test_mode</tt> - <tt>true</tt> or <tt>false</tt>. Runs the transaction with the 'TestMode' element set to 'TRUE' or 'FALSE'.
#
# ==== Examples
# response = gateway.refund(555, '9999999999',
# :vendor_data => [{'repId' => '1234567'}, {'customerId' => '9886'}],
# :send_customer_email => true,
# :send_merchant_email => true,
# :email_text => ['line1', 'line2', 'line3'],
# :test_mode => true
# )
#
def refund(money, authorization, options = {})
payload = Nokogiri::XML::Builder.new do |xml|
xml.TranCredTransaction {
xml.OperationXID(authorization)
add_invoice(xml, money, options)
add_transaction_control(xml, options)
add_vendor_data(xml, options)
}
end.doc
commit(payload)
end
private
def add_customer_data(xml, payment_source, options)
billing_address = options[:billing_address] || options[:address]
shipping_address = options[:shipping_address] || options[:address]
xml.CustomerData {
xml.Email(options[:email]) unless options[:email].blank?
xml.CustId(options[:order_id]) unless options[:order_id].blank?
xml.BillingAddress {
xml.FirstName(payment_source.first_name || parse_first_name(billing_address[:name]))
xml.LastName(payment_source.last_name || parse_last_name(billing_address[:name]))
xml.Address1(billing_address[:address1])
xml.Address2(billing_address[:address2]) unless billing_address[:address2].blank?
xml.City(billing_address[:city])
xml.State(billing_address[:state])
xml.Zip(billing_address[:zip])
xml.Country(billing_address[:country])
xml.Phone(billing_address[:phone])
}
xml.ShippingAddress {
xml.FirstName(payment_source.first_name || parse_first_name(shipping_address[:name]))
xml.LastName(payment_source.last_name || parse_last_name(shipping_address[:name]))
xml.Address1(shipping_address[:address1])
xml.Address2(shipping_address[:address2]) unless shipping_address[:address2].blank?
xml.City(shipping_address[:city])
xml.State(shipping_address[:state])
xml.Zip(shipping_address[:zip])
xml.Country(shipping_address[:country])
xml.Phone(shipping_address[:phone])
} unless shipping_address.blank?
}
end
def add_invoice(xml, money, options)
xml.AuthCode options[:force] if options[:force]
if options[:order_items].blank?
xml.Total(amount(money)) unless(money.nil? || money < 0.01)
xml.Description(options[:description]) unless( options[:description].blank?)
else
xml.OrderItems {
options[:order_items].each do |item|
xml.Item {
xml.Description(item[:description])
xml.Cost(amount(item[:cost]))
xml.Qty(item[:quantity].to_s)
}
end
}
end
end
def add_payment_source(xml, source)
case determine_funding_source(source)
when :credit_card then add_creditcard(xml, source)
when :check then add_check(xml, source)
end
end
def determine_funding_source(payment_source)
case payment_source
when ActiveMerchant::Billing::CreditCard
:credit_card
when ActiveMerchant::Billing::Check
:check
end
end
def add_creditcard(xml, creditcard)
xml.AccountInfo {
xml.CardAccount {
xml.AccountNumber(creditcard.number.to_s)
xml.ExpirationMonth(creditcard.month.to_s.rjust(2,'0'))
xml.ExpirationYear(creditcard.year.to_s)
xml.CVVNumber(creditcard.verification_value.to_s) unless creditcard.verification_value.blank?
}
}
end
def add_check(xml, check)
xml.AccountInfo {
xml.ABA(check.routing_number.to_s)
xml.AccountNumber(check.account_number.to_s)
xml.AccountSource(check.account_type.to_s)
xml.AccountType(check.account_holder_type.to_s)
xml.CheckNumber(check.number.to_s)
}
end
def add_transaction_control(xml, options)
xml.TransactionControl {
# if there was a 'global' option set...
xml.TestMode(@options[:test_mode].upcase) if !@options[:test_mode].blank?
# allow the global option to be overridden...
xml.TestMode(options[:test_mode].upcase) if !options[:test_mode].blank?
xml.SendCustomerEmail(options[:send_customer_email].upcase) unless options[:send_customer_email].blank?
xml.SendMerchantEmail(options[:send_merchant_email].upcase) unless options[:send_merchant_email].blank?
xml.EmailText {
options[:email_text].each do |item|
xml.EmailTextItem(item)
end
} if options[:email_text]
}
end
def add_vendor_data(xml, options)
return if options[:vendor_data].blank?
xml.VendorData {
options[:vendor_data].each do |k,v|
xml.Element {
xml.Name(k)
xml.Key(v)
}
end
}
end
def commit(payload)
# Set the Content-Type header -- otherwise the URL decoding messes up
# the Base64 encoded payload signature!
response = parse(ssl_post(self.live_url, post_data(payload), 'Content-Type' => 'text/xml'))
Response.new(successful?(response), response[:error_message], response,
:test => test?,
:authorization => response[:xid],
:avs_result => { :code => response[:avs_response] },
:cvv_result => response[:cvv_response])
end
def post_data(payload)
payload_xml = payload.root.to_xml(:indent => 0)
payload_signature = sign_payload(payload_xml)
request = Nokogiri::XML::Builder.new do |xml|
xml.GatewayInterface {
xml.APICredentials {
xml.Username(@options[:login])
xml.PayloadSignature(payload_signature)
xml.TargetGateway(@options[:gateway_id])
}
}
end.doc
request.root.children.first.after payload.root
request.to_xml(:indent => 0)
end
def parse(raw_xml)
doc = REXML::Document.new(raw_xml)
response = Hash.new
transaction_result = doc.root.get_elements('TransactionResponse/TransactionResult/*')
transaction_result.each do |e|
response[e.name.to_s.underscore.to_sym] = e.text unless e.text.blank?
end
response
end
def successful?(response)
# Turns out the PaymentClearing gateway is not consistent...
response[:status].downcase =='ok'
end
def test_mode?(response)
# The '1' is a legacy thing; most of the time it should be 'TRUE'...
response[:test_mode] == 'TRUE' || response[:test_mode] == '1'
end
def message_from(response)
response[:error_message]
end
def sign_payload(payload)
key = @options[:password].to_s
digest=OpenSSL::HMAC.digest(OpenSSL::Digest::SHA1.new(key), key, payload)
signature = Base64.encode64(digest)
signature.chomp!
end
end
end
end

View File

@@ -0,0 +1,275 @@
module ActiveMerchant #:nodoc:
module Billing #:nodoc:
class JetpayGateway < Gateway
self.test_url = 'https://test1.jetpay.com/jetpay'
self.live_url = 'https://gateway17.jetpay.com/jetpay'
# The countries the gateway supports merchants from as 2 digit ISO country codes
self.supported_countries = ['US']
# The card types supported by the payment gateway
self.supported_cardtypes = [:visa, :master, :american_express, :discover]
# The homepage URL of the gateway
self.homepage_url = 'http://www.jetpay.com/'
# The name of the gateway
self.display_name = 'JetPay'
# all transactions are in cents
self.money_format = :cents
ACTION_CODE_MESSAGES = {
"001" => "Refer to card issuer.",
"002" => "Refer to card issuer, special condition.",
"003" => "Pick up card.",
"200" => "Deny - Pick up card.",
"005" => "Do not honor.",
"100" => "Deny.",
"006" => "Error.",
"181" => "Format error.",
"007" => "Pickup card, special condition.",
"104" => "Deny - New card issued.",
"110" => "Invalid amount.",
"014" => "Invalid account number (no such number).",
"111" => "Invalid account.",
"015" => "No such issuer.",
"103" => "Deny - Invalid manual Entry 4DBC.",
"182" => "Please wait.",
"109" => "Invalid merchant.",
"041" => "Pick up card (lost card).",
"043" => "Pick up card (stolen card).",
"051" => "Insufficient funds.",
"052" => "No checking account.",
"105" => "Deny - Account Cancelled.",
"054" => "Expired Card.",
"101" => "Expired Card.",
"183" => "Invalid currency code.",
"057" => "Transaction not permitted to cardholder.",
"115" => "Service not permitted.",
"062" => "Restricted card.",
"189" => "Deny - Cancelled or Closed Merchant/SE.",
"188" => "Deny - Expiration date required.",
"125" => "Invalid effective date.",
"122" => "Invalid card (CID) security code.",
"400" => "Reversal accepted.",
"992" => "DECLINE/TIMEOUT.",
"107" => "Please Call Issuer.",
"025" => "Transaction Not Found.",
"981" => "AVS Error.",
"913" => "Invalid Card Type.",
"996" => "Terminal ID Not Found.",
nil => "No response returned (missing credentials?)."
}
def initialize(options = {})
requires!(options, :login)
super
end
def purchase(money, credit_card, options = {})
commit(money, build_sale_request(money, credit_card, options))
end
def authorize(money, credit_card, options = {})
commit(money, build_authonly_request(money, credit_card, options))
end
def capture(money, reference, options = {})
commit(money, build_capture_request('CAPT', reference.split(";").first))
end
def void(reference, options = {})
transaction_id, approval, amount = reference.split(";")
commit(amount.to_i, build_void_request(amount.to_i, transaction_id, approval))
end
def credit(money, transaction_id_or_card, options = {})
if transaction_id_or_card.is_a?(String)
deprecated CREDIT_DEPRECATION_MESSAGE
refund(money, transaction_id_or_card, options)
else
commit(money, build_credit_request('CREDIT', money, nil, transaction_id_or_card))
end
end
def refund(money, reference, options = {})
transaction_id = reference.split(";").first
credit_card = options[:credit_card]
commit(money, build_credit_request('CREDIT', money, transaction_id, credit_card))
end
private
def build_xml_request(transaction_type, transaction_id = nil, &block)
xml = Builder::XmlMarkup.new
xml.tag! 'JetPay' do
# The basic values needed for any request
xml.tag! 'TerminalID', @options[:login]
xml.tag! 'TransactionType', transaction_type
xml.tag! 'TransactionID', transaction_id.nil? ? generate_unique_id.slice(0, 18) : transaction_id
if block_given?
yield xml
else
xml.target!
end
end
end
def build_sale_request(money, credit_card, options)
build_xml_request('SALE') do |xml|
add_credit_card(xml, credit_card)
add_addresses(xml, options)
add_customer_data(xml, options)
add_invoice_data(xml, options)
xml.tag! 'TotalAmount', amount(money)
xml.target!
end
end
def build_authonly_request(money, credit_card, options)
build_xml_request('AUTHONLY') do |xml|
add_credit_card(xml, credit_card)
add_addresses(xml, options)
add_customer_data(xml, options)
add_invoice_data(xml, options)
xml.tag! 'TotalAmount', amount(money)
xml.target!
end
end
def build_capture_request(transaction_type, transaction_id)
build_xml_request(transaction_type, transaction_id)
end
def build_void_request(money, transaction_id, approval)
build_xml_request('VOID', transaction_id) do |xml|
xml.tag! 'Approval', approval
xml.tag! 'TotalAmount', amount(money)
xml.target!
end
end
# `transaction_id` may be nil for unlinked credit transactions.
def build_credit_request(transaction_type, money, transaction_id, card)
build_xml_request(transaction_type, transaction_id) do |xml|
add_credit_card(xml, card) if card
xml.tag! 'TotalAmount', amount(money)
xml.target!
end
end
def commit(money, request)
response = parse(ssl_post(test? ? self.test_url : self.live_url, request))
success = success?(response)
Response.new(success,
success ? 'APPROVED' : message_from(response),
response,
:test => test?,
:authorization => authorization_from(response, money),
:avs_result => { :code => response[:avs] },
:cvv_result => response[:cvv2]
)
end
def parse(body)
return {} if body.blank?
xml = REXML::Document.new(body)
response = {}
xml.root.elements.to_a.each do |node|
parse_element(response, node)
end
response
end
def parse_element(response, node)
if node.has_elements?
node.elements.each{|element| parse_element(response, element) }
else
response[node.name.underscore.to_sym] = node.text
end
end
def format_exp(value)
format(value, :two_digits)
end
def success?(response)
response[:action_code] == "000"
end
def message_from(response)
ACTION_CODE_MESSAGES[response[:action_code]]
end
def authorization_from(response, money)
original_amount = amount(money) if money
[ response[:transaction_id], response[:approval], original_amount ].join(";")
end
def add_credit_card(xml, credit_card)
xml.tag! 'CardNum', credit_card.number
xml.tag! 'CardExpMonth', format_exp(credit_card.month)
xml.tag! 'CardExpYear', format_exp(credit_card.year)
if credit_card.first_name || credit_card.last_name
xml.tag! 'CardName', [credit_card.first_name,credit_card.last_name].compact.join(' ')
end
unless credit_card.verification_value.nil? || (credit_card.verification_value.length == 0)
xml.tag! 'CVV2', credit_card.verification_value
end
end
def add_addresses(xml, options)
if billing_address = options[:billing_address] || options[:address]
xml.tag! 'BillingAddress', [billing_address[:address1], billing_address[:address2]].compact.join(" ")
xml.tag! 'BillingCity', billing_address[:city]
xml.tag! 'BillingStateProv', billing_address[:state]
xml.tag! 'BillingPostalCode', billing_address[:zip]
xml.tag! 'BillingCountry', lookup_country_code(billing_address[:country])
xml.tag! 'BillingPhone', billing_address[:phone]
end
if shipping_address = options[:shipping_address]
xml.tag! 'ShippingInfo' do
xml.tag! 'ShippingName', shipping_address[:name]
xml.tag! 'ShippingAddr' do
xml.tag! 'Address', [shipping_address[:address1], shipping_address[:address2]].compact.join(" ")
xml.tag! 'City', shipping_address[:city]
xml.tag! 'StateProv', shipping_address[:state]
xml.tag! 'PostalCode', shipping_address[:zip]
xml.tag! 'Country', lookup_country_code(shipping_address[:country])
end
end
end
end
def add_customer_data(xml, options)
xml.tag! 'Email', options[:email] if options[:email]
xml.tag! 'UserIPAddress', options[:ip] if options[:ip]
end
def add_invoice_data(xml, options)
xml.tag! 'OrderNumber', options[:order_id] if options[:order_id]
xml.tag! 'TaxAmount', amount(options[:tax]) if options[:tax]
end
def lookup_country_code(code)
country = Country.find(code) rescue nil
country && country.code(:alpha3)
end
end
end
end

View File

@@ -0,0 +1,447 @@
require 'rexml/document'
module ActiveMerchant #:nodoc:
module Billing #:nodoc:
# Initialization Options
# :login Your store number
# :pem The text of your linkpoint PEM file. Note
# this is not the path to file, but its
# contents. If you are only using one PEM
# file on your site you can declare it
# globally and then you won't need to
# include this option
#
#
# A valid store number is required. Unfortunately, with LinkPoint
# YOU CAN'T JUST USE ANY OLD STORE NUMBER. Also, you can't just
# generate your own PEM file. You'll need to use a special PEM file
# provided by LinkPoint.
#
# Go to http://www.linkpoint.com/support/sup_teststore.asp to set up
# a test account and obtain your PEM file.
#
# Declaring PEM file Globally
# ActiveMerchant::Billing::LinkpointGateway.pem_file = File.read( File.dirname(__FILE__) + '/../mycert.pem' )
#
#
# Valid Order Options
# :result =>
# LIVE Production mode
# GOOD Approved response in test mode
# DECLINE Declined response in test mode
# DUPLICATE Duplicate response in test mode
#
# :ponumber Order number
#
# :transactionorigin => Source of the transaction
# ECI Email or Internet
# MAIL Mail order
# MOTO Mail order/Telephone
# TELEPHONE Telephone
# RETAIL Face-to-face
#
# :ordertype =>
# SALE Real live sale
# PREAUTH Authorize only
# POSTAUTH Forced Ticket or Ticket Only transaction
# VOID
# CREDIT
# CALCSHIPPING For shipping charges calculations
# CALCTAX For sales tax calculations
#
# Recurring Options
# :action =>
# SUBMIT
# MODIFY
# CANCEL
#
# :installments Identifies how many recurring payments to charge the customer
# :startdate Date to begin charging the recurring payments. Format: YYYYMMDD or "immediate"
# :periodicity =>
# MONTHLY
# BIMONTHLY
# WEEKLY
# BIWEEKLY
# YEARLY
# DAILY
# :threshold Tells how many times to retry the transaction (if it fails) before contacting the merchant.
# :comments Uh... comments
#
#
# For reference:
#
# https://www.linkpointcentral.com/lpc/docs/Help/APIHelp/lpintguide.htm
#
# Entities = {
# :payment => [:subtotal, :tax, :vattax, :shipping, :chargetotal],
# :billing => [:name, :address1, :address2, :city, :state, :zip, :country, :email, :phone, :fax, :addrnum],
# :shipping => [:name, :address1, :address2, :city, :state, :zip, :country, :weight, :items, :carrier, :total],
# :creditcard => [:cardnumber, :cardexpmonth, :cardexpyear, :cvmvalue, :track],
# :telecheck => [:routing, :account, :checknumber, :bankname, :bankstate, :dl, :dlstate, :void, :accounttype, :ssn],
# :transactiondetails => [:transactionorigin, :oid, :ponumber, :taxexempt, :terminaltype, :ip, :reference_number, :recurring, :tdate],
# :periodic => [:action, :installments, :threshold, :startdate, :periodicity, :comments],
# :notes => [:comments, :referred]
# :items => [:item => [:price, :quantity, :description, :id, :options => [:option => [:name, :value]]]]
# }
#
#
# LinkPoint's Items entity is an optional entity that can be attached to orders.
# It is entered as :line_items to be consistent with the CyberSource implementation
#
# The line_item hash goes in the options hash and should look like
#
# :line_items => [
# {
# :id => '123456',
# :description => 'Logo T-Shirt',
# :price => '12.00',
# :quantity => '1',
# :options => [
# {
# :name => 'Color',
# :value => 'Red'
# },
# {
# :name => 'Size',
# :value => 'XL'
# }
# ]
# },
# {
# :id => '111',
# :description => 'keychain',
# :price => '3.00',
# :quantity => '1'
# }
# ]
# This functionality is only supported by this particular gateway may
# be changed at any time
#
class LinkpointGateway < Gateway
# Your global PEM file. This will be assigned to you by linkpoint
#
# Example:
#
# ActiveMerchant::Billing::LinkpointGateway.pem_file = File.read( File.dirname(__FILE__) + '/../mycert.pem' )
#
cattr_accessor :pem_file
self.test_url = 'https://staging.linkpt.net:1129/'
self.live_url = 'https://secure.linkpt.net:1129/'
self.supported_countries = ['US']
self.supported_cardtypes = [:visa, :master, :american_express, :discover, :jcb, :diners_club]
self.homepage_url = 'http://www.linkpoint.com/'
self.display_name = 'LinkPoint'
def initialize(options = {})
requires!(options, :login)
@options = {
:result => 'LIVE',
:pem => LinkpointGateway.pem_file
}.update(options)
raise ArgumentError, "You need to pass in your pem file using the :pem parameter or set it globally using ActiveMerchant::Billing::LinkpointGateway.pem_file = File.read( File.dirname(__FILE__) + '/../mycert.pem' ) or similar" if @options[:pem].blank?
end
# Send a purchase request with periodic options
# Recurring Options
# :action =>
# SUBMIT
# MODIFY
# CANCEL
#
# :installments Identifies how many recurring payments to charge the customer
# :startdate Date to begin charging the recurring payments. Format: YYYYMMDD or "immediate"
# :periodicity =>
# :monthly
# :bimonthly
# :weekly
# :biweekly
# :yearly
# :daily
# :threshold Tells how many times to retry the transaction (if it fails) before contacting the merchant.
# :comments Uh... comments
#
def recurring(money, creditcard, options={})
requires!(options, [:periodicity, :bimonthly, :monthly, :biweekly, :weekly, :yearly, :daily], :installments, :order_id )
options.update(
:ordertype => "SALE",
:action => options[:action] || "SUBMIT",
:installments => options[:installments] || 12,
:startdate => options[:startdate] || "immediate",
:periodicity => options[:periodicity].to_s || "monthly",
:comments => options[:comments] || nil,
:threshold => options[:threshold] || 3
)
commit(money, creditcard, options)
end
# Buy the thing
def purchase(money, creditcard, options={})
requires!(options, :order_id)
options.update(
:ordertype => "SALE"
)
commit(money, creditcard, options)
end
#
# Authorize the transaction
#
# Reserves the funds on the customer's credit card, but does not charge the card.
#
def authorize(money, creditcard, options = {})
requires!(options, :order_id)
options.update(
:ordertype => "PREAUTH"
)
commit(money, creditcard, options)
end
#
# Post an authorization.
#
# Captures the funds from an authorized transaction.
# Order_id must be a valid order id from a prior authorized transaction.
#
def capture(money, authorization, options = {})
options.update(
:order_id => authorization,
:ordertype => "POSTAUTH"
)
commit(money, nil, options)
end
# Void a previous transaction
def void(identification, options = {})
options.update(
:order_id => identification,
:ordertype => "VOID"
)
commit(nil, nil, options)
end
#
# Refund an order
#
# identification must be a valid order id previously submitted by SALE
#
def refund(money, identification, options = {})
options.update(
:ordertype => "CREDIT",
:order_id => identification
)
commit(money, nil, options)
end
def credit(money, identification, options = {})
deprecated CREDIT_DEPRECATION_MESSAGE
refund(money, identification, options)
end
private
# Commit the transaction by posting the XML file to the LinkPoint server
def commit(money, creditcard, options = {})
response = parse(ssl_post(test? ? self.test_url : self.live_url, post_data(money, creditcard, options)))
Response.new(successful?(response), response[:message], response,
:test => test?,
:authorization => response[:ordernum],
:avs_result => { :code => response[:avs].to_s[2,1] },
:cvv_result => response[:avs].to_s[3,1]
)
end
def successful?(response)
response[:approved] == "APPROVED"
end
# Build the XML file
def post_data(money, creditcard, options)
params = parameters(money, creditcard, options)
xml = REXML::Document.new
order = xml.add_element("order")
# Merchant Info
merchantinfo = order.add_element("merchantinfo")
merchantinfo.add_element("configfile").text = @options[:login]
# Loop over the params hash to construct the XML string
for key, value in params
elem = order.add_element(key.to_s)
if key == :items
build_items(elem, value)
else
for k, v in params[key]
elem.add_element(k.to_s).text = params[key][k].to_s if params[key][k]
end
end
# Linkpoint doesn't understand empty elements:
order.delete(elem) if elem.size == 0
end
return xml.to_s
end
# adds LinkPoint's Items entity to the XML. Called from post_data
def build_items(element, items)
for item in items
item_element = element.add_element("item")
for key, value in item
if key == :options
options_element = item_element.add_element("options")
for option in value
opt_element = options_element.add_element("option")
opt_element.add_element("name").text = option[:name] unless option[:name].blank?
opt_element.add_element("value").text = option[:value] unless option[:value].blank?
end
else
item_element.add_element(key.to_s).text = item[key].to_s unless item[key].blank?
end
end
end
end
# Set up the parameters hash just once so we don't have to do it
# for every action.
def parameters(money, creditcard, options = {})
params = {
:payment => {
:subtotal => amount(options[:subtotal]),
:tax => amount(options[:tax]),
:vattax => amount(options[:vattax]),
:shipping => amount(options[:shipping]),
:chargetotal => amount(money)
},
:transactiondetails => {
:transactionorigin => options[:transactionorigin] || "ECI",
:oid => options[:order_id],
:ponumber => options[:ponumber],
:taxexempt => options[:taxexempt],
:terminaltype => options[:terminaltype],
:ip => options[:ip],
:reference_number => options[:reference_number],
:recurring => options[:recurring] || "NO", #DO NOT USE if you are using the periodic billing option.
:tdate => options[:tdate]
},
:orderoptions => {
:ordertype => options[:ordertype],
:result => @options[:result]
},
:periodic => {
:action => options[:action],
:installments => options[:installments],
:threshold => options[:threshold],
:startdate => options[:startdate],
:periodicity => options[:periodicity],
:comments => options[:comments]
},
:telecheck => {
:routing => options[:telecheck_routing],
:account => options[:telecheck_account],
:checknumber => options[:telecheck_checknumber],
:bankname => options[:telecheck_bankname],
:dl => options[:telecheck_dl],
:dlstate => options[:telecheck_dlstate],
:void => options[:telecheck_void],
:accounttype => options[:telecheck_accounttype],
:ssn => options[:telecheck_ssn],
}
}
if creditcard
params[:creditcard] = {
:cardnumber => creditcard.number,
:cardexpmonth => creditcard.month,
:cardexpyear => format_creditcard_expiry_year(creditcard.year),
:track => nil
}
if creditcard.verification_value?
params[:creditcard][:cvmvalue] = creditcard.verification_value
params[:creditcard][:cvmindicator] = 'provided'
else
params[:creditcard][:cvmindicator] = 'not_provided'
end
end
if billing_address = options[:billing_address] || options[:address]
params[:billing] = {}
params[:billing][:name] = billing_address[:name] || (creditcard ? creditcard.name : nil)
params[:billing][:address1] = billing_address[:address1] unless billing_address[:address1].blank?
params[:billing][:address2] = billing_address[:address2] unless billing_address[:address2].blank?
params[:billing][:city] = billing_address[:city] unless billing_address[:city].blank?
params[:billing][:state] = billing_address[:state] unless billing_address[:state].blank?
params[:billing][:zip] = billing_address[:zip] unless billing_address[:zip].blank?
params[:billing][:country] = billing_address[:country] unless billing_address[:country].blank?
params[:billing][:company] = billing_address[:company] unless billing_address[:company].blank?
params[:billing][:phone] = billing_address[:phone] unless billing_address[:phone].blank?
params[:billing][:email] = options[:email] unless options[:email].blank?
end
if shipping_address = options[:shipping_address]
params[:shipping] = {}
params[:shipping][:name] = shipping_address[:name] || (creditcard ? creditcard.name : nil)
params[:shipping][:address1] = shipping_address[:address1] unless shipping_address[:address1].blank?
params[:shipping][:address2] = shipping_address[:address2] unless shipping_address[:address2].blank?
params[:shipping][:city] = shipping_address[:city] unless shipping_address[:city].blank?
params[:shipping][:state] = shipping_address[:state] unless shipping_address[:state].blank?
params[:shipping][:zip] = shipping_address[:zip] unless shipping_address[:zip].blank?
params[:shipping][:country] = shipping_address[:country] unless shipping_address[:country].blank?
end
params[:items] = options[:line_items] if options[:line_items]
return params
end
def parse(xml)
# For reference, a typical response...
# <r_csp></r_csp>
# <r_time></r_time>
# <r_ref></r_ref>
# <r_error></r_error>
# <r_ordernum></r_ordernum>
# <r_message>This is a test transaction and will not show up in the Reports</r_message>
# <r_code></r_code>
# <r_tdate>Thu Feb 2 15:40:21 2006</r_tdate>
# <r_score></r_score>
# <r_authresponse></r_authresponse>
# <r_approved>APPROVED</r_approved>
# <r_avs></r_avs>
response = {:message => "Global Error Receipt", :complete => false}
xml = REXML::Document.new("<response>#{xml}</response>")
xml.root.elements.each do |node|
response[node.name.downcase.sub(/^r_/, '').to_sym] = normalize(node.text)
end unless xml.root.nil?
response
end
# Make a ruby type out of the response string
def normalize(field)
case field
when "true" then true
when "false" then false
when "" then nil
when "null" then nil
else field
end
end
def format_creditcard_expiry_year(year)
sprintf("%.4i", year)[-2..-1]
end
end
end
end

View File

@@ -0,0 +1,540 @@
module ActiveMerchant #:nodoc:
module Billing #:nodoc:
class LitleGateway < Gateway
# Specific to Litle options:
# * <tt>:merchant_id</tt> - Merchant Id assigned by Litle
# * <tt>:user</tt> - Username assigned by Litle
# * <tt>:password</tt> - Password assigned by Litle
# * <tt>:version</tt> - The version of the api you are using (eg, '8.10')
# * <tt>:proxy_addr</tt> - Proxy address - nil if not needed
# * <tt>:proxy_port</tt> - Proxy port - nil if not needed
# * <tt>:url</tt> - URL assigned by Litle (for testing, use the sandbox)
#
# Standard Active Merchant options
# * <tt>:order_id</tt> - The order number
# * <tt>:ip</tt> - The IP address of the customer making the purchase
# * <tt>:customer</tt> - The name, customer number, or other information that identifies the customer
# * <tt>:invoice</tt> - The invoice number
# * <tt>:merchant</tt> - The name or description of the merchant offering the product
# * <tt>:description</tt> - A description of the transaction
# * <tt>:email</tt> - The email address of the customer
# * <tt>:currency</tt> - The currency of the transaction. Only important when you are using a currency that is not the default with a gateway that supports multiple currencies.
# * <tt>:billing_address</tt> - A hash containing the billing address of the customer.
# * <tt>:shipping_address</tt> - A hash containing the shipping address of the customer.
#
# The <tt>:billing_address</tt>, and <tt>:shipping_address</tt> hashes can have the following keys:
#
# * <tt>:name</tt> - The full name of the customer.
# * <tt>:company</tt> - The company name of the customer.
# * <tt>:address1</tt> - The primary street address of the customer.
# * <tt>:address2</tt> - Additional line of address information.
# * <tt>:city</tt> - The city of the customer.
# * <tt>:state</tt> - The state of the customer. The 2 digit code for US and Canadian addresses. The full name of the state or province for foreign addresses.
# * <tt>:country</tt> - The [ISO 3166-1-alpha-2 code](http://www.iso.org/iso/country_codes/iso_3166_code_lists/english_country_names_and_code_elements.htm) for the customer.
# * <tt>:zip</tt> - The zip or postal code of the customer.
# * <tt>:phone</tt> - The phone number of the customer.
self.test_url = 'https://www.testlitle.com/sandbox/communicator/online'
self.live_url = 'https://payments.litle.com/vap/communicator/online'
LITLE_SCHEMA_VERSION = '8.13'
# The countries the gateway supports merchants from as 2 digit ISO country codes
self.supported_countries = ['US']
# The card types supported by the payment gateway
self.supported_cardtypes = [:visa, :master, :american_express, :discover, :diners_club, :jcb]
# The homepage URL of the gateway
self.homepage_url = 'http://www.litle.com/'
# The name of the gateway
self.display_name = 'Litle & Co.'
self.default_currency = 'USD'
def initialize(options = {})
begin
require 'LitleOnline'
rescue LoadError
raise "Could not load the LitleOnline gem (>= 08.13.2). Use `gem install LitleOnline` to install it."
end
@litle = LitleOnline::LitleOnlineRequest.new
options[:version] ||= LITLE_SCHEMA_VERSION
options[:merchant] ||= 'Default Report Group'
options[:user] ||= options[:login]
requires!(options, :merchant_id, :user, :password, :merchant, :version)
super
end
def authorize(money, creditcard_or_token, options = {})
to_pass = build_authorize_request(money, creditcard_or_token, options)
build_response(:authorization, @litle.authorization(to_pass))
end
def purchase(money, creditcard_or_token, options = {})
to_pass = build_purchase_request(money, creditcard_or_token, options)
build_response(:sale, @litle.sale(to_pass))
end
def capture(money, authorization, options = {})
transaction_id, kind = split_authorization(authorization)
to_pass = create_capture_hash(money, transaction_id, options)
build_response(:capture, @litle.capture(to_pass))
end
# Note: Litle requires that authorization requests be voided via auth_reversal
# and other requests via void. To maintain the same interface as the other
# gateways the transaction_id and the kind of transaction are concatenated
# together with a ; separator (e.g. 1234;authorization)
#
# A partial auth_reversal can be accomplished by passing :amount as an option
def void(identification, options = {})
transaction_id, kind = split_authorization(identification)
if(kind == 'authorization')
to_pass = create_auth_reversal_hash(transaction_id, options[:amount], options)
build_response(:authReversal, @litle.auth_reversal(to_pass))
else
to_pass = create_void_hash(transaction_id, options)
build_response(:void, @litle.void(to_pass))
end
end
def credit(money, identification_or_token, options = {})
to_pass = build_credit_request(money, identification_or_token, options)
build_response(:credit, @litle.credit(to_pass))
end
def store(creditcard, options = {})
to_pass = create_token_hash(creditcard, options)
build_response(:registerToken, @litle.register_token_request(to_pass), %w(000 801 802))
end
private
CARD_TYPE = {
'visa' => 'VI',
'master' => 'MC',
'american_express' => 'AX',
'discover' => 'DI',
'jcb' => 'DI',
'diners_club' => 'DI'
}
AVS_RESPONSE_CODE = {
'00' => 'Y',
'01' => 'X',
'02' => 'D',
'10' => 'Z',
'11' => 'W',
'12' => 'A',
'13' => 'A',
'14' => 'P',
'20' => 'N',
'30' => 'S',
'31' => 'R',
'32' => 'U',
'33' => 'R',
'34' => 'I',
'40' => 'E'
}
def url
return @options[:url] if @options[:url].present?
test? ? self.test_url : self.live_url
end
def build_response(kind, litle_response, valid_responses=%w(000))
response = Hash.from_xml(litle_response.raw_xml.to_s)['litleOnlineResponse']
if response['response'] == "0"
detail = response["#{kind}Response"]
fraud = fraud_result(detail)
Response.new(
valid_responses.include?(detail['response']),
detail['message'],
{ :litleOnlineResponse => response },
:authorization => authorization_from(detail, kind),
:avs_result => { :code => fraud['avs'] },
:cvv_result => fraud['cvv'],
:test => test?
)
else
Response.new(false, response['message'], :litleOnlineResponse => response, :test => test?)
end
end
# Generates an authorization string of the appropriate id and the kind of transaction
# See #void for how the kind is used
def authorization_from(litle_response, kind)
case kind
when :registerToken
authorization = litle_response['litleToken']
else
authorization = [litle_response['litleTxnId'], kind.to_s].join(";")
end
end
def split_authorization(authorization)
transaction_id, kind = authorization.to_s.split(';')
[transaction_id, kind]
end
def build_authorize_request(money, creditcard_or_token, options)
payment_method = build_payment_method(creditcard_or_token, options)
hash = create_hash(money, options)
add_creditcard_or_cardtoken_hash(hash, payment_method)
hash
end
def build_purchase_request(money, creditcard_or_token, options)
payment_method = build_payment_method(creditcard_or_token, options)
hash = create_hash(money, options)
add_creditcard_or_cardtoken_hash(hash, payment_method)
hash
end
def build_credit_request(money, identification_or_token, options)
payment_method = build_payment_method(identification_or_token, options)
hash = create_hash(money, options)
add_identification_or_cardtoken_hash(hash, payment_method)
unless payment_method.is_a?(LitleCardToken)
hash['orderSource'] = nil
hash['orderId'] = nil
end
hash
end
def build_payment_method(payment_method, options)
result = payment_method
# Build instance of the LitleCardToken class for internal use if this is a token request.
if payment_method.is_a?(String) && options.has_key?(:token)
result = LitleCardToken.new(:token => payment_method)
result.month = options[:token][:month]
result.year = options[:token][:year]
result.verification_value = options[:token][:verification_value]
result.brand = options[:token][:brand]
end
result
end
def add_creditcard_or_cardtoken_hash(hash, creditcard_or_cardtoken)
if creditcard_or_cardtoken.is_a?(LitleCardToken)
add_cardtoken_hash(hash, creditcard_or_cardtoken)
else
add_creditcard_hash(hash, creditcard_or_cardtoken)
end
end
def add_identification_or_cardtoken_hash(hash, identification_or_cardtoken)
if identification_or_cardtoken.is_a?(LitleCardToken)
add_cardtoken_hash(hash, identification_or_cardtoken)
else
transaction_id, kind = split_authorization(identification_or_cardtoken)
hash['litleTxnId'] = transaction_id
end
end
def add_cardtoken_hash(hash, cardtoken)
token_info = {}
token_info['litleToken'] = cardtoken.token
token_info['expDate'] = cardtoken.exp_date if cardtoken.exp_date?
token_info['cardValidationNum'] = cardtoken.verification_value unless cardtoken.verification_value.blank?
token_info['type'] = cardtoken.type unless cardtoken.type.blank?
hash['token'] = token_info
hash
end
def add_creditcard_hash(hash, creditcard)
cc_type = CARD_TYPE[creditcard.brand]
exp_date_yr = creditcard.year.to_s[2..3]
exp_date_mo = '%02d' % creditcard.month.to_i
exp_date = exp_date_mo + exp_date_yr
card_info = {
'type' => cc_type,
'number' => creditcard.number,
'expDate' => exp_date,
'cardValidationNum' => creditcard.verification_value
}
hash['card'] = card_info
hash
end
def create_capture_hash(money, authorization, options)
hash = create_hash(money, options)
hash['litleTxnId'] = authorization
hash
end
def create_token_hash(creditcard, options)
hash = create_hash(0, options)
hash['accountNumber'] = creditcard.number
hash
end
def create_void_hash(identification, options)
hash = create_hash(nil, options)
hash['litleTxnId'] = identification
hash
end
def create_auth_reversal_hash(identification, money, options)
hash = create_hash(money, options)
hash['litleTxnId'] = identification
hash
end
def create_hash(money, options)
fraud_check_type = {}
if options[:ip]
fraud_check_type['customerIpAddress'] = options[:ip]
end
enhanced_data = {}
if options[:invoice]
enhanced_data['invoiceReferenceNumber'] = options[:invoice]
end
if options[:description]
enhanced_data['customerReference'] = options[:description]
end
if options[:billing_address]
bill_to_address = {
'name' => options[:billing_address][:name],
'companyName' => options[:billing_address][:company],
'addressLine1' => options[:billing_address][:address1],
'addressLine2' => options[:billing_address][:address2],
'city' => options[:billing_address][:city],
'state' => options[:billing_address][:state],
'zip' => options[:billing_address][:zip],
'country' => options[:billing_address][:country],
'email' => options[:email],
'phone' => options[:billing_address][:phone]
}
end
if options[:shipping_address]
ship_to_address = {
'name' => options[:shipping_address][:name],
'companyName' => options[:shipping_address][:company],
'addressLine1' => options[:shipping_address][:address1],
'addressLine2' => options[:shipping_address][:address2],
'city' => options[:shipping_address][:city],
'state' => options[:shipping_address][:state],
'zip' => options[:shipping_address][:zip],
'country' => options[:shipping_address][:country],
'email' => options[:email],
'phone' => options[:shipping_address][:phone]
}
end
hash = {
'billToAddress' => bill_to_address,
'shipToAddress' => ship_to_address,
'orderId' => (options[:order_id] || @options[:order_id]),
'customerId' => options[:customer],
'reportGroup' => (options[:merchant] || @options[:merchant]),
'merchantId' => (options[:merchant_id] || @options[:merchant_id]),
'orderSource' => (options[:order_source] || 'ecommerce'),
'enhancedData' => enhanced_data,
'fraudCheckType' => fraud_check_type,
'user' => (options[:user] || @options[:user]),
'password' => (options[:password] || @options[:password]),
'version' => (options[:version] || @options[:version]),
'url' => (options[:url] || url),
'proxy_addr' => (options[:proxy_addr] || @options[:proxy_addr]),
'proxy_port' => (options[:proxy_port] || @options[:proxy_port]),
'id' => (options[:id] || options[:order_id] || @options[:order_id])
}
if (!money.nil? && money.to_s.length > 0)
hash.merge!({ 'amount' => money })
end
hash
end
def fraud_result(authorization_response)
if result = authorization_response['fraudResult']
if result.key?('cardValidationResult')
cvv_to_pass = result['cardValidationResult'].blank? ? "P" : result['cardValidationResult']
end
avs_to_pass = AVS_RESPONSE_CODE[result['avsResult']] unless result['avsResult'].blank?
end
{ 'cvv' => cvv_to_pass, 'avs' => avs_to_pass }
end
# A +LitleCardToken+ object represents a tokenized credit card, and is capable of validating the various
# data associated with these.
#
# == Example Usage
# token = LitleCardToken.new(
# :token => '1234567890123456',
# :month => '9',
# :year => '2010',
# :brand => 'visa',
# :verification_value => '123'
# )
#
# token.valid? # => true
# cc.exp_date # => 0910
#
class LitleCardToken
include Validateable
# Returns or sets the token. (required)
#
# @return [String]
attr_accessor :token
# Returns or sets the expiry month for the card associated with token. (optional)
#
# @return [Integer]
attr_accessor :month
# Returns or sets the expiry year for the card associated with token. (optional)
#
# @return [Integer]
attr_accessor :year
# Returns or sets the card verification value. (optional)
#
# @return [String] the verification value
attr_accessor :verification_value
# Returns or sets the credit card brand. (optional)
#
# Valid card types are
#
# * +'visa'+
# * +'master'+
# * +'discover'+
# * +'american_express'+
# * +'diners_club'+
# * +'jcb'+
# * +'switch'+
# * +'solo'+
# * +'dankort'+
# * +'maestro'+
# * +'forbrugsforeningen'+
# * +'laser'+
#
# @return (String) the credit card brand
attr_accessor :brand
# Returns the Litle credit card type identifier.
#
# @return (String) the credit card type identifier
def type
CARD_TYPE[brand] unless brand.blank?
end
# Returns true if the expiration date is set.
#
# @return (Boolean)
def exp_date?
!month.to_i.zero? && !year.to_i.zero?
end
# Returns the card token expiration date in MMYY format.
#
# @return (String) the expiration date in MMYY format
def exp_date
result = ''
if exp_date?
exp_date_yr = year.to_s[2..3]
exp_date_mo = '%02d' % month.to_i
result = exp_date_mo + exp_date_yr
end
result
end
# Validates the card token details.
#
# Any validation errors are added to the {#errors} attribute.
def validate
validate_card_token
validate_expiration_date
validate_card_brand
end
def check?
false
end
private
CARD_TYPE = {
'visa' => 'VI',
'master' => 'MC',
'american_express' => 'AX',
'discover' => 'DI',
'jcb' => 'DI',
'diners_club' => 'DI'
}
def before_validate #:nodoc:
self.month = month.to_i
self.year = year.to_i
end
# Litle XML Reference Guide 1.8.2
#
# The length of the original card number is reflected in the token, so a
# submitted 16-digit number results in a 16-digit token. Also, all tokens
# use only numeric characters, so you do not have to change your
# systems to accept alpha-numeric characters.
#
# The credit card token numbers themselves have two parts.
# The last four digits match the last four digits of the card number.
# The remaining digits (length can vary based upon original card number
# length) are a randomly generated.
def validate_card_token #:nodoc:
if token.to_s.length < 12 || token.to_s.match(/\A\d+\Z/).nil?
errors.add :token, "is not a valid card token"
end
end
def validate_expiration_date #:nodoc:
if !month.to_i.zero? || !year.to_i.zero?
errors.add :month, "is not a valid month" unless valid_month?(month)
errors.add :year, "is not a valid year" unless valid_expiry_year?(year)
end
end
def validate_card_brand #:nodoc:
errors.add :brand, "is invalid" unless brand.blank? || CreditCard.card_companies.keys.include?(brand)
end
def valid_month?(month)
(1..12).include?(month.to_i)
end
def valid_expiry_year?(year)
year.to_s =~ /\A\d{4}\Z/ && year.to_i > 1987
end
end
end
end
end

View File

@@ -0,0 +1,176 @@
module ActiveMerchant #:nodoc:
module Billing #:nodoc:
class MerchantESolutionsGateway < Gateway
self.test_url = 'https://cert.merchante-solutions.com/mes-api/tridentApi'
self.live_url = 'https://api.merchante-solutions.com/mes-api/tridentApi'
# The countries the gateway supports merchants from as 2 digit ISO country codes
self.supported_countries = ['US']
# The card types supported by the payment gateway
self.supported_cardtypes = [:visa, :master, :american_express, :discover, :jcb]
# The homepage URL of the gateway
self.homepage_url = 'http://www.merchante-solutions.com/'
# The name of the gateway
self.display_name = 'Merchant e-Solutions'
def initialize(options = {})
requires!(options, :login, :password)
super
end
def authorize(money, creditcard_or_card_id, options = {})
post = {}
post[:client_reference_number] = options[:customer] if options.has_key?(:customer)
post[:moto_ecommerce_ind] = options[:moto_ecommerce_ind] if options.has_key?(:moto_ecommerce_ind)
add_invoice(post, options)
add_payment_source(post, creditcard_or_card_id, options)
add_address(post, options)
commit('P', money, post)
end
def purchase(money, creditcard_or_card_id, options = {})
post = {}
post[:client_reference_number] = options[:customer] if options.has_key?(:customer)
post[:moto_ecommerce_ind] = options[:moto_ecommerce_ind] if options.has_key?(:moto_ecommerce_ind)
add_invoice(post, options)
add_payment_source(post, creditcard_or_card_id, options)
add_address(post, options)
commit('D', money, post)
end
def capture(money, transaction_id, options = {})
post ={}
post[:transaction_id] = transaction_id
post[:client_reference_number] = options[:customer] if options.has_key?(:customer)
commit('S', money, post)
end
def store(creditcard, options = {})
post = {}
post[:client_reference_number] = options[:customer] if options.has_key?(:customer)
add_creditcard(post, creditcard, options)
commit('T', nil, post)
end
def unstore(card_id)
post = {}
post[:client_reference_number] = options[:customer] if options.has_key?(:customer)
post[:card_id] = card_id
commit('X', nil, post)
end
def refund(money, identification, options = {})
post = {}
post[:transaction_id] = identification
post[:client_reference_number] = options[:customer] if options.has_key?(:customer)
options.delete(:customer)
options.delete(:billing_address)
commit('U', money, options.merge(post))
end
def credit(money, creditcard_or_card_id, options = {})
post = {}
post[:client_reference_number] = options[:customer] if options.has_key?(:customer)
add_invoice(post, options)
add_payment_source(post, creditcard_or_card_id, options)
commit('C', money, post)
end
def void(transaction_id, options = {})
post = {}
post[:transaction_id] = transaction_id
post[:client_reference_number] = options[:customer] if options.has_key?(:customer)
options.delete(:customer)
options.delete(:billing_address)
commit('V', nil, options.merge(post))
end
private
def add_address(post, options)
if address = options[:billing_address] || options[:address]
post[:cardholder_street_address] = address[:address1].to_s.gsub(/[^\w.]/, '+')
post[:cardholder_zip] = address[:zip].to_s
end
end
def add_invoice(post, options)
if options.has_key? :order_id
post[:invoice_number] = options[:order_id].to_s.gsub(/[^\w.]/, '')
end
end
def add_payment_source(post, creditcard_or_card_id, options)
if creditcard_or_card_id.is_a?(String)
# using stored card
post[:card_id] = creditcard_or_card_id
post[:card_exp_date] = options[:expiration_date] if options[:expiration_date]
else
# card info is provided
add_creditcard(post, creditcard_or_card_id, options)
end
end
def add_creditcard(post, creditcard, options)
post[:card_number] = creditcard.number
post[:cvv2] = creditcard.verification_value if creditcard.verification_value?
post[:card_exp_date] = expdate(creditcard)
end
def parse(body)
results = {}
body.split(/&/).each do |pair|
key,val = pair.split(/=/)
results[key] = val
end
results
end
def commit(action, money, parameters)
url = test? ? self.test_url : self.live_url
parameters[:transaction_amount] = amount(money) if money unless action == 'V'
response = begin
parse( ssl_post(url, post_data(action,parameters)) )
rescue ActiveMerchant::ResponseError => e
{ "error_code" => "404", "auth_response_text" => e.to_s }
end
Response.new(response["error_code"] == "000", message_from(response), response,
:authorization => response["transaction_id"],
:test => test?,
:cvv_result => response["cvv2_result"],
:avs_result => { :code => response["avs_result"] }
)
end
def expdate(creditcard)
year = sprintf("%.4i", creditcard.year)
month = sprintf("%.2i", creditcard.month)
"#{month}#{year[-2..-1]}"
end
def message_from(response)
if response["error_code"] == "000"
"This transaction has been approved"
else
response["auth_response_text"]
end
end
def post_data(action, parameters = {})
post = {}
post[:profile_id] = @options[:login]
post[:profile_key] = @options[:password]
post[:transaction_type] = action if action
request = post.merge(parameters).map {|key,value| "#{key}=#{CGI.escape(value.to_s)}"}.join("&")
request
end
end
end
end

View File

@@ -0,0 +1,323 @@
module ActiveMerchant #:nodoc:
module Billing #:nodoc:
class MerchantWareGateway < Gateway
class_attribute :v4_live_url
self.live_url = self.test_url = 'https://ps1.merchantware.net/MerchantWARE/ws/RetailTransaction/TXRetail.asmx'
self.v4_live_url = 'https://ps1.merchantware.net/Merchantware/ws/RetailTransaction/v4/Credit.asmx'
self.supported_countries = ['US']
self.supported_cardtypes = [:visa, :master, :american_express, :discover]
self.homepage_url = 'http://merchantwarehouse.com/merchantware'
self.display_name = 'MerchantWARE'
ENV_NAMESPACES = { "xmlns:xsi" => "http://www.w3.org/2001/XMLSchema-instance",
"xmlns:xsd" => "http://www.w3.org/2001/XMLSchema",
"xmlns:env" => "http://schemas.xmlsoap.org/soap/envelope/"
}
ENV_NAMESPACES_V4 = { "xmlns:xsi" => "http://www.w3.org/2001/XMLSchema-instance",
"xmlns:xsd" => "http://www.w3.org/2001/XMLSchema",
"xmlns:soap" => "http://schemas.xmlsoap.org/soap/envelope/"
}
TX_NAMESPACE = "http://merchantwarehouse.com/MerchantWARE/Client/TransactionRetail"
TX_NAMESPACE_V4 = "http://schemas.merchantwarehouse.com/merchantware/40/Credit/"
ACTIONS = {
:purchase => "IssueKeyedSale",
:authorize => "IssueKeyedPreAuth",
:capture => "IssuePostAuth",
:void => "VoidPreAuthorization",
:credit => "IssueKeyedRefund",
:reference_credit => "IssueRefundByReference"
}
# Creates a new MerchantWareGateway
#
# The gateway requires that a valid login, password, and name be passed
# in the +options+ hash.
#
# ==== Options
#
# * <tt>:login</tt> - The MerchantWARE SiteID.
# * <tt>:password</tt> - The MerchantWARE Key.
# * <tt>:name</tt> - The MerchantWARE Name.
def initialize(options = {})
requires!(options, :login, :password, :name)
super
end
# Authorize a credit card for a given amount.
#
# ==== Parameters
# * <tt>money</tt> - The amount to be authorized as an Integer value in cents.
# * <tt>credit_card</tt> - The CreditCard details for the transaction.
# * <tt>options</tt>
# * <tt>:order_id</tt> - A unique reference for this order (required).
# * <tt>:billing_address</tt> - The billing address for the cardholder.
def authorize(money, credit_card, options = {})
request = build_purchase_request(:authorize, money, credit_card, options)
commit(:authorize, request)
end
# Authorize and immediately capture funds from a credit card.
#
# ==== Parameters
# * <tt>money</tt> - The amount to be authorized as anInteger value in cents.
# * <tt>credit_card</tt> - The CreditCard details for the transaction.
# * <tt>options</tt>
# * <tt>:order_id</tt> - A unique reference for this order (required).
# * <tt>:billing_address</tt> - The billing address for the cardholder.
def purchase(money, credit_card, options = {})
request = build_purchase_request(:purchase, money, credit_card, options)
commit(:purchase, request)
end
# Capture authorized funds from a credit card.
#
# ==== Parameters
# * <tt>money</tt> - The amount to be captured as anInteger value in cents.
# * <tt>authorization</tt> - The authorization string returned from the initial authorization.
def capture(money, authorization, options = {})
request = build_capture_request(:capture, money, authorization, options)
commit(:capture, request)
end
# Void a transaction.
#
# ==== Parameters
# * <tt>authorization</tt> - The authorization string returned from the initial authorization or purchase.
def void(authorization, options = {})
reference, options[:order_id] = split_reference(authorization)
request = v4_soap_request(:void) do |xml|
add_reference_token(xml, reference)
end
commit(:void, request, true)
end
# Refund an amount back a cardholder
#
# ==== Parameters
#
# * <tt>money</tt> - The amount to be refunded as an Integer value in cents.
# * <tt>identification</tt> - The credit card you want to refund or the authorization for the existing transaction you are refunding.
# * <tt>options</tt>
# * <tt>:order_id</tt> - A unique reference for this order (required when performing a non-referenced credit)
def credit(money, identification, options = {})
if identification.is_a?(String)
deprecated CREDIT_DEPRECATION_MESSAGE
refund(money, identification, options)
else
perform_credit(money, identification, options)
end
end
def refund(money, reference, options = {})
perform_reference_credit(money, reference, options)
end
private
def soap_request(action)
xml = Builder::XmlMarkup.new :indent => 2
xml.instruct!
xml.tag! "env:Envelope", ENV_NAMESPACES do
xml.tag! "env:Body" do
xml.tag! ACTIONS[action], "xmlns" => TX_NAMESPACE do
add_credentials(xml)
yield xml
end
end
end
xml.target!
end
def v4_soap_request(action)
xml = Builder::XmlMarkup.new :indent => 2
xml.instruct!
xml.tag! "soap:Envelope", ENV_NAMESPACES_V4 do
xml.tag! "soap:Body" do
xml.tag! ACTIONS[:void], "xmlns" => TX_NAMESPACE_V4 do
xml.tag! "merchantName", @options[:name]
xml.tag! "merchantSiteId", @options[:login]
xml.tag! "merchantKey", @options[:password]
yield xml
end
end
end
xml.target!
end
def build_purchase_request(action, money, credit_card, options)
requires!(options, :order_id)
request = soap_request(action) do |xml|
add_invoice(xml, options)
add_amount(xml, money)
add_credit_card(xml, credit_card)
add_address(xml, options)
end
end
def build_capture_request(action, money, identification, options)
reference, options[:order_id] = split_reference(identification)
request = soap_request(action) do |xml|
add_reference(xml, reference)
add_invoice(xml, options)
add_amount(xml, money)
end
end
def perform_reference_credit(money, identification, options)
reference, options[:order_id] = split_reference(identification)
request = soap_request(:reference_credit) do |xml|
add_reference(xml, reference)
add_invoice(xml, options)
add_amount(xml, money, "strOverrideAmount")
end
commit(:reference_credit, request)
end
def perform_credit(money, credit_card, options)
requires!(options, :order_id)
request = soap_request(:credit) do |xml|
add_invoice(xml, options)
add_amount(xml, money)
add_credit_card(xml, credit_card)
end
commit(:credit, request)
end
def add_credentials(xml)
xml.tag! "strSiteId", @options[:login]
xml.tag! "strKey", @options[:password]
xml.tag! "strName", @options[:name]
end
def expdate(credit_card)
year = sprintf("%.4i", credit_card.year)
month = sprintf("%.2i", credit_card.month)
"#{month}#{year[-2..-1]}"
end
def add_invoice(xml, options)
xml.tag! "strOrderNumber", options[:order_id].to_s.gsub(/[^\w]/, '').slice(0, 25)
end
def add_amount(xml, money, tag = "strAmount")
xml.tag! tag, amount(money)
end
def add_reference(xml, reference)
xml.tag! "strReferenceCode", reference
end
def add_reference_token(xml, reference)
xml.tag! "token", reference
end
def add_address(xml, options)
if address = options[:billing_address] || options[:address]
xml.tag! "strAVSStreetAddress", address[:address1]
xml.tag! "strAVSZipCode", address[:zip]
end
end
def add_credit_card(xml, credit_card)
xml.tag! "strPAN", credit_card.number
xml.tag! "strExpDate", expdate(credit_card)
xml.tag! "strCardHolder", credit_card.name
xml.tag! "strCVCode", credit_card.verification_value if credit_card.verification_value?
end
def split_reference(reference)
reference.to_s.split(";")
end
def parse(action, data)
response = {}
xml = REXML::Document.new(data)
root = REXML::XPath.first(xml, "//#{ACTIONS[action]}Response/#{ACTIONS[action]}Result")
root.elements.each do |element|
response[element.name] = element.text
end
status, code, message = response["ApprovalStatus"].split(";")
response[:status] = status
if response[:success] = status == "APPROVED"
response[:message] = status
else
response[:message] = message
response[:failure_code] = code
end
response
end
def parse_error(http_response)
response = {}
response[:http_code] = http_response.code
response[:http_message] = http_response.message
response[:success] = false
document = REXML::Document.new(http_response.body)
node = REXML::XPath.first(document, "//soap:Fault")
node.elements.each do |element|
response[element.name] = element.text
end
response[:message] = response["faultstring"].to_s.gsub("\n", " ")
response
rescue REXML::ParseException => e
response[:http_body] = http_response.body
response[:message] = "Failed to parse the failed response"
response
end
def soap_action(action, v4 = false)
v4 ? "#{TX_NAMESPACE_V4}#{ACTIONS[action]}" : "#{TX_NAMESPACE}/#{ACTIONS[action]}"
end
def url(v4 = false)
v4 ? v4_live_url : live_url
end
def commit(action, request, v4 = false)
begin
data = ssl_post(url(v4), request,
"Content-Type" => 'text/xml; charset=utf-8',
"SOAPAction" => soap_action(action, v4)
)
response = parse(action, data)
rescue ActiveMerchant::ResponseError => e
response = parse_error(e.response)
end
Response.new(response[:success], response[:message], response,
:test => test?,
:authorization => authorization_from(response),
:avs_result => { :code => response["AVSResponse"] },
:cvv_result => response["CVResponse"]
)
end
def authorization_from(response)
if response[:success]
[ response["ReferenceID"], response["OrderNumber"] ].join(";")
end
end
end
end
end

View File

@@ -0,0 +1,190 @@
require 'digest/md5'
require 'rexml/document'
module ActiveMerchant #:nodoc:
module Billing #:nodoc:
class MerchantWarriorGateway < Gateway
TOKEN_TEST_URL = 'https://base.merchantwarrior.com/token/'
TOKEN_LIVE_URL = 'https://api.merchantwarrior.com/token/'
POST_TEST_URL = 'https://base.merchantwarrior.com/post/'
POST_LIVE_URL = 'https://api.merchantwarrior.com/post/'
self.supported_countries = ['AU']
self.supported_cardtypes = [:visa, :master, :american_express,
:diners_club, :discover]
self.homepage_url = 'http://www.merchantwarrior.com/'
self.display_name = 'MerchantWarrior'
self.money_format = :dollars
self.default_currency = 'AUD'
def initialize(options = {})
requires!(options, :merchant_uuid, :api_key, :api_passphrase)
super
end
def authorize(money, payment_method, options = {})
post = {}
add_amount(post, money, options)
add_product(post, options)
add_address(post, options)
add_payment_method(post, payment_method)
commit('processAuth', post)
end
def purchase(money, payment_method, options = {})
post = {}
add_amount(post, money, options)
add_product(post, options)
add_address(post, options)
add_payment_method(post, payment_method)
commit('processCard', post)
end
def capture(money, identification)
post = {}
add_amount(post, money, options)
add_transaction(post, identification)
post.merge!('captureAmount' => money.to_s)
commit('processCapture', post)
end
def refund(money, identification)
post = {}
add_amount(post, money, options)
add_transaction(post, identification)
post['refundAmount'] = money
commit('refundCard', post)
end
def store(creditcard, options = {})
post = {
'cardName' => creditcard.name,
'cardNumber' => creditcard.number,
'cardExpiryMonth' => format(creditcard.month, :two_digits),
'cardExpiryYear' => format(creditcard.year, :two_digits)
}
commit('addCard', post)
end
private
def add_transaction(post, identification)
post['transactionID'] = identification
end
def add_address(post, options)
return unless(address = options[:address])
post['customerName'] = address[:name]
post['customerCountry'] = address[:country]
post['customerState'] = address[:state]
post['customerCity'] = address[:city]
post['customerAddress'] = address[:address1]
post['customerPostCode'] = address[:zip]
end
def add_product(post, options)
post['transactionProduct'] = options[:transaction_product]
end
def add_payment_method(post, payment_method)
if payment_method.respond_to?(:number)
add_creditcard(post, payment_method)
else
add_token(post, payment_method)
end
end
def add_token(post, token)
post['cardID'] = token
end
def add_creditcard(post, creditcard)
post['paymentCardNumber'] = creditcard.number
post['paymentCardName'] = creditcard.name
post['paymentCardExpiry'] = creditcard.expiry_date.expiration.strftime("%m%y")
end
def add_amount(post, money, options)
currency = (options[:currency] || currency(money))
post['transactionAmount'] = money.to_s
post['transactionCurrency'] = currency
post['hash'] = verification_hash(money, currency)
end
def verification_hash(money, currency)
Digest::MD5.hexdigest(
(
@options[:api_passphrase].to_s +
@options[:merchant_uuid].to_s +
money.to_s +
currency
).downcase
)
end
def parse(body)
xml = REXML::Document.new(body)
response = {}
xml.root.elements.to_a.each do |node|
parse_element(response, node)
end
response
end
def parse_element(response, node)
if node.has_elements?
node.elements.each{|element| parse_element(response, element)}
else
response[node.name.underscore.to_sym] = node.text
end
end
def commit(action, post)
add_auth(action, post)
response = parse(ssl_post(url_for(action, post), post_data(post)))
Response.new(
success?(response),
response[:response_message],
response,
:test => test?,
:authorization => (response[:card_id] || response[:transaction_id])
)
end
def add_auth(action, post)
post['merchantUUID'] = @options[:merchant_uuid]
post['apiKey'] = @options[:api_key]
unless token?(post)
post['method'] = action
end
end
def url_for(action, post)
if token?(post)
[(test? ? TOKEN_TEST_URL : TOKEN_LIVE_URL), action].join("/")
else
(test? ? POST_TEST_URL : POST_LIVE_URL)
end
end
def token?(post)
(post["cardID"] || post["cardName"])
end
def success?(response)
(response[:response_code] == '0')
end
def post_data(post)
post.collect{|k,v| "#{k}=#{CGI.escape(v.to_s)}" }.join("&")
end
end
end
end

View File

@@ -0,0 +1,272 @@
module ActiveMerchant #:nodoc:
module Billing #:nodoc:
class MercuryGateway < Gateway
URLS = {
:test => 'https://w1.mercurydev.net/ws/ws.asmx',
:live => 'https://w1.mercurypay.com/ws/ws.asmx'
}
self.homepage_url = 'http://www.mercurypay.com'
self.display_name = 'Mercury'
self.supported_countries = ['US']
self.supported_cardtypes = [:visa, :master, :american_express, :discover, :diners_club, :jcb]
self.default_currency = 'USD'
def initialize(options = {})
requires!(options, :login, :password)
super
end
def purchase(money, credit_card, options = {})
requires!(options, :order_id)
request = build_non_authorized_request('Sale', money, credit_card, options)
commit('Sale', request)
end
def credit(money, credit_card, options = {})
requires!(options, :order_id)
request = build_non_authorized_request('Return', money, credit_card, options)
commit('Return', request)
end
def authorize(money, credit_card, options = {})
requires!(options, :order_id)
options[:authorized] ||= money
request = build_non_authorized_request('PreAuth', money, credit_card, options)
commit('PreAuth', request)
end
def capture(money, authorization, options = {})
requires!(options, :credit_card)
options[:authorized] ||= money
request = build_authorized_request('PreAuthCapture', money, authorization, options[:credit_card], options)
commit('PreAuthCapture', request)
end
def refund(money, authorization, options = {})
requires!(options, :credit_card)
request = build_authorized_request('VoidSale', money, authorization, options[:credit_card], options)
commit(options[:void], request)
end
private
def build_non_authorized_request(action, money, credit_card, options)
xml = Builder::XmlMarkup.new
xml.tag! "TStream" do
xml.tag! "Transaction" do
xml.tag! 'TranType', 'Credit'
xml.tag! 'TranCode', action
if action == 'PreAuth' || action == 'Sale'
xml.tag! "PartialAuth", "Allow"
end
add_invoice(xml, options[:order_id], nil, options)
add_customer_data(xml, options)
add_amount(xml, money, options)
add_credit_card(xml, credit_card, action)
add_address(xml, options)
end
end
xml = xml.target!
end
def build_authorized_request(action, money, authorization, credit_card, options)
xml = Builder::XmlMarkup.new
invoice_no, ref_no, auth_code, acq_ref_data, process_data = split_authorization(authorization)
xml.tag! "TStream" do
xml.tag! "Transaction" do
xml.tag! 'TranType', 'Credit'
xml.tag! 'TranCode', action
if action == 'PreAuthCapture'
xml.tag! "PartialAuth", "Allow"
end
add_invoice(xml, invoice_no, ref_no, options)
add_customer_data(xml, options)
add_amount(xml, money, options)
add_credit_card(xml, credit_card, action)
add_address(xml, options)
xml.tag! 'TranInfo' do
xml.tag! "AuthCode", auth_code
xml.tag! "AcqRefData", acq_ref_data
xml.tag! "ProcessData", process_data
end
end
end
xml = xml.target!
end
def add_invoice(xml, invoice_no, ref_no, options)
if /^\d+$/ !~ invoice_no.to_s
raise ArgumentError.new("#{invoice_no} is not numeric as required by Mercury")
end
xml.tag! 'InvoiceNo', invoice_no
xml.tag! 'RefNo', ref_no || invoice_no
xml.tag! 'OperatorID', options[:merchant] if options[:merchant]
xml.tag! 'Memo', options[:description] if options[:description]
end
def add_customer_data(xml, options)
xml.tag! 'IpAddress', options[:ip] if options[:ip]
if options[:customer]
xml.tag! "TranInfo" do
xml.tag! 'CustomerCode', options[:customer]
end
end
xml.tag! 'MerchantID', @options[:login]
end
def add_amount(xml, money, options = {})
xml.tag! 'Amount' do
xml.tag! 'Purchase', amount(money)
xml.tag! 'Tax', options[:tax] if options[:tax]
xml.tag! 'Authorize', amount(options[:authorized]) if options[:authorized]
xml.tag! 'Gratuity', amount(options[:tip]) if options[:tip]
end
end
CARD_CODES = {
'visa' => 'VISA',
'master' => 'M/C',
'american_express' => 'AMEX',
'discover' => 'DCVR',
'diners_club' => 'DCLB',
'jcb' => 'JCB'
}
def add_credit_card(xml, credit_card, action)
xml.tag! 'Account' do
xml.tag! 'AcctNo', credit_card.number
xml.tag! 'ExpDate', expdate(credit_card)
end
xml.tag! 'CardType', CARD_CODES[credit_card.brand] if credit_card.brand
include_cvv = !%w(Return PreAuthCapture).include?(action)
xml.tag! 'CVVData', credit_card.verification_value if(include_cvv && credit_card.verification_value)
end
def expdate(credit_card)
year = sprintf("%.4i", credit_card.year)
month = sprintf("%.2i", credit_card.month)
"#{month}#{year[-2..-1]}"
end
def add_address(xml, options)
if billing_address = options[:billing_address] || options[:address]
xml.tag! 'AVS' do
xml.tag! 'Address', billing_address[:address1]
xml.tag! 'Zip', billing_address[:zip]
end
end
end
def parse(action, body)
response = {}
hashify_xml!(unescape_xml(body), response)
response
end
def hashify_xml!(xml, response)
xml = REXML::Document.new(xml)
xml.elements.each("//CmdResponse/*") do |node|
response[node.name.underscore.to_sym] = node.text
end
xml.elements.each("//TranResponse/*") do |node|
if node.name.to_s == "Amount"
node.elements.each do |amt|
response[amt.name.underscore.to_sym] = amt.text
end
else
response[node.name.underscore.to_sym] = node.text
end
end
end
def endpoint_url
URLS[test? ? :test : :live]
end
def build_soap_request(body)
xml = Builder::XmlMarkup.new
xml.instruct!
xml.tag! 'soap:Envelope', ENVELOPE_NAMESPACES do
xml.tag! 'soap:Body' do
xml.tag! 'CreditTransaction', 'xmlns' => homepage_url do
xml.tag! 'tran' do
xml << escape_xml(body)
end
xml.tag! 'pw', @options[:password]
end
end
end
xml.target!
end
def build_header
{
"SOAPAction" => "http://www.mercurypay.com/CreditTransaction",
"Content-Type" => "text/xml; charset=utf-8"
}
end
SUCCESS_CODES = [ 'Approved', 'Success' ]
def commit(action, request)
response = parse(action, ssl_post(endpoint_url, build_soap_request(request), build_header))
success = SUCCESS_CODES.include?(response[:cmd_status])
message = success ? 'Success' : message_from(response)
Response.new(success, message, response,
:test => test?,
:authorization => authorization_from(response),
:avs_result => { :code => response[:avs_result] },
:cvv_result => response[:cvv_result])
end
def message_from(response)
response[:text_response]
end
def authorization_from(response)
[
response[:invoice_no],
response[:ref_no],
response[:auth_code],
response[:acq_ref_data],
response[:process_data]
].join(";")
end
def split_authorization(authorization)
invoice_no, ref_no, auth_code, acq_ref_data, process_data = authorization.split(";")
[invoice_no, ref_no, auth_code, acq_ref_data, process_data]
end
ENVELOPE_NAMESPACES = {
'xmlns:xsd' => 'http://www.w3.org/2001/XMLSchema',
'xmlns:soap' => "http://schemas.xmlsoap.org/soap/envelope/",
'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance'
}
def escape_xml(xml)
"\n<![CDATA[\n#{xml}\n]]>\n"
end
def unescape_xml(escaped_xml)
escaped_xml.gsub(/\&gt;/,'>').gsub(/\&lt;/,'<')
end
end
end
end

Some files were not shown because too many files have changed in this diff Show More