# When an Order is first initialized it is done in the pending status # - when it's in the pending status, none of the buyer entered information is required # - when a pending order is rendered: # - if the user has a billing address, go to step 2 # - if the user has no billing address, go to step 1 # # After Step1, we go to the confirmed status # After Step2, we are in the purchased or declined status module Effective class Order < ActiveRecord::Base self.table_name = (EffectiveOrders.orders_table_name || :orders).to_s # Effective Resources acts_as_statused( :pending, # New orders are created in a pending state :confirmed, # Once the order has passed checkout step 1 :deferred, # Deferred providers. cheque, etransfer, phone or deluxe_delayed was selected. :purchased, # Purchased by provider :declined, # Declined by provider :voided, # Voided by admin :abandoned # Not set by this gem. Can be set outside it. ) # Effective Addresses acts_as_addressable(billing: { singular: true }, shipping: { singular: true }) # Effective Logging log_changes if respond_to?(:log_changes) # Effective Obfuscation if EffectiveOrders.obfuscate_order_ids raise('unsupported obfuscation with tenant') if defined?(Tenant) acts_as_obfuscated format: '###-####-###' end # Effective Reports acts_as_reportable if respond_to?(:acts_as_reportable) attr_accessor :current_user # Set by the checkout controller for non-admin users attr_accessor :terms_and_conditions # Yes, I agree to the terms and conditions attr_accessor :confirmed_checkout # Set on the Checkout Step 1 # 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 # If we want to use orders in a has_many way belongs_to :parent, polymorphic: true, optional: true # This is user the order is for belongs_to :user, polymorphic: true, optional: true, validate: false accepts_nested_attributes_for :user, allow_destroy: false, update_only: true # When an organization is present, any user with role :billing in that organization can purchase this order belongs_to :organization, polymorphic: true, optional: true, validate: false accepts_nested_attributes_for :organization, allow_destroy: false, update_only: true # When purchased, this is the user that purchased it. belongs_to :purchased_by, polymorphic: true, optional: true, validate: false has_many :order_items, -> { order(:id) }, inverse_of: :order, dependent: :delete_all accepts_nested_attributes_for :order_items, allow_destroy: true, reject_if: :all_blank # Attributes effective_resource do # Acts as Statused status :string status_steps :text purchased_at :datetime note :text # From buyer to admin note_to_buyer :text # From admin to buyer note_internal :text # Internal admin only billing_name :string # name of buyer email :string # same as user.email cc :string # can be set by admin payment :text # serialized hash containing all the payment details. payment_provider :string payment_card :string tax_rate :decimal, precision: 6, scale: 3 surcharge_percent :decimal, precision: 6, scale: 3 subtotal :integer # Sum of items subtotal tax :integer # Tax on subtotal amount_owing :integer # Subtotal + Tax surcharge :integer # Credit Card Surcharge surcharge_tax :integer # Tax on surcharge total :integer # Subtotal + Tax + Surcharge + Surcharge Tax # For use with the Deluxe Delayed Payment feature # When an order is created. These two attributes can be set to create a delayed? order delayed_payment :boolean delayed_payment_date :date # When the order goes to checkout we require the delayed_payment_intent and total # This stores the user's card information # This is required for the order to become deferred? delayed_payment_intent :text delayed_payment_total :integer # Only for reference, not really used. This is the order total we showed them when they last save card info'd # Set by the rake task that runs 1/day and processes any delayed orders before or on that day delayed_payment_purchase_ran_at :datetime delayed_payment_purchase_result :text timestamps end if EffectiveResources.serialize_with_coder? serialize :payment, type: Hash, coder: YAML else serialize :payment, Hash end scope :deep, -> { includes(:addresses, :user, :parent, :purchased_by, :organization, order_items: :purchasable) } scope :for, -> (user) { if user.respond_to?(:organizations) where(user: user).or(where(organization: user.organizations)) else where(user: user) end } scope :sorted, -> { order(:id) } scope :purchased, -> { where(status: :purchased) } scope :purchased_or_deferred, -> { where(status: [:purchased, :deferred]) } scope :purchased_by, lambda { |user| purchased.where(user: user) } scope :not_purchased, -> { where.not(status: [:purchased, :deferred]) } scope :was_not_purchased, -> { where.not(status: :purchased) } scope :pending, -> { where(status: :pending) } scope :confirmed, -> { where(status: :confirmed) } scope :deferred, -> { where(status: :deferred) } scope :declined, -> { where(status: :declined) } scope :abandoned, -> { where(status: :abandoned) } scope :voided, -> { where(status: :voided) } scope :refunds, -> { purchased.where('total < ?', 0) } scope :pending_refunds, -> { not_purchased.where('total < ?', 0) } scope :delayed, -> { where(delayed_payment: true).where.not(delayed_payment_date: nil) } scope :delayed_payment_date_past, -> { delayed.where(arel_table[:delayed_payment_date].lteq(Time.zone.today)) } scope :delayed_payment_date_upcoming, -> { delayed.where(arel_table[:delayed_payment_date].gt(Time.zone.today)) } # Used by the rake effective_orders:purchase_delayed_orders task scope :delayed_ready_to_purchase, -> { delayed.deferred.delayed_payment_date_past.where(delayed_payment_purchase_ran_at: nil) } # effective_reports def reportable_scopes { purchased: nil, not_purchased: nil, deferred: nil, refunds: nil, pending_refunds: nil } end before_validation do assign_attributes(status: :confirmed) if pending? && confirmed_checkout end before_validation do assign_attributes(user_type: nil) if user_type.present? && user_id.blank? assign_attributes(organization_type: nil) if organization_type.present? && organization_id.blank? end with_options(unless: -> { done? }) do before_validation { assign_organization_address } before_validation { assign_user_address } before_validation { assign_billing_name } before_validation { assign_billing_email } before_validation { assign_order_item_values } before_validation { assign_order_values } before_validation { assign_order_charges } end # Order validations validates :email, presence: true, email: true, if: -> { user_id.present? } # email and cc validators are from effective_resources validates :cc, email_cc: true validates :order_items, presence: { message: 'No items are present. Please add additional items.' } # Delayed Payment Validations validates :delayed_payment_date, presence: true, if: -> { delayed_payment? } validates :delayed_payment_date, absence: true, unless: -> { delayed_payment? } with_options(if: -> { delayed? && deferred? }) do validates :delayed_payment_intent, presence: { message: 'please provide your card information' } validates :delayed_payment_total, presence: true end validate do if EffectiveOrders.organization_enabled? errors.add(:base, "must have a User or #{EffectiveOrders.organization_class_name || 'Organization'}") if user_id.blank? && organization_id.blank? else errors.add(:base, "must have a User") if user_id.blank? end end # Price validations validates :subtotal, presence: true validates :total, presence: true, if: -> { EffectiveOrders.minimum_charge.to_i > 0 } validate(if: -> { total.present? && EffectiveOrders.minimum_charge.to_i > 0 }, unless: -> { purchased? || (free? && EffectiveOrders.free?) || (refund? && EffectiveOrders.refund?) }) do if total < EffectiveOrders.minimum_charge errors.add(:total, "must be $#{'%0.2f' % (EffectiveOrders.minimum_charge.to_i / 100.0)} or more. Please add additional items.") end end validate(if: -> { tax_rate.present? }) do if (tax_rate > 100.0 || (tax_rate < 0.25 && tax_rate > 0.0000)) errors.add(:tax_rate, "is invalid. expected a value between 100.0 (100%) and 0.25 (0.25%) or 0") end end # User validations -- An admin skips these when working in the admin/ namespace with_options(unless: -> { pending? || skip_buyer_validations? || purchased? }) do validates :tax_rate, presence: { message: "can't be determined based on billing address" } validates :tax, presence: true validates :billing_address, presence: true, if: -> { EffectiveOrders.billing_address } validates :shipping_address, presence: true, if: -> { EffectiveOrders.shipping_address } validates :note, presence: true, if: -> { EffectiveOrders.collect_note_required } end # When Purchased with_options(if: -> { purchased? }) do validates :purchased_at, presence: true validates :payment, presence: true validates :payment_provider, presence: true validates :payment_card, presence: true end with_options(if: -> { deferred? }) do validates :payment_provider, presence: true validate do unless EffectiveOrders.deferred_providers.include?(payment_provider) || EffectiveOrders.delayed_providers.include?(payment_provider) errors.add(:payment_provider, "unknown deferred payment provider") end end end validate(if: -> { was_voided? && status_changed? }) do errors.add(:status, "cannot update status of a voided order") unless voided? end # Sanity check before_save(if: -> { status_was.to_s == 'purchased' }) do raise('cannot unpurchase an order. try voiding instead.') unless purchased? || voided? raise('cannot change subtotal of a purchased order') if changes[:subtotal].present? raise('cannot change tax of a purchased order') if changes[:tax].present? raise('cannot change tax rate of a purchased order') if changes[:tax_rate].present? raise('cannot change surcharge of a purchased order') if changes[:surcharge].present? raise('cannot change surcharge percent of a purchased order') if changes[:surcharge_percent].present? raise('cannot change total of a purchased order') if changes[:total].present? end # 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(status: :pending) # Initialize with status pending return self unless atts.present? if atts.kind_of?(Hash) items = Array(atts[:item]) + Array(atts[:items]) self.user = atts[:user] || items.first.try(:user) if (address = atts[:billing_address]).present? self.billing_address = address self.billing_address.full_name ||= user.to_s.presence end if (address = atts[:shipping_address]).present? self.shipping_address = address self.shipping_address.full_name ||= user.to_s.presence end atts.except(:item, :items, :user, :billing_address, :shipping_address).each do |key, value| self.send("#{key}=", value) end add(items) if items.present? else # Attributes are not a Hash self.user = atts.user if atts.respond_to?(:user) add(atts) end self end def remove(*items) raise 'unable to alter a purchased order' if purchased? raise 'unable to alter a declined order' if declined? removed = items.map do |item| order_item = if item.kind_of?(Effective::OrderItem) order_items.find { |oi| oi == item } else order_items.find { |oi| oi.purchasable == item } end raise("Unable to find order item for #{item}") if order_item.blank? order_item end removed.each { |order_item| order_item.mark_for_destruction } # Make sure to reset stored aggregates assign_attributes(subtotal: nil, tax_rate: nil, tax: nil, amount_owing: nil, surcharge_percent: nil, surcharge: nil, total: nil) removed.length == 1 ? removed.first : removed end # Items can be an Effective::Cart, an Effective::order, a single acts_as_purchasable, or multiple acts_as_purchasables # 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 self.cc ||= item.cc item.order_items.select { |oi| oi.purchasable.kind_of?(Effective::Product) }.map do |oi| purchasable = oi.purchasable product = Effective::Product.new(name: purchasable.purchasable_name, price: purchasable.price, tax_exempt: purchasable.tax_exempt) # Copy over any extended attributes that may have been created atts = purchasable.dup.attributes.except('name', 'price', 'tax_exempt', 'purchased_order_id').compact atts.each do |k, v| next unless product.respond_to?("#{k}=") && product.respond_to?(k) product.send("#{k}=", v) if product.send(k).blank? end 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 assign_attributes(subtotal: nil, tax_rate: nil, tax: nil, amount_owing: nil, surcharge_percent: nil, surcharge: nil, total: nil) retval = cart_items.map do |item| order_items.build( name: item.name, 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 update_prices! raise('already purchased') if purchased? raise('must be pending or confirmed') unless pending? || confirmed? present_order_items.each do |item| purchasable = item.purchasable if purchasable.blank? || purchasable.marked_for_destruction? item.mark_for_destruction else item.assign_purchasable_attributes end end save! end def to_s [label, ' #', to_param].join end def label if refund? && purchased? 'Refund' elsif purchased? 'Receipt' elsif refund? && (pending? || confirmed?) 'Pending Refund' elsif (pending? || confirmed?) 'Pending Order' else 'Order' end end def total_label purchased? ? 'Total Paid' : 'Total Due' end def payment_method payment_method_value if purchased? end def delayed_payment_method payment_method_value if delayed? end def duplicate Effective::Order.new(self) end # For moneris and moneris_checkout. Just a unique value. Must be 50 characters or fewer or will raise moneris error. def transaction_id [to_param, billing_name.to_s.parameterize.first(20).presence, Time.zone.now.to_i, rand(1000..9999)].compact.join('-') end def billing_first_name billing_name.to_s.split(' ').first end def billing_last_name Array(billing_name.to_s.split(' ')[1..-1]).join(' ') end def in_progress? pending? || confirmed? || deferred? end def done? persisted? && (purchased? || declined? || voided? || abandoned?) end # A custom order is one that was created by an admin # We allow custom orders to have their order items updated def custom_order? order_items.all? { |oi| oi.purchasable_type == 'Effective::Product' } end def purchased?(provider = nil) return false if (status.to_sym != :purchased) return true if provider.nil? || payment_provider == provider.to_s false end def purchased_or_deferred? purchased? || deferred? end def purchased_with_credit_card? purchased? && EffectiveOrders.credit_card_payment_providers.include?(payment_provider) end def purchased_without_credit_card? purchased? && EffectiveOrders.credit_card_payment_providers.exclude?(payment_provider) end def purchasables present_order_items.map { |order_item| order_item.purchasable }.compact end def subtotal self[:subtotal] || get_subtotal() end def tax_rate self[:tax_rate] || get_tax_rate() end def tax self[:tax] || get_tax() end def amount_owing self[:amount_owing] || get_amount_owing() end def surcharge_percent self[:surcharge_percent] || get_surcharge_percent() end def surcharge self[:surcharge] || get_surcharge() end def surcharge_tax self[:surcharge_tax] || get_surcharge_tax() end def total self[:total] || get_total() end def total_to_f ((total || 0) / 100.0).to_f end def total_with_surcharge get_total_with_surcharge() end def total_without_surcharge get_total_without_surcharge() end def payment Hash(self[:payment]) end def free? total == 0 end def refund? total.to_i < 0 end # A new order is created. # If the delayed_payment and delayed_payment date are set, it's a delayed order # A delayed order is one in which we have to capture a payment intent for the amount of the order. # Once it's delayed and deferred we can purchase it at anytime. def delayed? delayed_payment? && delayed_payment_date.present? end def delayed_ready_to_purchase? return false unless delayed? return false unless deferred? return false unless delayed_payment_intent.present? return false if delayed_payment_date_upcoming? return false if delayed_payment_purchase_ran_at.present? # We ran before and probably failed true end def delayed_payment_info return unless delayed? && deferred? return unless delayed_payment_date_upcoming? "Your #{delayed_payment_method} will be charged $#{'%0.2f' % total_to_f} on #{delayed_payment_date.strftime('%F')}" end def delayed_payment_date_past? return false unless delayed? delayed_payment_date <= Time.zone.now.to_date end # This is checked by an effective_orders view helper. When upcoming we only collect card info. def delayed_payment_date_upcoming? return false unless delayed? delayed_payment_date > Time.zone.now.to_date end def delayed_payment_date_today? return false unless delayed? delayed_payment_date == Time.zone.now.to_date end def pending_refund? return false if EffectiveOrders.buyer_purchases_refund? return false if purchased? refund? end def num_items present_order_items.map { |oi| oi.quantity }.sum end def send_order_receipt_to_admin? return false if free? && !EffectiveOrders.send_order_receipts_when_free EffectiveOrders.send_order_receipt_to_admin end def send_order_receipt_to_buyer? return false if free? && !EffectiveOrders.send_order_receipts_when_free EffectiveOrders.send_order_receipt_to_buyer end def send_order_declined_to_admin? return false if free? && !EffectiveOrders.send_order_receipts_when_free EffectiveOrders.send_order_declined_to_admin end def send_order_declined_to_buyer? return false if free? && !EffectiveOrders.send_order_receipts_when_free EffectiveOrders.send_order_declined_to_buyer end def send_payment_request_to_buyer? return false if free? && !EffectiveOrders.send_order_receipts_when_free return false if refund? EffectiveResources.truthy?(send_payment_request_to_buyer) end def send_refund_notification_to_admin? return false unless refund? EffectiveOrders.send_refund_notification_to_admin end def send_mark_as_paid_email_to_buyer? EffectiveResources.truthy?(send_mark_as_paid_email_to_buyer) end def skip_buyer_validations? EffectiveResources.truthy?(skip_buyer_validations) 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 # It's basically the same as save! on a new order, except it might send the payment request to buyer def pending! return false if purchased? assign_attributes(status: :pending) self.addresses.clear if addresses.any? { |address| address.valid? == false } save! if send_payment_request_to_buyer? after_commit { send_payment_request_to_buyer! } end true end # Used by admin checkout only def confirm! return false if purchased? confirmed! end # This lets us skip to the confirmed workflow for an admin... def assign_confirmed_if_valid! return unless pending? assign_attributes(status: :confirmed) return true if valid? self.errors.clear assign_attributes(status: :pending) false end # Called by effective_memberships to update prices from purchasable fees # Not called internally def update_purchasable_attributes present_order_items.each { |oi| oi.update_purchasable_attributes } end def update_purchasable_attributes! raise('cannot update purchasable attributes of a purchased order') if purchased? update_purchasable_attributes save! end # Call this as a way to skip over non consequential orders # And mark some purchasables purchased # This is different than the Mark as Paid payment processor def mark_as_purchased! purchase!(skip_buyer_validations: true, email: false, skip_quickbooks: true) end # Effective::Order.new(items: Product.first, user: User.first).purchase!(email: false) def purchase!(payment: nil, provider: nil, card: nil, email: true, skip_buyer_validations: false, skip_quickbooks: false) return true if purchased? raise('unable to purchase voided order') if voided? # Assign attributes assign_attributes( skip_buyer_validations: skip_buyer_validations, status: :purchased, purchased_at: (purchased_at.presence || Time.zone.now), purchased_by: (purchased_by.presence || current_user), payment: payment_to_h(payment.presence || 'none'), payment_provider: (provider.presence || 'none'), payment_card: (card.presence || 'none'), delayed_payment_intent: nil # Do not store the delayed payment intent any longer ) if current_user&.email.present? assign_attributes(email: current_user.email) end # Updates surcharge and total based on payment_provider assign_order_charges() begin Effective::Order.transaction do run_purchasable_callbacks(:before_purchase) save! update_purchasables_purchased_order! run_purchasable_callbacks(:after_purchase) end rescue => e Effective::Order.transaction do save!(validate: false) update_purchasables_purchased_order! end raise(e) end send_order_receipts! if email after_commit { sync_quickbooks!(skip: skip_quickbooks) } true end # We support two different Quickbooks synchronization gems: effective_qb_sync and effective_qb_online def sync_quickbooks!(skip:) if EffectiveOrders.qb_online? skip ? EffectiveQbOnline.skip_order!(self) : EffectiveQbOnline.sync_order!(self) end if EffectiveOrders.qb_sync? skip ? EffectiveQbSync.skip_order!(self) : true # Nothing to do end true end def skip_quickbooks! sync_quickbooks!(skip: true) end # This was submitted via the deluxe_delayed provider checkout # This is a special case of a deferred provider. We require the payment_intent and payment info def delay!(payment:, payment_intent:, provider:, card:, email: false, validate: true) raise('expected payment intent to be a String') unless payment_intent.kind_of?(String) raise('expected a delayed payment provider') unless EffectiveOrders.delayed_providers.include?(provider) raise('expected a delayed payment order with a delayed_payment_date') unless delayed_payment? && delayed_payment_date.present? assign_attributes( delayed_payment_intent: payment_intent, delayed_payment_total: total(), payment: payment_to_h(payment), payment_card: (card.presence || 'none') ) defer!(provider: provider, email: email, validate: validate) end def defer!(provider: 'none', email: true, validate: true) raise('order already purchased') if purchased? # Assign attributes assign_attributes( payment_provider: provider, status: :deferred, purchased_at: nil, purchased_by: nil, deferred_at: (deferred_at.presence || Time.zone.now), deferred_by: (deferred_by.presence || current_user) ) if current_user&.email.present? assign_attributes(email: current_user.email) end error = nil begin Effective::Order.transaction do run_purchasable_callbacks(:before_defer) save!(validate: validate) run_purchasable_callbacks(:after_defer) end rescue => e self.status = status_was error = e.message end raise "Failed to defer order: #{error || errors.full_messages.to_sentence}" unless error.nil? send_pending_order_invoice_to_buyer! if email true end # We only turn on the email when done by a delayed payment or from a rake script. def decline!(payment: 'none', provider: 'none', card: 'none', validate: true, email: false) return false if declined? raise('order already purchased') if purchased? assign_attributes( skip_buyer_validations: true, status: :declined, purchased_at: nil, purchased_by: nil, payment: payment_to_h(payment), payment_provider: provider, payment_card: (card.presence || 'none') ) if current_user&.email.present? assign_attributes(email: current_user.email) end error = nil Effective::Order.transaction do begin run_purchasable_callbacks(:before_decline) save!(validate: validate) run_purchasable_callbacks(:after_decline) rescue => e self.status = status_was error = e.message end end raise "Failed to decline order: #{error || errors.full_messages.to_sentence}" unless error.nil? send_declined_notifications! if email true end def declined_reason return unless declined? delayed_payment_purchase_result.presence || 'credit card declined' end def void! raise('already voided') if voided? voided!(skip_buyer_validations: true) end def unvoid! raise('order must be voided to unvoid') unless voided? unvoided!(skip_buyer_validations: true) end def deluxe_delayed_purchase! raise('expected a delayed order') unless delayed? raise('expected a deferred order') unless deferred? raise('expected delayed payment intent') unless delayed_payment_intent.present? raise('expected a deluxe_delayed payment provider') unless payment_provider == 'deluxe_delayed' Effective::DeluxeApi.new().purchase_delayed_orders!(self) end # These are all the emails we send all notifications to def emails ([purchased_by.try(:email)] + [email] + [user.try(:email)] + Array(organization.try(:billing_emails))).map(&:presence).compact.uniq end # Doesn't control anything. Purely for the flash messaging def emails_send_to (emails + [cc.presence]).compact.uniq.to_sentence end def send_order_receipts! send_order_receipt_to_admin! if send_order_receipt_to_admin? send_order_receipt_to_buyer! if send_order_receipt_to_buyer? send_refund_notification! if send_refund_notification_to_admin? end def send_declined_notifications! send_order_declined_to_admin! if send_order_declined_to_admin? send_order_declined_to_buyer! if send_order_declined_to_buyer? end def send_order_declined_to_admin! EffectiveOrders.send_email(:order_declined_to_admin, self) if declined? end def send_order_declined_to_buyer! EffectiveOrders.send_email(:order_declined_to_buyer, self) if declined? end def send_order_receipt_to_admin! EffectiveOrders.send_email(:order_receipt_to_admin, self) if purchased? end def send_order_receipt_to_buyer! EffectiveOrders.send_email(:order_receipt_to_buyer, self) if purchased? end alias_method :send_buyer_receipt!, :send_order_receipt_to_buyer! def send_payment_request_to_buyer! EffectiveOrders.send_email(:payment_request_to_buyer, self) unless purchased? end def send_pending_order_invoice_to_buyer! EffectiveOrders.send_email(:pending_order_invoice_to_buyer, self) unless purchased? end def send_refund_notification! EffectiveOrders.send_email(:refund_notification_to_admin, self) if refund? end protected def get_subtotal subtotal = present_order_items.map { |oi| oi.subtotal }.sum if subtotal.to_i < 0 && EffectiveOrders.no_refund? return 0 end subtotal end def get_tax_rate rate = instance_exec(self, &EffectiveOrders.order_tax_rate_method).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 rate end def get_tax return 0 unless tax_rate.present? tax = present_order_items.reject { |oi| oi.tax_exempt? }.map { |oi| (oi.subtotal * (tax_rate / 100.0)).round(0).to_i }.sum if tax.to_i < 0 && EffectiveOrders.no_refund? return 0 end tax end def get_amount_owing subtotal + tax end def get_surcharge_percent percent = EffectiveOrders.credit_card_surcharge_percent.to_f return nil unless percent > 0.0 return 0.0 if purchased_without_credit_card? if (percent > 10.0 || percent < 0.5) raise "expected EffectiveOrders.credit_card_surcharge to return a value between 10.0 (10%) and 0.5 (0.5%) or nil. Received #{percent}. Please return 2.5 for 2.5% surcharge." end percent end def get_surcharge return 0 unless surcharge_percent.present? ((subtotal + tax) * (surcharge_percent / 100.0)).round(0).to_i end def get_surcharge_tax return 0 unless tax_rate.present? (surcharge * (tax_rate / 100.0)).round(0).to_i end def get_total subtotal + tax + surcharge + surcharge_tax end def get_total_with_surcharge subtotal + tax + surcharge + surcharge_tax end def get_total_without_surcharge subtotal + tax end # Visa - 1234 def payment_method_value provider = payment_provider if ['cheque', 'etransfer', 'phone', 'credit card'].include?(payment_provider) provider = 'credit card' if ['deluxe_delayed'].include?(payment_provider) # Normalize payment card card = case payment_card.to_s.downcase.gsub(' ', '').strip when '' then nil when 'v', 'visa' then 'Visa' when 'm', 'mc', 'master', 'mastercard' then 'MasterCard' when 'a', 'ax', 'american', 'americanexpress' then 'American Express' when 'd', 'discover' then 'Discover' else payment_card.to_s end # Try again if card == 'none' && payment['card_type'].present? card = case payment['card_type'].to_s.downcase.gsub(' ', '').strip when '' then nil when 'v', 'visa' then 'Visa' when 'm', 'mc', 'master', 'mastercard' then 'MasterCard' when 'a', 'ax', 'american', 'americanexpress' then 'American Express' when 'd', 'discover' then 'Discover' else payment_card.to_s end end last4 = if payment[:active_card] && payment[:active_card].include?('**** **** ****') payment[:active_card][15,4] end last4 ||= if payment['active_card'] && payment['active_card'].include?('**** **** ****') payment['active_card'][15,4] end # stripe, moneris, moneris_checkout last4 ||= (payment['f4l4'] || payment['first6last4']).to_s.last(4) [provider.presence, card.presence, last4.presence].compact.join(' - ') end private def present_order_items order_items.reject { |oi| oi.marked_for_destruction? } end # Organization first def assign_billing_name self.billing_name = billing_address.try(:full_name).presence || organization.to_s.presence || user.to_s.presence end # User first def assign_billing_email email = emails.first assign_attributes(email: email) if email.present? end def assign_organization_address return unless organization.present? if EffectiveOrders.billing_address && billing_address.blank? && organization.try(:billing_address).present? self.billing_address = organization.billing_address self.billing_address.full_name ||= organization.to_s.presence end if EffectiveOrders.shipping_address && shipping_address.blank? && organization.try(:shipping_address).present? self.shipping_address = organization.shipping_address self.shipping_address.full_name ||= organization.to_s.presence end end def assign_user_address return unless user.present? if EffectiveOrders.billing_address && billing_address.blank? && user.try(:billing_address).present? self.billing_address = user.billing_address self.billing_address.full_name ||= user.to_s.presence end if EffectiveOrders.shipping_address && shipping_address.blank? && user.try(:shipping_address).present? self.shipping_address = user.shipping_address self.shipping_address.full_name ||= user.to_s.presence end end # These two overwrites the prices, taxes, surcharge, etc on every save. # Does not get run from the before_validate on purchase. def assign_order_item_values # Copies prices from purchasable into order items present_order_items.each { |oi| oi.assign_purchasable_attributes } end def assign_order_values # Calculated from each item self.subtotal = get_subtotal() # We only know tax if there is a billing address self.tax_rate = get_tax_rate() self.tax = get_tax() # Subtotal + Tax self.amount_owing = get_amount_owing() end def assign_order_charges # We only apply surcharge for credit card orders. But we have to display and calculate for non purchased orders self.surcharge_percent = get_surcharge_percent() self.surcharge = get_surcharge() self.surcharge_tax = get_surcharge_tax() # Subtotal + Tax + Surcharge + Surcharge Tax self.total = get_total() end def update_purchasables_purchased_order! purchasables.each do |purchasable| columns = { purchased_order_id: id, purchased_at: (purchased_at if purchasable.respond_to?(:purchased_at=)), purchased_by_id: (purchased_by_id if purchasable.respond_to?(:purchased_by_id=)), purchased_by_type: (purchased_by_type if purchasable.respond_to?(:purchased_by_type=)) }.compact purchasable.update_columns(columns) end true end def run_purchasable_callbacks(name) order_items.select { |item| item.purchasable.respond_to?(name) }.each do |item| if item.class.respond_to?(:transaction) item.class.transaction { item.purchasable.public_send(name, self, item) } else item.purchasable.public_send(name, self, item) end end if parent.respond_to?(name) if parent.class.respond_to?(:transaction) parent.class.transaction { parent.public_send(name, self) } else parent.public_send(name, self) end end true end def payment_to_h(value) if value.respond_to?(:to_unsafe_h) value.to_unsafe_h.to_h elsif value.respond_to?(:to_h) value.to_h else { details: (value.to_s.presence || 'none') } end end end end