From 0bcbdaa0b208144b2b5f7add67f27680667d8b18 Mon Sep 17 00:00:00 2001 From: Brian Quinn Date: Thu, 28 Jan 2010 16:48:01 +0000 Subject: [PATCH] Added support for refunds --- .../admin/paypal_payments_controller.rb | 56 +++--- app/models/paypal_payment.rb | 24 ++- app/views/admin/payments/edit.html.erb | 121 ++++++++++++ app/views/admin/paypal_payments/edit.html.erb | 33 ---- app/views/admin/paypal_payments/new.html.erb | 15 -- .../admin/paypal_payments/refund.html.erb | 15 ++ config/locales/en-GB.yml | 13 +- config/locales/en-US.yml | 11 ++ config/routes.rb | 2 +- ...128115525_add_transaction_id_to_ppx_txn.rb | 9 + lib/spree/paypal_express.rb | 173 +++++++++++------- 11 files changed, 317 insertions(+), 155 deletions(-) create mode 100644 app/views/admin/payments/edit.html.erb delete mode 100644 app/views/admin/paypal_payments/edit.html.erb delete mode 100644 app/views/admin/paypal_payments/new.html.erb create mode 100644 app/views/admin/paypal_payments/refund.html.erb create mode 100644 db/migrate/20100128115525_add_transaction_id_to_ppx_txn.rb diff --git a/app/controllers/admin/paypal_payments_controller.rb b/app/controllers/admin/paypal_payments_controller.rb index 9b1dcc5..6ae41d0 100644 --- a/app/controllers/admin/paypal_payments_controller.rb +++ b/app/controllers/admin/paypal_payments_controller.rb @@ -1,53 +1,43 @@ class Admin::PaypalPaymentsController < Admin::BaseController - before_filter :load_data - before_filter :load_amount, :except => :country_changed resource_controller belongs_to :order ssl_required - update do - wants.html { redirect_to edit_object_url } - end - - def country_changed - end - - # to allow capture (NB also included in order controller...) + # to allow capture (NB also included in checkout controller...) include Spree::PaypalExpress def capture + load_object if !@order.paypal_payments.empty? && (payment = @order.paypal_payments.last).can_capture? - do_capture(payment.find_authorization) + paypal_capture(payment.find_authorization) flash[:notice] = t("paypal_capture_complete") else flash[:error] = t("unable_to_capture_paypal") end - redirect_to edit_object_url - end + redirect_to edit_admin_order_payment_url(@order, @paypal_payment) + end - private - def load_data + + def refund load_object - @selected_country_id = params[:payment_presenter][:address_country_id].to_i if params.has_key?('payment_presenter') - @selected_country_id ||= @order.bill_address.country_id if @order and @order.bill_address - @selected_country_id ||= Spree::Config[:default_country_id] - - @states = State.find_all_by_country_id(@selected_country_id, :order => 'name') - @countries = Country.find(:all) + if params.has_key? :amount + + if !@order.paypal_payments.empty? + payment = @order.paypal_payments.first + + paypal_refund(payment.find_capture, params[:amount].to_f) + + flash[:notice] = t("paypal_refund_complete") + else + flash[:error] = t("unable_to_refund_paypal") + end + redirect_to edit_admin_order_payment_url(@order, @paypal_payment) + + + end end - # what for? - def load_amount - @amount = params[:amount] || @order.total - end - - def build_object - @object ||= end_of_association_chain.send parent? ? :build : :new, object_params - # not relevant? - # @object.creditcard = Creditcard.new(:address => @object.order.bill_address.clone) unless @object.creditcard - @object - end - + end diff --git a/app/models/paypal_payment.rb b/app/models/paypal_payment.rb index 8491e8c..7b76c2e 100644 --- a/app/models/paypal_payment.rb +++ b/app/models/paypal_payment.rb @@ -3,15 +3,21 @@ class PaypalPayment < Payment 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 => {:pending_reason => "authorization", :payment_status => "Pending"}, + :order => 'created_at DESC') + end - def can_capture? # push to parent? perhaps not - true - #txns.last == find_authorization + def find_capture + #find the transaction associated with the original authorization/capture + txns.find(:first, + :conditions => {:payment_status => "Completed"}, + :order => 'created_at DESC') + end + + def can_capture? + find_capture.nil? end end diff --git a/app/views/admin/payments/edit.html.erb b/app/views/admin/payments/edit.html.erb new file mode 100644 index 0000000..8c0b7cc --- /dev/null +++ b/app/views/admin/payments/edit.html.erb @@ -0,0 +1,121 @@ +<%= render :partial => 'admin/shared/order_tabs', :locals => {:current => "Payments"} %> + +

<%= t("activerecord.models.#{@object.class.to_s.underscore}.one") %>

+ +<%=error_messages_for :creditcard_payment %> +<% form_for(object, :url => object_url, :html => { :method => :put}) do |payment_form| %> +<%= hidden_field_tag :payment_type, object.class.to_s.underscore %> + +

+ + <%= object.amount %> +

+ + <% if object.class == CreditcardPayment %> +
+ <%= t('creditcard') %> + + + + + + + + + + + + + + + + + + + +
<%= t('card_details') %>
+ XXXX-XXXX-XXXX-<%= object.creditcard.last_digits %> + + <%= object.creditcard.month %>/<%= object.creditcard.year %> + + <%= object.creditcard.verification_value %> +
+ <%= object.creditcard.issue_number %> + + <%= object.creditcard.start_month %>/<%= object.creditcard.start_year %> +
+ + <% payment_form.fields_for :order do |order_form| %> + <% order_form.fields_for :checkout do |checkout_form| %> + + <% checkout_form.fields_for :bill_address do |ba_form| %> + <%= render :partial => "admin/checkouts/address_form", :locals => {:f => ba_form, :name => t('billing_address')} %> + <% end %> + + <% end %> + <% end %> + +
+ <% end %> + + <% if object.class == PaypalPayment %> +
+ <%= t('paypal_payment') %> + + <% object.txns.reverse.each do |txn| %> + + + + + + + + + + + + + + + + + + + + <% if txn.payment_status == "Pending" %> + + + + + <% end %> +
<%= t('transaction') %> <%= txn.transaction_id %> - <%= txn.created_at.to_s(:date_time24) %>
+ <%= txn.payment_status %> + + <%= txn.ack %> + + <%= number_to_currency txn.gross_amount %> + + <% if object.can_capture? %> + <%= link_to t("capture").titleize, capture_admin_order_paypal_payment_url(@order, object) %> + <% end %> + <%= link_to t("refund"), refund_admin_order_paypal_payment_url(@order, object) %> +
+ <%= txn.message %> + + <%= txn.paypal_payment_id %> +
+ <%= txn.pending_reason %> +
+ <% end %> + +
+ <% end %> + +

+ <%= button t('continue') %> +

+ +<% end %> + +<%#= link_to t("capture").titleize, capture_admin_order_payment_url(@order, @creditcard_payment), :confirm => t('are_you_sure_you_want_to_capture') if object.can_capture? %>   + diff --git a/app/views/admin/paypal_payments/edit.html.erb b/app/views/admin/paypal_payments/edit.html.erb deleted file mode 100644 index 72d73cd..0000000 --- a/app/views/admin/paypal_payments/edit.html.erb +++ /dev/null @@ -1,33 +0,0 @@ -<%= render :partial => 'admin/shared/order_tabs', :locals => {:current => "Payments"} %> -
-

<%= t("paypal_payment")%>

-
-<%= t("paypal_txn_id")%>:   #<%= @paypal_payment.creditcard.display_number %>
-
-<%=error_messages_for :paypal_payment %> - -<% form_for(@paypal_payment, :url => object_url, :html => { :method => :put}) do |payment_form| %> - - - - - - - - <% @paypal_payment.txns.each do |t| %> - - - - - - - <% end %> -
<%= t("transaction") %><%= t("amount") %><%= t("response_code") %><%= "#{t('spree.date')}/#{t('spree.time')}" %>
<%=CreditcardTxn::TxnType.from_value t.txn_type.to_i%><%=number_to_currency t.amount%><%=t.response_code%><%=t.created_at.to_s(:date_time24)%>
- -

- <%= button t('update') %> -

- -<% end %> -<%= link_to t("capture").titleize, capture_admin_order_paypal_payment_url(@order, @paypal_payment), :confirm => t('are_you_sure_you_want_to_capture') if @paypal_payment.can_capture? %>   -<%= link_to t("list"), collection_url %> diff --git a/app/views/admin/paypal_payments/new.html.erb b/app/views/admin/paypal_payments/new.html.erb deleted file mode 100644 index cdd55e5..0000000 --- a/app/views/admin/paypal_payments/new.html.erb +++ /dev/null @@ -1,15 +0,0 @@ -<%= render :partial => 'admin/shared/order_tabs', :locals => {:current => "Payments"} %> - -

<%= t("new_credit_card_payment")%>

- -<%=error_messages_for :creditcard_payment %> -<% form_for @creditcard_payment, :url => collection_url do |payment_form| %> -

<%= t("billing_address")%>

- <% payment_form.fields_for :creditcard do |creditcard_form| %> - <%= render :partial => 'admin/shared/form_address', :locals => {:f => creditcard_form} %> - <% end %> -

- <%= button t('continue') %> - <%= t("or") %> <%= link_to t("actions.cancel"), admin_order_payments_url(@order) %> -

-<% end %> diff --git a/app/views/admin/paypal_payments/refund.html.erb b/app/views/admin/paypal_payments/refund.html.erb new file mode 100644 index 0000000..b9ff81b --- /dev/null +++ b/app/views/admin/paypal_payments/refund.html.erb @@ -0,0 +1,15 @@ +<%= render :partial => 'admin/shared/order_tabs', :locals => {:current => "Payments"} %> + +<% form_tag do %> + +

<%= t('refund') %>

+
+

+ + <%= text_field_tag :amount, @paypal_payment.amount %> +

+

+ <%= button t("make_refund") %> +

+
+<% end %> diff --git a/config/locales/en-GB.yml b/config/locales/en-GB.yml index 98f8c2c..6a26e1d 100644 --- a/config/locales/en-GB.yml +++ b/config/locales/en-GB.yml @@ -6,4 +6,15 @@ en-GB: 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." + paypal_payment_id: PayPal Payment ID + pending_reason: Pending Reason + result: Result + activerecord: + attributes: + paypal_payment: + amount: Amount + models: + paypal_payment: + one: PayPal Payment + other: PayPal Payments \ No newline at end of file diff --git a/config/locales/en-US.yml b/config/locales/en-US.yml index 8f6c01a..5186ff3 100644 --- a/config/locales/en-US.yml +++ b/config/locales/en-US.yml @@ -7,3 +7,14 @@ en-US: 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." + paypal_payment_id: PayPal Payment ID + pending_reason: Pending Reason + result: Result + activerecord: + attributes: + paypal_payment: + amount: Amount + models: + paypal_payment: + one: PayPal Payment + other: PayPal Payments diff --git a/config/routes.rb b/config/routes.rb index e569894..e6b349d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -8,7 +8,7 @@ map.resources :paypal_express_callbacks, :only => [:index] map.namespace :admin do |admin| admin.resources :orders do |order| - order.resources :paypal_payments, :member => {:capture => :get}, :has_many => [:paypal_payments] + order.resources :paypal_payments, :member => {:capture => :get, :refund => :any}, :has_many => [:txns] end end diff --git a/db/migrate/20100128115525_add_transaction_id_to_ppx_txn.rb b/db/migrate/20100128115525_add_transaction_id_to_ppx_txn.rb new file mode 100644 index 0000000..c14caa4 --- /dev/null +++ b/db/migrate/20100128115525_add_transaction_id_to_ppx_txn.rb @@ -0,0 +1,9 @@ +class AddTransactionIdToPpxTxn < ActiveRecord::Migration + def self.up + add_column :paypal_txns, :transaction_id, :string + end + + def self.down + remove_column :paypal_txns, :transaction_id + end +end \ No newline at end of file diff --git a/lib/spree/paypal_express.rb b/lib/spree/paypal_express.rb index 966ad91..107072c 100644 --- a/lib/spree/paypal_express.rb +++ b/lib/spree/paypal_express.rb @@ -42,36 +42,39 @@ module Spree::PaypalExpress gateway = paypal_gateway @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"] + if @ppx_details.success? + # 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) + @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 => @ppx_details.params["phone"] || "(not given)" + 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 => @ppx_details.params["phone"] || "(not given)" - if (state = State.find_by_abbr(ship_address["state"])) - order_ship_address.state = 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! + + @order.checkout.ship_address = order_ship_address + @order.checkout.save + render :partial => "shared/paypal_express_confirm", :layout => true else - order_ship_address.state_name = ship_address["state"] + gateway_error(@ppx_details) end - - order_ship_address.save! - - @order.checkout.ship_address = order_ship_address - @order.checkout.save - render :partial => "shared/paypal_express_confirm", :layout => true end def paypal_finish @@ -87,52 +90,98 @@ module Spree::PaypalExpress ppx_auth_response = gateway.authorize((@order.total*100).to_i, opts) end - gateway_error(ppx_auth_response) unless ppx_auth_response.success? + if ppx_auth_response.success? - 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) - 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_response => ppx_auth_response.avs_result["code"], - :cvv_response => ppx_auth_response.cvv_result["code"]) + 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_id => ppx_auth_response.params["transaction_id"], + :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_response => ppx_auth_response.avs_result["code"], + :cvv_response => ppx_auth_response.cvv_result["code"]) - payment.paypal_txns << transaction + payment.paypal_txns << transaction - @order.save! - @checkout.reload - until @checkout.state == "complete" - @checkout.next! + @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 + + else + order_params = {} + gateway_error(ppx_auth_response) 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) 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 paypal_capture(authorization) + ppx_response = paypal_gateway.capture((100 * authorization.gross_amount).to_i, authorization.transaction_id) + + if ppx_response.success? + payment = authorization.paypal_payment + + transaction = PaypalTxn.new(:paypal_payment => payment, + :gross_amount => ppx_response.params["gross_amount"].to_f, + :message => ppx_response.params["message"], + :payment_status => ppx_response.params["payment_status"], + :pending_reason => ppx_response.params["pending_reason"], + :transaction_id => ppx_response.params["transaction_id"], + :transaction_type => ppx_response.params["transaction_type"], + :payment_type => ppx_response.params["payment_type"], + :ack => ppx_response.params["ack"], + :token => ppx_response.params["token"], + :avs_response => ppx_response.avs_result["code"], + :cvv_response => ppx_response.cvv_result["code"]) + + payment.paypal_txns << transaction + + payment.save + else + gateway_error(ppx_response) + end + end + + def paypal_refund(authorization, amount=nil) + ppx_response = paypal_gateway.credit(amount.nil? ? (100 * authorization.gross_amount).to_i : (100 * amount).to_i, authorization.transaction_id) + + if ppx_response.success? + payment = authorization.paypal_payment + + transaction = PaypalTxn.new(:paypal_payment => payment, + :gross_amount => ppx_response.params["gross_refund_amount"].to_f, + :message => ppx_response.params["message"], + :payment_status => "Refunded", + :pending_reason => ppx_response.params["pending_reason"], + :transaction_id => ppx_response.params["refund_transaction_id"], + :transaction_type => ppx_response.params["transaction_type"], + :payment_type => ppx_response.params["payment_type"], + :ack => ppx_response.params["ack"], + :token => ppx_response.params["token"], + :avs_response => ppx_response.avs_result["code"], + :cvv_response => ppx_response.cvv_result["code"]) + + payment.paypal_txns << transaction + + payment.save + else + gateway_error(ppx_response) + end + end private def fixed_opts @@ -288,8 +337,6 @@ module Spree::PaypalExpress integration = BillingIntegration.find(params[:integration_id]) if params.key? :integration_id integration ||= BillingIntegration.current - - gateway = integration.provider end end