/
moneris.rb
494 lines (436 loc) · 19.9 KB
/
moneris.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
require 'rexml/document'
module ActiveMerchant #:nodoc:
module Billing #:nodoc:
# To learn more about the Moneris gateway, please contact
# eselectplus@moneris.com for a copy of their integration guide. For
# information on remote testing, please see "Test Environment Penny Value
# Response Table", and "Test Environment eFraud (AVS and CVD) Penny
# Response Values", available at Moneris' {eSelect Plus Documentation
# Centre}[https://www3.moneris.com/connect/en/documents/index.html].
class MonerisGateway < Gateway
WALLETS = %w(APP GPP)
self.test_url = 'https://esqa.moneris.com/gateway2/servlet/MpgRequest'
self.live_url = 'https://www3.moneris.com/gateway2/servlet/MpgRequest'
self.supported_countries = ['CA']
self.supported_cardtypes = %i[visa master american_express diners_club discover]
self.homepage_url = 'http://www.moneris.com/'
self.display_name = 'Moneris'
# Initialize the Gateway
#
# The gateway requires that a valid login and password be passed
# in the +options+ hash.
#
# ==== Options
#
# * <tt>:login</tt> -- Your Store ID
# * <tt>:password</tt> -- Your API Token
# * <tt>:cvv_enabled</tt> -- Specify that you would like the CVV passed to the gateway.
# Only particular account types at Moneris will allow this.
# Defaults to false. (optional)
def initialize(options = {})
requires!(options, :login, :password)
@cvv_enabled = options[:cvv_enabled]
@avs_enabled = options[:avs_enabled]
options[:crypt_type] = 7 unless options.has_key?(:crypt_type)
super
end
# Referred to as "PreAuth" in the Moneris integration guide, this action
# verifies and locks funds on a customer's card, which then must be
# captured at a later date.
#
# Pass in +order_id+ and optionally a +customer+ parameter.
def authorize(money, creditcard_or_datakey, options = {})
requires!(options, :order_id)
post = {}
add_payment_source(post, creditcard_or_datakey, options)
post[:amount] = amount(money)
post[:order_id] = format_order_id(post[:wallet_indicator], options[:order_id])
post[:address] = options[:billing_address] || options[:address]
post[:crypt_type] = options[:crypt_type] || @options[:crypt_type]
add_external_mpi_fields(post, options)
add_stored_credential(post, options)
add_cust_id(post, options)
action = if post[:cavv] || options[:three_d_secure]
'cavv_preauth'
elsif post[:data_key].blank?
'preauth'
else
'res_preauth_cc'
end
commit(action, post, options)
end
# This action verifies funding on a customer's card and readies them for
# deposit in a merchant's account.
#
# Pass in <tt>order_id</tt> and optionally a <tt>customer</tt> parameter
def purchase(money, creditcard_or_datakey, options = {})
requires!(options, :order_id)
post = {}
add_payment_source(post, creditcard_or_datakey, options)
post[:amount] = amount(money)
post[:order_id] = format_order_id(post[:wallet_indicator], options[:order_id])
post[:address] = options[:billing_address] || options[:address]
post[:crypt_type] = options[:crypt_type] || @options[:crypt_type]
add_external_mpi_fields(post, options)
add_stored_credential(post, options)
add_cust_id(post, options)
action = if post[:cavv] || options[:three_d_secure]
'cavv_purchase'
elsif post[:data_key].blank?
'purchase'
else
'res_purchase_cc'
end
commit(action, post, options)
end
# This method retrieves locked funds from a customer's account (from a
# PreAuth) and prepares them for deposit in a merchant's account.
#
# Note: Moneris requires both the order_id and the transaction number of
# the original authorization. To maintain the same interface as the other
# gateways the two numbers are concatenated together with a ; separator as
# the authorization number returned by authorization
def capture(money, authorization, options = {})
commit 'completion', crediting_params(authorization, comp_amount: amount(money))
end
# Voiding requires the original transaction ID and order ID of some open
# transaction. Closed transactions must be refunded.
#
# Moneris supports the voiding of an unsettled capture or purchase via
# its <tt>purchasecorrection</tt> command. This action can only occur
# on the same day as the capture/purchase prior to 22:00-23:00 EST. If
# you want to do this, pass <tt>:purchasecorrection => true</tt> as
# an option.
#
# Fun, Historical Trivia:
# Voiding an authorization in Moneris is a relatively new feature
# (September, 2011). It is actually done by doing a $0 capture.
#
# Concatenate your transaction number and order_id by using a semicolon
# (';'). This is to keep the Moneris interface consistent with other
# gateways. (See +capture+ for details.)
def void(authorization, options = {})
if options[:purchasecorrection]
commit 'purchasecorrection', crediting_params(authorization)
else
capture(0, authorization, options)
end
end
# Performs a refund. This method requires that the original transaction
# number and order number be included. Concatenate your transaction
# number and order_id by using a semicolon (';'). This is to keep the
# Moneris interface consistent with other gateways. (See +capture+ for
# details.)
def credit(money, authorization, options = {})
ActiveMerchant.deprecated CREDIT_DEPRECATION_MESSAGE
refund(money, authorization, options)
end
def refund(money, authorization, options = {})
commit 'refund', crediting_params(authorization, amount: amount(money))
end
def verify(credit_card, options = {})
requires!(options, :order_id)
post = {}
add_payment_source(post, credit_card, options)
post[:order_id] = options[:order_id]
post[:address] = options[:billing_address] || options[:address]
post[:crypt_type] = options[:crypt_type] || @options[:crypt_type]
add_stored_credential(post, options)
action = if post[:data_key].blank?
'card_verification'
else
'res_card_verification_cc'
end
commit(action, post)
end
# When passing a :duration option (time in seconds) you can create a
# temporary vault record to avoid incurring Moneris vault storage fees
#
# https://developer.moneris.com/Documentation/NA/E-Commerce%20Solutions/API/Vault#vaulttokenadd
def store(credit_card, options = {})
post = {}
post[:pan] = credit_card.number
post[:expdate] = expdate(credit_card)
post[:address] = options[:billing_address] || options[:address]
post[:crypt_type] = options[:crypt_type] || @options[:crypt_type]
add_stored_credential(post, options)
if options[:duration]
post[:duration] = options[:duration]
commit('res_temp_add', post)
else
commit('res_add_cc', post)
end
end
def unstore(data_key, options = {})
post = {}
post[:data_key] = data_key
commit('res_delete', post)
end
def update(data_key, credit_card, options = {})
post = {}
post[:pan] = credit_card.number
post[:expdate] = expdate(credit_card)
post[:data_key] = data_key
post[:crypt_type] = options[:crypt_type] || @options[:crypt_type]
commit('res_update_cc', post)
end
def supports_scrubbing?
true
end
def scrub(transcript)
transcript.
gsub(%r((<store_id>).+(</store_id>)), '\1[FILTERED]\2').
gsub(%r((<api_token>).+(</api_token>)), '\1[FILTERED]\2').
gsub(%r((<pan>).+(</pan>)), '\1[FILTERED]\2').
gsub(%r((<cvd_value>).+(</cvd_value>)), '\1[FILTERED]\2').
gsub(%r((<cavv>).+(</cavv>)), '\1[FILTERED]\2')
end
private # :nodoc: all
def expdate(creditcard)
sprintf('%.4i', creditcard.year)[-2..-1] + sprintf('%.2i', creditcard.month)
end
def add_external_mpi_fields(post, options)
# See these pages:
# https://developer.moneris.com/livedemo/3ds2/cavv_purchase/tool/php
# https://developer.moneris.com/livedemo/3ds2/cavv_preauth/guide/php
return unless options[:three_d_secure]
three_d_secure_options = options[:three_d_secure]
post[:threeds_version] = three_d_secure_options[:version]
post[:crypt_type] = three_d_secure_options[:eci]
post[:cavv] = three_d_secure_options[:cavv]
post[:threeds_server_trans_id] = three_d_secure_options[:three_ds_server_trans_id]
post[:ds_trans_id] = three_d_secure_options[:ds_transaction_id]
end
def add_payment_source(post, payment_method, options)
if payment_method.is_a?(String)
post[:data_key] = payment_method
post[:cust_id] = options[:customer]
else
if payment_method.respond_to?(:track_data) && payment_method.track_data.present?
post[:pos_code] = '00'
post[:track2] = payment_method.track_data
else
post[:pan] = payment_method.number
post[:expdate] = expdate(payment_method)
post[:cvd_value] = payment_method.verification_value if payment_method.verification_value?
post[:cavv] = payment_method.payment_cryptogram if payment_method.is_a?(NetworkTokenizationCreditCard)
post[:wallet_indicator] = wallet_indicator(payment_method.source.to_s) if payment_method.is_a?(NetworkTokenizationCreditCard)
post[:crypt_type] = (payment_method.eci || 7) if payment_method.is_a?(NetworkTokenizationCreditCard)
end
post[:cust_id] = options[:customer] || payment_method.name
end
end
def add_cof(post, options)
post[:issuer_id] = options[:issuer_id] if options[:issuer_id]
post[:payment_indicator] = options[:payment_indicator] if options[:payment_indicator]
post[:payment_information] = options[:payment_information] if options[:payment_information]
end
def add_cust_id(post, options)
post[:cust_id] = options[:cust_id] if options[:cust_id]
end
def add_stored_credential(post, options)
add_cof(post, options)
# if any of :issuer_id, :payment_information, or :payment_indicator is not passed,
# then check for :stored credential options
return unless (stored_credential = options[:stored_credential]) && !cof_details_present?(options)
if stored_credential[:initial_transaction]
add_stored_credential_initial(post, options)
else
add_stored_credential_used(post, options)
end
end
def add_stored_credential_initial(post, options)
post[:payment_information] ||= '0'
post[:issuer_id] ||= ''
if options[:stored_credential][:initiator] == 'merchant'
case options[:stored_credential][:reason_type]
when 'recurring', 'installment'
post[:payment_indicator] ||= 'R'
when 'unscheduled'
post[:payment_indicator] ||= 'C'
end
else
post[:payment_indicator] ||= 'C'
end
end
def add_stored_credential_used(post, options)
post[:payment_information] ||= '2'
post[:issuer_id] = options[:stored_credential][:network_transaction_id] if options[:issuer_id].blank?
if options[:stored_credential][:initiator] == 'merchant'
case options[:stored_credential][:reason_type]
when 'recurring', 'installment'
post[:payment_indicator] ||= 'R'
when '', 'unscheduled'
post[:payment_indicator] ||= 'U'
end
else
post[:payment_indicator] ||= 'Z'
end
end
# Common params used amongst the +credit+, +void+ and +capture+ methods
def crediting_params(authorization, options = {})
{
txn_number: split_authorization(authorization).first,
order_id: split_authorization(authorization).last,
crypt_type: options[:crypt_type] || @options[:crypt_type]
}.merge(options)
end
# Splits an +authorization+ param and retrieves the order id and
# transaction number in that order.
def split_authorization(authorization)
if authorization.nil? || authorization.empty? || authorization !~ /;/
raise ArgumentError, 'You must include a valid authorization code (e.g. "1234;567")'
else
authorization.split(';')
end
end
def commit(action, parameters = {}, options = {})
threed_ds_transaction = options[:three_d_secure].present?
data = post_data(action, parameters)
url = test? ? self.test_url : self.live_url
raw = ssl_post(url, data)
response = parse(raw)
Response.new(
successful?(action, response, threed_ds_transaction),
message_from(response[:message]),
response,
test: test?,
avs_result: { code: response[:avs_result_code] },
cvv_result: response[:cvd_result_code] && response[:cvd_result_code][-1, 1],
authorization: authorization_from(response)
)
end
# Generates a Moneris authorization string of the form 'trans_id;receipt_id'.
def authorization_from(response = {})
"#{response[:trans_id]};#{response[:receipt_id]}" if response[:trans_id] && response[:receipt_id]
end
# Tests for a successful response from Moneris' servers
def successful?(action, response, threed_ds_transaction = false)
# See 9.4 CAVV Result Codes in https://developer.moneris.com/livedemo/3ds2/reference/guide/php
cavv_accepted = if threed_ds_transaction
response[:cavv_result_code] && response[:cavv_result_code] == '2'
else
true
end
cavv_accepted &&
response[:response_code] &&
response[:complete] &&
(0..49).cover?(response[:response_code].to_i)
end
def parse(xml)
response = { message: 'Global Error Receipt', complete: false }
hashify_xml!(xml, response)
response
end
def hashify_xml!(xml, response)
xml = REXML::Document.new(xml)
return if xml.root.nil?
xml.elements.each('//receipt/*') do |node|
response[node.name.underscore.to_sym] = normalize(node.text)
end
end
def post_data(action, parameters = {})
xml = REXML::Document.new
root = xml.add_element('request')
root.add_element('store_id').text = options[:login]
root.add_element('api_token').text = options[:password]
root.add_element(transaction_element(action, parameters))
xml.to_s
end
def transaction_element(action, parameters)
transaction = REXML::Element.new(action)
# Must add the elements in the correct order
actions[action].each do |key|
case key
when :avs_info
transaction.add_element(avs_element(parameters[:address])) if @avs_enabled && parameters[:address]
when :cvd_info
transaction.add_element(cvd_element(parameters[:cvd_value])) if @cvv_enabled
when :cof_info
transaction.add_element(credential_on_file(parameters)) if cof_details_present?(parameters)
else
transaction.add_element(key.to_s).text = parameters[key] unless parameters[key].blank?
end
end
transaction
end
def avs_element(address)
full_address = "#{address[:address1]} #{address[:address2]}"
tokens = full_address.split(/\s+/)
element = REXML::Element.new('avs_info')
element.add_element('avs_street_number').text = tokens.select { |x| x =~ /\d/ }.join(' ')
element.add_element('avs_street_name').text = tokens.reject { |x| x =~ /\d/ }.join(' ')
element.add_element('avs_zipcode').text = address[:zip]
element
end
def cvd_element(cvd_value)
element = REXML::Element.new('cvd_info')
if cvd_value
element.add_element('cvd_indicator').text = '1'
element.add_element('cvd_value').text = cvd_value
else
element.add_element('cvd_indicator').text = '0'
end
element
end
def cof_details_present?(parameters)
parameters[:issuer_id] && parameters[:payment_indicator] && parameters[:payment_information]
end
def credential_on_file(parameters)
issuer_id = parameters[:issuer_id]
payment_indicator = parameters[:payment_indicator]
payment_information = parameters[:payment_information]
cof_info = REXML::Element.new('cof_info')
cof_info.add_element('issuer_id').text = issuer_id
cof_info.add_element('payment_indicator').text = payment_indicator
cof_info.add_element('payment_information').text = payment_information
cof_info
end
def wallet_indicator(token_source)
return {
'apple_pay' => 'APP',
'google_pay' => 'GPP',
'android_pay' => 'ANP'
}[token_source]
end
def format_order_id(wallet_indicator_code, order_id = nil)
# Truncate (max 100 characters) order id for
# google pay and apple pay (specific wallets / token sources)
return truncate_order_id(order_id) if WALLETS.include?(wallet_indicator_code)
order_id
end
def truncate_order_id(order_id = nil)
order_id.present? ? order_id[0, 100] : SecureRandom.alphanumeric(100)
end
def message_from(message)
return 'Unspecified error' if message.blank?
message.gsub(/[^\w]/, ' ').split.join(' ').capitalize
end
def actions
{
'purchase' => %i[order_id cust_id amount pan expdate crypt_type avs_info cvd_info track2 pos_code cof_info],
'preauth' => %i[order_id cust_id amount pan expdate crypt_type avs_info cvd_info track2 pos_code cof_info],
'command' => [:order_id],
'refund' => %i[order_id amount txn_number crypt_type],
'indrefund' => %i[order_id cust_id amount pan expdate crypt_type],
'completion' => %i[order_id comp_amount txn_number crypt_type],
'purchasecorrection' => %i[order_id txn_number crypt_type],
'cavv_preauth' => %i[order_id cust_id amount pan expdate cavv crypt_type wallet_indicator threeds_version threeds_server_trans_id ds_trans_id],
'cavv_purchase' => %i[order_id cust_id amount pan expdate cavv crypt_type wallet_indicator threeds_version threeds_server_trans_id ds_trans_id],
'card_verification' => %i[order_id cust_id pan expdate crypt_type avs_info cvd_info cof_info],
'transact' => %i[order_id cust_id amount pan expdate crypt_type],
'Batchcloseall' => [],
'opentotals' => [:ecr_number],
'batchclose' => [:ecr_number],
'res_add_cc' => %i[pan expdate crypt_type avs_info cof_info],
'res_temp_add' => %i[pan expdate crypt_type duration],
'res_delete' => [:data_key],
'res_update_cc' => %i[data_key pan expdate crypt_type avs_info cof_info],
'res_purchase_cc' => %i[data_key order_id cust_id amount crypt_type cof_info],
'res_preauth_cc' => %i[data_key order_id cust_id amount crypt_type cof_info],
'res_card_verification_cc' => %i[order_id data_key expdate crypt_type cof_info]
}
end
end
end
end