lib/active_merchant/billing/gateways/monei.rb in activemerchant-1.121.0 vs lib/active_merchant/billing/gateways/monei.rb in activemerchant-1.123.0

- old
+ new

@@ -3,67 +3,64 @@ module ActiveMerchant #:nodoc: module Billing #:nodoc: # # == Monei gateway # This class implements Monei gateway for Active Merchant. For more information about Monei - # gateway please go to http://www.monei.net + # gateway please go to http://www.monei.com # # === Setup - # In order to set-up the gateway you need four paramaters: sender_id, channel_id, login and pwd. + # In order to set-up the gateway you need only one paramater: the api_key # Request that data to Monei. class MoneiGateway < Gateway - self.test_url = 'https://test.monei-api.net/payment/ctpe' - self.live_url = 'https://monei-api.net/payment/ctpe' + self.live_url = self.test_url = 'https://api.monei.com/v1/payments' self.supported_countries = %w[AD AT BE BG CA CH CY CZ DE DK EE ES FI FO FR GB GI GR HU IE IL IS IT LI LT LU LV MT NL NO PL PT RO SE SI SK TR US VA] self.default_currency = 'EUR' + self.money_format = :cents self.supported_cardtypes = %i[visa master maestro jcb american_express] - self.homepage_url = 'http://www.monei.net/' - self.display_name = 'Monei' + self.homepage_url = 'https://monei.com/' + self.display_name = 'MONEI' # Constructor # # options - Hash containing the gateway credentials, ALL MANDATORY - # :sender_id Sender ID - # :channel_id Channel ID - # :login User login - # :pwd User password + # :api_key Account's API KEY # def initialize(options = {}) - requires!(options, :sender_id, :channel_id, :login, :pwd) + requires!(options, :api_key) super end # Public: Performs purchase operation # # money - Amount of purchase - # credit_card - Credit card + # payment_method - Credit card # options - Hash containing purchase options # :order_id Merchant created id for the purchase # :billing_address Hash with billing address information # :description Merchant created purchase description (optional) # :currency Sale currency to override money object or default (optional) # # Returns Active Merchant response object - def purchase(money, credit_card, options = {}) - execute_new_order(:purchase, money, credit_card, options) + def purchase(money, payment_method, options = {}) + execute_new_order(:purchase, money, payment_method, options) end # Public: Performs authorization operation # # money - Amount to authorize - # credit_card - Credit card + # payment_method - Credit card # options - Hash containing authorization options # :order_id Merchant created id for the authorization # :billing_address Hash with billing address information # :description Merchant created authorization description (optional) # :currency Sale currency to override money object or default (optional) # # Returns Active Merchant response object - def authorize(money, credit_card, options = {}) - execute_new_order(:authorize, money, credit_card, options) + def authorize(money, payment_method, options = {}) + execute_new_order(:authorize, money, payment_method, options) end # Public: Performs capture operation on previous authorization # # money - Amount to capture @@ -107,125 +104,134 @@ execute_dependant(:void, nil, authorization, options) end # Public: Verifies credit card. Does this by doing a authorization of 1.00 Euro and then voiding it. # - # credit_card - Credit card + # payment_method - Credit card # options - Hash containing authorization options # :order_id Merchant created id for the authorization # :billing_address Hash with billing address information # :description Merchant created authorization description (optional) # :currency Sale currency to override money object or default (optional) # # Returns Active Merchant response object of Authorization operation - def verify(credit_card, options = {}) + def verify(payment_method, options = {}) MultiResponse.run(:use_first_response) do |r| - r.process { authorize(100, credit_card, options) } + r.process { authorize(100, payment_method, options) } r.process(:ignore_result) { void(r.authorization, options) } end end + def store(payment_method, options = {}) + execute_new_order(:store, 0, payment_method, options) + end + + def supports_scrubbing? + true + end + + def scrub(transcript) + transcript. + gsub(%r((Authorization: )\w+), '\1[FILTERED]'). + gsub(%r(("number\\?":\\?")[^"]*)i, '\1[FILTERED]'). + gsub(%r(("cvc\\?":\\?")[^"]*)i, '\1[FILTERED]'). + gsub(%r(("cavv\\?":\\?")[^"]*)i, '\1[FILTERED]') + end + private # Private: Execute purchase or authorize operation - def execute_new_order(action, money, credit_card, options) - request = build_request do |xml| - add_identification_new_order(xml, options) - add_payment(xml, action, money, options) - add_account(xml, credit_card) - add_customer(xml, credit_card, options) - add_three_d_secure(xml, options) - end - - commit(request) + def execute_new_order(action, money, payment_method, options) + request = build_request + add_identification_new_order(request, options) + add_transaction(request, action, money, options) + add_payment(request, payment_method) + add_customer(request, payment_method, options) + add_3ds_authenticated_data(request, options) + add_browser_info(request, options) + commit(request, action, options) end # Private: Execute operation that depends on authorization code from previous purchase or authorize operation def execute_dependant(action, money, authorization, options) - request = build_request do |xml| - add_identification_authorization(xml, authorization, options) - add_payment(xml, action, money, options) - end + request = build_request - commit(request) + add_identification_authorization(request, authorization, options) + add_transaction(request, action, money, options) + + commit(request, action, options) end - # Private: Build XML wrapping code yielding to code to fill the transaction information + # Private: Build request object def build_request - builder = Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml| - xml.Request(version: '1.0') do - xml.Header { xml.Security(sender: @options[:sender_id]) } - xml.Transaction(mode: test? ? 'CONNECTOR_TEST' : 'LIVE', response: 'SYNC', channel: @options[:channel_id]) do - xml.User(login: @options[:login], pwd: @options[:pwd]) - yield xml - end - end - end - builder.to_xml + request = {} + request[:livemode] = test? ? 'false' : 'true' + request end - # Private: Add identification part to XML for new orders - def add_identification_new_order(xml, options) + # Private: Add identification part to request for new orders + def add_identification_new_order(request, options) requires!(options, :order_id) - xml.Identification do - xml.TransactionID options[:order_id] - end + request[:orderId] = options[:order_id] end - # Private: Add identification part to XML for orders that depend on authorization from previous operation - def add_identification_authorization(xml, authorization, options) - xml.Identification do - xml.ReferenceID authorization - xml.TransactionID options[:order_id] - end + # Private: Add identification part to request for orders that depend on authorization from previous operation + def add_identification_authorization(request, authorization, options) + options[:paymentId] = authorization + request[:orderId] = options[:order_id] if options[:order_id] end - # Private: Add payment part to XML - def add_payment(xml, action, money, options) - code = tanslate_payment_code(action) - - xml.Payment(code: code) do - xml.Presentation do - xml.Amount amount(money) - xml.Currency options[:currency] || currency(money) - xml.Usage options[:description] || options[:order_id] - end unless money.nil? + # Private: Add payment part to request + def add_transaction(request, action, money, options) + request[:transactionType] = translate_payment_code(action) + request[:description] = options[:description] || options[:order_id] + unless money.nil? + request[:amount] = amount(money).to_i + request[:currency] = options[:currency] || currency(money) end end - # Private: Add account part to XML - def add_account(xml, credit_card) - xml.Account do - xml.Holder credit_card.name - xml.Number credit_card.number - xml.Brand credit_card.brand.upcase - xml.Expiry(month: credit_card.month, year: credit_card.year) - xml.Verification credit_card.verification_value + # Private: Add payment method to request + def add_payment(request, payment_method) + if payment_method.is_a? String + request[:paymentToken] = payment_method + else + request[:paymentMethod] = {} + request[:paymentMethod][:card] = {} + request[:paymentMethod][:card][:number] = payment_method.number + request[:paymentMethod][:card][:expMonth] = format(payment_method.month, :two_digits) + request[:paymentMethod][:card][:expYear] = format(payment_method.year, :two_digits) + request[:paymentMethod][:card][:cvc] = payment_method.verification_value.to_s end end - # Private: Add customer part to XML - def add_customer(xml, credit_card, options) - requires!(options, :billing_address) - address = options[:billing_address] - xml.Customer do - xml.Name do - xml.Given credit_card.first_name - xml.Family credit_card.last_name - end - xml.Address do - xml.Street address[:address1].to_s - xml.Zip address[:zip].to_s - xml.City address[:city].to_s - xml.State address[:state].to_s if address.has_key? :state - xml.Country address[:country].to_s - end - xml.Contact do - xml.Email options[:email] || 'noemail@monei.net' - xml.Ip options[:ip] || '0.0.0.0' - end + # Private: Add customer part to request + def add_customer(request, payment_method, options) + address = options[:billing_address] || options[:address] + + request[:customer] = {} + request[:customer][:email] = options[:email] || 'support@monei.net' + + if address + request[:customer][:name] = address[:name].to_s if address[:name] + + request[:billingDetails] = {} + request[:billingDetails][:email] = options[:email] if options[:email] + request[:billingDetails][:name] = address[:name] if address[:name] + request[:billingDetails][:company] = address[:company] if address[:company] + request[:billingDetails][:phone] = address[:phone] if address[:phone] + request[:billingDetails][:address] = {} + request[:billingDetails][:address][:line1] = address[:address1] if address[:address1] + request[:billingDetails][:address][:line2] = address[:address2] if address[:address2] + request[:billingDetails][:address][:city] = address[:city] if address[:city] + request[:billingDetails][:address][:state] = address[:state] if address[:state].present? + request[:billingDetails][:address][:zip] = address[:zip].to_s if address[:zip] + request[:billingDetails][:address][:country] = address[:country] if address[:country] end + + request[:sessionDetails] = {} + request[:sessionDetails][:ip] = options[:ip] if options[:ip] end # Private : Convert ECI to ResultIndicator # Possible ECI values: # 02 or 05 - Fully Authenticated Transaction @@ -243,95 +249,173 @@ else return '07' end end - # Private : Add the 3DSecure infos to XML - def add_three_d_secure(xml, options) - if options[:three_d_secure] - xml.Authentication(type: '3DSecure') do - xml.ResultIndicator eci_to_result_indicator options[:three_d_secure][:eci] - xml.Parameter(name: 'VERIFICATION_ID') { xml.text options[:three_d_secure][:cavv] } - xml.Parameter(name: 'XID') { xml.text options[:three_d_secure][:xid] } - end + # Private: add the already validated 3DSecure info to request + def add_3ds_authenticated_data(request, options) + if options[:three_d_secure] && options[:three_d_secure][:eci] && options[:three_d_secure][:xid] + add_3ds1_authenticated_data(request, options) + elsif options[:three_d_secure] + add_3ds2_authenticated_data(request, options) end end - # Private: Parse XML response from Monei servers + def add_3ds1_authenticated_data(request, options) + three_d_secure_options = options[:three_d_secure] + request[:paymentMethod][:card][:auth] = { + cavv: three_d_secure_options[:cavv], + cavvAlgorithm: three_d_secure_options[:cavv_algorithm], + eci: three_d_secure_options[:eci], + xid: three_d_secure_options[:xid], + directoryResponse: three_d_secure_options[:enrolled], + authenticationResponse: three_d_secure_options[:authentication_response_status] + } + end + + def add_3ds2_authenticated_data(request, options) + three_d_secure_options = options[:three_d_secure] + # If the transaction was authenticated in a frictionless flow, send the transStatus from the ARes. + if three_d_secure_options[:authentication_response_status].nil? + authentication_response = three_d_secure_options[:directory_response_status] + else + authentication_response = three_d_secure_options[:authentication_response_status] + end + request[:paymentMethod][:card][:auth] = { + threeDSVersion: three_d_secure_options[:version], + eci: three_d_secure_options[:eci], + cavv: three_d_secure_options[:cavv], + dsTransID: three_d_secure_options[:ds_transaction_id], + directoryResponse: three_d_secure_options[:directory_response_status], + authenticationResponse: authentication_response + } + end + + def add_browser_info(request, options) + request[:sessionDetails][:ip] = options[:ip] if options[:ip] + request[:sessionDetails][:userAgent] = options[:user_agent] if options[:user_agent] + end + + # Private: Parse JSON response from Monei servers def parse(body) - xml = Nokogiri::XML(body) + JSON.parse(body) + end + + def json_error(raw_response) + msg = 'Invalid response received from the MONEI API. Please contact support@monei.net if you continue to receive this message.' + msg += " (The raw response returned by the API was #{raw_response.inspect})" { - unique_id: xml.xpath('//Response/Transaction/Identification/UniqueID').text, - status: translate_status_code(xml.xpath('//Response/Transaction/Processing/Status/@code').text), - reason: translate_status_code(xml.xpath('//Response/Transaction/Processing/Reason/@code').text), - message: xml.xpath('//Response/Transaction/Processing/Return').text + 'status' => 'error', + 'message' => msg } end - # Private: Send XML transaction to Monei servers and create AM response - def commit(xml) + def response_error(raw_response) + parse(raw_response) + rescue JSON::ParserError + json_error(raw_response) + end + + def api_request(url, parameters, options = {}) + raw_response = response = nil + begin + raw_response = ssl_post(url, post_data(parameters), options) + response = parse(raw_response) + rescue ResponseError => e + raw_response = e.response.body + response = response_error(raw_response) + rescue JSON::ParserError + response = json_error(raw_response) + end + response + end + + # Private: Send transaction to Monei servers and create AM response + def commit(request, action, options) url = (test? ? test_url : live_url) + endpoint = translate_action_endpoint(action, options) + headers = { + 'Content-Type': 'application/json;charset=UTF-8', + 'Authorization': @options[:api_key], + 'User-Agent': 'MONEI/Shopify/0.1.0' + } - response = parse(ssl_post(url, post_data(xml), 'Content-Type' => 'application/x-www-form-urlencoded;charset=UTF-8')) + response = api_request(url + endpoint, params(request, action), headers) + success = success_from(response) Response.new( - success_from(response), - message_from(response), + success, + message_from(response, success), response, - authorization: authorization_from(response), + authorization: authorization_from(response, action), test: test?, - error_code: error_code_from(response) + error_code: error_code_from(response, success) ) end # Private: Decide success from servers response def success_from(response) - response[:status] == :success || response[:status] == :new + %w[ + SUCCEEDED + AUTHORIZED + REFUNDED + PARTIALLY_REFUNDED + CANCELED + ].include? response['status'] end # Private: Get message from servers response - def message_from(response) - response[:message] + def message_from(response, success) + success ? 'Transaction approved' : response.fetch('statusMessage', response.fetch('message', 'No error details')) end # Private: Get error code from servers response - def error_code_from(response) - success_from(response) ? nil : STANDARD_ERROR_CODE[:card_declined] + def error_code_from(response, success) + success ? nil : STANDARD_ERROR_CODE[:card_declined] end # Private: Get authorization code from servers response - def authorization_from(response) - response[:unique_id] + def authorization_from(response, action) + case action + when :store + return response['paymentToken'] + else + return response['id'] + end end # Private: Encode POST parameters - def post_data(xml) - "load=#{CGI.escape(xml)}" + def post_data(params) + params.clone.to_json end - # Private: Translate Monei status code to native ruby symbols - def translate_status_code(code) - { - '00' => :success, - '40' => :neutral, - '59' => :waiting_bank, - '60' => :rejected_bank, - '64' => :waiting_risk, - '65' => :rejected_risk, - '70' => :rejected_validation, - '80' => :waiting, - '90' => :new - }[code] + # Private: generate request params depending on action + def params(request, action) + request[:generatePaymentToken] = true if action == :store + request end # Private: Translate AM operations to Monei operations codes - def tanslate_payment_code(action) + def translate_payment_code(action) { - purchase: 'CC.DB', - authorize: 'CC.PA', - capture: 'CC.CP', - refund: 'CC.RF', - void: 'CC.RV' + purchase: 'SALE', + store: 'SALE', + authorize: 'AUTH', + capture: 'CAPTURE', + refund: 'REFUND', + void: 'CANCEL' + }[action] + end + + # Private: Translate AM operations to Monei endpoints + def translate_action_endpoint(action, options) + { + purchase: '', + store: '', + authorize: '', + capture: "/#{options[:paymentId]}/capture", + refund: "/#{options[:paymentId]}/refund", + void: "/#{options[:paymentId]}/cancel" }[action] end end end end