CnpChargeback 2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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