Permalink
Browse files

Refactored checkout into its own model and controller to simplify cus…

…tomization and make things more restful.

[#477 state:resolved] [#496 state:resolved]
  • Loading branch information...
schof committed Jun 8, 2009
1 parent 11b41c5 commit ce1aad7bc25c15a794f8f5689efcdbf8c3311b7b
Showing with 655 additions and 248 deletions.
  1. +4 −8 app/controllers/admin/creditcard_payments_controller.rb
  2. +5 −4 app/controllers/admin/orders_controller.rb
  3. +78 −0 app/controllers/checkouts_controller.rb
  4. +1 −22 app/controllers/orders_controller.rb
  5. +1 −1 app/controllers/users_controller.rb
  6. +7 −0 app/helpers/checkouts_helper.rb
  7. +23 −0 app/models/checkout.rb
  8. +1 −1 app/models/creditcard.rb
  9. +12 −4 app/models/creditcard_payment.rb
  10. +28 −22 app/models/order.rb
  11. +13 −16 app/views/admin/orders/index.html.erb
  12. +2 −2 app/views/{orders → checkouts}/_billing.html.erb
  13. 0 app/views/{orders → checkouts}/_confirmation.html.erb
  14. +19 −0 app/views/checkouts/_form.html.erb
  15. +5 −5 app/views/{orders → checkouts}/_payment.html.erb
  16. +2 −3 app/views/{orders → checkouts}/_registration.html.erb
  17. +2 −2 app/views/{orders → checkouts}/_shipping.html.erb
  18. 0 app/views/{orders → checkouts}/_shipping_method.html.erb
  19. +10 −0 app/views/checkouts/edit.html.erb
  20. +10 −0 app/views/checkouts/new.html.erb
  21. +17 −0 app/views/layouts/checkouts.html.erb
  22. +0 −14 app/views/orders/_checkout_form.html.erb
  23. +1 −1 app/views/orders/edit.html.erb
  24. +1 −1 config/environments/test.rb
  25. +1 −1 config/routes.rb
  26. +60 −0 db/migrate/20090603153927_create_checkouts.rb
  27. +8 −0 db/migrate/20090617181106_change_txn_type_to_int.rb
  28. +9 −0 db/sample/checkouts.yml
  29. +0 −1 db/sample/creditcards.yml
  30. +0 −5 db/sample/orders.yml
  31. +0 −87 lib/spree/checkout.rb
  32. +6 −6 public/javascripts/checkout.js
  33. +54 −0 public/stylesheets/scaffold.css
  34. +43 −5 test/factories.rb
  35. +7 −0 test/fixtures/checkouts.yml
  36. +55 −0 test/functional/checkouts_controller_test.rb
  37. +0 −26 test/functional/orders_controller_test.rb
  38. +17 −0 test/test_helper.rb
  39. +39 −0 test/unit/checkout_test.rb
  40. +28 −0 test/unit/creditcard_payment_test.rb
  41. +24 −0 test/unit/creditcard_test.rb
  42. +26 −0 test/unit/helpers/checkouts_helper_test.rb
  43. +20 −0 test/unit/order_test.rb
  44. +16 −11 vendor/extensions/payment_gateway/lib/spree/payment_gateway.rb
@@ -14,14 +14,10 @@ def country_changed
def capture
if @creditcard_payment.can_capture?
creditcard = @creditcard_payment.creditcard
authorization = @creditcard_payment.find_authorization
Creditcard.transaction do
creditcard.order.state_events.create(:name => t('pay'), :user => current_user, :previous_state => creditcard.order.state)
creditcard.capture(authorization)
@creditcard_payment.amount = authorization.amount
@creditcard_payment.save
end
#Creditcard.transaction do
# @order.state_events.create(:name => t('pay'), :user => current_user, :previous_state => order.state)
@creditcard_payment.capture#(authorization)
#end
flash[:notice] = t("credit_card_capture_complete")
else
flash[:error] = t("unable_to_capture_credit_card")
@@ -33,17 +33,18 @@ def resend
def collection
@search = Order.new_search(params[:search])
if params[:search].nil? || params[:search][:conditions].nil?
@search.conditions.checkout_complete = true
if params[:search].nil? || params[:limit_complete]
@search.conditions.checkout.completed_at_does_not_equal = nil
@limit_complete = true
end
#set order by to default or form result
@search.order_by ||= :created_at
@search.order_as ||= "DESC"
#set results per page to default or form result
@search.per_page = Spree::Config[:orders_per_page]
@collection = @search.find(:all, :include => [:user, :shipments, {:creditcard_payments => {:creditcard => :address}}] )
@collection = @search.find(:all, :include => [:user, :shipments, {:creditcard_payments => {:creditcard => :address}}])
end
# Allows extensions to add new forms of payment to provide their own display of transactions
@@ -0,0 +1,78 @@
class CheckoutsController < Spree::BaseController
include ActionView::Helpers::NumberHelper # Needed for JS usable rate information
before_filter :load_data
resource_controller :singleton
belongs_to :order
layout 'application'
# alias original r_c method so we can handle special gateway exception that might be thrown
alias :rc_update :update
def update
begin
rc_update
rescue Spree::GatewayError => ge
flash[:error] = t("unable_to_authorize_credit_card") + ": #{ge.message}"
redirect_to edit_object_url and return
end
end
update do
flash nil
success.wants.html do
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) and next if params[:final_answer]
end
success.wants.js do
render :json => { :order_total => number_to_currency(@order.total),
:ship_amount => number_to_currency(@order.ship_amount),
:tax_amount => number_to_currency(@order.tax_amount),
:available_methods => rate_hash}.to_json,
:layout => false
end
end
update.before do
if params[:checkout]
# prevent double creation of addresses if user is jumping back to address stup without refreshing page
params[:checkout][:bill_address_attributes][:id] = @checkout.bill_address.id if @checkout.bill_address
params[:checkout][:ship_address_attributes][:id] = @checkout.ship_address.id if @checkout.ship_address
end
@checkout.ip_address ||= request.env['REMOTE_ADDR']
@checkout.email ||= current_user.email if current_user
@order.update_attribute(:user, current_user) if current_user and @order.user.blank?
end
private
def object
return @object if @object
default_country = Country.find Spree::Config[:default_country_id]
@object = parent_object.checkout
@object.ship_address ||= Address.new(:country => default_country)
@object.creditcard ||= Creditcard.new(:month => Date.today.month, :year => Date.today.year)
@object.bill_address ||= Address.new(:country => default_country)
@object
end
def load_data
@countries = Country.find(:all).sort
@shipping_countries = parent_object.shipping_countries.sort
default_country = Country.find Spree::Config[:default_country_id]
@states = default_country.states.sort
end
def rate_hash
fake_shipment = Shipment.new :order => @order, :address => @order.ship_address
@order.shipping_methods.collect do |ship_method|
{ :id => ship_method.id,
:name => ship_method.name,
:rate => number_to_currency(ship_method.calculate_shipping(fake_shipment)) }
end
end
end
@@ -1,7 +1,5 @@
class OrdersController < Spree::BaseController
include ActionView::Helpers::NumberHelper # Needed for JS usable rate information
prepend_before_filter :reject_unknown_order
prepend_before_filter :reject_unknown_order
before_filter :prevent_editing_complete_order, :only => [:edit, :update, :checkout]
ssl_required :show, :checkout
@@ -50,9 +48,6 @@ def destroy
format.html { redirect_to(edit_object_url) }
end
end
# feel free to override this library in your own extension
include Spree::Checkout
def can_access?
return true unless order = load_object
@@ -82,20 +77,4 @@ def prevent_editing_complete_order
load_object
redirect_to object_url if @order.checkout_complete
end
def load_data
@default_country = Country.find Spree::Config[:default_country_id]
@countries = Country.find(:all).sort
@shipping_countries = @order.shipping_countries.sort
@states = @default_country.states.sort
end
def rate_hash
fake_shipment = Shipment.new :order => @order, :address => @order.ship_address
@order.shipping_methods.collect do |ship_method|
{ :id => ship_method.id,
:name => ship_method.name,
:rate => number_to_currency(ship_method.calculate_shipping(fake_shipment)) }
end
end
end
@@ -17,7 +17,7 @@ class UsersController < Spree::BaseController
end
show.before do
@orders = Order.checkout_completed(true).find_all_by_user_id(current_user.id)
@orders = Order.checkout_complete.find_all_by_user_id(current_user.id)
end
def update
@@ -0,0 +1,7 @@
module CheckoutsHelper
def checkout_steps
checkout_steps = %w{registration billing shipping shipping_method payment confirmation}
checkout_steps.delete "registration" if current_user
checkout_steps
end
end
View
@@ -0,0 +1,23 @@
class Checkout < ActiveRecord::Base
after_update :update_order_totals
before_save :authorize_creditcard
belongs_to :order
belongs_to :shipping_method
belongs_to :bill_address, :foreign_key => "bill_address_id", :class_name => "Address"
belongs_to :ship_address, :foreign_key => "ship_address_id", :class_name => "Address"
accepts_nested_attributes_for :ship_address, :bill_address
# for memory-only storage of creditcard details
attr_accessor :creditcard
private
def authorize_creditcard
return unless order and creditcard and not creditcard[:number].blank?
cc = Creditcard.new(creditcard.merge(:address => self.bill_address, :checkout => self))
return unless cc.valid? and cc.authorize(order.total)
order.complete
end
def update_order_totals
order.update_totals
end
end
View
@@ -1,6 +1,6 @@
class Creditcard < ActiveRecord::Base
before_save :filter_sensitive
belongs_to :order
belongs_to :checkout
belongs_to :address
has_many :creditcard_payments
before_validation :prepare
@@ -5,14 +5,22 @@ class CreditcardPayment < Payment
alias :txns :creditcard_txns
def find_authorization
def can_capture?
txns.present? and txns.last == authorization
end
def capture
return unless can_capture?
original_auth = authorization
creditcard.capture(original_auth)
update_attribute("amount", original_auth.amount)
end
def 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.to_s],
:order => 'created_at DESC')
end
def can_capture?
txns.last == find_authorization
end
end
View
@@ -1,7 +1,7 @@
class Order < ActiveRecord::Base
# before_create :generate_order_number
before_save :update_line_items
before_create :generate_token
before_create :create_checkout
has_many :line_items, :dependent => :destroy, :attributes => true
has_many :inventory_units
@@ -10,9 +10,13 @@ class Order < ActiveRecord::Base
has_many :creditcard_payments
belongs_to :user
has_many :shipments, :dependent => :destroy
belongs_to :bill_address, :foreign_key => "bill_address_id", :class_name => "Address"
belongs_to :ship_address, :foreign_key => "ship_address_id", :class_name => "Address"
accepts_nested_attributes_for :ship_address, :bill_address
has_one :checkout
has_one :bill_address, :through => :checkout
has_one :ship_address, :through => :checkout
delegate :email, :to => :checkout
delegate :ip_address, :to => :checkout
delegate :special_instructions, :to => :checkout
validates_associated :line_items, :message => "are not valid"
validates_numericality_of :tax_amount
@@ -24,25 +28,21 @@ class Order < ActiveRecord::Base
named_scope :between, lambda {|*dates| {:conditions => ["orders.created_at between :start and :stop", {:start => dates.first.to_date, :stop => dates.last.to_date}]}}
named_scope :by_customer, lambda {|customer| {:include => :user, :conditions => ["users.email = ?", customer]}}
named_scope :by_state, lambda {|state| {:conditions => ["state = ?", state]}}
named_scope :checkout_completed, lambda {|state| {:conditions => ["checkout_complete = ?", state]}}
named_scope :checkout_complete, {:include => :checkout, :conditions => ["checkouts.completed_at IS NOT NULL"]}
make_permalink :field => :number
# attr_accessible is a nightmare with attachment_fu, so use attr_protected instead.
attr_protected :ship_amount, :tax_amount, :item_total, :total, :user, :number, :ip_address, :checkout_complete, :state, :token
# for memory-only storage of creditcard details
attr_accessor :creditcard
# for storage of shipping method before it is saved in a shipment
attr_accessor :initial_shipping_method
def to_param
self.number if self.number
generate_order_number unless self.number
self.number.parameterize.to_s.upcase
end
make_permalink :field => :number
def checkout_complete
checkout.completed_at
end
# order state machine (see http://github.com/pluginaweek/state_machine/tree/master for details)
state_machine :initial => 'in_progress' do
after_transition :to => 'in_progress', :do => lambda {|order| order.update_attribute(:checkout_complete, false)}
@@ -174,24 +174,26 @@ def shipping_methods
def update_totals
# finalize order totals
if initial_shipping_method
self.ship_amount = initial_shipping_method.calculate_shipping(self)
tmp_shipment = shipment || Shipment.new(:order => self, :address => checkout.ship_address)
if checkout.shipping_method
self.ship_amount = checkout.shipping_method.calculate_shipping(tmp_shipment) if checkout.ship_address
else
self.ship_amount = 0
end
self.tax_amount = calculate_tax
save!
end
private
def complete_order
shipments.build(:address => ship_address, :shipping_method => initial_shipping_method)
self.update_attribute(:checkout_complete, true)
def complete_order
shipments.build(:address => ship_address, :shipping_method => checkout.shipping_method)
checkout.update_attribute(:completed_at, Time.now)
InventoryUnit.sell_units(self)
update_totals
save_result = save!
#update_totals
save_result = save!
if user && user.email
OrderMailer.deliver_confirm(self)
end
end
save_result
end
@@ -214,5 +216,9 @@ def update_line_items
def generate_token
self.token = Authlogic::Random.friendly_token
end
def create_checkout
self.checkout = Checkout.create unless self.checkout
end
end
@@ -57,28 +57,25 @@
<p><label><%= t("status") %></label><br />
<%= orders.select :state, Order.state_machines['state'].states.collect {|status| [status.titleize, status]}, {:include_blank => true} %></p>
<% orders.fields_for orders.object.user do |user| %>
<% orders.fields_for orders.object.checkout do |checkout| %>
<p>
<label><%= t("email") %></label><br />
<%= user.text_field :email, :size=>25 %>
<%= checkout.text_field :email, :size=>25 %>
</p>
<% checkout.fields_for checkout.object.bill_address do |address| %>
<p>
<label><%= t("first_name") %></label><br />
<%= address.text_field :lower_of_firstname_contains, :size=>25 %>
</p>
<p>
<label><%= t("last_name") %></label><br />
<%= address.text_field :lower_of_lastname_contains, :size=>25 %>
</p>
<% end %>
<% end %>
<% orders.fields_for orders.object.creditcard_payments do |cc| %>
<% cc.fields_for cc.object.creditcard.address do |address| %>
<p>
<label><%= t("first_name") %></label><br />
<%= address.text_field :lower_of_firstname_contains, :size=>25 %>
</p>
<p>
<label><%= t("last_name") %></label><br />
<%= address.text_field :lower_of_lastname_contains, :size=>25 %>
</p>
<% end %>
<% end %>
<p>
<input type="checkbox" style="vertical-align:middle;" name="limit_complete" value="1" <%= "checked" if @limit_complete %> />
<label>
<%= orders.check_box :checkout_complete, {:style => "vertical-align:middle;"}, "1", "" %>
<%= t("show_only_complete_orders") %>
</label>
</p>
@@ -1,4 +1,4 @@
<% order_form.fields_for :bill_address do |bill_form| %>
<% checkout_form.fields_for :bill_address do |bill_form| %>
<h2><%= t("billing_address")%></h2>
<div class="inner">
<p id="bfname">
@@ -24,7 +24,7 @@
<p>
<label for="<%= t("state") %>"><%= t("state") %></label>
<span id="bstate">
<%= collection_select("order[bill_address_attributes]",
<%= collection_select("checkout[bill_address_attributes]",
:state_id,
@states,
:id,
Oops, something went wrong.

0 comments on commit ce1aad7

Please sign in to comment.