heartland_portico 3.2.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -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>