event_sourced_accounting 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +23 -0
- data/README.markdown +37 -0
- data/Rakefile +11 -0
- data/app/assets/javascripts/esa/application.js +15 -0
- data/app/assets/stylesheets/esa/application.css +13 -0
- data/app/assets/stylesheets/esa/main.css.scss +86 -0
- data/app/models/esa/account.rb +80 -0
- data/app/models/esa/accounts/asset.rb +22 -0
- data/app/models/esa/accounts/equity.rb +22 -0
- data/app/models/esa/accounts/expense.rb +22 -0
- data/app/models/esa/accounts/liability.rb +22 -0
- data/app/models/esa/accounts/revenue.rb +22 -0
- data/app/models/esa/amount.rb +27 -0
- data/app/models/esa/amounts/credit.rb +12 -0
- data/app/models/esa/amounts/debit.rb +12 -0
- data/app/models/esa/associations/amounts_extension.rb +79 -0
- data/app/models/esa/associations/events_extension.rb +33 -0
- data/app/models/esa/associations/flags_extension.rb +35 -0
- data/app/models/esa/associations/transactions_extension.rb +7 -0
- data/app/models/esa/chart.rb +41 -0
- data/app/models/esa/context.rb +218 -0
- data/app/models/esa/context_provider.rb +52 -0
- data/app/models/esa/context_providers/account_context_provider.rb +21 -0
- data/app/models/esa/context_providers/accountable_context_provider.rb +22 -0
- data/app/models/esa/context_providers/accountable_type_context_provider.rb +21 -0
- data/app/models/esa/context_providers/date_context_provider.rb +33 -0
- data/app/models/esa/contexts/account_context.rb +26 -0
- data/app/models/esa/contexts/accountable_context.rb +26 -0
- data/app/models/esa/contexts/accountable_type_context.rb +24 -0
- data/app/models/esa/contexts/created_at_context.rb +26 -0
- data/app/models/esa/contexts/date_context.rb +71 -0
- data/app/models/esa/contexts/empty_context.rb +19 -0
- data/app/models/esa/contexts/filter_context.rb +11 -0
- data/app/models/esa/contexts/open_close_context.rb +15 -0
- data/app/models/esa/event.rb +33 -0
- data/app/models/esa/filters/account_filter.rb +42 -0
- data/app/models/esa/filters/accountable_filter.rb +58 -0
- data/app/models/esa/filters/accountable_type_filter.rb +26 -0
- data/app/models/esa/filters/chart_filter.rb +17 -0
- data/app/models/esa/filters/context_filter.rb +35 -0
- data/app/models/esa/filters/date_time_filter.rb +52 -0
- data/app/models/esa/flag.rb +70 -0
- data/app/models/esa/ruleset.rb +175 -0
- data/app/models/esa/traits/accountable.rb +21 -0
- data/app/models/esa/traits/extendable.rb +93 -0
- data/app/models/esa/traits/or_scope.rb +35 -0
- data/app/models/esa/traits/union_scope.rb +24 -0
- data/app/models/esa/transaction.rb +74 -0
- data/config/backtrace_silencers.rb +7 -0
- data/config/database.yml +5 -0
- data/config/inflections.rb +10 -0
- data/config/mime_types.rb +5 -0
- data/config/routes.rb +6 -0
- data/config/secret_token.rb +7 -0
- data/config/session_store.rb +8 -0
- data/lib/esa/balance_checker.rb +17 -0
- data/lib/esa/blocking_processor.rb +158 -0
- data/lib/esa/config.rb +55 -0
- data/lib/esa/subcontext_checker.rb +15 -0
- data/lib/esa/version.rb +3 -0
- data/lib/esa.rb +8 -0
- data/lib/generators/esa/USAGE +11 -0
- data/lib/generators/esa/esa_generator.rb +26 -0
- data/lib/generators/esa/templates/migration.rb +142 -0
- data/spec/factories/account_factory.rb +51 -0
- data/spec/factories/amount_factory.rb +19 -0
- data/spec/factories/chart_factory.rb +7 -0
- data/spec/factories/transaction_factory.rb +11 -0
- data/spec/lib/esa_spec.rb +0 -0
- data/spec/models/account_spec.rb +31 -0
- data/spec/models/amount_spec.rb +8 -0
- data/spec/models/asset_spec.rb +9 -0
- data/spec/models/chart_spec.rb +52 -0
- data/spec/models/credit_amount_spec.rb +9 -0
- data/spec/models/debit_amount_spec.rb +9 -0
- data/spec/models/equity_spec.rb +9 -0
- data/spec/models/expense_spec.rb +9 -0
- data/spec/models/liability_spec.rb +9 -0
- data/spec/models/revenue_spec.rb +9 -0
- data/spec/models/transaction_spec.rb +118 -0
- data/spec/rcov.opts +2 -0
- data/spec/spec.opts +4 -0
- data/spec/spec_helper.rb +16 -0
- data/spec/support/account_shared_examples.rb +57 -0
- data/spec/support/amount_shared_examples.rb +21 -0
- metadata +306 -0
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Be sure to restart your server when you modify this file.
|
|
2
|
+
|
|
3
|
+
# You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces.
|
|
4
|
+
# Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ }
|
|
5
|
+
|
|
6
|
+
# You can also remove all the silencers if you're trying to debug a problem that might stem from framework code.
|
|
7
|
+
# Rails.backtrace_cleaner.remove_silencers!
|
data/config/database.yml
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Be sure to restart your server when you modify this file.
|
|
2
|
+
|
|
3
|
+
# Add new inflection rules using the following format
|
|
4
|
+
# (all these examples are active by default):
|
|
5
|
+
# ActiveSupport::Inflector.inflections do |inflect|
|
|
6
|
+
# inflect.plural /^(ox)$/i, '\1en'
|
|
7
|
+
# inflect.singular /^(ox)en/i, '\1'
|
|
8
|
+
# inflect.irregular 'person', 'people'
|
|
9
|
+
# inflect.uncountable %w( fish sheep )
|
|
10
|
+
# end
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Be sure to restart your server when you modify this file.
|
|
2
|
+
|
|
3
|
+
# Your secret key for verifying the integrity of signed cookies.
|
|
4
|
+
# If you change this key, all old signed cookies will become invalid!
|
|
5
|
+
# Make sure the secret is at least 30 characters and all random,
|
|
6
|
+
# no regular words or you'll be exposed to dictionary attacks.
|
|
7
|
+
ESA::Application.config.secret_token = 'f6b9c48aaf200fda4bbcf1642319084d5e5cda2bd95ac0a43220aab19f4ee240f9cdab08b77c3284b3a6396b13b1fe8be12a227af04fc5f5a8b7a52b626c4fdd'
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Be sure to restart your server when you modify this file.
|
|
2
|
+
|
|
3
|
+
ESA::Application.config.session_store :cookie_store, :key => '_esa_session'
|
|
4
|
+
|
|
5
|
+
# Use the database for sessions instead of the cookie-based default,
|
|
6
|
+
# which shouldn't be used to store highly confidential information
|
|
7
|
+
# (create the session table with "rails generate session_migration")
|
|
8
|
+
# Testtest::Application.config.session_store :active_record_store
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module ESA
|
|
2
|
+
class BalanceChecker
|
|
3
|
+
def self.check(context)
|
|
4
|
+
if context.can_be_persisted? and not context.freshness.nil?
|
|
5
|
+
#context.event_count = context.events.created_before(context.freshness).count
|
|
6
|
+
#context.flag_count = context.flags.created_before(context.freshness).count
|
|
7
|
+
context.transaction_count = context.transactions.created_before(context.freshness).count
|
|
8
|
+
context.amount_count = context.amounts.created_before(context.freshness).count
|
|
9
|
+
|
|
10
|
+
context.debits_total = context.amounts.debits.created_before(context.freshness).total
|
|
11
|
+
context.credits_total = context.amounts.credits.created_before(context.freshness).total
|
|
12
|
+
context.opening_balance = context.opening_context.amounts.created_before(context.freshness).balance
|
|
13
|
+
context.closing_balance = context.closing_context.amounts.created_before(context.freshness).balance
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
module ESA
|
|
2
|
+
class BlockingProcessor
|
|
3
|
+
def self.enqueue(accountable)
|
|
4
|
+
if accountable.present? and accountable.class.ancestors.include? ESA::Traits::Accountable
|
|
5
|
+
process_accountable(accountable)
|
|
6
|
+
end
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def self.process_accountable(accountable)
|
|
10
|
+
events_created = create_events(accountable)
|
|
11
|
+
|
|
12
|
+
if events_created
|
|
13
|
+
unprocessed_events = accountable.esa_events.
|
|
14
|
+
where(processed: false).
|
|
15
|
+
order('time ASC, created_at ASC')
|
|
16
|
+
|
|
17
|
+
unprocessed_events.each do |event|
|
|
18
|
+
event.processed = process_event(event)
|
|
19
|
+
event.save if event.changed?
|
|
20
|
+
|
|
21
|
+
# do not process later events if one fails
|
|
22
|
+
return false if not event.processed
|
|
23
|
+
end
|
|
24
|
+
else
|
|
25
|
+
false
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.create_events(accountable)
|
|
30
|
+
produce_events(accountable).map(&:save).all?
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.produce_events(accountable)
|
|
34
|
+
ruleset = Ruleset.extension_instance(accountable)
|
|
35
|
+
if ruleset.present?
|
|
36
|
+
last_event_time = accountable.esa_events.maximum(:time)
|
|
37
|
+
unrecorded_events = ruleset.unrecorded_events_as_attributes(accountable)
|
|
38
|
+
valid_events = unrecorded_events.select{|e| last_event_time.nil? or e[:time] >= last_event_time}
|
|
39
|
+
accountable.esa_events.new(valid_events)
|
|
40
|
+
else
|
|
41
|
+
[]
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def self.process_event(event)
|
|
46
|
+
flags_created = create_flags(event)
|
|
47
|
+
|
|
48
|
+
if flags_created
|
|
49
|
+
unprocessed_flags = []
|
|
50
|
+
|
|
51
|
+
if event.nature.adjustment?
|
|
52
|
+
unprocessed_flags += event.accountable.esa_flags.
|
|
53
|
+
where(adjusted: true, processed: false).
|
|
54
|
+
order('time ASC, created_at ASC')
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
unprocessed_flags += event.flags.
|
|
58
|
+
where(processed: false).
|
|
59
|
+
order('time ASC, created_at ASC')
|
|
60
|
+
|
|
61
|
+
unprocessed_flags.map do |flag|
|
|
62
|
+
flag.processed = process_flag(flag)
|
|
63
|
+
if flag.changed?
|
|
64
|
+
flag.save and flag.processed
|
|
65
|
+
else
|
|
66
|
+
flag.processed
|
|
67
|
+
end
|
|
68
|
+
end.all?
|
|
69
|
+
else
|
|
70
|
+
false
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def self.create_flags(event)
|
|
75
|
+
if not event.processed and not event.processed_was
|
|
76
|
+
produce_flags(event).map(&:save).all?
|
|
77
|
+
else
|
|
78
|
+
true
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def self.produce_flags(event)
|
|
83
|
+
flags = event.flags.all
|
|
84
|
+
if event.ruleset.present?
|
|
85
|
+
if event.nature.adjustment?
|
|
86
|
+
adjusted_flags = event.ruleset.flags_needing_adjustment(event.accountable)
|
|
87
|
+
adjusted_flags.map do |flag|
|
|
88
|
+
flag.processed = false
|
|
89
|
+
flag.adjusted = true
|
|
90
|
+
flag.adjustment_time = event.time
|
|
91
|
+
|
|
92
|
+
attrs = {
|
|
93
|
+
:accountable => event.accountable,
|
|
94
|
+
:nature => flag.nature,
|
|
95
|
+
:state => flag.state,
|
|
96
|
+
:event => event,
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
adjustment = event.accountable.esa_flags.new(attrs)
|
|
100
|
+
event.flags << adjustment
|
|
101
|
+
|
|
102
|
+
[flag, adjustment]
|
|
103
|
+
end.flatten
|
|
104
|
+
else
|
|
105
|
+
required_flags = event.ruleset.event_flags_as_attributes(event)
|
|
106
|
+
required_flags.map do |attrs|
|
|
107
|
+
existing = flags.find{|f| f.nature == attrs[:nature].to_s and f.state == attrs[:state]}
|
|
108
|
+
if existing.present?
|
|
109
|
+
existing
|
|
110
|
+
else
|
|
111
|
+
flag = event.accountable.esa_flags.new(attrs)
|
|
112
|
+
event.flags << flag
|
|
113
|
+
flag
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
else
|
|
118
|
+
flags
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def self.process_flag(flag)
|
|
123
|
+
flag.transition ||= flag.accountable.esa_flags.transition_for(flag)
|
|
124
|
+
create_transactions(flag)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def self.create_transactions(flag)
|
|
128
|
+
if not flag.processed and not flag.processed_was
|
|
129
|
+
if flag.transition.present? and flag.transition.in? [-1, 0, 1]
|
|
130
|
+
produce_transactions(flag).map(&:save).all?
|
|
131
|
+
else
|
|
132
|
+
false
|
|
133
|
+
end
|
|
134
|
+
else
|
|
135
|
+
true
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def self.produce_transactions(flag)
|
|
140
|
+
transactions = flag.transactions.all
|
|
141
|
+
if flag.ruleset.present? and flag.transition.present? and flag.transition.in? [-1, 0, 1]
|
|
142
|
+
required_transactions = flag.ruleset.flag_transactions_as_attributes(flag)
|
|
143
|
+
required_transactions.map do |attrs|
|
|
144
|
+
existing = transactions.find{|f| f.description == attrs[:description]}
|
|
145
|
+
if existing.present?
|
|
146
|
+
existing
|
|
147
|
+
else
|
|
148
|
+
transaction = flag.accountable.esa_transactions.new(attrs)
|
|
149
|
+
flag.transactions << transaction
|
|
150
|
+
transaction
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
else
|
|
154
|
+
transactions
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
data/lib/esa/config.rb
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
require 'esa/blocking_processor'
|
|
2
|
+
require 'esa/balance_checker'
|
|
3
|
+
require 'esa/subcontext_checker'
|
|
4
|
+
|
|
5
|
+
module ESA
|
|
6
|
+
module Config
|
|
7
|
+
mattr_accessor :processor
|
|
8
|
+
self.processor = ESA::BlockingProcessor
|
|
9
|
+
|
|
10
|
+
mattr_accessor :context_checkers
|
|
11
|
+
self.context_checkers = Set.new
|
|
12
|
+
self.context_checkers << ESA::BalanceChecker
|
|
13
|
+
self.context_checkers << ESA::SubcontextChecker
|
|
14
|
+
|
|
15
|
+
mattr_accessor :context_providers
|
|
16
|
+
self.context_providers = {
|
|
17
|
+
'account' => ESA::ContextProviders::AccountContextProvider,
|
|
18
|
+
'accountable' => ESA::ContextProviders::AccountableContextProvider,
|
|
19
|
+
'accountable_type' => ESA::ContextProviders::AccountableTypeContextProvider,
|
|
20
|
+
'monthly' => [ESA::ContextProviders::DateContextProvider, {period: :month}],
|
|
21
|
+
'daily' => [ESA::ContextProviders::DateContextProvider, {period: :day}],
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
mattr_accessor :context_tree
|
|
25
|
+
self.context_tree = {
|
|
26
|
+
'account' => {
|
|
27
|
+
'monthly' => {
|
|
28
|
+
'daily' => {},
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
'monthly' => {
|
|
32
|
+
'account' => {
|
|
33
|
+
'daily' => {},
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
'daily' => {
|
|
37
|
+
'account' => {},
|
|
38
|
+
},
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
def self.walk_context_tree(path=[], tree=self.context_tree)
|
|
42
|
+
if path.respond_to? :count and path.count == 0
|
|
43
|
+
tree || {}
|
|
44
|
+
elsif path.respond_to? :first and tree.is_a? Hash and path.first.in? tree
|
|
45
|
+
walk_context_tree(path.drop(1), tree[path.first])
|
|
46
|
+
else
|
|
47
|
+
{}
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def self.context_providers_for_path(path=[])
|
|
52
|
+
context_providers.slice(*walk_context_tree(path).keys)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module ESA
|
|
2
|
+
class SubcontextChecker
|
|
3
|
+
def self.check(context)
|
|
4
|
+
ESA::Config.context_providers_for_path(context.effective_path).each do |namespace,provider|
|
|
5
|
+
if provider.is_a? Class and provider.respond_to? :check_subcontexts
|
|
6
|
+
provider.check_subcontexts(context, namespace)
|
|
7
|
+
elsif provider.respond_to? :count and provider.count == 2 and
|
|
8
|
+
provider[0].is_a? Class and provider[0].respond_to? :check_subcontexts and provider[1].is_a? Hash
|
|
9
|
+
klass, options = provider
|
|
10
|
+
klass.check_subcontexts(context, namespace, options)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
data/lib/esa/version.rb
ADDED
data/lib/esa.rb
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# lib/generators/esa/esa_generator.rb
|
|
2
|
+
require 'rails/generators'
|
|
3
|
+
require 'rails/generators/migration'
|
|
4
|
+
|
|
5
|
+
class ESAGenerator < Rails::Generators::Base
|
|
6
|
+
include Rails::Generators::Migration
|
|
7
|
+
|
|
8
|
+
def self.source_root
|
|
9
|
+
@source_root ||= File.join(File.dirname(__FILE__), 'templates')
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Implement the required interface for Rails::Generators::Migration.
|
|
13
|
+
# taken from http://github.com/rails/rails/blob/master/activerecord/lib/generators/active_record.rb
|
|
14
|
+
def self.next_migration_number(dirname)
|
|
15
|
+
if ActiveRecord::Base.timestamped_migrations
|
|
16
|
+
Time.now.utc.strftime("%Y%m%d%H%M%S")
|
|
17
|
+
else
|
|
18
|
+
"%.3d" % (current_migration_number(dirname) + 1)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def create_migration_file
|
|
23
|
+
migration_template 'migration.rb', 'db/migrate/create_esa_tables.rb'
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
end
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
class CreateEsaTables < ActiveRecord::Migration
|
|
2
|
+
def self.up
|
|
3
|
+
create_table :esa_charts do |t|
|
|
4
|
+
t.string :name
|
|
5
|
+
|
|
6
|
+
t.timestamps
|
|
7
|
+
end
|
|
8
|
+
add_index :esa_charts, [:name]
|
|
9
|
+
|
|
10
|
+
create_table :esa_accounts do |t|
|
|
11
|
+
t.string :code
|
|
12
|
+
t.string :name
|
|
13
|
+
t.string :type
|
|
14
|
+
t.boolean :contra
|
|
15
|
+
t.string :normal_balance
|
|
16
|
+
t.references :chart
|
|
17
|
+
|
|
18
|
+
t.timestamps
|
|
19
|
+
end
|
|
20
|
+
add_index :esa_accounts, [:name, :type, :chart_id]
|
|
21
|
+
add_index :esa_accounts, :normal_balance
|
|
22
|
+
|
|
23
|
+
create_table :esa_events, :force => true do |t|
|
|
24
|
+
t.string :type
|
|
25
|
+
t.datetime :time
|
|
26
|
+
t.string :nature
|
|
27
|
+
t.boolean :processed
|
|
28
|
+
t.references :accountable, :polymorphic => true
|
|
29
|
+
t.references :ruleset
|
|
30
|
+
|
|
31
|
+
t.timestamps
|
|
32
|
+
end
|
|
33
|
+
add_index :esa_events, :type
|
|
34
|
+
add_index :esa_events, :time
|
|
35
|
+
add_index :esa_events, :nature
|
|
36
|
+
add_index :esa_events, [:accountable_id, :accountable_type], :name => "index_accountable_on_events"
|
|
37
|
+
add_index :esa_events, :ruleset_id
|
|
38
|
+
add_index :esa_events, [:time, :nature, :accountable_id, :accountable_type], :unique => true, :name => "unique_contents_on_events"
|
|
39
|
+
|
|
40
|
+
create_table :esa_flags, :force => true do |t|
|
|
41
|
+
t.string :type
|
|
42
|
+
t.datetime :time
|
|
43
|
+
t.string :nature
|
|
44
|
+
t.boolean :state
|
|
45
|
+
t.integer :transition, :limit => 1
|
|
46
|
+
t.boolean :processed
|
|
47
|
+
t.boolean :adjusted
|
|
48
|
+
t.datetime :adjustment_time
|
|
49
|
+
t.references :accountable, :polymorphic => true
|
|
50
|
+
t.references :event
|
|
51
|
+
t.references :ruleset
|
|
52
|
+
|
|
53
|
+
t.timestamps
|
|
54
|
+
end
|
|
55
|
+
add_index :esa_flags, :type
|
|
56
|
+
add_index :esa_flags, :time
|
|
57
|
+
add_index :esa_flags, :nature
|
|
58
|
+
add_index :esa_flags, [:accountable_id, :accountable_type], :name => "index_accountable_on_flags"
|
|
59
|
+
add_index :esa_flags, :event_id
|
|
60
|
+
add_index :esa_flags, :ruleset_id
|
|
61
|
+
add_index :esa_flags, [:time, :nature, :accountable_id, :accountable_type], :unique => true, :name => "unique_contents_on_flags"
|
|
62
|
+
|
|
63
|
+
create_table :esa_transactions do |t|
|
|
64
|
+
t.string :type
|
|
65
|
+
t.datetime :time
|
|
66
|
+
t.string :description
|
|
67
|
+
t.references :accountable, :polymorphic => true
|
|
68
|
+
t.references :flag
|
|
69
|
+
|
|
70
|
+
t.timestamps
|
|
71
|
+
end
|
|
72
|
+
add_index :esa_transactions, [:accountable_id, :accountable_type], :name => "index_accountable_on_transactions"
|
|
73
|
+
add_index :esa_transactions, [:time, :description, :accountable_id, :accountable_type], :unique => true, :name => "unique_contents_on_transactions"
|
|
74
|
+
|
|
75
|
+
create_table :esa_rulesets, :force => true do |t|
|
|
76
|
+
t.string :type
|
|
77
|
+
t.string :name
|
|
78
|
+
t.references :chart
|
|
79
|
+
|
|
80
|
+
t.timestamps
|
|
81
|
+
end
|
|
82
|
+
add_index :esa_rulesets, :type
|
|
83
|
+
add_index :esa_rulesets, :chart_id
|
|
84
|
+
|
|
85
|
+
create_table :esa_amounts do |t|
|
|
86
|
+
t.string :type
|
|
87
|
+
t.references :account
|
|
88
|
+
t.references :transaction
|
|
89
|
+
t.decimal :amount, :precision => 20, :scale => 10
|
|
90
|
+
|
|
91
|
+
t.timestamps
|
|
92
|
+
end
|
|
93
|
+
add_index :esa_amounts, :type
|
|
94
|
+
add_index :esa_amounts, [:account_id, :transaction_id]
|
|
95
|
+
add_index :esa_amounts, [:transaction_id, :account_id]
|
|
96
|
+
add_index :esa_amounts, [:type, :account_id, :transaction_id, :amount], :unique => true, :name => "unique_contents_on_amounts"
|
|
97
|
+
|
|
98
|
+
create_table :esa_contexts do |t|
|
|
99
|
+
t.string :type
|
|
100
|
+
t.string :name
|
|
101
|
+
t.references :chart
|
|
102
|
+
t.references :parent
|
|
103
|
+
t.references :account
|
|
104
|
+
t.references :accountable, :polymorphic => true
|
|
105
|
+
t.string :namespace
|
|
106
|
+
t.integer :position
|
|
107
|
+
t.date :start_date
|
|
108
|
+
t.date :end_date
|
|
109
|
+
|
|
110
|
+
t.datetime :freshness
|
|
111
|
+
t.decimal :event_count, :precision => 16
|
|
112
|
+
t.decimal :flag_count, :precision => 16
|
|
113
|
+
t.decimal :transaction_count, :precision => 16
|
|
114
|
+
t.decimal :amount_count, :precision => 16
|
|
115
|
+
t.decimal :debits_total, :precision => 20, :scale => 10
|
|
116
|
+
t.decimal :credits_total, :precision => 20, :scale => 10
|
|
117
|
+
t.decimal :opening_balance, :precision => 20, :scale => 10
|
|
118
|
+
t.decimal :closing_balance, :precision => 20, :scale => 10
|
|
119
|
+
|
|
120
|
+
t.timestamps
|
|
121
|
+
end
|
|
122
|
+
add_index :esa_contexts, [:type, :chart_id]
|
|
123
|
+
add_index :esa_contexts, :parent_id
|
|
124
|
+
add_index :esa_contexts, :account_id
|
|
125
|
+
add_index :esa_contexts, [:accountable_id, :accountable_type], :name => "index_accountable_on_contexts"
|
|
126
|
+
add_index :esa_contexts, :namespace
|
|
127
|
+
add_index :esa_contexts, :start_date
|
|
128
|
+
add_index :esa_contexts, :end_date
|
|
129
|
+
add_index :esa_contexts, :freshness
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def self.down
|
|
133
|
+
drop_table :esa_charts
|
|
134
|
+
drop_table :esa_accounts
|
|
135
|
+
drop_table :esa_events
|
|
136
|
+
drop_table :esa_flags
|
|
137
|
+
drop_table :esa_transactions
|
|
138
|
+
drop_table :esa_rulesets
|
|
139
|
+
drop_table :esa_amounts
|
|
140
|
+
drop_table :esa_contexts
|
|
141
|
+
end
|
|
142
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
FactoryGirl.define do
|
|
2
|
+
factory :account, :class => ESA::Account do |account|
|
|
3
|
+
account.code
|
|
4
|
+
account.name
|
|
5
|
+
account.contra false
|
|
6
|
+
account.association :chart, :factory => :chart
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
factory :asset, :class => ESA::Accounts::Asset do |account|
|
|
10
|
+
account.code
|
|
11
|
+
account.name
|
|
12
|
+
account.contra false
|
|
13
|
+
account.association :chart, :factory => :chart
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
factory :equity, :class => ESA::Accounts::Equity do |account|
|
|
17
|
+
account.code
|
|
18
|
+
account.name
|
|
19
|
+
account.contra false
|
|
20
|
+
account.association :chart, :factory => :chart
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
factory :expense, :class => ESA::Accounts::Expense do |account|
|
|
24
|
+
account.code
|
|
25
|
+
account.name
|
|
26
|
+
account.contra false
|
|
27
|
+
account.association :chart, :factory => :chart
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
factory :liability, :class => ESA::Accounts::Liability do |account|
|
|
31
|
+
account.code
|
|
32
|
+
account.name
|
|
33
|
+
account.contra false
|
|
34
|
+
account.association :chart, :factory => :chart
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
factory :revenue, :class => ESA::Accounts::Revenue do |account|
|
|
38
|
+
account.code
|
|
39
|
+
account.name
|
|
40
|
+
account.contra false
|
|
41
|
+
account.association :chart, :factory => :chart
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
sequence :name do |n|
|
|
45
|
+
"Factory Name #{n}"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
sequence :code do |n|
|
|
49
|
+
"#{n}"
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
FactoryGirl.define do
|
|
2
|
+
factory :amount, :class => ESA::Amount do |amount|
|
|
3
|
+
amount.amount BigDecimal.new('473')
|
|
4
|
+
amount.association :transaction, :factory => :transaction_with_credit_and_debit
|
|
5
|
+
amount.association :account, :factory => :asset
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
factory :credit_amount, :class => ESA::Amounts::Credit do |credit_amount|
|
|
9
|
+
credit_amount.amount BigDecimal.new('473')
|
|
10
|
+
credit_amount.association :transaction, :factory => :transaction_with_credit_and_debit
|
|
11
|
+
credit_amount.association :account, :factory => :revenue
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
factory :debit_amount, :class => ESA::Amounts::Debit do |debit_amount|
|
|
15
|
+
debit_amount.amount BigDecimal.new('473')
|
|
16
|
+
debit_amount.association :transaction, :factory => :transaction_with_credit_and_debit
|
|
17
|
+
debit_amount.association :account, :factory => :asset
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
FactoryGirl.define do
|
|
2
|
+
factory :transaction, :class => ESA::Transaction do |transaction|
|
|
3
|
+
transaction.description 'factory description'
|
|
4
|
+
factory :transaction_with_credit_and_debit, :class => ESA::Transaction do |transaction_cd|
|
|
5
|
+
transaction_cd.after_build do |t|
|
|
6
|
+
t.amounts << FactoryGirl.build(:credit_amount, :transaction => t)
|
|
7
|
+
t.amounts << FactoryGirl.build(:debit_amount, :transaction => t)
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
File without changes
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
module ESA
|
|
4
|
+
describe Account do
|
|
5
|
+
let(:account) { FactoryGirl.build(:account) }
|
|
6
|
+
subject { account }
|
|
7
|
+
|
|
8
|
+
# must construct a child type instead
|
|
9
|
+
it { should_not be_valid }
|
|
10
|
+
|
|
11
|
+
# must respond to normal_balance, but always answer "none"
|
|
12
|
+
it { should respond_to(:normal_balance) }
|
|
13
|
+
its(:normal_balance) { should be_kind_of(Enumerize::Value) }
|
|
14
|
+
its(:normal_balance) { should eq("none") }
|
|
15
|
+
|
|
16
|
+
# must respond to balance, but always answer nil
|
|
17
|
+
it { should respond_to(:balance) }
|
|
18
|
+
its(:balance) { should be_nil }
|
|
19
|
+
|
|
20
|
+
describe "when using a child type" do
|
|
21
|
+
let(:account) { FactoryGirl.create(:account, type: "Finance::Asset") }
|
|
22
|
+
it { should be_valid }
|
|
23
|
+
|
|
24
|
+
it "should be unique per name" do
|
|
25
|
+
conflict = FactoryGirl.build(:account, name: account.name, type: account.type)
|
|
26
|
+
conflict.should_not be_valid
|
|
27
|
+
conflict.errors[:name].should == ["has already been taken"]
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|