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, use_full_name: EffectiveOrders.use_address_full_name }, shipping: { singular: true, use_full_name: EffectiveOrders.use_address_full_name } ) attr_accessor :terms_and_conditions # Yes, I agree to the terms and conditions # Settings in the /admin action forms attr_accessor :send_payment_request_to_buyer # Set by Admin::Orders#new. Should the payment request email be sent after creating an order? attr_accessor :send_mark_as_paid_email_to_buyer # Set by Admin::Orders#mark_as_paid attr_accessor :skip_buyer_validations # Set by Admin::Orders#create attr_accessor :skip_minimum_charge_validation # Set by Admin::Orders#show belongs_to :user, validate: false # This is the buyer/user of the order. We validate it below. has_many :order_items, -> { order(:id) }, inverse_of: :order, class_name: 'Effective::OrderItem' accepts_nested_attributes_for :order_items, allow_destroy: false, reject_if: :all_blank accepts_nested_attributes_for :user, allow_destroy: false, update_only: true # Attributes # purchase_state :string # purchased_at :datetime # # note_to_buyer :text # note_internal :text # # payment :text # serialized hash containing all the payment details. see below. # # payment_provider :string # payment_card :string # # tax_rate :decimal, precision: 6, scale: 3 # # subtotal :integer # tax :integer # total :integer # # timestamps serialize :payment, Hash before_validation { assign_totals! } # Order validations validates :order_items, presence: { message: 'No items are present. Please add additional items.' } validates :purchase_state, inclusion: { in: EffectiveOrders::PURCHASE_STATES.keys } validates :subtotal, presence: true if EffectiveOrders.minimum_charge.to_i > 0 validates :total, presence: true, numericality: { greater_than_or_equal_to: EffectiveOrders.minimum_charge.to_i, message: "must be $#{'%0.2f' % (EffectiveOrders.minimum_charge.to_i / 100.0)} or more. Please add additional items." }, unless: -> { (total == 0 && EffectiveOrders.allow_free_orders) || (total < 0 && EffectiveOrders.allow_refunds && skip_minimum_charge_validation?) } end # User validations -- An admin skips these when working in the admin/ namespace with_options unless: -> { skip_buyer_validations? } do |order| order.validates :tax_rate, presence: { message: "can't be determined based on billing address" } order.validates :tax, presence: true unless EffectiveOrders.skip_user_validation order.validates :user_id, presence: true order.validates :user, associated: true end if EffectiveOrders.require_billing_address order.validates :billing_address, presence: true end if EffectiveOrders.require_shipping_address order.validates :shipping_address, presence: true end if EffectiveOrders.collect_note_required order.validates :note, presence: true end end # When Purchased with_options if: -> { purchased? } do |order| order.validates :purchased_at, presence: true order.validates :payment, presence: true order.validates :payment_provider, presence: true, inclusion: { in: EffectiveOrders.payment_providers + EffectiveOrders.other_payment_providers } order.validates :payment_card, presence: true end before_save { assign_totals! unless self[:total].present? } # Incase we save!(validate: false) before_save :save_addresses scope :deep, -> { includes(:user).includes(order_items: :purchasable) } scope :sorted, -> { order(created_at: :desc) } scope :purchased, -> { sorted.where(purchase_state: EffectiveOrders::PURCHASED) } scope :purchased_by, lambda { |user| purchased.where(user_id: user.try(:id)) } scope :declined, -> { where(purchase_state: EffectiveOrders::DECLINED) } scope :pending, -> { where(purchase_state: EffectiveOrders::PENDING) } scope :for_users, -> (users) { # Expects a Users relation, an Array of ids, or Array of users users = users.kind_of?(::ActiveRecord::Relation) ? users.pluck(:id) : Array(users) where(user_id: (users.first.kind_of?(Integer) ? users : users.map { |user| user.id })) } # Effective::Order.new() # Effective::Order.new(Product.first) # Effective::Order.new(current_cart) # Effective::Order.new(Effective::Order.last) # Effective::Order.new(items: Product.first) # Effective::Order.new(items: [Product.first, Product.second], user: User.first) # Effective::Order.new(items: Product.first, user: User.first, billing_address: Effective::Address.new, shipping_address: Effective::Address.new) def initialize(atts = nil, &block) super(purchase_state: EffectiveOrders::PENDING) # Initialize with state: PENDING return unless atts.present? if atts.kind_of?(Hash) items = Array(atts.delete(:item)) + Array(atts.delete(:items)) self.user = atts.delete(:user) || (items.first.user if items.first.respond_to?(:user)) if (address = atts.delete(:billing_address)).present? self.billing_address = address self.billing_address.full_name ||= user.to_s.presence end if (address = atts.delete(:shipping_address)).present? self.shipping_address = address self.shipping_address.full_name ||= user.to_s.presence end atts.each { |key, value| self.send("#{key}=", value) } add(items) if items.present? else # Attributes are not a Hash self.user = atts.user if atts.respond_to?(:user) add(atts) end end # add(Product.first) => returns an Effective::OrderItem # add(Product.first, current_cart) => returns an array of Effective::OrderItems def add(*items, quantity: 1) raise 'unable to alter a purchased order' if purchased? raise 'unable to alter a declined order' if declined? cart_items = items.flatten.flat_map do |item| if item.kind_of?(Effective::Cart) item.cart_items.to_a elsif item.kind_of?(ActsAsPurchasable) Effective::CartItem.new(quantity: quantity, purchasable: item) elsif item.kind_of?(Effective::Order) # Duplicate an existing order self.note_to_buyer ||= item.note_to_buyer self.note_internal ||= item.note_internal item.order_items.select { |oi| oi.purchasable.kind_of?(Effective::Product) }.map do |oi| product = Effective::Product.new(title: oi.purchasable.title, price: oi.purchasable.price, tax_exempt: oi.purchasable.tax_exempt) Effective::CartItem.new(quantity: oi.quantity, purchasable: product) end else raise 'add() expects one or more acts_as_purchasable objects, or an Effective::Cart' end end.compact # Make sure to reset stored aggregates self.total = nil self.subtotal = nil self.tax = nil retval = cart_items.map do |item| order_items.build( title: item.title, quantity: item.quantity, price: item.price, tax_exempt: (item.tax_exempt || false), ).tap { |order_item| order_item.purchasable = item.purchasable } end retval.size == 1 ? retval.first : retval end def user=(user) super return unless user.present? # Copy user addresses into this order if they are present if user.respond_to?(:billing_address) && user.billing_address.present? self.billing_address = user.billing_address end if user.respond_to?(:shipping_address) && user.shipping_address.present? self.shipping_address = user.shipping_address end user end # This is called from admin/orders#create # This is intended for use as an admin action only # It skips any address or bad user validations def create_as_pending! return false unless new_record? self.purchase_state = EffectiveOrders::PENDING self.skip_buyer_validations = true self.addresses.clear if addresses.any? { |address| address.valid? == false } save! send_payment_request_to_buyer! if send_payment_request_to_buyer? true end def to_s if refund? "Refund ##{to_param}" elsif purchased? "Receipt ##{to_param}" elsif pending? "Pending Order ##{to_param}" else "Order ##{to_param}" end end def free? total == 0 end def refund? total.to_i < 0 end def purchasables order_items.map { |order_item| order_item.purchasable } end def tax_rate self[:tax_rate] || get_tax_rate() end def tax self[:tax] || get_tax() end def subtotal self[:subtotal] || order_items.map { |oi| oi.subtotal }.sum end def total (self[:total] || (subtotal + tax.to_i)).to_i end def num_items order_items.map { |oi| oi.quantity }.sum end def send_payment_request_to_buyer? truthy?(send_payment_request_to_buyer) end def send_mark_as_paid_email_to_buyer? truthy?(send_mark_as_paid_email_to_buyer) end def skip_buyer_validations? truthy?(skip_buyer_validations) end def skip_minimum_charge_validation? truthy?(skip_minimum_charge_validation) || skip_buyer_validations? 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 # Effective::Order.new(Product.first, user: User.first).purchase!(details: 'manual purchase') # order.purchase!(details: {key: value}) def purchase!(details: 'none', provider: 'none', card: 'none', validate: true, email: true, skip_buyer_validations: false) return false if purchased? success = false error = nil Effective::Order.transaction do begin self.purchase_state = EffectiveOrders::PURCHASED self.purchased_at ||= Time.zone.now self.payment = ( if details.kind_of?(Hash) details elsif details.respond_to?(:to_unsafe_h) details.to_unsafe_h else { details: details.to_s } end ) self.payment_provider = provider.to_s self.payment_card = card.to_s.presence || 'none' self.skip_buyer_validations = skip_buyer_validations run_purchasable_callbacks(:before_purchase) save!(validate: validate) success = true rescue => e self.purchase_state = purchase_state_was self.purchased_at = purchased_at_was error = e.message raise ::ActiveRecord::Rollback end end raise "Failed to purchase Effective::Order: #{error || errors.full_messages.to_sentence}" unless success send_order_receipts! if email run_purchasable_callbacks(:after_purchase) true end def decline!(details: 'none', provider: 'none', card: 'none', validate: true) return false if declined? raise EffectiveOrders::AlreadyPurchasedException.new('order already purchased') if purchased? success = false error = nil Effective::Order.transaction do begin self.purchase_state = EffectiveOrders::DECLINED self.purchased_at = nil self.payment = details.kind_of?(Hash) ? details : { details: details.to_s } self.payment_provider = provider.to_s self.payment_card = card.to_s.presence || 'none' save!(validate: validate) success = true rescue => e self.purchase_state = purchase_state_was self.purchased_at = purchased_at_was error = e.message raise ::ActiveRecord::Rollback end end raise "Failed to decline! Effective::Order: #{error || errors.full_messages.to_sentence}" unless success run_purchasable_callbacks(:after_decline) true end def abandoned? purchase_state == EffectiveOrders::ABANDONED end def purchased?(provider = nil) return false if (purchase_state != EffectiveOrders::PURCHASED) return true if provider.nil? || payment_provider == provider.to_s unless EffectiveOrder.payment_providers.include?(provider.to_s) raise "Unknown provider #{provider}. Known providers are #{EffectiveOrders.payment_providers}" end end def declined? purchase_state == EffectiveOrders::DECLINED end def pending? purchase_state == EffectiveOrders::PENDING end def send_order_receipts! send_order_receipt_to_admin! if EffectiveOrders.mailer[:send_order_receipt_to_admin] send_order_receipt_to_buyer! if EffectiveOrders.mailer[:send_order_receipt_to_buyer] send_order_receipt_to_seller! if EffectiveOrders.mailer[:send_order_receipt_to_seller] end def send_order_receipt_to_admin! send_email(:order_receipt_to_admin, to_param) if purchased? end def send_order_receipt_to_buyer! send_email(:order_receipt_to_buyer, to_param) if purchased? end def send_order_receipt_to_seller! return false unless (EffectiveOrders.stripe_connect_enabled && purchased?(:stripe_connect)) order_items.group_by { |oi| oi.seller }.each do |seller, order_items| send_email(:order_receipt_to_seller, to_param, seller, order_items) end end def send_payment_request_to_buyer! send_email(:payment_request_to_buyer, to_param) unless purchased? end def send_pending_order_invoice_to_buyer! send_email(:pending_order_invoice_to_buyer, to_param) unless purchased? end protected def get_tax_rate self.instance_exec(self, &EffectiveOrders.order_tax_rate_method).tap do |rate| rate = rate.to_f if (rate > 100.0 || (rate < 0.25 && rate > 0.0000)) raise "expected EffectiveOrders.order_tax_rate_method to return a value between 100.0 (100%) and 0.25 (0.25%) or 0 or nil. Received #{rate}. Please return 5.25 for 5.25% tax." end end end def get_tax return nil unless tax_rate.present? order_items.reject { |oi| oi.tax_exempt? }.map { |oi| (oi.subtotal * (tax_rate / 100.0)).round(0).to_i }.sum end private def assign_totals! self.subtotal = order_items.map { |oi| oi.subtotal }.sum self.tax_rate = get_tax_rate() self.tax = get_tax() self.total = subtotal + (tax || 0) end def save_addresses if user.respond_to?(:billing_address=) && billing_address.present? user.billing_address = billing_address user.billing_address.save end if user.respond_to?(:shipping_address=) && shipping_address.present? user.shipping_address = shipping_address user.shipping_address.save end end def run_purchasable_callbacks(name) begin order_items.each { |oi| oi.purchasable.public_send(name, self, oi) if oi.purchasable.respond_to?(name) } rescue => e raise e unless Rails.env.production? end end def send_email(email, *mailer_args) begin if EffectiveOrders.mailer[:delayed_job_deliver] && EffectiveOrders.mailer[:deliver_method] == :deliver_later Effective::OrdersMailer.delay.public_send(email, *mailer_args) elsif EffectiveOrders.mailer[:deliver_method].present? Effective::OrdersMailer.public_send(email, *mailer_args).public_send(EffectiveOrders.mailer[:deliver_method]) else Effective::OrdersMailer.public_send(email, *mailer_args).deliver_now end rescue => e raise e unless Rails.env.production? return false end end def truthy?(value) if defined?(::ActiveRecord::ConnectionAdapters::Column::TRUE_VALUES) # Rails <5 ::ActiveRecord::ConnectionAdapters::Column::TRUE_VALUES.include?(value) else ::ActiveRecord::Type::Boolean.new.cast(value) end end end end