Skip to content

Commit

Permalink
SumUp Gateway: Fix refund method (#4924)
Browse files Browse the repository at this point in the history
Description
-------------------------
Fix refund method to SumUp Gateway adapter.

This are the relevant links to review the implementation:

- [Refund a transaction](https://developer.sumup.com/docs/api/refund-transaction/)

Tickets for Spreedly reference
SER-836

Note: SumUp has shared with us an account to test with which you can refund a purchase

Unit test
-------------------------
Finished in 32.469516 seconds.
5638 tests, 78183 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications
100% passed

Remote test
-------------------------
173.64 tests/s, 2407.89 assertions/s

Rubocop
-------------------------
773 files inspected, no offenses detected

Co-authored-by: Luis <sinourain+endava@gmail.com>
  • Loading branch information
sinourain and Luis committed Nov 17, 2023
1 parent 5a1c4a3 commit 3b9de1f
Show file tree
Hide file tree
Showing 5 changed files with 95 additions and 36 deletions.
1 change: 1 addition & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
* Vantiv Express: New Xml gateway [DustinHaefele] #4956
* Shift4 V2: Add unstore function [javierpedrozaing] #4953
* CommerceHub: Add 3DS global support [sinourain] #4957
* SumUp Gateway: Fix refund method [sinourain] #4924

== Version 1.135.0 (August 24, 2023)
* PaymentExpress: Correct endpoints [steveh] #4827
Expand Down
81 changes: 52 additions & 29 deletions lib/active_merchant/billing/gateways/sum_up.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,8 @@ def void(authorization, options = {})
end

def refund(money, authorization, options = {})
transaction_id = authorization.split('#')[-1]
payment_currency = options[:currency] || currency(money)
post = money ? { amount: localized_amount(money, payment_currency) } : {}
transaction_id = authorization.split('#').last
post = money ? { amount: amount(money) } : {}
add_merchant_data(post, options)

commit('me/refund/' + transaction_id, post)
Expand Down Expand Up @@ -106,10 +105,9 @@ def add_address(post, options)
end

def add_invoice(post, money, options)
payment_currency = options[:currency] || currency(money)
post[:checkout_reference] = options[:order_id]
post[:amount] = localized_amount(money, payment_currency)
post[:currency] = payment_currency
post[:amount] = amount(money)
post[:currency] = options[:currency] || currency(money)
post[:description] = options[:description]
end

Expand All @@ -127,36 +125,40 @@ def add_payment(post, payment, options)

def commit(action, post, method = :post)
response = api_request(action, post.compact, method)
succeeded = success_from(response)

Response.new(
success_from(response),
message_from(response),
response,
succeeded,
message_from(succeeded, response),
action.include?('refund') ? { response_code: response.to_s } : response,
authorization: authorization_from(response),
test: test?,
error_code: error_code_from(response)
error_code: error_code_from(succeeded, response)
)
end

def api_request(action, post, method)
begin
raw_response = ssl_request(method, live_url + action, post.to_json, auth_headers)
rescue ResponseError => e
raw_response = e.response.body
end

raw_response =
begin
ssl_request(method, live_url + action, post.to_json, auth_headers)
rescue ResponseError => e
e.response.body
end
response = parse(raw_response)
# Multiple invalid parameters
response = format_multiple_errors(response) if raw_response.include?('error_code') && response.is_a?(Array)
response = response.is_a?(Hash) ? response.symbolize_keys : response

return response.symbolize_keys
return format_errors(response) if raw_response.include?('error_code') && response.is_a?(Array)

response
end

def parse(body)
JSON.parse(body)
end

def success_from(response)
return true if response == 204

return false unless %w(PENDING EXPIRED PAID).include?(response[:status])

response[:transactions].each do |transaction|
Expand All @@ -166,13 +168,19 @@ def success_from(response)
true
end

def message_from(response)
return response[:status] if success_from(response)
def message_from(succeeded, response)
if succeeded
return 'Succeeded' if response.is_a?(Integer)

return response[:status]
end

response[:message] || response[:error_message]
end

def authorization_from(response)
return nil if response.is_a?(Integer)

return response[:id] unless response[:transaction_id]

[response[:id], response[:transaction_id]].join('#')
Expand All @@ -185,21 +193,36 @@ def auth_headers
}
end

def error_code_from(response)
response[:error_code] unless success_from(response)
def error_code_from(succeeded, response)
response[:error_code] unless succeeded
end

def format_multiple_errors(responses)
errors = responses.map do |response|
{ error_code: response['error_code'], param: response['param'] }
end

def format_error(error, key)
{
:error_code => error['error_code'],
key => error['param']
}
end

def format_errors(errors)
return format_error(errors.first, :message) if errors.size == 1

return {
error_code: STANDARD_ERROR_CODE_MAPPING[:multiple_invalid_parameters],
message: 'Validation error',
errors: errors
errors: errors.map { |error| format_error(error, :param) }
}
end

def handle_response(response)
case response.code.to_i
# to get the response code (204) when the body is nil
when 200...300
response.body || response.code
else
raise ResponseError.new(response)
end
end
end
end
end
4 changes: 4 additions & 0 deletions test/fixtures.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1343,6 +1343,10 @@ sum_up:
access_token: SOMECREDENTIAL
pay_to_email: SOMECREDENTIAL

sum_up_account_for_successful_purchases:
access_token: SOMECREDENTIAL
pay_to_email: SOMECREDENTIAL

# Working credentials, no need to replace
swipe_checkout:
login: 2077103073D8B5
Expand Down
26 changes: 26 additions & 0 deletions test/remote/gateways/remote_sum_up_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,32 @@ def test_failed_void_invalid_checkout_id
assert_equal 'Resource not found', response.message
end

# In Sum Up the account can only return checkout/purchase in pending or success status,
# to obtain a successful refund we will need an account that returns the checkout/purchase in successful status
#
# For this example configure in the fixtures => :sum_up_account_for_successful_purchases
def test_successful_refund
gateway = SumUpGateway.new(fixtures(:sum_up_account_for_successful_purchases))
purchase = gateway.purchase(@amount, @credit_card, @options)
transaction_id = purchase.params['transaction_id']
assert_not_nil transaction_id

response = gateway.refund(@amount, transaction_id, {})
assert_success response
assert_equal 'Succeeded', response.message
end

def test_successful_partial_refund
gateway = SumUpGateway.new(fixtures(:sum_up_account_for_successful_purchases))
purchase = gateway.purchase(@amount * 10, @credit_card, @options)
transaction_id = purchase.params['transaction_id']
assert_not_nil transaction_id

response = gateway.refund(@amount, transaction_id, {})
assert_success response
assert_equal 'Succeeded', response.message
end

def test_failed_refund_for_pending_checkout
purchase = @gateway.purchase(@amount, @credit_card, @options)
assert_success purchase
Expand Down
19 changes: 12 additions & 7 deletions test/unit/gateways/sum_up_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ def test_success_from

def test_message_from
response = @gateway.send(:parse, successful_complete_checkout_response)
message_from = @gateway.send(:message_from, response.symbolize_keys)
message_from = @gateway.send(:message_from, true, response.symbolize_keys)
assert_equal 'PENDING', message_from
end

Expand All @@ -83,15 +83,15 @@ def test_authorization_from
assert_equal '8d8336a1-32e2-4f96-820a-5c9ee47e76fc', authorization_from
end

def test_format_multiple_errors
def test_format_errors
responses = @gateway.send(:parse, failed_complete_checkout_array_response)
error_code = @gateway.send(:format_multiple_errors, responses)
assert_equal format_multiple_errors_response, error_code
error_code = @gateway.send(:format_errors, responses)
assert_equal format_errors_response, error_code
end

def test_error_code_from
response = @gateway.send(:parse, failed_complete_checkout_response)
error_code_from = @gateway.send(:error_code_from, response.symbolize_keys)
error_code_from = @gateway.send(:error_code_from, false, response.symbolize_keys)
assert_equal 'CHECKOUT_SESSION_IS_EXPIRED', error_code_from
end

Expand Down Expand Up @@ -422,6 +422,11 @@ def failed_complete_checkout_array_response
"message": "Validation error",
"param": "card",
"error_code": "The card is expired"
},
{
"message": "Validation error",
"param": "card",
"error_code": "The value located under the \'$.card.number\' path is not a valid card number"
}
]
RESPONSE
Expand Down Expand Up @@ -484,11 +489,11 @@ def failed_refund_response
RESPONSE
end

def format_multiple_errors_response
def format_errors_response
{
error_code: 'MULTIPLE_INVALID_PARAMETERS',
message: 'Validation error',
errors: [{ error_code: 'The card is expired', param: 'card' }]
errors: [{ error_code: 'The card is expired', param: 'card' }, { error_code: "The value located under the '$.card.number' path is not a valid card number", param: 'card' }]
}
end
end

0 comments on commit 3b9de1f

Please sign in to comment.