LitleOnline 8.18.0 → 8.19.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,513 @@
1
+ =begin
2
+ Copyright (c) 2011 Litle & Co.
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
+ require 'net/sftp'
27
+ require 'libxml'
28
+ require 'crack/xml'
29
+ require 'socket'
30
+
31
+ include Socket::Constants
32
+ #
33
+ # This class handles sending the Litle Request (which is actually a series of batches!)
34
+ #
35
+
36
+ module LitleOnline
37
+ class LitleRequest
38
+ include XML::Mapping
39
+ def initialize(options = {})
40
+ #load configuration data
41
+ @config_hash = Configuration.new.config
42
+ @num_batch_requests = 0
43
+ @path_to_request = ""
44
+ @path_to_batches = ""
45
+ @num_total_transactions = 0
46
+ @MAX_NUM_TRANSACTIONS = 500000
47
+ @options = options
48
+ # current time out set to 2 mins
49
+ # this value is in seconds
50
+ @RESPONSE_TIME_OUT = 360
51
+ @POLL_DELAY = 0
52
+ @responses_expected = 0
53
+ end
54
+
55
+ # Creates the necessary files for the LitleRequest at the path specified. path/request_(TIMESTAMP) will be
56
+ # the final XML markup and path/request_(TIMESTAMP) will hold intermediary XML markup
57
+ # Params:
58
+ # +path+:: A +String+ containing the path to the folder on disc to write the files to
59
+ def create_new_litle_request(path)
60
+ ts = Time::now.to_i.to_s
61
+ begin
62
+ ts += Time::now.nsec.to_s
63
+ rescue NoMethodError # ruby 1.8.7 fix
64
+ ts += Time::now.usec.to_s
65
+ end
66
+
67
+ if(File.file?(path)) then
68
+ raise RuntimeError, "Entered a file not a path."
69
+ end
70
+
71
+ if(path[-1,1] != '/' and path[-1,1] != '\\') then
72
+ path = path + File::SEPARATOR
73
+ end
74
+
75
+ if !File.directory?(path) then
76
+ Dir.mkdir(path)
77
+ end
78
+
79
+ @path_to_request = path + 'request_' + ts
80
+ @path_to_batches = @path_to_request + '_batches'
81
+
82
+ if File.file?(@path_to_request) or File.file?(@path_to_batches) then
83
+ create_new_litle_request(path)
84
+ return
85
+ end
86
+
87
+ File.open(@path_to_request, 'a+') do |file|
88
+ file.write("")
89
+ end
90
+ File.open(@path_to_batches, 'a+') do |file|
91
+ file.write("")
92
+ end
93
+ end
94
+
95
+ # Adds a batch to the LitleRequest. If the batch is open when passed, it will be closed prior to being added.
96
+ # Params:
97
+ # +arg+:: a +LitleBatchRequest+ containing the transactions you wish to send or a +String+ specifying the
98
+ # path to the batch file
99
+ def commit_batch(arg)
100
+ path_to_batch = ""
101
+ #they passed a batch
102
+ if arg.kind_of?(LitleBatchRequest) then
103
+ path_to_batch = arg.get_batch_name
104
+ if((au = arg.get_au_batch) != nil) then
105
+ # also commit the account updater batch
106
+ commit_batch(au)
107
+ end
108
+ elsif arg.kind_of?(LitleAUBatch) then
109
+ path_to_batch = arg.get_batch_name
110
+ elsif arg.kind_of?(String) then
111
+ path_to_batch = arg
112
+ else
113
+ raise RuntimeError, "You entered neither a path nor a batch. Game over :("
114
+ end
115
+ #the batch isn't closed. let's help a brother out
116
+ if (ind = path_to_batch.index(/\.closed/)) == nil then
117
+ if arg.kind_of?(String) then
118
+ new_batch = LitleBatchRequest.new
119
+ new_batch.open_existing_batch(path_to_batch)
120
+ new_batch.close_batch()
121
+ path_to_batch = new_batch.get_batch_name
122
+ # if we passed a path to an AU batch, then new_batch will be a new, empty batch and the batch we passed
123
+ # will be in the AU batch variable. thus, we wanna grab that file name and remove the empty batch.
124
+ if(new_batch.get_au_batch != nil) then
125
+ File.remove(path_to_batch)
126
+ path_to_batch = new_batch.get_au_batch.get_batch_name
127
+ end
128
+ elsif arg.kind_of?(LitleBatchRequest) then
129
+ arg.close_batch()
130
+ path_to_batch = arg.get_batch_name
131
+ elsif arg.kind_of?(LitleAUBatch) then
132
+ arg.close_batch()
133
+ path_to_batch = arg.get_batch_name
134
+ end
135
+ ind = path_to_batch.index(/\.closed/)
136
+ end
137
+ transactions_in_batch = path_to_batch[ind+8..path_to_batch.length].to_i
138
+
139
+ # if the litle request would be too big, let's make another!
140
+ if (@num_total_transactions + transactions_in_batch) > @MAX_NUM_TRANSACTIONS then
141
+ finish_request
142
+ initialize(@options)
143
+ create_new_litle_request
144
+ else #otherwise, let's add it line by line to the request doc
145
+ @num_batch_requests += 1
146
+ #how long we wnat to wait around for the FTP server to get us a response
147
+ @RESPONSE_TIME_OUT += 90 + (transactions_in_batch * 0.25)
148
+ #don't start looking until there could possibly be a response
149
+ @POLL_DELAY += 30 +(transactions_in_batch * 0.02)
150
+ @num_total_transactions += transactions_in_batch
151
+
152
+ File.open(@path_to_batches, 'a+') do |fo|
153
+ File.foreach(path_to_batch) do |li|
154
+ fo.puts li
155
+ end
156
+ end
157
+
158
+ File.delete(path_to_batch)
159
+ end
160
+ end
161
+
162
+ # Adds an RFRRequest to the LitleRequest.
163
+ # params:
164
+ # +options+:: a required +Hash+ containing configuration info for the RFRRequest. If the RFRRequest is for a batch, then the
165
+ # litleSessionId is required as a key/val pair. If the RFRRequest is for account updater, then merchantId and postDay are required
166
+ # as key/val pairs.
167
+ # +path+:: optional path to save the new litle request containing the RFRRequest at
168
+ def add_rfr_request(options, path = (File.dirname(@path_to_batches)))
169
+
170
+ rfrrequest = LitleRFRRequest.new
171
+ if(options['litleSessionId'] != nil) then
172
+ rfrrequest.litleSessionId = options['litleSessionId']
173
+ elsif(options['merchantId'] != nil and options['postDay'] != nil) then
174
+ accountUpdate = AccountUpdateFileRequestData.new
175
+ accountUpdate.merchantId = options['merchantId']
176
+ accountUpdate.postDay = options['postDay']
177
+ rfrrequest.accountUpdateFileRequestData = accountUpdate
178
+ else
179
+ raise ArgumentError, "For an RFR Request, you must specify either a litleSessionId for an RFRRequest for batch or a merchantId
180
+ and a postDay for an RFRRequest for account updater."
181
+ end
182
+
183
+ litleRequest = LitleRequestForRFR.new
184
+ litleRequest.rfrRequest = rfrrequest
185
+
186
+ authentication = Authentication.new
187
+ authentication.user = get_config(:user, options)
188
+ authentication.password = get_config(:password, options)
189
+
190
+ litleRequest.authentication = authentication
191
+ litleRequest.numBatchRequests = "0"
192
+
193
+ litleRequest.version = '8.18'
194
+ litleRequest.xmlns = "http://www.litle.com/schema"
195
+
196
+
197
+ xml = litleRequest.save_to_xml.to_s
198
+
199
+ ts = Time::now.to_i.to_s
200
+ begin
201
+ ts += Time::now.nsec.to_s
202
+ rescue NoMethodError # ruby 1.8.7 fix
203
+ ts += Time::now.usec.to_s
204
+ end
205
+ if(File.file?(path)) then
206
+ raise RuntimeError, "Entered a file not a path."
207
+ end
208
+
209
+ if(path[-1,1] != '/' and path[-1,1] != '\\') then
210
+ path = path + File::SEPARATOR
211
+ end
212
+
213
+ if !File.directory?(path) then
214
+ Dir.mkdir(path)
215
+ end
216
+
217
+ path_to_request = path + 'request_' + ts
218
+
219
+ File.open(path_to_request, 'a+') do |file|
220
+ file.write xml
221
+ end
222
+ File.rename(path_to_request, path_to_request + '.complete')
223
+ @RESPONSE_TIME_OUT += 90
224
+ end
225
+
226
+ # FTPs all previously unsent LitleRequests located in the folder denoted by path to the server
227
+ # Params:
228
+ # +path+:: A +String+ containing the path to the folder on disc where LitleRequests are located.
229
+ # This should be the same location where the LitleRequests were written to. If no path is explicitly
230
+ # provided, then we use the directory where the current working batches file is stored.
231
+ # +options+:: An (option) +Hash+ containing the username, password, and URL to attempt to sFTP to.
232
+ # If not provided, the values will be populated from the configuration file.
233
+ def send_to_litle(path = (File.dirname(@path_to_batches)), options = {})
234
+ username = get_config(:sftp_username, options)
235
+ password = get_config(:sftp_password, options)
236
+ url = get_config(:sftp_url, options)
237
+ if(username == nil or password == nil or url == nil) then
238
+ raise ArgumentError, "You are not configured to use sFTP for batch processing. Please run /bin/Setup.rb again!"
239
+ end
240
+
241
+ if(path[-1,1] != '/' && path[-1,1] != '\\') then
242
+ path = path + File::SEPARATOR
243
+ end
244
+
245
+ begin
246
+ Net::SFTP.start(url, username, :password => password) do |sftp|
247
+ # our folder is /SHORTNAME/SHORTNAME/INBOUND
248
+ Dir.foreach(path) do |filename|
249
+ #we have a complete report according to filename regex
250
+ if((filename =~ /request_\d+.complete\z/) != nil) then
251
+ # adding .prg extension per the XML
252
+ File.rename(path + filename, path + filename + '.prg')
253
+ end
254
+ end
255
+
256
+ @responses_expected = 0
257
+ Dir.foreach(path) do |filename|
258
+ if((filename =~ /request_\d+.complete.prg\z/) != nil) then
259
+ # upload the file
260
+ sftp.upload!(path + filename, '/inbound/' + filename)
261
+ @responses_expected += 1
262
+ # rename now that we're done
263
+ sftp.rename!('/inbound/'+ filename, '/inbound/' + filename.gsub('prg', 'asc'))
264
+ File.rename(path + filename, path + filename.gsub('prg','sent'))
265
+ end
266
+ end
267
+ end
268
+ rescue Net::SSH::AuthenticationFailed
269
+ raise ArgumentError, "The sFTP credentials provided were incorrect. Try again!"
270
+ end
271
+ end
272
+
273
+ # Sends all previously unsent LitleRequests in the specified directory to the Litle server
274
+ # by use of fast batch. All results will be written to disk as we get them. Note that use
275
+ # of fastbatch is strongly discouraged!
276
+ def send_to_litle_stream(options = {}, path = (File.dirname(@path_to_batches)))
277
+ url = get_config(:fast_url, options)
278
+ port = get_config(:fast_port, options)
279
+
280
+ if(url == nil or url == "") then
281
+ raise ArgumentError, "A URL for fastbatch was not specified in the config file or passed options. Reconfigure and try again."
282
+ end
283
+
284
+ if(port == "" or port == nil) then
285
+ raise ArgumentError, "A port number for fastbatch was not specified in the config file or passed options. Reconfigure and try again."
286
+ end
287
+
288
+ if(path[-1,1] != '/' && path[-1,1] != '\\') then
289
+ path = path + File::SEPARATOR
290
+ end
291
+
292
+ if (!File.directory?(path + 'responses/')) then
293
+ Dir.mkdir(path + 'responses/')
294
+ end
295
+
296
+ Dir.foreach(path) do |filename|
297
+ if((filename =~ /request_\d+.complete\z/) != nil) then
298
+ begin
299
+ socket = Socket.new( AF_INET, SOCK_STREAM, 0 )
300
+ sockaddr = Socket.pack_sockaddr_in( port.to_i, url )
301
+ socket.connect( sockaddr )
302
+ rescue => e
303
+ raise "A connection couldn't be established. Are you sure you have the correct credentials? Exception: " + e.message
304
+ end
305
+
306
+ File.foreach(path + filename) do |li|
307
+ socket.write(li)
308
+ end
309
+ File.rename(path + filename, path + filename + '.sent')
310
+ File.open(path + 'responses/' + (filename + '.asc.received').gsub("request", "response"), 'a+') do |fo|
311
+ fo.puts(socket.read)
312
+ end
313
+
314
+ end
315
+ end
316
+ end
317
+
318
+
319
+ # Grabs response files over SFTP from Litle.
320
+ # Params:
321
+ # +args+:: An (optional) +Hash+ containing values for the number of responses expected, the
322
+ # path to the folder on disk to write the responses from the Litle server to, the username and
323
+ # password with which to connect ot the sFTP server, and the URL to connect over sFTP. Values not
324
+ # provided in the hash will be populate automatically based on our best guess
325
+ def get_responses_from_server(args = {})
326
+ @responses_expected = args[:responses_expected] ||= @responses_expected
327
+ response_path = args[:response_path] ||= (File.dirname(@path_to_batches) + '/responses/')
328
+ username = get_config(:sftp_username, args)
329
+ password = get_config(:sftp_password, args)
330
+ url = get_config(:sftp_url, args)
331
+
332
+ if(username == nil or password == nil or url == nil) then
333
+ raise ConfigurationException, "You are not configured to use sFTP for batch processing. Please run /bin/Setup.rb again!"
334
+ end
335
+
336
+ if(response_path[-1,1] != '/' && response_path[-1,1] != '\\') then
337
+ response_path = response_path + File::SEPARATOR
338
+ end
339
+
340
+ if(!File.directory?(response_path)) then
341
+ Dir.mkdir(response_path)
342
+ end
343
+ begin
344
+ responses_grabbed = 0
345
+ Net::SFTP.start(url, username, :password => password) do |sftp|
346
+ # clear out the sFTP outbound dir prior to checking for new files, avoids leaving files on the server
347
+ # if files are left behind we are not counting then towards the expected total
348
+ sftp.dir.foreach('/outbound/') do |entry|
349
+ if((entry.name =~ /request_\d+.complete.asc\z/) != nil) then
350
+ sftp.download!('/outbound/' + entry.name, response_path + entry.name.gsub('request', 'response') + '.received')
351
+ 3.times{
352
+ begin
353
+ sftp.remove!('/outbound/' + entry.name)
354
+ break
355
+ rescue Net::SFTP::StatusException
356
+ #try, try, try again
357
+ puts "We couldn't remove it! Try again"
358
+ end
359
+ }
360
+ end
361
+ end
362
+ end
363
+ #wait until a response has a possibility of being there
364
+ sleep(@POLL_DELAY)
365
+ time_begin = Time.now
366
+ Net::SFTP.start(url, username, :password => password) do |sftp|
367
+ while((Time.now - time_begin) < @RESPONSE_TIME_OUT && responses_grabbed < @responses_expected)
368
+ #sleep for 60 seconds, ¿no es bueno?
369
+ sleep(60)
370
+ sftp.dir.foreach('/outbound/') do |entry|
371
+ if((entry.name =~ /request_\d+.complete.asc\z/) != nil) then
372
+ sftp.download!('/outbound/' + entry.name, response_path + entry.name.gsub('request', 'response') + '.received')
373
+ responses_grabbed += 1
374
+ 3.times{
375
+ begin
376
+ sftp.remove!('/outbound/' + entry.name)
377
+ break
378
+ rescue Net::SFTP::StatusException
379
+ #try, try, try again
380
+ puts "We couldn't remove it! Try again"
381
+ end
382
+ }
383
+ end
384
+ end
385
+ end
386
+ #if our timeout timed out, we're having problems
387
+ if responses_grabbed < @responses_expected then
388
+ raise RuntimeError, "We timed out in waiting for a response from the server. :("
389
+ end
390
+ end
391
+ rescue Net::SSH::AuthenticationFailed
392
+ raise ArgumentError, "The sFTP credentials provided were incorrect. Try again!"
393
+ end
394
+ end
395
+
396
+ # Params:
397
+ # +args+:: A +Hash+ containing arguments for the processing process. This hash MUST contain an entry
398
+ # for a transaction listener (see +DefaultLitleListener+). It may also include a batch listener and a
399
+ # custom path where response files from the server are located (if it is not provided, we'll guess the position)
400
+ def process_responses(args)
401
+ #the transaction listener is required
402
+ if(!args.has_key?(:transaction_listener)) then
403
+ raise ArgumentError, "The arguments hash must contain an entry for transaction listener!"
404
+ end
405
+
406
+ transaction_listener = args[:transaction_listener]
407
+ batch_listener = args[:batch_listener] ||= nil
408
+ path_to_responses = args[:path_to_responses] ||= (File.dirname(@path_to_batches) + '/responses/')
409
+
410
+ Dir.foreach(path_to_responses) do |filename|
411
+ if ((filename =~ /response_\d+.complete.asc.received\z/) != nil) then
412
+ process_response(path_to_responses + filename, transaction_listener, batch_listener)
413
+ File.rename(path_to_responses + filename, path_to_responses + filename + '.processed')
414
+ end
415
+ end
416
+ end
417
+
418
+ # Params:
419
+ # +path_to_response+:: The path to a specific .asc file to process
420
+ # +transaction_listener+:: A listener to be applied to the hash of each transaction
421
+ # (see +DefaultLitleListener+)
422
+ # +batch_listener+:: An (optional) listener to be applied to the hash of each batch.
423
+ # Note that this will om-nom-nom quite a bit of memory
424
+ def process_response(path_to_response, transaction_listener, batch_listener = nil)
425
+ reader = LibXML::XML::Reader.file(path_to_response)
426
+ reader.read # read into the root node
427
+ #if the response attribute is nil, we're dealing with an RFR and everything is a-okay
428
+ if reader.get_attribute('response') != "0" and reader.get_attribute('response') != nil then
429
+ raise RuntimeError, "Error parsing Litle Request: " + reader.get_attribute("message")
430
+ end
431
+
432
+ reader.read
433
+ count = 0
434
+ while true and count < 500001 do
435
+
436
+ count += 1
437
+ if(reader.node == nil) then
438
+ return false
439
+ end
440
+
441
+ case reader.node.name.to_s
442
+ when "batchResponse"
443
+ reader.read
444
+ when "litleResponse"
445
+ return false
446
+ when "text"
447
+ reader.read
448
+ else
449
+ xml = reader.read_outer_xml
450
+ duck = Crack::XML.parse(xml)
451
+ duck[duck.keys[0]]["type"] = duck.keys[0]
452
+ duck = duck[duck.keys[0]]
453
+ transaction_listener.apply(duck)
454
+ reader.next
455
+ end
456
+ end
457
+ end
458
+
459
+ def get_path_to_batches
460
+ return @path_to_batches
461
+ end
462
+
463
+ # Called when you wish to finish adding batches to your request, this method rewrites the aggregate
464
+ # batch file to the final LitleRequest xml doc with the appropos LitleRequest tags.
465
+ def finish_request
466
+ File.open(@path_to_request, 'w') do |f|
467
+ #jam dat header in there
468
+ f.puts(build_request_header())
469
+ #read into the request file from the batches file
470
+ File.foreach(@path_to_batches) do |li|
471
+ f.puts li
472
+ end
473
+ #finally, let's poot in a header, for old time's sake
474
+ f.puts '</litleRequest>'
475
+ end
476
+
477
+ #rename the requests file
478
+ File.rename(@path_to_request, @path_to_request + '.complete')
479
+ #we don't need the master batch file anymore
480
+ File.delete(@path_to_batches)
481
+ end
482
+
483
+ private
484
+
485
+ def build_request_header(options = @options)
486
+ litle_request = self
487
+
488
+ authentication = Authentication.new
489
+ authentication.user = get_config(:user, options)
490
+ authentication.password = get_config(:password, options)
491
+
492
+ litle_request.authentication = authentication
493
+ litle_request.version = '8.18'
494
+ litle_request.xmlns = "http://www.litle.com/schema"
495
+ # litle_request.id = options['sessionId'] #grab from options; okay if nil
496
+ litle_request.numBatchRequests = @num_batch_requests
497
+
498
+ xml = litle_request.save_to_xml.to_s
499
+ xml[/<\/litleRequest>/]=''
500
+ return xml
501
+ end
502
+
503
+ def get_config(field, options)
504
+ if options[field.to_s] == nil and options[field] == nil then
505
+ return @config_hash[field.to_s]
506
+ elsif options[field.to_s] != nil then
507
+ return options[field.to_s]
508
+ else
509
+ return options[field]
510
+ end
511
+ end
512
+ end
513
+ end