diff --git a/lib/active_merchant/billing/gateways/cyber_source_rest.rb b/lib/active_merchant/billing/gateways/cyber_source_rest.rb index f2103f23f7e..511e703e34c 100644 --- a/lib/active_merchant/billing/gateways/cyber_source_rest.rb +++ b/lib/active_merchant/billing/gateways/cyber_source_rest.rb @@ -55,31 +55,50 @@ def authorize(money, payment, options = {}, capture = false) post = build_auth_request(money, payment, options) post[:processingInformation][:capture] = true if capture - commit('payments', post, options) + commit('pts/v2/payments', post, options) end def capture(money, authorization, options = {}) payment = authorization.split('|').first post = build_reference_request(money, options) - commit("payments/#{payment}/captures", post, options) + commit("pts/v2/payments/#{payment}/captures", post, options) end def refund(money, authorization, options = {}) payment = authorization.split('|').first post = build_reference_request(money, options) - commit("payments/#{payment}/refunds", post, options) + commit("pts/v2/payments/#{payment}/refunds", post, options) end def credit(money, payment, options = {}) post = build_credit_request(money, payment, options) - commit('credits', post) + commit('pts/v2/credits', post) end def void(authorization, options = {}) - payment, amount = authorization.split('|') + payment, amount, action = authorization.split('|') post = build_void_request(options, amount) - commit("payments/#{payment}/reversals", post) + endpoint = + case action + when 'captures', 'payments', 'refunds' + "pts/v2/#{action}/#{payment}/voids" + else + "pts/v2/payments/#{payment}/reversals" + end + commit(endpoint, post) + end + + def store(money, payment, options = {}) + post = build_auth_request(money, payment, options) + post[:processingInformation][:capture] = !money.zero? + add_tms_create_information(post, options) + + commit('pts/v2/payments', post, options) + end + + def unstore(customer_id, options = {}) + commit("tms/v2/customers/#{customer_id}", {}, options, :delete) end def verify(credit_card, options = {}) @@ -149,15 +168,18 @@ def add_three_ds(post, payment_method, options) end def build_void_request(options, amount = nil) - { reversalInformation: { amountDetails: { totalAmount: nil } } }.tap do |post| + { clientReferenceInformation: {}, reversalInformation: { amountDetails: { totalAmount: nil } } }.tap do |post| add_reversal_amount(post, amount.to_i) if amount.present? add_merchant_category_code(post, options) + add_code(post, options) end.compact end def build_auth_request(amount, payment, options) { clientReferenceInformation: {}, paymentInformation: {}, orderInformation: {} }.tap do |post| add_customer_id(post, options) + add_customer_information(post, options) + add_device_information(post, options) add_code(post, options) add_payment(post, payment, options) add_mdd_fields(post, options) @@ -208,6 +230,26 @@ def add_customer_id(post, options) post[:paymentInformation][:customer] = { customerId: options[:customer_id] } end + def add_customer_information(post, options) + post[:customerInformation] = { + email: options[:email], + merchantCustomerId: options[:merchant_customer_id] + }.compact + end + + def add_device_information(post, options) + post[:deviceInformation] = { + ipAddress: options[:ip_address] + }.compact + end + + def add_tms_create_information(post, options) + post[:processingInformation].tap do |hash| + hash[:actionList] = options[:action_list] || %w[TOKEN_CREATE] + hash[:actionTokenTypes] = options[:action_token_types] || %w[customer paymentInstrument] + end + end + def add_reversal_amount(post, amount) currency = options[:currency] || currency(amount) @@ -293,11 +335,11 @@ def add_apple_pay_google_pay_cryptogram(post, payment) def add_credit_card(post, creditcard) post[:paymentInformation][:card] = { number: creditcard.number, - expirationMonth: format(creditcard.month, :two_digits), - expirationYear: format(creditcard.year, :four_digits), + expirationMonth: format(creditcard.month, :two_digits).presence, + expirationYear: format(creditcard.year, :four_digits).presence, securityCode: creditcard.verification_value, type: CREDIT_CARD_CODES[card_brand(creditcard).to_sym] - } + }.compact end def add_address(post, payment_method, address, options, address_type) @@ -360,14 +402,15 @@ def commerce_indicator(reason_type) end def add_authorization_options(post, payment, options) - initiator = options.dig(:stored_credential, :initiator) == 'cardholder' ? 'customer' : 'merchant' + initiator = options.dig(:stored_credential, :initiator) + initiator = 'customer' if initiator == 'cardholder' authorization_options = { authorizationOptions: { initiator: { type: initiator - } + }.compact } - }.compact + } authorization_options[:authorizationOptions][:initiator][:storedCredentialUsed] = true if initiator == 'merchant' authorization_options[:authorizationOptions][:initiator][:credentialStoredOnFile] = true if options.dig(:stored_credential, :initial_transaction) @@ -378,7 +421,7 @@ def add_authorization_options(post, payment, options) authorization_options[:authorizationOptions][:initiator][:merchantInitiatedTransaction][:originalAuthorizedAmount] = post.dig(:orderInformation, :amountDetails, :totalAmount) if card_brand(payment) == 'discover' end authorization_options[:authorizationOptions][:initiator][:merchantInitiatedTransaction][:reason] = options[:reason_code] if options[:reason_code] - post[:processingInformation].merge!(authorization_options) + post[:processingInformation].deep_merge!(authorization_options) end def network_transaction_id_from(response) @@ -386,7 +429,7 @@ def network_transaction_id_from(response) end def url(action) - "#{test? ? test_url : live_url}/pts/v2/#{action}" + "#{test? ? test_url : live_url}/#{action}" end def host @@ -394,14 +437,17 @@ def host end def parse(body) - JSON.parse(body) + JSON.parse(body || '{}') end - def commit(action, post, options = {}) + def commit(action, post, options = {}, http_method = 'post') add_reconciliation_id(post, options) add_sec_code(post, options) add_invoice_number(post, options) - response = parse(ssl_post(url(action), post.to_json, auth_headers(action, options, post))) + + response = parse(ssl_action(http_method, url(action), post.to_json, auth_headers(action, options, post, http_method).compact)) + return Response.new(true, 'No content', response, test: test?) if response.empty? + Response.new( success_from(response), message_from(response), @@ -420,7 +466,7 @@ def commit(action, post, options = {}) end def success_from(response) - %w(AUTHORIZED PENDING REVERSED).include?(response['status']) + %w(AUTHORIZED PENDING REVERSED VOIDED).include?(response['status']) end def message_from(response) @@ -446,10 +492,10 @@ def get_http_signature(resource, digest, http_method = 'post', gmtdatetime = Tim string_to_sign = { host:, date: gmtdatetime, - 'request-target': "#{http_method} /pts/v2/#{resource}", + 'request-target': "#{http_method} /#{resource}", digest:, 'v-c-merchant-id': @options[:merchant_id] - }.map { |k, v| "#{k}: #{v}" }.join("\n").force_encoding(Encoding::UTF_8) + }.compact.map { |k, v| "#{k}: #{v}" }.join("\n").force_encoding(Encoding::UTF_8) { keyid: @options[:public_key], diff --git a/lib/active_merchant/posts_data.rb b/lib/active_merchant/posts_data.rb index 581a4317003..e6cf0ada1c8 100644 --- a/lib/active_merchant/posts_data.rb +++ b/lib/active_merchant/posts_data.rb @@ -34,6 +34,14 @@ def self.included(base) base.class_attribute :proxy_password end + def ssl_action(http_method, endpoint, data = {}, headers = {}) + if http_method == 'post' + ssl_post(endpoint, data, headers) + else + send("ssl_#{http_method}", endpoint, headers) + end + end + def ssl_get(endpoint, headers = {}) ssl_request(:get, endpoint, nil, headers) end @@ -42,6 +50,10 @@ def ssl_post(endpoint, data, headers = {}) ssl_request(:post, endpoint, data, headers) end + def ssl_delete(endpoint, headers = {}) + ssl_request(:delete, endpoint, nil, headers) + end + def ssl_request(method, endpoint, data, headers) handle_response(raw_ssl_request(method, endpoint, data, headers)) end diff --git a/test/remote/gateways/remote_cyber_source_rest_test.rb b/test/remote/gateways/remote_cyber_source_rest_test.rb index d6f61965138..900d6507b30 100644 --- a/test/remote/gateways/remote_cyber_source_rest_test.rb +++ b/test/remote/gateways/remote_cyber_source_rest_test.rb @@ -673,4 +673,30 @@ def test_successful_purchase_with_level_2_and_3_data assert_equal 'AUTHORIZED', response.message assert_nil response.params['_links']['capture'] end + + def test_void_of_purchase + purchase_response = @gateway.purchase(@amount, @visa_card, @options) + response = @gateway.void([purchase_response.authorization, 'captures'].join('|'), @options) + assert_success response + assert response.params['id'].present? + assert_equal 'VOIDED', response.message + assert_nil response.params['_links']['capture'] + end + + def test_store_with_purchase + response = @gateway.store(@amount, @visa_card, @options) + assert_success response + assert response.test? + assert_equal 'AUTHORIZED', response.message + assert_nil response.params['_links']['capture'] + end + + def test_store_without_purchase + response = @gateway.store(0, @visa_card, @options) + assert_success response + assert response.test? + assert_equal 'AUTHORIZED', response.message + refute_empty response.params['_links']['capture'] + assert_equal '0.00', response.params['orderInformation']['amountDetails']['authorizedAmount'] + end end diff --git a/test/unit/gateways/cyber_source_rest_test.rb b/test/unit/gateways/cyber_source_rest_test.rb index e841e4518e2..062386bf552 100644 --- a/test/unit/gateways/cyber_source_rest_test.rb +++ b/test/unit/gateways/cyber_source_rest_test.rb @@ -342,7 +342,7 @@ def test_authorize_apple_pay_jcb end def test_url_building - assert_equal "#{@gateway.class.test_url}/pts/v2/action", @gateway.send(:url, 'action') + assert_equal "#{@gateway.class.test_url}/action", @gateway.send(:url, 'action') end def test_stored_credential_cit_initial @@ -636,6 +636,8 @@ def test_failed_void response = stub_comms do @gateway.void(purchase, @options) + end.check_request do |endpoint, _data, _headers| + assert_equal "#{@gateway.class.test_url}/pts/v2/payments/1000/reversals", endpoint end.respond_with(successful_void_response) assert_failure response @@ -643,6 +645,90 @@ def test_failed_void assert_equal nil, response.error_code end + def test_void_captures + authorization = '1234567890|1999|captures' + + stub_comms do + @gateway.void(authorization, @options) + end.check_request do |endpoint, data, _headers| + request = JSON.parse(data) + assert_equal "#{@gateway.class.test_url}/pts/v2/captures/1234567890/voids", endpoint + assert_equal '1', request['clientReferenceInformation']['code'] + end.respond_with(successful_void_response) + end + + def test_void_payments + authorization = '1234567890|1999|payments' + + stub_comms do + @gateway.void(authorization, @options) + end.check_request do |endpoint, data, _headers| + request = JSON.parse(data) + assert_equal "#{@gateway.class.test_url}/pts/v2/payments/1234567890/voids", endpoint + assert_equal '1', request['clientReferenceInformation']['code'] + end.respond_with(successful_void_response) + end + + def test_void_refunds + authorization = '1234567890|1999|refunds' + + stub_comms do + @gateway.void(authorization, @options) + end.check_request do |endpoint, data, _headers| + request = JSON.parse(data) + assert_equal "#{@gateway.class.test_url}/pts/v2/refunds/1234567890/voids", endpoint + assert_equal '1', request['clientReferenceInformation']['code'] + end.respond_with(successful_void_response) + end + + def test_store_credit_card_with_payment + stub_comms do + @gateway.store(100, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_equal true, request['processingInformation']['capture'] + assert_equal %w[TOKEN_CREATE], request['processingInformation']['actionList'] + assert_equal %w[customer paymentInstrument], request['processingInformation']['actionTokenTypes'] + end.respond_with(successful_purchase_response) + end + + def test_store_credit_card_with_payment_and_stored_credential + stub_comms do + @options.merge!( + ignore_avs: true, + stored_credential: stored_credential(:cardholder, :internet, :initial) + ) + @gateway.store(100, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_equal true, request['processingInformation']['capture'] + assert_equal 'true', request['processingInformation']['authorizationOptions']['ignoreAvsResult'] + assert_equal %w[TOKEN_CREATE], request['processingInformation']['actionList'] + assert_equal %w[customer paymentInstrument], request['processingInformation']['actionTokenTypes'] + end.respond_with(successful_purchase_response) + end + + def test_store_credit_card_without_payment + stub_comms do + @gateway.store(0, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_equal false, request['processingInformation']['capture'] + assert_equal %w[TOKEN_CREATE], request['processingInformation']['actionList'] + assert_equal %w[customer paymentInstrument], request['processingInformation']['actionTokenTypes'] + end.respond_with(successful_purchase_response) + end + + def test_unstore + customer_id = '12345' + + stub_comms(@gateway, :ssl_delete) do + @gateway.unstore(customer_id, @options) + end.check_request do |endpoint, _data, _headers| + assert_equal "#{@gateway.class.test_url}/tms/v2/customers/12345", endpoint + end.respond_with(successful_unstore_response) + end + private def parse_signature(signature) @@ -964,4 +1050,6 @@ def successful_void_response } RESPONSE end + + def successful_unstore_response; end end