diff --git a/app/controllers/paypal_express_callbacks_controller.rb b/app/controllers/paypal_express_callbacks_controller.rb new file mode 100644 index 0000000..863ce1f --- /dev/null +++ b/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 diff --git a/app/models/billing_integration/paypal_express.rb b/app/models/billing_integration/paypal_express.rb new file mode 100644 index 0000000..c37e35f --- /dev/null +++ b/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 diff --git a/app/models/paypal_payment.rb b/app/models/paypal_payment.rb index ac76f9f..8491e8c 100644 --- a/app/models/paypal_payment.rb +++ b/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 diff --git a/app/models/paypal_txn.rb b/app/models/paypal_txn.rb new file mode 100644 index 0000000..885fe4a --- /dev/null +++ b/app/models/paypal_txn.rb @@ -0,0 +1,3 @@ +class PaypalTxn < ActiveRecord::Base + belongs_to :paypal_payment +end diff --git a/app/views/shared/_paypal_button.html.erb b/app/views/shared/_paypal_button.html.erb deleted file mode 100644 index 0ac9f15..0000000 --- a/app/views/shared/_paypal_button.html.erb +++ /dev/null @@ -1,6 +0,0 @@ -
-- OR CHOOSE - -
- - - diff --git a/app/views/shared/_paypal_express_checkout.html.erb b/app/views/shared/_paypal_express_checkout.html.erb new file mode 100644 index 0000000..517cb05 --- /dev/null +++ b/app/views/shared/_paypal_express_checkout.html.erb @@ -0,0 +1,3 @@ + + + diff --git a/app/views/shared/_paypal_express_confirm.html.erb b/app/views/shared/_paypal_express_confirm.html.erb new file mode 100644 index 0000000..a5f776a --- /dev/null +++ b/app/views/shared/_paypal_express_confirm.html.erb @@ -0,0 +1,17 @@ +

<%= t("confirm") %>

+

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

+ + <%= render :partial => 'shared/order_details', :locals => {:order => @order} -%> +
+ <%= button_to t('place_order'), "/orders/#{@order.number}/paypal_finish?token=#{params[:token]}&PayerID=#{params[:PayerID]}", :class => "button primary" %> + +
+ + + + +
+<%= @ppx_details.to_yaml %>
+
\ No newline at end of file diff --git a/app/views/shared/_paypal_express_finish.html.erb b/app/views/shared/_paypal_express_finish.html.erb new file mode 100644 index 0000000..b07dd2c --- /dev/null +++ b/app/views/shared/_paypal_express_finish.html.erb @@ -0,0 +1 @@ +Thanks. \ No newline at end of file diff --git a/app/views/shared/_paypal_express_payment.html.erb b/app/views/shared/_paypal_express_payment.html.erb new file mode 100644 index 0000000..1b5d71b --- /dev/null +++ b/app/views/shared/_paypal_express_payment.html.erb @@ -0,0 +1,3 @@ + + + diff --git a/config/locales/en-GB.yml b/config/locales/en-GB.yml index d947c67..98f8c2c 100644 --- a/config/locales/en-GB.yml +++ b/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 diff --git a/config/locales/en-US.yml b/config/locales/en-US.yml new file mode 100644 index 0000000..8f6c01a --- /dev/null +++ b/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 not been be placed, please review the details and click Confirm below to finalise your order." diff --git a/config/routes.rb b/config/routes.rb index caf8bc8..6f43b20 100644 --- a/config/routes.rb +++ b/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| diff --git a/db/migrate/20090513103111_create_paypal_express_gateway.rb b/db/migrate/20090513103111_create_paypal_express_gateway.rb deleted file mode 100644 index f222981..0000000 --- a/db/migrate/20090513103111_create_paypal_express_gateway.rb +++ /dev/null @@ -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 diff --git a/db/migrate/20100122120551_create_paypal_txns.rb b/db/migrate/20100122120551_create_paypal_txns.rb new file mode 100644 index 0000000..a0dbbb5 --- /dev/null +++ b/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 diff --git a/lib/active_merchant/billing/gateways/paypal/paypal_common_api.rb b/lib/active_merchant/billing/gateways/paypal/paypal_common_api.rb index 14e45ef..7fa9690 100644 --- a/lib/active_merchant/billing/gateways/paypal/paypal_common_api.rb +++ b/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 # - # :signature The text of your PayPal signature. + # :signature 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 diff --git a/lib/active_merchant/billing/gateways/paypal_express.rb b/lib/active_merchant/billing/gateways/paypal_express.rb index 8565832..a19b047 100644 --- a/lib/active_merchant/billing/gateways/paypal_express.rb +++ b/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 diff --git a/lib/spree/paypal_express.rb b/lib/spree/paypal_express.rb index dd1e04c..b80820c 100644 --- a/lib/spree/paypal_express.rb +++ b/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 - - - 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 + include ActiveMerchant::RequiresParameters 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 - - opts = all_opts(@order) + opts = all_opts(@order, 'checkout') + opts.merge!(address_and_selected_shipping_options(@order)) gateway = paypal_gateway + 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 - def paypal_finish - order = Order.find_by_number(params[:id]) - - opts = { :token => params[:token], :payer_id => params[:PayerID] }.merge all_opts(order) + def paypal_payment + opts = all_opts(@order, 'payment') + opts.merge!(address_and_selected_shipping_options(@order)) gateway = paypal_gateway - info = gateway.details_for params[:token] - gateway_error(info) unless info.success? + response = gateway.setup_authorization(opts[:money], opts) + 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 - ship_address = info.address - order_ship_address = Address.new :firstname => info.params["first_name"], - :lastname => info.params["last_name"], + # now save the updated order info + @order.checkout.email = @ppx_details.email + @order.checkout.special_instructions = @ppx_details.params["note"] + + @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"], :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? + 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 - 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) + 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 + 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 = gw_opts[:ppx_class].constantize.new(gw_opts) + gateway = integration.provider_class.new(gw_opts) end end diff --git a/paypal_express_extension.rb b/paypal_express_extension.rb index 47ba90a..e949b25 100644 --- a/paypal_express_extension.rb +++ b/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 diff --git a/spec/spec.opts b/spec/spec.opts deleted file mode 100644 index d8c8db5..0000000 --- a/spec/spec.opts +++ /dev/null @@ -1,6 +0,0 @@ ---colour ---format -progress ---loadby -mtime ---reverse diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb deleted file mode 100644 index ffde315..0000000 --- a/spec/spec_helper.rb +++ /dev/null @@ -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 \ No newline at end of file