Browse Source

Initial fork changes

1-2-stable-sysmocom
Brian Quinn 12 years ago
parent
commit
7da2fed7ac
  1. 8
      app/controllers/paypal_express_callbacks_controller.rb
  2. 10
      app/models/billing_integration/paypal_express.rb
  3. 23
      app/models/paypal_payment.rb
  4. 3
      app/models/paypal_txn.rb
  5. 6
      app/views/shared/_paypal_button.html.erb
  6. 3
      app/views/shared/_paypal_express_checkout.html.erb
  7. 17
      app/views/shared/_paypal_express_confirm.html.erb
  8. 1
      app/views/shared/_paypal_express_finish.html.erb
  9. 3
      app/views/shared/_paypal_express_payment.html.erb
  10. 1
      config/locales/en-GB.yml
  11. 9
      config/locales/en-US.yml
  12. 4
      config/routes.rb
  13. 16
      db/migrate/20090513103111_create_paypal_express_gateway.rb
  14. 22
      db/migrate/20100122120551_create_paypal_txns.rb
  15. 137
      lib/active_merchant/billing/gateways/paypal/paypal_common_api.rb
  16. 42
      lib/active_merchant/billing/gateways/paypal_express.rb
  17. 394
      lib/spree/paypal_express.rb
  18. 15
      paypal_express_extension.rb
  19. 6
      spec/spec.opts
  20. 37
      spec/spec_helper.rb

8
app/controllers/paypal_express_callbacks_controller.rb

@ -0,0 +1,8 @@
class PaypalExpressCallbacksController < Admin::BaseController
def index
render :text => "index"
end
def show
render :text => "text to render..."
end
end

10
app/models/billing_integration/paypal_express.rb

@ -0,0 +1,10 @@
class BillingIntegration::PaypalExpress < BillingIntegration
preference :login, :string
preference :password, :password
preference :signature, :string
def provider_class
ActiveMerchant::Billing::PaypalExpressGateway
end
end

23
app/models/paypal_payment.rb

@ -1,18 +1,17 @@
class PaypalPayment < Payment
has_many :creditcard_txns, :foreign_key => 'creditcard_payment_id' # reused and faked
belongs_to :creditcard # allow for saving of fake details
# accepts_nested_attributes_for :creditcard
has_many :paypal_txns
alias :txns :creditcard_txns # should PUSH to parent/interface
alias :txns :paypal_txns
# def find_authorization
# #find the transaction associated with the original authorization/capture
# txns.find(:first,
# :conditions => ["txn_type = ? AND response_code IS NOT NULL", CreditcardTxn::TxnType::AUTHORIZE],
# :order => 'created_at DESC')
# end
def find_authorization
#find the transaction associated with the original authorization/capture
txns.find(:first,
:conditions => ["txn_type = ? AND response_code IS NOT NULL", CreditcardTxn::TxnType::AUTHORIZE],
:order => 'created_at DESC')
end
def can_capture? # push to parent? perhaps not
txns.last == find_authorization
true
#txns.last == find_authorization
end
end

3
app/models/paypal_txn.rb

@ -0,0 +1,3 @@
class PaypalTxn < ActiveRecord::Base
belongs_to :paypal_payment
end

6
app/views/shared/_paypal_button.html.erb

@ -1,6 +0,0 @@
<div style="width: 100%; margin-top: 10px; margin-bottom: 8px;">
<strong style="font-size: 90%; text-align: center;">- OR CHOOSE -</strong>
</div>
<a href="<%= paypal_checkout_order_url order %>" style="text-align: center;">
<img src="https://www.paypal.com/en_GB/GB/i/btn/btn_xpressCheckout.gif" align="left" style="margin-right:7px;"/>
</a>

3
app/views/shared/_paypal_express_checkout.html.erb

@ -0,0 +1,3 @@
<a href="<%= paypal_checkout_order_url @checkout.order, :integration_id => integration %>" style="text-align: center;">
<img src="https://www.paypal.com/en_GB/GB/i/btn/btn_xpressCheckout.gif" align="left" style="margin-right:7px;"/>
</a>

17
app/views/shared/_paypal_express_confirm.html.erb

@ -0,0 +1,17 @@
<h1><%= t("confirm") %></h1>
<p>
<%= t("order_not_yet_placed") %>
</p>
<%= render :partial => 'shared/order_details', :locals => {:order => @order} -%>
<div class="form-buttons">
<%= button_to t('place_order'), "/orders/#{@order.number}/paypal_finish?token=#{params[:token]}&PayerID=#{params[:PayerID]}", :class => "button primary" %>
</div>
<pre>
<%= @ppx_details.to_yaml %>
</pre>

1
app/views/shared/_paypal_express_finish.html.erb

@ -0,0 +1 @@
Thanks.

3
app/views/shared/_paypal_express_payment.html.erb

@ -0,0 +1,3 @@
<a href="<%= paypal_payment_order_url @checkout.order, :integration_id => integration %>" style="text-align: center;">
<img src="https://www.paypal.com/en_GB/GB/i/btn/btn_xpressCheckout.gif" align="left" style="margin-right:7px;"/>
</a>

1
config/locales/en-GB.yml

@ -5,4 +5,5 @@ en-GB:
paypal_txn_id: Transaction Code
paypal_capture_complete: Paypal Transaction has been captured.
unable_to_capture_paypal: Unable to capture Paypal Transaction.
signature: Signature

9
config/locales/en-US.yml

@ -0,0 +1,9 @@
---
en-US:
edit_paypal_info: Edit Paypal Express Payment
paypal_payment: Paypal Express Payment
paypal_txn_id: Transaction Code
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 <strong>not</strong> been be placed, please review the details and click Confirm below to finalise your order."

4
config/routes.rb

@ -1,6 +1,8 @@
# Put your extension routes here.
map.resources :orders, :member => {:paypal_checkout => :any, :paypal_finish => :any}
map.resources :orders, :member => {:paypal_checkout => :any, :paypal_payment => :any, :paypal_confirm => :any, :paypal_finish => :any}
map.resources :paypal_express_callbacks, :only => [:index, :show]
map.namespace :admin do |admin|
admin.resources :orders do |order|

16
db/migrate/20090513103111_create_paypal_express_gateway.rb

@ -1,16 +0,0 @@
class CreatePaypalExpressGateway < ActiveRecord::Migration
def self.up
login = GatewayOption.create(:name => "login", :description => "Your login email.")
password = GatewayOption.create(:name => "password", :description => "Your Paypal API Credentials Password.")
signature = GatewayOption.create(:name => "signature", :textarea => true, :description => "Your Paypal API Credentials signature string.")
gateway = Gateway.create(:name => "Paypal Express UK",
:clazz => "ActiveMerchant::Billing::PaypalExpressUkGateway",
:description => "Active Merchant's Paypal Express (UK) Gateway.",
:gateway_options => [login, password, signature])
end
def self.down
Gateway.find_by_name("Paypal Express UK").destroy
end
end

22
db/migrate/20100122120551_create_paypal_txns.rb

@ -0,0 +1,22 @@
class CreatePaypalTxns < ActiveRecord::Migration
def self.up
create_table :paypal_txns do |t|
t.references :paypal_payment
t.decimal :gross_amount, :precision => 8, :scale => 2
t.string :payment_status
t.text :message
t.string :pending_reason
t.string :transaction_type
t.string :payment_type
t.string :ack
t.string :token
t.string :avs_code
t.string :cvv_code
t.timestamps
end
end
def self.down
drop_table :paypal_txns
end
end

137
lib/active_merchant/billing/gateways/paypal/paypal_common_api.rb

@ -7,19 +7,19 @@ module ActiveMerchant #:nodoc:
base.cattr_accessor :pem_file
base.cattr_accessor :signature
end
API_VERSION = '57.0' # TODO - check absolute adherence in this file, override in sub?
API_VERSION = '60.0' # TODO - check absolute adherence in this file, override in sub?
URLS = {
:test => { :certificate => 'https://api.sandbox.paypal.com/2.0/',
:signature => 'https://api-3t.sandbox.paypal.com/2.0/' },
:live => { :certificate => 'https://api-aa.paypal.com/2.0/',
:signature => 'https://api-3t.paypal.com/2.0/' }
}
PAYPAL_NAMESPACE = 'urn:ebay:api:PayPalAPI'
EBAY_NAMESPACE = 'urn:ebay:apis:eBLBaseComponents'
ENVELOPE_NAMESPACES = { 'xmlns:xsd' => 'http://www.w3.org/2001/XMLSchema',
'xmlns:env' => 'http://schemas.xmlsoap.org/soap/envelope/',
'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance'
@ -28,7 +28,7 @@ module ActiveMerchant #:nodoc:
'xmlns:n1' => EBAY_NAMESPACE,
'env:mustUnderstand' => '0'
}
AUSTRALIAN_STATES = {
'ACT' => 'Australian Capital Territory',
'NSW' => 'New South Wales',
@ -39,11 +39,11 @@ module ActiveMerchant #:nodoc:
'VIC' => 'Victoria',
'WA' => 'Western Australia'
}
SUCCESS_CODES = [ 'Success', 'SuccessWithWarning' ]
FRAUD_REVIEW_CODE = "11610"
# The gateway must be configured with either your PayPal PEM file
# or your PayPal API Signature. Only one is required.
#
@ -54,27 +54,27 @@ module ActiveMerchant #:nodoc:
# globally and then you won't need to
# include this option
#
# <tt>:signature</tt> The text of your PayPal signature.
# <tt>:signature</tt> The text of your PayPal signature.
# If you are only using one API Signature
# on your site you can declare it
# globally and then you won't need to
# include this option
def initialize(options = {})
requires!(options, :login, :password)
@options = {
:pem => pem_file,
:signature => signature
}.update(options)
if @options[:pem].blank? && @options[:signature].blank?
raise ArgumentError, "An API Certificate or API Signature is required to make requests to PayPal"
raise ArgumentError, "An API Certificate or API Signature is required to make requests to PayPal"
end
super
end
def test?
@options[:test] || Base.gateway_mode == :test
end
@ -82,11 +82,11 @@ module ActiveMerchant #:nodoc:
def reauthorize(money, authorization, options = {})
commit 'DoReauthorization', build_reauthorize_request(money, authorization, options)
end
def capture(money, authorization, options = {})
commit 'DoCapture', build_capture_request(money, authorization, options)
end
# Transfer money to one or more recipients.
#
# gateway.transfer 1000, 'bob@example.com',
@ -104,7 +104,7 @@ module ActiveMerchant #:nodoc:
def void(authorization, options = {})
commit 'DoVoid', build_void_request(authorization, options)
end
def credit(money, identification, options = {})
commit 'RefundTransaction', build_credit_request(money, identification, options)
end
@ -112,7 +112,7 @@ module ActiveMerchant #:nodoc:
private
def build_reauthorize_request(money, authorization, options)
xml = Builder::XmlMarkup.new
xml.tag! 'DoReauthorizationReq', 'xmlns' => PAYPAL_NAMESPACE do
xml.tag! 'DoReauthorizationRequest', 'xmlns:n2' => EBAY_NAMESPACE do
xml.tag! 'n2:Version', API_VERSION
@ -121,12 +121,12 @@ module ActiveMerchant #:nodoc:
end
end
xml.target!
xml.target!
end
def build_capture_request(money, authorization, options)
def build_capture_request(money, authorization, options)
xml = Builder::XmlMarkup.new
xml.tag! 'DoCaptureReq', 'xmlns' => PAYPAL_NAMESPACE do
xml.tag! 'DoCaptureRequest', 'xmlns:n2' => EBAY_NAMESPACE do
xml.tag! 'n2:Version', API_VERSION
@ -137,12 +137,12 @@ module ActiveMerchant #:nodoc:
end
end
xml.target!
xml.target!
end
def build_credit_request(money, identification, options)
xml = Builder::XmlMarkup.new
xml.tag! 'RefundTransactionReq', 'xmlns' => PAYPAL_NAMESPACE do
xml.tag! 'RefundTransactionRequest', 'xmlns:n2' => EBAY_NAMESPACE do
xml.tag! 'n2:Version', API_VERSION
@ -152,13 +152,13 @@ module ActiveMerchant #:nodoc:
xml.tag! 'Memo', options[:note] unless options[:note].blank?
end
end
xml.target!
xml.target!
end
def build_void_request(authorization, options)
xml = Builder::XmlMarkup.new
xml.tag! 'DoVoidReq', 'xmlns' => PAYPAL_NAMESPACE do
xml.tag! 'DoVoidRequest', 'xmlns:n2' => EBAY_NAMESPACE do
xml.tag! 'n2:Version', API_VERSION
@ -167,15 +167,15 @@ module ActiveMerchant #:nodoc:
end
end
xml.target!
xml.target!
end
def build_mass_pay_request(*args)
def build_mass_pay_request(*args)
default_options = args.last.is_a?(Hash) ? args.pop : {}
recipients = args.first.is_a?(Array) ? args : [args]
xml = Builder::XmlMarkup.new
xml.tag! 'MassPayReq', 'xmlns' => PAYPAL_NAMESPACE do
xml.tag! 'MassPayRequest', 'xmlns:n2' => EBAY_NAMESPACE do
xml.tag! 'n2:Version', API_VERSION
@ -191,24 +191,24 @@ module ActiveMerchant #:nodoc:
end
end
end
xml.target!
end
def parse(action, xml)
response = {}
error_messages = []
error_codes = []
xml = REXML::Document.new(xml)
if root = REXML::XPath.first(xml, "//#{action}Response")
root.elements.each do |node|
root.elements.each do |node|
case node.name
when 'Errors'
short_message = nil
long_message = nil
node.elements.each do |child|
case child.name
when "LongMessage"
@ -250,20 +250,20 @@ module ActiveMerchant #:nodoc:
def build_request(body)
xml = Builder::XmlMarkup.new
xml.instruct!
xml.tag! 'env:Envelope', ENVELOPE_NAMESPACES do
xml.tag! 'env:Header' do
add_credentials(xml)
end
xml.tag! 'env:Body' do
xml << body
end
end
xml.target!
end
def add_credentials(xml)
xml.tag! 'RequesterCredentials', CREDENTIALS_NAMESPACES do
xml.tag! 'n1:Credentials' do
@ -274,7 +274,7 @@ module ActiveMerchant #:nodoc:
end
end
end
def add_address(xml, element, address)
return if address.nil?
xml.tag! element do
@ -306,22 +306,23 @@ module ActiveMerchant #:nodoc:
# xml.tag! 'n2:EbayItemPaymentDetailsItem', item[:name]
end
end
def add_payment_details(xml, money, options)
def add_payment_details(xml, money, options)
currency_code = options[:currency] || currency(money)
# COULD USE options[:currency] || currency(options[:actual_opt])
xml.tag! 'n2:PaymentDetails' do
xml.tag! 'n2:OrderTotal', amount(money), 'currencyID' => currency_code
# All of the values must be included together and add up to the order total
puts "-----------#{options[:shipping]}---------------------#{amount(options[:shipping])}----------------"
if [:subtotal, :shipping, :handling, :tax].all?{ |o| options.has_key?(o) }
xml.tag! 'n2:ItemTotal', amount(options[:subtotal]), 'currencyID' => currency_code
xml.tag! 'n2:ShippingTotal', amount(options[:shipping]),'currencyID' => currency_code
xml.tag! 'n2:HandlingTotal', amount(options[:handling]),'currencyID' => currency_code
xml.tag! 'n2:TaxTotal', amount(options[:tax]), 'currencyID' => currency_code
end
# don't enforce inclusion yet - see how it works
xml.tag! 'n2:InsuranceOptionOffered', options[:insurance_offered] ? '1' : '0' unless options[:insurance_offered].blank?
xml.tag! 'n2:InsuranceTotal', amount(options[:insurance]), 'currencyID' => currency_code unless options[:insurance].blank?
@ -331,13 +332,29 @@ module ActiveMerchant #:nodoc:
xml.tag! 'n2:OrderDescription', options[:description] unless options[:description].blank?
xml.tag! 'n2:Custom', options[:custom] unless options[:custom].blank?
xml.tag! 'n2:InvoiceID', options[:order_id] unless options[:order_id].blank?
xml.tag! 'n2:ButtonSource', application_id.to_s.slice(0,32) unless application_id.blank?
xml.tag! 'n2:ButtonSource', application_id.to_s.slice(0,32) unless application_id.blank?
xml.tag! 'n2:NotifyURL', options[:notify_url] unless options[:notify_url].blank?
add_address(xml, 'n2:ShipToAddress', options[:shipping_address] || options[:address])
options[:items].each {|i| add_payment_detail_item xml, i } if options[:items]
end
end
def add_shipping_options(xml, shipping_options, options)
currency_code = options[:currency]
xml.tag! 'n2:FlatRateShippingOptions' do
shipping_options.each_with_index do |shipping_option, i|
xml.tag! 'n2:ShippingOptions' do
xml.tag! 'n2:ShippingOptionIsDefault', (i == 0)
xml.tag! 'n2:ShippingOptionName', shipping_option[:name]
xml.tag! 'n2:ShippingOptionLabel', shipping_option[:label]
xml.tag! 'n2:ShippingOptionAmount', amount(shipping_option[:amount] ), 'currencyID' => 'USD'
end
end
end
end
def endpoint_url
URLS[test? ? :test : :live][@options[:signature].blank? ? :certificate : :signature]
end
@ -346,26 +363,26 @@ module ActiveMerchant #:nodoc:
response = parse(action, ssl_post(endpoint_url, build_request(request)))
build_response(successful?(response), message_from(response), response,
:test => test?,
:authorization => authorization_from(response),
:fraud_review => fraud_review?(response),
:avs_result => { :code => response[:avs_code] },
:cvv_result => response[:cvv2_code]
:test => test?,
:authorization => authorization_from(response),
:fraud_review => fraud_review?(response),
:avs_result => { :code => response[:avs_code] },
:cvv_result => response[:cvv2_code]
)
end
def fraud_review?(response)
response[:error_codes] == FRAUD_REVIEW_CODE
end
def authorization_from(response)
response[:transaction_id] || response[:authorization_id] || response[:refund_transaction_id] # middle one is from reauthorization
end
def successful?(response)
SUCCESS_CODES.include?(response[:ack])
end
def message_from(response)
response[:message] || response[:ack]
end

42
lib/active_merchant/billing/gateways/paypal_express.rb

@ -7,21 +7,25 @@ module ActiveMerchant #:nodoc:
class PaypalExpressGateway < Gateway
include PaypalCommonAPI
include PaypalExpressCommon
self.test_redirect_url = 'https://www.sandbox.paypal.com/cgi-bin/webscr?cmd=_express-checkout&token='
self.supported_countries = ['US']
self.homepage_url = 'https://www.paypal.com/cgi-bin/webscr?cmd=xpt/merchant/ExpressCheckoutIntro-outside'
self.display_name = 'PayPal Express Checkout'
def setup_authorization(money, options = {})
requires!(options, :return_url, :cancel_return_url)
commit 'SetExpressCheckout', build_setup_request('Authorization', money, options)
req = build_setup_request('Authorization', money, options)
puts req
commit 'SetExpressCheckout', req
end
def setup_purchase(money, options = {})
requires!(options, :return_url, :cancel_return_url)
commit 'SetExpressCheckout', build_setup_request('Sale', money, options)
end
@ -31,13 +35,17 @@ module ActiveMerchant #:nodoc:
def authorize(money, options = {})
requires!(options, :token, :payer_id)
commit 'DoExpressCheckoutPayment', build_sale_or_authorization_request('Authorization', money, options)
req = build_sale_or_authorization_request('Authorization', money, options)
puts req
commit 'DoExpressCheckoutPayment', req
end
def purchase(money, options = {})
requires!(options, :token, :payer_id)
commit 'DoExpressCheckoutPayment', build_sale_or_authorization_request('Sale', money, options)
end
@ -53,10 +61,10 @@ module ActiveMerchant #:nodoc:
xml.target!
end
def build_sale_or_authorization_request(action, money, options)
currency_code = options[:currency] || currency(money)
xml = Builder::XmlMarkup.new :indent => 2
xml.tag! 'DoExpressCheckoutPaymentReq', 'xmlns' => PAYPAL_NAMESPACE do
xml.tag! 'DoExpressCheckoutPaymentRequest', 'xmlns:n2' => EBAY_NAMESPACE do
@ -84,16 +92,16 @@ module ActiveMerchant #:nodoc:
end
xml.tag! 'n2:ReturnURL', options[:return_url]
xml.tag! 'n2:CancelURL', options[:cancel_return_url]
xml.tag! 'n2:CallbackURL', options[:callback_url] unless options[:callback_url].blank?
xml.tag! 'n2:CallbackTimeout', options[:callback_timeout] unless options[:callback_timeout].blank?
# flat rate shipping options -- required if using callback, TODO
# xml.tag! 'n2:CallbackURL', options[:callback_url] unless options[:callback_url].blank?
# xml.tag! 'n2:CallbackTimeout', options[:callback_timeout] unless options[:callback_timeout].blank?
xml.tag! 'n2:ReqConfirmShipping', options[:req_confirm_shipping] ? '1' : '0'
xml.tag! 'n2:NoShipping', options[:no_shipping] ? '1' : '0'
# NOT INCLUDED IN SETUP -- GRAB ELSEWHERE? -- xml.tag! 'n2:IPAddress', options[:ip]
## add flat rates for shipping
# add_shipping_options(xml, options[:shipping_options], options) if options[:shipping_options]
xml.tag! 'n2:AllowNote', options[:allow_note] ? '1' : '0'
xml.tag! 'n2:AddressOverride', options[:address_override] ? '1' : '0' # force yours
xml.tag! 'n2:LocaleCode', options[:locale] unless options[:locale].blank?
# Customization of the payment page
xml.tag! 'n2:PageStyle', options[:page_style] unless options[:page_style].blank?
xml.tag! 'n2:cpp-header-image', options[:header_image] unless options[:header_image].blank?
@ -120,7 +128,7 @@ module ActiveMerchant #:nodoc:
xml.target!
end
def build_response(success, message, response, options = {})
PaypalExpressResponse.new(success, message, response, options)
end

394
lib/spree/paypal_express.rb

@ -1,193 +1,113 @@
# aim to unpick this later
module Spree::PaypalExpress
include ERB::Util
include Spree::PaymentGateway
include ActiveMerchant::RequiresParameters
include ActiveMerchant::RequiresParameters
def paypal_checkout
opts = all_opts(@order, 'checkout')
opts.merge!(address_and_selected_shipping_options(@order))
gateway = paypal_gateway
def fixed_opts
{ :description => "Goods from #{Spree::Config[:site_name]}", # site details...
#:page_style => "foobar", # merchant account can set named config
:header_image => "https://" + Spree::Config[:site_url] + "/images/logo.png",
:background_color => "ffffff", # must be hex only, six chars
:header_background_color => "ffffff",
:header_border_color => "ffffff",
:allow_note => true,
:locale => Spree::Config[:default_locale],
:notify_url => 'to be done', # this is a callback, not tried it yet
:req_confirm_shipping => false, # for security, might make an option later
# :no_shipping => false,
# :address_override => false,
# WARNING -- don't use :ship_discount, :insurance_offered, :insurance since
# they've not been tested and may trigger some paypal bugs, eg not showing order
# see http://www.pdncommunity.com/t5/PayPal-Developer-Blog/Displaying-Order-Details-in-Express-Checkout/bc-p/92902#C851
}
end
# TODO: might be able to get paypal to do some of the shipping choice and costing
def order_opts(order)
items = order.line_items.map do |item|
tax = paypal_variant_tax(item.price, item.variant)
price = (item.price * 100).to_i # convert for gateway
tax = (tax * 100).to_i # truncate the tax slice
{ :name => item.variant.product.name,
:description => item.variant.product.description[0..120],
:sku => item.variant.sku,
:qty => item.quantity,
:amount => price - tax,
:tax => tax,
:weight => item.variant.weight,
:height => item.variant.height,
:width => item.variant.width,
:depth => item.variant.weight }
end
opts = { :return_url => request.protocol + request.host_with_port + "/orders/#{order.number}/paypal_finish",
:cancel_return_url => "http://" + request.host_with_port + "/orders/#{order.number}/edit",
:order_id => order.number,
:custom => order.number,
:items => items,
}
opts
end
# hook for supplying tax amount for a single unit of a variant
# expects the sale price from the line_item and the variant itself, since
# line_item price and variant price can diverge in time
def paypal_variant_tax(sale_price, variant)
0.0
end
# hook for easy site configuration, needs to return a hash
# you probably wanto over-ride the description option here, maybe the colours and logo
def paypal_site_options(order)
{}
end
response = gateway.setup_authorization(opts[:money], opts)
unless response.success?
gateway_error(response)
redirect_to edit_order_url(@order)
return
end
# hook to allow applications to load in their own shipping and handling costs
# eg might want to estimate from cheapest shipping option and rely on ability to
# claim an extra 15% in the final auth
def paypal_shipping_and_handling_costs(order)
{}
redirect_to (gateway.redirect_url_for response.token)
end
def all_opts(order)
opts = fixed_opts.merge(order_opts order).
merge({ :shipping => 0, :handling => 0 } ).
merge(paypal_shipping_and_handling_costs order).
merge(paypal_site_options order)
# 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
# prepare the shipping and handling costs
[:shipping, :handling].each {|amt| opts[amt] *= 100 }
# overall total
opts[:money] = opts.slice(:subtotal, :tax, :shipping, :handling).values.sum
# # add the shipping and handling estimates to spree's order total
# # (spree won't add them yet, since we've not officially chosen the shipping method)
# spree_total = order.total + opts[:shipping] + opts[:handling]
# # paypal expects this sum to work out (TODO: shift to AM code? and throw wobbly?)
# # there might be rounding issues when it comes to tax, though you can capture slightly extra
# if opts[:money] != spree_total
# raise "Ouch - precision problems: #{opts[:money]} vs #{spree_total}"
# if (opts[:money].to_f - spree_total.to_f).abs > 0.01
# raise "Ouch - precision problems: #{opts[:money].to_f} vs #{spree_total.to_f}, diff #{opts[:money].to_f - spree_total.to_f}"
# end
def paypal_payment
opts = all_opts(@order, 'payment')
opts.merge!(address_and_selected_shipping_options(@order))
gateway = paypal_gateway
# suggest current user's email or any email stored in the order
opts[:email] = current_user ? current_user.email : order.checkout.email
response = gateway.setup_authorization(opts[:money], opts)
unless response.success?
gateway_error(response)
redirect_to edit_order_checkout_url(@order, :step => "payment")
return
end
opts
redirect_to (gateway.redirect_url_for response.token)
end
def paypal_checkout
# fix a shipping method if not already done - DISABLE - avoid spree totals interference
# @order.checkout.shipment.shipping_method ||= ShippingMethod.first
# @order.checkout.shipment.save
def paypal_confirm
@order = Order.find_by_number(params[:id])
opts = all_opts(@order)
opts = { :token => params[:token], :payer_id => params[:PayerID] }.merge all_opts(@order)
gateway = paypal_gateway
response = gateway.setup_authorization(opts[:money], opts)
gateway_error(response) unless response.success?
redirect_to (gateway.redirect_url_for response.token)
end
def paypal_finish
order = Order.find_by_number(params[:id])
@ppx_details = gateway.details_for params[:token]
gateway_error(@ppx_details) unless @ppx_details.success?
opts = { :token => params[:token], :payer_id => params[:PayerID] }.merge all_opts(order)
gateway = paypal_gateway
info = gateway.details_for params[:token]
gateway_error(info) unless info.success?
# now save the updated order info
@order.checkout.email = @ppx_details.email
@order.checkout.special_instructions = @ppx_details.params["note"]
# now save the order info
order.checkout.email = info.email
order.checkout.special_instructions = info.params["note"]
order.checkout.save
order.update_attribute(:user, current_user)
@order.update_attribute(:user, current_user)
# save the address info
ship_address = info.address
order_ship_address = Address.new :firstname => info.params["first_name"],
:lastname => info.params["last_name"],
ship_address = @ppx_details.address
order_ship_address = Address.new :firstname => @ppx_details.params["first_name"],
:lastname => @ppx_details.params["last_name"],
:address1 => ship_address["address1"],
:address2 => ship_address["address2"],
:city => ship_address["city"],
:country => Country.find_by_iso(ship_address["country"]),
:zipcode => ship_address["zip"],
# phone is currently blanked in AM's PPX response lib
:phone => info.params["phone"] || "(not given)"
:phone => @ppx_details.params["phone"] || "(not given)"
if (state = State.find_by_name(ship_address["state"]))
if (state = State.find_by_abbr(ship_address["state"]))
order_ship_address.state = state
else
order_ship_address.state_name = ship_address["state"]
end
order_ship_address.save!
# TODO: refine/choose the shipping method via paypal, or in advance
order.checkout.shipment.update_attributes :address => order_ship_address
@order.checkout.ship_address = order_ship_address
@order.checkout.save
render :partial => "shared/paypal_express_confirm", :layout => true
end
def paypal_finish
order = Order.find_by_number(params[:id])
opts = { :token => params[:token], :payer_id => params[:PayerID] }.merge all_opts(@order)
gateway = paypal_gateway
# now do the authorization and build the record of payment
# use the info total from paypal, in case the user has changed their order
response = gateway.authorize(opts[:money], opts)
gateway_error(response) unless response.success?
fake_card = Creditcard.new :checkout => order.checkout,
:cc_type => "visa", # fixed set of labels here
:month => Time.now.month,
:year => Time.now.year,
:first_name => info.params["first_name"],
:last_name => info.params["last_name"],
:display_number => "paypal:" + info.payer_id
payment = order.paypal_payments.create(:amount => response.params["gross_amount"].to_f,
:creditcard => fake_card)
ppx_auth_response = gateway.authorize((order.total*100).to_i, opts)
gateway_error(ppx_auth_response) unless ppx_auth_response.success?
puts "------------------------------------------------"
puts ppx_auth_response.to_yaml
puts "-----#{ppx_auth_response.avs_result.class}--------------------------------#{ppx_auth_response.avs_result["code"]}-----------"
payment = order.paypal_payments.create(:amount => ppx_auth_response.params["gross_amount"].to_f)
# query - need 0 in amount for an auth? see main code
transaction = CreditcardTxn.new( :amount => response.params["gross_amount"].to_f,
:response_code => response.authorization,
:txn_type => CreditcardTxn::TxnType::AUTHORIZE)
payment.creditcard_txns << transaction
transaction = PaypalTxn.new (:paypal_payment => payment,
:gross_amount => ppx_auth_response.params["gross_amount"].to_f,
:payment_status => ppx_auth_response.params["payment_status"],
:pending_reason => ppx_auth_response.params["pending_reason"],
:transaction_type => ppx_auth_response.params["transaction_type"],
:payment_type => ppx_auth_response.params["payment_type"],
:ack => ppx_auth_response.params["ack"],
:token => ppx_auth_response.params["token"])# ,
# :avs_code => ppx_auth_response.params["avs_result"]["code"],
# :cvv_code => ppx_auth_response.params["cvv_result"]["code"])
payment.paypal_txns << transaction
# save this for future reference
order.checkout.shipment.shipping_method ||= ShippingMethod.first
order.checkout.shipment.save
# order.checkout.shipment.shipping_method ||= ShippingMethod.first
# order.checkout.shipment.save
order.save!
order.complete # get return of status? throw of problems??? else weak go-ahead
@ -197,8 +117,8 @@ module Spree::PaypalExpress
order_params = {:checkout_complete => true}
order_params[:order_token] = order.token unless order.user
session[:order_id] = nil if order.checkout.completed_at
redirect_to order_url(order, order_params)
end
redirect_to order_url(order, order_params)
end
def do_capture(authorization)
response = paypal_gateway.capture((100 * authorization.amount).to_i, authorization.response_code)
@ -213,26 +133,164 @@ module Spree::PaypalExpress
:response_code => response.authorization,
:txn_type => CreditcardTxn::TxnType::CAPTURE )
payment.save
end
end
private
def fixed_opts
{ :description => "Goods from #{Spree::Config[:site_name]}", # site details...
#:page_style => "foobar", # merchant account can set named config
:header_image => "https://" + Spree::Config[:site_url] + "/images/logo.png",
:background_color => "ffffff", # must be hex only, six chars
:header_background_color => "ffffff",
:header_border_color => "ffffff",
:allow_note => true,
:locale => Spree::Config[:default_locale],
:notify_url => 'to be done', # this is a callback, not tried it yet
:req_confirm_shipping => false, # for security, might make an option later
# WARNING -- don't use :ship_discount, :insurance_offered, :insurance since
# they've not been tested and may trigger some paypal bugs, eg not showing order
# see http://www.pdncommunity.com/t5/PayPal-Developer-Blog/Displaying-Order-Details-in-Express-Checkout/bc-p/92902#C851
}
end
def order_opts(order, stage)
items = order.line_items.map do |item|
tax = paypal_variant_tax(item.price, item.variant)
price = (item.price * 100).to_i # convert for gateway
tax = (tax * 100).to_i # truncate the tax slice
{ :name => item.variant.product.name,
:description => item.variant.product.description[0..120],
:sku => item.variant.sku,
:qty => item.quantity,
:amount => price - tax,
:tax => tax,
:weight => item.variant.weight,
:height => item.variant.height,
:width => item.variant.width,
:depth => item.variant.weight }
end
opts = { :return_url => request.protocol + request.host_with_port + "/orders/#{order.number}/paypal_confirm",
:cancel_return_url => "http://" + request.host_with_port + "/orders/#{order.number}/edit",
:order_id => order.number,
:custom => order.number,
:items => items
}
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
# 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*100).to_i
opts[:tax] = 0 # BQ : not sure what to do here
opts[:shipping] = (order.ship_total*100).to_i
opts[:handling] = 0 # BQ : not sure what to do here
# overall total
opts[:money] = opts.slice(:subtotal, :tax, :shipping, :handling).values.sum
opts[:money] = (order.total*100).to_i
end
opts
end
# hook for supplying tax amount for a single unit of a variant
# expects the sale price from the line_item and the variant itself, since
# line_item price and variant price can diverge in time
def paypal_variant_tax(sale_price, variant)
0.0
end
def address_and_selected_shipping_options(order)
{
:no_shipping => false,
:address_override => true,
:address => {
:name => "#{order.ship_address.firstname} #{order.ship_address.lastname}",
:address1 => order.ship_address.address1,
:address2 => order.ship_address.address2,
:city => order.ship_address.city,
:state => order.ship_address.state.nil? ? order.ship_address.state_name.to_s : order.ship_address.state.abbr,
:country => order.ship_address.country.iso,
:zip => order.ship_address.zipcode,
:phone => order.ship_address.phone
}
}
end
def all_opts(order, stage=nil)
opts = fixed_opts.merge(order_opts(order, stage))#.
# merge(paypal_site_options order) BQ
if stage == "payment"
opts.merge! flat_rate_shipping_and_handling_options(order, stage)
end
# suggest current user's email or any email stored in the order
opts[:email] = current_user ? current_user.email : order.checkout.email
opts
end
# hook to allow applications to load in their own shipping and handling costs
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,
: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
end
def gateway_error(response)
text = response.params['message'] ||
response.params['response_reason_text'] ||
response.message
msg = "#{I18n.t('gateway_error')} ... #{text}"
logger.error(msg)
flash[:error] = msg
end
# create the gateway from the supplied options
def paypal_gateway
gw_defaults = { :ppx_class => "ActiveMerchant::Billing::PaypalExpressUkGateway" }
gw_opts = gw_defaults.merge(paypal_site_options @order)
begin
requires!(gw_opts, :ppx_class, :login, :password, :signature)
rescue ArgumentError => err
raise ArgumentError.new(<<"EOM" + err.message)
Problem with configuring Paypal Express Gateway:
You need to ensure that hook "paypal_site_options" sets values for login, password, and signature.
It currently produces: #{paypal_site_options.inspect}
EOM
end
gateway = gw_opts[:ppx_class].constantize.new(gw_opts)
integration = BillingIntegration.find(params[:integration_id]) if params.key? :integration_id
integration ||= BillingIntegration.current
gw_opts = integration.options
logger.debug { "-----------#{integration.provider_class}-------------------------------------" }
requires!(gw_opts, :login, :password, :signature)
gateway = integration.provider_class.new(gw_opts)
end
end

15
paypal_express_extension.rb

@ -6,25 +6,18 @@ class PaypalExpressExtension < Spree::Extension
description "Describe your extension here"
url "http://yourwebsite.com/paypal_express"
# Please use paypal_express/config/routes.rb instead for extension routes.
# def self.require_gems(config)
# config.gem "gemname-goes-here", :version => '1.2.3'
# end
def activate
# admin.tabs.add "Paypal Express", "/admin/paypal_express", :after => "Layouts", :visibility => [:all]
BillingIntegration::PaypalExpress.register
# Load up over-rides for ActiveMerchant files
# these will be submitted to ActiveMerchant some time...
require File.join(PaypalExpressExtension.root, "lib", "active_merchant", "billing", "gateways", "paypal", "paypal_common_api.rb")
require File.join(PaypalExpressExtension.root, "lib", "active_merchant", "billing", "gateways", "paypal_express_uk.rb")
require File.join(PaypalExpressExtension.root, "lib", "active_merchant", "billing", "gateways", "paypal_express_uk.rb")
# inject paypal code into orders controller
# inject paypal code into orders controller
OrdersController.class_eval do
ssl_required :paypal_checkout, :paypal_finish
include Spree::PaypalExpress
end

6
spec/spec.opts

@ -1,6 +0,0 @@
--colour
--format
progress
--loadby
mtime
--reverse

37
spec/spec_helper.rb

@ -1,37 +0,0 @@
unless defined? SPREE_ROOT
ENV["RAILS_ENV"] = "test"
case
when ENV["SPREE_ENV_FILE"]
require ENV["SPREE_ENV_FILE"]
when File.dirname(__FILE__) =~ %r{vendor/SPREE/vendor/extensions}
require "#{File.expand_path(File.dirname(__FILE__) + "/../../../../../../")}/config/environment"
else
require "#{File.expand_path(File.dirname(__FILE__) + "/../../../../")}/config/environment"
end
end
require "#{SPREE_ROOT}/spec/spec_helper"
if File.directory?(File.dirname(__FILE__) + "/scenarios")
Scenario.load_paths.unshift File.dirname(__FILE__) + "/scenarios"
end
if File.directory?(File.dirname(__FILE__) + "/matchers")
Dir[File.dirname(__FILE__) + "/matchers/*.rb"].each {|file| require file }
end
Spec::Runner.configure do |config|
# config.use_transactional_fixtures = true
# config.use_instantiated_fixtures = false
# config.fixture_path = RAILS_ROOT + '/spec/fixtures'
# You can declare fixtures for each behaviour like this:
# describe "...." do
# fixtures :table_a, :table_b
#
# Alternatively, if you prefer to declare them only once, you can
# do so here, like so ...
#
# config.global_fixtures = :table_a, :table_b
#
# If you declare global fixtures, be aware that they will be declared
# for all of your examples, even those that don't use them.
end
Loading…
Cancel
Save