From ca60407d5d95f3b50f72a73f81a5d3427340bf30 Mon Sep 17 00:00:00 2001 From: Vladimir Fedorov Date: Sun, 11 Nov 2012 22:18:37 -0500 Subject: [PATCH] Added PayPal Instant Update feature --- README.markdown | 3 +- .../spree/checkout_controller_decorator.rb | 111 +++++++++++++++--- .../paypal_express_callbacks_controller.rb | 48 +++++++- .../paypal_express_base.rb | 3 +- config/locales/en-GB.yml | 1 + config/locales/en.yml | 1 + config/routes.rb | 2 + 7 files changed, 150 insertions(+), 19 deletions(-) diff --git a/README.markdown b/README.markdown index 0fba39d..c3c9576 100644 --- a/README.markdown +++ b/README.markdown @@ -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). - 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 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 the order (possibly select / change shipping address and method), then the user is redirected back to Spree to confirm the order. The user diff --git a/app/controllers/spree/checkout_controller_decorator.rb b/app/controllers/spree/checkout_controller_decorator.rb index 1c6bde4..66a7cad 100644 --- a/app/controllers/spree/checkout_controller_decorator.rb +++ b/app/controllers/spree/checkout_controller_decorator.rb @@ -29,7 +29,12 @@ module Spree def paypal_payment load_order opts = all_opts(@order,params[:payment_method_id], 'payment') - opts.merge!(address_options(@order)) + unless payment_method.preferred_cart_checkout + opts.merge!(address_options(@order)) + else + opts.merge!(shipping_options) + end + @gateway = paypal_gateway if Spree::Config[:auto_capture] @@ -61,6 +66,8 @@ module Spree if @ppx_details.success? # now save the updated order info + #TODO Search for existing records + Spree::PaypalAccount.create(:email => @ppx_details.params["payer"], :payer_id => @ppx_details.params["payer_id"], :payer_country => @ppx_details.params["payer_country"], @@ -90,11 +97,18 @@ module Spree @order.ship_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 @order.state = "payment" @order.save if payment_method.preferred_review + @order.next render 'spree/shared/paypal_express_confirm' else @@ -231,7 +245,7 @@ module Spree :background_color => "ffffff", # must be hex only, six chars :header_background_color => "ffffff", :header_border_color => "ffffff", - :header_image => chosen_image, + :header_image => chosen_image, :allow_note => true, :locale => user_locale, :req_confirm_shipping => false, # for security, might make an option later @@ -283,6 +297,15 @@ module Spree credits_total = credits.map {|i| i[:amount] * i[:quantity] }.sum 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), :cancel_return_url => edit_order_checkout_url(order, :state => :payment), :order_id => order.number, @@ -290,8 +313,9 @@ module Spree :items => items, :subtotal => ((order.item_total * 100) + credits_total).to_i, :tax => (order.tax_total*100).to_i, - :shipping => (order.ship_total*100).to_i, - :money => (order.total * 100 ).to_i } + :shipping => shipping_total, + :money => order_total, + :max_amount => (order.total * 300).to_i} if stage == "checkout" opts[:handling] = 0 @@ -302,12 +326,40 @@ module Spree #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 #removed once Spree's currency values are persisted as integers (normally only 1c) - opts[:handling] = (order.total*100).to_i - opts.slice(:subtotal, :tax, :shipping).values.sum + 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 + end end opts 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) if payment_method.preferred_no_shipping { :no_shipping => true } @@ -346,22 +398,19 @@ module Spree def flat_rate_shipping_and_handling_options(order, stage) # max_fallback = 0.0 # shipping_options = ShippingMethod.all.map do |shipping_method| - # max_fallback = shipping_method.fallback_amount if shipping_method.fallback_amount > max_fallback - # { :name => "#{shipping_method.id}", - # :label => "#{shipping_method.name} - #{shipping_method.zone.name}", - # :amount => (shipping_method.fallback_amount*100) + 1, + # { :name => "#{shipping_method.name}", + # :amount => (shipping_method.rate), # :default => shipping_method.is_default } # end - # - # + + # default_shipping_method = ShippingMethod.find(:first, :conditions => {:is_default => true}) - # + # 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 {} end @@ -389,5 +438,35 @@ module Spree payment_method.provider 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< true 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 def retrieve_details @order = Spree::Order.find_by_number(params["invoice"]) @@ -40,5 +72,19 @@ module Spree 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< false preference :no_shipping, :boolean, :default => false + preference :cart_checkout, :boolean, :default => false preference :currency, :string, :default => 'USD' 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 ActiveMerchant::Billing::PaypalExpressGateway diff --git a/config/locales/en-GB.yml b/config/locales/en-GB.yml index 43d57e9..8b6f83f 100644 --- a/config/locales/en-GB.yml +++ b/config/locales/en-GB.yml @@ -12,6 +12,7 @@ en-GB: result: Result review: Review no_shipping: No Shipping + cart_checkout: Checkout From Cart paypal_account: PayPal Account payer_id: Payer ID payer_country: Country diff --git a/config/locales/en.yml b/config/locales/en.yml index 1208f78..930bc70 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -14,6 +14,7 @@ en: result: Result review: Review no_shipping: No Shipping + cart_checkout: Checkout From Cart paypal_account: PayPal Account payer_id: Payer ID payer_country: Country diff --git a/config/routes.rb b/config/routes.rb index ee82a60..86ac6ee 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -12,6 +12,8 @@ Spree::Core::Engine.routes.draw do match '/paypal_notify' => 'paypal_express_callbacks#notify', :via => [:get, :post] + match '/paypal_shipping_update' => 'paypal_express_callbacks#shipping_estimate', :via => :post + namespace :admin do resources :orders do resources :paypal_payments do