Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Gwi 248 card connect update live/test ur ls on adapter #4533

Draft
wants to merge 7 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,10 @@
* Orbital: Remove `DPANInd` field for RC transactions [ajawadmirza] #4502
* EBANX: Add Spreedly tag to payment body [flaaviaa] #4527
* Shift4: Add `expiration_date` field for refund transactions [ajawadmirza] #4532
* Improve handling of AVS and CVV Results in Multiresponses [gasb150] #4516
* Airwallex: Add `skip_3ds` field for create payment transactions [ajawadmirza] #4534
* Shift4: Typo correction for `initial_transaction` [ajawadmirza] #4537
* Rapyd: Pass Customer ID and fix `add_token` method [naashton] #4538

== Version 1.126.0 (April 15th, 2022)
* Moneris: Add 3DS MPI field support [esmitperez] #4373
Expand Down
1 change: 1 addition & 0 deletions lib/active_merchant/billing/gateways/airwallex.rb
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ def create_payment_intent(money, options = {})
post[:merchant_order_id] = merchant_order_id(options)
add_referrer_data(post)
add_descriptor(post, options)
post['payment_method_options'] = { 'card' => { 'risk_control' => { 'three_ds_action' => 'SKIP_3DS' } } } if options[:skip_3ds]

response = commit(:setup, post)
raise ArgumentError.new(response.message) unless response.success?
Expand Down
4 changes: 2 additions & 2 deletions lib/active_merchant/billing/gateways/card_connect.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
module ActiveMerchant #:nodoc:
module Billing #:nodoc:
class CardConnectGateway < Gateway
self.test_url = 'https://fts.cardconnect.com:6443/cardconnect/rest/'
self.live_url = 'https://fts.cardconnect.com:8443/cardconnect/rest/'
self.test_url = 'https://fts-uat.cardconnect.com/cardconnect/rest/'
self.live_url = 'https://fts.cardconnect.com/cardconnect/rest/'

self.supported_countries = ['US']
self.default_currency = 'USD'
Expand Down
10 changes: 9 additions & 1 deletion lib/active_merchant/billing/gateways/rapyd.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ def purchase(money, payment, options = {})
add_ewallet(post, options)
add_payment_fields(post, options)
add_payment_urls(post, options)
add_customer_id(post, options)
post[:capture] = true if payment.is_a?(CreditCard)

if payment.is_a?(Check)
Expand All @@ -53,6 +54,7 @@ def authorize(money, payment, options = {})
add_ewallet(post, options)
add_payment_fields(post, options)
add_payment_urls(post, options)
add_customer_id(post, options)
post[:capture] = false

commit(:post, 'payments', post)
Expand Down Expand Up @@ -185,7 +187,9 @@ def add_ach(post, payment, options)
end

def add_token(post, payment, options)
post[:payment_method] = payment
return unless token = payment.split('|')[1]

post[:payment_method] = token
end

def add_3ds(post, payment, options)
Expand Down Expand Up @@ -226,6 +230,10 @@ def add_customer_object(post, payment, options)
post[:email] = options[:email] if options[:email]
end

def add_customer_id(post, options)
post[:customer] = options[:customer_id] if options[:customer_id]
end

def parse(body)
return {} if body.empty? || body.nil?

Expand Down
2 changes: 1 addition & 1 deletion lib/active_merchant/billing/gateways/shift4.rb
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ def add_card_on_file(post, options)
return unless stored_credential = options[:stored_credential]

post[:cardOnFile] = {}
post[:cardOnFile][:usageIndicator] = stored_credential[:inital_transaction] ? '01' : '02'
post[:cardOnFile][:usageIndicator] = stored_credential[:initial_transaction] ? '01' : '02'
post[:cardOnFile][:indicator] = options[:card_on_file_indicator] || '01'
post[:cardOnFile][:scheduledIndicator] = RECURRING_TYPE_TRANSACTIONS.include?(stored_credential[:reason_type]) ? '01' : '02' if stored_credential[:reason_type]
post[:cardOnFile][:transactionId] = stored_credential[:network_transaction_id] if stored_credential[:network_transaction_id]
Expand Down
16 changes: 15 additions & 1 deletion lib/active_merchant/billing/response.rb
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,21 @@ def success?
(primary_response ? primary_response.success? : true)
end

%w(params message test authorization avs_result cvv_result error_code emv_authorization test? fraud_review?).each do |m|
def avs_result
return @primary_response.try(:avs_result) if @use_first_response

result = responses.reverse.find { |r| r.avs_result['code'].present? }
result.try(:avs_result) || responses.last.try(:avs_result)
end

def cvv_result
return @primary_response.try(:cvv_result) if @use_first_response

result = responses.reverse.find { |r| r.cvv_result['code'].present? }
result.try(:cvv_result) || responses.last.try(:cvv_result)
end

%w(params message test authorization error_code emv_authorization test? fraud_review?).each do |m|
class_eval %(
def #{m}
(@responses.empty? ? nil : primary_response.#{m})
Expand Down
6 changes: 6 additions & 0 deletions test/remote/gateways/remote_airwallex_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ def test_successful_purchase_with_specified_ids
assert_match(merchant_order_id, response.params.dig('merchant_order_id'))
end

def test_successful_purchase_with_skip_3ds
response = @gateway.purchase(@amount, @credit_card, @options.merge({ skip_3ds: 'true' }))
assert_success response
assert_equal 'AUTHORIZED', response.message
end

def test_failed_purchase
response = @gateway.purchase(@declined_amount, @declined_card, @options)
assert_failure response
Expand Down
44 changes: 28 additions & 16 deletions test/remote/gateways/remote_card_connect_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -201,22 +201,34 @@ def test_failed_echeck_purchase
assert_equal 'Invalid card', response.message
end

def test_successful_refund
purchase = @gateway.purchase(@amount, @credit_card, @options)
assert_success purchase

assert refund = @gateway.refund(@amount, purchase.authorization)
assert_success refund
assert_equal 'Approval', refund.message
end

def test_partial_refund
purchase = @gateway.purchase(@amount, @credit_card, @options)
assert_success purchase

assert refund = @gateway.refund(@amount - 1, purchase.authorization)
assert_success refund
end
# A transaction cannot be refunded before settlement so these tests will fail with the following response
# {
# "respproc"=>"PPS",
# "amount"=>"0.00",
# "resptext"=>"Txn not settled",
# "currency"=>"USD",
# "retref"=>"222509002106",
# "respstat"=>"C",
# "respcode"=>"28",
# "merchid"=>"496160873888"
# }

# def test_successful_refund
# purchase = @gateway.purchase(@amount, @credit_card, @options)
# assert_success purchase

# assert refund = @gateway.refund(@amount, purchase.authorization)
# assert_success refund
# assert_equal 'Approval', refund.message
# end

# def test_partial_refund
# purchase = @gateway.purchase(@amount, @credit_card, @options)
# assert_success purchase

# assert refund = @gateway.refund(@amount - 1, purchase.authorization)
# assert_success refund
# end

def test_failed_refund
response = @gateway.refund(@amount, @invalid_txn)
Expand Down
13 changes: 12 additions & 1 deletion test/remote/gateways/remote_rapyd_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ def setup
@declined_card = credit_card('4111111111111105')
@check = check
@options = {
pm_type: 'us_visa_card',
pm_type: 'us_debit_visa_card',
currency: 'USD',
complete_payment_url: 'www.google.com',
error_payment_url: 'www.google.com',
Expand Down Expand Up @@ -204,6 +204,17 @@ def test_failed_verify
assert_equal 'Do Not Honor', response.message
end

def test_successful_store_and_purchase
store = @gateway.store(@credit_card, @options)
assert_success store
assert store.params.dig('data', 'id')
assert store.params.dig('data', 'default_payment_method')

# 3DS authorization is required on storing a payment method for future transactions
# purchase = @gateway.purchase(100, store.authorization, @options.merge(customer_id: customer_id))
# assert_sucess purchase
end

def test_successful_store_and_unstore
store = @gateway.store(@credit_card, @options)
assert_success store
Expand Down
4 changes: 2 additions & 2 deletions test/remote/gateways/remote_shift4_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -55,15 +55,15 @@ def test_successful_purchase_with_extra_options

def test_successful_purchase_with_stored_credential
stored_credential_options = {
inital_transaction: true,
initial_transaction: true,
reason_type: 'recurring'
}
first_response = @gateway.purchase(@amount, @credit_card, @options.merge(@extra_options.merge({ stored_credential: stored_credential_options })))
assert_success first_response

ntxid = first_response.params['result'].first['transaction']['cardOnFile']['transactionId']
stored_credential_options = {
inital_transaction: false,
initial_transaction: false,
reason_type: 'recurring',
network_transaction_id: ntxid
}
Expand Down
16 changes: 16 additions & 0 deletions test/unit/gateways/airwallex_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,22 @@ def test_successful_purchase_with_3ds_version_formatting
assert_equal 'AUTHORIZED', response.message
end

def test_successful_skip_3ds_in_payment_intent
stub_comms do
@gateway.purchase(@amount, @credit_card, @options.merge({ skip_3ds: true }))
end.check_request do |endpoint, data, _headers|
data = JSON.parse(data)
assert_match(data['payment_method_options']['card']['risk_control']['three_ds_action'], 'SKIP_3DS') if endpoint == setup_endpoint
end.respond_with(successful_purchase_response)

stub_comms do
@gateway.purchase(@amount, @credit_card, @options.merge({ skip_3ds: 'true' }))
end.check_request do |endpoint, data, _headers|
data = JSON.parse(data)
assert_match(data['payment_method_options']['card']['risk_control']['three_ds_action'], 'SKIP_3DS') if endpoint == setup_endpoint
end.respond_with(successful_purchase_response)
end

def test_successful_capture
@gateway.expects(:ssl_post).returns(successful_capture_response)

Expand Down
13 changes: 10 additions & 3 deletions test/unit/gateways/rapyd_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ def setup
@gateway = RapydGateway.new(secret_key: 'secret_key', access_key: 'access_key')
@credit_card = credit_card
@amount = 100
@authorization = 'cus_9e1b5a357b2b7f25f8dd98827fbc4f22|card_cf105df9e77462deb34ffef33c3e3d05'

@options = {
pm_type: 'in_amex_card',
Expand Down Expand Up @@ -58,10 +59,16 @@ def test_successful_purchase_with_ach
assert_equal 'ACT', response.params['data']['status']
end

def test_successful_purchase_with_options
@gateway.expects(:ssl_request).returns(successful_purchase_with_options_response)
def test_successful_purchase_with_token
@options.merge(customer_id: 'cus_9e1b5a357b2b7f25f8dd98827fbc4f22')
response = stub_comms(@gateway, :ssl_request) do
@gateway.purchase(@amount, @authorization, @options)
end.check_request do |_method, _endpoint, data, _headers|
request = JSON.parse(data)
assert_equal request['payment_method'], @authorization.split('|')[1]
assert_equal request['customer'], @options[:customer_id]
end.respond_with(successful_purchase_with_options_response)

response = @gateway.purchase(@amount, @credit_card, @options.merge(metadata: @metadata))
assert_success response
assert_equal @metadata, response.params['data']['metadata'].deep_transform_keys(&:to_sym)
end
Expand Down
23 changes: 18 additions & 5 deletions test/unit/gateways/shift4_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -77,22 +77,35 @@ def test_successful_purchase_with_extra_fields

def test_successful_purchase_with_stored_credential
stored_credential_options = {
inital_transaction: false,
reason_type: 'recurring',
network_transaction_id: '123abcdefg'
initial_transaction: true,
reason_type: 'recurring'
}
response = stub_comms do
@gateway.purchase(@amount, @credit_card, @options.merge({ stored_credential: stored_credential_options }))
end.check_request do |_endpoint, data, _headers|
request = JSON.parse(data)['transaction']
assert_equal request['cardOnFile']['usageIndicator'], '02'
assert_equal request['cardOnFile']['usageIndicator'], '01'
assert_equal request['cardOnFile']['indicator'], '01'
assert_equal request['cardOnFile']['scheduledIndicator'], '01'
assert_equal request['cardOnFile']['transactionId'], stored_credential_options[:network_transaction_id]
assert_nil request['cardOnFile']['transactionId']
end.respond_with(successful_purchase_response)

assert response.success?
assert_equal response.message, 'Transaction successful'

stored_credential_options = {
reason_type: 'recurring',
network_transaction_id: '123abcdefg'
}
stub_comms do
@gateway.purchase(@amount, @credit_card, @options.merge({ stored_credential: stored_credential_options }))
end.check_request do |_endpoint, data, _headers|
request = JSON.parse(data)['transaction']
assert_equal request['cardOnFile']['usageIndicator'], '02'
assert_equal request['cardOnFile']['indicator'], '01'
assert_equal request['cardOnFile']['scheduledIndicator'], '01'
assert_equal request['cardOnFile']['transactionId'], stored_credential_options[:network_transaction_id]
end.respond_with(successful_purchase_response)
end

def test_successful_store
Expand Down
90 changes: 90 additions & 0 deletions test/unit/multi_response_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -172,4 +172,94 @@ def test_handles_ignores_optional_request_result

assert m.success?
end

def test_handles_responses_with_only_one_with_avs_and_cvv_result
r1 = Response.new(true, '1', {}, { avs_result: AVSResult.new(code: 'Y'), cvv_result: 'M' })
r2 = Response.new(true, '2', {})
m = MultiResponse.run do |r|
r.process { r1 }
r.process { r2 }
end
assert_equal [r1, r2], m.responses
assert_equal m.avs_result, { 'code' => 'Y', 'message' => 'Street address and 5-digit postal code match.', 'street_match' => 'Y', 'postal_match' => 'Y' }
assert_equal m.cvv_result, { 'code' => 'M', 'message' => 'CVV matches' }
end

def test_handles_responses_using_last_response_cvv_and_avs_result
r1 = Response.new(true, '1', {}, { avs_result: AVSResult.new(code: 'Y'), cvv_result: 'M' })
r2 = Response.new(true, '1', {}, { avs_result: AVSResult.new(code: 'B'), cvv_result: 'N' })
m = MultiResponse.run do |r|
r.process { r1 }
r.process { r2 }
end
assert_equal [r1, r2], m.responses
assert_equal m.avs_result, { 'code' => 'B', 'message' => 'Street address matches, but postal code not verified.', 'street_match' => 'Y', 'postal_match' => nil }
assert_equal m.cvv_result, { 'code' => 'N', 'message' => 'CVV does not match' }
end

def test_handles_responses_using_first_response_cvv_and_avs_result
r1 = Response.new(true, '1', {}, { avs_result: AVSResult.new(code: 'Y'), cvv_result: 'M' })
r2 = Response.new(true, '1', {}, { avs_result: AVSResult.new(code: 'B'), cvv_result: 'N' })
m = MultiResponse.run(:use_first_response) do |r|
r.process { r1 }
r.process { r2 }
end
assert_equal [r1, r2], m.responses
assert_equal m.avs_result, { 'code' => 'Y', 'message' => 'Street address and 5-digit postal code match.', 'street_match' => 'Y', 'postal_match' => 'Y' }
assert_equal m.cvv_result, { 'code' => 'M', 'message' => 'CVV matches' }
end

def test_handles_responses_using_first_response_cvv_that_no_has_cvv_and_avs_result
r1 = Response.new(true, '1', {})
r2 = Response.new(true, '1', {}, { avs_result: AVSResult.new(code: 'B'), cvv_result: 'N' })
m = MultiResponse.run(:use_first_response) do |r|
r.process { r1 }
r.process { r2 }
end
assert_equal [r1, r2], m.responses
assert_equal m.avs_result, { 'code' => nil, 'message' => nil, 'street_match' => nil, 'postal_match' => nil }
assert_equal m.cvv_result, { 'code' => nil, 'message' => nil }
end

def test_handles_response_with_avs_and_without_cvv_result
r1 = Response.new(true, '1', {}, { avs_result: AVSResult.new(code: 'X'), cvv_result: CVVResult.new(nil) })
r2 = Response.new(true, '2', {})
m = MultiResponse.run do |r|
r.process { r1 }
r.process { r2 }
end
assert_equal [r1, r2], m.responses
assert_equal m.avs_result, { 'code' => 'X', 'message' => 'Street address and 9-digit postal code match.', 'street_match' => 'Y', 'postal_match' => 'Y' }
assert_equal m.cvv_result, { 'code' => nil, 'message' => nil }
end

def test_handles_response_avs_and_cvv_result_with_wrong_values_avs_and_cvv_code
r1 = Response.new(true, '1', {}, { avs_result: AVSResult.new(code: '1234567'), cvv_result: CVVResult.new('987654') })
r2 = Response.new(true, '2', {})
m = MultiResponse.run do |r|
r.process { r1 }
r.process { r2 }
end
assert_equal [r1, r2], m.responses
assert_equal m.avs_result, { 'code' => '1234567', 'message' => nil, 'street_match' => nil, 'postal_match' => nil }
assert_equal m.cvv_result, { 'code' => '987654', 'message' => nil }
end

def test_handles_response_without_avs_and_cvv_result
r1 = Response.new(true, '1', {})
r2 = Response.new(true, '2', {})
m = MultiResponse.run do |r|
r.process { r1 }
r.process { r2 }
end
assert_equal [r1, r2], m.responses
assert_equal m.avs_result, { 'code' => nil, 'message' => nil, 'street_match' => nil, 'postal_match' => nil }
assert_equal m.cvv_result, { 'code' => nil, 'message' => nil }
end

def test_handles_responses_avs_and_cvv_result_with_no_responses_provideds
m = MultiResponse.new
assert_equal m.avs_result, nil
assert_equal m.cvv_result, nil
end
end