drhenner/ror_ecommerce

View on GitHub
app/models/order_item.rb

Summary

Maintainability
A
2 hrs
Test Coverage
# == Schema Information
#
# Table name: order_items
#
#  id               :integer(4)      not null, primary key
#  price            :decimal(8, 2)
#  total            :decimal(8, 2)
#  order_id         :integer(4)      not null
#  variant_id       :integer(4)      not null
#  state            :string(255)     not null
#  tax_rate_id      :integer(4)
#  shipping_rate_id :integer(4)
#  shipment_id      :integer(4)
#  created_at       :datetime
#  updated_at       :datetime
#

class OrderItem < ApplicationRecord
  include AASM

  belongs_to :order
  belongs_to :shipping_rate
  belongs_to :variant
  belongs_to :tax_rate
  belongs_to :shipment

  has_many   :return_items

  #after_save :calculate_order
  after_find :set_beginning_values
  after_destroy :set_order_calculated_at_to_nil

  validates :variant_id,  presence: true
  validates :order_id,    presence: true

  def set_beginning_values
    @beginning_tax_rate_id      = self.tax_rate_id      rescue @beginning_tax_rate_id = nil # this stores the initial value of the tax_rate
    @beginning_shipping_rate_id = self.shipping_rate_id rescue @beginning_shipping_rate_id = nil # this stores the initial value of the tax_rate
    @beginning_total            = self.total            rescue @beginning_total = nil # this stores the initial value of the total
  end

  aasm column: :state do
    state :unpaid, initial: true
    state :paid
    state :returned

    event :pay do
      transitions to: :paid, from: :unpaid
    end

    event :return do
      transitions to: :returned, from: :paid
    end
    #after_transition :to => 'complete', :do => [:update_inventory]
  end

  def product_type
    variant.product.product_type
  end

  def product_type_ids
    product_type.self_and_ancestors.map(&:id)
  end

 # if the order item has been shipped it will return true
 #
 # @param [none]
 # @return [Boolean]
  def shipped?
    shipment_id?
  end

  def unshipped?
    !shipped?
  end

  # shipping method for the order item
  #
  # @param [none]
  # @return [ShippingMethod]
  def shipping_method
    shipping_rate.shipping_method
  end

  # shipping method id for the order item (use to reduce SQL calls)
  #   refer to ShippingMethod ID constants to determine what shipping method is called
  #
  # @param [none]
  # @return [Integer] ShippingMethod id
  def shipping_method_id
    shipping_rate.shipping_method_id
  end

  def ship_category_id
    variant.product.shipping_category_id
  end

  def shipping_rate_options(total_charge)
    ShippingRate.joins(:shipping_method).where(['shipping_rates.shipping_category_id = ?
                        AND shipping_methods.shipping_zone_id = ?
                        AND shipping_rates.minimum_charge <= ?', ship_category_id, order.ship_address.shipping_zone_id, total_charge])
  end

  # called in checkout process. will give you the 'quantity', 'sum of all the prices' and 'sum of all the totals'
  #  it is better to do the math in SQL than ruby
  #
  # @param [Integer]  order.id
  # @return [OrderItem] Object has addional attributes of [sum_price, sum_total, shipping_category_id, and quantity]
  def self.order_items_in_cart(order_id)
    joins({:variant => :product }).
    where({ :order_items => { :order_id => order_id}}).
    select( "order_items.id, order_items.order_id, order_items.shipping_rate_id, order_items.state, order_items.tax_rate_id, order_items.price, order_items.total, order_items.variant_id,
                           products.shipping_category_id,
                                          count(*) as quantity,
                                          products.shipping_category_id as shipping_category_id,
                                          SUM(order_items.price) as sum_price,
                                          SUM(order_items.total) as sum_total").
     group( "order_items.id,
                products.shipping_category_id,
                order_items.order_id,
                order_items.shipping_rate_id,
                order_items.state,
                order_items.tax_rate_id,
                order_items.price,
                order_items.total, order_items.variant_id")
  end

  # forces the order to be re-calculated.  If the order item has changed then the order totals need to be adjusted
  #
  # @param [none]
  # @return [none]
  def calculate_order
    if self.ready_to_calculate? &&
        (shipping_rate_id != @beginning_shipping_rate_id || tax_rate_id != @beginning_tax_rate_id)
      set_beginning_values
      order.calculate_totals
    end
  end

  # if something changes to the order item and you dont want to recalculate
  #   (maybe because you are chnging several more things) then
  #    this method will mark the calculated at to be nil and thus tell the order that
  #    it needs to calculate the total again
  #
  # @param [none]
  # @return [none]
  def set_order_calculated_at_to_nil
    order.update_attribute(:calculated_at, nil)
  end

  # determines if the order item has all the attributes set and thus you can now determine the final total
  #
  # @param [none]
  # @return [Boolean]
  def ready_to_calculate?
    shipping_rate_id && tax_rate_id
  end

  # this is the price after coupons and anything before calculating the price + tax
  #  in the future coupons and discounts could change this value
  #
  # @param [none]
  # @return [Float] this is the price that the tax will be applied to.

  def adjusted_price(coupon = nil)
    ## coupon credit is calculated at the order level but because taxes we need to apply it now
    coupon_credit = coupon ? coupon.value([sale_price(order.transaction_time)], order) : 0.0

    self.price - coupon_credit
  end

  def sale_price(at)
    sale = sale_at(at)
    sale ? ( (1.0 - sale.percent_off) * self.price ).round_at(2) : self.price
  end

  def sale_at(at = Time.zone.now)
    Sale.for(variant.product_id, at)
  end

  # this is the price after coupons and taxes
  #   * this return total if has not been calculated, otherwise calculates the total.
  #
  # @param [none]
  # @return [Float] this is the total of the item after taxes and coupons...
  def item_total(coupon = nil)
    # shipping charges are calculated in order.rb

    self.total ||= calculate_total(coupon)
  end

  # this is the price after coupons, deals and sales
  #   * this method does not save it just sets the value of total.
  #   * Thus allowing you to save the whole order with one opperation
  #
  # @param [none]
  # @return [Float] this is the total of the item after coupons/deals/sales...
  def calculate_total(coupon = nil)
    # shipping charges are calculated in order.rb

    self.total = (adjusted_price(coupon)).round_at(2)
  end

  # the tax charge on an item
  #
  # @param [none]
  # @return [Float] tax charge on the item.
  def tax_charge
    tax_percentage = tax_rate.try(:tax_percentage) ? tax_rate.tax_percentage : 0.0
    adjusted_price * tax_percentage / 100.0
  end

  # the VAT charge on an item
  #
  # @param [none]
  # @return [Float] tax charge on the item.
  def amount_of_charge_is_vat
    vat_percentage = tax_rate.try(:vat_percentage) ? tax_rate.vat_percentage : 0.0
    (adjusted_price * (vat_percentage / (100.0 + vat_percentage))).round_at(2)
  end

  # the amount of an item if there were zero VAT
  #
  # @param [none]
  # @return [Float] tax charge on the item.
  def amount_of_charge_without_vat
    vat_percentage = tax_rate.try(:vat_percentage) ? tax_rate.vat_percentage : 0.0
    (100.0 * adjusted_price / (100.0 + vat_percentage)).round_at(2)
  end
end