Skip to content

Commit

Permalink
WorldPay: Add support for encrypted ApplePay and GooglePay (#5271)
Browse files Browse the repository at this point in the history
updated elevon and global collect to receive an object for payment_data

Co-authored-by: Alma Malambo <[email protected]>
  • Loading branch information
DustinHaefele and Alma Malambo authored Oct 10, 2024
1 parent f974b4c commit d579b24
Show file tree
Hide file tree
Showing 9 changed files with 153 additions and 21 deletions.
1 change: 1 addition & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
* Worldpay: Add customStringFields [jcreiff] #5284
* Airwallex: truncate descriptor field to 32 characters [jcreiff] #5292
* DLocal: Add the description field for refund [yunnydang] #5296
* Worldpay: Add support for Worldpay decrypted apple pay and google pay [dustinhaefele] #5271

== Version 1.137.0 (August 2, 2024)
* Unlock dependency on `rexml` to allow fixing a CVE (#5181).
Expand Down
12 changes: 12 additions & 0 deletions lib/active_merchant/billing/credit_card.rb
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,18 @@ def allow_spaces_in_card?(number = nil)
BRANDS_WITH_SPACES_IN_NUMBER.include?(self.class.brand?(self.number || number))
end

def network_token?
false
end

def mobile_wallet?
false
end

def encrypted_wallet?
false
end

private

def filter_number(value)
Expand Down
2 changes: 1 addition & 1 deletion lib/active_merchant/billing/gateways/elavon.rb
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ def add_txn_id(xml, authorization)
end

def add_network_token(xml, payment_method)
payment = payment_method.payment_data&.gsub('=>', ':')
payment = payment_method.payment_data.to_s&.gsub('=>', ':')
case payment_method.source
when :apple_pay
xml.ssl_applepay_web url_encode(payment)
Expand Down
4 changes: 2 additions & 2 deletions lib/active_merchant/billing/gateways/global_collect.rb
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,7 @@ def add_mobile_credit_card(post, payment, options, specifics_inputs, expirydate)
post['mobilePaymentMethodSpecificInput'] = specifics_inputs

if options[:use_encrypted_payment_data]
post['mobilePaymentMethodSpecificInput']['encryptedPaymentData'] = payment.payment_data
post['mobilePaymentMethodSpecificInput']['encryptedPaymentData'] = payment.payment_data.to_s&.gsub('=>', ':')
else
add_decrypted_payment_data(post, payment, options, expirydate)
end
Expand All @@ -315,7 +315,7 @@ def add_decrypted_payment_data(post, payment, options, expirydate)
'dpan' => payment.number
}
when :google_pay
payment.payment_data
payment.payment_data.to_s&.gsub('=>', ':')
end

post['mobilePaymentMethodSpecificInput']["#{data_type}PaymentData"] = data if data
Expand Down
59 changes: 52 additions & 7 deletions lib/active_merchant/billing/gateways/worldpay.rb
Original file line number Diff line number Diff line change
Expand Up @@ -630,6 +630,8 @@ def add_payment_method(xml, amount, payment_method, options)
case options[:payment_type]
when :pay_as_order
add_amount_for_pay_as_order(xml, amount, payment_method, options)
when :encrypted_wallet
add_encrypted_wallet(xml, payment_method)
when :network_token
add_network_tokenization_card(xml, payment_method, options)
else
Expand Down Expand Up @@ -683,6 +685,37 @@ def merchant_initiated?(options)
options.dig(:stored_credential, :initiator) == 'merchant'
end

def add_encrypted_wallet(xml, payment_method)
source = encrypted_wallet_source(payment_method.source)

xml.paymentDetails do
xml.tag! "#{source}-SSL" do
if source == 'APPLEPAY'
add_encrypted_apple_pay(xml, payment_method)
else
add_encrypted_google_pay(xml, payment_method)
end
end
end
end

def add_encrypted_apple_pay(xml, payment_method)
xml.header do
xml.ephemeralPublicKey payment_method.payment_data.dig(:header, :ephemeralPublicKey)
xml.publicKeyHash payment_method.payment_data.dig(:header, :publicKeyHash)
xml.transactionId payment_method.payment_data.dig(:header, :transactionId)
end
xml.signature payment_method.payment_data[:signature]
xml.version payment_method.payment_data[:version]
xml.data payment_method.payment_data[:data]
end

def add_encrypted_google_pay(xml, payment_method)
xml.protocolVersion payment_method.payment_data[:version]
xml.signature payment_method.payment_data[:signature]
xml.signedMessage payment_method.payment_data[:signed_message]
end

def add_card_or_token(xml, payment_method, options)
xml.paymentDetails credit_fund_transfer_attribute(options) do
if options[:payment_type] == :token
Expand Down Expand Up @@ -1066,16 +1099,17 @@ def payment_details(payment_method, options = {})
when String
token_type_and_details(payment_method)
else
type = network_token?(payment_method) || wallet_type_google_pay?(options) || payment_method_apple_pay?(payment_method) ? :network_token : :credit

{ payment_type: type }
payment_method_type(payment_method, options)
end
end

def network_token?(payment_method)
payment_method.respond_to?(:source) &&
payment_method.respond_to?(:payment_cryptogram) &&
payment_method.respond_to?(:eci)
def payment_method_type(payment_method, options)
type = if payment_method.is_a?(NetworkTokenizationCreditCard)
payment_method.encrypted_wallet? ? :encrypted_wallet : :network_token
else
wallet_type_google_pay?(options) ? :network_token : :credit
end
{ payment_type: type }
end

def payment_method_apple_pay?(payment_method)
Expand Down Expand Up @@ -1129,6 +1163,17 @@ def generate_stored_credential_params(is_initial_transaction, reason = nil, init
stored_credential_params[customer_or_merchant] = reason if reason
stored_credential_params
end

def encrypted_wallet_source(source)
case source
when :apple_pay
'APPLEPAY'
when :google_pay
'PAYWITHGOOGLE'
else
raise ArgumentError, 'Invalid encrypted wallet source'
end
end
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ def mobile_wallet?
%i[apple_pay android_pay google_pay].include?(source)
end

def encrypted_wallet?
payment_data.present?
end

def type
'network_tokenization'
end
Expand Down
8 changes: 4 additions & 4 deletions test/unit/gateways/elavon_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,12 @@ def setup

@google_pay = ActiveMerchant::Billing::NetworkTokenizationCreditCard.new({
source: :google_pay,
payment_data: "{ 'version': 'EC_v1', 'data': 'QlzLxRFnNP9/GTaMhBwgmZ2ywntbr9'}"
payment_data: { 'version' => 'EC_v1', 'data' => 'QlzLxRFnNP9/GTaMhBwgmZ2ywntbr9' }
})

@apple_pay = ActiveMerchant::Billing::NetworkTokenizationCreditCard.new({
source: :apple_pay,
payment_data: "{ 'version': 'EC_v1', 'data': 'QlzLxRFnNP9/GTaMhBwgmZ2ywntbr9'}"
payment_data: { 'version' => 'EC_v1', 'data' => 'QlzLxRFnNP9/GTaMhBwgmZ2ywntbr9' }
})
end

Expand Down Expand Up @@ -163,15 +163,15 @@ def test_successful_purchase_with_apple_pay
stub_comms do
@gateway.purchase(@amount, @apple_pay, @options)
end.check_request do |_endpoint, data, _headers|
assert_match(/<ssl_applepay_web>%7B %27version%27%3A %27EC_v1%27%2C %27data%27%3A %27QlzLxRFnNP9%2FGTaMhBwgmZ2ywntbr9%27%7D<\/ssl_applepay_web>/, data)
assert_match(/<ssl_applepay_web>%7B%22version%22%3A%22EC_v1%22%2C %22data%22%3A%22QlzLxRFnNP9%2FGTaMhBwgmZ2ywntbr9%22%7D<\/ssl_applepay_web>/, data)
end.respond_with(successful_purchase_response)
end

def test_successful_purchase_with_google_pay
stub_comms do
@gateway.purchase(@amount, @google_pay, @options)
end.check_request do |_endpoint, data, _headers|
assert_match(/<ssl_google_pay>%7B %27version%27%3A %27EC_v1%27%2C %27data%27%3A %27QlzLxRFnNP9%2FGTaMhBwgmZ2ywntbr9%27%7D<\/ssl_google_pay>/, data)
assert_match(/<ssl_google_pay>%7B%22version%22%3A%22EC_v1%22%2C %22data%22%3A%22QlzLxRFnNP9%2FGTaMhBwgmZ2ywntbr9%22%7D<\/ssl_google_pay>/, data)
end.respond_with(successful_purchase_response)
end

Expand Down
8 changes: 4 additions & 4 deletions test/unit/gateways/global_collect_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ def setup

@google_pay_network_token = ActiveMerchant::Billing::NetworkTokenizationCreditCard.new({
source: :google_pay,
payment_data: "{ 'version': 'EC_v1', 'data': 'QlzLxRFnNP9/GTaMhBwgmZ2ywntbr9'}"
payment_data: { 'version' => 'EC_v1', 'data' => 'QlzLxRFnNP9/GTaMhBwgmZ2ywntbr9' }
})

@declined_card = credit_card('5424180279791732')
Expand Down Expand Up @@ -91,14 +91,14 @@ def test_successful_purchase_with_requires_approval_true
def test_purchase_request_with_encrypted_google_pay
google_pay = ActiveMerchant::Billing::NetworkTokenizationCreditCard.new({
source: :google_pay,
payment_data: "{ 'version': 'EC_v1', 'data': 'QlzLxRFnNP9/GTaMhBwgmZ2ywntbr9'}"
payment_data: { 'version' => 'EC_v1', 'data' => 'QlzLxRFnNP9/GTaMhBwgmZ2ywntbr9' }
})

stub_comms(@gateway, :ssl_request) do
@gateway.purchase(@accepted_amount, google_pay, { use_encrypted_payment_data: true })
end.check_request(skip_response: true) do |_method, _endpoint, data, _headers|
assert_equal '320', JSON.parse(data)['mobilePaymentMethodSpecificInput']['paymentProductId']
assert_equal google_pay.payment_data, JSON.parse(data)['mobilePaymentMethodSpecificInput']['encryptedPaymentData']
assert_equal google_pay.payment_data.to_s&.gsub('=>', ':'), JSON.parse(data)['mobilePaymentMethodSpecificInput']['encryptedPaymentData']
end
end

Expand Down Expand Up @@ -131,7 +131,7 @@ def test_add_payment_for_google_pay
assert_includes post.keys.first, 'mobilePaymentMethodSpecificInput'
assert_equal post['mobilePaymentMethodSpecificInput']['paymentProductId'], '320'
assert_equal post['mobilePaymentMethodSpecificInput']['authorizationMode'], 'FINAL_AUTHORIZATION'
assert_equal post['mobilePaymentMethodSpecificInput']['encryptedPaymentData'], @google_pay_network_token.payment_data
assert_equal post['mobilePaymentMethodSpecificInput']['encryptedPaymentData'], @google_pay_network_token.payment_data.to_s&.gsub('=>', ':')
end

def test_add_payment_for_apple_pay
Expand Down
76 changes: 73 additions & 3 deletions test/unit/gateways/worldpay_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -186,9 +186,9 @@ def test_payment_type_for_network_card
assert_equal payment, :network_token
end

def test_payment_type_returns_network_token_if_the_payment_method_responds_to_source_payment_cryptogram_and_eci
payment_method = mock
payment_method.stubs(source: nil, payment_cryptogram: nil, eci: nil)
def test_payment_type_returns_network_token_if_payment_method_is_nt_credit_card_and_not_encrypted
payment_method = @nt_credit_card
payment_method.stubs(source: nil, payment_cryptogram: nil, eci: nil, payment_data: nil)
result = @gateway.send(:payment_details, payment_method)
assert_equal({ payment_type: :network_token }, result)
end
Expand Down Expand Up @@ -243,6 +243,76 @@ def test_successful_authorize_without_name
assert_equal 'R50704213207145707', response.authorization
end

def test_successful_authorize_encrypted_apple_pay
apple_pay = ActiveMerchant::Billing::NetworkTokenizationCreditCard.new({
source: :apple_pay,
payment_data: {
version: 'EC_v1',
data: 'QlzLxRFnNP9/GTaMhBwgmZ2ywntbr9',
signature: 'signature',
header: {
ephemeralPublicKey: 'ephemeralPublicKey',
publicKeyHash: 'publicKeyHash',
transactionId: 'transactionId'
}
}
})

stub_comms do
@gateway.authorize(@amount, apple_pay, @options)
end.check_request do |_endpoint, data, _headers|
assert_match(/APPLEPAY-SSL/, data)
assert_match(%r(<version>EC_v1</version>), data)
assert_match(%r(<transactionId>transactionId</transactionId>), data)
assert_match(%r(<data>QlzLxRFnNP9/GTaMhBwgmZ2ywntbr9</data>), data)
end.respond_with(successful_authorize_response)
end

def test_successful_authorize_encrypted_google_pay
google_pay = ActiveMerchant::Billing::NetworkTokenizationCreditCard.new({
source: :google_pay,
payment_data: {
version: 'EC_v1',
data: 'QlzLxRFnNP9/GTaMhBwgmZ2ywntbr9',
signature: 'signature',
header: {
ephemeralPublicKey: 'ephemeralPublicKey',
publicKeyHash: 'publicKeyHash',
transactionId: 'transactionId'
}
}
})

stub_comms do
@gateway.authorize(@amount, google_pay, @options)
end.check_request do |_endpoint, data, _headers|
assert_match(/PAYWITHGOOGLE-SSL/, data)
assert_match(%r(<protocolVersion>EC_v1</protocolVersion>), data)
assert_match(%r(<signature>signature</signature>), data)
end.respond_with(successful_authorize_response)
end

def test_encrypted_payment_with_invalid_source
apple_pay = ActiveMerchant::Billing::NetworkTokenizationCreditCard.new({
source: :android_pay,
payment_data: {
version: 'EC_v1',
data: 'QlzLxRFnNP9/GTaMhBwgmZ2ywntbr9',
signature: 'signature',
header: {
ephemeralPublicKey: 'ephemeralPublicKey',
publicKeyHash: 'publicKeyHash',
transactionId: 'transactionId'
}
}
})
error = assert_raises(ArgumentError) do
@gateway.authorize(@amount, apple_pay, @options)
end

assert_equal 'Invalid encrypted wallet source', error.message
end

def test_successful_authorize_by_reference
response = stub_comms do
@gateway.authorize(@amount, @options[:order_id].to_s, @options)
Expand Down

0 comments on commit d579b24

Please sign in to comment.