Updated to support core Billing Integrations

This commit is contained in:
Brian Quinn 2010-01-25 10:31:43 +00:00
parent 7da2fed7ac
commit c14eefa55a
13 changed files with 103 additions and 345 deletions

View File

@ -1,134 +1,20 @@
# Paypal Express for Spree
# Official PayPal Express for Spree
Bridge between ActiveMerchant's Paypal Express (henceforth PPX) gateway code and Spree
This is the official PayPal Express extension for Spree, based on the extension by PaulCC it has been extended to support Spree's
Billing Integrations which allows users to configure the PayPal Express gateway including API login / password and signatures fields
via the Admin UI.
This extension allows the store to use PayPal Express from two locations:
## Setup and Customization
1. Checkout Payment - When configured the PayPal Express checkout button will appear alongside the standard credit card payment
options on the payment stage of the standard checkout. The selected shipping address and shipping method / costs are automatically
sent to the PayPal review page (along with detailed order information).
THIS FEATURE IS NOT YET COMPLETE
2. Cart Checkout - Presents the PayPal checkout button on the users Cart page and redirects the user to complete all shipping / addressing
information on PaypPal's site. This also supports PayPal's Instant Update feature to retrieve shipping options live from Spree when the user
selects / changes their shipping address on PayPal's site.
1. Start by identifying the relevant class representing your locale's paypal express gateway
2. If there isn't one, then you can easily create one and patch it in. You can drop it in
the directory +lib/active_merchant/billing/gateways/+ in an extension - probably best in your
+site+ extension - and make sure it is loaded with the following line (suitably modified) in
your extension activation code :
require File.join(SiteExtension.root, "lib", "active_merchant", "billing", "gateways", "paypal_express_narnia.rb")
See how I've handled the UK gateway customization in this extension for more info.
3. Over-ride the hook +paypal_site_options+ in the OrdersController (eg with a +class_eval+ in your
+site+ extension's activation code) so that it returns a _hash_ with at least the following
fields. The hook is sent the current order value, in case it is needed.
* +:ppx_class+ -- name of the actual gateway class
* +:login+ -- the merchant's login email address
* +:password+ -- the merchant's Paypal API Credentials Password
* +:signature+ -- the merchant's Paypal API Credentials signature string
4. You can also over-ride other PPX settings from this hook, eg the +:description+ string
attached to transactions, or the colour scheme and logo, or ... (see +lib/spree/paypal_express.rb+
for more information.
5. Over-ride the hook +paypal_variant_tax+ to calculate the tax amount for a single unit of a
variant. The hook is passed the +price+ from the containing +line_item+, plus the variant
itself. Note that the line_item price and the variant price can diverge (the former won't be
changed if the administrator changes the variant price), and Spree usually ignores the
variant price after the line_item has been created, so you probably want to calculate tax
from the line_item price. You should return a floating point number here. The hook is
located in the OrdersController.
6. Over-ride the hook +paypal_shipping_and_handling_costs+ (also in the OrdersController), to
determine a shipping and handling estimate for the order. See below for a discussion of
shipping issues and how they affect PPX.
The hook is sent the order value, and must return a _hash_ containing (at least) a
:shipping and a :handling value (both floats), which are the total costs for the order.
## Interaction with Spree
The bridge code receives authorization and transaction info from PPX and converts it into the Spree
equivalent.
The payment representation isn't perfect: basically, Spree is oriented towards creditcards and some
work is needed to generalise it to other options. For now, it is a bit hacked. (See the TODO list.)
## Relationship with Active Merchant
This extension contains three files which are updates or extensions to current active merchant code. They are
loaded up when the extension is initialized, and will over-ride the existing gem files. The modifications
update the base protocol, eg allowing detailed order info to be passed, and supporting _some_ (not all)
of the new options in version 57.0.
## Testing
Get an account for Paypal's Sandbox system first. Very good testing system!
Pity it logs you off automatically after a relatively short time period
## Status and Known issues
IMPORTANT: requires spree version 0.8.5 or later (there's a tag in the repo for earlier versions, but it needs some bug patching now)
[15Jul09] I don't know of any serious bugs or issues at present in this code, so you should be able to
start using this without serious problems - but do note the TODO list below.
Temporarily, I've had to over-ride two admin views: order/show and payments/index: this will be unpicked
once Spree is generalised to support payment types other than creditcards
WARNING: there seems to be an issue with the :shipping_discount issue which causes submitted order
info to be ignored (and not displayed - probably because the PPX addition checking doesn't tally).
See +lib/spree/paypal_express.rb+ for more info. I suggest avoiding this option unless you've
tested it carefully. The insurance options are also not tested yet.
## Hooks
These were discussed in the customization section above, but for reference, they are:
* +paypal_variant_tax(sale_price, variant)+
* +paypal_site_opts(order)+
* +paypal_shipping_and_handling_costs(order)+
## Shipping Issues
It is important to note that Spree won't have selected a shipping method when the PPX process
is started. My sites only have a single shipping method, so I can get away with defaulting to
that method and using that for calculations. It also means that I've not written any code yet
for selecting from applicable methods etc etc.
Beware that this code does make some big assumptions about shipping. In particular, it AVOIDS
use of the Spree shipping calcs, effectively performing its own calcs (via the hook), but then
assigning the first shipping method at the end, just so order display will work. This stuff
is ok when there's a single shipping option defined (like me), but will need work if you have
more options.
Note that PPX allows you to capture up to 115% of the original authorized amount: this could
allow some flexibility in shipping choices, eg you could add a stage after return from PPX
which asks for a shipping choice and confirms the final amount to be captured.
It seems that PPX might have support for choosing a shipping method on its screens, but I
have not tried to use this yet.
## TODO
0. Add support for accepting PPX payment at the credit card stage (important)
1. Look at using PPX to assist in shipping method choices (or present user with a choice before
they jump to PPX interaction)
2. Improve payment tracking support in Spree (eg generalise beyond creditcard bias)
3. Add some tests
4. Get some of my code into active merchant
5. Double-check implementation of the full PPX process
6. Look at shipping method selection integration
This extension follows the documented flow for a PayPal Express Checkout, where a user is forwarded to PayPal to allow them to login and review
the order (possibly select / change shipping address and method), then the user is redirected back to Spree to confirm the order. The user
MUST confirm the order on the Spree site before the payment is authorized / captured from PayPal (and the order is transitioned to the New state).

View File

@ -1,80 +0,0 @@
<div class='toolbar order-links'>
<%= button_link_to t("resend"), resend_admin_order_url(@order), :method => :post, :icon => 'send-email' %>
<%= event_links %>
</div>
<%= render :partial => 'admin/shared/order_tabs', :locals => {:current => "Order Details"} %>
<%= render :partial => 'admin/shared/order_details', :locals => {:order => @order} -%>
<% if @order.bill_address %>
<div class='adr'>
<h4>
<%= link_to t("bill_address"), edit_admin_order_creditcard_payment_url(@order, @order.creditcard_payments.last) if @order.creditcard_payments.present? %>
<%= t("bill_address") if @order.creditcard_payments.empty? %>
</h4>
<%= render :partial => 'admin/shared/address', :locals => {:address => @order.bill_address} %>
</div>
<% end %>
<% if (payment = @order.payments.last).class != CreditcardPayment %>
<%# look at the most recent (= up to date) payment for paypal %>
<% url = edit_admin_order_paypal_payment_url(@order) %> <%#, payment) %>
<div class='adr'>
<h4><%= link_to t("edit_paypal_info"), edit_admin_order_paypal_payment_url(@order, payment) %></h4>
(no billing address available)
</div>
<% end %>
<% if @order.ship_address %>
<div class='adr'>
<h4>
<%= link_to t("ship_address"), edit_admin_order_shipment_url(@order, @order.shipments.last) if @order.shipments.present? %>
<%= t("ship_address") if @order.shipments.empty? %>
</h4>
<%= render :partial => 'admin/shared/address', :locals => {:address => @order.ship_address} %>
</div>
<% end %>
<hr />
<table class="index">
<tr>
<th><%= t("email") %></th>
</tr>
<tr>
<td><%= @order.email %></td>
</tr>
</table>
<% unless @order.special_instructions.blank? %>
<table class="index">
<tr>
<th><%= t("shipping_instructions") %></th>
</tr>
<tr>
<td><pre><%= @order.special_instructions %></pre></td>
</tr>
</table>
<% end %>
<h4><%= t('history') %></h4>
<table class="index">
<tr>
<th><%= t("event") %></th>
<th><%= t("user") %></th>
<th><%= "#{t('spree.date')}/#{t('spree.time')}" %></th>
</tr>
<% @order.state_events.sort.each do |event| %>
<tr>
<td><%=t("#{event.name}") %></td>
<td><%=event.user.email if event.user %></td>
<td><%=event.created_at.to_s(:date_time24) %></td>
</tr>
<% end %>
<% if @order.state_events.empty? %>
<tr>
<td colspan="3"><%= t("none_available") %></td>
</tr>
<% end %>
</table>

View File

@ -1,31 +0,0 @@
<div class='toolbar'>
<ul class='actions'>
<li>
<%= button_link_to t("new_credit_card_payment"), new_admin_order_creditcard_payment_url(@order), :icon => 'add' %>
</li>
</ul>
<br class='clear' />
</div>
<%= render :partial => 'admin/shared/order_tabs', :locals => {:current => "Payments"} %>
<table class="index">
<tr>
<th><%= "#{t('spree.date')}/#{t('spree.time')}" %></th>
<th><%= t("amount") %></th>
<th><%= t("type") %></th>
<th></th>
</tr>
<% @payments.each do |payment| %>
<tr>
<td><%= payment.created_at.to_s(:date_time24) %></td>
<td><%= number_to_currency(payment.amount) %></td>
<td><%= payment.class.to_s %></td>
<!-- TODO: don't assume credit card, make it possible to edit other kinds of payments -->
<td>
<% url = payment.type == "CreditcardPayment" ? edit_admin_order_creditcard_payment_url(@order, payment) : edit_admin_order_paypal_payment_url(@order, payment) %>
<%= link_to_with_icon 'edit', t('edit'), url %>
</td>
</tr>
<% end %>
</table>

View File

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

View File

@ -3,15 +3,7 @@
<%= t("order_not_yet_placed") %>
</p>
<%= render :partial => 'shared/order_details', :locals => {:order => @order} -%>
<%= 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" %>
<%= button_to t('place_order'), paypal_finish_order_checkout_url(@checkout.order, {:token => params[:token] , :PayerID => params[:PayerID] } ), :class => "button primary" %>
</div>
<pre>
<%= @ppx_details.to_yaml %>
</pre>

View File

@ -1 +0,0 @@
Thanks.

View File

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

View File

@ -1,2 +0,0 @@
<!-- PayPal Logo --><table border="0" cellpadding="10" cellspacing="0" align="center"><tr><td align="center"></td></tr><tr><td align="center"><a href="#" onclick="javascript:window.open('https://www.paypal.com/uk/cgi-bin/webscr?cmd=xpt/Marketing/popup/OLCWhatIsPayPal-outside','olcwhatispaypal','toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=yes, resizable=yes, width=400, height=350');"><img src="https://www.paypal.com/en_GB/GB/i/logo/PayPal_mark_50x34.gif" border="0" alt="Acceptance Mark"></a></td></tr></table><!-- PayPal Logo -->

View File

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

View File

@ -8,41 +8,43 @@ module ActiveMerchant #:nodoc:
base.cattr_accessor :signature
end
API_VERSION = '60.0' # TODO - check absolute adherence in this file, override in sub?
silence_warnings do
API_VERSION = '60.0'
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/' }
}
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'
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'
}
CREDENTIALS_NAMESPACES = { 'xmlns' => PAYPAL_NAMESPACE,
'xmlns:n1' => EBAY_NAMESPACE,
'env:mustUnderstand' => '0'
}
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'
}
CREDENTIALS_NAMESPACES = { 'xmlns' => PAYPAL_NAMESPACE,
'xmlns:n1' => EBAY_NAMESPACE,
'env:mustUnderstand' => '0'
}
AUSTRALIAN_STATES = {
'ACT' => 'Australian Capital Territory',
'NSW' => 'New South Wales',
'NT' => 'Northern Territory',
'QLD' => 'Queensland',
'SA' => 'South Australia',
'TAS' => 'Tasmania',
'VIC' => 'Victoria',
'WA' => 'Western Australia'
}
AUSTRALIAN_STATES = {
'ACT' => 'Australian Capital Territory',
'NSW' => 'New South Wales',
'NT' => 'Northern Territory',
'QLD' => 'Queensland',
'SA' => 'South Australia',
'TAS' => 'Tasmania',
'VIC' => 'Victoria',
'WA' => 'Western Australia'
}
SUCCESS_CODES = [ 'Success', 'SuccessWithWarning' ]
SUCCESS_CODES = [ 'Success', 'SuccessWithWarning' ]
FRAUD_REVIEW_CODE = "11610"
FRAUD_REVIEW_CODE = "11610"
end
# The gateway must be configured with either your PayPal PEM file
# or your PayPal API Signature. Only one is required.
@ -314,8 +316,6 @@ module ActiveMerchant #:nodoc:
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

View File

@ -16,11 +16,7 @@ module ActiveMerchant #:nodoc:
def setup_authorization(money, options = {})
requires!(options, :return_url, :cancel_return_url)
req = build_setup_request('Authorization', money, options)
puts req
commit 'SetExpressCheckout', req
commit 'SetExpressCheckout', build_setup_request('Authorization', money, options)
end
def setup_purchase(money, options = {})
@ -36,11 +32,7 @@ module ActiveMerchant #:nodoc:
def authorize(money, options = {})
requires!(options, :token, :payer_id)
req = build_sale_or_authorization_request('Authorization', money, options)
puts req
commit 'DoExpressCheckoutPayment', req
commit 'DoExpressCheckoutPayment', build_sale_or_authorization_request('Authorization', money, options)
end
def purchase(money, options = {})

View File

@ -4,8 +4,9 @@ module Spree::PaypalExpress
include ActiveMerchant::RequiresParameters
def paypal_checkout
load_object
opts = all_opts(@order, 'checkout')
opts.merge!(address_and_selected_shipping_options(@order))
opts.merge!(address_options(@order))
gateway = paypal_gateway
response = gateway.setup_authorization(opts[:money], opts)
@ -19,8 +20,9 @@ module Spree::PaypalExpress
end
def paypal_payment
load_object
opts = all_opts(@order, 'payment')
opts.merge!(address_and_selected_shipping_options(@order))
opts.merge!(address_options(@order))
gateway = paypal_gateway
response = gateway.setup_authorization(opts[:money], opts)
@ -34,7 +36,7 @@ module Spree::PaypalExpress
end
def paypal_confirm
@order = Order.find_by_number(params[:id])
load_object
opts = { :token => params[:token], :payer_id => params[:PayerID] }.merge all_opts(@order)
gateway = paypal_gateway
@ -42,8 +44,6 @@ module Spree::PaypalExpress
@ppx_details = gateway.details_for params[:token]
gateway_error(@ppx_details) unless @ppx_details.success?
# now save the updated order info
@order.checkout.email = @ppx_details.email
@order.checkout.special_instructions = @ppx_details.params["note"]
@ -75,65 +75,64 @@ module Spree::PaypalExpress
end
def paypal_finish
order = Order.find_by_number(params[:id])
load_object
#order = Order.find_by_number(params[:id])
opts = { :token => params[:token], :payer_id => params[:PayerID] }.merge all_opts(@order)
gateway = paypal_gateway
if Spree::Config[:auto_capture]
ppx_auth_response = gateway.purchase((@order.total*100).to_i, opts)
else
ppx_auth_response = gateway.authorize((@order.total*100).to_i, opts)
end
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)
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 = PaypalTxn.new (:paypal_payment => payment,
transaction = PaypalTxn.new(:paypal_payment => payment,
:gross_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_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"])
:token => ppx_auth_response.params["token"],
:avs_code => ppx_auth_response.avs_result["code"],
:cvv_code => ppx_auth_response.cvv_result["code"])
payment.paypal_txns << transaction
# save this for future reference
# 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
@order.save!
@checkout.reload
until @checkout.state == "complete"
@checkout.next!
end
# todo - share code
flash[:notice] = t('order_processed_successfully')
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)
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
def do_capture(authorization)
response = paypal_gateway.capture((100 * authorization.amount).to_i, authorization.response_code)
gateway_error(response) unless response.success?
# TODO needs to be cleaned up or recast...
payment = PaypalPayment.find(authorization.creditcard_payment_id)
# create a transaction to reflect the capture
payment.txns << CreditcardTxn.new( :amount => authorization.amount,
:response_code => response.authorization,
:txn_type => CreditcardTxn::TxnType::CAPTURE )
payment.save
end
# def do_capture(authorization)
# response = paypal_gateway.capture((100 * authorization.amount).to_i, authorization.response_code)
#
# gateway_error(response) unless response.success?
#
# # TODO needs to be cleaned up or recast...
# payment = PaypalPayment.find(authorization.creditcard_payment_id)
#
# # create a transaction to reflect the capture
# payment.txns << CreditcardTxn.new( :amount => authorization.amount,
# :response_code => response.authorization,
# :txn_type => CreditcardTxn::TxnType::CAPTURE )
# payment.save
# end
private
def fixed_opts
@ -175,7 +174,7 @@ module Spree::PaypalExpress
end
opts = { :return_url => request.protocol + request.host_with_port + "/orders/#{order.number}/paypal_confirm",
opts = { :return_url => request.protocol + request.host_with_port + "/orders/#{order.number}/checkout/paypal_confirm",
:cancel_return_url => "http://" + request.host_with_port + "/orders/#{order.number}/edit",
:order_id => order.number,
:custom => order.number,
@ -220,7 +219,7 @@ module Spree::PaypalExpress
0.0
end
def address_and_selected_shipping_options(order)
def address_options(order)
{
:no_shipping => false,
:address_override => true,
@ -288,7 +287,7 @@ module Spree::PaypalExpress
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)

View File

@ -8,6 +8,7 @@ class PaypalExpressExtension < Spree::Extension
def activate
BillingIntegration::PaypalExpress.register
BillingIntegration::PaypalExpressUk.register
# Load up over-rides for ActiveMerchant files
# these will be submitted to ActiveMerchant some time...
@ -17,7 +18,7 @@ class PaypalExpressExtension < Spree::Extension
# inject paypal code into orders controller
OrdersController.class_eval do
CheckoutsController.class_eval do
include Spree::PaypalExpress
end