blockchyp 2.2.1

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 (40) hide show
  1. checksums.yaml +7 -0
  2. data/Makefile +71 -0
  3. data/README.md +997 -0
  4. data/Rakefile +39 -0
  5. data/lib/blockchyp.rb +190 -0
  6. data/lib/blockchyp/version.rb +5 -0
  7. data/lib/blockchyp_client.rb +346 -0
  8. data/lib/crypto_utils.rb +25 -0
  9. data/test/boolean_prompt_test.rb +43 -0
  10. data/test/capture_signature_test.rb +40 -0
  11. data/test/gateway_timeout_test.rb +40 -0
  12. data/test/heartbeat_test.rb +27 -0
  13. data/test/new_transaction_display_test.rb +78 -0
  14. data/test/pan_charge_test.rb +53 -0
  15. data/test/pan_enroll_test.rb +51 -0
  16. data/test/pan_preauth_test.rb +52 -0
  17. data/test/simple_batch_close_test.rb +48 -0
  18. data/test/simple_capture_test.rb +47 -0
  19. data/test/simple_gift_activate_test.rb +42 -0
  20. data/test/simple_message_test.rb +40 -0
  21. data/test/simple_ping_test.rb +39 -0
  22. data/test/simple_refund_test.rb +48 -0
  23. data/test/simple_reversal_test.rb +48 -0
  24. data/test/simple_void_test.rb +48 -0
  25. data/test/terminal_charge_test.rb +51 -0
  26. data/test/terminal_clear_test.rb +39 -0
  27. data/test/terminal_ebt_balance_test.rb +41 -0
  28. data/test/terminal_ebt_charge_test.rb +53 -0
  29. data/test/terminal_enroll_test.rb +50 -0
  30. data/test/terminal_gift_card_balance_test.rb +40 -0
  31. data/test/terminal_keyed_charge_test.rb +52 -0
  32. data/test/terminal_manual_ebt_charge_test.rb +54 -0
  33. data/test/terminal_preauth_test.rb +51 -0
  34. data/test/terminal_status_test.rb +39 -0
  35. data/test/terminal_timeout_test.rb +39 -0
  36. data/test/terms_and_conditions_test.rb +44 -0
  37. data/test/test_helper.rb +65 -0
  38. data/test/text_prompt_test.rb +41 -0
  39. data/test/update_transaction_display_test.rb +78 -0
  40. metadata +81 -0
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+
5
+ begin
6
+ Bundler.setup(:default)
7
+ rescue Bundler::BundlerError => e
8
+ warn e.message
9
+ warn 'Run `bundle install` to install missing gems'
10
+ exit e.status_code
11
+ end
12
+
13
+ require 'rake'
14
+ require 'rake/testtask'
15
+
16
+ task(default: %i[test lint])
17
+
18
+ Rake::TestTask.new(:test) do |t|
19
+ t.libs << '.' << 'lib' << 'test'
20
+ t.test_files = FileList['test/*_test.rb']
21
+ t.verbose = false
22
+ end
23
+
24
+ task :lint do
25
+ if RUBY_ENGINE == 'ruby'
26
+ require 'rubocop/rake_task'
27
+ RuboCop::RakeTask.new
28
+ end
29
+ end
30
+
31
+ task(gem: :build)
32
+ task :build do
33
+ system 'gem build blockchyp.gemspec'
34
+ end
35
+
36
+ task publish: :build do
37
+ system "gem push blockchyp-#{BlockChyp::VERSION}.gem"
38
+ system "rm blockchyp-#{BlockChyp::VERSION}.gem"
39
+ end
@@ -0,0 +1,190 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2019 BlockChyp, Inc. All rights reserved. Use of this code is
4
+ # governed by a license that can be found in the LICENSE file.
5
+ #
6
+ # This file was generated automatically. Changes to this file will be lost
7
+ # every time the code is regenerated.
8
+
9
+ require_relative 'blockchyp_client'
10
+
11
+ module CardType
12
+ CREDIT = 0
13
+ DEBIT = 1
14
+ EBT = 2
15
+ BLOCKCHAIN_GIFT = 3
16
+ end
17
+
18
+ module SignatureFormat
19
+ NONE = ''
20
+ PNG = 'png'
21
+ JPG = 'jpg'
22
+ GIF = 'gif'
23
+ end
24
+
25
+ module PromptType
26
+ AMOUNT = 'amount'
27
+ EMAIL = 'email'
28
+ PHONE_NUMBER = 'phone'
29
+ CUSTOMER_NUMBER = 'customer-number'
30
+ REWARDS_NUMBER = 'rewards-number'
31
+ FIRST_NAME = 'first-name'
32
+ LAST_NAME = 'last-name'
33
+ end
34
+
35
+ module AVSResponse
36
+ NOT_APPLICABLE = ''
37
+ NOT_SUPPORTED = 'not_supported'
38
+ RETRY = 'retry'
39
+ NO_MATCH = 'no_match'
40
+ ADDRESS_MATCH = 'address_match'
41
+ POSTAL_CODE_MATCH = 'zip_match'
42
+ ADDRESS_AND_POSTAL_CODE_MATCH = 'match'
43
+ end
44
+
45
+ module BlockChyp
46
+ # the main autogenerated blockchyp client
47
+ class BlockChyp < BlockChypClient
48
+
49
+ def heartbeat(test)
50
+ gateway_request('GET', '/api/heartbeat', {'test' => test})
51
+ end
52
+
53
+ # Executes a standard direct preauth and capture.
54
+ def charge(request)
55
+ route_terminal_request('POST', '/api/charge', '/api/charge', request)
56
+ end
57
+
58
+ # Executes a preauthorization intended to be captured later.
59
+ def preauth(request)
60
+ route_terminal_request('POST', '/api/preauth', '/api/preauth', request)
61
+ end
62
+
63
+ # Tests connectivity with a payment terminal.
64
+ def ping(request)
65
+ route_terminal_request('POST', '/api/test', '/api/terminal-test', request)
66
+ end
67
+
68
+ # Checks the remaining balance on a payment method.
69
+ def balance(request)
70
+ route_terminal_request('POST', '/api/balance', '/api/balance', request)
71
+ end
72
+
73
+ # Clears the line item display and any in progress transaction.
74
+ def clear(request)
75
+ route_terminal_request('POST', '/api/clear', '/api/terminal-clear', request)
76
+ end
77
+
78
+ # Prompts the user to accept terms and conditions.
79
+ def terms_and_conditions(request)
80
+ route_terminal_request('POST', '/api/tc', '/api/terminal-tc', request)
81
+ end
82
+
83
+ # Appends items to an existing transaction display. Subtotal, Tax, and
84
+ # Total are overwritten by the request. Items with the same description
85
+ # are combined into groups.
86
+ def update_transaction_display(request)
87
+ route_terminal_request('PUT', '/api/txdisplay', '/api/terminal-txdisplay', request)
88
+ end
89
+
90
+ # Displays a new transaction on the terminal.
91
+ def new_transaction_display(request)
92
+ route_terminal_request('POST', '/api/txdisplay', '/api/terminal-txdisplay', request)
93
+ end
94
+
95
+ # Asks the consumer a text based question.
96
+ def text_prompt(request)
97
+ route_terminal_request('POST', '/api/text-prompt', '/api/text-prompt', request)
98
+ end
99
+
100
+ # Asks the consumer a yes/no question.
101
+ def boolean_prompt(request)
102
+ route_terminal_request('POST', '/api/boolean-prompt', '/api/boolean-prompt', request)
103
+ end
104
+
105
+ # Displays a short message on the terminal.
106
+ def message(request)
107
+ route_terminal_request('POST', '/api/message', '/api/message', request)
108
+ end
109
+
110
+ # Executes a refund.
111
+ def refund(request)
112
+ route_terminal_request('POST', '/api/refund', '/api/refund', request)
113
+ end
114
+
115
+ # Adds a new payment method to the token vault.
116
+ def enroll(request)
117
+ route_terminal_request('POST', '/api/enroll', '/api/enroll', request)
118
+ end
119
+
120
+ # Activates or recharges a gift card.
121
+ def gift_activate(request)
122
+ route_terminal_request('POST', '/api/gift-activate', '/api/gift-activate', request)
123
+ end
124
+
125
+ # Returns the current status of a terminal.
126
+ def terminal_status(request)
127
+ route_terminal_request('POST', '/api/terminal-status', '/api/terminal-status', request)
128
+ end
129
+
130
+ # Captures and returns a signature.
131
+ def capture_signature(request)
132
+ route_terminal_request('POST', '/api/capture-signature', '/api/capture-signature', request)
133
+ end
134
+
135
+ # Executes a manual time out reversal.
136
+ #
137
+ # We love time out reversals. Don't be afraid to use them whenever a
138
+ # request to a BlockChyp terminal times out. You have up to two minutes to
139
+ # reverse any transaction. The only caveat is that you must assign
140
+ # transactionRef values when you build the original request. Otherwise, we
141
+ # have no real way of knowing which transaction you're trying to reverse
142
+ # because we may not have assigned it an id yet. And if we did assign it
143
+ # an id, you wouldn't know what it is because your request to the terminal
144
+ # timed out before you got a response.
145
+ def reverse(request)
146
+ gateway_request('POST', '/api/reverse', request)
147
+ end
148
+
149
+ # Captures a preauthorization.
150
+ def capture(request)
151
+ gateway_request('POST', '/api/capture', request)
152
+ end
153
+
154
+ # Closes the current credit card batch.
155
+ def close_batch(request)
156
+ gateway_request('POST', '/api/close-batch', request)
157
+ end
158
+
159
+ # Discards a previous preauth transaction.
160
+ def void(request)
161
+ gateway_request('POST', '/api/void', request)
162
+ end
163
+
164
+ # Updates or creates a customer record.
165
+ def update_customer(request)
166
+ gateway_request('POST', '/api/update-customer', request)
167
+ end
168
+
169
+ # Retrieves a customer by id.
170
+ def customer(request)
171
+ gateway_request('POST', '/api/customer', request)
172
+ end
173
+
174
+ # Searches the customer database.
175
+ def customer_search(request)
176
+ gateway_request('POST', '/api/customer-search', request)
177
+ end
178
+
179
+ # Retrieves the current status of a transaction.
180
+ def transaction_status(request)
181
+ gateway_request('POST', '/api/tx-status', request)
182
+ end
183
+
184
+ # Creates and send a payment link to a customer.
185
+ def send_payment_link(request)
186
+ gateway_request('POST', '/api/send-payment-link', request)
187
+ end
188
+
189
+ end
190
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BlockChyp
4
+ VERSION = '2.2.1'
5
+ end
@@ -0,0 +1,346 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'base64'
4
+ require 'cgi'
5
+ require 'digest'
6
+ require 'json'
7
+ require 'net/http'
8
+ require 'openssl'
9
+ require 'time'
10
+ require 'tmpdir'
11
+ require 'uri'
12
+
13
+ require_relative 'crypto_utils'
14
+
15
+ module BlockChyp
16
+ # base class for the blockchyp generated blockchyp client
17
+ class BlockChypClient
18
+ def initialize(api_key, bearer_token, signing_key)
19
+ @api_key = api_key
20
+ @bearer_token = bearer_token
21
+ @signing_key = signing_key
22
+ @gateway_host = 'https://api.blockchyp.com'
23
+ @test_gateway_host = 'https://test.blockchyp.com'
24
+ @https = false
25
+ @route_cache_location = File.join(Dir.tmpdir, '.blockchyp_route')
26
+ @route_cache_ttl = 60
27
+ @gateway_timeout = 20
28
+ @terminal_timeout = 120
29
+ @terminal_connect_timeout = 5
30
+ @offline_cache_enabled = true
31
+ @route_cache = {}
32
+ @offline_fixed_key = 'cb22789c9d5c344a10e0474f134db39e25eb3bbf5a1b1a5e89b507f15ea9519c'
33
+ end
34
+
35
+ attr_reader :api_key
36
+ attr_reader :bearer_token
37
+ attr_reader :signing_key
38
+ attr_reader :offline_fixed_key
39
+ attr_accessor :gateway_host
40
+ attr_accessor :test_gateway_host
41
+ attr_accessor :https
42
+ attr_accessor :route_cache_ttl
43
+ attr_accessor :gateway_timeout
44
+ attr_accessor :terminal_timeout
45
+ attr_accessor :offline_cache_enabled
46
+ attr_accessor :terminal_connect_timeout
47
+ attr_accessor :route_cache_location
48
+
49
+ def generate_gateway_headers
50
+ nonce = CryptoUtils.generate_nonce
51
+ tsp = CryptoUtils.timestamp
52
+
53
+ sig = compute_hmac(tsp, nonce)
54
+
55
+ {
56
+ 'Nonce' => nonce,
57
+ 'Timestamp' => tsp,
58
+ 'Authorization' => 'Dual ' + bearer_token + ':' + api_key + ':' + sig
59
+ }
60
+ end
61
+
62
+ def compute_hmac(tsp, nonce)
63
+ canonical_string = api_key + bearer_token + tsp + nonce
64
+
65
+ OpenSSL::HMAC.hexdigest('SHA256', CryptoUtils.hex2bin(signing_key), canonical_string)
66
+ end
67
+
68
+ def resolve_gateway_uri(path, request)
69
+ url = if request.nil? || !request['test']
70
+ gateway_host
71
+ else
72
+ test_gateway_host
73
+ end
74
+
75
+ URI.parse(url + path)
76
+ end
77
+
78
+ def generate_error_response(msg)
79
+ [
80
+ 'success' => false,
81
+ 'error' => msg,
82
+ 'responseDescription' => msg
83
+ ]
84
+ end
85
+
86
+ def route_terminal_request(method, terminal_path, gateway_path, request)
87
+ if request['terminalName'].nil?
88
+ return gateway_request(method, gateway_path, request)
89
+ end
90
+
91
+ route = resolve_terminal_route(request['terminalName'])
92
+ if !route
93
+ return generate_error_response('Unkown Terminal')
94
+ elsif route['cloudRelayEnabled']
95
+ return gateway_request(method, gateway_path, request, relay: true)
96
+ end
97
+
98
+ terminal_request(method, route, terminal_path, request, true)
99
+ end
100
+
101
+ def terminal_request(method, route, path, request, open_retry)
102
+ uri = resolve_terminal_uri(route, path)
103
+ http = Net::HTTP.new(uri.host, uri.port)
104
+ http.use_ssl = uri.instance_of?(URI::HTTPS)
105
+ timeout = get_timeout(request, terminal_timeout)
106
+ http.open_timeout = timeout
107
+ http.read_timeout = timeout
108
+
109
+ tx_creds = route['transientCredentials']
110
+
111
+ wrapped_request = {
112
+ 'apiKey' => tx_creds['apiKey'],
113
+ 'bearerToken' => tx_creds['bearerToken'],
114
+ 'signingKey' => tx_creds['signingKey'],
115
+ 'request' => request
116
+ }
117
+
118
+ req = get_http_request(method, uri)
119
+
120
+ req['User-Agent'] = user_agent
121
+ json = wrapped_request.to_json
122
+ req['Content-Type'] = 'application/json'
123
+ req['Content-Length'] = json.length
124
+ req.body = json
125
+
126
+ begin
127
+ response = http.request(req)
128
+ rescue Net::OpenTimeout, Errno::EHOSTDOWN, Errno::EHOSTUNREACH, Errno::ETIMEDOUT, Errno::ENETUNREACH
129
+ if open_retry
130
+ evict(route['terminalName'])
131
+ route = resolve_terminal_route(route['terminalName'])
132
+ return terminal_request(method, route, path, request, false)
133
+ end
134
+ raise
135
+ end
136
+ if response.is_a?(Net::HTTPSuccess)
137
+ JSON.parse(response.body)
138
+ else
139
+ raise response.message
140
+ end
141
+ end
142
+
143
+ def get_http_request(method, uri)
144
+ case method
145
+ when 'GET'
146
+ Net::HTTP::Get.new(uri.request_uri)
147
+ when 'PUT'
148
+ Net::HTTP::Put.new(uri.request_uri)
149
+ when 'POST'
150
+ Net::HTTP::Post.new(uri.request_uri)
151
+ end
152
+ end
153
+
154
+ def resolve_terminal_uri(route, path)
155
+ url = if https
156
+ 'https://'
157
+ else
158
+ 'http://'
159
+ end
160
+ url += route['ipAddress']
161
+ port = if https
162
+ ':8443'
163
+ else
164
+ ':8080'
165
+ end
166
+ URI.parse(url + port + path)
167
+ end
168
+
169
+ def gateway_request(method, path, request = nil, relay = false)
170
+ uri = resolve_gateway_uri(path, request)
171
+ http = Net::HTTP.new(uri.host, uri.port)
172
+ http.use_ssl = uri.instance_of?(URI::HTTPS)
173
+ timeout = get_timeout(request, relay ? terminal_timeout : gateway_timeout)
174
+ http.open_timeout = timeout
175
+ http.read_timeout = timeout
176
+
177
+ req = get_http_request(method, uri)
178
+
179
+ req['User-Agent'] = user_agent
180
+ unless request.nil?
181
+ json = request.to_json
182
+ req['Content-Type'] = 'application/json'
183
+ req['Content-Length'] = json.length
184
+ req.body = json
185
+ end
186
+
187
+ headers = generate_gateway_headers
188
+ headers.each do |key, value|
189
+ req[key] = value
190
+ end
191
+
192
+ response = http.request(req)
193
+
194
+ if response.is_a?(Net::HTTPSuccess)
195
+ JSON.parse(response.body)
196
+ else
197
+ raise response.message
198
+ end
199
+ end
200
+
201
+ attr_accessor :routeCache
202
+
203
+ def resolve_terminal_route(terminal_name)
204
+ route = route_cache_get(terminal_name, false)
205
+
206
+ if route.nil?
207
+ route = request_route_from_gateway(terminal_name)
208
+ if route.nil?
209
+ return route
210
+ end
211
+
212
+ ttl = Time.now.utc + (route_cache_ttl * 60)
213
+ route_cache_entry = {}
214
+ route_cache_entry['route'] = route
215
+ route_cache_entry['ttl'] = ttl
216
+ @route_cache[api_key + terminal_name] = route_cache_entry
217
+ update_offline_cache(route_cache_entry)
218
+ end
219
+ route
220
+ end
221
+
222
+ def update_offline_cache(route_cache_entry)
223
+ if offline_cache_enabled
224
+ offline_cache = read_offline_cache
225
+ offline_entry = route_cache_entry.clone
226
+ route = route_cache_entry['route'].clone
227
+ tx_creds = route['transientCredentials'].clone
228
+ tx_creds['apiKey'] = encrypt(tx_creds['apiKey'])
229
+ tx_creds['bearerToken'] = encrypt(tx_creds['bearerToken'])
230
+ tx_creds['signingKey'] = encrypt(tx_creds['signingKey'])
231
+ route['transientCredentials'] = tx_creds
232
+ offline_entry['route'] = route
233
+ offline_cache[api_key + route['terminalName']] = offline_entry
234
+ File.write(route_cache_location, offline_cache.to_json)
235
+ end
236
+ end
237
+
238
+ def encrypt(plain_text)
239
+ return plain_text if plain_text.nil? || plain_text.empty?
240
+
241
+ cipher = OpenSSL::Cipher::AES256.new(:CBC)
242
+ cipher.encrypt
243
+ cipher.key = derive_offline_key
244
+ iv = cipher.random_iv
245
+
246
+ Base64.encode64(iv) + ':' + Base64.encode64(cipher.update(plain_text) + cipher.final)
247
+ end
248
+
249
+ def decrypt(cipher_text)
250
+ return cipher_text if cipher_text.nil? || cipher_text.empty?
251
+
252
+ tokens = cipher_text.split(':')
253
+
254
+ return cipher_text if tokens[0].nil? || tokens[1].nil?
255
+
256
+ iv = Base64.decode64(tokens[0])
257
+ cp = Base64.decode64(tokens[1])
258
+
259
+ decipher = OpenSSL::Cipher::AES256.new(:CBC)
260
+ decipher.decrypt
261
+ decipher.key = derive_offline_key
262
+ decipher.iv = iv
263
+
264
+ decipher.update(cp) + decipher.final
265
+ end
266
+
267
+ def derive_offline_key
268
+ Digest::SHA256.digest offline_fixed_key + signing_key
269
+ end
270
+
271
+ def request_route_from_gateway(terminal_name)
272
+ route = gateway_request('GET', '/api/terminal-route?terminal=' + CGI.escape(terminal_name))
273
+ if !route.nil? && !route['ipAddress'].empty?
274
+ route['exists'] = true
275
+ end
276
+ route
277
+ end
278
+
279
+ def evict(terminal_name)
280
+ route_cache.delete(api_key + terminal_name)
281
+
282
+ offline_cache = read_offline_cache
283
+ offline_cache.delete(api_key + terminal_name)
284
+ File.write(route_cache_location, route_cache.to_json)
285
+ end
286
+
287
+ def route_cache_get(terminal_name, stale)
288
+ route_cache_entry = @route_cache[api_key + terminal_name]
289
+
290
+ if route_cache_entry.nil? && offline_cache_enabled
291
+ offline_cache = read_offline_cache
292
+ route_cache_entry = offline_cache[@api_key + terminal_name]
293
+ end
294
+
295
+ if route_cache_entry
296
+ route = route_cache_entry['route']
297
+ tx_creds = route['transientCredentials']
298
+ tx_creds['apiKey'] = decrypt(tx_creds['apiKey'])
299
+ tx_creds['bearerToken'] = decrypt(tx_creds['bearerToken'])
300
+ tx_creds['signingKey'] = decrypt(tx_creds['signingKey'])
301
+ route['transientCredentials'] = tx_creds
302
+ route_cache_entry['route'] = route
303
+ end
304
+
305
+ if route_cache_entry
306
+ now = Time.new
307
+ raw_ttl = route_cache_entry['ttl']
308
+ if raw_ttl.instance_of?(Time)
309
+ ttl = raw_ttl
310
+ else
311
+ ttl = Time.parse(route_cache_entry['ttl'])
312
+ end
313
+ if stale || now < ttl
314
+ route_cache_entry['route']
315
+ end
316
+ end
317
+ end
318
+
319
+ def read_offline_cache
320
+ if File.file?(route_cache_location)
321
+
322
+ config_file = File.open(route_cache_location)
323
+ content = config_file.read
324
+
325
+ return JSON.parse(content)
326
+ end
327
+ {}
328
+ end
329
+
330
+ def user_agent
331
+ if defined? VERSION
332
+ "BlockChyp-Ruby/#{VERSION}"
333
+ else
334
+ "BlockChyp-Ruby"
335
+ end
336
+ end
337
+
338
+ def get_timeout(request, default)
339
+ if request.nil? || request['timeout'].nil? || request['timeout'].zero?
340
+ return default
341
+ end
342
+
343
+ request['timeout']
344
+ end
345
+ end
346
+ end