Skip to content

Instantly share code, notes, and snippets.

@lucatironi
Created December 2, 2011 16:28
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save lucatironi/1423846 to your computer and use it in GitHub Desktop.
Save lucatironi/1423846 to your computer and use it in GitHub Desktop.
Order Placement Wizard for a services e-commerce
class Order < ActiveRecord::Base
extend FriendlyId
belongs_to :customer
belongs_to :service
belongs_to :request_address, :foreign_key => "request_address_id", :class_name => "Address"
belongs_to :billing_address, :foreign_key => "billing_address_id", :class_name => "Address"
has_many :options, :through => :choices, :autosave => true
has_many :choices, :dependent => :destroy
has_many :payment_notifications, :dependent => :destroy
attr_accessible :request_address_attributes, :billing_address_attributes, :request_address, :billing_address, :email, :option_ids, :notes, :use_request_address, :payment_method_id, :customer_first_name, :customer_last_name, :customer_phone, :customer_company_name, :customer_fiscal_code, :customer_piva
accepts_nested_attributes_for :request_address
accepts_nested_attributes_for :billing_address
friendly_id :id, :use => :slugged, :slug_column => :number
attr_accessor :customer_first_name, :customer_last_name, :customer_phone, :customer_company_name, :customer_fiscal_code, :customer_piva
attr_accessor :use_request_address
attr_writer :current_step
validates_presence_of :customer_first_name, :customer_last_name, :customer_phone, :if => lambda { |o| o.current_step == "customer_details" }
validates :email,
:presence => true,
:email_format => true,
:if => lambda { |o| o.current_step == "customer_details" }
validates :payment_method_id,
:presence => true,
:if => lambda { |o| o.current_step == "billing" }
before_validation :clone_request_address, :if => "@use_request_address"
before_create :find_or_create_customer, :freeze_service, :freeze_addresses, :calculate_total
after_create :associate_addresses_to_customer
scope :recent, lambda { |time| where(["created_at >= ?", Time.now - time]).order('created_at DESC') }
scope :pending_payment, Order.where(:status => "pending_payment")
scope :paid, Order.where(:status => "paid")
scope :completed, Order.where(:status => "completed")
scope :canceled, Order.where(:status => "canceled")
NUMBER_SEED = 100100100100
CHARACTERS_SEED = 26
PAYPAL_WPP = "PayPal"
CREDIT_TRANSFER = "Bonifico Bancario"
PAYMENT_METHOD_TYPES = [PAYPAL_WPP, CREDIT_TRANSFER]
state_machine :status, :initial => :in_progress do
before_transition :in_progress => :pending_payment do |order, transition|
PostOffice.order_invoiced_email(order).deliver
end
before_transition :pending_payment => :paid do |order, transition|
order.paid_at = Time.zone.now
PostOffice.order_paid_email(order).deliver
end
before_transition :paid => :completed do |order, transition|
order.completed_at = Time.zone.now
PostOffice.order_completed_email(order).deliver
end
event :invoice do
transition :in_progress => :pending_payment
end
event :pay do
transition :pending_payment => :paid
end
event :complete do
transition :paid => :completed
end
event :cancel do
transition [:pending_payment, :paid] => :canceled
end
end
# For friendly_id:
# Return a randomized dummy order number while order hasn't been saved
# Return the generator when it's saved
def normalize_friendly_id(string)
return (NUMBER_SEED + Time.now.to_i).to_s(CHARACTERS_SEED).upcase if new_record?
(NUMBER_SEED + string.to_i).to_s(CHARACTERS_SEED).upcase
end
# Return the chosen payment method's name
#
# @param [none]
# @return [String] the payment method's name
def payment_method
payment_method_id.nil? ? 'No payment option chosen' : PAYMENT_METHOD_TYPES[self.payment_method_id]
end
# Return a formatted title of the order
#
# @param [none]
# @return [String] title based on the order number
def title
self.class.human_name + ": " + self.number
end
# Returns the calculated subtotal of the order
#
# @param [none]
# @return [Decimal] calculated subtotal of the order
def subtotal
service.price + options.to_a.sum(&:price)
end
# Returns the calculated vat of the order
#
# @param [none]
# @return [Decimal] calculated vat of the order
def vat
subtotal * APP_CONFIG[:vat]
end
# Returns the calculated total plus vat of the order
#
# @param [none]
# @return [String] calculated total plus vat of the order
def total_with_vat
subtotal + vat
end
# Clone the request address if the customer wants to use it for the billing address
#
# @param [none]
# @return [Boolean] true
def clone_request_address
if request_address and self.billing_address.nil?
self.billing_address = request_address.clone
else
self.billing_address.attributes = request_address.address_attributes
end
true
end
# Order's already choosen suboption for a given option
#
# @param [Object] given option
# @return [Integer] id of the suboption of given option if already choosen
def choosen_suboption_for(option)
option.suboptions.each do |suboption|
return suboption.id if self.options.include?(suboption)
end
return nil
end
# Order's Steps
#
# @param [none]
# @return [Array] Steps names for order
def steps
%w[order_details customer_details billing]
end
# Switch to the next step
#
# @param [none]
# @return [none] current_step is switched to the next step in the steps array
def next_step
self.current_step = steps[steps.index(current_step)+1]
end
# Switch to the previous step
#
# @param [none]
# @return [none] current_step is switched to the previous step in the steps array
def previous_step
self.current_step = steps[steps.index(current_step)-1]
end
# Checks if current_step is the first step in the steps array
#
# @param [none]
# @return [Boolean] true if current_step is the first step in steps array
def first_step?
current_step == steps.first
end
# Checks if current_step is the last step in the steps array
#
# @param [none]
# @return [Boolean] true if current_step is the last step in steps array
def last_step?
current_step == steps.last
end
# Returns the current step
#
# @param [none]
# @return [String] current_step or steps.first if current_step isn't set
def current_step
@current_step || steps.first
end
# Validates all the steps
#
# @param [none]
# @return [Boolean] true if all the attributes are valid
def all_valid?
steps.all? do |step|
self.current_step = step
valid?
end
end
# Determines if email is required
#
# @param [none]
# @return [Boolean] true if the current step is the first one (order_details)
def require_email
self.current_step != steps.first
end
private
# Called before_create: saves the customer's attributes
def find_or_create_customer
existing_customer = Customer.find_by_email(self.email)
if existing_customer.nil?
self.customer = Customer.create(:email => self.email, :first_name => self.customer_first_name, :last_name => self.customer_last_name, :phone => self.customer_phone, :company_name => self.customer_company_name, :fiscal_code => self.customer_fiscal_code, :piva => self.customer_piva)
return
end
self.customer = existing_customer
end
# Called before_create: saves the service's attributes
def freeze_service
self.service_name = self.service.name
self.service_price = self.service.price
end
# Called before_create: saves the addresses' attributes
def freeze_addresses
self.saved_request_address = self.request_address.to_s
self.saved_billing_address = self.billing_address.to_s
end
# Called before_create: sets the order total
def calculate_total
self.total = self.subtotal + self.vat
end
# Called after_create: associate the order's addresses to the order's customer
def associate_addresses_to_customer
request_address.update_attribute :customer_id, self.customer_id
billing_address.update_attribute :customer_id, self.customer_id
end
end
# coding: utf-8
class OrdersController < ApplicationController
before_filter :force_ssl
before_filter :find_service, :only => [:new, :create, :pay]
def show
if session[:order_number] == params[:id]
@order = Order.find(params[:id])
else
redirect_to root_path, :notice => "Your session is expired or invalid."
end
end
def new
# Initialize the session with current time (expires in 20 minutes)
session[:started_at] = Time.zone.now
# Checks if the service is already set and/or I switched it with another one
# Then creates the order from scratch or with the session's params
if session[:order_params].present? and session[:order_params][:service_id] != @service.id
session[:order_params] = {}
session[:order_params][:service_id] = @service.id
@order = @service.orders.new
@order.current_step = @order.steps.first
else
session[:order_params] ||= {}
session[:order_params][:service_id] = @service.id
@order = @service.orders.new(session[:order_params])
@order.current_step = session[:order_step]
end
# Initialized the default address (for accepts_nested_attributes_for)
@order.request_address ||= Address.default
end
def create
if expired? or invalid?
session[:order_step] = session[:order_params] = session[:started_at] = nil
redirect_to category_service_buy_path(@category, @service), :notice => "Your session for this order is expired or invalid."
else
# Re-initialize the order if the service has been changed or creates from session's params
if session[:order_params][:service_id] != @service.id
session[:order_params] = {}
@order = @service.orders.new(session[:order_params])
@order.current_step = session[:order_step]
else
session[:order_params].deep_merge!(params[:order]) if params[:order]
@order = @service.orders.new(session[:order_params])
@order.current_step = session[:order_step]
end
order_valid = @order.valid?
if order_valid
# Going to a previous step?
if params[:back_button]
@order.previous_step
elsif @order.last_step?
# Checks if all the attributes are valid and finally save the order to the db
if @order.all_valid?
@order.save
@order.invoice!
end
else
# Advance to the next step
@order.next_step
end
session[:order_step] = @order.current_step
end
# If order hasn't been saved yet (i.e. it's not last step or is invalid)
# It will be redirected to new action or rendered with errors
if @order.new_record?
if order_valid
redirect_to category_service_buy_url(@category, @service, :step => @order.current_step)
else
@order.request_address ||= Address.default
@order.billing_address ||= Address.default
render :new
end
else
# Order has been saved so cancel the sessions and redirect to cc payment or show action if no payment is needed (credit transfer option chosen)
session[:order_step] = session[:order_params] = session[:started_at] = nil
session[:order_id] = @order.id
session[:order_number] = @order.number
if @order.payment_method == Order::PAYPAL_WPP
redirect_to category_service_pay_path(@category, @service)
else
redirect_to @order, :notice => "Your order has been saved successfully!"
end
end
end
end
# Non-restful action for payement
def pay
if session[:order_id]
@order = Order.find(session[:order_id])
if @order.paid?
redirect_to @order, :notice => "Ordine pagato con successo!"
end
else
redirect_to root_path, :notice => "Your sessions for this order is expired or invalid"
end
end
def destroy
session[:order_step] = session[:order_params] = session[:started_at] = nil
redirect_to categories_url
end
private
# Find needed order's associated objects from url
def find_service
if params[:category_id].present?
@category = Category.find(params[:category_id])
@service = @category.services.published.find(params[:service_id])
else
@service = Service.find(params[:service_id])
@category = @service.category
end
end
def expired?
session[:started_at].nil? || (Time.zone.now - session[:started_at] > (60 * 20) ) ## 20 minutes
end
def invalid?
session[:order_params].nil?
end
# All the order actions are under SSL
def force_ssl
if !request.ssl? && Rails.env.production?
redirect_to :protocol => 'https://', :status => :moved_permanently
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment