Added PayPal Instant Update feature

This commit is contained in:
Vladimir Fedorov 2012-11-11 22:18:37 -05:00 committed by Jeff Dutil
parent 56930b93aa
commit ca60407d5d
7 changed files with 150 additions and 19 deletions

View File

@ -13,9 +13,10 @@ This extension allows the store to use PayPal Express from two locations:
sent to the PayPal review page (along with detailed order information). sent to the PayPal review page (along with detailed order information).
2. Cart Checkout (THIS FEATURE IS NOT YET COMPLETE) - Presents the PayPal checkout button on the users Cart page and redirects the user to complete 2. Cart Checkout* - Presents the PayPal checkout button on the users Cart page and redirects the user to complete
all shipping / addressing information on PaypPal's site. This also supports PayPal's Instant Update feature to retrieve shipping options live from all shipping / addressing information on PaypPal's site. This also supports PayPal's Instant Update feature to retrieve shipping options live from
Spree when the user selects / changes their shipping address on PayPal's site. Spree when the user selects / changes their shipping address on PayPal's site.
* Check "Checkout from cart" in admin for feature to work
This extension follows the documented flow for a PayPal Express Checkout, where a user is forwarded to PayPal to allow them to login and review This extension follows the documented flow for a PayPal Express Checkout, where a user is forwarded to PayPal to allow them to login and review
the order (possibly select / change shipping address and method), then the user is redirected back to Spree to confirm the order. The user the order (possibly select / change shipping address and method), then the user is redirected back to Spree to confirm the order. The user

View File

@ -29,7 +29,12 @@ module Spree
def paypal_payment def paypal_payment
load_order load_order
opts = all_opts(@order,params[:payment_method_id], 'payment') opts = all_opts(@order,params[:payment_method_id], 'payment')
unless payment_method.preferred_cart_checkout
opts.merge!(address_options(@order)) opts.merge!(address_options(@order))
else
opts.merge!(shipping_options)
end
@gateway = paypal_gateway @gateway = paypal_gateway
if Spree::Config[:auto_capture] if Spree::Config[:auto_capture]
@ -61,6 +66,8 @@ module Spree
if @ppx_details.success? if @ppx_details.success?
# now save the updated order info # now save the updated order info
#TODO Search for existing records
Spree::PaypalAccount.create(:email => @ppx_details.params["payer"], Spree::PaypalAccount.create(:email => @ppx_details.params["payer"],
:payer_id => @ppx_details.params["payer_id"], :payer_id => @ppx_details.params["payer_id"],
:payer_country => @ppx_details.params["payer_country"], :payer_country => @ppx_details.params["payer_country"],
@ -90,11 +97,18 @@ module Spree
@order.ship_address = order_ship_address @order.ship_address = order_ship_address
@order.bill_address ||= order_ship_address @order.bill_address ||= order_ship_address
#Add Instant Update Shipping
if payment_method.preferred_cart_checkout
add_shipping_charge
end
end end
@order.state = "payment" @order.state = "payment"
@order.save @order.save
if payment_method.preferred_review if payment_method.preferred_review
@order.next @order.next
render 'spree/shared/paypal_express_confirm' render 'spree/shared/paypal_express_confirm'
else else
@ -283,6 +297,15 @@ module Spree
credits_total = credits.map {|i| i[:amount] * i[:quantity] }.sum credits_total = credits.map {|i| i[:amount] * i[:quantity] }.sum
end end
unless @order.payment_method.preferred_cart_checkout
order_total = (order.total * 100).to_i
shipping_total = (order.ship_total*100).to_i
else
shipping_cost = shipping_options[:shipping_options].first[:amount]
order_total = (order.total * 100 + (shipping_cost)).to_i
shipping_total = (shipping_cost).to_i
end
opts = { :return_url => paypal_confirm_order_checkout_url(order, :payment_method_id => payment_method), opts = { :return_url => paypal_confirm_order_checkout_url(order, :payment_method_id => payment_method),
:cancel_return_url => edit_order_checkout_url(order, :state => :payment), :cancel_return_url => edit_order_checkout_url(order, :state => :payment),
:order_id => order.number, :order_id => order.number,
@ -290,8 +313,9 @@ module Spree
:items => items, :items => items,
:subtotal => ((order.item_total * 100) + credits_total).to_i, :subtotal => ((order.item_total * 100) + credits_total).to_i,
:tax => (order.tax_total*100).to_i, :tax => (order.tax_total*100).to_i,
:shipping => (order.ship_total*100).to_i, :shipping => shipping_total,
:money => (order.total * 100 ).to_i } :money => order_total,
:max_amount => (order.total * 300).to_i}
if stage == "checkout" if stage == "checkout"
opts[:handling] = 0 opts[:handling] = 0
@ -302,12 +326,40 @@ module Spree
#hack to add float rounding difference in as handling fee - prevents PayPal from rejecting orders #hack to add float rounding difference in as handling fee - prevents PayPal from rejecting orders
#because the integer totals are different from the float based total. This is temporary and will be #because the integer totals are different from the float based total. This is temporary and will be
#removed once Spree's currency values are persisted as integers (normally only 1c) #removed once Spree's currency values are persisted as integers (normally only 1c)
if @order.payment_method.preferred_cart_checkout
opts[:handling] = 0
else
opts[:handling] = (order.total*100).to_i - opts.slice(:subtotal, :tax, :shipping).values.sum opts[:handling] = (order.total*100).to_i - opts.slice(:subtotal, :tax, :shipping).values.sum
end end
end
opts opts
end end
def shipping_options
#Uses users address if exists, if not uses first shipping method
if (current_user.present? && current_user.addresses.present?)
estimate_shipping_for_user
shipping_default = @rate_hash_user.map.with_index do |shipping_method, idx|
{ :default => (idx == 0 ? true : false),
:name => shipping_method.name,
:amount => (shipping_method.cost*100).to_i }
end
else
shipping_method = ShippingMethod.all.first
shipping_default = [{ :default => true,
:name => shipping_method.name,
:amount => ((shipping_method.calculator.compute(self).to_f) * 100).to_i }]
end
{
:callback_url => spree_root_url + "paypal_shipping_update",
:callback_timeout => 6,
:callback_version => '61.0',
:shipping_options => shipping_default
}
end
def address_options(order) def address_options(order)
if payment_method.preferred_no_shipping if payment_method.preferred_no_shipping
{ :no_shipping => true } { :no_shipping => true }
@ -346,22 +398,19 @@ module Spree
def flat_rate_shipping_and_handling_options(order, stage) def flat_rate_shipping_and_handling_options(order, stage)
# max_fallback = 0.0 # max_fallback = 0.0
# shipping_options = ShippingMethod.all.map do |shipping_method| # shipping_options = ShippingMethod.all.map do |shipping_method|
# max_fallback = shipping_method.fallback_amount if shipping_method.fallback_amount > max_fallback # { :name => "#{shipping_method.name}",
# { :name => "#{shipping_method.id}", # :amount => (shipping_method.rate),
# :label => "#{shipping_method.name} - #{shipping_method.zone.name}",
# :amount => (shipping_method.fallback_amount*100) + 1,
# :default => shipping_method.is_default } # :default => shipping_method.is_default }
# end # end
#
#
# default_shipping_method = ShippingMethod.find(:first, :conditions => {:is_default => true}) # default_shipping_method = ShippingMethod.find(:first, :conditions => {:is_default => true})
#
# opts = { :shipping_options => shipping_options, # opts = { :shipping_options => shipping_options,
# :max_amount => (order.total + max_fallback)*100
# } # }
#
# opts[:shipping] = (default_shipping_method.nil? ? 0 : default_shipping_method.fallback_amount) if stage == "checkout" # #opts[:shipping] = (default_shipping_method.nil? ? 0 : default_shipping_method.fallback_amount) if stage == "checkout"
#
# opts # opts
{} {}
end end
@ -389,5 +438,35 @@ module Spree
payment_method.provider payment_method.provider
end end
def add_shipping_charge
# Replace with these changes once Active_Merchant pushes pending pull request
# shipment_name = @ppx_details.shipping['amount'].chomp(" Shipping")
# shipment_cost = @ppx_details.shipping['name'].to_f
shipment_name = @ppx_details.params['UserSelectedOptions']['ShippingOptionName'].chomp(" Shipping")
shipment_cost = @ppx_details.params['UserSelectedOptions']['ShippingOptionAmount'].to_f
if @order.shipping_method_id.blank? && @order.rate_hash.present?
selected_shipping = @order.rate_hash.detect { |v| v['name'] == shipment_name && v['cost'] == shipment_cost }
@order.shipping_method_id = selected_shipping.id
end
@order.shipments.each { |s| s.destroy unless s.shipping_method.available_to_order?(@order) }
@order.create_shipment!
@order.update!
end
def estimate_shipping_for_user
zipcode = current_user.addresses.first.zipcode
country = current_user.addresses.first.country.iso
shipping_methods = Spree::ShippingMethod.all
#TODO remove hard coded shipping
#Make a deep copy of the order object then stub out the parts required to get a shipping quote
@shipping_order = Marshal::load(Marshal.dump(@order)) #Make a deep copy of the order object
@shipping_order.ship_address = Spree::Address.new(:country => Spree::Country.find_by_iso(country), :zipcode => zipcode)
shipment = Spree::Shipment.new(:address => @shipping_order.ship_address)
@shipping_order.ship_address.shipments<<shipment
@shipping_order.shipments<<shipment
@rate_hash_user = @shipping_order.rate_hash
#TODO
end
end end
end end

View File

@ -8,7 +8,6 @@ module Spree
def notify def notify
retrieve_details #need to retreive details first to ensure ActiveMerchant gets configured correctly. retrieve_details #need to retreive details first to ensure ActiveMerchant gets configured correctly.
@notification = Paypal::Notification.new(request.raw_post) @notification = Paypal::Notification.new(request.raw_post)
# we only care about eChecks (for now?) # we only care about eChecks (for now?)
@ -29,6 +28,39 @@ module Spree
render :nothing => true render :nothing => true
end end
def shipping_estimate
#details from Paypal
if request.post?
@method = params[:METHOD]
@version = params[:CALLBACKVERSION]
@token = params[:TOKEN]
@currency = params[:CURRENCYCODE]
@locale = params[:LOCALECODE]
@street = params[:SHIPTOSTREET]
@street2 = params[:SHIPTOSTREET2]
@city = params[:SHIPTOCITY]
@state = params[:SHIPTOSTATE]
@country = params[:SHIPTOCOUNTRY]
@zip = params[:SHIPTOZIP]
end
#available shipping based on paypal details
estimate_shipping_and_taxes
payment_methods_atts2 = {}
@rate_hash.each_with_index do |shipping_method, idx|
payment_methods_atts2["L_TAXAMT#{idx}"] = @order.tax_total #TODO need to calculate based on shipping method
payment_methods_atts2["L_SHIPPINGOPTIONAMOUNT#{idx}"] = shipping_method.cost
payment_methods_atts2["L_SHIPPINGOPTIONNAME#{idx}"] = shipping_method.name
payment_methods_atts2["L_SHIPPINGOPTIONLABEL#{idx}"] = "Shipping" #Do not change, required field
payment_methods_atts2["L_SHIPPINGOPTIONISDEFAULT#{idx}"] = (idx == 0 ? true : false)
end
#compiles NVP query used by paypal callback
query = payment_methods_atts2.inject('METHOD=CallbackResponse&CALLBACKVERSION=61&OFFERINSURANCEOPTION=false') { |string, pair| string + '&' + pair[0].to_s + '=' + pair[1].to_s }
render :text => query #query read by PayPal
end
private private
def retrieve_details def retrieve_details
@order = Spree::Order.find_by_number(params["invoice"]) @order = Spree::Order.find_by_number(params["invoice"])
@ -40,5 +72,19 @@ module Spree
end end
end end
def estimate_shipping_and_taxes
@order = Spree::Order.find_by_number(current_order(true).number)
zipcode = @zip
shipping_methods = Spree::ShippingMethod.all
#TODO remove hard coded shipping
#Make a deep copy of the order object then stub out the parts required to get a shipping quote
@shipping_order = Marshal::load(Marshal.dump(@order)) #Make a deep copy of the order object
@shipping_order.ship_address = Spree::Address.new(:country => Spree::Country.find_by_iso("#{@country}"), :zipcode => zipcode)
shipment = Spree::Shipment.new(:address => @shipping_order.ship_address)
@shipping_order.ship_address.shipments<<shipment
@shipping_order.shipments<<shipment
@rate_hash = @shipping_order.rate_hash
end
end end
end end

View File

@ -4,10 +4,11 @@ class Spree::BillingIntegration::PaypalExpressBase < Spree::BillingIntegration
preference :signature, :string preference :signature, :string
preference :review, :boolean, :default => false preference :review, :boolean, :default => false
preference :no_shipping, :boolean, :default => false preference :no_shipping, :boolean, :default => false
preference :cart_checkout, :boolean, :default => false
preference :currency, :string, :default => 'USD' preference :currency, :string, :default => 'USD'
preference :allow_guest_checkout, :boolean, :default => false preference :allow_guest_checkout, :boolean, :default => false
attr_accessible :preferred_login, :preferred_password, :preferred_signature, :preferred_review, :preferred_no_shipping, :preferred_currency, :preferred_allow_guest_checkout, :preferred_server, :preferred_test_mode attr_accessible :preferred_login, :preferred_password, :preferred_signature, :preferred_review, :preferred_no_shipping, :preferred_currency, :preferred_allow_guest_checkout, :preferred_server, :preferred_test_mode, :preferred_cart_checkout
def provider_class def provider_class
ActiveMerchant::Billing::PaypalExpressGateway ActiveMerchant::Billing::PaypalExpressGateway

View File

@ -12,6 +12,7 @@ en-GB:
result: Result result: Result
review: Review review: Review
no_shipping: No Shipping no_shipping: No Shipping
cart_checkout: Checkout From Cart
paypal_account: PayPal Account paypal_account: PayPal Account
payer_id: Payer ID payer_id: Payer ID
payer_country: Country payer_country: Country

View File

@ -14,6 +14,7 @@ en:
result: Result result: Result
review: Review review: Review
no_shipping: No Shipping no_shipping: No Shipping
cart_checkout: Checkout From Cart
paypal_account: PayPal Account paypal_account: PayPal Account
payer_id: Payer ID payer_id: Payer ID
payer_country: Country payer_country: Country

View File

@ -12,6 +12,8 @@ Spree::Core::Engine.routes.draw do
match '/paypal_notify' => 'paypal_express_callbacks#notify', :via => [:get, :post] match '/paypal_notify' => 'paypal_express_callbacks#notify', :via => [:get, :post]
match '/paypal_shipping_update' => 'paypal_express_callbacks#shipping_estimate', :via => :post
namespace :admin do namespace :admin do
resources :orders do resources :orders do
resources :paypal_payments do resources :paypal_payments do