heartland_portico 3.2.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.document +5 -0
- data/.rspec +2 -0
- data/.ruby-version +1 -0
- data/Gemfile +33 -0
- data/Gemfile.lock +204 -0
- data/LICENSE.txt +20 -0
- data/Rakefile +66 -0
- data/VERSION +1 -0
- data/config/solano.yml +12 -0
- data/config/spec.yml +15 -0
- data/data/PosGateway.xsd +7186 -0
- data/lib/active_merchant/billing/gateways/heartland_portico.rb +165 -0
- data/lib/heartland_portico.rb +182 -0
- data/lib/heartland_portico/ach.rb +81 -0
- data/lib/wsdl.xml +43 -0
- data/spec/fixtures/check_sale_request_stub.xml +39 -0
- data/spec/fixtures/check_sale_response_success.xml +30 -0
- data/spec/fixtures/credit_account_verify_request_stub.xml +31 -0
- data/spec/fixtures/credit_account_verify_response_success.xml +29 -0
- data/spec/fixtures/credit_auth_request_stub.xml +27 -0
- data/spec/fixtures/credit_auth_response_approval.xml +29 -0
- data/spec/fixtures/credit_auth_response_decline.xml +29 -0
- data/spec/fixtures/response_authentication_error.xml +15 -0
- data/spec/heartland_portico/ach_spec.rb +108 -0
- data/spec/heartland_portico_spec.rb +108 -0
- data/spec/spec_helper.rb +25 -0
- data/test/comm_stub.rb +40 -0
- data/test/fixtures.yml +40 -0
- data/test/remote/gateways/certification_test.rb +172 -0
- data/test/remote/gateways/remote_heartland_portico_test.rb +126 -0
- data/test/test_helper.rb +259 -0
- data/test/unit/gateways/heartland_portico_test.rb +136 -0
- metadata +357 -0
@@ -0,0 +1,165 @@
|
|
1
|
+
require 'heartland_portico'
|
2
|
+
|
3
|
+
module ActiveMerchant #:nodoc:
|
4
|
+
module Billing #:nodoc:
|
5
|
+
class HeartlandPorticoGateway < Gateway
|
6
|
+
#self.test_url = 'https://example.com/test'
|
7
|
+
#self.live_url = 'https://example.com/live'
|
8
|
+
|
9
|
+
# The countries the gateway supports merchants from as 2 digit ISO country codes
|
10
|
+
self.supported_countries = ['US']
|
11
|
+
|
12
|
+
# The card types supported by the payment gateway
|
13
|
+
self.supported_cardtypes = [:visa, :master, :american_express, :discover]
|
14
|
+
|
15
|
+
# The homepage URL of the gateway
|
16
|
+
self.homepage_url = 'http://www.heartlandpaymentsystems.com/Payment-Processing/Payment-Processing/Portico-Virtual-Terminal'
|
17
|
+
|
18
|
+
# The name of the gateway
|
19
|
+
self.display_name = 'Heartland Portico'
|
20
|
+
|
21
|
+
self.money_format = :dollars
|
22
|
+
|
23
|
+
def initialize(options = {})
|
24
|
+
@options = options
|
25
|
+
requires!(@options, :user_name, :password)
|
26
|
+
requires!(@options, :developer_i_d, :device_id, :license_id, :site_id, :version_nbr)
|
27
|
+
super
|
28
|
+
end
|
29
|
+
|
30
|
+
def verify(creditcard, options = {})
|
31
|
+
post = {}
|
32
|
+
add_creditcard(post, creditcard)
|
33
|
+
add_address(post, creditcard, options)
|
34
|
+
add_customer_data(post, options)
|
35
|
+
|
36
|
+
commit('credit_account_verify', nil, post)
|
37
|
+
end
|
38
|
+
|
39
|
+
def authorize(money, creditcard, options = {})
|
40
|
+
post = initial_post(options)
|
41
|
+
add_invoice(post, options)
|
42
|
+
add_creditcard(post, creditcard)
|
43
|
+
add_address(post, creditcard, options)
|
44
|
+
add_customer_data(post, options)
|
45
|
+
|
46
|
+
commit('credit_auth', money, post)
|
47
|
+
end
|
48
|
+
|
49
|
+
def purchase(money, creditcard, options = {})
|
50
|
+
post = initial_post(options)
|
51
|
+
add_invoice(post, options)
|
52
|
+
add_creditcard(post, creditcard)
|
53
|
+
add_address(post, creditcard, options)
|
54
|
+
add_customer_data(post, options)
|
55
|
+
|
56
|
+
commit('credit_sale', money, post)
|
57
|
+
end
|
58
|
+
|
59
|
+
def capture(money, authorization, options = {})
|
60
|
+
post = initial_post(options)
|
61
|
+
add_authorization(post, authorization)
|
62
|
+
|
63
|
+
commit('credit_add_to_batch', money, post)
|
64
|
+
end
|
65
|
+
|
66
|
+
def return(money, creditcard, options = {})
|
67
|
+
post = initial_post(options)
|
68
|
+
add_creditcard(post, creditcard)
|
69
|
+
|
70
|
+
commit('credit_return', money, post)
|
71
|
+
end
|
72
|
+
alias refund return
|
73
|
+
|
74
|
+
def reverse(money, authorization, options = {})
|
75
|
+
post = initial_post(options)
|
76
|
+
add_authorization(post, authorization)
|
77
|
+
|
78
|
+
commit('credit_reversal', money, post)
|
79
|
+
end
|
80
|
+
|
81
|
+
def void(authorization, options = {})
|
82
|
+
post = initial_post(options)
|
83
|
+
add_authorization(post, authorization)
|
84
|
+
|
85
|
+
commit('credit_void', nil, post)
|
86
|
+
end
|
87
|
+
|
88
|
+
private
|
89
|
+
|
90
|
+
def add_customer_data(post, options)
|
91
|
+
# TODO?
|
92
|
+
end
|
93
|
+
|
94
|
+
def add_address(post, creditcard, options)
|
95
|
+
address = options[:billing_address] || {}
|
96
|
+
post[:card_holder_data] = {
|
97
|
+
:card_holder_first_name => creditcard.first_name,
|
98
|
+
:card_holder_last_name => creditcard.last_name,
|
99
|
+
:card_holder_addr => [address[:address1], address[:address2]].compact.join(" "),
|
100
|
+
:card_holder_city => address[:city],
|
101
|
+
:card_holder_state => address[:state],
|
102
|
+
:card_holder_zip => address[:zip].to_s.gsub(/[^0-9A-Za-z]/, ''),
|
103
|
+
:card_holder_phone => address[:phone].to_s.gsub(/[^0-9]/, ''),
|
104
|
+
#:card_holder_email => "",
|
105
|
+
}
|
106
|
+
end
|
107
|
+
|
108
|
+
def add_authorization(post, authorization)
|
109
|
+
post[:gateway_txn_id] = authorization
|
110
|
+
end
|
111
|
+
|
112
|
+
def add_creditcard(post, creditcard)
|
113
|
+
post[:card_data] = {
|
114
|
+
:manual_entry => {
|
115
|
+
:card_nbr => creditcard.number,
|
116
|
+
:exp_month => creditcard.month,
|
117
|
+
:exp_year => creditcard.year,
|
118
|
+
:c_v_v_2 => creditcard.verification_value,
|
119
|
+
:card_present => 'N',
|
120
|
+
:reader_present => 'N',
|
121
|
+
}
|
122
|
+
}
|
123
|
+
end
|
124
|
+
|
125
|
+
def initial_post(options)
|
126
|
+
# Not usually expected to be true, mostly just here for
|
127
|
+
# testing purposes
|
128
|
+
{ :allow_dup => options[:allow_dup] }
|
129
|
+
end
|
130
|
+
|
131
|
+
def add_invoice(post, options)
|
132
|
+
# avoid empty hash
|
133
|
+
return unless options[:invoice] || options[:invoice_ship_month] || options[:invoice_ship_day]
|
134
|
+
post[:direct_mkt_data] = {
|
135
|
+
:direct_mkt_invoice_nbr => options[:invoice],
|
136
|
+
:direct_mkt_ship_month => options[:invoice_ship_month],
|
137
|
+
:direct_mkt_ship_day => options[:invoice_ship_day],
|
138
|
+
}
|
139
|
+
end
|
140
|
+
|
141
|
+
def client
|
142
|
+
@client ||= HeartlandPortico.new(@options, test?)
|
143
|
+
end
|
144
|
+
|
145
|
+
def commit(action, money, parameters)
|
146
|
+
parameters[:amt] = amount(money)
|
147
|
+
translate_response(client.send(action, parameters))
|
148
|
+
end
|
149
|
+
|
150
|
+
def translate_response(heartland_response)
|
151
|
+
Response.new(
|
152
|
+
heartland_response.success?,
|
153
|
+
heartland_response.message,
|
154
|
+
heartland_response.body,
|
155
|
+
{ :fraud_review => false,
|
156
|
+
:authorization => heartland_response.authorization,
|
157
|
+
:test => test?,
|
158
|
+
:avs_result => { :code => heartland_response.avs_code },
|
159
|
+
:cvv_result => heartland_response.cvv_code
|
160
|
+
}
|
161
|
+
)
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
@@ -0,0 +1,182 @@
|
|
1
|
+
require 'savon'
|
2
|
+
|
3
|
+
# Note: Full data schema can be found at:
|
4
|
+
# https://cert.api2.heartlandportico.com/Hps.Exchange.PosGateway/POSGatewayService.asmx?schema=schema1
|
5
|
+
|
6
|
+
# Additional Note: No idea what schema version that's for, or if it ever returns
|
7
|
+
# anything except latest :(
|
8
|
+
|
9
|
+
class HeartlandPortico
|
10
|
+
attr_reader :credentials
|
11
|
+
def initialize(credentials, test=false)
|
12
|
+
@credentials = credentials
|
13
|
+
@test = test
|
14
|
+
end
|
15
|
+
|
16
|
+
SUPPORTED_REQUEST_METHODS = %w(
|
17
|
+
check_sale
|
18
|
+
check_void
|
19
|
+
credit_account_verify
|
20
|
+
credit_add_to_batch
|
21
|
+
credit_auth
|
22
|
+
credit_return
|
23
|
+
credit_reversal
|
24
|
+
credit_sale
|
25
|
+
credit_void
|
26
|
+
)
|
27
|
+
|
28
|
+
SUPPORTED_REQUEST_METHODS.each do |method|
|
29
|
+
define_method method do |params|
|
30
|
+
client_call(method, params)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
def camelize(string)
|
36
|
+
# Couldn't get ActiveSupport to load standalone, rolling my own
|
37
|
+
string.to_s.gsub(/(?:^|_)(.)/) { $1.upcase }
|
38
|
+
end
|
39
|
+
|
40
|
+
def client
|
41
|
+
Savon.client do |savon|
|
42
|
+
savon.wsdl wsdl_path
|
43
|
+
savon.soap_version 2
|
44
|
+
savon.endpoint endpoint
|
45
|
+
|
46
|
+
# savon.log_level :debug
|
47
|
+
# savon.log true
|
48
|
+
|
49
|
+
savon.convert_request_keys_to :camelcase
|
50
|
+
savon.pretty_print_xml true
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def client_call(operation, params)
|
55
|
+
message = xml_message(operation) do |xml|
|
56
|
+
if pin_block?(operation)
|
57
|
+
xml.tag!("tns:Block1") do
|
58
|
+
xml_build(xml, params)
|
59
|
+
end
|
60
|
+
else
|
61
|
+
xml_build(xml, params)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
Response.new(operation, client.call(:do_transaction, :message_tag => 'PosRequest', :message => message))
|
65
|
+
end
|
66
|
+
|
67
|
+
def endpoint
|
68
|
+
if @test
|
69
|
+
"https://cert.api2-C.heartlandportico.com/Hps.Exchange.PosGateway/POSGatewayService.asmx"
|
70
|
+
else
|
71
|
+
"https://api2-C.heartlandportico.com/Hps.Exchange.PosGateway/PosGatewayService.asmx"
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def pin_block?(operation)
|
76
|
+
%w(
|
77
|
+
check_sale
|
78
|
+
check_void
|
79
|
+
credit_account_verify
|
80
|
+
credit_auth
|
81
|
+
credit_sale
|
82
|
+
credit_return
|
83
|
+
credit_reversal
|
84
|
+
).include? operation.to_s
|
85
|
+
end
|
86
|
+
|
87
|
+
# Loaded live in production, or from a local cache for testing
|
88
|
+
def wsdl_path
|
89
|
+
if @test
|
90
|
+
File.expand_path(File.join(__FILE__, '..', 'wsdl.xml'))
|
91
|
+
else
|
92
|
+
"#{endpoint}?wsdl=wsdl1"
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def xml_build(xml, values)
|
97
|
+
values.keys.sort_by{|k|k.to_s}.each do |key|
|
98
|
+
value = values[key]
|
99
|
+
tag = "tns:#{camelize(key)}"
|
100
|
+
|
101
|
+
if value.to_s.empty?
|
102
|
+
# Skip empty values
|
103
|
+
elsif value.kind_of? Hash
|
104
|
+
xml.tag!(tag) { xml_build(xml, value) }
|
105
|
+
else
|
106
|
+
xml.tag!(tag, value)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def xml_message(operation)
|
112
|
+
xml = Builder::XmlMarkup.new
|
113
|
+
xml.tag!("tns:Ver1.0") do
|
114
|
+
xml.tag!("tns:Header") do
|
115
|
+
xml_build(xml, credentials)
|
116
|
+
end
|
117
|
+
xml.tag!("tns:Transaction") do
|
118
|
+
xml.tag!("tns:#{camelize(operation)}") do
|
119
|
+
yield xml
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
class Response
|
126
|
+
attr_reader :body, :operation
|
127
|
+
def initialize(operation, savon_response)
|
128
|
+
@operation = operation.to_sym
|
129
|
+
@body = savon_response.body[:pos_response][:ver1_0]
|
130
|
+
end
|
131
|
+
|
132
|
+
def authorization
|
133
|
+
body[:header][:gateway_txn_id] if success?
|
134
|
+
end
|
135
|
+
|
136
|
+
def message
|
137
|
+
return "ERROR" unless successful_request?
|
138
|
+
# CreditAddToBatch has an explicit empty response, with hard errors if not successful
|
139
|
+
return "APPROVAL" if body[:transaction].has_key?(operation) and body[:transaction][operation].nil?
|
140
|
+
response[:rsp_text] || response[:rsp_message]
|
141
|
+
end
|
142
|
+
|
143
|
+
def success?
|
144
|
+
["APPROVAL", "CARD OK", "Transaction Approved"].include? message
|
145
|
+
end
|
146
|
+
|
147
|
+
def successful_request?
|
148
|
+
body[:header][:gateway_rsp_msg] == "Success"
|
149
|
+
end
|
150
|
+
|
151
|
+
def avs_code
|
152
|
+
response[:avs_rslt_code]
|
153
|
+
end
|
154
|
+
def avs_address
|
155
|
+
%w(A Y X B D M).include? avs_code
|
156
|
+
end
|
157
|
+
def avs_zip
|
158
|
+
%w(Z W Y X D M P).include? avs_code
|
159
|
+
end
|
160
|
+
|
161
|
+
def cvv_code
|
162
|
+
response[:cvv_rslt_code]
|
163
|
+
end
|
164
|
+
|
165
|
+
def cvv
|
166
|
+
# Valid Values:
|
167
|
+
# M: Match
|
168
|
+
# N: No Match
|
169
|
+
# P: Not Processed
|
170
|
+
# S: Should have been present
|
171
|
+
# U: Issuer not certified
|
172
|
+
cvv_code == "M"
|
173
|
+
end
|
174
|
+
|
175
|
+
def response
|
176
|
+
# Yes, I know that's a lot of edge case handling, but I _really_
|
177
|
+
# want that to be a hash.
|
178
|
+
body[:transaction][operation] || {} rescue {}
|
179
|
+
end
|
180
|
+
alias params response
|
181
|
+
end
|
182
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
class HeartlandPortico
|
2
|
+
class ACH
|
3
|
+
# Credentials include:
|
4
|
+
# developer_i_d
|
5
|
+
# device_id
|
6
|
+
# license_id
|
7
|
+
# password
|
8
|
+
# site_id
|
9
|
+
# user_name
|
10
|
+
# version_nbr
|
11
|
+
def initialize(credentials, test=true)
|
12
|
+
@credentials = credentials
|
13
|
+
@test = test
|
14
|
+
end
|
15
|
+
|
16
|
+
# Params:
|
17
|
+
# sec_code (PPD for consumer, CCD for corporate)
|
18
|
+
# routing_number
|
19
|
+
# account_number
|
20
|
+
# account_type (CHECKING or SAVINGS)
|
21
|
+
# check_type (PERSONAL, BUSINESS, or PAYROLL)
|
22
|
+
# first_name
|
23
|
+
# last_name
|
24
|
+
# address
|
25
|
+
# city
|
26
|
+
# state
|
27
|
+
# zip
|
28
|
+
# phone
|
29
|
+
def sale(amount, params)
|
30
|
+
client.check_sale(
|
31
|
+
:check_action => 'SALE', # OVERRIDE, RETURN
|
32
|
+
:data_entry_mode => 'MANUAL', # SWIPE
|
33
|
+
:amt => "%.2f" % (amount.to_i / 100.0),
|
34
|
+
|
35
|
+
:s_e_c_code => params[:sec_code], # PPD, CCD
|
36
|
+
:account_info => {
|
37
|
+
:routing_number => params[:routing_number],
|
38
|
+
:account_number => params[:account_number],
|
39
|
+
#:check_number # Populate this with transaction ID?
|
40
|
+
#:m_i_c_r_data
|
41
|
+
:account_type => params[:account_type], # CHECKING, SAVINGS
|
42
|
+
},
|
43
|
+
:check_type => params[:check_type], # PERSONAL, BUSINESS, PAYROLL
|
44
|
+
#:verify_info
|
45
|
+
#:check_verify
|
46
|
+
#:a_c_h_verify
|
47
|
+
:consumer_info => {
|
48
|
+
:first_name => params[:first_name],
|
49
|
+
:last_name => params[:last_name],
|
50
|
+
:check_name => params[:memo],
|
51
|
+
:address1 => params[:address],
|
52
|
+
#:address2
|
53
|
+
:city => params[:city],
|
54
|
+
:state => params[:state],
|
55
|
+
:zip => params[:zip],
|
56
|
+
:phone_number => params[:phone],
|
57
|
+
#:email_address => params[:email],
|
58
|
+
#:d_l_state
|
59
|
+
#:d_l_number
|
60
|
+
#:courtesy_card
|
61
|
+
#:identity_info
|
62
|
+
#:s_s_n_l4
|
63
|
+
#:d_o_b_year
|
64
|
+
}
|
65
|
+
#:additional_txn_fields
|
66
|
+
#:payment_method_key
|
67
|
+
#:recurring_data
|
68
|
+
#:token_value
|
69
|
+
)
|
70
|
+
end
|
71
|
+
|
72
|
+
def void(authorization)
|
73
|
+
client.check_void(authorization)
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
def client
|
78
|
+
HeartlandPortico.new(@credentials, @test)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
data/lib/wsdl.xml
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
<wsdl:definitions xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns:tm="http://microsoft.com/wsdl/mime/textMatching/" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/" xmlns:mime="http://schemas.xmlsoap.org/wsdl/mime/" xmlns:tns="http://Hps.Exchange.PosGateway" xmlns:s="http://www.w3.org/2001/XMLSchema" xmlns:soap12="http://schemas.xmlsoap.org/wsdl/soap12/" xmlns:http="http://schemas.xmlsoap.org/wsdl/http/" xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/" targetNamespace="http://Hps.Exchange.PosGateway">
|
2
|
+
<wsdl:types>
|
3
|
+
<s:schema targetNamespace="http://Hps.Exchange.PosGateway">
|
4
|
+
<s:include schemaLocation="https://cert.api2.heartlandportico.com/Hps.Exchange.PosGateway/POSGatewayService.asmx?schema=schema1"/>
|
5
|
+
</s:schema>
|
6
|
+
</wsdl:types>
|
7
|
+
<wsdl:message name="DoTransactionSoapIn">
|
8
|
+
<wsdl:part name="PosRequest" element="tns:PosRequest"/>
|
9
|
+
</wsdl:message>
|
10
|
+
<wsdl:message name="DoTransactionSoapOut">
|
11
|
+
<wsdl:part name="DoTransactionResult" element="tns:PosResponse"/>
|
12
|
+
</wsdl:message>
|
13
|
+
<wsdl:portType name="PosGatewayInterface">
|
14
|
+
<wsdl:operation name="DoTransaction">
|
15
|
+
<wsdl:input message="tns:DoTransactionSoapIn"/>
|
16
|
+
<wsdl:output message="tns:DoTransactionSoapOut"/>
|
17
|
+
</wsdl:operation>
|
18
|
+
</wsdl:portType>
|
19
|
+
<wsdl:binding name="PosGatewayInterface" type="tns:PosGatewayInterface">
|
20
|
+
<soap:binding transport="http://schemas.xmlsoap.org/soap/http"/>
|
21
|
+
<wsdl:operation name="DoTransaction">
|
22
|
+
<soap:operation soapAction="" style="document"/>
|
23
|
+
<wsdl:input>
|
24
|
+
<soap:body use="literal"/>
|
25
|
+
</wsdl:input>
|
26
|
+
<wsdl:output>
|
27
|
+
<soap:body use="literal"/>
|
28
|
+
</wsdl:output>
|
29
|
+
</wsdl:operation>
|
30
|
+
</wsdl:binding>
|
31
|
+
<wsdl:binding name="PosGatewayInterface1" type="tns:PosGatewayInterface">
|
32
|
+
<soap12:binding transport="http://schemas.xmlsoap.org/soap/http"/>
|
33
|
+
<wsdl:operation name="DoTransaction">
|
34
|
+
<soap12:operation soapAction="" style="document"/>
|
35
|
+
<wsdl:input>
|
36
|
+
<soap12:body use="literal"/>
|
37
|
+
</wsdl:input>
|
38
|
+
<wsdl:output>
|
39
|
+
<soap12:body use="literal"/>
|
40
|
+
</wsdl:output>
|
41
|
+
</wsdl:operation>
|
42
|
+
</wsdl:binding>
|
43
|
+
</wsdl:definitions>
|