openfoodfoundation/openfoodnetwork

View on GitHub
app/models/invoice/data_presenter.rb

Summary

Maintainability
A
0 mins
Test Coverage
# frozen_string_literal: true

class Invoice
  class DataPresenter
    include ::ActionView::Helpers::NumberHelper
    attr_reader :invoice

    delegate :display_number, :data, :previous_invoice, to: :invoice
    delegate :date, to: :invoice, prefix: true

    FINALIZED_NON_SUCCESSFUL_STATES = %w(canceled returned).freeze

    extend Invoice::DataPresenterAttributes

    attributes :additional_tax_total, :currency, :included_tax_total, :payment_total,
               :shipping_method_id, :state, :total, :number, :note, :special_instructions,
               :completed_at

    attributes_with_presenter :bill_address, :customer, :distributor, :ship_address,
                              :shipping_method, :order_cycle

    array_attribute :sorted_line_items, class_name: 'LineItem'
    array_attribute :all_eligible_adjustments, class_name: 'Adjustment'
    array_attribute :payments, class_name: 'Payment'

    # if any of the following attributes is updated, a new invoice should be generated
    invoice_generation_attributes :additional_tax_total, :all_eligible_adjustments, :bill_address,
                                  :included_tax_total, :payments, :payment_total, :ship_address,
                                  :shipping_method_id, :sorted_line_items, :total

    # if any of the following attributes is updated, the latest invoice should be updated
    invoice_update_attributes :note, :special_instructions, :state,
                              :all_eligible_adjustments, :payments

    def initialize(invoice)
      @invoice = invoice
    end

    def has_taxes_included
      included_tax_total > 0
    end

    def total_tax
      additional_tax_total + included_tax_total
    end

    def order_completed_at
      return nil if data[:completed_at].blank?

      Time.zone.parse(data[:completed_at])
    end

    def checkout_adjustments(exclude: [])
      adjustments = all_eligible_adjustments
        .reject { |a| a.originator.type == 'Spree::TaxRate' }
        .map(&:clone)

      adjustments.reject! { |a| a.amount == 0 }
      [:line_item, :shipment].each do |type|
        next unless exclude.include? type

        adjustments.reject! { |a|
          a.adjustable_type == "Spree::#{type.to_s.classify}"
        }
      end
      adjustments
    end

    def shipment_adjustment
      all_eligible_adjustments.find { |a| a.originator.type == 'Spree::ShippingMethod' }
    end

    # contains limited information about the shipment
    def shipment
      shipment_adjustment&.adjustable || null_shipment
    end

    def null_shipment
      Struct.new(
        :amount,
        :included_tax_total,
        :additional_tax_total,
      ).new(0, 0, 0)
    end

    def display_shipment_amount_without_taxes
      Spree::Money.new(shipment.amount - shipment.included_tax_total, currency:)
    end

    def display_shipment_amount_with_taxes
      Spree::Money.new(shipment.amount + shipment.additional_tax_total, currency:)
    end

    def display_line_item_tax_rate(item)
      all_tax_adjustments.select { |a|
        a.adjustable.type == 'Spree::LineItem' && a.adjustable.id == item.id
      }.map(&:originator).map { |tr|
        number_to_percentage(tr.amount * 100, precision: 1)
      }.join(", ")
    end

    def display_shipment_tax_rates
      all_eligible_adjustments.select { |a|
        a.originator.type == 'Spree::TaxRate' && a.adjustable_type == 'Spree::Shipment'
      }.map(&:originator)
        .map { |tr| number_to_percentage(tr.amount * 100, precision: 1) }.join(", ")
    end

    def display_checkout_taxes_hash
      tax_adjustment_totals.map do |tax_rate_id, tax_amount|
        tax_rate = tax_rate_by_id[tax_rate_id]
        {
          amount: Spree::Money.new(tax_amount, currency:),
          percentage: number_to_percentage(tax_rate.amount * 100, precision: 1),
          rate_amount: tax_rate.amount,
        }
      end.sort_by { |tax| tax[:rate_amount] }
    end

    def display_date
      I18n.l(invoice_date.to_date, format: :long)
    end

    def display_tax_adjustment_total
      Spree::Money.new(all_tax_adjustments.map(&:amount).sum, currency:)
    end

    def tax_adjustment_totals
      all_tax_adjustments.each_with_object(Hash.new(0)) do |adjustment, totals|
        totals[adjustment.originator.id] += adjustment.amount
      end
    end

    def tax_rate_by_id
      all_tax_adjustments.each_with_object({}) do |adjustment, tax_rates|
        tax_rates[adjustment.originator.id] = adjustment.originator
      end
    end

    def all_tax_adjustments
      all_eligible_adjustments.select { |a| a.originator.type == 'Spree::TaxRate' }
    end

    def paid?
      data[:payment_state] == 'paid' || data[:payment_state] == 'credit_owed'
    end

    def outstanding_balance?
      !new_outstanding_balance.zero?
    end

    def new_outstanding_balance
      if state.in?(FINALIZED_NON_SUCCESSFUL_STATES)
        -payment_total
      else
        total - payment_total
      end
    end

    def outstanding_balance_label
      new_outstanding_balance.negative? ? I18n.t(:credit_owed) : I18n.t(:balance_due)
    end

    def last_payment
      payments.max_by(&:created_at)
    end

    def last_payment_method
      last_payment&.payment_method
    end

    def display_outstanding_balance
      Spree::Money.new(new_outstanding_balance, currency:)
    end

    def display_checkout_tax_total
      Spree::Money.new(total_tax, currency:)
    end

    def display_checkout_total_less_tax
      Spree::Money.new(total - total_tax, currency:)
    end

    def display_total
      Spree::Money.new(total, currency:)
    end
  end
end