LitleOnline 8.16.0 → 8.17.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.
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