drhenner/ror_ecommerce

View on GitHub
app/models/invoice.rb

Summary

Maintainability
A
2 hrs
Test Coverage
# == Class Info
#
#  Every monetary charge creates an invoice.  Thus when you charge a customer XYZ,
#  you are actually using an invoice for the charge and not the order.  This allows
#  flexibility within accounting and returns and even some new charge that will be made
#  in the future.  If the system was only doing Cash-Based account the Invoices table
#  would be the only table needed.  NOTE: DO NOT THINK FOR A SECOND CASH BASED ACCOUNT IS GOOD ENOUGH!


# == Schema Information
#
# Table name: invoices
#
#  id              :integer(4)      not null, primary key
#  order_id        :integer(4)      not null
#  amount          :decimal(8, 2)   not null
#  invoice_type    :string(255)     default("Purchase"), not null
#  state           :string(255)     not null
#  active          :boolean(1)      default(TRUE), not null
#  created_at      :datetime
#  updated_at      :datetime
#  credited_amount :decimal(8, 2)   default(0.0)
#

class Invoice < ApplicationRecord
  include AASM

  has_many :payments
  has_many :batches, as: :batchable#, :polymorphic => true
  belongs_to :order, required: true


  validates :amount,        presence: true
  validates :invoice_type,  presence: true

  PURCHASE  = 'Purchase'
  RMA       = 'RMA'

  INVOICE_TYPES = [PURCHASE, RMA]
  NUMBER_SEED     = 3002001004005
  CHARACTERS_SEED = 20

  aasm column: :state do
    state :pending, initial: true
    state :authorized
    state :paid
    state :payment_declined
    state :canceled
    state :refunded

    #after_transition :on => 'cancel', :do => :cancel_authorized_payment

    event :payment_rma do
      transitions from: :pending, to: :refunded
    end
    event :payment_authorized do
      transitions from: [:pending, :payment_declined], to: :authorized
    end

    event :payment_captured do
      transitions from: :authorized, to: :paid
    end

    event :transaction_declined do
      transitions from:  :pending,
                  to:    :payment_declined
      transitions from:  :payment_declined,
                  to:    :payment_declined
      transitions from:  :authorized,
                  to:    :authorized
    end

    event :cancel do
      transitions from: :authorized, to: :canceled
    end
  end

  #  the full shipping address as an array compacted.
  #
  # @param [none]
  # @return [Array]  has ("name", "address1", "address2"(unless nil), "city state zip") or nil
  def order_ship_address_lines
    order.ship_address.try(:full_address_array)
  end

  #  the full order address as an array compacted.
  #
  # @param [none]
  # @return [Array]  has ("name", "address1", "address2"(unless nil), "city state zip") or nil
  def order_billing_address_lines
    order.bill_address.try(:full_address_array)
  end

  # date the invoice was created (not the date it was paid)
  #
  # @param [Optional Symbol] date format to be returned
  # @return [String] formated date
  def invoice_date(format = :us_date)
    I18n.localize(created_at, format: format)
  end

  # invoice number
  #
  # @param [none]
  # @return [String] invoice number calculated based off the id and preset values
  def number
    (NUMBER_SEED + id).to_s(CHARACTERS_SEED)
  end

  # invoice id calculated from the id of the number
  #
  # @param [none]
  # @return [Integer] invoice id calculated based off the id and preset values
  def self.id_from_number(num)
    num.to_i(CHARACTERS_SEED) - NUMBER_SEED
  end

  # find invoice based off the invoice's number
  #
  # @param [String] invoice Number
  # @return [Invoice] invoice
  def self.find_by_number(num)
    find(id_from_number(num))##  now we can search by id which should be much faster
  end

  # make an invoice object (not saved)
  #
  # @param [Integer] order id
  # @param [Decimal] amount in dollars
  # @return [Invoice] invoice object
  def self.generate(order_id, charge_amount, credited_amount = 0.0)
    Invoice.new(order_id: order_id, amount: charge_amount, invoice_type: PURCHASE, credited_amount: credited_amount)
  end

  def capture_complete_order
    if batches.empty?
      # this means we never authorized just captured payment
      capture_complete_order_without_authorization
    else
      capture_authorized_order
    end
  end

  def capture_authorized_order
    batch       = batches.first
    batch.transactions.push(CreditCardReceivePayment.new_capture_authorized_payment(order.user, amount))
    batch.save
  end

  def capture_complete_order_without_authorization
    batch = self.batches.create()
    batch.transactions.push(CreditCardCapture.new_capture_payment_directly(order.user, amount))
    batch.save
  end

  def authorize_complete_order#(amount)
    order.complete!
    if batches.empty?
      batch = self.batches.create()
      batch.transactions.push(CreditCardPayment.new_authorized_payment(order.user, amount))
      batch.save
    else
      raise error ###  something messed up I think
    end
  end

  def cancel_authorized_payment
    batch       = batches.first
    if batch# if not we never authorized the payment
      self.cancel!
      batch.transactions.push(CreditCardCancel.new_cancel_authorized_payment(order.user, amount))
      batch.save
    end
  end

  def self.process_rma(return_amount, order)
    transaction do
      this_invoice = Invoice.new(order: order, amount: return_amount, invoice_type: RMA)
      this_invoice.save
      this_invoice.complete_rma_return
      this_invoice.payment_rma!
      this_invoice
    end
  end

  def complete_rma_return
    batch       = batches.first || self.batches.create()
    batch.transactions.push(ReturnMerchandiseComplete.new_complete_rma(order.user, amount))
    batch.save
  end


  # call to find the confirmation_id sent by the payment processor.
  #
  # @param [none]
  # @return [String] id the payment processor sends you after authorization.
  def authorization_reference
    if authorization = payments.order('id ASC').find_by(action: 'authorization', success: true )
      authorization.confirmation_id #reference
    end
  end

  # call to find out if the transaction has succeeded.
  #
  # @param [none]
  # @return [Boolean] returns true if the invoice is paid or has been authorized for payment
  def succeeded?
    authorized? || paid?
  end

  # call to find out the amount of the invoice in cents
  #
  # @param [none]
  # @return [Integer] amount of the invoice in cents
  def integer_amount
    times_x_amount = amount.integer? ? 1 : 100
    (amount * times_x_amount).to_i
  end

  def authorize_payment(credit_card, options = {})
    options[:number] ||= unique_order_number
    transaction do
      authorization = Payment.authorize(integer_amount, credit_card, options)
      payments.push(authorization)
      if authorization.success?
        payment_authorized!
        authorize_complete_order
      else
        transaction_declined!
      end
      authorization
    end
  end

  def capture_payment(options = {})
    transaction do
      capture = Payment.capture(integer_amount, authorization_reference, options)
      payments.push(capture)
      if capture.success?
        payment_captured!
        capture_complete_order
      else
        transaction_declined!
      end
      capture
    end
  end

  # find the user id of the order associated to the invoice.
  #
  # @param [none]
  # @return [Integer] represents the id of the user
  def user_id
    order.user_id
  end

  # find the user of the order associated to the invoice.
  #
  # @param [none]
  # @return [User]
  def user
    order.user
  end

  def self.admin_grid(args)
    if args[:order_number].present?
      with_order_number(args[:order_number])
    else
      all
    end
  end

  def self.with_order_number(order_number)
    where("orders.number = ?", order_number)
  end
  private

  def unique_order_number
    "#{Time.now.to_i}-#{rand(1000000)}"
  end
end