Skip to content
This repository has been archived by the owner on Aug 10, 2021. It is now read-only.

An Event Sourced Accounting implementation for Ruby on Rails

License

Notifications You must be signed in to change notification settings

lnagel/event-sourced-accounting

Repository files navigation

Gem Version Build Status Code Climate Code Climate

Event-Sourced Accounting

The Event-Sourced Accounting plugin provides an event-sourced double entry accounting system. It uses the data models of a Rails application as a data source and automatically generates accounting transactions based on defined accounting rules.

This plugin began life as a fork of the Plutus plugin with many added features and refactored compontents. As the aims of the ESA plug-in have completely changed compared to the original project, it warrants a release under its own name.

Installation

  • Add gem "event_sourced_accounting" to your Gemfile

  • generate migration files with rails g event_sourced_accounting

  • run migrations rake db:migrate

Integration

First, configure the gem by creating config/initializers/accounting.rb.

require 'esa'

ESA.configure do |config|
  config.processor = ESA::BlockingProcessor # default
  config.extension_namespace = 'Accounting' # default
  config.register('BankTransaction')
  ...
end

Then add include ESA::Traits::Accountable to the registered models.

class BankTransaction < ActiveRecord::Base
  include ESA::Traits::Accountable
  ...
end

Implement the corresponding Event, Flag, Ruleset and Transaction classes for the registered models.

# app/models/accounting/events/bank_transaction_event.rb
module Accounting
  module Events
    class BankTransactionEvent < ESA::Event
      enumerize :nature, in: [
                        :adjustment, # mandatory
                        :confirm,    # example
                        :revoke,     # example
                      ]
    end
  end
end
# app/models/accounting/flags/bank_transaction_flag.rb
module Accounting
  module Flags
    class BankTransactionFlag < ESA::Flag
      enumerize :nature, in: [
                        :complete, # example
                     ]
    end
  end
end
# app/models/accounting/transactions/bank_transaction_transaction.rb
module Accounting
  module Transactions
    class BankTransactionTransaction < ESA::Transaction
      # this relation definition is optional
      has_one :bank_transaction, :through => :flag, :source => :accountable, :source_type => "BankTransaction"
    end
  end
end
# app/models/accounting/rulesets/bank_transaction_ruleset.rb
module Accounting
  module Rulesets
    class BankTransactionRuleset < ESA::Ruleset
      # events that have happened according to the current state
      def event_times(bank_transaction)
        {
          confirm: bank_transaction.confirm_time,
          revoke: bank_transaction.revoke_time,
        }
      end
      
      # flags to be changed when events occur
      def event_nature_flags
        {
          confirm: {complete: true},
          revoke: {complete: false},
        }
      end

      # transaction for when the :complete flag is switched to true
      def flag_complete_transactions(bank_transaction)
        {
          :description => 'BankTransaction completed',
          :debits => [
            {
              :account => find_account('Asset', 'Bank'),
              :amount => bank_transaction.transferred_amount
            }
          ],
          :credits => [
            {
              :account => find_account('Asset', 'Bank Transit'),
              :amount => bank_transaction.transferred_amount
            }
          ],
        }
      end
    end
  end
end

Usage

In order to create events and transactions, the accountable objects have to pass through a processor, which will register the necessary Events, Flags & Transactions in the database.

You can use the provided processor implementation, or inherit from the base implementation and provide your own class (e.g. to implement delayed or scheduled processing).

>> bank_transaction = BankTransaction.find(..)
>> bank_transaction.confirm_time = Time.now
>> bank_transaction.save
true

>> ESA.configuration.processor.enqueue(bank_transaction)

>> bank_transaction.esa_events.count
1

>> bank_transaction.esa_flags.count
1

>> bank_transaction.esa_transactions.count
1

Reporting

There are many different reporting and filtering implementations available. For a simple example, let's look at a report that only involves the transaction.

The following commands initialize the report and update the persisted values to the depth of 1, which includes the creation of sub-reports per each account involved in the transactions of that BankAccount.

>> report = ESA::Contexts::AccountableContext.create(chart: ESA::Chart.first, accountable: bank_transaction)
>> report.check_freshness(1)

Complex reports can be constructed automatically using the context provider functionality. Reports, filters and context providers are available for:

  • account
  • accountable object (e.g. a single BankTransaction)
  • accountable type (e.g. all known BankTransactions)
  • date periods (year, month, date, custom)

Please refer to the source code for examples.

Subreport structure and context providers need to be configured:

ESA.configure do |config|
  ...
  config.context_providers['bank_account'] = Accounting::ContextProviders::BankAccountContextProvider
  
  config.context_tree = {
    'month' => {
      'account' => {
        'bank_account' => {},
        'date' => {},
      },
    },
  }
  ...
end

Development

Any comments and contributions are welcome. Will gladly accept patches sent via pull requests.

  • run rspec tests simply with rake

  • update documentation with yard