From 5232f001fcce6919e508d2850371786f9a7e3a7f Mon Sep 17 00:00:00 2001 From: Edgar Villamarin Date: Tue, 14 May 2024 09:10:36 -0500 Subject: [PATCH 1/2] Flex Charge: Add support for TPV store Test summary: Local: 5898 tests, 79569 assertions, 0 failures, 17 errors, 0 pendings, 0 omissions, 0 notifications 99.7118% passed Unit: 12 tests, 58 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications 100% passed Remote: 13 tests, 34 assertions, 1 failures, 0 errors, 0 pendings, 1 omissions, 0 notifications 91.6667% passed --- CHANGELOG | 1 + .../billing/gateways/flex_charge.rb | 60 +++++++++---- .../gateways/remote_flex_charge_test.rb | 8 ++ test/unit/gateways/flex_charge_test.rb | 87 +++++++++++++++++++ 4 files changed, 139 insertions(+), 17 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 0e092e22a19..4f5b6e96aef 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,6 +2,7 @@ = ActiveMerchant CHANGELOG == HEAD +* FlexCharge: Add support for TPV store [edgarv09] #5120 * Shift4V2: Add new gateway based on SecurionPay adapter [heavyblade] #4860 * TNS: Use the specified order_id in request if available [yunnydang] #4880 * Cybersource: Support recurring apple pay [aenand] #4874 diff --git a/lib/active_merchant/billing/gateways/flex_charge.rb b/lib/active_merchant/billing/gateways/flex_charge.rb index df8e5e982a6..41f1376c7cb 100644 --- a/lib/active_merchant/billing/gateways/flex_charge.rb +++ b/lib/active_merchant/billing/gateways/flex_charge.rb @@ -15,7 +15,8 @@ class FlexChargeGateway < Gateway authenticate: 'oauth2/token', purchase: 'evaluate', sync: 'outcome', - refund: 'orders/%s/refund' + refund: 'orders/%s/refund', + store: 'tokenize' } SUCCESS_MESSAGES = %w(APPROVED CHALLENGE SUBMITTED SUCCESS).freeze @@ -43,6 +44,25 @@ def refund(money, authorization, options = {}) commit(:refund, { amountToRefund: (money.to_f / 100).round(2) }, authorization) end + def store(credit_card, options = {}) + address = options[:billing_address] || options[:address] || {} + first_name, last_name = address_names(address[:name], credit_card) + + post = { + payment_method: { + credit_card: { + first_name: first_name, + last_name: last_name, + month: credit_card.month, + year: credit_card.year, + number: credit_card.number, + verification_value: credit_card.verification_value + }.compact + } + } + commit(:store, post) + end + def supports_scrubbing? true end @@ -115,23 +135,29 @@ def add_invoice(post, money, credit_card, options) avsResultCode: options[:avs_result_code], cvvResultCode: options[:cvv_result_code], cavvResultCode: options[:cavv_result_code], - cardNotPresent: credit_card.verification_value.blank? + cardNotPresent: credit_card.is_a?(String) ? false : credit_card.verification_value.blank? }.compact end def add_payment_method(post, credit_card, address, options) - post[:paymentMethod] = { - holderName: credit_card.name, - cardType: 'CREDIT', - cardBrand: credit_card.brand&.upcase, - cardCountry: address[:country], - expirationMonth: credit_card.month, - expirationYear: credit_card.year, - cardBinNumber: credit_card.number[0..5], - cardLast4Digits: credit_card.number[-4..-1], - cardNumber: credit_card.number, - Token: false - }.compact + payment_method = case credit_card + when String + { Token: true, cardNumber: credit_card } + else + { + holderName: credit_card.name, + cardType: 'CREDIT', + cardBrand: credit_card.brand&.upcase, + cardCountry: address[:country], + expirationMonth: credit_card.month, + expirationYear: credit_card.year, + cardBinNumber: credit_card.number[0..5], + cardLast4Digits: credit_card.number[-4..-1], + cardNumber: credit_card.number, + Token: false + } + end + post[:paymentMethod] = payment_method.compact end def address_names(address_name, payment_method) @@ -205,7 +231,7 @@ def api_request(action, post, authorization = nil) success_from(response), message_from(response), response, - authorization: authorization_from(response), + authorization: authorization_from(action, response), test: test?, error_code: error_code_from(response) ) @@ -228,8 +254,8 @@ def message_from(response) response[:title] || response[:responseMessage] || response[:status] end - def authorization_from(response) - response[:orderSessionKey] + def authorization_from(action, response) + action == :store ? response.dig(:transaction, :payment_method, :token) : response[:orderSessionKey] end def error_code_from(response) diff --git a/test/remote/gateways/remote_flex_charge_test.rb b/test/remote/gateways/remote_flex_charge_test.rb index c96e8e7076e..ab8a401cccc 100644 --- a/test/remote/gateways/remote_flex_charge_test.rb +++ b/test/remote/gateways/remote_flex_charge_test.rb @@ -147,6 +147,14 @@ def test_failed_fetch_access_token assert_match(/400/, error.message) end + def test_successful_purchase_with_token + store = @gateway.store(@credit_card_cit, {}) + assert_success store + + response = @gateway.purchase(@amount, store.authorization, @options) + assert_success response + end + def test_transcript_scrubbing transcript = capture_transcript(@gateway) do @gateway.purchase(@amount, @credit_card_cit, @cit_options) diff --git a/test/unit/gateways/flex_charge_test.rb b/test/unit/gateways/flex_charge_test.rb index 013122c6cc1..8af35fe05a4 100644 --- a/test/unit/gateways/flex_charge_test.rb +++ b/test/unit/gateways/flex_charge_test.rb @@ -65,6 +65,11 @@ def test_build_request_url_with_id_param assert_equal @gateway.send(:url, action, id), "#{@gateway.test_url}orders/123/refund" end + def test_build_request_url_for_store + action = :store + assert_equal @gateway.send(:url, action), "#{@gateway.test_url}tokenize" + end + def test_invalid_instance error = assert_raises(ArgumentError) { FlexChargeGateway.new } assert_equal 'Missing required parameter: app_key', error.message @@ -153,6 +158,15 @@ def test_address_names_from_credit_card assert_equal 'Doe', names.last end + def test_successful_store + response = stub_comms do + @gateway.store(@credit_card, @options) + end.respond_with(successful_access_token_response, successful_store_response) + + assert_success response + assert_equal 'd3e10716-6aac-4eb8-a74d-c1a3027f1d96', response.authorization + end + private def pre_scrubbed @@ -357,6 +371,79 @@ def successful_purchase_response RESPONSE end + def successful_store_response + <<~RESPONSE + { + "transaction": { + "on_test_gateway": true, + "created_at": "2024-05-14T13:44:25.3179186Z", + "updated_at": "2024-05-14T13:44:25.3179187Z", + "succeeded": true, + "state": null, + "token": null, + "transaction_type": null, + "order_id": null, + "ip": null, + "description": null, + "email": null, + "merchant_name_descriptor": null, + "merchant_location_descriptor": null, + "gateway_specific_fields": null, + "gateway_specific_response_fields": null, + "gateway_transaction_id": null, + "gateway_latency_ms": null, + "amount": 0, + "currency_code": null, + "retain_on_success": null, + "payment_method_added": false, + "message_key": null, + "message": null, + "response": null, + "payment_method": { + "token": "d3e10716-6aac-4eb8-a74d-c1a3027f1d96", + "created_at": "2024-05-14T13:44:25.3179205Z", + "updated_at": "2024-05-14T13:44:25.3179206Z", + "email": null, + "data": null, + "storage_state": null, + "test": false, + "metadata": null, + "last_four_digits": "1111", + "first_six_digits": "41111111", + "card_type": null, + "first_name": "Cure", + "last_name": "Tester", + "month": 9, + "year": 2025, + "address1": null, + "address2": null, + "city": null, + "state": null, + "zip": null, + "country": null, + "phone_number": null, + "company": null, + "full_name": null, + "payment_method_type": null, + "errors": null, + "fingerprint": null, + "verification_value": null, + "number": null + } + }, + "cardBinInfo": null, + "success": true, + "result": null, + "status": null, + "statusCode": null, + "errors": [], + "customProperties": {}, + "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIwYmE4NGY2ZS03YTllLTQzZjEtYWU2ZC1jNTA4YjQ2NjQyNGEiLCJ1bmlxdWVfbmFtZSI6IjBiYTg0ZjZlLTdhOWUtNDNmMS1hZTZkLWM1MDhiNDY2NDI0YSIsImp0aSI6IjczZTVkOGZiLWYxMDMtNGVlYy1iYTAzLTM2MmY1YjA5MmNkMCIsImlhdCI6IjE3MTU2OTQyNjQ3MDMiLCJhdWQiOlsicGF5bWVudHMiLCJvcmRlcnMiLCJtZXJjaGFudHMiLCJlbGlnaWJpbGl0eS1zZnRwIiwiZWxpZ2liaWxpdHkiLCJjb250YWN0Il0sImN1c3RvbTptaWQiOiJkOWQwYjVmZC05NDMzLTQ0ZDMtODA1MS02M2ZlZTI4NzY4ZTgiLCJuYmYiOjE3MTU2OTQyNjQsImV4cCI6MTcxNTY5NDg2NCwiaXNzIjoiQXBpLUNsaWVudC1TZXJ2aWNlIn0.oB9xtWGthG6tcDie8Q3fXPc1fED8pBAlv8yZQuoiEkA", + "token_expires": 1715694864703 + } + RESPONSE + end + def failed_purchase_response <<~RESPONSE { From d61748b8d11222f470420fbd1d7d5addace14d5e Mon Sep 17 00:00:00 2001 From: Edgar Villamarin Date: Mon, 20 May 2024 10:22:46 -0500 Subject: [PATCH 2/2] Update CHANGELOG --- CHANGELOG | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 4f5b6e96aef..b18ed3f6185 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,7 +2,6 @@ = ActiveMerchant CHANGELOG == HEAD -* FlexCharge: Add support for TPV store [edgarv09] #5120 * Shift4V2: Add new gateway based on SecurionPay adapter [heavyblade] #4860 * TNS: Use the specified order_id in request if available [yunnydang] #4880 * Cybersource: Support recurring apple pay [aenand] #4874 @@ -164,7 +163,7 @@ * Cybersource Rest: Add support for network tokens [aenand] #5107 * Decidir: Add support for customer object [rachelkirk] #5071 * Worldpay: Add support for stored credentials with network tokens [aenand] #5114 - +* FlexCharge: Add support for TPV store [edgarv09] #5120 == Version 1.135.0 (August 24, 2023) * PaymentExpress: Correct endpoints [steveh] #4827