Skip to content

Commit

Permalink
CecabankRest: Add AP/GP (#5295)
Browse files Browse the repository at this point in the history
Summary
----------------
This commit implement Apple Pay and Google Pay payment methods
the unit and remote tests, and fix few faling tests in current
implementation.

[SER-1431](https://spreedly.atlassian.net/browse/SER-1431)

Unit Tests:
----------------
Finished in 0.031554 seconds.
20 tests, 106 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications
100% passed

Remote Tests:
----------------
Finished in 20.386373 seconds.
20 tests, 69 assertions, 1 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications
95% passed

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

Co-authored-by: Gustavo Sanmartin <[email protected]>
Co-authored-by: Nick Ashton <[email protected]>
  • Loading branch information
3 people authored Oct 16, 2024
1 parent ec93b3a commit 32b58d6
Show file tree
Hide file tree
Showing 4 changed files with 155 additions and 17 deletions.
1 change: 1 addition & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
* Worldpay: Add support for Worldpay decrypted apple pay and google pay [dustinhaefele] #5271
* Orbital: Update alternate_ucaf_flow [almalee24] #5282
* Adyen: Remove cryptogram flag [almalee24] #5300
* Cecabank: Include Apple Pay and Google Pay for recurring payments [gasn150] #5295

== Version 1.137.0 (August 2, 2024)
* Unlock dependency on `rexml` to allow fixing a CVE (#5181).
Expand Down
34 changes: 23 additions & 11 deletions lib/active_merchant/billing/gateways/cecabank/cecabank_json.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ class CecabankJsonGateway < Gateway
transaction_risk_analysis_exemption: :TRA
}.freeze

WALLET_PAYMENT_METHODS = {
apple_pay: 'A',
google_pay: 'G'
}

self.test_url = 'https://tpv.ceca.es/tpvweb/rest/procesos/'
self.live_url = 'https://pgw.ceca.es/tpvweb/rest/procesos/'

Expand Down Expand Up @@ -113,7 +118,7 @@ def handle_purchase(action, money, creditcard, options)
post = { parametros: { accion: CECA_ACTIONS_DICTIONARY[action] } }

add_invoice(post, money, options)
add_creditcard(post, creditcard)
add_payment_method(post, creditcard, options)
add_stored_credentials(post, creditcard, options)
add_three_d_secure(post, options)

Expand Down Expand Up @@ -159,20 +164,28 @@ def add_invoice(post, money, options)
post[:parametros][:exponente] = 2.to_s
end

def add_creditcard(post, creditcard)
def add_payment_method(post, payment_method, options)
params = post[:parametros] ||= {}
three_d_secure = options.fetch(:three_d_secure, {})

payment_method = {
pan: creditcard.number,
caducidad: strftime_yyyymm(creditcard)
pm = {
pan: payment_method.number,
caducidad: strftime_yyyymm(payment_method)
}
if CreditCard.brand?(creditcard.number) == 'american_express'
payment_method[:csc] = creditcard.verification_value

if payment_method.is_a?(NetworkTokenizationCreditCard) && WALLET_PAYMENT_METHODS[payment_method.source.to_sym]
pm[:wallet] = {

# the authentication value should come nil (for recurring cases) or should I remove it?
authentication_value: (payment_method.payment_cryptogram unless options.dig(:stored_credential, :network_transaction_id)),
xid: three_d_secure[:xid] || three_d_secure[:ds_transaction_id] || options[:xid],
walletType: WALLET_PAYMENT_METHODS[payment_method.source.to_sym],
eci: payment_method.eci || (three_d_secure[:eci] if three_d_secure) || '07'
}.compact.to_json
else
payment_method[:cvv2] = creditcard.verification_value
pm[CreditCard.brand?(payment_method.number) == 'american_express' ? :csc : :cvv2] = payment_method.verification_value
end

@options[:encryption_key] ? params[:encryptedData] = payment_method : params.merge!(payment_method)
@options[:encryption_key] ? params[:encryptedData] = pm : params.merge!(pm)
end

def add_stored_credentials(post, creditcard, options)
Expand Down Expand Up @@ -233,7 +246,6 @@ def commit(action, post)

add_encryption(post)
add_merchant_data(post)

params_encoded = encode_post_parameters(post)
add_signature(post, params_encoded, options)

Expand Down
69 changes: 63 additions & 6 deletions test/remote/gateways/remote_cecabank_rest_json_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ def setup

@options = {
order_id: generate_unique_id,
three_d_secure: three_d_secure
three_d_secure: three_d_secure,
exemption_type: 'transaction_risk_analysis_exemption'
}

@cit_options = @options.merge({
Expand All @@ -21,6 +22,26 @@ def setup
initiator: 'cardholder'
}
})

@apple_pay_network_token = network_tokenization_credit_card(
'4507670001000009',
eci: '05',
payment_cryptogram: 'xgQAAAAAAAAAAAAAAAAAAAAAAAAA',
month: '12',
year: Time.now.year,
source: :apple_pay,
verification_value: '989'
)

@google_pay_network_token = network_tokenization_credit_card(
'4507670001000009',
eci: '05',
payment_cryptogram: 'AgAAAAAAAIR8CQrXcIhbQAAAAAA',
month: '10',
year: Time.now.year + 1,
source: :google_pay,
verification_value: nil
)
end

def test_successful_authorize
Expand Down Expand Up @@ -57,6 +78,24 @@ def test_successful_purchase
assert_equal %i[codAut numAut referencia], JSON.parse(response.message).symbolize_keys.keys.sort
end

def test_successful_purchase_with_apple_pay
assert response = @gateway.purchase(@amount, @apple_pay_network_token, { order_id: generate_unique_id })
assert_success response
assert_equal %i[codAut numAut referencia], JSON.parse(response.message).symbolize_keys.keys.sort
end

def test_successful_purchase_with_google_pay
assert response = @gateway.purchase(@amount, @apple_pay_network_token, { order_id: generate_unique_id })
assert_success response
assert_equal %i[codAut numAut referencia], JSON.parse(response.message).symbolize_keys.keys.sort
end

def test_failed_purchase_with_apple_pay_sending_three_ds_data
assert response = @gateway.purchase(@amount, @apple_pay_network_token, @options)
assert_failure response
assert_equal response.error_code, '1061'
end

def test_unsuccessful_purchase
assert response = @gateway.purchase(@amount, @declined_card, @options)
assert_failure response
Expand Down Expand Up @@ -111,7 +150,7 @@ def test_purchase_using_stored_credential_cit

def test_purchase_stored_credential_with_network_transaction_id
@cit_options.merge!({ network_transaction_id: '999999999999999' })
assert purchase = @gateway.purchase(@amount, @credit_card, @cit_options)
assert purchase = @gateway.purchase(@amount, @credit_card, @options)
assert_success purchase
end

Expand All @@ -124,6 +163,21 @@ def test_purchase_using_auth_capture_and_stored_credential_cit
assert_success capture
end

def test_purchase_with_apple_pay_using_stored_credential_recurring_mit
@cit_options[:stored_credential][:reason_type] = 'installment'
assert purchase = @gateway.purchase(@amount, @apple_pay_network_token, @cit_options.except(:three_d_secure))
assert_success purchase

options = @cit_options.except(:three_d_secure, :extra_options_for_three_d_secure)
options[:stored_credential][:reason_type] = 'recurring'
options[:stored_credential][:initiator] = 'merchant'
options[:stored_credential][:network_transaction_id] = purchase.network_transaction_id
options[:order_id] = generate_unique_id

assert purchase2 = @gateway.purchase(@amount, @apple_pay_network_token, options)
assert_success purchase2
end

def test_purchase_using_stored_credential_recurring_mit
@cit_options[:stored_credential][:reason_type] = 'installment'
assert purchase = @gateway.purchase(@amount, @credit_card, @cit_options)
Expand All @@ -133,6 +187,7 @@ def test_purchase_using_stored_credential_recurring_mit
options[:stored_credential][:reason_type] = 'recurring'
options[:stored_credential][:initiator] = 'merchant'
options[:stored_credential][:network_transaction_id] = purchase.network_transaction_id
options[:order_id] = generate_unique_id

assert purchase2 = @gateway.purchase(@amount, @credit_card, options)
assert_success purchase2
Expand Down Expand Up @@ -170,12 +225,14 @@ def get_response_params(transcript)
def three_d_secure
{
version: '2.2.0',
eci: '02',
cavv: '4F80DF50ADB0F9502B91618E9B704790EABA35FDFC972DDDD0BF498C6A75E492',
eci: '07',
ds_transaction_id: 'a2bf089f-cefc-4d2c-850f-9153827fe070',
acs_transaction_id: '18c353b0-76e3-4a4c-8033-f14fe9ce39dc',
authentication_response_status: 'Y',
three_ds_server_trans_id: '9bd9aa9c-3beb-4012-8e52-214cccb25ec5'
authentication_response_status: 'I',
three_ds_server_trans_id: '9bd9aa9c-3beb-4012-8e52-214cccb25ec5',
enrolled: 'true',
cavv: 'AJkCC1111111111122222222AAAA',
xid: '22222'
}
end
end
68 changes: 68 additions & 0 deletions test/unit/gateways/cecabank_rest_json_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@ def setup
initiator_vector: '0000000000000000'
)

@no_encrypted_gateway = CecabankJsonGateway.new(
merchant_id: '12345678',
acquirer_bin: '12345678',
terminal_id: '00000003',
cypher_key: 'enc_key'
)

@credit_card = credit_card
@amex_card = credit_card('374245455400001', { month: 10, year: Time.now.year + 1, verification_value: '1234' })
@amount = 100
Expand All @@ -31,6 +38,26 @@ def setup
authentication_response_status: 'Y',
three_ds_server_trans_id: '9bd9aa9c-3beb-4012-8e52-214cccb25ec5'
}

@apple_pay_network_token = network_tokenization_credit_card(
'4507670001000009',
eci: '05',
payment_cryptogram: 'xgQAAAAAAAAAAAAAAAAAAAAAAAAA',
month: '12',
year: Time.now.year,
source: :apple_pay,
verification_value: '989'
)

@google_pay_network_token = network_tokenization_credit_card(
'4507670001000009',
eci: '05',
payment_cryptogram: 'xgQAAAAAAAAAAAAAAAAAAAAAAAAA',
month: '12',
year: Time.now.year,
source: :google_pay,
verification_value: '989'
)
end

def test_successful_authorize
Expand Down Expand Up @@ -149,6 +176,38 @@ def test_purchase_without_exemption_type
end.respond_with(successful_purchase_response)
end

def test_successful_purchase_with_apple_pay
stub_comms(@no_encrypted_gateway, :ssl_post) do
@no_encrypted_gateway.purchase(@amount, @apple_pay_network_token, @options.merge(xid: 'some_xid'))
end.check_request do |_endpoint, data, _headers|
data = JSON.parse(data)
params = JSON.parse(Base64.decode64(data['parametros']))
common_ap_gp_assertions(params, @apple_pay_network_token, wallet_type: 'A')
end.respond_with(successful_purchase_response)
end

def test_successful_purchase_with_google_pay
stub_comms(@no_encrypted_gateway, :ssl_post) do
@no_encrypted_gateway.purchase(@amount, @google_pay_network_token, @options.merge(xid: 'some_xid'))
end.check_request do |_endpoint, data, _headers|
data = JSON.parse(data)
params = JSON.parse(Base64.decode64(data['parametros']))
common_ap_gp_assertions(params, @google_pay_network_token, wallet_type: 'G')
end.respond_with(successful_purchase_response)
end

def test_successful_purchase_with_apple_pay_encrypted_gateway
stub_comms do
@gateway.purchase(@amount, @apple_pay_network_token, @options.merge(xid: 'some_xid'))
end.check_request do |_endpoint, data, _headers|
data = JSON.parse(data)
encryoted_params = JSON.parse(Base64.decode64(data['parametros']))
sensitive_json = decrypt_sensitive_fields(@gateway.options, encryoted_params['encryptedData'])
sensitive_params = JSON.parse(sensitive_json)
common_ap_gp_assertions(sensitive_params, @apple_pay_network_token, wallet_type: 'A')
end.respond_with(successful_purchase_response)
end

def test_purchase_with_low_value_exemption
@options[:exemption_type] = 'low_value_exemption'
@options[:three_d_secure] = @three_d_secure
Expand Down Expand Up @@ -217,6 +276,15 @@ def decrypt_sensitive_fields(options, data)
cipher.update([data].pack('H*')) + cipher.final
end

def common_ap_gp_assertions(params, payment_method, wallet_type)
assert_include params, 'wallet'
assert_equal params['pan'], payment_method.number
wallet = JSON.parse(params['wallet'])
assert_equal wallet['authentication_value'], payment_method.payment_cryptogram
assert_equal wallet['xid'], 'some_xid'
assert_equal wallet['eci'], payment_method.eci
end

def transcript
<<~RESPONSE
"opening connection to tpv.ceca.es:443...\nopened\nstarting SSL for tpv.ceca.es:443...\nSSL established, protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384\n<- \"POST /tpvweb/rest/procesos/compra HTTP/1.1\\r\\nContent-Type: application/json\\r\\nHost: tpv.ceca.es\\r\\nConnection: close\\r\\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\\r\\nAccept: */*\\r\\nUser-Agent: Ruby\\r\\nContent-Length: 1397\\r\\n\\r\\n\"\n<- \"{\\\"parametros\\\":\\\"eyJhY2Npb24iOiJSRVNUX0FVVE9SSVpBQ0lPTiIsIm51bU9wZXJhY2lvbiI6ImYxZDdlNjBlMDYzMTJiNjI5NDEzOTUxM2YwMGQ2YWM4IiwiaW1wb3J0ZSI6IjEwMCIsInRpcG9Nb25lZGEiOiI5NzgiLCJleHBvbmVudGUiOiIyIiwiZW5jcnlwdGVkRGF0YSI6IjhlOWZhY2RmMDk5NDFlZTU0ZDA2ODRiNDNmNDNhMmRmOGM4ZWE5ODlmYTViYzYyOTM4ODFiYWVjNDFiYjU4OGNhNDc3MWI4OTFmNTkwMWVjMmJhZmJhOTBmMDNkM2NiZmUwNTJlYjAzMDU4Zjk1MGYyNzY4YTk3OWJiZGQxNmJlZmIyODQ2Zjc2MjkyYTFlODYzMDNhNTVhYTIzNjZkODA5MDEyYzlhNzZmYTZiOTQzOWNlNGQ3MzY5NTYwOTNhMDAwZTk5ZDMzNmVhZDgwMjBmOTk5YjVkZDkyMTFjMjE5ZWRhMjVmYjVkZDY2YzZiOTMxZWY3MjY5ZjlmMmVjZGVlYTc2MWRlMDEyZmFhMzg3MDlkODcyNTI4ODViYjI1OThmZDI2YTQzMzNhNDEwMmNmZTg4YjM1NTJjZWU0Yzc2IiwiZXhlbmNpb25TQ0EiOiJOT05FIiwiVGhyZWVEc1Jlc3BvbnNlIjoie1wiZXhlbXB0aW9uX3R5cGVcIjpudWxsLFwidGhyZWVfZHNfdmVyc2lvblwiOlwiMi4yLjBcIixcImRpcmVjdG9yeV9zZXJ2ZXJfdHJhbnNhY3Rpb25faWRcIjpcImEyYmYwODlmLWNlZmMtNGQyYy04NTBmLTkxNTM4MjdmZTA3MFwiLFwiYWNzX3RyYW5zYWN0aW9uX2lkXCI6XCIxOGMzNTNiMC03NmUzLTRhNGMtODAzMy1mMTRmZTljZTM5ZGNcIixcImF1dGhlbnRpY2F0aW9uX3Jlc3BvbnNlX3N0YXR1c1wiOlwiWVwiLFwidGhyZWVfZHNfc2VydmVyX3RyYW5zX2lkXCI6XCI5YmQ5YWE5Yy0zYmViLTQwMTItOGU1Mi0yMTRjY2NiMjVlYzVcIixcImVjb21tZXJjZV9pbmRpY2F0b3JcIjpcIjAyXCIsXCJlbnJvbGxlZFwiOm51bGwsXCJhbW91bnRcIjpcIjEwMFwifSIsIm1lcmNoYW50SUQiOiIxMDY5MDA2NDAiLCJhY3F1aXJlckJJTiI6IjAwMDA1NTQwMDAiLCJ0ZXJtaW5hbElEIjoiMDAwMDAwMDMifQ==\\\",\\\"cifrado\\\":\\\"SHA2\\\",\\\"firma\\\":\\\"ac7e5eb06b675be6c6f58487bbbaa1ddc07518e216cb0788905caffd911eea87\\\"}\"\n-> \"HTTP/1.1 200 OK\\r\\n\"\n-> \"Date: Thu, 14 Dec 2023 15:52:41 GMT\\r\\n\"\n-> \"Server: Apache\\r\\n\"\n-> \"Strict-Transport-Security: max-age=31536000; includeSubDomains\\r\\n\"\n-> \"X-XSS-Protection: 1; mode=block\\r\\n\"\n-> \"X-Content-Type-Options: nosniff\\r\\n\"\n-> \"Content-Length: 103\\r\\n\"\n-> \"Connection: close\\r\\n\"\n-> \"Content-Type: application/json\\r\\n\"\n-> \"\\r\\n\"\nreading 103 bytes...\n-> \"{\\\"cifrado\\\":\\\"SHA2\\\",\\\"parametros\\\":\\\"eyJudW1BdXQiOiIxMDEwMDAiLCJyZWZlcmVuY2lhIjoiMTIwMDQzOTQ4MzIzMTIxNDE2NDg0NjYwMDcwMDAiLCJjb2RBdXQiOiIwMDAifQ==\\\",\\\"firma\\\":\\\"5ce066be8892839d6aa6da15405c9be8987642f4245fac112292084a8532a538\\\",\\\"fecha\\\":\\\"231214164846089\\\",\\\"idProceso\\\":\\\"106900640-adeda8b09b84630d6247b53748ab9c66\\\"}\"\nread 300 bytes\nConn close\n"
Expand Down

0 comments on commit 32b58d6

Please sign in to comment.