LitleOnline 8.18.0 → 8.19.0

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,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