module Effective class Order < ActiveRecord::Base self.table_name = EffectiveOrders.orders_table_name.to_s if EffectiveOrders.obfuscate_order_ids acts_as_obfuscated :format => '###-####-###' end acts_as_addressable :billing => {:singular => true, :presence => EffectiveOrders.require_billing_address, :use_full_name => EffectiveOrders.use_address_full_name}, :shipping => {:singular => true, :presence => EffectiveOrders.require_shipping_address, :use_full_name => EffectiveOrders.use_address_full_name} attr_accessor :save_billing_address, :save_shipping_address, :shipping_address_same_as_billing # save these addresses to the user if selected belongs_to :user # This is the user who purchased the order has_many :order_items, :inverse_of => :order structure do payment :text # serialized hash, see below purchase_state :string, :validates => [:inclusion => {:in => [nil, EffectiveOrders::PURCHASED, EffectiveOrders::DECLINED]}] purchased_at :datetime, :validates => [:presence => {:if => Proc.new { |order| order.purchase_state == EffectiveOrders::PURCHASED}}] timestamps end accepts_nested_attributes_for :order_items, :allow_destroy => false, :reject_if => :all_blank accepts_nested_attributes_for :user, :allow_destroy => false, :update_only => true unless EffectiveOrders.skip_user_validation validates_presence_of :user_id validates_associated :user end if ((minimum_charge = EffectiveOrders.minimum_charge.to_i) rescue nil).present? if EffectiveOrders.allow_free_orders validates_numericality_of :total, :greater_than_or_equal_to => minimum_charge, :unless => Proc.new { |order| order.total == 0 }, :message => "A minimum order of #{EffectiveOrders.minimum_charge} is required. Please add additional items to your cart." else validates_numericality_of :total, :greater_than_or_equal_to => minimum_charge, :message => "A minimum order of #{EffectiveOrders.minimum_charge} is required. Please add additional items to your cart." end end validates_presence_of :order_items, :message => 'No items are present. Please add one or more item to your cart.' validates_associated :order_items serialize :payment, Hash default_scope -> { includes(:user).includes(:order_items => :purchasable).order('created_at DESC') } scope :purchased, -> { where(:purchase_state => EffectiveOrders::PURCHASED) } scope :purchased_by, lambda { |user| purchased.where(:user_id => user.try(:id)) } scope :declined, -> { where(:purchase_state => EffectiveOrders::DECLINED) } def initialize(cart = {}, user = nil) super() # Call super with no arguments # Set up defaults self.save_billing_address = true self.save_shipping_address = true self.shipping_address_same_as_billing = true add_to_order(cart) if cart.present? self.user = user if user.present? end def add(item, quantity = 1) raise 'unable to alter a purchased order' if purchased? raise 'unable to alter a declined order' if declined? if item.kind_of?(Effective::Cart) cart_items = item.cart_items else purchasables = [item].flatten if purchasables.any? { |p| !p.respond_to?(:is_effectively_purchasable?) } raise ArgumentError.new('Effective::Order.add() expects a single acts_as_purchasable item, or an array of acts_as_purchasable items') end cart_items = purchasables.map do |purchasable| CartItem.new(:quantity => quantity).tap { |cart_item| cart_item.purchasable = purchasable } end end retval = cart_items.map do |item| order_items.build( :title => item.title, :quantity => item.quantity, :price => item.price, :tax_exempt => item.tax_exempt, :tax_rate => item.tax_rate, :seller_id => (item.purchasable.try(:seller).try(:id) rescue nil) ).tap { |order_item| order_item.purchasable = item.purchasable } end retval.size == 1 ? retval.first : retval end alias_method :add_to_order, :add def user=(user) super # Copy user addresses into this order if they are present if user.respond_to?(:billing_address) && !user.billing_address.nil? self.billing_address = user.billing_address end if user.respond_to?(:shipping_address) && !user.shipping_address.nil? self.shipping_address = user.shipping_address end # If our addresses are required, make sure they exist if EffectiveOrders.require_billing_address self.billing_address ||= Effective::Address.new() end if EffectiveOrders.require_shipping_address self.shipping_address ||= Effective::Address.new() end # Ensure the Full Name is assigned when an address exists if billing_address.nil? == false && billing_address.full_name.blank? self.billing_address.full_name = billing_name end if shipping_address.nil? == false && shipping_address.full_name.blank? self.shipping_address.full_name = billing_name end end # This is used for updating Subscription codes. # We want to update the underlying purchasable object of an OrderItem # Passing the order_item_attributes using rails default acts_as_nested creates a new object instead of updating the temporary one. # So we override this method to do the updates on the non-persisted OrderItem objects # Right now strong_paramaters only lets through stripe_coupon_id # {"0"=>{"class"=>"Effective::Subscription", "stripe_coupon_id"=>"50OFF", "id"=>"2"}}} def order_items_attributes=(order_item_attributes) if self.persisted? == false (order_item_attributes || {}).each do |_, atts| order_item = self.order_items.find { |oi| oi.purchasable.class.name == atts[:class] && oi.purchasable.id == atts[:id].to_i } if order_item order_item.purchasable.attributes = atts.except(:id, :class) # Recalculate the OrderItem based on the updated purchasable object order_item.title = order_item.purchasable.title order_item.price = order_item.purchasable.price order_item.tax_exempt = order_item.purchasable.tax_exempt order_item.tax_rate = order_item.purchasable.tax_rate order_item.seller_id = (order_item.purchasable.try(:seller).try(:id) rescue nil) end end end end def total [order_items.map(&:total).sum, 0].max end def subtotal order_items.map(&:subtotal).sum end def tax [order_items.map(&:tax).sum, 0].max end def num_items order_items.map(&:quantity).sum end def save_billing_address? ::ActiveRecord::ConnectionAdapters::Column::TRUE_VALUES.include?(self.save_billing_address) end def save_shipping_address? ::ActiveRecord::ConnectionAdapters::Column::TRUE_VALUES.include?(self.save_shipping_address) end def shipping_address_same_as_billing? if self.shipping_address_same_as_billing.nil? true # Default value else ::ActiveRecord::ConnectionAdapters::Column::TRUE_VALUES.include?(self.shipping_address_same_as_billing) end end def billing_name name ||= billing_address.try(:full_name).presence name ||= user.try(:full_name).presence name ||= (user.try(:first_name).to_s + ' ' + user.try(:last_name).to_s).presence name ||= user.try(:email).presence name ||= user.to_s name ||= "User #{user.try(:id)}" name end # :validate => false, :email => false def purchase!(payment_details = nil, opts = {}) opts = {:validate => true, :email => true}.merge(opts) raise EffectiveOrders::AlreadyPurchasedException.new('order already purchased') if self.purchased? raise EffectiveOrders::AlreadyDeclinedException.new('order already declined') if (self.declined? && opts[:validate]) Order.transaction do self.purchase_state = EffectiveOrders::PURCHASED self.purchased_at ||= Time.zone.now self.payment = payment_details.kind_of?(Hash) ? payment_details : {:details => (payment_details || 'none').to_s} self.save!(:validate => opts[:validate]) order_items.each { |item| item.purchasable.purchased!(self, item) } if EffectiveOrders.mailer[:send_order_receipt_to_admin] && opts[:email] if Rails.env.production? (OrdersMailer.order_receipt_to_admin(self).deliver rescue false) else OrdersMailer.order_receipt_to_admin(self).deliver end end if EffectiveOrders.mailer[:send_order_receipt_to_buyer] && opts[:email] if Rails.env.production? (OrdersMailer.order_receipt_to_buyer(self).deliver rescue false) else OrdersMailer.order_receipt_to_buyer(self).deliver end end if EffectiveOrders.mailer[:send_order_receipt_to_seller] && self.purchased?(:stripe_connect) && opts[:email] self.order_items.group_by(&:seller).each do |seller, order_items| if Rails.env.production? (OrdersMailer.order_receipt_to_seller(self, seller, order_items).deliver rescue false) else OrdersMailer.order_receipt_to_seller(self, seller, order_items).deliver end end end return true end false end def decline!(payment_details = nil) raise EffectiveOrders::AlreadyPurchasedException.new('order already purchased') if self.purchased? raise EffectiveOrders::AlreadyDeclinedException.new('order already declined') if self.declined? Order.transaction do self.purchase_state = EffectiveOrders::DECLINED self.payment = payment_details.kind_of?(Hash) ? payment_details : {:details => (payment_details || 'none').to_s} order_items.each { |item| item.purchasable.declined!(self, item) } self.save! end end def purchased?(provider = nil) return false if (purchase_state != EffectiveOrders::PURCHASED) return true if provider == nil begin case provider when :stripe_connect payment.keys.first.kind_of?(Numeric) && payment[payment.keys.first].key?('object') && payment[payment.keys.first]['object'] == 'charge' when :stripe payment.key?('object') && payment['object'] == 'charge' when :moneris payment.key?('response_code') && payment.key?('transactionKey') when :paypal else false end rescue => e false end end def declined? purchase_state == EffectiveOrders::DECLINED end end end