spree_paypal_express_mutalis 2.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. checksums.yaml +15 -0
  2. data/.gitignore +10 -0
  3. data/.rspec +1 -0
  4. data/.travis.yml +16 -0
  5. data/CONTRIBUTING.md +81 -0
  6. data/Gemfile +8 -0
  7. data/LICENSE.md +26 -0
  8. data/README.md +86 -0
  9. data/Rakefile +23 -0
  10. data/app/assets/javascripts/admin/spree_paypal_express.js +25 -0
  11. data/app/assets/javascripts/store/spree_paypal_express.js +19 -0
  12. data/app/assets/stylesheets/admin/spree_paypal_express.css +3 -0
  13. data/app/assets/stylesheets/store/spree_paypal_express.css +3 -0
  14. data/app/controllers/spree/admin/payments_controller_decorator.rb +19 -0
  15. data/app/controllers/spree/admin/paypal_payments_controller.rb +15 -0
  16. data/app/controllers/spree/paypal_controller.rb +144 -0
  17. data/app/models/spree/gateway/pay_pal_express.rb +100 -0
  18. data/app/models/spree/paypal_express_checkout.rb +4 -0
  19. data/app/views/spree/admin/payments/_paypal_complete.html.erb +20 -0
  20. data/app/views/spree/admin/payments/paypal_refund.html.erb +31 -0
  21. data/app/views/spree/admin/payments/source_forms/_paypal.html.erb +6 -0
  22. data/app/views/spree/admin/payments/source_views/_paypal.html.erb +35 -0
  23. data/app/views/spree/checkout/payment/_paypal.html.erb +6 -0
  24. data/config/locales/en.yml +16 -0
  25. data/config/routes.rb +18 -0
  26. data/db/migrate/20130723042610_create_spree_paypal_express_checkouts.rb +8 -0
  27. data/db/migrate/20130808030836_add_transaction_id_to_spree_paypal_express_checkouts.rb +6 -0
  28. data/db/migrate/20130809013846_add_state_to_spree_paypal_express_checkouts.rb +5 -0
  29. data/db/migrate/20130809014319_add_refunded_fields_to_spree_paypal_express_checkouts.rb +8 -0
  30. data/lib/generators/spree_paypal_express/install/install_generator.rb +31 -0
  31. data/lib/spree_paypal_express.rb +3 -0
  32. data/lib/spree_paypal_express/engine.rb +26 -0
  33. data/lib/spree_paypal_express/factories.rb +6 -0
  34. data/lib/spree_paypal_express/version.rb +3 -0
  35. data/script/rails +7 -0
  36. data/spec/features/paypal_spec.rb +244 -0
  37. data/spec/models/pay_pal_express_spec.rb +50 -0
  38. data/spec/spec_helper.rb +45 -0
  39. data/spree_paypal_express.gemspec +39 -0
  40. metadata +253 -0
@@ -0,0 +1,100 @@
1
+ require 'paypal-sdk-merchant'
2
+ module Spree
3
+ class Gateway::PayPalExpress < Gateway
4
+ preference :login, :string
5
+ preference :password, :string
6
+ preference :signature, :string
7
+ preference :server, :string, default: 'sandbox'
8
+
9
+ def supports?(source)
10
+ true
11
+ end
12
+
13
+ def provider_class
14
+ ::PayPal::SDK::Merchant::API
15
+ end
16
+
17
+ def provider
18
+ ::PayPal::SDK.configure(
19
+ :mode => preferred_server.present? ? preferred_server : "sandbox",
20
+ :username => preferred_login,
21
+ :password => preferred_password,
22
+ :signature => preferred_signature)
23
+ provider_class.new
24
+ end
25
+
26
+ def auto_capture?
27
+ true
28
+ end
29
+
30
+ def method_type
31
+ 'paypal'
32
+ end
33
+
34
+ def purchase(amount, express_checkout, gateway_options={})
35
+ pp_request = provider.build_do_express_checkout_payment({
36
+ :DoExpressCheckoutPaymentRequestDetails => {
37
+ :PaymentAction => "Sale",
38
+ :Token => express_checkout.token,
39
+ :PayerID => express_checkout.payer_id,
40
+ :PaymentDetails => [{
41
+ :OrderTotal => {
42
+ :currencyID => Spree::Config[:currency],
43
+ # gsub is here because PayPal fails to acknowledge
44
+ # that some people like their currencies written as:
45
+ # 21,99
46
+ # As opposed to:
47
+ # 21.99
48
+ # The international payments company fails to handle
49
+ # international payment amounts. SMH.
50
+ :value => ::Money.new(amount, Spree::Config[:currency]).to_s.gsub(',', '.') }
51
+ }]
52
+ }
53
+ })
54
+
55
+ pp_response = provider.do_express_checkout_payment(pp_request)
56
+ if pp_response.success?
57
+ # We need to store the transaction id for the future.
58
+ # This is mainly so we can use it later on to refund the payment if the user wishes.
59
+ transaction_id = pp_response.do_express_checkout_payment_response_details.payment_info.first.transaction_id
60
+ express_checkout.update_column(:transaction_id, transaction_id)
61
+ # This is rather hackish, required for payment/processing handle_response code.
62
+ Class.new do
63
+ def success?; true; end
64
+ def authorization; nil; end
65
+ end.new
66
+ else
67
+ class << pp_response
68
+ def to_s
69
+ errors.map(&:long_message).join(" ")
70
+ end
71
+ end
72
+ pp_response
73
+ end
74
+ end
75
+
76
+ def refund(payment, amount)
77
+ refund_type = payment.amount == amount.to_f ? "Full" : "Partial"
78
+ refund_transaction = provider.build_refund_transaction({
79
+ :TransactionID => payment.source.transaction_id,
80
+ :RefundType => refund_type,
81
+ :Amount => {
82
+ :currencyID => payment.currency,
83
+ :value => amount },
84
+ :RefundSource => "any" })
85
+ refund_transaction_response = provider.refund_transaction(refund_transaction)
86
+ if refund_transaction_response.success?
87
+ payment.source.update_attributes({
88
+ :refunded_at => Time.now,
89
+ :refund_transaction_id => refund_transaction_response.RefundTransactionID,
90
+ :state => "refunded",
91
+ :refund_type => refund_type
92
+ } )
93
+ end
94
+ refund_transaction_response
95
+ end
96
+ end
97
+ end
98
+
99
+ # payment.state = 'completed'
100
+ # current_order.state = 'complete'
@@ -0,0 +1,4 @@
1
+ module Spree
2
+ class PaypalExpressCheckout < ActiveRecord::Base
3
+ end
4
+ end
@@ -0,0 +1,20 @@
1
+ <%= form_tag paypal_refund_admin_order_payment_path(@order, @payment) do %>
2
+ <div class="label-block left five columns alpha">
3
+ <div>
4
+ <fieldset data-hook="admin_variant_new_form">
5
+ <legend><%= Spree.t('refund', :scope => :paypal) %></legend>
6
+ <div class='field'>
7
+ <%= label_tag 'refund_amount', Spree.t(:refund_amount, :scope => 'paypal') %>
8
+ <small><em><%= Spree.t(:original_amount, :scope => 'paypal', :amount => @payment.display_amount) %></em></small><br>
9
+ <% symbol = ::Money.new(1, Spree::Config[:currency]).symbol %>
10
+ <% if Spree::Config[:currency_symbol_position] == "before" %>
11
+ <%= symbol %><%= text_field_tag 'refund_amount', @payment.amount %>
12
+ <% else %>
13
+ <%= text_field_tag 'refund_amount', @payment.amount %><%= symbol %>
14
+ <% end %>
15
+ </div>
16
+ <%= button Spree.t(:refund, :scope => 'paypal'), 'icon-dollar' %>
17
+ </fieldset>
18
+ </div>
19
+ </div>
20
+ <% end %>
@@ -0,0 +1,31 @@
1
+ <%= render :partial => 'spree/admin/shared/order_tabs', :locals => { :current => 'Payments' } %>
2
+
3
+ <% content_for :page_title do %>
4
+ <i class="icon-arrow-right"></i>
5
+ <%= link_to Spree.t(:payments), admin_order_payments_path(@order) %>
6
+ <i class="icon-arrow-right"></i>
7
+ <%= payment_method_name(@payment) %>
8
+ <i class="icon-arrow-right"></i>
9
+ <%= Spree.t('refund', :scope => :paypal) %>
10
+ <% end %>
11
+
12
+ <%= form_tag paypal_refund_admin_order_payment_path(@order, @payment) do %>
13
+ <div class="label-block left five columns alpha">
14
+ <div>
15
+ <fieldset data-hook="admin_variant_new_form">
16
+ <legend><%= Spree.t('refund', :scope => :paypal) %></legend>
17
+ <div class='field'>
18
+ <%= label_tag 'refund_amount', Spree.t(:refund_amount, :scope => 'paypal') %>
19
+ <small><em><%= Spree.t(:original_amount, :scope => 'paypal', :amount => @payment.display_amount) %></em></small><br>
20
+ <% symbol = ::Money.new(1, Spree::Config[:currency]).symbol %>
21
+ <% if Spree::Config[:currency_symbol_position] == "before" %>
22
+ <%= symbol %><%= text_field_tag 'refund_amount', @payment.amount %>
23
+ <% else %>
24
+ <%= text_field_tag 'refund_amount', @payment.amount %><%= symbol %>
25
+ <% end %>
26
+ </div>
27
+ <%= button Spree.t(:refund, :scope => 'paypal'), 'icon-dollar' %>
28
+ </fieldset>
29
+ </div>
30
+ </div>
31
+ <% end %>
@@ -0,0 +1,6 @@
1
+ <div id='paypal-warning' style="display:none">
2
+ <strong>You cannot charge PayPal accounts through the admin backend at this time.</strong>
3
+ </div>
4
+ <script>
5
+ SpreePaypalExpress.paymentMethodID = "<%= payment_method.id %>"
6
+ </script>
@@ -0,0 +1,35 @@
1
+ <fieldset data-hook="paypal">
2
+ <legend align="center"><%= Spree.t(:transaction, :scope => :paypal) %></legend>
3
+
4
+ <div class="row">
5
+ <div class="alpha six columns">
6
+ <dl>
7
+ <dt><%= Spree.t(:payer_id, :scope => :paypal) %>:</dt>
8
+ <dd><%= payment.source.payer_id %></dd>
9
+
10
+ <dt><%= Spree.t(:token, :scope => :paypal) %>:</dt>
11
+ <dd><%= payment.source.token %></dd>
12
+
13
+ <dt><%= Spree.t(:transaction_id) %>:</dt>
14
+ <dd><%= payment.source.transaction_id %></dd>
15
+ </dl>
16
+ </div>
17
+
18
+ <% if payment.source.state != 'refunded' %>
19
+ <%= button_link_to Spree.t('actions.refund', :scope => :paypal), spree.paypal_refund_admin_order_payment_path(@order, payment), :icon => 'icon-dollar' %>
20
+ <% else %>
21
+ <div class="alpha six columns">
22
+ <dl>
23
+ <dt><%= Spree.t(:state, :scope => :paypal) %>:</dt>
24
+ <dd><%= payment.source.state.titleize %></dd>
25
+
26
+ <dt><%= Spree.t(:refunded_at, :scope => :paypal) %>:</dt>
27
+ <dd><%= pretty_time(payment.source.refunded_at) %></dd>
28
+
29
+ <dt><%= Spree.t(:refund_transaction_id, :scope => :paypal) %>:</dt>
30
+ <dd><%= payment.source.refund_transaction_id %></dd>
31
+ </dl>
32
+ </div>
33
+ <% end %>
34
+ </div>
35
+ </fieldset>
@@ -0,0 +1,6 @@
1
+
2
+ <%= link_to(image_tag("https://www.paypal.com/en_US/i/btn/btn_xpressCheckout.gif"), paypal_express_url(:payment_method_id => payment_method.id), :method => :post, :id => "paypal_button") %>
3
+
4
+ <script>
5
+ SpreePaypalExpress.paymentMethodID = "<%= payment_method.id %>"
6
+ </script>
@@ -0,0 +1,16 @@
1
+ ---
2
+ en:
3
+ spree:
4
+ paypal:
5
+ already_refunded: "This payment has been refunded and no further action can be taken on it."
6
+ transaction: "PayPal Transaction"
7
+ payer_id: "Payer ID"
8
+ transaction_id: "Transaction ID"
9
+ token: "Token"
10
+ refund: "Refund"
11
+ refund_amount: "Amount"
12
+ original_amount: "Original amount: %{amount}"
13
+ refund_successful: "PayPal refund successful"
14
+ refund_unsuccessful: "PayPal refund unsuccessful"
15
+ actions:
16
+ refund: "Refund"
data/config/routes.rb ADDED
@@ -0,0 +1,18 @@
1
+ Spree::Core::Engine.routes.draw do
2
+ post '/paypal', :to => "paypal#express", :as => :paypal_express
3
+ get '/paypal/confirm', :to => "paypal#confirm", :as => :confirm_paypal
4
+ get '/paypal/cancel', :to => "paypal#cancel", :as => :cancel_paypal
5
+ get '/paypal/notify', :to => "paypal#notify", :as => :notify_paypal
6
+
7
+ namespace :admin do
8
+ # Using :only here so it doesn't redraw those routes
9
+ resources :orders, :only => [] do
10
+ resources :payments, :only => [] do
11
+ member do
12
+ get 'paypal_refund'
13
+ post 'paypal_refund'
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,8 @@
1
+ class CreateSpreePaypalExpressCheckouts < ActiveRecord::Migration
2
+ def change
3
+ create_table :spree_paypal_express_checkouts do |t|
4
+ t.string :token
5
+ t.string :payer_id
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,6 @@
1
+ class AddTransactionIdToSpreePaypalExpressCheckouts < ActiveRecord::Migration
2
+ def change
3
+ add_column :spree_paypal_express_checkouts, :transaction_id, :string
4
+ add_index :spree_paypal_express_checkouts, :transaction_id
5
+ end
6
+ end
@@ -0,0 +1,5 @@
1
+ class AddStateToSpreePaypalExpressCheckouts < ActiveRecord::Migration
2
+ def change
3
+ add_column :spree_paypal_express_checkouts, :state, :string, :default => "complete"
4
+ end
5
+ end
@@ -0,0 +1,8 @@
1
+ class AddRefundedFieldsToSpreePaypalExpressCheckouts < ActiveRecord::Migration
2
+ def change
3
+ add_column :spree_paypal_express_checkouts, :refund_transaction_id, :string
4
+ add_column :spree_paypal_express_checkouts, :refunded_at, :datetime
5
+ add_column :spree_paypal_express_checkouts, :refund_type, :string
6
+ add_column :spree_paypal_express_checkouts, :created_at, :datetime
7
+ end
8
+ end
@@ -0,0 +1,31 @@
1
+ module SpreePaypalExpress
2
+ module Generators
3
+ class InstallGenerator < Rails::Generators::Base
4
+
5
+ class_option :auto_run_migrations, :type => :boolean, :default => false
6
+
7
+ def add_javascripts
8
+ append_file 'app/assets/javascripts/store/all.js', "//= require store/spree_paypal_express\n"
9
+ append_file 'app/assets/javascripts/admin/all.js', "//= require admin/spree_paypal_express\n"
10
+ end
11
+
12
+ def add_stylesheets
13
+ inject_into_file 'app/assets/stylesheets/store/all.css', " *= require store/spree_paypal_express\n", :before => /\*\//, :verbose => true
14
+ inject_into_file 'app/assets/stylesheets/admin/all.css', " *= require admin/spree_paypal_express\n", :before => /\*\//, :verbose => true
15
+ end
16
+
17
+ def add_migrations
18
+ run 'bundle exec rake railties:install:migrations FROM=spree_paypal_express'
19
+ end
20
+
21
+ def run_migrations
22
+ run_migrations = options[:auto_run_migrations] || ['', 'y', 'Y'].include?(ask 'Would you like to run the migrations now? [Y/n]')
23
+ if run_migrations
24
+ run 'bundle exec rake db:migrate'
25
+ else
26
+ puts 'Skipping rake db:migrate, don\'t forget to run it!'
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,3 @@
1
+ require 'spree_core'
2
+ require 'spree_paypal_express/version'
3
+ require 'spree_paypal_express/engine'
@@ -0,0 +1,26 @@
1
+ module SpreePaypalExpress
2
+ class Engine < Rails::Engine
3
+ require 'spree/core'
4
+ isolate_namespace Spree
5
+ engine_name 'spree_paypal_express'
6
+
7
+ config.autoload_paths += %W(#{config.root}/lib)
8
+
9
+ # use rspec for tests
10
+ config.generators do |g|
11
+ g.test_framework :rspec
12
+ end
13
+
14
+ def self.activate
15
+ Dir.glob(File.join(File.dirname(__FILE__), '../../app/**/*_decorator*.rb')) do |c|
16
+ Rails.configuration.cache_classes ? require(c) : load(c)
17
+ end
18
+ end
19
+
20
+ config.to_prepare &method(:activate).to_proc
21
+
22
+ initializer "spree.paypal_express.payment_methods", :after => "spree.register.payment_methods" do |app|
23
+ app.config.spree.payment_methods << Spree::Gateway::PayPalExpress
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,6 @@
1
+ FactoryGirl.define do
2
+ # Define your Spree extensions Factories within this file to enable applications, and other extensions to use and override them.
3
+ #
4
+ # Example adding this to your spec_helper will load these Factories for use:
5
+ # require 'spree_paypal_express/factories'
6
+ end
@@ -0,0 +1,3 @@
1
+ module SpreePayPalExpress
2
+ VERSION = '2.0.4'
3
+ end
data/script/rails ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ ENGINE_ROOT = File.expand_path('../..', __FILE__)
4
+ ENGINE_PATH = File.expand_path('../../lib/spree_paypal_express/engine', __FILE__)
5
+
6
+ require 'rails/all'
7
+ require 'rails/engine/commands'
@@ -0,0 +1,244 @@
1
+ require 'spec_helper'
2
+
3
+ describe "PayPal", :js => true do
4
+ let!(:product) { FactoryGirl.create(:product, :name => 'iPad') }
5
+ before do
6
+ @gateway = Spree::Gateway::PayPalExpress.create!({
7
+ :preferred_login => "paypal_api1.ryanbigg.com",
8
+ :preferred_password => "1373587879",
9
+ :preferred_signature => "ACOYQHq-aXKftiD4jURhihawsVSsAsaMr4qH4Tz4K17mJoa3K4M0Dvop",
10
+ :name => "PayPal",
11
+ :active => true,
12
+ :environment => Rails.env
13
+ })
14
+ FactoryGirl.create(:shipping_method)
15
+ end
16
+ def fill_in_billing
17
+ within("#billing") do
18
+ fill_in "First Name", :with => "Test"
19
+ fill_in "Last Name", :with => "User"
20
+ fill_in "Street Address", :with => "1 User Lane"
21
+ # City, State and ZIP must all match for PayPal to be happy
22
+ fill_in "City", :with => "Adamsville"
23
+ select "United States of America", :from => "order_bill_address_attributes_country_id"
24
+ select "Alabama", :from => "order_bill_address_attributes_state_id"
25
+ fill_in "Zip", :with => "35005"
26
+ fill_in "Phone", :with => "555-AME-RICA"
27
+ end
28
+ end
29
+
30
+ def switch_to_paypal_login
31
+ # If you go through a payment once in the sandbox, it remembers your preferred setting.
32
+ # It defaults to the *wrong* setting for the first time, so we need to have this method.
33
+ unless page.has_selector?("#login_email")
34
+ find("#loadLogin").click
35
+ end
36
+ end
37
+ it "pays for an order successfully" do
38
+ visit spree.root_path
39
+ click_link 'iPad'
40
+ click_button 'Add To Cart'
41
+ click_button 'Checkout'
42
+ within("#guest_checkout") do
43
+ fill_in "Email", :with => "test@example.com"
44
+ click_button 'Continue'
45
+ end
46
+ fill_in_billing
47
+ click_button "Save and Continue"
48
+ # Delivery step doesn't require any action
49
+ click_button "Save and Continue"
50
+ find("#paypal_button").click
51
+ switch_to_paypal_login
52
+ fill_in "login_email", :with => "pp@spreecommerce.com"
53
+ fill_in "login_password", :with => "thequickbrownfox"
54
+ click_button "Log In"
55
+ find("#continue_abovefold").click # Because there's TWO continue buttons.
56
+ page.should have_content("Your order has been processed successfully")
57
+
58
+ Spree::Payment.last.source.transaction_id.should_not be_blank
59
+ end
60
+
61
+ it "includes adjustments in PayPal summary" do
62
+ visit spree.root_path
63
+ click_link 'iPad'
64
+ click_button 'Add To Cart'
65
+ # TODO: Is there a better way to find this current order?
66
+ order = Spree::Order.last
67
+ order.adjustments.create!(:amount => -5, :label => "$5 off")
68
+ order.adjustments.create!(:amount => 10, :label => "$10 on")
69
+ visit '/cart'
70
+ within("#cart_adjustments") do
71
+ page.should have_content("$5 off")
72
+ page.should have_content("$10 on")
73
+ end
74
+ click_button 'Checkout'
75
+ within("#guest_checkout") do
76
+ fill_in "Email", :with => "test@example.com"
77
+ click_button 'Continue'
78
+ end
79
+ fill_in_billing
80
+ click_button "Save and Continue"
81
+ # Delivery step doesn't require any action
82
+ click_button "Save and Continue"
83
+ find("#paypal_button").click
84
+ within("#miniCart") do
85
+ page.should have_content("$5 off")
86
+ page.should have_content("$10 on")
87
+ end
88
+ end
89
+
90
+ # Regression test for #10
91
+ context "will skip $0 items" do
92
+ let!(:product2) { FactoryGirl.create(:product, :name => 'iPod') }
93
+
94
+ specify do
95
+ visit spree.root_path
96
+ click_link 'iPad'
97
+ click_button 'Add To Cart'
98
+
99
+ visit spree.root_path
100
+ click_link 'iPod'
101
+ click_button 'Add To Cart'
102
+
103
+ # TODO: Is there a better way to find this current order?
104
+ order = Spree::Order.last
105
+ order.line_items.last.update_attribute(:price, 0)
106
+ click_button 'Checkout'
107
+ within("#guest_checkout") do
108
+ fill_in "Email", :with => "test@example.com"
109
+ click_button 'Continue'
110
+ end
111
+ fill_in_billing
112
+ click_button "Save and Continue"
113
+ # Delivery step doesn't require any action
114
+ click_button "Save and Continue"
115
+ find("#paypal_button").click
116
+ within("#miniCart") do
117
+ page.should have_content('iPad')
118
+ page.should_not have_content('iPod')
119
+ end
120
+ end
121
+ end
122
+
123
+ context "can process an order with $0 item total" do
124
+ before do
125
+ # If we didn't do this then the order would be free and skip payment altogether
126
+ calculator = Spree::ShippingMethod.first.calculator
127
+ calculator.preferred_amount = 10
128
+ calculator.save
129
+ end
130
+
131
+ specify do
132
+ visit spree.root_path
133
+ click_link 'iPad'
134
+ click_button 'Add To Cart'
135
+ # TODO: Is there a better way to find this current order?
136
+ order = Spree::Order.last
137
+ order.adjustments.create!(:amount => -order.line_items.last.price, :label => "FREE iPad ZOMG!")
138
+ click_button 'Checkout'
139
+ within("#guest_checkout") do
140
+ fill_in "Email", :with => "test@example.com"
141
+ click_button 'Continue'
142
+ end
143
+ fill_in_billing
144
+ click_button "Save and Continue"
145
+ # Delivery step doesn't require any action
146
+ click_button "Save and Continue"
147
+ find("#paypal_button").click
148
+ within("#miniCart") do
149
+ page.should have_content('Current purchase')
150
+ end
151
+ end
152
+ end
153
+
154
+ context "cannot process a payment with invalid gateway details" do
155
+ before do
156
+ @gateway.preferred_login = nil
157
+ @gateway.save
158
+ end
159
+
160
+ specify do
161
+ visit spree.root_path
162
+ click_link 'iPad'
163
+ click_button 'Add To Cart'
164
+ click_button 'Checkout'
165
+ within("#guest_checkout") do
166
+ fill_in "Email", :with => "test@example.com"
167
+ click_button 'Continue'
168
+ end
169
+ fill_in_billing
170
+ click_button "Save and Continue"
171
+ # Delivery step doesn't require any action
172
+ click_button "Save and Continue"
173
+ find("#paypal_button").click
174
+ page.should have_content("PayPal failed. Security header is not valid")
175
+ end
176
+ end
177
+
178
+ context "as an admin" do
179
+ stub_authorization!
180
+
181
+ context "refunding payments" do
182
+ before do
183
+ visit spree.root_path
184
+ click_link 'iPad'
185
+ click_button 'Add To Cart'
186
+ click_button 'Checkout'
187
+ within("#guest_checkout") do
188
+ fill_in "Email", :with => "test@example.com"
189
+ click_button 'Continue'
190
+ end
191
+ fill_in_billing
192
+ click_button "Save and Continue"
193
+ # Delivery step doesn't require any action
194
+ click_button "Save and Continue"
195
+ find("#paypal_button").click
196
+ switch_to_paypal_login
197
+ fill_in "login_email", :with => "pp@spreecommerce.com"
198
+ fill_in "login_password", :with => "thequickbrownfox"
199
+ click_button "Log In"
200
+ find("#continue_abovefold").click # Because there's TWO continue buttons.
201
+ page.should have_content("Your order has been processed successfully")
202
+
203
+ visit '/admin'
204
+ click_link Spree::Order.last.number
205
+ click_link "Payments"
206
+ click_link "PayPal"
207
+ click_link "Refund"
208
+ end
209
+
210
+ it "can refund payments fully" do
211
+ click_button "Refund"
212
+ page.should have_content("PayPal refund successful")
213
+
214
+ payment = Spree::Payment.last
215
+ source = payment.source
216
+ source.refund_transaction_id.should_not be_blank
217
+ source.refunded_at.should_not be_blank
218
+ source.state.should eql("refunded")
219
+ source.refund_type.should eql("Full")
220
+ end
221
+
222
+ it "can refund payments partially" do
223
+ payment = Spree::Payment.last
224
+ # Take a dollar off, which should cause refund type to be...
225
+ fill_in "Amount", :with => payment.amount - 1
226
+ click_button "Refund"
227
+ page.should have_content("PayPal refund successful")
228
+
229
+ source = payment.source
230
+ source.refund_transaction_id.should_not be_blank
231
+ source.refunded_at.should_not be_blank
232
+ source.state.should eql("refunded")
233
+ # ... a partial refund
234
+ source.refund_type.should eql("Partial")
235
+ end
236
+
237
+ it "errors when given an invalid refund amount" do
238
+ fill_in "Amount", :with => "lol"
239
+ click_button "Refund"
240
+ page.should have_content("PayPal refund unsuccessful (The partial refund amount is not valid)")
241
+ end
242
+ end
243
+ end
244
+ end