Class: LitleOnline::LitleRequest

Inherits:
Object
  • Object
show all
Includes:
XML::Mapping
Defined in:
lib/XMLFields.rb,
lib/LitleRequest.rb

Instance Method Summary collapse

Constructor Details

#initialize(options = {}) ⇒ LitleRequest

Returns a new instance of LitleRequest.



37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# File 'lib/LitleRequest.rb', line 37

def initialize(options = {})
  # load configuration data
  @config_hash = Configuration.new.config
  @num_batch_requests = 0
  @path_to_request = ''
  @path_to_batches = ''
  @num_total_transactions = 0
  @MAX_NUM_TRANSACTIONS = 500_000
  @options = options
  # current time out set to 2 mins
  # this value is in seconds
  @RESPONSE_TIME_OUT = 360
  @POLL_DELAY = 0
  @responses_expected = 0
end

Instance Method Details

#add_rfr_request(options, path = File.dirname(@path_to_batches)) ⇒ Object

Adds an RFRRequest to the LitleRequest. params:

options

a required Hash containing configuration info for the RFRRequest. If the RFRRequest is for a batch, then the

litleSessionId is required as a key/val pair. If the RFRRequest is for account updater, then merchantId and postDay are required as key/val pairs.

path

optional path to save the new litle request containing the RFRRequest at



161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
# File 'lib/LitleRequest.rb', line 161

def add_rfr_request(options, path = File.dirname(@path_to_batches))
  rfrrequest = LitleRFRRequest.new
  if options['litleSessionId']
    rfrrequest.litleSessionId = options['litleSessionId']
  elsif options['merchantId'] && options['postDay']
    accountUpdate = AccountUpdateFileRequestData.new
    accountUpdate.merchantId = options['merchantId']
    accountUpdate.postDay = options['postDay']
    rfrrequest.accountUpdateFileRequestData = accountUpdate
  else
    raise ArgumentError, "For an RFR Request, you must specify either a litleSessionId for an RFRRequest for batch or a merchantId
    and a postDay for an RFRRequest for account updater."
  end

  litleRequest = LitleRequestForRFR.new
  litleRequest.rfrRequest = rfrrequest

  authentication = Authentication.new
  authentication.user = get_config(:user, options)
  authentication.password = get_config(:password, options)

  litleRequest.authentication = authentication
  litleRequest.numBatchRequests = '0'

  litleRequest.version         = '8.31'
  litleRequest.xmlns           = 'http://www.litle.com/schema'

  xml = litleRequest.save_to_xml.to_s

  ts = Time.now.to_i.to_s
  begin
    ts += Time.now.nsec.to_s
  rescue NoMethodError # ruby 1.8.7 fix
    ts += Time.now.usec.to_s
  end
  raise 'Entered a file not a path.' if File.file?(path)

  path += File::SEPARATOR if path[-1, 1] != '/' && path[-1, 1] != '\\'

  Dir.mkdir(path) unless File.directory?(path)

  path_to_request = path + 'request_' + ts

  File.open(path_to_request, 'a+') do |file|
    file.write xml
  end
  File.rename(path_to_request, path_to_request + '.complete')
  @RESPONSE_TIME_OUT += 90
end

#commit_batch(arg) ⇒ Object

Adds a batch to the LitleRequest. If the batch is open when passed, it will be closed prior to being added. Params:

arg

a LitleBatchRequest containing the transactions you wish to send or a String specifying the

path to the batch file



91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
# File 'lib/LitleRequest.rb', line 91

def commit_batch(arg)
  path_to_batch = ''
  # they passed a batch
  if arg.is_a?(LitleBatchRequest)
    path_to_batch = arg.get_batch_name
    if (au = arg.get_au_batch) != nil
      # also commit the account updater batch
      commit_batch(au)
    end
  elsif arg.is_a?(LitleAUBatch)
    path_to_batch = arg.get_batch_name
  elsif arg.is_a?(String)
    path_to_batch = arg
  else
    raise 'You entered neither a path nor a batch. Game over :('
  end
  # the batch isn't closed. let's help a brother out
  if (ind = path_to_batch.index(/\.closed/)).nil?
    if arg.is_a?(String)
      new_batch = LitleBatchRequest.new
      new_batch.open_existing_batch(path_to_batch)
      new_batch.close_batch
      path_to_batch = new_batch.get_batch_name
      # if we passed a path to an AU batch, then new_batch will be a new, empty batch and the batch we passed
      # will be in the AU batch variable. thus, we wanna grab that file name and remove the empty batch.
      unless new_batch.get_au_batch.nil?
        File.remove(path_to_batch)
        path_to_batch = new_batch.get_au_batch.get_batch_name
      end
    elsif arg.is_a?(LitleBatchRequest)
      arg.close_batch
      path_to_batch = arg.get_batch_name
    elsif arg.is_a?(LitleAUBatch)
      arg.close_batch
      path_to_batch = arg.get_batch_name
    end
    ind = path_to_batch.index(/\.closed/)
  end
  transactions_in_batch = path_to_batch[ind + 8..path_to_batch.length].to_i

  # if the litle request would be too big, let's make another!
  if (@num_total_transactions + transactions_in_batch) > @MAX_NUM_TRANSACTIONS
    finish_request
    initialize(@options)
    create_new_litle_request
  else # otherwise, let's add it line by line to the request doc
    # @num_batch_requests += 1
    # how long we wnat to wait around for the FTP server to get us a response
    @RESPONSE_TIME_OUT += 90 + (transactions_in_batch * 0.25)
    # don't start looking until there could possibly be a response
    @POLL_DELAY += 30 + (transactions_in_batch * 0.02)
    @num_total_transactions += transactions_in_batch
    # Don't add empty batches
    @num_batch_requests += 1 unless transactions_in_batch.eql?(0)
    File.open(@path_to_batches, 'a+') do |fo|
      File.foreach(path_to_batch) do |li|
        fo.puts li
      end
    end

    File.delete(path_to_batch)
  end
end

#create_new_litle_request(path) ⇒ Object

Creates the necessary files for the LitleRequest at the path specified. path/request_(TIMESTAMP) will be the final XML markup and path/request_(TIMESTAMP) will hold intermediary XML markup Params:

path

A String containing the path to the folder on disc to write the files to



57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/LitleRequest.rb', line 57

def create_new_litle_request(path)
  ts = Time.now.to_i.to_s
  begin
    ts += Time.now.nsec.to_s
  rescue NoMethodError # ruby 1.8.7 fix
    ts += Time.now.usec.to_s
  end

  raise 'Entered a file not a path.' if File.file?(path)

  path += File::SEPARATOR if path[-1, 1] != '/' && path[-1, 1] != '\\'

  Dir.mkdir(path) unless File.directory?(path)

  @path_to_request = path + 'request_' + ts
  @path_to_batches = @path_to_request + '_batches'

  if File.file?(@path_to_request) || File.file?(@path_to_batches)
    create_new_litle_request(path)
    return
  end

  File.open(@path_to_request, 'a+') do |file|
    file.write('')
  end
  File.open(@path_to_batches, 'a+') do |file|
    file.write('')
  end
end

#finish_requestObject

Called when you wish to finish adding batches to your request, this method rewrites the aggregate batch file to the final LitleRequest xml doc with the appropos LitleRequest tags.



446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
# File 'lib/LitleRequest.rb', line 446

def finish_request
  File.open(@path_to_request, 'w') do |f|
    # jam dat header in there
    f.puts(build_request_header)
    # read into the request file from the batches file
    File.foreach(@path_to_batches) do |li|
      f.puts li
    end
    # finally, let's poot in a header, for old time's sake
    f.puts '</litleRequest>'
  end

  # rename the requests file
  File.rename(@path_to_request, @path_to_request + '.complete')
  # we don't need the master batch file anymore
  File.delete(@path_to_batches)
end

#get_path_to_batchesObject



440
441
442
# File 'lib/LitleRequest.rb', line 440

def get_path_to_batches
  @path_to_batches
end

#get_responses_from_server(args = {}) ⇒ Object

Grabs response files over SFTP from Litle. Params:

args

An (optional) Hash containing values for the number of responses expected, the

path to the folder on disk to write the responses from the Litle server to, the username and password with which to connect ot the sFTP server, and the URL to connect over sFTP. Values not provided in the hash will be populate automatically based on our best guess



310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
# File 'lib/LitleRequest.rb', line 310

def get_responses_from_server(args = {})
  @responses_expected = args[:responses_expected] ||= @responses_expected
  response_path = args[:response_path] ||= (File.dirname(@path_to_batches) + '/responses/')
  username = get_config(:sftp_username, args)
  password = get_config(:sftp_password, args)
  url = get_config(:sftp_url, args)

  if username.nil? || password.nil? || url.nil?
    raise ConfigurationException, 'You are not configured to use sFTP for batch processing. Please run /bin/Setup.rb again!'
  end

  if response_path[-1, 1] != '/' && response_path[-1, 1] != '\\'
    response_path += File::SEPARATOR
  end

  Dir.mkdir(response_path) unless File.directory?(response_path)
  begin
    responses_grabbed = 0
    Net::SFTP.start(url, username, password: password) do |sftp|
      # clear out the sFTP outbound dir prior to checking for new files, avoids leaving files on the server
      # if files are left behind we are not counting then towards the expected total
      sftp.dir.foreach('/outbound/') do |entry|
        if (entry.name =~ /request_\d+.complete.asc\z/) != nil
          sftp.download!('/outbound/' + entry.name, response_path + entry.name.gsub('request', 'response') + '.received')
          3.times do
            begin
              sftp.remove!('/outbound/' + entry.name)
              break
            rescue Net::SFTP::StatusException
              # try, try, try again
              puts "We couldn't remove it! Try again"
            end
          end
        end
      end
    end
    # wait until a response has a possibility of being there
    sleep(@POLL_DELAY)
    time_begin = Time.now
    Net::SFTP.start(url, username, password: password) do |sftp|
      while (Time.now - time_begin) < @RESPONSE_TIME_OUT && responses_grabbed < @responses_expected
        # sleep for 60 seconds, ¿no es bueno?
        sleep(60)
        sftp.dir.foreach('/outbound/') do |entry|
          if (entry.name =~ /request_\d+.complete.asc\z/) != nil
            sftp.download!('/outbound/' + entry.name, response_path + entry.name.gsub('request', 'response') + '.received')
            responses_grabbed += 1
            3.times do
              begin
                sftp.remove!('/outbound/' + entry.name)
                break
              rescue Net::SFTP::StatusException
                # try, try, try again
                puts "We couldn't remove it! Try again"
              end
            end
          end
        end
      end
      # if our timeout timed out, we're having problems
      if responses_grabbed < @responses_expected
        raise 'We timed out in waiting for a response from the server. :('
      end
    end
  rescue Net::SSH::AuthenticationFailed
    raise ArgumentError, 'The sFTP credentials provided were incorrect. Try again!'
  end
end

#process_response(path_to_response, transaction_listener, _batch_listener = nil) ⇒ Object

Params:

path_to_response

The path to a specific .asc file to process

transaction_listener

A listener to be applied to the hash of each transaction

(see DefaultLitleListener)

batch_listener

An (optional) listener to be applied to the hash of each batch.

Note that this will om-nom-nom quite a bit of memory



407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
# File 'lib/LitleRequest.rb', line 407

def process_response(path_to_response, transaction_listener, _batch_listener = nil)
  reader = LibXML::XML::Reader.file(path_to_response)
  reader.read # read into the root node
  # if the response attribute is nil, we're dealing with an RFR and everything is a-okay
  if reader.get_attribute('response') != '0' && !reader.get_attribute('response').nil?
    raise 'Error parsing Litle Request: ' + reader.get_attribute('message')
  end

  reader.read
  count = 0
  while true && count < 500_001

    count += 1
    return false if reader.node.nil?

    case reader.node.name.to_s
    when 'batchResponse'
      reader.read
    when 'litleResponse'
      return false
    when 'text'
      reader.read
    else
      xml = reader.read_outer_xml
      duck = Crack::XML.parse(xml)
      duck[duck.keys[0]]['type'] = duck.keys[0]
      duck = duck[duck.keys[0]]
      transaction_listener.apply(duck)
      reader.next
    end
  end
end

#process_responses(args) ⇒ Object

Params:

args

A Hash containing arguments for the processing process. This hash MUST contain an entry

for a transaction listener (see DefaultLitleListener). It may also include a batch listener and a custom path where response files from the server are located (if it is not provided, we’ll guess the position)



383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
# File 'lib/LitleRequest.rb', line 383

def process_responses(args)
  # the transaction listener is required
  unless args.key?(:transaction_listener)
    raise ArgumentError, 'The arguments hash must contain an entry for transaction listener!'
  end

  transaction_listener = args[:transaction_listener]
  batch_listener = args[:batch_listener] ||= nil
  path_to_responses = args[:path_to_responses] ||= (File.dirname(@path_to_batches) + '/responses/')

  Dir.foreach(path_to_responses) do |filename|
    if (filename =~ /response_\d+.complete.asc.received\z/) != nil
      process_response(path_to_responses + filename, transaction_listener, batch_listener)
      File.rename(path_to_responses + filename, path_to_responses + filename + '.processed')
    end
  end
end

#send_to_litle(path = File.dirname(@path_to_batches), options = {}) ⇒ Object

FTPs all previously unsent LitleRequests located in the folder denoted by path to the server Params:

path

A String containing the path to the folder on disc where LitleRequests are located.

This should be the same location where the LitleRequests were written to. If no path is explicitly provided, then we use the directory where the current working batches file is stored.

options

An (option) Hash containing the username, password, and URL to attempt to sFTP to.

If not provided, the values will be populated from the configuration file.



218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
# File 'lib/LitleRequest.rb', line 218

def send_to_litle(path = File.dirname(@path_to_batches), options = {})
  username = get_config(:sftp_username, options)
  password = get_config(:sftp_password, options)

  url = get_config(:sftp_url, options)

  if username.nil? || password.nil? || url.nil?
    raise ArgumentError, 'You are not configured to use sFTP for batch processing. Please run /bin/Setup.rb again!'
  end

  path += File::SEPARATOR if path[-1, 1] != '/' && path[-1, 1] != '\\'

  begin
    Net::SFTP.start(url, username, password: password) do |sftp|
      # our folder is /SHORTNAME/SHORTNAME/INBOUND
      Dir.foreach(path) do |filename|
        # we have a complete report according to filename regex
        if (filename =~ /request_\d+.complete\z/) != nil
          # adding .prg extension per the XML
          File.rename(path + filename, path + filename + '.prg')
        end
      end

      @responses_expected = 0
      Dir.foreach(path) do |filename|
        if (filename =~ /request_\d+.complete.prg\z/) != nil
          # upload the file
          sftp.upload!(path + filename, '/inbound/' + filename)
          @responses_expected += 1
          # rename now that we're done
          sftp.rename!('/inbound/' + filename, '/inbound/' + filename.gsub('prg', 'asc'))
          File.rename(path + filename, path + filename.gsub('prg', 'sent'))
        end
      end
    end
  rescue Net::SSH::AuthenticationFailed
    raise ArgumentError, 'The sFTP credentials provided were incorrect. Try again!'
  end
end

#send_to_litle_stream(options = {}, path = File.dirname(@path_to_batches)) ⇒ Object

Sends all previously unsent LitleRequests in the specified directory to the Litle server by use of fast batch. All results will be written to disk as we get them. Note that use of fastbatch is strongly discouraged!



261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
# File 'lib/LitleRequest.rb', line 261

def send_to_litle_stream(options = {}, path = File.dirname(@path_to_batches))
  url = get_config(:fast_url, options)
  port = get_config(:fast_port, options)

  if url.nil? || url == ''
    raise ArgumentError, 'A URL for fastbatch was not specified in the config file or passed options. Reconfigure and try again.'
  end

  if port == '' || port.nil?
    raise ArgumentError, 'A port number for fastbatch was not specified in the config file or passed options. Reconfigure and try again.'
  end

  path += File::SEPARATOR if path[-1, 1] != '/' && path[-1, 1] != '\\'

  Dir.mkdir(path + 'responses/') unless File.directory?(path + 'responses/')

  Dir.foreach(path) do |filename|
    if (filename =~ /request_\d+.complete\z/) != nil
      begin
         socket = TCPSocket.open(url, port.to_i)
         ssl_context = OpenSSL::SSL::SSLContext.new
         ssl_context.ssl_version = :SSLv23
         ssl_socket = OpenSSL::SSL::SSLSocket.new(socket, ssl_context)
         ssl_socket.sync_close = true
         ssl_socket.connect
       rescue => e
         raise "A connection couldn't be established. Are you sure you have the correct credentials? Exception: " + e.message
       end

      File.foreach(path + filename) do |li|
        ssl_socket.puts li
      end
      File.rename(path + filename, path + filename + '.sent')
      File.open(path + 'responses/' + (filename + '.asc.received').gsub('request', 'response'), 'a+') do |fo|
        while line = ssl_socket.gets
          fo.puts(line)
         end
      end

    end
  end
end