openfoodfoundation/openfoodnetwork

View on GitHub
app/models/spree/order/checkout.rb

Summary

Maintainability
A
0 mins
Test Coverage
# frozen_string_literal: true

module Spree
  class Order < ApplicationRecord
    module Checkout
      def self.included(klass)
        klass.class_eval do
          class_attribute :next_event_transitions
          class_attribute :previous_states
          class_attribute :checkout_flow

          def self.checkout_flow(&block)
            if block_given?
              @checkout_flow = block
              define_state_machine!
            else
              @checkout_flow
            end
          end

          def self.define_state_machine!
            self.next_event_transitions = []
            self.previous_states = [:cart]

            # Build the checkout flow using the checkout_flow defined either
            # within the Order class, or a decorator for that class.
            #
            # This method may be called multiple times depending on if the
            # checkout_flow is re-defined in a decorator or not.
            instance_eval(&checkout_flow)

            klass = self

            # To avoid a ton of warnings when the state machine is re-defined
            StateMachines::Machine.ignore_method_conflicts = true
            # To avoid multiple occurrences of the same transition being defined
            # On first definition, state_machines will not be defined
            state_machines.clear if respond_to?(:state_machines)
            state_machine :state, initial: :cart do
              klass.next_event_transitions.each { |t| transition(t.merge(on: :next)) }

              # Persist the state on the order
              after_transition do |order|
                order.state = order.state
                order.save
              end

              event :cancel do
                transition to: :canceled, if: :allow_cancel?
              end

              event :return do
                transition to: :returned, from: :awaiting_return, unless: :awaiting_returns?
              end

              event :resume do
                transition to: :resumed, from: :canceled, if: :allow_resume?
              end

              event :authorize_return do
                transition to: :awaiting_return
              end

              event :restart_checkout do
                transition to: :cart, unless: :completed?
              end

              event :confirm do
                transition to: :complete, from: :confirmation
              end

              event :back_to_payment do
                transition to: :payment, from: :confirmation
              end

              event :back_to_address do
                transition to: :address, from: [:payment, :confirmation]
              end

              before_transition from: :cart, do: :ensure_line_items_present

              before_transition to: :delivery, do: :create_proposed_shipments
              before_transition to: :delivery, do: :ensure_available_shipping_rates
              before_transition to: :confirmation, do: :validate_payment_method!

              after_transition to: :payment do |order|
                order.create_tax_charge!
                order.update_totals_and_states
              end

              after_transition to: :complete, do: :finalize!
              after_transition to: :resumed,  do: :after_resume
              after_transition to: :canceled, do: :after_cancel
            end
          end

          def self.go_to_state(name, options = {})
            previous_states.each do |state|
              add_transition({ from: state, to: name }.merge(options))
            end
            if options[:if]
              previous_states << name
            else
              self.previous_states = [name]
            end
          end

          def self.next_event_transitions
            @next_event_transitions ||= []
          end

          def self.add_transition(options)
            next_event_transitions << { options.delete(:from) => options.delete(:to) }.
              merge(options)
          end

          def restart_checkout_flow
            update_columns(
              state: "address",
              updated_at: Time.zone.now,
            )
          end

          def state_changed(name)
            state = "#{name}_state"
            return unless persisted?

            old_state = __send__("#{state}_was")
            state_changes.create(
              previous_state: old_state,
              next_state: __send__(state),
              name:,
              user_id:
            )
          end

          private

          def after_cancel
            shipments.reject(&:canceled?).each(&:cancel!)
            payments.checkout.each(&:void!)

            OrderMailer.cancel_email(id).deliver_later if send_cancellation_email
            update(payment_state: updater.update_payment_state)
          end

          def after_resume
            shipments.each(&:resume!)
            payments.void.each(&:resume!)

            update(payment_state: updater.update_payment_state)
          end

          def validate_payment_method!
            return unless checkout_processing
            return if payments.any?

            errors.add :payment_method, I18n.t('checkout.errors.select_a_payment_method')
            throw :halt
          end
        end
      end
    end
  end
end