Skip to content

Commit

Permalink
Airwallex: Add support for original_transaction_id
Browse files Browse the repository at this point in the history
The `original_transaction_id` field allows users to manually  override the
`network_transaction_id`. This is useful when testing MITs using Stored
Credentials on Airwallex because they only allow specific values to be
passed which they do not return, and would normally be passed automatically in a
standard MIT Stored Credentials flow.

This PR also cleans up remote and unit tests for Airwallex stored creds.
CE-2560

Unit:
33 tests, 176 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications
100% passed

Remote:
27 tests, 64 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications
100% passed
  • Loading branch information
drkjc committed Apr 15, 2022
1 parent a9d5b3a commit f7ca1f9
Show file tree
Hide file tree
Showing 4 changed files with 165 additions and 68 deletions.
1 change: 1 addition & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
* Multiple Gateways: Resolve when/case bug [naashton] #4399
* Airwallex: Add 3DS MPI support [drkjc] #4395
* Add Cartes Bancaires card bin ranges [leahriffell] #4398
* Airwallex: Add support for `original_transaction_id` field [drkjc] #4401

== Version 1.125.0 (January 20, 2022)
* Wompi: support gateway [therufs] #4173
Expand Down
9 changes: 8 additions & 1 deletion lib/active_merchant/billing/gateways/airwallex.rb
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,7 @@ def add_stored_credential(post, options)
return unless stored_credential = options[:stored_credential]

external_recurring_data = post[:external_recurring_data] = {}
original_transaction_id = add_original_transaction_id(options)

case stored_credential.dig(:reason_type)
when 'recurring', 'installment'
Expand All @@ -240,7 +241,7 @@ def add_stored_credential(post, options)
external_recurring_data[:merchant_trigger_reason] = 'unscheduled'
end

external_recurring_data[:original_transaction_id] = stored_credential.dig(:network_transaction_id)
external_recurring_data[:original_transaction_id] = original_transaction_id || stored_credential.dig(:network_transaction_id)
external_recurring_data[:triggered_by] = stored_credential.dig(:initiator) == 'cardholder' ? 'customer' : 'merchant'
end

Expand Down Expand Up @@ -279,6 +280,12 @@ def three_ds_version_specific_fields(three_d_secure)
end
end

def add_original_transaction_id(options)
return unless options[:auto_capture] == false || original_transaction_id = options[:original_transaction_id]

original_transaction_id
end

def authorization_only?(options = {})
options.include?(:auto_capture) && options[:auto_capture] == false
end
Expand Down
76 changes: 44 additions & 32 deletions test/remote/gateways/remote_airwallex_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ def setup
@credit_card = credit_card('4111 1111 1111 1111')
@declined_card = credit_card('2223 0000 1018 1375')
@options = { return_url: 'https://example.com', description: 'a test transaction' }
@stored_credential_cit_options = { initial_transaction: true, initiator: 'cardholder', reason_type: 'recurring', network_transaction_id: nil }
@stored_credential_mit_options = { initial_transaction: false, initiator: 'merchant', reason_type: 'recurring', network_transaction_id: '123456789012345' }
end

def test_successful_purchase
Expand Down Expand Up @@ -131,52 +133,62 @@ def test_failed_verify
assert_match %r{Invalid card number}, response.message
end

def test_successful_cit_transaction_with_recurring_stored_credential
stored_credential_params = {
initial_transaction: true,
reason_type: 'recurring',
initiator: 'cardholder',
network_transaction_id: nil
}
def test_successful_cit_with_recurring_stored_credential
auth = @gateway.authorize(@amount, @credit_card, @options.merge(stored_credential: @stored_credential_cit_options))
assert_success auth
end

auth = @gateway.authorize(@amount, @credit_card, @options.merge(stored_credential: stored_credential_params))
def test_successful_mit_with_recurring_stored_credential
auth = @gateway.authorize(@amount, @credit_card, @options.merge(stored_credential: @stored_credential_cit_options))
assert_success auth

purchase = @gateway.purchase(@amount, @credit_card, @options.merge(stored_credential: @stored_credential_mit_options))
assert_success purchase
end

def test_successful_mit_transaction_with_recurring_stored_credential
stored_credential_params = {
initial_transaction: false,
reason_type: 'recurring',
initiator: 'merchant',
network_transaction_id: 'MCC123ABC0101'
}
def test_successful_mit_with_unscheduled_stored_credential
@stored_credential_cit_options[:reason_type] = 'unscheduled'
@stored_credential_mit_options[:reason_type] = 'unscheduled'

auth = @gateway.authorize(@amount, @credit_card, @options.merge(stored_credential: stored_credential_params))
auth = @gateway.authorize(@amount, @credit_card, @options.merge(stored_credential: @stored_credential_cit_options))
assert_success auth

purchase = @gateway.purchase(@amount, @credit_card, @options.merge(stored_credential: @stored_credential_mit_options))
assert_success purchase
end

def test_successful_mit_transaction_with_unscheduled_stored_credential
stored_credential_params = {
initial_transaction: false,
reason_type: 'unscheduled',
initiator: 'merchant',
network_transaction_id: 'MCC123ABC0101'
}
def test_successful_mit_with_installment_stored_credential
@stored_credential_cit_options[:reason_type] = 'installment'
@stored_credential_mit_options[:reason_type] = 'installment'

auth = @gateway.authorize(@amount, @credit_card, @options.merge(stored_credential: stored_credential_params))
auth = @gateway.authorize(@amount, @credit_card, @options.merge(stored_credential: @stored_credential_cit_options))
assert_success auth

purchase = @gateway.purchase(@amount, @credit_card, @options.merge(stored_credential: @stored_credential_mit_options))
assert_success purchase
end

def test_successful_mit_transaction_with_installment_stored_credential
stored_credential_params = {
initial_transaction: false,
reason_type: 'installment',
initiator: 'cardholder',
network_transaction_id: 'MCC123ABC0101'
}
def test_successful_mit_with_original_transaction_id
mastercard = credit_card('2223 0000 1018 1375', { brand: 'master' })

auth = @gateway.authorize(@amount, mastercard, @options.merge(stored_credential: @stored_credential_cit_options))
assert_success auth

@options[:original_transaction_id] = 'MCC123ABC0101'

auth = @gateway.authorize(@amount, @credit_card, @options.merge(stored_credential: stored_credential_params))
purchase = @gateway.purchase(@amount, mastercard, @options.merge(stored_credential: @stored_credential_mit_options))
assert_success purchase
end

def test_failed_mit_with_unapproved_ntid
auth = @gateway.authorize(@amount, @credit_card, @options.merge(stored_credential: @stored_credential_cit_options))
assert_success auth

@stored_credential_mit_options[:network_transaction_id] = 'abc123'

purchase = @gateway.purchase(@amount, @credit_card, @options.merge(stored_credential: @stored_credential_mit_options))
assert_failure purchase
assert_equal 'external_recurring_data.original_transaction_id should be 13-15 characters long', purchase.message
end

def test_transcript_scrubbing
Expand Down
147 changes: 112 additions & 35 deletions test/unit/gateways/airwallex_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ def setup
billing_address: address,
return_url: 'https://example.com'
}

@stored_credential_cit_options = { initial_transaction: true, initiator: 'cardholder', reason_type: 'recurring', network_transaction_id: nil }
@stored_credential_mit_options = { initial_transaction: false, initiator: 'merchant', reason_type: 'recurring', network_transaction_id: '123456789012345' }
end

def test_gateway_has_access_token
Expand Down Expand Up @@ -301,68 +304,138 @@ def test_invalid_login
end

def test_successful_cit_with_stored_credential
stored_credential_params = {
initial_transaction: true,
reason_type: 'recurring',
initiator: 'cardholder',
network_transaction_id: nil
}

auth = stub_comms do
@gateway.authorize(@amount, @credit_card, @options.merge({ stored_credential: stored_credential_params }))
@gateway.authorize(@amount, @credit_card, @options.merge!({ stored_credential: @stored_credential_cit_options }))
end.check_request do |endpoint, data, _headers|
# This conditional asserts after the initial setup call is made
assert_match(/"external_recurring_data\":{\"merchant_trigger_reason\":\"scheduled\",\"original_transaction_id\":null,\"triggered_by\":\"customer\"}/, data) if endpoint != 'https://api-demo.airwallex.com/api/v1/pa/payment_intents/create'
# This conditional runs assertions after the initial setup call is made
unless endpoint == 'https://api-demo.airwallex.com/api/v1/pa/payment_intents/create'
assert_match(/"external_recurring_data\"/, data)
assert_match(/"merchant_trigger_reason\":\"scheduled\"/, data)
assert_match(/"original_transaction_id\":null,/, data)
assert_match(/"triggered_by\":\"customer\"/, data)
end
end.respond_with(successful_authorize_response)
assert_success auth
end

def test_successful_mit_with_recurring_stored_credential
stored_credential_params = {
initial_transaction: false,
reason_type: 'recurring',
initiator: 'merchant',
network_transaction_id: 'MCC123ABC0101'
}

auth = stub_comms do
@gateway.authorize(@amount, @credit_card, @options.merge({ stored_credential: stored_credential_params }))
@gateway.authorize(@amount, @credit_card, @options.merge!({ stored_credential: @stored_credential_cit_options }))
end.check_request do |endpoint, data, _headers|
assert_match(/"external_recurring_data\":{\"merchant_trigger_reason\":\"scheduled\",\"original_transaction_id\":\"MCC123ABC0101\",\"triggered_by\":\"merchant\"}/, data) if endpoint != 'https://api-demo.airwallex.com/api/v1/pa/payment_intents/create'
unless endpoint == 'https://api-demo.airwallex.com/api/v1/pa/payment_intents/create'
assert_match(/"external_recurring_data\"/, data)
assert_match(/"merchant_trigger_reason\":\"scheduled\"/, data)
assert_match(/"original_transaction_id\":null,/, data)
assert_match(/"triggered_by\":\"customer\"/, data)
end
end.respond_with(successful_authorize_response)
assert_success auth

purchase = stub_comms do
@gateway.purchase(@amount, @credit_card, @options.merge!({ stored_credential: @stored_credential_mit_options }))
end.check_request do |endpoint, data, _headers|
unless endpoint == 'https://api-demo.airwallex.com/api/v1/pa/payment_intents/create'
assert_match(/"external_recurring_data\"/, data)
assert_match(/"merchant_trigger_reason\":\"scheduled\"/, data)
assert_match(/"original_transaction_id\":\"123456789012345\"/, data)
assert_match(/"triggered_by\":\"merchant\"/, data)
end
end.respond_with(successful_purchase_response)
assert_success purchase
end

def test_successful_mit_with_unscheduled_stored_credential
stored_credential_params = {
initial_transaction: false,
reason_type: 'unscheduled',
initiator: 'merchant',
network_transaction_id: 'MCC123ABC0101'
}
@stored_credential_cit_options[:reason_type] = 'unscheduled'
@stored_credential_mit_options[:reason_type] = 'unscheduled'

auth = stub_comms do
@gateway.authorize(@amount, @credit_card, @options.merge({ stored_credential: stored_credential_params }))
@gateway.authorize(@amount, @credit_card, @options.merge!({ stored_credential: @stored_credential_cit_options }))
end.check_request do |endpoint, data, _headers|
assert_match(/"external_recurring_data\":{\"merchant_trigger_reason\":\"unscheduled\",\"original_transaction_id\":\"MCC123ABC0101\",\"triggered_by\":\"merchant\"}/, data) if endpoint != 'https://api-demo.airwallex.com/api/v1/pa/payment_intents/create'
unless endpoint == 'https://api-demo.airwallex.com/api/v1/pa/payment_intents/create'
assert_match(/"external_recurring_data\"/, data)
assert_match(/"merchant_trigger_reason\":\"unscheduled\"/, data)
assert_match(/"original_transaction_id\":null,/, data)
assert_match(/"triggered_by\":\"customer\"/, data)
end
end.respond_with(successful_authorize_response)
assert_success auth

purchase = stub_comms do
@gateway.purchase(@amount, @credit_card, @options.merge!({ stored_credential: @stored_credential_mit_options }))
end.check_request do |endpoint, data, _headers|
unless endpoint == 'https://api-demo.airwallex.com/api/v1/pa/payment_intents/create'
assert_match(/"external_recurring_data\"/, data)
assert_match(/"merchant_trigger_reason\":\"unscheduled\"/, data)
assert_match(/"original_transaction_id\":\"123456789012345\"/, data)
assert_match(/"triggered_by\":\"merchant\"/, data)
end
end.respond_with(successful_purchase_response)
assert_success purchase
end

def test_successful_mit_with_installment_stored_credential
stored_credential_params = {
initial_transaction: false,
reason_type: 'installment',
initiator: 'merchant',
network_transaction_id: 'MCC123ABC0101'
}
@stored_credential_cit_options[:reason_type] = 'installment'
@stored_credential_mit_options[:reason_type] = 'installment'

auth = stub_comms do
@gateway.authorize(@amount, @credit_card, @options.merge!({ stored_credential: @stored_credential_cit_options }))
end.check_request do |endpoint, data, _headers|
unless endpoint == 'https://api-demo.airwallex.com/api/v1/pa/payment_intents/create'
assert_match(/"external_recurring_data\"/, data)
assert_match(/"merchant_trigger_reason\":\"scheduled\"/, data)
assert_match(/"original_transaction_id\":null,/, data)
assert_match(/"triggered_by\":\"customer\"/, data)
end
end.respond_with(successful_authorize_response)
assert_success auth

purchase = stub_comms do
@gateway.purchase(@amount, @credit_card, @options.merge!({ stored_credential: @stored_credential_mit_options }))
end.check_request do |endpoint, data, _headers|
unless endpoint == 'https://api-demo.airwallex.com/api/v1/pa/payment_intents/create'
assert_match(/"external_recurring_data\"/, data)
assert_match(/"merchant_trigger_reason\":\"scheduled\"/, data)
assert_match(/"original_transaction_id\":\"123456789012345\"/, data)
assert_match(/"triggered_by\":\"merchant\"/, data)
end
end.respond_with(successful_purchase_response)
assert_success purchase
end

def test_successful_mit_with_original_transaction_id
mastercard = credit_card('2223 0000 1018 1375', { brand: 'master' })
@options[:original_transaction_id] = 'MCC123ABC0101'

auth = stub_comms do
@gateway.authorize(@amount, @credit_card, @options.merge({ stored_credential: stored_credential_params }))
@gateway.authorize(@amount, mastercard, @options.merge!({ stored_credential: @stored_credential_cit_options }))
end.check_request do |endpoint, data, _headers|
assert_match(/"external_recurring_data\":{\"merchant_trigger_reason\":\"scheduled\",\"original_transaction_id\":\"MCC123ABC0101\",\"triggered_by\":\"merchant\"}/, data) if endpoint != 'https://api-demo.airwallex.com/api/v1/pa/payment_intents/create'
unless endpoint == 'https://api-demo.airwallex.com/api/v1/pa/payment_intents/create'
assert_match(/"external_recurring_data\"/, data)
assert_match(/"merchant_trigger_reason\":\"scheduled\"/, data)
assert_match(/"original_transaction_id\":null,/, data)
assert_match(/"triggered_by\":\"customer\"/, data)
end
end.respond_with(successful_authorize_response)
assert_success auth

purchase = stub_comms do
@gateway.purchase(@amount, mastercard, @options.merge!({ stored_credential: @stored_credential_mit_options }))
end.check_request do |endpoint, data, _headers|
unless endpoint == 'https://api-demo.airwallex.com/api/v1/pa/payment_intents/create'
assert_match(/"external_recurring_data\"/, data)
assert_match(/"merchant_trigger_reason\":\"scheduled\"/, data)
assert_match(/"original_transaction_id\":\"MCC123ABC0101\"/, data)
assert_match(/"triggered_by\":\"merchant\"/, data)
end
end.respond_with(successful_purchase_response)
assert_success purchase
end

def test_failed_mit_with_unapproved_ntid
@gateway.expects(:ssl_post).returns(failed_ntid_response)
assert_raise ArgumentError do
@gateway.authorize(@amount, @credit_card, @options.merge!({ stored_credential: @stored_credential_cit_options }))
end
end

def test_scrub
Expand Down Expand Up @@ -438,4 +511,8 @@ def successful_void_response
def failed_void_response
%({"code":"not_found","message":"The requested endpoint does not exist [/api/v1/pa/payment_intents/12345/cancel]"})
end

def failed_ntid_response
%({"code":"validation_error","source":"external_recurring_data.original_transaction_id","message":"external_recurring_data.original_transaction_id should be 13-15 characters long"})
end
end

0 comments on commit f7ca1f9

Please sign in to comment.