LitleOnline 8.16.0 → 8.17.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. data/CHANGELOG +7 -0
  2. data/DESCRIPTION +1 -1
  3. data/README.md +2 -2
  4. data/Rakefile +6 -3
  5. data/bin/Setup.rb +14 -0
  6. data/bin/sample_batch_driver.rb +122 -0
  7. data/lib/LitleBatchRequest.rb +466 -0
  8. data/lib/LitleListeners.rb +142 -0
  9. data/lib/LitleOnline.rb +4 -0
  10. data/lib/LitleOnlineRequest.rb +25 -144
  11. data/lib/LitleRequest.rb +513 -0
  12. data/lib/LitleTransaction.rb +295 -0
  13. data/lib/XMLFields.rb +114 -4
  14. data/test/certification/certTest_batchAll.rb +337 -0
  15. data/test/certification/ts_all.rb +2 -0
  16. data/test/functional/test_batch.rb +164 -0
  17. data/test/functional/test_credit.rb +0 -18
  18. data/test/functional/test_litle_requests.rb +355 -0
  19. data/test/functional/test_updateCardValidationNumOnToken.rb +1 -1
  20. data/test/functional/test_xmlfields.rb +0 -19
  21. data/test/functional/ts_all.rb +2 -0
  22. data/test/unit/test_LitleAUBatch.rb +217 -0
  23. data/test/unit/test_LitleBatchRequest.rb +599 -0
  24. data/test/unit/test_LitleOnlineRequest.rb +9 -57
  25. data/test/unit/test_LitleRequest.rb +320 -0
  26. data/test/unit/test_LitleTransaction.rb +398 -0
  27. data/test/unit/test_auth.rb +25 -2
  28. data/test/unit/test_authReversal.rb +28 -5
  29. data/test/unit/test_capture.rb +24 -1
  30. data/test/unit/test_captureGivenAuth.rb +23 -1
  31. data/test/unit/test_credit.rb +92 -19
  32. data/test/unit/test_echeckCredit.rb +1 -1
  33. data/test/unit/test_echeckRedeposit.rb +1 -1
  34. data/test/unit/test_echeckSale.rb +1 -1
  35. data/test/unit/test_echeckVerification.rb +1 -1
  36. data/test/unit/test_echeckVoid.rb +1 -1
  37. data/test/unit/test_forceCapture.rb +24 -1
  38. data/test/unit/test_sale.rb +25 -2
  39. data/test/unit/test_token.rb +1 -1
  40. data/test/unit/test_updateCardValidationNumOnToken.rb +1 -1
  41. data/test/unit/test_xmlfields.rb +1 -1
  42. data/test/unit/ts_unit.rb +4 -0
  43. metadata +59 -14
@@ -0,0 +1,142 @@
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
+
26
+
27
+ # This file contains the preloaded listeners for parsing the response XML.
28
+
29
+
30
+ module LitleOnline
31
+
32
+ # This listener will run the provided closure over every response hash
33
+ # This is the base class for all listeners applied to transaction responses
34
+ class DefaultLitleListener
35
+ def initialize(&action)
36
+ @action = action
37
+ end
38
+
39
+ def apply(duck)
40
+ # apply the proc uniformly across all response types
41
+ @action.call(duck)
42
+ end
43
+ end
44
+
45
+ class AuthorizationListener < DefaultLitleListener
46
+ def apply(duck)
47
+ if(duck["type"] == "authorizationResponse") then
48
+ @action.call(duck)
49
+ end
50
+ end
51
+ end
52
+
53
+ class CaptureListener < DefaultLitleListener
54
+ def apply(duck)
55
+ if(duck["type"] == "captureResponse") then
56
+ @action.call(duck)
57
+ end
58
+ end
59
+ end
60
+
61
+ class ForceCaptureListener < DefaultLitleListener
62
+ def apply(duck)
63
+ if(duck["type"] == "forceCaptureResponse") then
64
+ @action.call(duck)
65
+ end
66
+ end
67
+ end
68
+
69
+ class CaptureGivenAuthListener < DefaultLitleListener
70
+ def apply(duck)
71
+ if(duck["type"] == "captureGivenAuthResponse") then
72
+ @action.call(duck)
73
+ end
74
+ end
75
+ end
76
+
77
+ class SaleListener < DefaultLitleListener
78
+ def apply(duck)
79
+ if(duck["type"] == "saleResponse") then
80
+ @action.call(duck)
81
+ end
82
+ end
83
+ end
84
+
85
+
86
+ class CreditListener < DefaultLitleListener
87
+ def apply(duck)
88
+ if(duck["type"] == "creditResponse") then
89
+ @action.call(duck)
90
+ end
91
+ end
92
+ end
93
+
94
+ class EcheckSaleListener < DefaultLitleListener
95
+ def apply(duck)
96
+ if(duck["type"] == "echeckSaleResponse") then
97
+ @action.call(duck)
98
+ end
99
+ end
100
+ end
101
+
102
+ class EcheckCreditListener < DefaultLitleListener
103
+ def apply(duck)
104
+ if(duck["type"] == "echeckCreditResponse") then
105
+ @action.call(duck)
106
+ end
107
+ end
108
+ end
109
+
110
+ class EcheckVerificationListener < DefaultLitleListener
111
+ def apply(duck)
112
+ if(duck["type"] == "echeckVerificationResponse") then
113
+ @action.call(duck)
114
+ end
115
+ end
116
+ end
117
+
118
+ class EcheckRedepositListener < DefaultLitleListener
119
+ def apply(duck)
120
+ if(duck["type"] == "echeckRedepositResponse") then
121
+ @action.call(duck)
122
+ end
123
+ end
124
+ end
125
+
126
+ class AuthReversalListener < DefaultLitleListener
127
+ def apply(duck)
128
+ if(duck["type"] == "authReversalResponse") then
129
+ @action.call(duck)
130
+ end
131
+ end
132
+ end
133
+
134
+ class RegisterTokenListener < DefaultLitleListener
135
+ def apply(duck)
136
+ if(duck["type"] == "registerTokenResponse") then
137
+ @action.call(duck)
138
+ end
139
+ end
140
+ end
141
+
142
+ end
data/lib/LitleOnline.rb CHANGED
@@ -47,7 +47,11 @@ end
47
47
  require_relative 'Communications'
48
48
  require_relative 'LitleXmlMapper'
49
49
  require_relative 'XMLFields'
50
+ require_relative 'LitleTransaction'
51
+ require_relative 'LitleBatchRequest'
50
52
  require_relative 'LitleOnlineRequest'
53
+ require_relative 'LitleRequest'
54
+ require_relative 'LitleListeners'
51
55
  require_relative 'Configuration'
52
56
 
53
57
  #allows attribute values to be in double quotes, required by Litle Server
@@ -25,9 +25,7 @@ OTHER DEALINGS IN THE SOFTWARE.
25
25
  require_relative 'Configuration'
26
26
 
27
27
  #
28
- # This class does all the heavy lifting of mapping the Ruby hash into Litle XML format
29
- # It also handles validation looking for missing or incorrect fields
30
- #contains the methods to properly create each transaction type
28
+ # This class handles sending the Litle online request
31
29
  #
32
30
  module LitleOnline
33
31
 
@@ -35,152 +33,87 @@ module LitleOnline
35
33
  def initialize
36
34
  #load configuration data
37
35
  @config_hash = Configuration.new.config
36
+ @litle_transaction = LitleTransaction.new
38
37
  end
39
38
 
40
39
  def authorization(options)
41
- transaction = Authorization.new
42
- add_transaction_info(transaction, options)
40
+ transaction = @litle_transaction.authorization(options)
43
41
 
44
42
  commit(transaction, :authorization, options)
45
43
  end
46
44
 
47
45
  def sale(options)
48
- transaction = Sale.new
49
- add_transaction_info(transaction, options)
50
-
51
- transaction.fraudCheck = FraudCheck.from_hash(options,'fraudCheck')
52
- transaction.payPalOrderComplete = options['payPalOrderComplete']
53
- transaction.payPalNotes = options['payPalNotes']
46
+ transaction = @litle_transaction.sale(options)
54
47
 
55
48
  commit(transaction, :sale, options)
56
49
  end
57
50
 
58
51
  def auth_reversal(options)
59
- transaction = AuthReversal.new
60
-
61
- transaction.litleTxnId = options['litleTxnId']
62
- transaction.amount = options['amount']
63
- transaction.payPalNotes = options['payPalNotes']
64
- transaction.actionReason = options['actionReason']
52
+ transaction = @litle_transaction.auth_reversal(options)
65
53
 
66
54
  commit(transaction, :authReversal, options)
67
55
  end
68
56
 
69
57
  def credit(options)
70
- transaction = Credit.new
71
- add_order_info(transaction, options)
72
-
73
- transaction.litleTxnId = options['litleTxnId']
74
- transaction.customBilling = CustomBilling.from_hash(options)
75
- transaction.billMeLaterRequest = BillMeLaterRequest.from_hash(options)
76
- transaction.payPalNotes = options['payPalNotes']
77
- transaction.actionReason = options['actionReason']
78
- transaction.paypal = CreditPayPal.from_hash(options,'paypal')
79
-
58
+ transaction = @litle_transaction.credit(options)
59
+
80
60
  commit(transaction, :credit, options)
81
61
  end
82
62
 
83
63
  def register_token_request(options)
84
- transaction = RegisterTokenRequest.new
85
-
86
- transaction.orderId = options['orderId']
87
- transaction.accountNumber = options['accountNumber']
88
- transaction.echeckForToken = EcheckForToken.from_hash(options)
89
- transaction.paypageRegistrationId = options['paypageRegistrationId']
64
+ transaction = @litle_transaction.register_token_request(options)
90
65
 
91
66
  commit(transaction, :registerTokenRequest, options)
92
67
  end
93
68
 
94
69
  def update_card_validation_num_on_token(options)
95
- transaction = UpdateCardValidationNumOnToken.new
70
+ transaction = @litle_transaction.update_card_validation_num_on_token(options)
96
71
 
97
- transaction.orderId = options['orderId']
98
- transaction.litleToken = options['litleToken']
99
- transaction.cardValidationNum = options['cardValidationNum']
100
-
101
- SchemaValidation.validate_length(transaction.litleToken, true, 13, 25, "updateCardValidationNumOnToken", "litleToken")
102
- SchemaValidation.validate_length(transaction.cardValidationNum, true, 1, 4, "updateCardValidationNumOnToken", "cardValidationNum")
103
-
104
72
  commit(transaction, :updateCardValidationNumOnToken, options)
105
73
  end
106
74
 
107
75
  def force_capture(options)
108
- transaction = ForceCapture.new
109
- transaction.customBilling = CustomBilling.from_hash(options)
110
-
111
- add_order_info(transaction, options)
112
-
76
+ transaction = @litle_transaction.force_capture(options)
77
+
113
78
  commit(transaction, :forceCapture, options)
114
79
  end
115
80
 
116
81
  def capture(options)
117
- transaction = Capture.new
118
-
119
- transaction.partial = options['partial']
120
- transaction.litleTxnId = options['litleTxnId']
121
- transaction.amount = options['amount']
122
- transaction.enhancedData = EnhancedData.from_hash(options)
123
- transaction.processingInstructions = ProcessingInstructions.from_hash(options)
124
- transaction.payPalOrderComplete = options['payPalOrderComplete']
125
- transaction.payPalNotes = options['payPalNotes']
126
-
82
+ transaction = @litle_transaction.capture(options)
83
+
127
84
  commit(transaction, :captureTxn, options)
128
85
  end
129
86
 
130
87
  def capture_given_auth(options)
131
- transaction = CaptureGivenAuth.new
132
- add_order_info(transaction, options)
133
-
134
- transaction.authInformation = AuthInformation.from_hash(options)
135
- transaction.shipToAddress = Contact.from_hash(options,'shipToAddress')
136
- transaction.customBilling = CustomBilling.from_hash(options)
137
- transaction.billMeLaterRequest = BillMeLaterRequest.from_hash(options)
138
-
88
+ transaction = @litle_transaction.capture_given_auth(options)
89
+
139
90
  commit(transaction, :captureGivenAuth, options)
140
91
  end
141
92
 
142
93
  def void(options)
143
- transaction = Void.new
144
-
145
- transaction.litleTxnId = options['litleTxnId']
146
- transaction.processingInstructions = ProcessingInstructions.from_hash(options)
94
+ transaction = @litle_transaction.void(options)
147
95
 
148
96
  commit(transaction, :void, options)
149
97
  end
150
98
 
151
99
  def echeck_redeposit(options)
152
- transaction = EcheckRedeposit.new
153
- add_echeck(transaction, options)
154
-
155
- transaction.litleTxnId = options['litleTxnId']
156
- transaction.merchantData = MerchantData.from_hash(options)
157
-
100
+ transaction = @litle_transaction.echeck_redeposit(options)
101
+
158
102
  commit(transaction, :echeckRedeposit, options)
159
103
  end
160
104
 
161
105
  def echeck_sale(options)
162
- transaction = EcheckSale.new
163
- add_echeck(transaction, options)
164
- add_echeck_order_info(transaction, options)
165
-
166
- transaction.verify = options['verify']
167
- transaction.shipToAddress = Contact.from_hash(options,'shipToAddress')
168
- transaction.customBilling = CustomBilling.from_hash(options)
106
+ transaction = @litle_transaction.echeck_sale(options)
169
107
 
170
108
  commit(transaction, :echeckSale, options)
171
109
  end
172
110
 
173
111
  def echeck_credit(options)
174
- transaction = EcheckCredit.new
175
- transaction.customBilling = CustomBilling.from_hash(options)
176
-
177
- add_echeck_order_info(transaction, options)
178
- add_echeck(transaction, options)
112
+ transaction = @litle_transaction.echeck_credit(options)
179
113
 
180
114
  begin
181
115
  commit(transaction, :echeckCredit, options)
182
116
  rescue XML::MappingError => e
183
- puts e
184
117
  response = LitleOnlineResponse.new
185
118
  response.message = "The content of element 'echeckCredit' is not complete"
186
119
  return response
@@ -188,20 +121,14 @@ module LitleOnline
188
121
  end
189
122
 
190
123
  def echeck_verification(options)
191
- transaction = EcheckVerification.new
192
-
193
- add_echeck_order_info(transaction, options)
194
- add_echeck(transaction, options)
195
- transaction.merchantData = MerchantData.from_hash(options)
196
-
124
+ transaction = @litle_transaction.echeck_verification(options)
197
125
 
198
126
  commit(transaction, :echeckVerification, options)
199
127
  end
200
128
 
201
129
  def echeck_void(options)
202
- transaction = EcheckVoid.new
203
- transaction.litleTxnId = options['litleTxnId']
204
-
130
+ transaction = @litle_transaction.echeck_void(options)
131
+
205
132
  commit(transaction, :echeckVoid, options)
206
133
  end
207
134
 
@@ -213,52 +140,6 @@ module LitleOnline
213
140
  transaction.customerId = options['customerId']
214
141
  end
215
142
 
216
- def add_transaction_info(transaction, options)
217
- transaction.litleTxnId = options['litleTxnId']
218
- transaction.customerInfo = CustomerInfo.from_hash(options)
219
- transaction.shipToAddress = Contact.from_hash(options,'shipToAddress')
220
- transaction.billMeLaterRequest = BillMeLaterRequest.from_hash(options)
221
- transaction.cardholderAuthentication = FraudCheck.from_hash(options)
222
- transaction.allowPartialAuth = options['allowPartialAuth']
223
- transaction.healthcareIIAS = HealthcareIIAS.from_hash(options)
224
- transaction.filtering = Filtering.from_hash(options)
225
- transaction.merchantData = MerchantData.from_hash(options)
226
- transaction.recyclingRequest = RecyclingRequest.from_hash(options)
227
- transaction.fraudFilterOverride = options['fraudFilterOverride']
228
- transaction.customBilling = CustomBilling.from_hash(options)
229
- transaction.paypal = PayPal.from_hash(options,'paypal')
230
-
231
- add_order_info(transaction, options)
232
- end
233
-
234
- def add_order_info(transaction, options)
235
- transaction.amount = options['amount']
236
- transaction.orderId = options['orderId']
237
- transaction.orderSource = options['orderSource']
238
- transaction.taxType = options['taxType']
239
- transaction.billToAddress = Contact.from_hash(options,'billToAddress')
240
- transaction.enhancedData = EnhancedData.from_hash(options)
241
- transaction.processingInstructions = ProcessingInstructions.from_hash(options)
242
- transaction.pos = Pos.from_hash(options)
243
- transaction.amexAggregatorData = AmexAggregatorData.from_hash(options)
244
- transaction.card = Card.from_hash(options)
245
- transaction.token = CardToken.from_hash(options,'token')
246
- transaction.paypage = CardPaypage.from_hash(options,'paypage')
247
- end
248
-
249
- def add_echeck_order_info(transaction, options)
250
- transaction.litleTxnId = options['litleTxnId']
251
- transaction.orderId = options['orderId']
252
- transaction.amount = options['amount']
253
- transaction.orderSource = options['orderSource']
254
- transaction.billToAddress = Contact.from_hash(options,'billToAddress')
255
- end
256
-
257
- def add_echeck(transaction, options)
258
- transaction.echeck = Echeck.from_hash(options)
259
- transaction.echeckToken = EcheckToken.from_hash(options)
260
- end
261
-
262
143
  def build_request(options)
263
144
  request = OnlineRequest.new
264
145
 
@@ -268,7 +149,7 @@ module LitleOnline
268
149
 
269
150
  request.authentication = authentication
270
151
  request.merchantId = get_merchant_id(options)
271
- request.version = '8.16'
152
+ request.version = '8.17'
272
153
  request.loggedInUser = get_logged_in_user(options)
273
154
  request.xmlns = "http://www.litle.com/schema"
274
155
  request.merchantSdk = get_merchant_sdk(options)
@@ -299,7 +180,7 @@ module LitleOnline
299
180
  end
300
181
 
301
182
  def get_merchant_sdk(options)
302
- options['merchantSdk'] || 'Ruby;8.16.0'
183
+ options['merchantSdk'] || 'Ruby;8.17.0'
303
184
  end
304
185
 
305
186
  def get_report_group(options)
@@ -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.17'
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.17'
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