From 2adc3a7ac896ef7476b238ef7121e0dbda50a940 Mon Sep 17 00:00:00 2001 From: William Harris Date: Mon, 15 Jun 2015 04:16:34 -0400 Subject: [PATCH 1/4] super rough around the edges api for better spree paypal express basically just takes what the regular spree version does and lets you do it passing json also made simple poro model for returning the url to redirect to on paypal --- .../spree/api/paypal_controller.rb | 202 ++++++++++++++++++ app/models/spree/paypal.rb | 13 ++ config/routes.rb | 10 +- spree_paypal_express.gemspec | 2 + 4 files changed, 226 insertions(+), 1 deletion(-) create mode 100644 app/controllers/spree/api/paypal_controller.rb create mode 100644 app/models/spree/paypal.rb diff --git a/app/controllers/spree/api/paypal_controller.rb b/app/controllers/spree/api/paypal_controller.rb new file mode 100644 index 00000000..227ebe98 --- /dev/null +++ b/app/controllers/spree/api/paypal_controller.rb @@ -0,0 +1,202 @@ +module Spree + module Api + class PaypalController < Spree::Api::BaseController + ssl_allowed + + def express + + order = current_order || raise(ActiveRecord::RecordNotFound) + items = order.line_items.map(&method(:line_item)) + + tax_adjustments = order.all_adjustments.tax.additional + shipping_adjustments = order.all_adjustments.shipping + + order.all_adjustments.eligible.each do |adjustment| + next if (tax_adjustments + shipping_adjustments).include?(adjustment) + items << { + :Name => adjustment.label, + :Quantity => 1, + :Amount => { + :currencyID => order.currency, + :value => adjustment.amount + } + } + end + + # 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." + items.reject! do |item| + item[:Amount][:value].zero? + end + pp_request = provider.build_set_express_checkout(express_checkout_request_details(order, items)) + + # Trying to conform to https://guides.spreecommerce.com/api/summary.html + # as much as possible + begin + pp_response = provider.set_express_checkout(pp_request) + if pp_response.success? + url = provider.express_checkout_url(pp_response, :useraction => 'commit') + + response = Spree::Paypal.new + response.redirect_url = url + respond_with response + else + # this one is easy we can just respond with pp_response errors + render json: {errors:pp_response.errors.collect(&:long_message).join(" ")}, status: 500 + end + rescue SocketError + render json: {errors:[Spree.t('flash.connection_failed', :scope => 'paypal')]}, status: 500 + 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 => order.total, + :payment_method => payment_method + }) + + # using code from regular controller as psuedocode comment for what you are need to do on frontend now + # + # order.next # move order to next state + # + # if order.complete? + # flash.notice = Spree.t(:order_processed_successfully) + # redirect_to completion_route(order) + # else + # redirect_to checkout_state_path(order.state) + # end + respond_with order + end + + def cancel + # unneeded in api version where paypal calls back to client first + # you can just do this client side + # + # flash[:notice] = Spree.t('flash.cancel', :scope => 'paypal') + # order = current_order || raise(ActiveRecord::RecordNotFound) + # redirect_to checkout_state_path(order.state, paypal_cancel_token: params[:token]) + + render status: 200 + end + + private + + def current_order + @order = Spree::Order.find_by(number: order_id) + end + + def line_item(item) + { + :Name => item.product.name, + :Number => item.variant.sku, + :Quantity => item.quantity, + :Amount => { + :currencyID => item.order.currency, + :value => item.price + }, + :ItemCategory => "Physical" + } + end + + def express_checkout_request_details order, items + { :SetExpressCheckoutRequestDetails => { + :InvoiceID => order.number, + :BuyerEmail => order.email, + # Here we tell paypal redirect to client and have the client post back status to rails server + :ReturnURL => params[:confirm_url], + :CancelURL => params[:cancel_url], + :SolutionType => payment_method.preferred_solution.present? ? payment_method.preferred_solution : "Mark", + :LandingPage => payment_method.preferred_landing_page.present? ? payment_method.preferred_landing_page : "Billing", + :cppheaderimage => payment_method.preferred_logourl.present? ? payment_method.preferred_logourl : "", + :NoShipping => 1, + :PaymentDetails => [payment_details(items)] + }} + end + + + def payment_method + Spree::PaymentMethod.find(params[:payment_method_id]) + end + + def provider + payment_method.provider + end + + 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 + + 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 + }, + :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 + + def address_options + return {} unless address_required? + + address_to_bill = current_order.bill_address || current_order.ship_address + { + :Name => address_to_bill.try(:full_name), + :Street1 => address_to_bill.address1, + :Street2 => address_to_bill.address2, + :CityName => address_to_bill.city, + :Phone => address_to_bill.phone, + :StateOrProvince => address_to_bill.state_text, + :Country => address_to_bill.country.iso, + :PostalCode => address_to_bill.zipcode + } + end + + def completion_route(order) + order_path(order, :token => order.guest_token) + end + + def address_required? + payment_method.preferred_solution.eql?('Sole') + end + end + end +end diff --git a/app/models/spree/paypal.rb b/app/models/spree/paypal.rb new file mode 100644 index 00000000..7e9031ea --- /dev/null +++ b/app/models/spree/paypal.rb @@ -0,0 +1,13 @@ +module Spree + class Paypal + include ActiveModel::SerializerSupport + + ## simple poro to make serializing easy + # http://blog.honeybadger.io/poro-plain-old-ruby-object-tests-and-specs/ + # + # maybe this functionality should be rolled into + # the paypal express checkout object or be called paypal express idk + # + attr_accessor :redirect_url + end +end diff --git a/config/routes.rb b/config/routes.rb index 01b4dfc9..74c1769d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -15,4 +15,12 @@ end end end -end \ No newline at end of file + + namespace :api do + post '/paypal', :to => "paypal#express", :as => :paypal_express_api + post '/paypal/confirm', :to => "paypal#confirm", :as => :confirm_paypal_api + post '/paypal/cancel', :to => "paypal#cancel", :as => :cancel_paypal_api + get '/paypal/notify', :to => "paypal#notify", :as => :notify_paypal_api + end + +end diff --git a/spree_paypal_express.gemspec b/spree_paypal_express.gemspec index 083874ee..75779f8b 100644 --- a/spree_paypal_express.gemspec +++ b/spree_paypal_express.gemspec @@ -23,7 +23,9 @@ Gem::Specification.new do |s| s.requirements << 'none' s.add_dependency 'spree_core', '~> 2.4.0' + s.add_dependency 'spree_api', '~> 2.4.0' s.add_dependency 'paypal-sdk-merchant', '1.106.1' + s.add_dependency 'active_model_serializers', '~> 0.8.2' s.add_development_dependency 'capybara', '~> 2.1' s.add_development_dependency 'coffee-rails' From 60ab41b8774022a2402ba7be8dab7f45436f721d Mon Sep 17 00:00:00 2001 From: Kunal Chaudhari Date: Thu, 25 Jun 2015 23:51:16 +0530 Subject: [PATCH 2/4] fix missing template error --- app/controllers/spree/api/paypal_controller.rb | 6 +++--- app/views/spree/api/paypal/express.v1.rabl | 1 + spree_paypal_express.gemspec | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) create mode 100644 app/views/spree/api/paypal/express.v1.rabl diff --git a/app/controllers/spree/api/paypal_controller.rb b/app/controllers/spree/api/paypal_controller.rb index 227ebe98..2032e1f5 100644 --- a/app/controllers/spree/api/paypal_controller.rb +++ b/app/controllers/spree/api/paypal_controller.rb @@ -39,9 +39,9 @@ def express if pp_response.success? url = provider.express_checkout_url(pp_response, :useraction => 'commit') - response = Spree::Paypal.new - response.redirect_url = url - respond_with response + @paypal_response = Spree::Paypal.new + @paypal_response.redirect_url = url + respond_with @paypal_response else # this one is easy we can just respond with pp_response errors render json: {errors:pp_response.errors.collect(&:long_message).join(" ")}, status: 500 diff --git a/app/views/spree/api/paypal/express.v1.rabl b/app/views/spree/api/paypal/express.v1.rabl new file mode 100644 index 00000000..1f48032e --- /dev/null +++ b/app/views/spree/api/paypal/express.v1.rabl @@ -0,0 +1 @@ +object @paypal_response diff --git a/spree_paypal_express.gemspec b/spree_paypal_express.gemspec index 75779f8b..23eecb08 100644 --- a/spree_paypal_express.gemspec +++ b/spree_paypal_express.gemspec @@ -25,7 +25,7 @@ Gem::Specification.new do |s| s.add_dependency 'spree_core', '~> 2.4.0' s.add_dependency 'spree_api', '~> 2.4.0' s.add_dependency 'paypal-sdk-merchant', '1.106.1' - s.add_dependency 'active_model_serializers', '~> 0.8.2' + s.add_dependency 'active_model_serializers', '0.9.0.alpha1' s.add_development_dependency 'capybara', '~> 2.1' s.add_development_dependency 'coffee-rails' From 2135c6cc218b52483b33506d2abdfd936e4f1929 Mon Sep 17 00:00:00 2001 From: Kunal Chaudhari Date: Wed, 1 Jul 2015 11:15:09 +0530 Subject: [PATCH 3/4] fix express action to return proper response --- app/controllers/spree/api/paypal_controller.rb | 3 +-- app/views/spree/api/paypal/express.v1.rabl | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) delete mode 100644 app/views/spree/api/paypal/express.v1.rabl diff --git a/app/controllers/spree/api/paypal_controller.rb b/app/controllers/spree/api/paypal_controller.rb index 2032e1f5..4711a915 100644 --- a/app/controllers/spree/api/paypal_controller.rb +++ b/app/controllers/spree/api/paypal_controller.rb @@ -38,10 +38,9 @@ def express pp_response = provider.set_express_checkout(pp_request) if pp_response.success? url = provider.express_checkout_url(pp_response, :useraction => 'commit') - @paypal_response = Spree::Paypal.new @paypal_response.redirect_url = url - respond_with @paypal_response + render json: @paypal_response.to_json, status: 200 else # this one is easy we can just respond with pp_response errors render json: {errors:pp_response.errors.collect(&:long_message).join(" ")}, status: 500 diff --git a/app/views/spree/api/paypal/express.v1.rabl b/app/views/spree/api/paypal/express.v1.rabl deleted file mode 100644 index 1f48032e..00000000 --- a/app/views/spree/api/paypal/express.v1.rabl +++ /dev/null @@ -1 +0,0 @@ -object @paypal_response From f6f3b16f67e151fb6fdfc2674b15ded835664a57 Mon Sep 17 00:00:00 2001 From: MisinformedDNA Date: Thu, 2 Jul 2015 16:35:44 +0000 Subject: [PATCH 4/4] Fix response for confirm call --- app/controllers/spree/api/paypal_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/spree/api/paypal_controller.rb b/app/controllers/spree/api/paypal_controller.rb index 4711a915..d541e6ec 100644 --- a/app/controllers/spree/api/paypal_controller.rb +++ b/app/controllers/spree/api/paypal_controller.rb @@ -71,7 +71,7 @@ def confirm # else # redirect_to checkout_state_path(order.state) # end - respond_with order + render json: order.to_json, status: 200 end def cancel