module ActiveMerchant #:nodoc: module Billing #:nodoc: class XpayGateway < Gateway self.display_name = 'XPay Gateway' self.homepage_url = 'https://developer.nexi.it/en' self.test_url = 'https://xpaysandbox.nexigroup.com/api/phoenix-0.0/psp/api/v1/' self.live_url = 'https://xpay.nexigroup.com/api/phoenix-0.0/psp/api/v1/' self.supported_countries = %w(AT BE CY EE FI FR DE GR IE IT LV LT LU MT PT SK SI ES BG HR DK NO PL RO RO SE CH HU) self.default_currency = 'EUR' self.currencies_without_fractions = %w(BGN HRK DKK NOK GBP PLN CZK RON SEK CHF HUF) self.money_format = :cents self.supported_cardtypes = %i[visa master maestro american_express jcb] ENDPOINTS_MAPPING = { validation: 'orders/3steps/validation', purchase: 'orders/3steps/payment', authorize: 'orders/3steps/payment', preauth: 'orders/3steps/init', capture: 'operations/%s/captures', verify: 'orders/card_verification', refund: 'operations/%s/refunds' } SUCCESS_MESSAGES = %w(PENDING AUTHORIZED THREEDS_VALIDATED EXECUTED).freeze def initialize(options = {}) requires!(options, :api_key) @api_key = options[:api_key] super end def preauth(amount, credit_card, options = {}) order_request(:preauth, amount, {}, credit_card, options) end def purchase(amount, credit_card, options = {}) complete_order_request(:purchase, amount, credit_card, options) end def authorize(amount, credit_card, options = {}) complete_order_request(:authorize, amount, credit_card, options) end def capture(amount, authorization, options = {}) operation_request(:capture, amount, authorization, options) end def refund(amount, authorization, options = {}) operation_request(:refund, amount, authorization, options) end def verify(credit_card, options = {}) post = {} add_invoice(post, 0, options) add_customer_data(post, credit_card, options) add_credit_card(post, credit_card) commit(:verify, post, options) end def supports_scrubbing? true end def scrub(transcript) transcript. gsub(%r((X-Api-Key: )(\w|-)+), '\1[FILTERED]'). gsub(%r(("pan\\?"\s*:\s*\\?")[^"]*)i, '\1[FILTERED]'). gsub(%r(("cvv\\?":\\?")\d+), '\1[FILTERED]') end private def validation(options = {}) post = {} add_3ds_validation_params(post, options) commit(:validation, post, options) end def complete_order_request(action, amount, credit_card, options = {}) MultiResponse.run do |r| r.process { validation(options) } r.process { order_request(action, amount, { captureType: (action == :authorize ? 'EXPLICIT' : 'IMPLICIT') }, credit_card, options.merge!(validation: r.params)) } end end def order_request(action, amount, post, credit_card, options = {}) add_invoice(post, amount, options) add_credit_card(post, credit_card) add_customer_data(post, credit_card, options) add_address(post, options) add_recurrence(post, options) unless options[:operation_id] add_exemptions(post, options) add_3ds_params(post, options[:validation]) if options[:validation] commit(action, post, options) end def operation_request(action, amount, authorization, options) options[:correlation_id], options[:reference] = authorization.split('#') commit(action, { amount: amount, currency: options[:currency] }, options) end def add_invoice(post, amount, options) currency = options[:currency] || currency(amount) post[:order] = { orderId: options[:order_id], amount: localized_amount(amount, currency), currency: currency }.compact end def add_credit_card(post, credit_card) post[:card] = { pan: credit_card.number, expiryDate: expdate(credit_card), cvv: credit_card.verification_value } end def add_customer_data(post, credit_card, options) post[:order][:customerInfo] = { cardHolderName: credit_card.name, cardHolderEmail: options[:email] }.compact end def add_address(post, options) if address = options[:billing_address] || options[:address] post[:order][:customerInfo][:billingAddress] = { name: address[:name], street: address[:address1], additionalInfo: address[:address2], city: address[:city], postCode: address[:zip], country: address[:country] }.compact end if address = options[:shipping_address] post[:order][:customerInfo][:shippingAddress] = { name: address[:name], street: address[:address1], additionalInfo: address[:address2], city: address[:city], postCode: address[:zip], country: address[:country] }.compact end end def add_recurrence(post, options) post[:recurrence] = { action: options[:recurrence] || 'NO_RECURRING' } end def add_exemptions(post, options) post[:exemptions] = options[:exemptions] || 'NO_PREFERENCE' end def add_3ds_params(post, validation) post[:threeDSAuthData] = { authenticationValue: validation['threeDSAuthResult']['authenticationValue'], eci: validation['threeDSAuthResult']['eci'], xid: validation['threeDSAuthResult']['xid'] } post[:operationId] = validation['operation']['operationId'] end def add_3ds_validation_params(post, options) post[:operationId] = options[:operation_id] post[:threeDSAuthResponse] = options[:three_ds_auth_response] end def parse(body) JSON.parse(body) end def commit(action, params, options) options[:correlation_id] ||= SecureRandom.uuid transaction_id = transaction_id_from(params, options, action) raw_response = begin url = build_request_url(action, transaction_id) ssl_post(url, params.to_json, request_headers(options, action)) rescue ResponseError => e { errors: [code: e.response.code, description: e.response.body] }.to_json end response = parse(raw_response) Response.new( success_from(action, response), message_from(response), response, authorization: authorization_from(options[:correlation_id], response), test: test?, error_code: error_code_from(response) ) end def request_headers(options, action = nil) headers = { 'X-Api-Key' => @api_key, 'Content-Type' => 'application/json', 'Correlation-Id' => options[:correlation_id] } headers.merge!('Idempotency-Key' => options[:idempotency_key] || SecureRandom.uuid) if %i[capture refund].include?(action) headers end def transaction_id_from(params, options, action = nil) case action when :refund, :capture return options[:reference] else return params[:operation_id] end end def build_request_url(action, id = nil) "#{test? ? test_url : live_url}#{ENDPOINTS_MAPPING[action.to_sym] % id}" end def success_from(action, response) case action when :capture, :refund response.include?('operationId') && response.include?('operationTime') else SUCCESS_MESSAGES.include?(response.dig('operation', 'operationResult')) end end def message_from(response) response['operationId'] || response.dig('operation', 'operationResult') || response.dig('errors', 0, 'description') end def authorization_from(correlation_id, response = {}) [correlation_id, (response['operationId'] || response.dig('operation', 'operationId'))].join('#') end def error_code_from(response) response.dig('errors', 0, 'code') end end end end