lnagel/event-sourced-accounting

View on GitHub
lib/esa/blocking_processor.rb

Summary

Maintainability
A
1 hr
Test Coverage
module ESA
  class BlockingProcessor
    def self.enqueue(accountable)
      if accountable.present? and accountable.class.ancestors.include? ESA::Traits::Accountable
        process_accountable(accountable)
      end
    end

    def self.process_accountable(accountable)
      processed = create_and_process_events(accountable)

      state = ESA::State.where(accountable_id: accountable.id, accountable_type: accountable.class).first_or_create
      state.processed_at = Time.now
      state.unprocessed = processed ? 0 : accountable.esa_events.where(processed: false).count

      state.save
    end

    def self.create_and_process_events(accountable)
      events_created = create_events(accountable)

      if events_created
        unprocessed_events(accountable).each do |event|
          event.processed = process_event(event)
          event.save if event.changed?

          # do not process later events if one fails
          return false if not event.processed
        end

        true
      else
        false
      end
    end

    def self.unprocessed_events(accountable)
      accountable.esa_events.
          where(processed: false).
          order(:time, :created_at, :id)
    end

    def self.create_events(accountable)
      produce_events(accountable).map(&:save).all?
    end

    def self.produce_events(accountable)
      ruleset = Ruleset.extension_instance(accountable)

      if ruleset.present?
        events = ruleset.addable_unrecorded_events_as_attributes(accountable)
        adjusted_events = event_attrs_with_adjustment(ruleset, accountable, events)
        accountable.esa_events.new(adjusted_events)
      else
        []
      end
    end

    def self.event_attrs_with_adjustment(ruleset, accountable, events)
      if ruleset.is_adjustment_event_needed?(accountable)
        events.unshift({
          accountable: accountable,
          ruleset: ruleset,
          nature: :adjustment,
          time: [events.map{|e| e[:time]}.min, Time.zone.now].compact.min,
        })
      else
        events
      end
    end

    def self.process_event(event)
      flags_created = create_flags(event)

      if flags_created
        unprocessed_flags(event).map do |flag|
          flag.processed = process_flag(flag)

          if flag.changed?
            flag.save and flag.processed
          else
            flag.processed
          end
        end.all?
      else
        false
      end
    end

    def self.unprocessed_flags(event)
      flags = []

      if event.nature.adjustment?
        flags += event.accountable.esa_flags.
            where(adjusted: true, processed: false).
            order(:time, :created_at, :id)
      end

      flags += event.flags.
          where(processed: false).
          order(:time, :created_at, :id)

      flags
    end

    def self.create_flags(event)
      if not event.processed and not event.processed_was
        produce_flags(event).map(&:save).all?
      else
        true
      end
    end

    def self.produce_flags(event)
      if event.nature.adjustment?
        produce_flags_for_adjustment(event)
      else
        produce_flags_for_regular(event)
      end
    end

    def self.produce_flags_for_adjustment(event)
      if event.ruleset.present?
        adjusted_flags = event.ruleset.flags_needing_adjustment(event.accountable)
        adjusted_flags.map do |flag|
          flag.processed = false
          flag.adjusted = true
          flag.adjustment_time = event.time

          adjustment = initialize_adjustment_flag(event, flag)
          event.flags << adjustment

          [flag, adjustment]
        end.flatten
      else
        []
      end
    end

    def self.initialize_adjustment_flag(event, flag)
      attrs = {
        :accountable => event.accountable,
        :nature => flag.nature,
        :state => flag.state,
        :event => event,
      }

      event.accountable.esa_flags.new(attrs)
    end

    def self.produce_flags_for_regular(event)
      if event.ruleset.present?
        existing_flags = event.flags.all
        required_flags = event.ruleset.event_flags_as_attributes(event)

        required_flags.map do |attrs|
          flag = existing_flags.find{|flag| flag.matches_spec?(attrs)}

          if flag.nil?
            flag = event.accountable.esa_flags.new(attrs)
            event.flags << flag
          end

          flag
        end
      else
        []
      end
    end

    def self.process_flag(flag)
      flag.transition ||= flag.accountable.esa_flags.transition_for(flag)
      create_transactions(flag)
    end

    def self.create_transactions(flag)
      if not flag.processed and not flag.processed_was
        if flag.transition.present? and flag.transition.in? [-1, 0, 1]
          produce_transactions(flag).map(&:save).all?
        else
          false
        end
      else
        true
      end
    end

    def self.produce_transactions(flag)
      txs = flag.accountable.esa_transactions.new(missing_transactions(flag))
      flag.transactions += txs
      txs
    end

    def self.missing_transactions(flag)
      if flag.ruleset.present? and flag.transition.present? and flag.transition.in? [-1, 0, 1]
        existing = flag.transactions.all
        required = flag.ruleset.flag_transactions_as_attributes(flag)

        required.reject do |attrs|
          existing.find{|tx| tx.matches_spec?(attrs)}.present?
        end
      else
        []
      end
    end
  end
end