From b79fb41c4d280c9ea0b5044b00c78a210828895b Mon Sep 17 00:00:00 2001 From: reaper Date: Sun, 28 Nov 2010 23:31:31 +0900 Subject: [PATCH] Modified to work with Spree 0.30.1. --- LICENSE | 23 +++ Rakefile | 129 ++----------- app/models/paypal_account.rb | 90 ++++----- .../source_views/_paypalexpress.html.erb | 19 +- .../payment/_paypalexpress.html.erb | 0 ...ml.erb => paypal_express_confirm.html.erb} | 4 +- config/locales/en-GB.yml | 2 +- config/locales/en.yml | 2 +- config/routes.rb | 36 ++-- ...heckouts_controller_with_paypal_express.rb | 2 +- lib/spree/paypal_express.rb | 178 ++++++++---------- lib/spree_paypal_express.rb | 29 +++ lib/spree_paypal_express_hooks.rb | 3 + lib/tasks/spree_paypal_express.rake | 26 +++ spec/spec_helper.rb | 31 +++ spree_paypal_express.gemspec | 21 +++ 16 files changed, 320 insertions(+), 275 deletions(-) create mode 100644 LICENSE rename app/views/{checkouts => checkout}/payment/_paypalexpress.html.erb (100%) rename app/views/shared/{_paypal_express_confirm.html.erb => paypal_express_confirm.html.erb} (67%) create mode 100644 lib/spree_paypal_express.rb create mode 100644 lib/spree_paypal_express_hooks.rb create mode 100644 lib/tasks/spree_paypal_express.rake create mode 100644 spec/spec_helper.rb create mode 100644 spree_paypal_express.gemspec diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5800638 --- /dev/null +++ b/LICENSE @@ -0,0 +1,23 @@ +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name of the Rails Dog LLC nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/Rakefile b/Rakefile index 2070887..09dbc4e 100644 --- a/Rakefile +++ b/Rakefile @@ -1,120 +1,31 @@ -# I think this is the one that should be moved to the extension Rakefile template - -# In rails 1.2, plugins aren't available in the path until they're loaded. -# Check to see if the rspec plugin is installed first and require -# it if it is. If not, use the gem version. - -# Determine where the RSpec plugin is by loading the boot -unless defined? SPREE_ROOT - ENV["RAILS_ENV"] = "test" - case - when ENV["SPREE_ENV_FILE"] - require File.dirname(ENV["SPREE_ENV_FILE"]) + "/boot" - when File.dirname(__FILE__) =~ %r{vendor/SPREE/vendor/extensions} - require "#{File.expand_path(File.dirname(__FILE__) + "/../../../../../")}/config/boot" - else - require "#{File.expand_path(File.dirname(__FILE__) + "/../../../")}/config/boot" - end -end +require File.expand_path('../../config/application', __FILE__) +require 'rubygems' require 'rake' -require 'rake/rdoctask' require 'rake/testtask' +require 'rake/packagetask' +require 'rake/gempackagetask' -rspec_base = File.expand_path(SPREE_ROOT + '/vendor/plugins/rspec/lib') -$LOAD_PATH.unshift(rspec_base) if File.exist?(rspec_base) -require 'spec/rake/spectask' -# require 'spec/translator' +spec = eval(File.read('spree_paypal_express.gemspec')) -# Cleanup the SPREE_ROOT constant so specs will load the environment -Object.send(:remove_const, :SPREE_ROOT) - -extension_root = File.expand_path(File.dirname(__FILE__)) - -task :default => :spec -task :stats => "spec:statsetup" - -desc "Run all specs in spec directory" -Spec::Rake::SpecTask.new(:spec) do |t| - t.spec_opts = ['--options', "\"#{extension_root}/spec/spec.opts\""] - t.spec_files = FileList["#{extension_root}/spec/**/*_spec.rb"] +Rake::GemPackageTask.new(spec) do |p| + p.gem_spec = spec end -namespace :spec do - desc "Run all specs in spec directory with RCov" - Spec::Rake::SpecTask.new(:rcov) do |t| - t.spec_opts = ['--options', "\"#{extension_root}/spec/spec.opts\""] - t.spec_files = FileList['spec/**/*_spec.rb'] - t.rcov = true - t.rcov_opts = ['--exclude', 'spec', '--rails'] - end - - desc "Print Specdoc for all specs" - Spec::Rake::SpecTask.new(:doc) do |t| - t.spec_opts = ["--format", "specdoc", "--dry-run"] - t.spec_files = FileList['spec/**/*_spec.rb'] - end - - [:models, :controllers, :views, :helpers].each do |sub| - desc "Run the specs under spec/#{sub}" - Spec::Rake::SpecTask.new(sub) do |t| - t.spec_opts = ['--options', "\"#{extension_root}/spec/spec.opts\""] - t.spec_files = FileList["spec/#{sub}/**/*_spec.rb"] - end - end - - # Hopefully no one has written their extensions in pre-0.9 style - # desc "Translate specs from pre-0.9 to 0.9 style" - # task :translate do - # translator = ::Spec::Translator.new - # dir = RAILS_ROOT + '/spec' - # translator.translate(dir, dir) - # end - - # Setup specs for stats - task :statsetup do - require 'code_statistics' - ::STATS_DIRECTORIES << %w(Model\ specs spec/models) - ::STATS_DIRECTORIES << %w(View\ specs spec/views) - ::STATS_DIRECTORIES << %w(Controller\ specs spec/controllers) - ::STATS_DIRECTORIES << %w(Helper\ specs spec/views) - ::CodeStatistics::TEST_TYPES << "Model specs" - ::CodeStatistics::TEST_TYPES << "View specs" - ::CodeStatistics::TEST_TYPES << "Controller specs" - ::CodeStatistics::TEST_TYPES << "Helper specs" - ::STATS_DIRECTORIES.delete_if {|a| a[0] =~ /test/} - end - - namespace :db do - namespace :fixtures do - desc "Load fixtures (from spec/fixtures) into the current environment's database. Load specific fixtures using FIXTURES=x,y" - task :load => :environment do - require 'active_record/fixtures' - ActiveRecord::Base.establish_connection(RAILS_ENV.to_sym) - (ENV['FIXTURES'] ? ENV['FIXTURES'].split(/,/) : Dir.glob(File.join(RAILS_ROOT, 'spec', 'fixtures', '*.{yml,csv}'))).each do |fixture_file| - Fixtures.create_fixtures('spec/fixtures', File.basename(fixture_file, '.*')) - end - end - end - end +desc "Release to gemcutter" +task :release => :package do + require 'rake/gemcutter' + Rake::Gemcutter::Tasks.new(spec).define + Rake::Task['gem:push'].invoke end -desc 'Generate documentation for the paypal_express extension.' -Rake::RDocTask.new(:rdoc) do |rdoc| - rdoc.rdoc_dir = 'rdoc' - rdoc.title = 'PaypalExpressExtension' - rdoc.options << '--line-numbers' << '--inline-source' - rdoc.rdoc_files.include('README') - rdoc.rdoc_files.include('lib/**/*.rb') -end +desc "Default Task" +task :default => [ :spec ] -# For extensions that are in transition -desc 'Test the paypal_express extension.' -Rake::TestTask.new(:test) do |t| - t.libs << 'lib' - t.pattern = 'test/**/*_test.rb' - t.verbose = true -end +require 'rspec/core/rake_task' +RSpec::Core::RakeTask.new -# Load any custom rakefiles for extension -Dir[File.dirname(__FILE__) + '/tasks/*.rake'].sort.each { |f| require f } \ No newline at end of file +# require 'cucumber/rake/task' +# Cucumber::Rake::Task.new do |t| +# t.cucumber_opts = %w{--format pretty} +# end \ No newline at end of file diff --git a/app/models/paypal_account.rb b/app/models/paypal_account.rb index 0bbdc8d..e58d28f 100644 --- a/app/models/paypal_account.rb +++ b/app/models/paypal_account.rb @@ -8,23 +8,10 @@ class PaypalAccount < ActiveRecord::Base def capture(payment) authorization = find_authorization(payment) - ppx_response = payment.payment_method.provider.capture((100 * payment.amount).to_i, authorization.transaction_id) - if ppx_response.success? - PaypalTxn.create(:payment => payment, - :txn_type => PaypalTxn::TxnType::CAPTURE, - :amount => ppx_response.params["gross_amount"].to_f, - :message => ppx_response.params["message"], - :payment_status => ppx_response.params["payment_status"], - :pending_reason => ppx_response.params["pending_reason"], - :transaction_id => ppx_response.params["transaction_id"], - :transaction_type => ppx_response.params["transaction_type"], - :payment_type => ppx_response.params["payment_type"], - :response_code => ppx_response.params["ack"], - :token => ppx_response.params["token"], - :avs_response => ppx_response.avs_result["code"], - :cvv_response => ppx_response.cvv_result["code"]) - - payment.finalize! + ppx_response = payment.payment_method.provider.capture((100 * payment.amount).to_i, authorization.params["transaction_id"]) + if ppx_response.success? + record_log payment, ppx_response + payment.complete else gateway_error(ppx_response.message) end @@ -32,37 +19,30 @@ class PaypalAccount < ActiveRecord::Base end def can_capture?(payment) - !echeck?(payment) && find_capture(payment).nil? + !echeck?(payment) && payment.state == "pending" end def credit(payment, amount=nil) authorization = find_capture(payment) - amount ||= payment.order.outstanding_credit - ppx_response = payment.payment_method.provider.credit(amount.nil? ? (100 * amount).to_i : (100 * amount).to_i, authorization.transaction_id) + + amount = payment.credit_allowed >= payment.order.outstanding_balance.abs ? payment.order.outstanding_balance : payment.credit_allowed + + ppx_response = payment.payment_method.provider.credit(amount.nil? ? (100 * amount).to_i : (100 * amount).to_i, authorization.params['transaction_id']) if ppx_response.success? - PaypalTxn.new(:payment => payment, - :txn_type => PaypalTxn::TxnType::CREDIT, - :amount => ppx_response.params["gross_refund_amount"].to_f, - :message => ppx_response.params["message"], - :payment_status => "Refunded", - :pending_reason => ppx_response.params["pending_reason"], - :transaction_id => ppx_response.params["refund_transaction_id"], - :transaction_type => ppx_response.params["transaction_type"], - :payment_type => ppx_response.params["payment_type"], - :response_code => ppx_response.params["ack"], - :token => ppx_response.params["token"], - :avs_response => ppx_response.avs_result["code"], - :cvv_response => ppx_response.cvv_result["code"]) - + record_log payment, ppx_response payment.update_attribute(:amount, payment.amount - amount) - payment.order.update_totals! + payment.complete + payment.order.update! else gateway_error(ppx_response.message) end end def can_credit?(payment) + return false unless payment.state == "completed" + return false unless payment.order.payment_state == "credit_owed" + payment.credit_allowed > 0 !find_capture(payment).nil? end @@ -70,24 +50,44 @@ class PaypalAccount < ActiveRecord::Base def payment_gateway false end - + + def record_log(payment, response) + payment.log_entries.create(:details => response.to_yaml) + end + private def find_authorization(payment) - #find the transaction associated with the original authorization/capture - payment.txns.find(:first, - :conditions => {:pending_reason => "authorization", :payment_status => "Pending", :txn_type => PaypalTxn::TxnType::AUTHORIZE.to_s}, - :order => 'created_at DESC') + logs = payment.log_entries.all(:order => 'created_at DESC') + logs.each do |log| + details = YAML.load(log.details) # return the transaction details + if (details.params['payment_status'] == 'Pending' && details.params['pending_reason'] == 'authorization') + return details + end + end + return nil end def find_capture(payment) #find the transaction associated with the original authorization/capture - payment.txns.find(:first, - :conditions => {:payment_status => "Completed", :txn_type => PaypalTxn::TxnType::CAPTURE.to_s}, - :order => 'created_at DESC') + logs = payment.log_entries.all(:order => 'created_at DESC') + logs.each do |log| + details = YAML.load(log.details) # return the transaction details + if details.params['payment_status'] == 'Completed' + return details + end + end + return nil end - + def echeck?(payment) - payment.txns.exists?(:payment_type => "echeck") + logs = payment.log_entries.all(:order => 'created_at DESC') + logs.each do |log| + details = YAML.load(log.details) # return the transaction details + if details.params['payment_type'] == 'echeck' + return true + end + end + return false end def gateway_error(text) diff --git a/app/views/admin/payments/source_views/_paypalexpress.html.erb b/app/views/admin/payments/source_views/_paypalexpress.html.erb index a5b79a0..0b349b9 100644 --- a/app/views/admin/payments/source_views/_paypalexpress.html.erb +++ b/app/views/admin/payments/source_views/_paypalexpress.html.erb @@ -31,40 +31,41 @@
<%= t('transactions') %> - <% payment.txns.reverse.each do |txn| %> + <% payment.log_entries.reverse.each do |log| %> + <% details = YAML.load(log.details) %> - + - <% if txn.payment_status == "Pending" %> + <% if details.params["payment_status"] == "Pending" %> <% end %> diff --git a/app/views/checkouts/payment/_paypalexpress.html.erb b/app/views/checkout/payment/_paypalexpress.html.erb similarity index 100% rename from app/views/checkouts/payment/_paypalexpress.html.erb rename to app/views/checkout/payment/_paypalexpress.html.erb diff --git a/app/views/shared/_paypal_express_confirm.html.erb b/app/views/shared/paypal_express_confirm.html.erb similarity index 67% rename from app/views/shared/_paypal_express_confirm.html.erb rename to app/views/shared/paypal_express_confirm.html.erb index 26a44f8..6acd7ef 100644 --- a/app/views/shared/_paypal_express_confirm.html.erb +++ b/app/views/shared/paypal_express_confirm.html.erb @@ -1,10 +1,10 @@

<%= t("confirm") %>

- <%= t("order_not_yet_placed") %> + <%= raw t("order_not_yet_placed") %>

<%= render :partial => 'shared/order_details', :locals => {:order => @order} -%>
- <%= button_to t('place_order'), paypal_finish_order_checkout_url(@checkout.order, {:token => params[:token] , :PayerID => params[:PayerID], :payment_method_id => + <%= button_to t('place_order'), paypal_finish_order_checkout_url(@order, {:token => params[:token] , :PayerID => params[:PayerID], :payment_method_id => params[:payment_method_id] } ), :class => "button primary" %>
diff --git a/config/locales/en-GB.yml b/config/locales/en-GB.yml index 17c9cd5..43d57e9 100644 --- a/config/locales/en-GB.yml +++ b/config/locales/en-GB.yml @@ -6,7 +6,7 @@ en-GB: paypal_capture_complete: Paypal Transaction has been captured. unable_to_capture_paypal: Unable to capture Paypal Transaction. signature: Signature - order_not_yet_placed: "Your order has not been be placed, please review the details and click Confirm below to finalise your order." + order_not_yet_placed: "Your order has NOT been be placed, please review the details and click Place Order below to finalize your order." paypal_payment_id: PayPal Payment ID pending_reason: Pending Reason result: Result diff --git a/config/locales/en.yml b/config/locales/en.yml index 5fcf6e2..921235a 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -6,7 +6,7 @@ en: paypal_capture_complete: Paypal Transaction has been captured. unable_to_capture_paypal: Unable to capture Paypal Transaction. signature: Signature - order_not_yet_placed: "Your order has not been be placed, please review the details and click Confirm below to finalise your order." + order_not_yet_placed: "Your order has NOT been be placed, please review the details and click Place Order below to finalize your order." paypal_payment_id: PayPal Payment ID pending_reason: Pending Reason result: Result diff --git a/config/routes.rb b/config/routes.rb index ee34104..c92a53b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,14 +1,26 @@ -# Put your extension routes here. - -map.resources :orders do |order| - order.resource :checkout, :member => {:paypal_checkout => :any, :paypal_payment => :any, :paypal_confirm => :any, :paypal_finish => :any} -end - -map.paypal_notify "/paypal_notify", :controller => :paypal_express_callbacks, :action => :notify, :method => [:post, :get] - -map.namespace :admin do |admin| - admin.resources :orders do |order| - order.resources :paypal_payments, :member => {:capture => :get, :refund => :any}, :has_many => [:txns] +Rails.application.routes.draw do + resources :orders do + resource :checkout, :controller => 'checkout' do + member do + get :paypal_checkout + get :paypal_payment + get :paypal_confirm + post :paypal_finish + end + end end -end + match '/paypal_notify' => 'paypal_express_callbacks#notify', :via => [:get, :post] + + resources :paypal_express_callbacks + namespace :admin do + resources :orders do + resources :paypal_payments do + member do + get :refund + get :capture + end + end + end + end +end \ No newline at end of file diff --git a/lib/spree/checkouts_controller_with_paypal_express.rb b/lib/spree/checkouts_controller_with_paypal_express.rb index 318770d..a67d8e9 100644 --- a/lib/spree/checkouts_controller_with_paypal_express.rb +++ b/lib/spree/checkouts_controller_with_paypal_express.rb @@ -1,4 +1,4 @@ -module Spree::CheckoutsControllerWithPaypalExpress +module Spree::CheckoutControllerWithPaypalExpress def self.included(target) target.before_filter :redirect_to_paypal_express_form, :only => [:update] end diff --git a/lib/spree/paypal_express.rb b/lib/spree/paypal_express.rb index 35a6d44..d3a9455 100644 --- a/lib/spree/paypal_express.rb +++ b/lib/spree/paypal_express.rb @@ -1,3 +1,4 @@ +# aim to unpick this later module Spree::PaypalExpress include ERB::Util include ActiveMerchant::RequiresParameters @@ -6,11 +7,8 @@ module Spree::PaypalExpress target.before_filter :redirect_to_paypal_express_form_if_needed, :only => [:update] end - # Outbound redirect to PayPal from Cart Page - # Not fully implmented or tested. - # def paypal_checkout - load_object + load_order opts = all_opts(@order, params[:payment_method_id], 'checkout') opts.merge!(address_options(@order)) gateway = paypal_gateway @@ -20,7 +18,7 @@ module Spree::PaypalExpress else response = gateway.setup_authorization(opts[:money], opts) end - + unless response.success? gateway_error(response) redirect_to edit_order_url(@order) @@ -30,10 +28,8 @@ module Spree::PaypalExpress redirect_to (gateway.redirect_url_for response.token, :review => payment_method.preferred_review) end - # Outbound redirect to PayPal from checkout payments step - # def paypal_payment - load_object + load_order opts = all_opts(@order,params[:payment_method_id], 'payment') opts.merge!(address_options(@order)) gateway = paypal_gateway @@ -46,17 +42,15 @@ module Spree::PaypalExpress unless response.success? gateway_error(response) - redirect_to edit_order_checkout_url(@order, :step => "payment") + redirect_to edit_order_checkout_url(@order, :state => "payment") return end redirect_to (gateway.redirect_url_for response.token, :review => payment_method.preferred_review) end - # Inbound post from PayPal after (possible) successful completion - # def paypal_confirm - load_object + load_order opts = { :token => params[:token], :payer_id => params[:PayerID] }.merge all_opts(@order, params[:payment_method_id], 'payment') gateway = paypal_gateway @@ -71,9 +65,8 @@ module Spree::PaypalExpress :payer_country => @ppx_details.params["payer_country"], :payer_status => @ppx_details.params["payer_status"]) - @order.checkout.special_instructions = @ppx_details.params["note"] + @order.special_instructions = @ppx_details.params["note"] - #@order.update_attribute(:user, current_user) unless payment_method.preferred_no_shipping ship_address = @ppx_details.address order_ship_address = Address.new :firstname => @ppx_details.params["first_name"], @@ -94,28 +87,25 @@ module Spree::PaypalExpress order_ship_address.save! - @order.checkout.ship_address = order_ship_address + @order.ship_address = order_ship_address end - @order.checkout.save + @order.save if payment_method.preferred_review - render :partial => "shared/paypal_express_confirm", :layout => true + render 'shared/paypal_express_confirm', :layout => 'spree_application' else paypal_finish end else gateway_error(@ppx_details) - + #Failed trying to get payment details from PPX - redirect_to edit_order_checkout_url(@order, :step => "payment") + redirect_to edit_order_checkout_url(@order, :state => "payment") end end - # Local call from A) Order Review Screen, or B) Automatically after paypal_confirm (no review). - # Completes checkout & order - # def paypal_finish - load_object + load_order opts = { :token => params[:token], :payer_id => params[:PayerID] }.merge all_opts(@order, params[:payment_method_id], 'payment' ) gateway = paypal_gateway @@ -127,71 +117,70 @@ module Spree::PaypalExpress end if ppx_auth_response.success? + paypal_account = PaypalAccount.find_by_payer_id(params[:PayerID]) + + payment = @order.payments.create( + :amount => ppx_auth_response.params["gross_amount"].to_f, + :source => paypal_account, + :source_type => 'PaypalAccount', + :payment_method_id => params[:payment_method_id], + :response_code => ppx_auth_response.params["ack"], + :avs_response => ppx_auth_response.avs_result["code"]) + + # transition through the state machine. This way any callbacks can be made + payment.started_processing! + payment.pend! + #confirm status case ppx_auth_response.params["payment_status"] - when "Completed" - txn_type = PaypalTxn::TxnType::CAPTURE - when "Pending" - txn_type = PaypalTxn::TxnType::AUTHORIZE - else - txn_type = PaypalTxn::TxnType::UNKNOWN - Rails.logger.error "Unexpected response from PayPal Express" - Rails.logger.error ppx_auth_response.to_yaml + when "Completed" + payment.complete! + when "Pending" + else + Rails.logger.error "Unexpected response from PayPal Express" + Rails.logger.error ppx_auth_response.to_yaml end - paypal_account = PaypalAccount.find_by_payer_id(params[:PayerID]) - - payment = @order.checkout.payments.create(:amount => ppx_auth_response.params["gross_amount"].to_f, - :source => paypal_account, - :payment_method_id => params[:payment_method_id]) - - PaypalTxn.create(:payment => payment, - :txn_type => txn_type, - :amount => ppx_auth_response.params["gross_amount"].to_f, - :message => ppx_auth_response.params["message"], - :payment_status => ppx_auth_response.params["payment_status"], - :pending_reason => ppx_auth_response.params["pending_reason"], - :transaction_id => ppx_auth_response.params["transaction_id"], - :transaction_type => ppx_auth_response.params["transaction_type"], - :payment_type => ppx_auth_response.params["payment_type"], - :response_code => ppx_auth_response.params["ack"], - :token => ppx_auth_response.params["token"], - :avs_response => ppx_auth_response.avs_result["code"], - :cvv_response => ppx_auth_response.cvv_result["code"]) - - + record_log payment, ppx_auth_response + @order.save! - @checkout.reload + #need to force checkout to complete state - until @checkout.state == "complete" - @checkout.next! - end - complete_checkout - - # even with auto_capture , an Auth might be returned / forced by PPX - if Spree::Config[:auto_capture] && txn_type == PaypalTxn::TxnType::CAPTURE - payment.finalize! + until @order.state == "complete" + if @order.next! + state_callback(:after) + end end + + flash[:notice] = I18n.t(:order_processed_successfully) + redirect_to completion_route else order_params = {} gateway_error(ppx_auth_response) - + #Failed trying to complete pending payment! - redirect_to edit_order_checkout_url(@order, :step => "payment") + redirect_to edit_order_checkout_url(@order, :state => "payment") end end - + private + + def record_log(payment, response) + payment.log_entries.create(:details => response.to_yaml) + end + def redirect_to_paypal_express_form_if_needed - return unless params[:step] == "payment" - - load_object - - payment_method = PaymentMethod.find(params[:checkout][:payments_attributes].first[:payment_method_id]) + return unless (params[:state] == "payment") + if params[:order][:coupon_code] + @order.update_attributes(object_params) + @order.process_coupon_code + end + load_order + payment_method = PaymentMethod.find(params[:order][:payments_attributes].first[:payment_method_id]) if payment_method.kind_of?(BillingIntegration::PaypalExpress) || payment_method.kind_of?(BillingIntegration::PaypalExpressUk) - redirect_to paypal_payment_order_checkout_url(@checkout.order, :payment_method_id => payment_method) + redirect_to paypal_payment_order_checkout_url(@order, :payment_method_id => payment_method) end end @@ -237,19 +226,20 @@ module Spree::PaypalExpress :height => item.variant.height, :width => item.variant.width, :depth => item.variant.weight } + end + + credits = order.adjustments.map do |credit| + if credit.amount < 0.00 + { :name => credit.label, + :description => credit.label, + :sku => credit.id, + :qty => 1, + :amount => (credit.amount*100).to_i } + end end - - #add each credit a line item to ensure totals sum up correctly - credits = order.credits.map do |credit| - { :name => credit.description, - :description => credit.description, - :sku => credit.id, - :qty => 1, - :amount => (credit.amount*100).to_i } - end - - items.concat credits - + items.concat credits.compact! + credits_total = credits.map {|i| i[:amount] * i[:qty] }.sum + opts = { :return_url => request.protocol + request.host_with_port + "/orders/#{order.number}/checkout/paypal_confirm?payment_method_id=#{payment_method}", :cancel_return_url => "http://" + request.host_with_port + "/orders/#{order.number}/edit", :order_id => order.number, @@ -259,26 +249,24 @@ module Spree::PaypalExpress if stage == "checkout" # recalculate all totals here as we need to ignore shipping & tax because we are checking-out via paypal (spree checkout not started) - - # get the main totals from the items (already *100) - opts[:subtotal] = opts[:items].map {|i| i[:amount] * i[:qty] }.sum - opts[:tax] = opts[:items].map {|i| i[:tax] * i[:qty] }.sum + opts[:subtotal] = ((order.item_total * 100) + credits_total ).to_i opts[:handling] = 0 - opts[:shipping] = (order.ship_total*100).to_i - + opts[:tax] = ((order.adjustments.map { |a| a.amount if ( a.source_type == 'Order' && a.label == 'Tax') }.compact!.sum) * 100 ).to_i + opts[:shipping] = ((order.adjustments.map { |a| a.amount if a.source_type == 'Shipment' }.compact!.sum) * 100 ).to_i + # overall total - opts[:money] = opts.slice(:subtotal, :tax, :shipping, :handling).values.sum + opts[:money] = (order.total * 100 ).to_i opts[:callback_url] = "http://" + request.host_with_port + "/paypal_express_callbacks/#{order.number}" opts[:callback_timeout] = 3 elsif stage == "payment" #use real totals are we are paying via paypal (spree checkout almost complete) - opts[:subtotal] = ((order.item_total + order.credits.total)*100).to_i - opts[:tax] = (order.tax_total*100).to_i - opts[:shipping] = (order.ship_total*100).to_i - + opts[:subtotal] = ((order.item_total * 100) + credits_total ).to_i + opts[:tax] = ((order.adjustments.map { |a| a.amount if ( a.source_type == 'Order' && a.label == 'Tax') }.compact!.sum) * 100 ).to_i + opts[:shipping] = ((order.adjustments.map { |a| a.amount if a.source_type == 'Shipment' }.compact!.sum) * 100 ).to_i + #hack to add float rounding difference in as handling fee - prevents PayPal from rejecting orders - #becuase 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) opts[:handling] = (order.total*100).to_i - opts.slice(:subtotal, :tax, :shipping).values.sum @@ -317,7 +305,7 @@ module Spree::PaypalExpress end # suggest current user's email or any email stored in the order - opts[:email] = current_user ? current_user.email : order.checkout.email + opts[:email] = current_user ? current_user.email : order.email opts end diff --git a/lib/spree_paypal_express.rb b/lib/spree_paypal_express.rb new file mode 100644 index 0000000..707412c --- /dev/null +++ b/lib/spree_paypal_express.rb @@ -0,0 +1,29 @@ +require 'spree_core' +require 'spree_paypal_express_hooks' + +module SpreePaypalExpress + class Engine < Rails::Engine + + config.autoload_paths += %W(#{config.root}/lib) + + def self.activate + Dir.glob(File.join(File.dirname(__FILE__), "../app/**/*_decorator*.rb")) do |c| + Rails.env.production? ? require(c) : load(c) + end + BillingIntegration::PaypalExpress.register + BillingIntegration::PaypalExpressUk.register + + # Load up over-rides for ActiveMerchant files + # these will be submitted to ActiveMerchant some time... + require File.join(File.dirname(__FILE__), "active_merchant", "billing", "gateways", "paypal", "paypal_common_api.rb") + require File.join(File.dirname(__FILE__), "active_merchant", "billing", "gateways", "paypal_express_uk.rb") + + # inject paypal code into orders controller + CheckoutController.class_eval do + include Spree::PaypalExpress + end + end + + config.to_prepare &method(:activate).to_proc + end +end diff --git a/lib/spree_paypal_express_hooks.rb b/lib/spree_paypal_express_hooks.rb new file mode 100644 index 0000000..5c157f9 --- /dev/null +++ b/lib/spree_paypal_express_hooks.rb @@ -0,0 +1,3 @@ +class SpreePaypalExpressHooks < Spree::ThemeSupport::HookListener + # custom hooks go here +end \ No newline at end of file diff --git a/lib/tasks/spree_paypal_express.rake b/lib/tasks/spree_paypal_express.rake new file mode 100644 index 0000000..17b0531 --- /dev/null +++ b/lib/tasks/spree_paypal_express.rake @@ -0,0 +1,26 @@ +namespace :spree_paypal_express do + desc "Copies all migrations and assets (NOTE: This will be obsolete with Rails 3.1)" + task :install do + Rake::Task['spree_paypal_express:install:migrations'].invoke + Rake::Task['spree_paypal_express:install:assets'].invoke + end + + namespace :install do + desc "Copies all migrations (NOTE: This will be obsolete with Rails 3.1)" + task :migrations do + source = File.join(File.dirname(__FILE__), '..', '..', 'db') + destination = File.join(Rails.root, 'db') + puts "INFO: Mirroring assets from #{source} to #{destination}" + Spree::FileUtilz.mirror_files(source, destination) + end + + desc "Copies all assets (NOTE: This will be obsolete with Rails 3.1)" + task :assets do + source = File.join(File.dirname(__FILE__), '..', '..', 'public') + destination = File.join(Rails.root, 'public') + puts "INFO: Mirroring assets from #{source} to #{destination}" + Spree::FileUtilz.mirror_files(source, destination) + end + end + +end \ No newline at end of file diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..353fdcc --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,31 @@ +# This file is copied to ~/spec when you run 'ruby script/generate rspec' +# from the project root directory. +ENV["RAILS_ENV"] ||= 'test' +require File.expand_path("../../../config/environment", __FILE__) +require 'rspec/rails' +require 'fabrication' + +# Requires supporting files with custom matchers and macros, etc, +# in ./support/ and its subdirectories. +Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f} + +RSpec.configure do |config| + # == Mock Framework + # + # If you prefer to use mocha, flexmock or RR, uncomment the appropriate line: + # + # config.mock_with :mocha + # config.mock_with :flexmock + # config.mock_with :rr + config.mock_with :rspec + + config.fixture_path = "#{::Rails.root}/spec/fixtures" + + #config.include Devise::TestHelpers, :type => :controller + # If you're not using ActiveRecord, or you'd prefer not to run each of your + # examples within a transaction, comment the following line or assign false + # instead of true. + config.use_transactional_fixtures = true +end + +@configuration ||= AppConfiguration.find_or_create_by_name("Default configuration") diff --git a/spree_paypal_express.gemspec b/spree_paypal_express.gemspec new file mode 100644 index 0000000..e650619 --- /dev/null +++ b/spree_paypal_express.gemspec @@ -0,0 +1,21 @@ +Gem::Specification.new do |s| + s.platform = Gem::Platform::RUBY + s.name = 'spree_paypal_express' + s.version = '2.0.0' + s.summary = 'Add gem summary here' + #s.description = 'Add (optional) gem description here' + s.required_ruby_version = '>= 1.8.7' + + # s.author = 'David Heinemeier Hansson' + # s.email = 'david@loudthinking.com' + # s.homepage = 'http://www.rubyonrails.org' + # s.rubyforge_project = 'actionmailer' + + s.files = Dir['CHANGELOG', 'README.md', 'LICENSE', 'lib/**/*', 'app/**/*'] + s.require_path = 'lib' + s.requirements << 'none' + + s.has_rdoc = true + + s.add_dependency('spree_core', '>= 0.30.1') +end \ No newline at end of file
<%= t('transaction') %> <%= txn.transaction_id %> - <%= txn.created_at.to_s(:date_time24) %><%= t('transaction') %> <%= details.params["transaction_id"] %> - <%= log.created_at.to_s(:date_time24) %>
- <%= txn.txn_type_name %> + <%= details.params["transaction_type"] %> - <%= txn.response_code %> + <%= details.message %> - <%= number_to_currency txn.amount %> + <%= number_to_currency details.params["gross_amount"] %>
- <%= txn.message %> + <%= details.params["message"] %> - <%= txn.payment_status %> + <%= details.params["payment_status"] %>
- <%= txn.pending_reason %> + <%= details.params["pending_reason"] %>