event_sourced_accounting 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (87) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +23 -0
  3. data/README.markdown +37 -0
  4. data/Rakefile +11 -0
  5. data/app/assets/javascripts/esa/application.js +15 -0
  6. data/app/assets/stylesheets/esa/application.css +13 -0
  7. data/app/assets/stylesheets/esa/main.css.scss +86 -0
  8. data/app/models/esa/account.rb +80 -0
  9. data/app/models/esa/accounts/asset.rb +22 -0
  10. data/app/models/esa/accounts/equity.rb +22 -0
  11. data/app/models/esa/accounts/expense.rb +22 -0
  12. data/app/models/esa/accounts/liability.rb +22 -0
  13. data/app/models/esa/accounts/revenue.rb +22 -0
  14. data/app/models/esa/amount.rb +27 -0
  15. data/app/models/esa/amounts/credit.rb +12 -0
  16. data/app/models/esa/amounts/debit.rb +12 -0
  17. data/app/models/esa/associations/amounts_extension.rb +79 -0
  18. data/app/models/esa/associations/events_extension.rb +33 -0
  19. data/app/models/esa/associations/flags_extension.rb +35 -0
  20. data/app/models/esa/associations/transactions_extension.rb +7 -0
  21. data/app/models/esa/chart.rb +41 -0
  22. data/app/models/esa/context.rb +218 -0
  23. data/app/models/esa/context_provider.rb +52 -0
  24. data/app/models/esa/context_providers/account_context_provider.rb +21 -0
  25. data/app/models/esa/context_providers/accountable_context_provider.rb +22 -0
  26. data/app/models/esa/context_providers/accountable_type_context_provider.rb +21 -0
  27. data/app/models/esa/context_providers/date_context_provider.rb +33 -0
  28. data/app/models/esa/contexts/account_context.rb +26 -0
  29. data/app/models/esa/contexts/accountable_context.rb +26 -0
  30. data/app/models/esa/contexts/accountable_type_context.rb +24 -0
  31. data/app/models/esa/contexts/created_at_context.rb +26 -0
  32. data/app/models/esa/contexts/date_context.rb +71 -0
  33. data/app/models/esa/contexts/empty_context.rb +19 -0
  34. data/app/models/esa/contexts/filter_context.rb +11 -0
  35. data/app/models/esa/contexts/open_close_context.rb +15 -0
  36. data/app/models/esa/event.rb +33 -0
  37. data/app/models/esa/filters/account_filter.rb +42 -0
  38. data/app/models/esa/filters/accountable_filter.rb +58 -0
  39. data/app/models/esa/filters/accountable_type_filter.rb +26 -0
  40. data/app/models/esa/filters/chart_filter.rb +17 -0
  41. data/app/models/esa/filters/context_filter.rb +35 -0
  42. data/app/models/esa/filters/date_time_filter.rb +52 -0
  43. data/app/models/esa/flag.rb +70 -0
  44. data/app/models/esa/ruleset.rb +175 -0
  45. data/app/models/esa/traits/accountable.rb +21 -0
  46. data/app/models/esa/traits/extendable.rb +93 -0
  47. data/app/models/esa/traits/or_scope.rb +35 -0
  48. data/app/models/esa/traits/union_scope.rb +24 -0
  49. data/app/models/esa/transaction.rb +74 -0
  50. data/config/backtrace_silencers.rb +7 -0
  51. data/config/database.yml +5 -0
  52. data/config/inflections.rb +10 -0
  53. data/config/mime_types.rb +5 -0
  54. data/config/routes.rb +6 -0
  55. data/config/secret_token.rb +7 -0
  56. data/config/session_store.rb +8 -0
  57. data/lib/esa/balance_checker.rb +17 -0
  58. data/lib/esa/blocking_processor.rb +158 -0
  59. data/lib/esa/config.rb +55 -0
  60. data/lib/esa/subcontext_checker.rb +15 -0
  61. data/lib/esa/version.rb +3 -0
  62. data/lib/esa.rb +8 -0
  63. data/lib/generators/esa/USAGE +11 -0
  64. data/lib/generators/esa/esa_generator.rb +26 -0
  65. data/lib/generators/esa/templates/migration.rb +142 -0
  66. data/spec/factories/account_factory.rb +51 -0
  67. data/spec/factories/amount_factory.rb +19 -0
  68. data/spec/factories/chart_factory.rb +7 -0
  69. data/spec/factories/transaction_factory.rb +11 -0
  70. data/spec/lib/esa_spec.rb +0 -0
  71. data/spec/models/account_spec.rb +31 -0
  72. data/spec/models/amount_spec.rb +8 -0
  73. data/spec/models/asset_spec.rb +9 -0
  74. data/spec/models/chart_spec.rb +52 -0
  75. data/spec/models/credit_amount_spec.rb +9 -0
  76. data/spec/models/debit_amount_spec.rb +9 -0
  77. data/spec/models/equity_spec.rb +9 -0
  78. data/spec/models/expense_spec.rb +9 -0
  79. data/spec/models/liability_spec.rb +9 -0
  80. data/spec/models/revenue_spec.rb +9 -0
  81. data/spec/models/transaction_spec.rb +118 -0
  82. data/spec/rcov.opts +2 -0
  83. data/spec/spec.opts +4 -0
  84. data/spec/spec_helper.rb +16 -0
  85. data/spec/support/account_shared_examples.rb +57 -0
  86. data/spec/support/amount_shared_examples.rb +21 -0
  87. 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!
@@ -0,0 +1,5 @@
1
+ # config/database.yml in Rails
2
+ test:
3
+ adapter: sqlite3
4
+ database: ":memory:"
5
+ timeout: 500
@@ -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
@@ -0,0 +1,5 @@
1
+ # Be sure to restart your server when you modify this file.
2
+
3
+ # Add new mime types for use in respond_to blocks:
4
+ # Mime::Type.register "text/richtext", :rtf
5
+ # Mime::Type.register_alias "text/html", :iphone
data/config/routes.rb ADDED
@@ -0,0 +1,6 @@
1
+ ESA::Engine.routes.draw do
2
+ root :to => "accounts#index"
3
+
4
+ resources :accounts
5
+ resources :transactions
6
+ end
@@ -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
@@ -0,0 +1,3 @@
1
+ module ESA
2
+ VERSION = "0.1.0"
3
+ end
data/lib/esa.rb ADDED
@@ -0,0 +1,8 @@
1
+ # ESA
2
+ require "rails"
3
+ require "enumerize"
4
+ module ESA
5
+ class Engine < Rails::Engine
6
+ isolate_namespace ESA
7
+ end
8
+ end
@@ -0,0 +1,11 @@
1
+ Description:
2
+ Generator for ESA Plugin
3
+
4
+ Example:
5
+ rails g esa
6
+
7
+ This will create:
8
+ Create a migration in the application for the "Account" and Transaction" tables.
9
+
10
+ Once generated, simply run:
11
+ rake db:migrate
@@ -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,7 @@
1
+ FactoryGirl.define do
2
+ factory :chart, :class => ESA::Chart do
3
+ id 1
4
+ name "Chart of Accounts"
5
+ initialize_with { ESA::Chart.find_or_create_by_id(id)}
6
+ end
7
+ 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
@@ -0,0 +1,8 @@
1
+ require 'spec_helper'
2
+
3
+ module ESA
4
+ describe Amount do
5
+ subject { FactoryGirl.build(:amount) }
6
+ it { should_not be_valid } # construct a child class instead
7
+ end
8
+ end
@@ -0,0 +1,9 @@
1
+ require 'spec_helper'
2
+
3
+ module ESA
4
+ module Accounts
5
+ describe Asset do
6
+ it_behaves_like 'a ESA::Account subtype', kind: :asset, normal_balance: :debit
7
+ end
8
+ end
9
+ end