Initial fork changes

This commit is contained in:
Brian Quinn 2010-01-22 16:29:55 +00:00
parent 48bdd6c2d1
commit 7da2fed7ac
20 changed files with 427 additions and 338 deletions

View File

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

View File

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

View File

@ -1,18 +1,17 @@
class PaypalPayment < Payment class PaypalPayment < Payment
has_many :creditcard_txns, :foreign_key => 'creditcard_payment_id' # reused and faked has_many :paypal_txns
belongs_to :creditcard # allow for saving of fake details
# accepts_nested_attributes_for :creditcard
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 def can_capture? # push to parent? perhaps not
txns.last == find_authorization true
#txns.last == find_authorization
end end
end end

3
app/models/paypal_txn.rb Normal file
View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1 @@
Thanks.

View File

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

View File

@ -5,4 +5,5 @@ en-GB:
paypal_txn_id: Transaction Code paypal_txn_id: Transaction Code
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

9
config/locales/en-US.yml Normal file
View File

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

View File

@ -1,6 +1,8 @@
# Put your extension routes here. # 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| map.namespace :admin do |admin|
admin.resources :orders do |order| admin.resources :orders do |order|

View File

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

View File

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

View File

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

View File

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

View File

@ -1,193 +1,113 @@
# aim to unpick this later # aim to unpick this later
module Spree::PaypalExpress module Spree::PaypalExpress
include ERB::Util include ERB::Util
include Spree::PaymentGateway include ActiveMerchant::RequiresParameters
include ActiveMerchant::RequiresParameters
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
# 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)
{}
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
# suggest current user's email or any email stored in the order
opts[:email] = current_user ? current_user.email : order.checkout.email
opts
end
def paypal_checkout def paypal_checkout
# fix a shipping method if not already done - DISABLE - avoid spree totals interference opts = all_opts(@order, 'checkout')
# @order.checkout.shipment.shipping_method ||= ShippingMethod.first opts.merge!(address_and_selected_shipping_options(@order))
# @order.checkout.shipment.save
opts = all_opts(@order)
gateway = paypal_gateway gateway = paypal_gateway
response = gateway.setup_authorization(opts[:money], opts) response = gateway.setup_authorization(opts[:money], opts)
unless response.success?
gateway_error(response)
redirect_to edit_order_url(@order)
return
end
gateway_error(response) unless response.success? redirect_to (gateway.redirect_url_for response.token)
redirect_to (gateway.redirect_url_for response.token)
end end
def paypal_finish def paypal_payment
order = Order.find_by_number(params[:id]) opts = all_opts(@order, 'payment')
opts.merge!(address_and_selected_shipping_options(@order))
opts = { :token => params[:token], :payer_id => params[:PayerID] }.merge all_opts(order)
gateway = paypal_gateway gateway = paypal_gateway
info = gateway.details_for params[:token] response = gateway.setup_authorization(opts[:money], opts)
gateway_error(info) unless info.success? unless response.success?
gateway_error(response)
redirect_to edit_order_checkout_url(@order, :step => "payment")
return
end
redirect_to (gateway.redirect_url_for response.token)
end
def paypal_confirm
@order = Order.find_by_number(params[:id])
opts = { :token => params[:token], :payer_id => params[:PayerID] }.merge all_opts(@order)
gateway = paypal_gateway
@ppx_details = gateway.details_for params[:token]
gateway_error(@ppx_details) unless @ppx_details.success?
# 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)
# save the address info # now save the updated order info
ship_address = info.address @order.checkout.email = @ppx_details.email
order_ship_address = Address.new :firstname => info.params["first_name"], @order.checkout.special_instructions = @ppx_details.params["note"]
:lastname => info.params["last_name"],
@order.update_attribute(:user, current_user)
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"], :address1 => ship_address["address1"],
:address2 => ship_address["address2"], :address2 => ship_address["address2"],
:city => ship_address["city"], :city => ship_address["city"],
:country => Country.find_by_iso(ship_address["country"]), :country => Country.find_by_iso(ship_address["country"]),
:zipcode => ship_address["zip"], :zipcode => ship_address["zip"],
# phone is currently blanked in AM's PPX response lib # 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 order_ship_address.state = state
else else
order_ship_address.state_name = ship_address["state"] order_ship_address.state_name = ship_address["state"]
end end
order_ship_address.save! order_ship_address.save!
# TODO: refine/choose the shipping method via paypal, or in advance @order.checkout.ship_address = order_ship_address
order.checkout.shipment.update_attributes :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 ppx_auth_response = gateway.authorize((order.total*100).to_i, opts)
# use the info total from paypal, in case the user has changed their order gateway_error(ppx_auth_response) unless ppx_auth_response.success?
response = gateway.authorize(opts[:money], opts) puts "------------------------------------------------"
gateway_error(response) unless response.success? puts ppx_auth_response.to_yaml
fake_card = Creditcard.new :checkout => order.checkout, puts "-----#{ppx_auth_response.avs_result.class}--------------------------------#{ppx_auth_response.avs_result["code"]}-----------"
:cc_type => "visa", # fixed set of labels here
:month => Time.now.month, payment = order.paypal_payments.create(:amount => ppx_auth_response.params["gross_amount"].to_f)
: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)
# query - need 0 in amount for an auth? see main code # query - need 0 in amount for an auth? see main code
transaction = CreditcardTxn.new( :amount => response.params["gross_amount"].to_f, transaction = PaypalTxn.new (:paypal_payment => payment,
:response_code => response.authorization, :gross_amount => ppx_auth_response.params["gross_amount"].to_f,
:txn_type => CreditcardTxn::TxnType::AUTHORIZE) :payment_status => ppx_auth_response.params["payment_status"],
payment.creditcard_txns << transaction :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 # save this for future reference
order.checkout.shipment.shipping_method ||= ShippingMethod.first # order.checkout.shipment.shipping_method ||= ShippingMethod.first
order.checkout.shipment.save # order.checkout.shipment.save
order.save! order.save!
order.complete # get return of status? throw of problems??? else weak go-ahead 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 = {:checkout_complete => true}
order_params[:order_token] = order.token unless order.user order_params[:order_token] = order.token unless order.user
session[:order_id] = nil if order.checkout.completed_at session[:order_id] = nil if order.checkout.completed_at
redirect_to order_url(order, order_params) redirect_to order_url(order, order_params)
end end
def do_capture(authorization) def do_capture(authorization)
response = paypal_gateway.capture((100 * authorization.amount).to_i, authorization.response_code) response = paypal_gateway.capture((100 * authorization.amount).to_i, authorization.response_code)
@ -213,26 +133,164 @@ module Spree::PaypalExpress
:response_code => response.authorization, :response_code => response.authorization,
:txn_type => CreditcardTxn::TxnType::CAPTURE ) :txn_type => CreditcardTxn::TxnType::CAPTURE )
payment.save payment.save
end end
private 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 # create the gateway from the supplied options
def paypal_gateway def paypal_gateway
gw_defaults = { :ppx_class => "ActiveMerchant::Billing::PaypalExpressUkGateway" } integration = BillingIntegration.find(params[:integration_id]) if params.key? :integration_id
gw_opts = gw_defaults.merge(paypal_site_options @order) integration ||= BillingIntegration.current
gw_opts = integration.options
begin logger.debug { "-----------#{integration.provider_class}-------------------------------------" }
requires!(gw_opts, :ppx_class, :login, :password, :signature) requires!(gw_opts, :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) gateway = integration.provider_class.new(gw_opts)
end end
end end

View File

@ -6,25 +6,18 @@ class PaypalExpressExtension < Spree::Extension
description "Describe your extension here" description "Describe your extension here"
url "http://yourwebsite.com/paypal_express" 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 def activate
# admin.tabs.add "Paypal Express", "/admin/paypal_express", :after => "Layouts", :visibility => [:all] BillingIntegration::PaypalExpress.register
# Load up over-rides for ActiveMerchant files # Load up over-rides for ActiveMerchant files
# these will be submitted to ActiveMerchant some time... # 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", "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")
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 OrdersController.class_eval do
ssl_required :paypal_checkout, :paypal_finish
include Spree::PaypalExpress include Spree::PaypalExpress
end end

View File

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

View File

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