Created
December 2, 2011 16:28
-
-
Save lucatironi/1423846 to your computer and use it in GitHub Desktop.
Order Placement Wizard for a services e-commerce
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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