CnpChargeback 2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,97 @@
1
+ =begin
2
+ Copyright (c) 2017 Vantiv eCommerce
3
+
4
+ Permission is hereby granted, free of charge, to any person
5
+ obtaining a copy of this software and associated documentation
6
+ files (the "Software"), to deal in the Software without
7
+ restriction, including without limitation the rights to use,
8
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the
10
+ Software is furnished to do so, subject to the following
11
+ conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
18
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
20
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
21
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
22
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
23
+ OTHER DEALINGS IN THE SOFTWARE.
24
+ =end
25
+ require_relative 'Configuration'
26
+
27
+ #
28
+ # This class handles sending the Cnp online request
29
+ #
30
+ module CnpOnline
31
+
32
+ class ChargebackUpdate
33
+ def initialize
34
+ @config_hash = Configuration.new.config
35
+ end
36
+
37
+ def assign_case_to_user(case_id:, user_id:, note:, config: @config_hash)
38
+ update_request = build_request
39
+ update_request.activityType = "ASSIGN_TO_USER"
40
+ update_request.note = note
41
+ update_request.assignedTo = user_id
42
+ return _get_update_response(case_id, update_request, config)
43
+ end
44
+
45
+ def add_note_to_case(case_id:, note:, config: @config_hash)
46
+ update_request = build_request
47
+ update_request.activityType = "ADD_NOTE"
48
+ update_request.note = note
49
+ return _get_update_response(case_id, update_request, config)
50
+ end
51
+
52
+ def assume_liability(case_id:, note:, config: @config_hash)
53
+ update_request = build_request
54
+ update_request.activityType = "MERCHANT_ACCEPTS_LIABILITY"
55
+ update_request.note = note
56
+ return _get_update_response(case_id, update_request, config)
57
+ end
58
+
59
+ def represent_case(case_id:, note:, representment_amount: nil, config: @config_hash)
60
+ update_request = build_request
61
+ update_request.activityType = "MERCHANT_REPRESENT"
62
+ update_request.note = note
63
+ update_request.representedAmount = representment_amount
64
+ return _get_update_response(case_id, update_request, config)
65
+ end
66
+
67
+ def respond_to_retrieval_request(case_id:, note:, config: @config_hash)
68
+ update_request = build_request
69
+ update_request.activityType = "MERCHANT_RESPOND"
70
+ update_request.note = note
71
+ return _get_update_response(case_id, update_request, config)
72
+ end
73
+
74
+ def request_arbitration(case_id:, note:, config: @config_hash)
75
+ update_request = build_request
76
+ update_request.activityType = "MERCHANT_REQUESTS_ARBITRATION"
77
+ update_request.note = note
78
+ return _get_update_response(case_id, update_request, config)
79
+ end
80
+
81
+ private
82
+
83
+ def build_request()
84
+ update_request = UpdateRequest.new
85
+ update_request.xmlns = "http://www.vantivcnp.com/chargebacks"
86
+ return update_request
87
+ end
88
+
89
+ def _get_update_response(parameter_value1, update_request, config)
90
+ request_url = config['url'] + "/" + parameter_value1.to_s
91
+ request_xml = update_request.save_to_xml.to_s
92
+ response_xml = Communications.http_put_update_request(request_url, request_xml, config)
93
+ return XMLObject.new(response_xml)
94
+ end
95
+
96
+ end
97
+ end
@@ -0,0 +1,61 @@
1
+ =begin
2
+ Copyright (c) 2017 Vantiv eCommerce
3
+
4
+ Permission is hereby granted, free of charge, to any person
5
+ obtaining a copy of this software and associated documentation
6
+ files (the "Software"), to deal in the Software without
7
+ restriction, including without limitation the rights to use,
8
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the
10
+ Software is furnished to do so, subject to the following
11
+ conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
18
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
20
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
21
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
22
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
23
+ OTHER DEALINGS IN THE SOFTWARE.
24
+ =end
25
+
26
+ #
27
+ # Header file containing required gems and ruby files
28
+ # Defines the Gem
29
+ #
30
+ require 'rubygems'
31
+ require 'net/http'
32
+ require 'xml-object'
33
+ require 'yaml'
34
+ require 'uri'
35
+ require 'net/https'
36
+ require 'xml/mapping'
37
+ require 'mimemagic'
38
+ require 'iostreams'
39
+
40
+ unless Kernel.respond_to?(:require_relative)
41
+ module Kernel
42
+ def require_relative(path)
43
+ require File.join(File.dirname(caller[0]), path.to_str)
44
+ end
45
+ end
46
+ end
47
+
48
+ require_relative 'Communications'
49
+ require_relative 'XMLFields'
50
+ require_relative 'Configuration'
51
+ require_relative 'ChargebackRetrieval'
52
+ require_relative 'ChargebackUpdate'
53
+ require_relative 'ChargebackDocument'
54
+
55
+
56
+ #allows attribute values to be in double quotes, required by Cnp Server
57
+ REXML::Attribute.class_eval( %q^
58
+ def to_string
59
+ %Q[#@expanded_name="#{to_s().gsub(/"/, '"')}"]
60
+ end
61
+ ^ )
@@ -0,0 +1,314 @@
1
+ =begin
2
+ Copyright (c) 2017 Vantiv eCommerce
3
+
4
+ Permission is hereby granted, free of charge, to any person
5
+ obtaining a copy of this software and associated documentation
6
+ files (the "Software"), to deal in the Software without
7
+ restriction, including without limitation the rights to use,
8
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the
10
+ Software is furnished to do so, subject to the following
11
+ conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
18
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
20
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
21
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
22
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
23
+ OTHER DEALINGS IN THE SOFTWARE.
24
+ =end
25
+
26
+ #
27
+ # Used for all transmission to Cnp over HTTP or HTTPS
28
+ # works with or without an HTTP proxy
29
+ #
30
+ # URL and proxy server settings are derived from the configuration file
31
+ #
32
+
33
+ module CnpOnline
34
+ class Communications
35
+ CHARGEBACK_API_HEADERS = {'Accept' => 'application/com.vantivcnp.services-v2+xml',
36
+ 'Content-Type' => 'application/com.vantivcnp.services-v2+xml'}
37
+
38
+ CB_DOCUMENT_API_HEADERS = {'Content-Type' => 'image/gif'}
39
+
40
+ def self.http_get_document_list_request(request_url, config_hash)
41
+ proxy_addr = config_hash['proxy_addr']
42
+ proxy_port = config_hash['proxy_port']
43
+ url = URI.parse(request_url)
44
+ logger = initialize_logger(config_hash)
45
+ http_response = nil
46
+
47
+ https = Net::HTTP.new(url.host, url.port, proxy_addr, proxy_port)
48
+ if url.scheme == 'https'
49
+ https.use_ssl = url.scheme=='https'
50
+ https.verify_mode = OpenSSL::SSL::VERIFY_PEER
51
+ https.ca_file = File.join(File.dirname(__FILE__), "cacert.pem")
52
+ end
53
+
54
+ req = Net::HTTP::Get.new(url, CB_DOCUMENT_API_HEADERS)
55
+ req.basic_auth(config_hash['user'], config_hash['password'])
56
+
57
+ logger.debug "Get request to: " + url.to_s + "\n"
58
+
59
+ https.start { |http|
60
+ http_response = http.request(req)
61
+ }
62
+
63
+ logger.debug http_response.body
64
+ check_response(http_response, config_hash)
65
+ return http_response.body
66
+ end
67
+
68
+ def self.http_delete_document_request(request_url, config_hash)
69
+ proxy_addr = config_hash['proxy_addr']
70
+ proxy_port = config_hash['proxy_port']
71
+ url = URI.parse(request_url)
72
+ logger = initialize_logger(config_hash)
73
+ http_response = nil
74
+
75
+ https = Net::HTTP.new(url.host, url.port, proxy_addr, proxy_port)
76
+ if url.scheme == 'https'
77
+ https.use_ssl = url.scheme=='https'
78
+ https.verify_mode = OpenSSL::SSL::VERIFY_PEER
79
+ https.ca_file = File.join(File.dirname(__FILE__), "cacert.pem")
80
+ end
81
+
82
+ req = Net::HTTP::Delete.new(url)
83
+ req.basic_auth(config_hash['user'], config_hash['password'])
84
+
85
+ logger.debug "DELETE request to: " + url.to_s + "\n"
86
+
87
+ https.start { |http|
88
+ http_response = http.request(req)
89
+ }
90
+
91
+ logger.debug http_response.body
92
+ check_response(http_response, config_hash)
93
+ return http_response.body
94
+ end
95
+
96
+
97
+ def self.http_put_document_request(request_url, document_path, config_hash)
98
+ proxy_addr = config_hash['proxy_addr']
99
+ proxy_port = config_hash['proxy_port']
100
+ url = URI.parse(request_url)
101
+ logger = initialize_logger(config_hash)
102
+ http_response = nil
103
+
104
+ https = Net::HTTP.new(url.host, url.port, proxy_addr, proxy_port)
105
+ if url.scheme == 'https'
106
+ https.use_ssl = url.scheme=='https'
107
+ https.verify_mode = OpenSSL::SSL::VERIFY_PEER
108
+ https.ca_file = File.join(File.dirname(__FILE__), "cacert.pem")
109
+ end
110
+
111
+ data, content_type = read_document(document_path)
112
+ req = Net::HTTP::Put.new(url, {'Content-Type' => content_type})
113
+ req.basic_auth(config_hash['user'], config_hash['password'])
114
+ req.body = data
115
+
116
+ logger.debug "PUT request to: " + url.to_s + "\n"
117
+
118
+ https.start { |http|
119
+ http_response = http.request(req)
120
+ }
121
+
122
+ logger.debug http_response.body
123
+ check_response(http_response, config_hash)
124
+ return http_response.body
125
+ end
126
+
127
+ def self.http_post_document_request(request_url, document_path, config_hash)
128
+ proxy_addr = config_hash['proxy_addr']
129
+ proxy_port = config_hash['proxy_port']
130
+ url = URI.parse(request_url)
131
+ logger = initialize_logger(config_hash)
132
+ http_response = nil
133
+
134
+ https = Net::HTTP.new(url.host, url.port, proxy_addr, proxy_port)
135
+ if url.scheme == 'https'
136
+ https.use_ssl = url.scheme=='https'
137
+ https.verify_mode = OpenSSL::SSL::VERIFY_PEER
138
+ https.ca_file = File.join(File.dirname(__FILE__), "cacert.pem")
139
+ end
140
+
141
+ data, content_type = read_document(document_path)
142
+ req = Net::HTTP::Post.new(url, {'Content-Type' => content_type})
143
+ req.basic_auth(config_hash['user'], config_hash['password'])
144
+ req.body = data
145
+
146
+ logger.debug "POST request to: " + url.to_s + "\n"
147
+
148
+ https.start { |http|
149
+ http_response = http.request(req)
150
+ }
151
+
152
+ logger.debug http_response.body
153
+ check_response(http_response, config_hash)
154
+ return http_response.body
155
+ end
156
+
157
+ def self.http_get_document_request(request_url, document_path, config_hash)
158
+ proxy_addr = config_hash['proxy_addr']
159
+ proxy_port = config_hash['proxy_port']
160
+ url = URI.parse(request_url)
161
+ logger = initialize_logger(config_hash)
162
+ http_response = nil
163
+
164
+ https = Net::HTTP.new(url.host, url.port, proxy_addr, proxy_port)
165
+ if url.scheme == 'https'
166
+ https.use_ssl = url.scheme=='https'
167
+ https.verify_mode = OpenSSL::SSL::VERIFY_PEER
168
+ https.ca_file = File.join(File.dirname(__FILE__), "cacert.pem")
169
+ end
170
+
171
+ req = Net::HTTP::Get.new(url)
172
+ req.basic_auth(config_hash['user'], config_hash['password'])
173
+
174
+ logger.debug "GET request to: " + url.to_s + "\n"
175
+
176
+ https.start { |http|
177
+ http_response = http.request(req)
178
+ }
179
+
180
+ check_response(http_response, config_hash)
181
+ write_document(http_response, document_path, config_hash)
182
+ end
183
+
184
+ def self.http_get_retrieval_request(request_url, config_hash)
185
+ proxy_addr = config_hash['proxy_addr']
186
+ proxy_port = config_hash['proxy_port']
187
+ url = URI.parse(request_url)
188
+ logger = initialize_logger(config_hash)
189
+ http_response = nil
190
+
191
+ https = Net::HTTP.new(url.host, url.port, proxy_addr, proxy_port)
192
+ if url.scheme == 'https'
193
+ https.use_ssl = url.scheme=='https'
194
+ https.verify_mode = OpenSSL::SSL::VERIFY_PEER
195
+ https.ca_file = File.join(File.dirname(__FILE__), "cacert.pem")
196
+ end
197
+
198
+ req = Net::HTTP::Get.new(url, CHARGEBACK_API_HEADERS)
199
+ req.basic_auth(config_hash['user'], config_hash['password'])
200
+
201
+ logger.debug "GET request to: " + url.to_s + "\n"
202
+
203
+ https.start { |http|
204
+ http_response = http.request(req)
205
+ }
206
+
207
+ logger.debug http_response.body
208
+ check_response(http_response, config_hash)
209
+ return http_response.body
210
+ end
211
+
212
+ def self.http_put_update_request(request_url, request_xml, config_hash)
213
+ proxy_addr = config_hash['proxy_addr']
214
+ proxy_port = config_hash['proxy_port']
215
+ url = URI.parse(request_url)
216
+ logger = initialize_logger(config_hash)
217
+ http_response = nil
218
+
219
+ https = Net::HTTP.new(url.host, url.port, proxy_addr, proxy_port)
220
+ if url.scheme == 'https'
221
+ https.use_ssl = url.scheme=='https'
222
+ https.verify_mode = OpenSSL::SSL::VERIFY_PEER
223
+ https.ca_file = File.join(File.dirname(__FILE__), "cacert.pem")
224
+ end
225
+
226
+ req = Net::HTTP::Put.new(url, CHARGEBACK_API_HEADERS)
227
+ req.basic_auth(config_hash['user'], config_hash['password'])
228
+ req.body = request_xml
229
+
230
+ logger.debug "PUT request to: " + url.to_s + "\n"
231
+ logger.debug request_xml + "\n"
232
+
233
+ https.start { |http|
234
+ http_response = http.request(req)
235
+ }
236
+
237
+ logger.debug http_response.body
238
+ check_response(http_response, config_hash)
239
+ return http_response.body
240
+ end
241
+
242
+ private
243
+
244
+ def self.read_document(document_path)
245
+ data = nil
246
+ open(document_path, 'rb') { |f|
247
+ data = f.read
248
+ }
249
+ content_type = nil
250
+ mimemagic = MimeMagic.by_path(document_path)
251
+ if mimemagic
252
+ content_type = mimemagic.type
253
+ end
254
+ return data, content_type
255
+ end
256
+
257
+ def self.write_document(http_response, document_path, config_hash)
258
+ content_type = http_response.content_type
259
+ if content_type != "image/tiff"
260
+ raise ("Wrong response content type")
261
+ end
262
+ else
263
+ open(document_path, "wb") do |file|
264
+ file.write(http_response.body)
265
+ end
266
+ puts("\nDocument saved at: ", document_path)
267
+ end
268
+
269
+ def self.check_response(http_response, config_hash)
270
+ if http_response == nil
271
+ raise("The response is empty, Please call Vantiv eCommerce")
272
+ end
273
+
274
+ if http_response.code != "200"
275
+ puts("Error Response : \n" + http_response.body)
276
+ raise("Error with http response, code:" + http_response.header.code)
277
+ end
278
+ end
279
+
280
+ def self.initialize_logger(config_hash)
281
+ # Sadly, this needs to be static (the alternative would be to change the CnpXmlMapper.request API
282
+ # to accept a Configuration instance instead of the config_hash)
283
+ Configuration.logger ||= default_logger config_hash['printxml'] ? Logger::DEBUG : Logger::INFO
284
+ end
285
+
286
+ def self.default_logger(level) # :nodoc:
287
+ logger = Logger.new(STDOUT)
288
+ logger.level = level
289
+ # Backward compatible logging format for pre 8.16
290
+ logger.formatter = proc { |severity, datetime, progname, msg| "#{msg}\n" }
291
+ logger
292
+ end
293
+ end
294
+ end
295
+
296
+ =begin
297
+ NOTES ON HTTP TIMEOUT
298
+
299
+ Vantiv eCommerce optimizes our systems to ensure the return of responses as quickly as possible, some portions of the process are beyond our control.
300
+ The round-trip time of an Authorization can be broken down into three parts, as follows:
301
+ 1. Transmission time (across the internet) to Vantiv eCommerce and back to the merchant
302
+ 2. Processing time by the authorization provider
303
+ 3. Processing time by Cnp
304
+ Under normal operating circumstances, the transmission time to and from Cnp does not exceed 0.6 seconds
305
+ and processing overhead by Cnp occurs in 0.1 seconds.
306
+ Typically, the processing time by the card association or authorization provider can take between 0.5 and 3 seconds,
307
+ but some percentage of transactions may take significantly longer.
308
+
309
+ Because the total processing time can vary due to a number of factors, Vantiv eCommerce recommends using a minimum timeout setting of
310
+ 60 seconds to accomodate Sale transactions and 30 seconds if you are not utilizing Sale tranactions.
311
+
312
+ These settings should ensure that you do not frequently disconnect prior to receiving a valid authorization causing dropped orders
313
+ and/or re-auths and duplicate auths.
314
+ =end