Modified to work with Spree 0.30.1.

This commit is contained in:
reaper 2010-11-28 23:31:31 +09:00
parent 898b6d4bb3
commit b79fb41c4d
16 changed files with 320 additions and 275 deletions

23
LICENSE Normal file
View File

@ -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.

129
Rakefile
View File

@ -1,120 +1,31 @@
# I think this is the one that should be moved to the extension Rakefile template require File.expand_path('../../config/application', __FILE__)
# 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 'rubygems'
require 'rake' require 'rake'
require 'rake/rdoctask'
require 'rake/testtask' require 'rake/testtask'
require 'rake/packagetask'
require 'rake/gempackagetask'
rspec_base = File.expand_path(SPREE_ROOT + '/vendor/plugins/rspec/lib') spec = eval(File.read('spree_paypal_express.gemspec'))
$LOAD_PATH.unshift(rspec_base) if File.exist?(rspec_base)
require 'spec/rake/spectask'
# require 'spec/translator'
# Cleanup the SPREE_ROOT constant so specs will load the environment Rake::GemPackageTask.new(spec) do |p|
Object.send(:remove_const, :SPREE_ROOT) p.gem_spec = spec
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"]
end end
namespace :spec do desc "Release to gemcutter"
desc "Run all specs in spec directory with RCov" task :release => :package do
Spec::Rake::SpecTask.new(:rcov) do |t| require 'rake/gemcutter'
t.spec_opts = ['--options', "\"#{extension_root}/spec/spec.opts\""] Rake::Gemcutter::Tasks.new(spec).define
t.spec_files = FileList['spec/**/*_spec.rb'] Rake::Task['gem:push'].invoke
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
end end
desc 'Generate documentation for the paypal_express extension.' desc "Default Task"
Rake::RDocTask.new(:rdoc) do |rdoc| task :default => [ :spec ]
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
# For extensions that are in transition require 'rspec/core/rake_task'
desc 'Test the paypal_express extension.' RSpec::Core::RakeTask.new
Rake::TestTask.new(:test) do |t|
t.libs << 'lib'
t.pattern = 'test/**/*_test.rb'
t.verbose = true
end
# Load any custom rakefiles for extension # require 'cucumber/rake/task'
Dir[File.dirname(__FILE__) + '/tasks/*.rake'].sort.each { |f| require f } # Cucumber::Rake::Task.new do |t|
# t.cucumber_opts = %w{--format pretty}
# end

View File

@ -8,23 +8,10 @@ class PaypalAccount < ActiveRecord::Base
def capture(payment) def capture(payment)
authorization = find_authorization(payment) authorization = find_authorization(payment)
ppx_response = payment.payment_method.provider.capture((100 * payment.amount).to_i, authorization.transaction_id) ppx_response = payment.payment_method.provider.capture((100 * payment.amount).to_i, authorization.params["transaction_id"])
if ppx_response.success? if ppx_response.success?
PaypalTxn.create(:payment => payment, record_log payment, ppx_response
:txn_type => PaypalTxn::TxnType::CAPTURE, payment.complete
: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!
else else
gateway_error(ppx_response.message) gateway_error(ppx_response.message)
end end
@ -32,37 +19,30 @@ class PaypalAccount < ActiveRecord::Base
end end
def can_capture?(payment) def can_capture?(payment)
!echeck?(payment) && find_capture(payment).nil? !echeck?(payment) && payment.state == "pending"
end end
def credit(payment, amount=nil) def credit(payment, amount=nil)
authorization = find_capture(payment) 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? if ppx_response.success?
PaypalTxn.new(:payment => payment, record_log payment, ppx_response
: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"])
payment.update_attribute(:amount, payment.amount - amount) payment.update_attribute(:amount, payment.amount - amount)
payment.order.update_totals! payment.complete
payment.order.update!
else else
gateway_error(ppx_response.message) gateway_error(ppx_response.message)
end end
end end
def can_credit?(payment) 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? !find_capture(payment).nil?
end end
@ -70,24 +50,44 @@ class PaypalAccount < ActiveRecord::Base
def payment_gateway def payment_gateway
false false
end end
def record_log(payment, response)
payment.log_entries.create(:details => response.to_yaml)
end
private private
def find_authorization(payment) def find_authorization(payment)
#find the transaction associated with the original authorization/capture logs = payment.log_entries.all(:order => 'created_at DESC')
payment.txns.find(:first, logs.each do |log|
:conditions => {:pending_reason => "authorization", :payment_status => "Pending", :txn_type => PaypalTxn::TxnType::AUTHORIZE.to_s}, details = YAML.load(log.details) # return the transaction details
:order => 'created_at DESC') if (details.params['payment_status'] == 'Pending' && details.params['pending_reason'] == 'authorization')
return details
end
end
return nil
end end
def find_capture(payment) def find_capture(payment)
#find the transaction associated with the original authorization/capture #find the transaction associated with the original authorization/capture
payment.txns.find(:first, logs = payment.log_entries.all(:order => 'created_at DESC')
:conditions => {:payment_status => "Completed", :txn_type => PaypalTxn::TxnType::CAPTURE.to_s}, logs.each do |log|
:order => 'created_at DESC') details = YAML.load(log.details) # return the transaction details
if details.params['payment_status'] == 'Completed'
return details
end
end
return nil
end end
def echeck?(payment) 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 end
def gateway_error(text) def gateway_error(text)

View File

@ -31,40 +31,41 @@
<fieldset> <fieldset>
<legend><%= t('transactions') %></legend> <legend><%= t('transactions') %></legend>
<% payment.txns.reverse.each do |txn| %> <% payment.log_entries.reverse.each do |log| %>
<% details = YAML.load(log.details) %>
<table class="index"> <table class="index">
<tr> <tr>
<th colspan="6"><%= t('transaction') %> <%= txn.transaction_id %> - <%= txn.created_at.to_s(:date_time24) %></th> <th colspan="6"><%= t('transaction') %> <%= details.params["transaction_id"] %> - <%= log.created_at.to_s(:date_time24) %></th>
</tr> </tr>
<tr> <tr>
<td width="12%;"><label><%= t('type') %>:</label></td> <td width="12%;"><label><%= t('type') %>:</label></td>
<td width="20%;"> <td width="20%;">
<%= txn.txn_type_name %> <%= details.params["transaction_type"] %>
</td> </td>
<td width="8%;"><label><%= t("result") %>:</label></td> <td width="8%;"><label><%= t("result") %>:</label></td>
<td width="20%;"> <td width="20%;">
<%= txn.response_code %> <%= details.message %>
</td> </td>
<td width="15%;"><label><%= t("amount") %>:</label></td> <td width="15%;"><label><%= t("amount") %>:</label></td>
<td width="20%;"> <td width="20%;">
<%= number_to_currency txn.amount %> <%= number_to_currency details.params["gross_amount"] %>
</td> </td>
</tr> </tr>
<tr> <tr>
<td><label><%= t("comment") %>:</label></td> <td><label><%= t("comment") %>:</label></td>
<td colspan="3"> <td colspan="3">
<%= txn.message %> <%= details.params["message"] %>
</td> </td>
<td><label><%= t("status") %>:</label></td> <td><label><%= t("status") %>:</label></td>
<td> <td>
<%= txn.payment_status %> <%= details.params["payment_status"] %>
</td> </td>
</tr> </tr>
<% if txn.payment_status == "Pending" %> <% if details.params["payment_status"] == "Pending" %>
<tr> <tr>
<td><label><%= t("pending_reason") %>:</label></td> <td><label><%= t("pending_reason") %>:</label></td>
<td colspan="5"> <td colspan="5">
<%= txn.pending_reason %> <%= details.params["pending_reason"] %>
</td> </td>
</tr> </tr>
<% end %> <% end %>

View File

@ -1,10 +1,10 @@
<h1><%= t("confirm") %></h1> <h1><%= t("confirm") %></h1>
<p> <p>
<%= t("order_not_yet_placed") %> <%= raw t("order_not_yet_placed") %>
</p> </p>
<%= render :partial => 'shared/order_details', :locals => {:order => @order} -%> <%= render :partial => 'shared/order_details', :locals => {:order => @order} -%>
<div class="form-buttons"> <div class="form-buttons">
<%= 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" %> params[:payment_method_id] } ), :class => "button primary" %>
</div> </div>

View File

@ -6,7 +6,7 @@ en-GB:
paypal_capture_complete: Paypal Transaction has been captured. paypal_capture_complete: Paypal Transaction has been captured.
unable_to_capture_paypal: Unable to capture Paypal Transaction. unable_to_capture_paypal: Unable to capture Paypal Transaction.
signature: Signature signature: Signature
order_not_yet_placed: "Your order has <strong>not</strong> been be placed, please review the details and click Confirm below to finalise your order." order_not_yet_placed: "Your order has <strong>NOT</strong> been be placed, please review the details and click <strong>Place Order</strong> below to finalize your order."
paypal_payment_id: PayPal Payment ID paypal_payment_id: PayPal Payment ID
pending_reason: Pending Reason pending_reason: Pending Reason
result: Result result: Result

View File

@ -6,7 +6,7 @@ en:
paypal_capture_complete: Paypal Transaction has been captured. paypal_capture_complete: Paypal Transaction has been captured.
unable_to_capture_paypal: Unable to capture Paypal Transaction. unable_to_capture_paypal: Unable to capture Paypal Transaction.
signature: Signature signature: Signature
order_not_yet_placed: "Your order has <strong>not</strong> been be placed, please review the details and click Confirm below to finalise your order." order_not_yet_placed: "Your order has <strong>NOT</strong> been be placed, please review the details and click <strong>Place Order</strong> below to finalize your order."
paypal_payment_id: PayPal Payment ID paypal_payment_id: PayPal Payment ID
pending_reason: Pending Reason pending_reason: Pending Reason
result: Result result: Result

View File

@ -1,14 +1,26 @@
# Put your extension routes here. Rails.application.routes.draw do
resources :orders do
map.resources :orders do |order| resource :checkout, :controller => 'checkout' do
order.resource :checkout, :member => {:paypal_checkout => :any, :paypal_payment => :any, :paypal_confirm => :any, :paypal_finish => :any} member do
end get :paypal_checkout
get :paypal_payment
map.paypal_notify "/paypal_notify", :controller => :paypal_express_callbacks, :action => :notify, :method => [:post, :get] get :paypal_confirm
post :paypal_finish
map.namespace :admin do |admin| end
admin.resources :orders do |order| end
order.resources :paypal_payments, :member => {:capture => :get, :refund => :any}, :has_many => [:txns]
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

View File

@ -1,4 +1,4 @@
module Spree::CheckoutsControllerWithPaypalExpress module Spree::CheckoutControllerWithPaypalExpress
def self.included(target) def self.included(target)
target.before_filter :redirect_to_paypal_express_form, :only => [:update] target.before_filter :redirect_to_paypal_express_form, :only => [:update]
end end

View File

@ -1,3 +1,4 @@
# aim to unpick this later
module Spree::PaypalExpress module Spree::PaypalExpress
include ERB::Util include ERB::Util
include ActiveMerchant::RequiresParameters include ActiveMerchant::RequiresParameters
@ -6,11 +7,8 @@ module Spree::PaypalExpress
target.before_filter :redirect_to_paypal_express_form_if_needed, :only => [:update] target.before_filter :redirect_to_paypal_express_form_if_needed, :only => [:update]
end end
# Outbound redirect to PayPal from Cart Page
# Not fully implmented or tested.
#
def paypal_checkout def paypal_checkout
load_object load_order
opts = all_opts(@order, params[:payment_method_id], 'checkout') opts = all_opts(@order, params[:payment_method_id], 'checkout')
opts.merge!(address_options(@order)) opts.merge!(address_options(@order))
gateway = paypal_gateway gateway = paypal_gateway
@ -20,7 +18,7 @@ module Spree::PaypalExpress
else else
response = gateway.setup_authorization(opts[:money], opts) response = gateway.setup_authorization(opts[:money], opts)
end end
unless response.success? unless response.success?
gateway_error(response) gateway_error(response)
redirect_to edit_order_url(@order) 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) redirect_to (gateway.redirect_url_for response.token, :review => payment_method.preferred_review)
end end
# Outbound redirect to PayPal from checkout payments step
#
def paypal_payment def paypal_payment
load_object load_order
opts = all_opts(@order,params[:payment_method_id], 'payment') opts = all_opts(@order,params[:payment_method_id], 'payment')
opts.merge!(address_options(@order)) opts.merge!(address_options(@order))
gateway = paypal_gateway gateway = paypal_gateway
@ -46,17 +42,15 @@ module Spree::PaypalExpress
unless response.success? unless response.success?
gateway_error(response) gateway_error(response)
redirect_to edit_order_checkout_url(@order, :step => "payment") redirect_to edit_order_checkout_url(@order, :state => "payment")
return return
end end
redirect_to (gateway.redirect_url_for response.token, :review => payment_method.preferred_review) redirect_to (gateway.redirect_url_for response.token, :review => payment_method.preferred_review)
end end
# Inbound post from PayPal after (possible) successful completion
#
def paypal_confirm def paypal_confirm
load_object load_order
opts = { :token => params[:token], :payer_id => params[:PayerID] }.merge all_opts(@order, params[:payment_method_id], 'payment') opts = { :token => params[:token], :payer_id => params[:PayerID] }.merge all_opts(@order, params[:payment_method_id], 'payment')
gateway = paypal_gateway gateway = paypal_gateway
@ -71,9 +65,8 @@ module Spree::PaypalExpress
:payer_country => @ppx_details.params["payer_country"], :payer_country => @ppx_details.params["payer_country"],
:payer_status => @ppx_details.params["payer_status"]) :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 unless payment_method.preferred_no_shipping
ship_address = @ppx_details.address ship_address = @ppx_details.address
order_ship_address = Address.new :firstname => @ppx_details.params["first_name"], order_ship_address = Address.new :firstname => @ppx_details.params["first_name"],
@ -94,28 +87,25 @@ module Spree::PaypalExpress
order_ship_address.save! order_ship_address.save!
@order.checkout.ship_address = order_ship_address @order.ship_address = order_ship_address
end end
@order.checkout.save @order.save
if payment_method.preferred_review if payment_method.preferred_review
render :partial => "shared/paypal_express_confirm", :layout => true render 'shared/paypal_express_confirm', :layout => 'spree_application'
else else
paypal_finish paypal_finish
end end
else else
gateway_error(@ppx_details) gateway_error(@ppx_details)
#Failed trying to get payment details from PPX #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
end end
# Local call from A) Order Review Screen, or B) Automatically after paypal_confirm (no review).
# Completes checkout & order
#
def paypal_finish def paypal_finish
load_object load_order
opts = { :token => params[:token], :payer_id => params[:PayerID] }.merge all_opts(@order, params[:payment_method_id], 'payment' ) opts = { :token => params[:token], :payer_id => params[:PayerID] }.merge all_opts(@order, params[:payment_method_id], 'payment' )
gateway = paypal_gateway gateway = paypal_gateway
@ -127,71 +117,70 @@ module Spree::PaypalExpress
end end
if ppx_auth_response.success? 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 #confirm status
case ppx_auth_response.params["payment_status"] case ppx_auth_response.params["payment_status"]
when "Completed" when "Completed"
txn_type = PaypalTxn::TxnType::CAPTURE payment.complete!
when "Pending" when "Pending"
txn_type = PaypalTxn::TxnType::AUTHORIZE else
else Rails.logger.error "Unexpected response from PayPal Express"
txn_type = PaypalTxn::TxnType::UNKNOWN Rails.logger.error ppx_auth_response.to_yaml
Rails.logger.error "Unexpected response from PayPal Express"
Rails.logger.error ppx_auth_response.to_yaml
end end
paypal_account = PaypalAccount.find_by_payer_id(params[:PayerID]) record_log payment, ppx_auth_response
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"])
@order.save! @order.save!
@checkout.reload
#need to force checkout to complete state #need to force checkout to complete state
until @checkout.state == "complete" until @order.state == "complete"
@checkout.next! if @order.next!
end state_callback(:after)
complete_checkout end
# even with auto_capture , an Auth might be returned / forced by PPX
if Spree::Config[:auto_capture] && txn_type == PaypalTxn::TxnType::CAPTURE
payment.finalize!
end end
flash[:notice] = I18n.t(:order_processed_successfully)
redirect_to completion_route
else else
order_params = {} order_params = {}
gateway_error(ppx_auth_response) gateway_error(ppx_auth_response)
#Failed trying to complete pending payment! #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
end end
private private
def record_log(payment, response)
payment.log_entries.create(:details => response.to_yaml)
end
def redirect_to_paypal_express_form_if_needed def redirect_to_paypal_express_form_if_needed
return unless params[:step] == "payment" return unless (params[:state] == "payment")
if params[:order][:coupon_code]
load_object @order.update_attributes(object_params)
@order.process_coupon_code
payment_method = PaymentMethod.find(params[:checkout][:payments_attributes].first[:payment_method_id]) 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) 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
end end
@ -237,19 +226,20 @@ module Spree::PaypalExpress
:height => item.variant.height, :height => item.variant.height,
:width => item.variant.width, :width => item.variant.width,
:depth => item.variant.weight } :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 end
items.concat credits.compact!
#add each credit a line item to ensure totals sum up correctly credits_total = credits.map {|i| i[:amount] * i[:qty] }.sum
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
opts = { :return_url => request.protocol + request.host_with_port + "/orders/#{order.number}/checkout/paypal_confirm?payment_method_id=#{payment_method}", 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", :cancel_return_url => "http://" + request.host_with_port + "/orders/#{order.number}/edit",
:order_id => order.number, :order_id => order.number,
@ -259,26 +249,24 @@ module Spree::PaypalExpress
if stage == "checkout" 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) # recalculate all totals here as we need to ignore shipping & tax because we are checking-out via paypal (spree checkout not started)
opts[:subtotal] = ((order.item_total * 100) + credits_total ).to_i
# 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[:handling] = 0 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 # 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_url] = "http://" + request.host_with_port + "/paypal_express_callbacks/#{order.number}"
opts[:callback_timeout] = 3 opts[:callback_timeout] = 3
elsif stage == "payment" elsif stage == "payment"
#use real totals are we are paying via paypal (spree checkout almost complete) #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[:subtotal] = ((order.item_total * 100) + credits_total ).to_i
opts[:tax] = (order.tax_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.ship_total*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 #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) #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 opts[:handling] = (order.total*100).to_i - opts.slice(:subtotal, :tax, :shipping).values.sum
@ -317,7 +305,7 @@ module Spree::PaypalExpress
end end
# suggest current user's email or any email stored in the order # 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 opts
end end

View File

@ -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

View File

@ -0,0 +1,3 @@
class SpreePaypalExpressHooks < Spree::ThemeSupport::HookListener
# custom hooks go here
end

View File

@ -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

31
spec/spec_helper.rb Normal file
View File

@ -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")

View File

@ -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