spree-contrib / better_spree_paypal_express

A better Spree PayPal Express Extension.
http://guides.spreecommerce.org
BSD 3-Clause "New" or "Revised" License
110 stars 270 forks source link

Paypal receives full amount when there are multiple payments on order #190

Open joeswann opened 7 years ago

joeswann commented 7 years ago

So I got here via quite a roundabout route. The long and short of it is I wanted paypal to receive a correct total when I had already applied a payment (in this case a gift card).

I am somewhat cheating in my solution, because in order to update the outstanding_balance I need to first process the gift card. I believe store_credit also has the ability to do a partial deduction so this might apply there too?

My current solution is to replace the payment_details function in app/controllers/spree/paypal_controller.rb with one that uses outstanding_balance.

This may not work if store credit amount doesn't process until the order state == complete. In that case maybe it would be better to manually add up uncaptured payment amounts (from methods that aren't the current payment method) and use that instead?

joeswann commented 7 years ago

Update: just using outstanding_balance doesn't work because capture doesn't generally fire until after the order is completed.

What I'm going to try instead is to calculate an order total that takes other (valid) payments into account when creating its total.

joeswann commented 7 years ago

Okay so I have this working, basically when it has other valid payments on the order it calculates the new total using current_order.total - payment_adjustment where payment_adjustment is attached to an item as follows

payment_adjustment = current_order.payments.valid.sum(:amount)

items << {
  Name: Spree.t(:payment_adjustment, :scope => 'paypal'), 
  Quantity: 1,
  Amount: {
    currencyID: current_order.currency,
    value: payment_adjustment * -1
  }
}
tibomogul commented 5 years ago

You also need to adjust the order.total in the confirm action, so that the payment recorded in spree tallies with that on Paypal

chozandrias76 commented 5 years ago

This is my full file to get paypal_express to work with store credit

Spree::PaypalController.class_eval do
  # rubocop:disable Metrics/AbcSize
  # rubocop:disable Metrics/CyclomaticComplexity
  # rubocop:disable Metrics/MethodLength
  # rubocop:disable Metrics/PerceivedComplexity
  def express
    order = current_order || raise(ActiveRecord::RecordNotFound)
    items = order.line_items.map(&method(:line_item))

    additional_adjustments = order.all_adjustments.additional
    tax_adjustments = additional_adjustments.tax
    shipping_adjustments = additional_adjustments.shipping

    additional_adjustments.eligible.each do |adjustment|
      # Because PayPal doesn't accept $0 items at all. See #10
      # https://cms.paypal.com/uk/cgi-bin/?cmd=_render-content&content_ID=developer/e_howto_api_ECCustomizing
      # "It can be a positive or negative value but not zero."
      next if adjustment.amount.zero?
      next if tax_adjustments.include?(adjustment) || shipping_adjustments.include?(adjustment)

      items << {
        Name: adjustment.label,
        Quantity: 1,
        Amount: {
          currencyID: order.currency,
          value: adjustment.amount
        }
      }
    end

    if current_order.using_store_credit?
      items << {
        Name: "Store Credits",
        Quantity: 1,
        Amount: {
          currencyID: current_order.currency,
          value: current_order.total_applied_store_credit * -1
        }
      }
    end

    pp_request = provider.build_set_express_checkout(express_checkout_request_details(order, items))

    begin
      pp_response = provider.set_express_checkout(pp_request)
      if pp_response.success?
        redirect_to provider.express_checkout_url(pp_response, useraction: 'commit')
      else
        flash[:error] = Spree.t('flash.generic_error', scope: 'paypal',
           reasons: pp_response.errors.map(&:long_message).join(" "))
        redirect_to checkout_state_path(:payment)
      end
    rescue SocketError
      flash[:error] = Spree.t('flash.connection_failed', scope: 'paypal')
      redirect_to checkout_state_path(:payment)
    end
  end

  def confirm
    order = current_order || raise(ActiveRecord::RecordNotFound)
    order.payments.create!(
      source: Spree::PaypalExpressCheckout.create(
        token: params[:token],
          payer_id: params[:PayerID]
      ),
      amount: current_order.using_store_credit? ? order.total - order.total_applied_store_credit : order.total,
      payment_method: payment_method
    )
    order.next
    if order.complete?
      flash.notice = Spree.t(:order_processed_successfully)
      flash[:order_completed] = true
      session[:order_id] = nil
      redirect_to completion_route(order)
    else
      redirect_to checkout_state_path(order.state)
    end
  end
  # rubocop:enable Metrics/AbcSize
  # rubocop:enable Metrics/CyclomaticComplexity
  # rubocop:enable Metrics/MethodLength
  # rubocop:enable Metrics/PerceivedComplexity

  # rubocop:disable Metrics/MethodLength
  def payment_details(items)
    # This retrieves the cost of shipping after promotions are applied
    # For example, if shippng costs $10, and is free with a promotion, shipment_sum is now $10
    shipment_sum = current_order.shipments.map(&:discounted_cost).sum

    # This calculates the item sum based upon what is in the order total, but not for shipping
    # or tax.  This is the easiest way to determine what the items should cost, as that
    # functionality doesn't currently exist in Spree core
    item_sum = current_order.total - shipment_sum - current_order.additional_tax_total -
               current_order.total_applied_store_credit

    # # Applying store credit to deduct from the order total
    # item_sum = item_sum - current_order.payments.where(payment_method_id: 8).sum(&:amount)

    if item_sum.zero?
      # Paypal does not support no items or a zero dollar ItemTotal
      # This results in the order summary being simply "Current purchase"
      {
        OrderTotal: {
          currencyID: current_order.currency,
          value: current_order.total
        }
      }
    else
      {
        OrderTotal: {
          currencyID: current_order.currency,
          value: current_order.total - current_order.total_applied_store_credit
        },
        ItemTotal: {
          currencyID: current_order.currency,
          value: item_sum
        },
        ShippingTotal: {
          currencyID: current_order.currency,
          value: shipment_sum,
        },
        TaxTotal: {
          currencyID: current_order.currency,
          value: current_order.additional_tax_total
        },
        ShipToAddress: address_options,
        PaymentDetailsItem: items,
        ShippingMethod: "Shipping Method Name Goes Here",
        PaymentAction: "Sale"
      }
    end
  end
  # rubocop:enable Metrics/MethodLength
end