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

CyberSourceRest: support TMS store/unstore, and void of captures/refunds #5369

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
86 changes: 66 additions & 20 deletions lib/active_merchant/billing/gateways/cyber_source_rest.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {})
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -386,22 +429,25 @@ 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
URI.parse(url('')).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),
Expand All @@ -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)
Expand All @@ -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],
Expand Down
12 changes: 12 additions & 0 deletions lib/active_merchant/posts_data.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
26 changes: 26 additions & 0 deletions test/remote/gateways/remote_cyber_source_rest_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
74 changes: 73 additions & 1 deletion test/unit/gateways/cyber_source_rest_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -636,13 +636,83 @@ 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
assert_equal nil, response.message
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_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)
Expand Down Expand Up @@ -964,4 +1034,6 @@ def successful_void_response
}
RESPONSE
end

def successful_unstore_response; end
end