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

Cyber source rest store unstore #4709

Draft
wants to merge 9 commits into
base: master
Choose a base branch
from
128 changes: 109 additions & 19 deletions lib/active_merchant/billing/gateways/cyber_source_rest.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,22 @@ def authorize(money, payment, options = {}, capture = false)
commit('/pts/v2/payments/', post)
end

def store(payment, options = {})
MultiResponse.run do |r|
customer = create_customer(payment, options)
customer_response = r.process { commit('/tms/v2/customers', customer) }
instrument_identifier = r.process { create_instrument_identifier(payment, options) }
r.process { create_payment_instrument(payment, instrument_identifier, options) }
r.process { create_customer_payment_instrument(payment, options, customer_response, instrument_identifier) }
end
end

def unstore(options = {})
customer_token_id = options[:customer_token_id]
payment_instrument_id = options[:payment_instrument_id]
commit("/tms/v2/customers/#{customer_token_id}/payment-instruments/#{payment_instrument_id}/", nil, :delete)
end

def supports_scrubbing?
true
end
Expand All @@ -62,6 +78,74 @@ def scrub(transcript)

private

def create_customer(payment, options)
{ buyerInformation: {}, clientReferenceInformation: {}, merchantDefinedInformation: [] }.tap do |post|
post[:buyerInformation][:merchantCustomerId] = options[:customer_id]
post[:buyerInformation][:email] = options[:email].presence || '[email protected]'
add_code(post, options)
post[:merchantDefinedInformation] = []
Copy link
Contributor

@sinourain sinourain Feb 27, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems that this is twice (82 and 86 have the same merchantDefinedInformation definition)

end.compact
end

def create_instrument_identifier(payment, options)
instrument_identifier = {
card: {
number: payment.number
}
}
commit('/tms/v1/instrumentidentifiers', instrument_identifier)
end

def create_payment_instrument(payment, instrument_identifier, options)
post = {
card: {
expirationMonth: payment.month.to_s,
expirationYear: payment.year.to_s,
type: payment.brand
},
billTo: {
firstName: options[:billing_address][:name].split.first,
lastName: options[:billing_address][:name].split.last,
company: options[:company],
address1: options[:billing_address][:address1],
locality: options[:billing_address][:city],
administrativeArea: options[:billing_address][:state],
postalCode: options[:billing_address][:zip],
country: options[:billing_address][:country],
email: options[:email],
phoneNumber: options[:billing_address][:phone]
},
instrumentIdentifier: {
id: instrument_identifier.params['id']
}
}
commit('/tms/v1/paymentinstruments', post)
end

def create_customer_payment_instrument(payment, options, customer_token, instrument_identifier)
post = {}
post[:deafult] = 'true'
post[:card] = {}
post[:card][:type] = CREDIT_CARD_CODES[payment.brand.to_sym]
post[:card][:expirationMonth] = payment.month.to_s
post[:card][:expirationYear] = payment.year.to_s
post[:billTo] = {
firstName: options[:billing_address][:name].split.first,
lastName: options[:billing_address][:name].split.last,
company: options[:company],
address1: options[:billing_address][:address1],
locality: options[:billing_address][:city],
administrativeArea: options[:billing_address][:state],
postalCode: options[:billing_address][:zip],
country: options[:billing_address][:country],
email: options[:email],
phoneNumber: options[:billing_address][:phone]
}
post[:instrumentIdentifier] = {}
post[:instrumentIdentifier][:id] = instrument_identifier.params['id']
commit("/tms/v2/customers/#{customer_token.params['id']}/payment-instruments", post)
end

def build_auth_request(amount, payment, options)
{ clientReferenceInformation: {}, paymentInformation: {}, orderInformation: {} }.tap do |post|
add_customer_id(post, options)
Expand Down Expand Up @@ -141,30 +225,34 @@ def parse(body)
JSON.parse(body)
end

def commit(action, post)
response = parse(ssl_post(url(action), post.to_json, auth_headers(action, post)))

def commit(action, post, http_method = :post)
response = parse(ssl_request(http_method, url(action), post.to_json, auth_headers(action, post, http_method)))
Response.new(
success_from(response),
message_from(response),
success_from(action, response),
message_from(action, response),
response,
authorization: authorization_from(response),
avs_result: AVSResult.new(code: response.dig('processorInformation', 'avs', 'code')),
# cvv_result: CVVResult.new(response['some_cvv_response_key']),
test: test?,
error_code: error_code_from(response)
error_code: error_code_from(action, response)
)
rescue ActiveMerchant::ResponseError => e
response = e.response.body.present? ? parse(e.response.body) : { 'response' => { 'rmsg' => e.response.msg } }
Response.new(false, response.dig('response', 'rmsg'), response, test: test?)
end

def success_from(response)
response['status'] == 'AUTHORIZED'
def success_from(action, response)
case action
when /payments/
response['status'] == 'AUTHORIZED'
else
!response['id'].nil?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why don't we use response['id'].present?

end
end

def message_from(response)
return response['status'] if success_from(response)
def message_from(action, response)
return response['status'] if success_from(action, response)

response['errorInformation']['message']
end
Expand All @@ -173,13 +261,13 @@ def authorization_from(response)
response['id']
end

def error_code_from(response)
response['errorInformation']['reason'] unless success_from(response)
def error_code_from(action, response)
response['errorInformation']['reason'] unless success_from(action, response)
end

# This implementation follows the Cybersource guide on how create the request signature, see:
# https://developer.cybersource.com/docs/cybs/en-us/payments/developer/all/rest/payments/GenerateHeader/httpSignatureAuthentication.html
def get_http_signature(resource, digest, http_method = 'post', gmtdatetime = Time.now.httpdate)
def get_http_signature(resource, digest, http_method = :post, gmtdatetime = Time.now.httpdate)
string_to_sign = {
host: host,
date: gmtdatetime,
Expand All @@ -191,7 +279,7 @@ def get_http_signature(resource, digest, http_method = 'post', gmtdatetime = Tim
{
keyid: @options[:public_key],
algorithm: 'HmacSHA256',
headers: "host date (request-target)#{digest.present? ? ' digest' : ''} v-c-merchant-id",
headers: "host#{http_method == :delete ? '' : ' date'} (request-target)#{digest.present? ? ' digest' : ''} v-c-merchant-id",
signature: sign_payload(string_to_sign)
}.map { |k, v| %{#{k}="#{v}"} }.join(', ')
end
Expand All @@ -201,19 +289,21 @@ def sign_payload(payload)
Base64.strict_encode64(OpenSSL::HMAC.digest('sha256', decoded_key, payload))
end

def auth_headers(action, post, http_method = 'post')
def auth_headers(action, post, http_method = :post)
digest = "SHA-256=#{Digest::SHA256.base64digest(post.to_json)}" if post.present?
date = Time.now.httpdate

{
'Accept' => 'application/hal+json;charset=utf-8',
date = Time.now.httpdate
accept = /payments/.match?(action) ? 'application/hal+json;charset=utf-8' : 'application/json;charset=utf-8'
headers = {
'Accept' => accept,
'Content-Type' => 'application/json;charset=utf-8',
'V-C-Merchant-Id' => @options[:merchant_id],
'Date' => date,
'Host' => host,
'Signature' => get_http_signature(action, digest, http_method, date),
'Digest' => digest
}
headers.merge!(http_method == :delete ? { 'v-c-date' => date } : { 'date' => date })
headers
end
end
end
Expand Down
16 changes: 16 additions & 0 deletions test/remote/gateways/remote_cyber_source_rest_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,22 @@ def test_successful_purchase
assert_nil response.params['_links']['capture']
end

def test_successful_store
@options[:billing_address] = @billing_address
response = @gateway.store(@visa_card, @options.merge(customer_id: '10'))
assert_success response
end

def test_successful_unstore
@options[:billing_address] = @billing_address
store_transaction = @gateway.store(@visa_card, @options.merge(customer_id: '10'))
@options[:customer_token_id] = store_transaction.params['id']
@options[:payment_instrument_id] = store_transaction.params['instrumentIdentifier']['id']
unstore = @gateway.unstore(@options)

assert_success unstore
end

def test_transcript_scrubbing
transcript = capture_transcript(@gateway) do
@gateway.authorize(@amount, @visa_card, @options)
Expand Down