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.
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