module ActiveMerchant #:nodoc: module Billing #:nodoc: KB_PLUGIN_VERSION = Gem.loaded_specs['killbill-paypal-express'].version.version rescue nil module PaypalCommonAPI DUPLICATE_REQUEST_CODE = '11607' alias_method :original_successful?, :successful? # Note: this may need more thoughts when/if we want to support MsgSubID # See https://developer.paypal.com/docs/classic/express-checkout/integration-guide/ECRelatedAPIOps/#idempotency # For now, we just want to correctly handle a subsequent payment using a one-time token # (error "A successful transaction has already been completed for this token.") def successful?(response) response[:error_codes] == DUPLICATE_REQUEST_CODE ? false : original_successful?(response) end # Note: ActiveMerchant is missing InvoiceID in RefundTransactionReq. # See https://github.com/activemerchant/active_merchant/blob/v1.48.0/lib/active_merchant/billing/gateways/paypal/paypal_common_api.rb#L314 def build_refund_request(money, identification, options) xml = Builder::XmlMarkup.new xml.tag! 'RefundTransactionReq', 'xmlns' => PAYPAL_NAMESPACE do xml.tag! 'RefundTransactionRequest', 'xmlns:n2' => EBAY_NAMESPACE do xml.tag! 'n2:Version', API_VERSION xml.tag! 'TransactionID', identification xml.tag! 'Amount', amount(money), 'currencyID' => (options[:currency] || currency(money)) if money.present? xml.tag! 'RefundType', (options[:refund_type] || (money.present? ? 'Partial' : 'Full')) xml.tag! 'Memo', options[:note] unless options[:note].blank? xml.tag! 'InvoiceID', (options[:order_id] || options[:invoice_id]) unless (options[:order_id] || options[:invoice_id]).blank? end end xml.target! end def commit(action, request) response = parse(action, ssl_post(endpoint_url, build_request(request), build_headers)) build_response(successful?(response), message_from(response), response, :test => test?, :authorization => authorization_from(response), :fraud_review => fraud_review?(response), :avs_result => {:code => response[:avs_code]}, :cvv_result => response[:cvv2_code]) end def build_headers x_r_id = x_request_id headers = (@options[:headers] || {}).dup headers['Content-Type'] ||= 'text/xml' headers['User-Agent'] ||= user_agent headers['X-Request-Id'] ||= x_r_id unless x_r_id.blank? headers end def user_agent @@ua ||= JSON.dump({ :bindings_version => KB_PLUGIN_VERSION, :lang => 'ruby', :lang_version => "#{RUBY_VERSION} p#{RUBY_PATCHLEVEL} (#{RUBY_RELEASE_DATE})", :platform => RUBY_PLATFORM, :publisher => 'killbill' }) end def x_request_id # See KillbillMDCInsertingServletFilter org::slf4j::MDC::get('req.requestId') rescue nil end end end end