Skip to content

Commit f7ca1f9

Browse files
committed
Airwallex: Add support for original_transaction_id
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
1 parent a9d5b3a commit f7ca1f9

File tree

4 files changed

+165
-68
lines changed

4 files changed

+165
-68
lines changed

CHANGELOG

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@
7575
* Multiple Gateways: Resolve when/case bug [naashton] #4399
7676
* Airwallex: Add 3DS MPI support [drkjc] #4395
7777
* Add Cartes Bancaires card bin ranges [leahriffell] #4398
78+
* Airwallex: Add support for `original_transaction_id` field [drkjc] #4401
7879

7980
== Version 1.125.0 (January 20, 2022)
8081
* Wompi: support gateway [therufs] #4173

lib/active_merchant/billing/gateways/airwallex.rb

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,7 @@ def add_stored_credential(post, options)
232232
return unless stored_credential = options[:stored_credential]
233233

234234
external_recurring_data = post[:external_recurring_data] = {}
235+
original_transaction_id = add_original_transaction_id(options)
235236

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

243-
external_recurring_data[:original_transaction_id] = stored_credential.dig(:network_transaction_id)
244+
external_recurring_data[:original_transaction_id] = original_transaction_id || stored_credential.dig(:network_transaction_id)
244245
external_recurring_data[:triggered_by] = stored_credential.dig(:initiator) == 'cardholder' ? 'customer' : 'merchant'
245246
end
246247

@@ -279,6 +280,12 @@ def three_ds_version_specific_fields(three_d_secure)
279280
end
280281
end
281282

283+
def add_original_transaction_id(options)
284+
return unless options[:auto_capture] == false || original_transaction_id = options[:original_transaction_id]
285+
286+
original_transaction_id
287+
end
288+
282289
def authorization_only?(options = {})
283290
options.include?(:auto_capture) && options[:auto_capture] == false
284291
end

test/remote/gateways/remote_airwallex_test.rb

Lines changed: 44 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ def setup
1010
@credit_card = credit_card('4111 1111 1111 1111')
1111
@declined_card = credit_card('2223 0000 1018 1375')
1212
@options = { return_url: 'https://example.com', description: 'a test transaction' }
13+
@stored_credential_cit_options = { initial_transaction: true, initiator: 'cardholder', reason_type: 'recurring', network_transaction_id: nil }
14+
@stored_credential_mit_options = { initial_transaction: false, initiator: 'merchant', reason_type: 'recurring', network_transaction_id: '123456789012345' }
1315
end
1416

1517
def test_successful_purchase
@@ -131,52 +133,62 @@ def test_failed_verify
131133
assert_match %r{Invalid card number}, response.message
132134
end
133135

134-
def test_successful_cit_transaction_with_recurring_stored_credential
135-
stored_credential_params = {
136-
initial_transaction: true,
137-
reason_type: 'recurring',
138-
initiator: 'cardholder',
139-
network_transaction_id: nil
140-
}
136+
def test_successful_cit_with_recurring_stored_credential
137+
auth = @gateway.authorize(@amount, @credit_card, @options.merge(stored_credential: @stored_credential_cit_options))
138+
assert_success auth
139+
end
141140

142-
auth = @gateway.authorize(@amount, @credit_card, @options.merge(stored_credential: stored_credential_params))
141+
def test_successful_mit_with_recurring_stored_credential
142+
auth = @gateway.authorize(@amount, @credit_card, @options.merge(stored_credential: @stored_credential_cit_options))
143143
assert_success auth
144+
145+
purchase = @gateway.purchase(@amount, @credit_card, @options.merge(stored_credential: @stored_credential_mit_options))
146+
assert_success purchase
144147
end
145148

146-
def test_successful_mit_transaction_with_recurring_stored_credential
147-
stored_credential_params = {
148-
initial_transaction: false,
149-
reason_type: 'recurring',
150-
initiator: 'merchant',
151-
network_transaction_id: 'MCC123ABC0101'
152-
}
149+
def test_successful_mit_with_unscheduled_stored_credential
150+
@stored_credential_cit_options[:reason_type] = 'unscheduled'
151+
@stored_credential_mit_options[:reason_type] = 'unscheduled'
153152

154-
auth = @gateway.authorize(@amount, @credit_card, @options.merge(stored_credential: stored_credential_params))
153+
auth = @gateway.authorize(@amount, @credit_card, @options.merge(stored_credential: @stored_credential_cit_options))
155154
assert_success auth
155+
156+
purchase = @gateway.purchase(@amount, @credit_card, @options.merge(stored_credential: @stored_credential_mit_options))
157+
assert_success purchase
156158
end
157159

158-
def test_successful_mit_transaction_with_unscheduled_stored_credential
159-
stored_credential_params = {
160-
initial_transaction: false,
161-
reason_type: 'unscheduled',
162-
initiator: 'merchant',
163-
network_transaction_id: 'MCC123ABC0101'
164-
}
160+
def test_successful_mit_with_installment_stored_credential
161+
@stored_credential_cit_options[:reason_type] = 'installment'
162+
@stored_credential_mit_options[:reason_type] = 'installment'
165163

166-
auth = @gateway.authorize(@amount, @credit_card, @options.merge(stored_credential: stored_credential_params))
164+
auth = @gateway.authorize(@amount, @credit_card, @options.merge(stored_credential: @stored_credential_cit_options))
167165
assert_success auth
166+
167+
purchase = @gateway.purchase(@amount, @credit_card, @options.merge(stored_credential: @stored_credential_mit_options))
168+
assert_success purchase
168169
end
169170

170-
def test_successful_mit_transaction_with_installment_stored_credential
171-
stored_credential_params = {
172-
initial_transaction: false,
173-
reason_type: 'installment',
174-
initiator: 'cardholder',
175-
network_transaction_id: 'MCC123ABC0101'
176-
}
171+
def test_successful_mit_with_original_transaction_id
172+
mastercard = credit_card('2223 0000 1018 1375', { brand: 'master' })
173+
174+
auth = @gateway.authorize(@amount, mastercard, @options.merge(stored_credential: @stored_credential_cit_options))
175+
assert_success auth
176+
177+
@options[:original_transaction_id] = 'MCC123ABC0101'
177178

178-
auth = @gateway.authorize(@amount, @credit_card, @options.merge(stored_credential: stored_credential_params))
179+
purchase = @gateway.purchase(@amount, mastercard, @options.merge(stored_credential: @stored_credential_mit_options))
180+
assert_success purchase
181+
end
182+
183+
def test_failed_mit_with_unapproved_ntid
184+
auth = @gateway.authorize(@amount, @credit_card, @options.merge(stored_credential: @stored_credential_cit_options))
179185
assert_success auth
186+
187+
@stored_credential_mit_options[:network_transaction_id] = 'abc123'
188+
189+
purchase = @gateway.purchase(@amount, @credit_card, @options.merge(stored_credential: @stored_credential_mit_options))
190+
assert_failure purchase
191+
assert_equal 'external_recurring_data.original_transaction_id should be 13-15 characters long', purchase.message
180192
end
181193

182194
def test_transcript_scrubbing

test/unit/gateways/airwallex_test.rb

Lines changed: 112 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ def setup
2424
billing_address: address,
2525
return_url: 'https://example.com'
2626
}
27+
28+
@stored_credential_cit_options = { initial_transaction: true, initiator: 'cardholder', reason_type: 'recurring', network_transaction_id: nil }
29+
@stored_credential_mit_options = { initial_transaction: false, initiator: 'merchant', reason_type: 'recurring', network_transaction_id: '123456789012345' }
2730
end
2831

2932
def test_gateway_has_access_token
@@ -301,68 +304,138 @@ def test_invalid_login
301304
end
302305

303306
def test_successful_cit_with_stored_credential
304-
stored_credential_params = {
305-
initial_transaction: true,
306-
reason_type: 'recurring',
307-
initiator: 'cardholder',
308-
network_transaction_id: nil
309-
}
310-
311307
auth = stub_comms do
312-
@gateway.authorize(@amount, @credit_card, @options.merge({ stored_credential: stored_credential_params }))
308+
@gateway.authorize(@amount, @credit_card, @options.merge!({ stored_credential: @stored_credential_cit_options }))
313309
end.check_request do |endpoint, data, _headers|
314-
# This conditional asserts after the initial setup call is made
315-
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'
310+
# This conditional runs assertions after the initial setup call is made
311+
unless endpoint == 'https://api-demo.airwallex.com/api/v1/pa/payment_intents/create'
312+
assert_match(/"external_recurring_data\"/, data)
313+
assert_match(/"merchant_trigger_reason\":\"scheduled\"/, data)
314+
assert_match(/"original_transaction_id\":null,/, data)
315+
assert_match(/"triggered_by\":\"customer\"/, data)
316+
end
316317
end.respond_with(successful_authorize_response)
317318
assert_success auth
318319
end
319320

320321
def test_successful_mit_with_recurring_stored_credential
321-
stored_credential_params = {
322-
initial_transaction: false,
323-
reason_type: 'recurring',
324-
initiator: 'merchant',
325-
network_transaction_id: 'MCC123ABC0101'
326-
}
327-
328322
auth = stub_comms do
329-
@gateway.authorize(@amount, @credit_card, @options.merge({ stored_credential: stored_credential_params }))
323+
@gateway.authorize(@amount, @credit_card, @options.merge!({ stored_credential: @stored_credential_cit_options }))
330324
end.check_request do |endpoint, data, _headers|
331-
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'
325+
unless endpoint == 'https://api-demo.airwallex.com/api/v1/pa/payment_intents/create'
326+
assert_match(/"external_recurring_data\"/, data)
327+
assert_match(/"merchant_trigger_reason\":\"scheduled\"/, data)
328+
assert_match(/"original_transaction_id\":null,/, data)
329+
assert_match(/"triggered_by\":\"customer\"/, data)
330+
end
332331
end.respond_with(successful_authorize_response)
333332
assert_success auth
333+
334+
purchase = stub_comms do
335+
@gateway.purchase(@amount, @credit_card, @options.merge!({ stored_credential: @stored_credential_mit_options }))
336+
end.check_request do |endpoint, data, _headers|
337+
unless endpoint == 'https://api-demo.airwallex.com/api/v1/pa/payment_intents/create'
338+
assert_match(/"external_recurring_data\"/, data)
339+
assert_match(/"merchant_trigger_reason\":\"scheduled\"/, data)
340+
assert_match(/"original_transaction_id\":\"123456789012345\"/, data)
341+
assert_match(/"triggered_by\":\"merchant\"/, data)
342+
end
343+
end.respond_with(successful_purchase_response)
344+
assert_success purchase
334345
end
335346

336347
def test_successful_mit_with_unscheduled_stored_credential
337-
stored_credential_params = {
338-
initial_transaction: false,
339-
reason_type: 'unscheduled',
340-
initiator: 'merchant',
341-
network_transaction_id: 'MCC123ABC0101'
342-
}
348+
@stored_credential_cit_options[:reason_type] = 'unscheduled'
349+
@stored_credential_mit_options[:reason_type] = 'unscheduled'
343350

344351
auth = stub_comms do
345-
@gateway.authorize(@amount, @credit_card, @options.merge({ stored_credential: stored_credential_params }))
352+
@gateway.authorize(@amount, @credit_card, @options.merge!({ stored_credential: @stored_credential_cit_options }))
346353
end.check_request do |endpoint, data, _headers|
347-
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'
354+
unless endpoint == 'https://api-demo.airwallex.com/api/v1/pa/payment_intents/create'
355+
assert_match(/"external_recurring_data\"/, data)
356+
assert_match(/"merchant_trigger_reason\":\"unscheduled\"/, data)
357+
assert_match(/"original_transaction_id\":null,/, data)
358+
assert_match(/"triggered_by\":\"customer\"/, data)
359+
end
348360
end.respond_with(successful_authorize_response)
349361
assert_success auth
362+
363+
purchase = stub_comms do
364+
@gateway.purchase(@amount, @credit_card, @options.merge!({ stored_credential: @stored_credential_mit_options }))
365+
end.check_request do |endpoint, data, _headers|
366+
unless endpoint == 'https://api-demo.airwallex.com/api/v1/pa/payment_intents/create'
367+
assert_match(/"external_recurring_data\"/, data)
368+
assert_match(/"merchant_trigger_reason\":\"unscheduled\"/, data)
369+
assert_match(/"original_transaction_id\":\"123456789012345\"/, data)
370+
assert_match(/"triggered_by\":\"merchant\"/, data)
371+
end
372+
end.respond_with(successful_purchase_response)
373+
assert_success purchase
350374
end
351375

352376
def test_successful_mit_with_installment_stored_credential
353-
stored_credential_params = {
354-
initial_transaction: false,
355-
reason_type: 'installment',
356-
initiator: 'merchant',
357-
network_transaction_id: 'MCC123ABC0101'
358-
}
377+
@stored_credential_cit_options[:reason_type] = 'installment'
378+
@stored_credential_mit_options[:reason_type] = 'installment'
379+
380+
auth = stub_comms do
381+
@gateway.authorize(@amount, @credit_card, @options.merge!({ stored_credential: @stored_credential_cit_options }))
382+
end.check_request do |endpoint, data, _headers|
383+
unless endpoint == 'https://api-demo.airwallex.com/api/v1/pa/payment_intents/create'
384+
assert_match(/"external_recurring_data\"/, data)
385+
assert_match(/"merchant_trigger_reason\":\"scheduled\"/, data)
386+
assert_match(/"original_transaction_id\":null,/, data)
387+
assert_match(/"triggered_by\":\"customer\"/, data)
388+
end
389+
end.respond_with(successful_authorize_response)
390+
assert_success auth
391+
392+
purchase = stub_comms do
393+
@gateway.purchase(@amount, @credit_card, @options.merge!({ stored_credential: @stored_credential_mit_options }))
394+
end.check_request do |endpoint, data, _headers|
395+
unless endpoint == 'https://api-demo.airwallex.com/api/v1/pa/payment_intents/create'
396+
assert_match(/"external_recurring_data\"/, data)
397+
assert_match(/"merchant_trigger_reason\":\"scheduled\"/, data)
398+
assert_match(/"original_transaction_id\":\"123456789012345\"/, data)
399+
assert_match(/"triggered_by\":\"merchant\"/, data)
400+
end
401+
end.respond_with(successful_purchase_response)
402+
assert_success purchase
403+
end
404+
405+
def test_successful_mit_with_original_transaction_id
406+
mastercard = credit_card('2223 0000 1018 1375', { brand: 'master' })
407+
@options[:original_transaction_id] = 'MCC123ABC0101'
359408

360409
auth = stub_comms do
361-
@gateway.authorize(@amount, @credit_card, @options.merge({ stored_credential: stored_credential_params }))
410+
@gateway.authorize(@amount, mastercard, @options.merge!({ stored_credential: @stored_credential_cit_options }))
362411
end.check_request do |endpoint, data, _headers|
363-
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'
412+
unless endpoint == 'https://api-demo.airwallex.com/api/v1/pa/payment_intents/create'
413+
assert_match(/"external_recurring_data\"/, data)
414+
assert_match(/"merchant_trigger_reason\":\"scheduled\"/, data)
415+
assert_match(/"original_transaction_id\":null,/, data)
416+
assert_match(/"triggered_by\":\"customer\"/, data)
417+
end
364418
end.respond_with(successful_authorize_response)
365419
assert_success auth
420+
421+
purchase = stub_comms do
422+
@gateway.purchase(@amount, mastercard, @options.merge!({ stored_credential: @stored_credential_mit_options }))
423+
end.check_request do |endpoint, data, _headers|
424+
unless endpoint == 'https://api-demo.airwallex.com/api/v1/pa/payment_intents/create'
425+
assert_match(/"external_recurring_data\"/, data)
426+
assert_match(/"merchant_trigger_reason\":\"scheduled\"/, data)
427+
assert_match(/"original_transaction_id\":\"MCC123ABC0101\"/, data)
428+
assert_match(/"triggered_by\":\"merchant\"/, data)
429+
end
430+
end.respond_with(successful_purchase_response)
431+
assert_success purchase
432+
end
433+
434+
def test_failed_mit_with_unapproved_ntid
435+
@gateway.expects(:ssl_post).returns(failed_ntid_response)
436+
assert_raise ArgumentError do
437+
@gateway.authorize(@amount, @credit_card, @options.merge!({ stored_credential: @stored_credential_cit_options }))
438+
end
366439
end
367440

368441
def test_scrub
@@ -438,4 +511,8 @@ def successful_void_response
438511
def failed_void_response
439512
%({"code":"not_found","message":"The requested endpoint does not exist [/api/v1/pa/payment_intents/12345/cancel]"})
440513
end
514+
515+
def failed_ntid_response
516+
%({"code":"validation_error","source":"external_recurring_data.original_transaction_id","message":"external_recurring_data.original_transaction_id should be 13-15 characters long"})
517+
end
441518
end

0 commit comments

Comments
 (0)