subledger 0.7.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (85) hide show
  1. data/.gitignore +9 -0
  2. data/LICENSE.txt +12 -0
  3. data/README.md +136 -0
  4. data/Rakefile +5 -0
  5. data/bin/subledger +6 -0
  6. data/lib/subledger/actor.rb +32 -0
  7. data/lib/subledger/collection_name.rb +25 -0
  8. data/lib/subledger/domain/account.rb +168 -0
  9. data/lib/subledger/domain/balance.rb +102 -0
  10. data/lib/subledger/domain/book.rb +111 -0
  11. data/lib/subledger/domain/category.rb +157 -0
  12. data/lib/subledger/domain/control.rb +180 -0
  13. data/lib/subledger/domain/formatters.rb +31 -0
  14. data/lib/subledger/domain/identity.rb +159 -0
  15. data/lib/subledger/domain/journal_entry.rb +293 -0
  16. data/lib/subledger/domain/key.rb +113 -0
  17. data/lib/subledger/domain/line.rb +272 -0
  18. data/lib/subledger/domain/org.rb +110 -0
  19. data/lib/subledger/domain/report.rb +247 -0
  20. data/lib/subledger/domain/report_rendering.rb +233 -0
  21. data/lib/subledger/domain/roles/activatable.rb +11 -0
  22. data/lib/subledger/domain/roles/archivable.rb +11 -0
  23. data/lib/subledger/domain/roles/attributable.rb +14 -0
  24. data/lib/subledger/domain/roles/collectable.rb +175 -0
  25. data/lib/subledger/domain/roles/creatable.rb +58 -0
  26. data/lib/subledger/domain/roles/describable.rb +33 -0
  27. data/lib/subledger/domain/roles/describable_report_rendering.rb +50 -0
  28. data/lib/subledger/domain/roles/identifiable.rb +15 -0
  29. data/lib/subledger/domain/roles/postable.rb +54 -0
  30. data/lib/subledger/domain/roles/progressable.rb +11 -0
  31. data/lib/subledger/domain/roles/readable.rb +34 -0
  32. data/lib/subledger/domain/roles/restable.rb +69 -0
  33. data/lib/subledger/domain/roles/storable.rb +30 -0
  34. data/lib/subledger/domain/roles/timeable.rb +18 -0
  35. data/lib/subledger/domain/roles/updatable.rb +35 -0
  36. data/lib/subledger/domain/roles/versionable.rb +35 -0
  37. data/lib/subledger/domain/roles.rb +16 -0
  38. data/lib/subledger/domain/value/credit.rb +16 -0
  39. data/lib/subledger/domain/value/debit.rb +16 -0
  40. data/lib/subledger/domain/value/zero.rb +24 -0
  41. data/lib/subledger/domain/value.rb +111 -0
  42. data/lib/subledger/domain.rb +95 -0
  43. data/lib/subledger/exception_handler.rb +65 -0
  44. data/lib/subledger/interface/client.rb +295 -0
  45. data/lib/subledger/interface/dispatcher.rb +20 -0
  46. data/lib/subledger/interface.rb +2 -0
  47. data/lib/subledger/path.rb +106 -0
  48. data/lib/subledger/rest.rb +128 -0
  49. data/lib/subledger/server.rb +3 -0
  50. data/lib/subledger/store/api/errors.rb +95 -0
  51. data/lib/subledger/store/api/roles/activate.rb +21 -0
  52. data/lib/subledger/store/api/roles/archive.rb +21 -0
  53. data/lib/subledger/store/api/roles/balance.rb +39 -0
  54. data/lib/subledger/store/api/roles/categories.rb +51 -0
  55. data/lib/subledger/store/api/roles/collect.rb +58 -0
  56. data/lib/subledger/store/api/roles/create.rb +26 -0
  57. data/lib/subledger/store/api/roles/create_and_post.rb +35 -0
  58. data/lib/subledger/store/api/roles/create_identity.rb +39 -0
  59. data/lib/subledger/store/api/roles/create_line.rb +24 -0
  60. data/lib/subledger/store/api/roles/first_and_last_line.rb +31 -0
  61. data/lib/subledger/store/api/roles/post.rb +25 -0
  62. data/lib/subledger/store/api/roles/progress.rb +21 -0
  63. data/lib/subledger/store/api/roles/read.rb +19 -0
  64. data/lib/subledger/store/api/roles/reports.rb +77 -0
  65. data/lib/subledger/store/api/roles/update.rb +24 -0
  66. data/lib/subledger/store/api/store.rb +103 -0
  67. data/lib/subledger/store/api.rb +20 -0
  68. data/lib/subledger/store.rb +236 -0
  69. data/lib/subledger/supervisor.rb +21 -0
  70. data/lib/subledger/uuid.rb +52 -0
  71. data/lib/subledger/version.rb +4 -0
  72. data/lib/subledger.rb +234 -0
  73. data/spec/spec_helper.rb +77 -0
  74. data/spec/subledger_account_spec.rb +354 -0
  75. data/spec/subledger_book_spec.rb +130 -0
  76. data/spec/subledger_category_spec.rb +203 -0
  77. data/spec/subledger_control_spec.rb +43 -0
  78. data/spec/subledger_identity_spec.rb +47 -0
  79. data/spec/subledger_journal_entry_spec.rb +417 -0
  80. data/spec/subledger_key_spec.rb +43 -0
  81. data/spec/subledger_org_spec.rb +68 -0
  82. data/spec/subledger_report_spec.rb +295 -0
  83. data/spec/subledger_spec.rb +101 -0
  84. data/subledger.gemspec +52 -0
  85. metadata +205 -0
@@ -0,0 +1,175 @@
1
+ module Subledger
2
+ module Domain
3
+ module Roles
4
+ module Collectable
5
+ def self.included base
6
+ base.extend CollectableClass
7
+ end
8
+
9
+ module CollectableClass
10
+
11
+ def collect args, &block
12
+ limit = args[:limit] ||= 25 unless block_given?
13
+
14
+ validate args
15
+
16
+ store = args[:store]
17
+ client = args[:client]
18
+ anchor = args[:anchor]
19
+ action = args[:action]
20
+ collection_name = CollectionName.with_state( args.
21
+ merge :klass => self )
22
+
23
+ args[:collection_name] = collection_name
24
+
25
+ unless anchor
26
+ anchor = args[:anchor] = client.send collection_name, args
27
+
28
+ anchor.read if [:preceding, :following].include? action
29
+ end
30
+
31
+ validate_criterion args
32
+
33
+ if block_given?
34
+ collect_with_block args, &block
35
+ else
36
+ store.collect args
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ ACTION_MAP = { :before => :preceding,
43
+ :ending => :preceding,
44
+ :starting => :following,
45
+ :after => :following,
46
+ :preceding => :preceding,
47
+ :following => :following }
48
+
49
+ MAX_LIMIT = 100
50
+
51
+ def collect_with_block args
52
+ store = args[:store]
53
+ anchor = args[:anchor]
54
+ action = args[:action]
55
+ limit = args[:limit]
56
+
57
+ next_action = ACTION_MAP[ action ]
58
+
59
+ total = 0
60
+
61
+ store_limit = first_limit limit
62
+
63
+ while not ( finished_collecting? limit, total ) and
64
+ not ( collected = store.collect(
65
+ args.merge!(
66
+ :anchor => anchor,
67
+ :action => action,
68
+ :limit => store_limit ) ) ).empty?
69
+
70
+ collected.each do |item|
71
+ yield item
72
+
73
+ total += 1
74
+ end
75
+
76
+ action = next_action
77
+ anchor = collected.last
78
+ store_limit = next_limit limit, total
79
+ end
80
+ end
81
+
82
+ def finished_collecting? limit, total
83
+ not limit.nil? and total >= limit
84
+ end
85
+
86
+ def first_limit limit
87
+ ( limit.nil? or limit > MAX_LIMIT ) ? MAX_LIMIT : limit
88
+ end
89
+
90
+ def next_limit limit, total
91
+ limit.nil? ? MAX_LIMIT : calculated_limit( limit, total )
92
+ end
93
+
94
+ def calculated_limit limit,total
95
+ new_limit = limit - total
96
+
97
+ new_limit < MAX_LIMIT ? new_limit : MAX_LIMIT
98
+ end
99
+
100
+ def validate args
101
+ validate_action args
102
+ validate_limit args
103
+ validate_state args
104
+ end
105
+
106
+ ACTIONS = [ :before,
107
+ :ending,
108
+ :starting,
109
+ :after,
110
+ :preceding,
111
+ :following ]
112
+
113
+ def validate_action args
114
+ unless ACTIONS.include? args[:action]
115
+ raise CollectableError, ':action must be :before, :ending, :starting, :after, :preceding, or :following'
116
+ end
117
+ end
118
+
119
+ def validate_limit args
120
+ limit = args[:limit]
121
+
122
+ if not limit.nil? and limit < 1
123
+ raise CollectableError, ':limit must be greater than zero or nil'
124
+ end
125
+ end
126
+
127
+ def validate_state args
128
+ state = args[:state]
129
+
130
+ if state.nil?
131
+ raise CollectableError, ':state is required'
132
+ end
133
+ end
134
+
135
+ COLLECTABLE_BY = { :building_report_renderings => :rendered_at,
136
+ :completed_report_renderings => :rendered_at,
137
+
138
+ :archived_accounts => :description,
139
+ :active_accounts => :description,
140
+ :archived_books => :description,
141
+ :active_books => :description,
142
+ :archived_categories => :description,
143
+ :active_categories => :description,
144
+ :archived_reports => :description,
145
+ :active_reports => :description,
146
+
147
+ :account_lines => :effective_at,
148
+ :active_journal_entries => :effective_at,
149
+ :archived_journal_entries => :effective_at,
150
+ :posting_journal_entries => :effective_at,
151
+ :posted_journal_entries => :effective_at,
152
+
153
+ :archived_lines => :order,
154
+ :active_lines => :order,
155
+ :posted_lines => :order,
156
+
157
+ :archived_controls => :id,
158
+ :active_controls => :id,
159
+ :archived_keys => :id,
160
+ :active_keys => :id }
161
+
162
+ def validate_criterion args
163
+ anchor = args[ :anchor ]
164
+ collection_name = args[ :collection_name ]
165
+ required_field = COLLECTABLE_BY[ collection_name ]
166
+
167
+ if anchor.send( required_field ).nil?
168
+ raise CollectableError, "#{required_field.inspect} is required"
169
+ end
170
+ end
171
+ end
172
+ end
173
+ end
174
+ end
175
+ end
@@ -0,0 +1,58 @@
1
+ module Subledger
2
+ module Domain
3
+ module Roles
4
+ module Creatable
5
+ def self.included base
6
+ base.extend CreatableClass
7
+ end
8
+
9
+ module CreatableClass
10
+ def create args
11
+ validate_creatability args
12
+
13
+ creatable = args[:client].send active_klass.collection_name, args
14
+
15
+ args[:store].create creatable
16
+ end
17
+
18
+ def new_or_create args
19
+ id = args[:id]
20
+
21
+ if id.nil?
22
+ item = active_klass.create args
23
+
24
+ yield item
25
+
26
+ item
27
+ else
28
+ active_klass.new args
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def validate_creatability args
35
+ raise_unless_creatable args
36
+ validate_creatability_modules args
37
+ end
38
+
39
+ def validate_creatability_modules args
40
+ self.included_modules.each do |mod|
41
+ if mod.respond_to? :raise_unless_creatable
42
+ mod.raise_unless_creatable args
43
+ end
44
+ end
45
+ end
46
+ end
47
+
48
+ def create
49
+ created = self.class.create self.attributes
50
+
51
+ initialize created.attributes
52
+
53
+ self
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,33 @@
1
+ module Subledger
2
+ module Domain
3
+ module Roles
4
+ module Describable
5
+ attr_accessor :description, :reference
6
+
7
+ def self.raise_unless_creatable args
8
+ description = args[:description]
9
+
10
+ if description.nil? or not description.kind_of? String
11
+ raise DescribableError, ':description is required and must be a String'
12
+ end
13
+
14
+ reference = args[:reference]
15
+
16
+ if not reference.nil? and reference !~ URI.regexp
17
+ raise DescribableError, ':reference must be a URL'
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def describable args
24
+ description = args[:description]
25
+ @description = description.nil? ? nil : description.encode('UTF-8')
26
+
27
+ reference = args[:reference]
28
+ @reference = reference.nil? ? nil : reference.encode('UTF-8')
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,50 @@
1
+ module Subledger
2
+ module Domain
3
+ module Roles
4
+ module DescribableReportRendering
5
+ attr_accessor :description, :reference
6
+
7
+ def self.raise_unless_creatable args
8
+ report = args[:report]
9
+
10
+ if report.nil? or not report.kind_of? Report
11
+ raise DescribableReportRenderingError, ':report is required and must be a Report'
12
+ end
13
+
14
+ description = report.description
15
+
16
+ if description.nil? or not description.kind_of? String
17
+ raise DescribableReportRenderingError, ':report :description is required and must be a String'
18
+ end
19
+
20
+ reference = report.reference
21
+
22
+ if not reference.nil? and reference !~ URI.regexp
23
+ raise DescribableReportRenderingError, ':report :reference must be a URL'
24
+ end
25
+
26
+ end
27
+
28
+ private
29
+
30
+ def describable_report_rendering args
31
+ description = args[:description]
32
+ reference = args[:reference]
33
+ report = args[:report]
34
+
35
+ if description.nil?
36
+ @description = report.nil? ? nil : report.description
37
+ else
38
+ @description = description.encode('UTF-8')
39
+ end
40
+
41
+ if reference.nil?
42
+ @reference = report.nil? ? nil : report.reference
43
+ else
44
+ @reference = reference.encode('UTF-8')
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,15 @@
1
+ module Subledger
2
+ module Domain
3
+ module Roles
4
+ module Identifiable
5
+ attr_reader :id
6
+
7
+ private
8
+
9
+ def identifiable args
10
+ @id = args[:id]
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,54 @@
1
+ module Subledger
2
+ module Domain
3
+ module Roles
4
+ module Postable
5
+ def post
6
+ validate_postability
7
+
8
+ from_db = read
9
+
10
+ from_store = store.post self
11
+
12
+ if from_db.kind_of? posted_klass
13
+ from_db
14
+ else
15
+ from_store
16
+ end
17
+ end
18
+
19
+ def posting_klass
20
+ PostingJournalEntry
21
+ end
22
+
23
+ def posted_klass
24
+ PostedJournalEntry
25
+ end
26
+
27
+ private
28
+
29
+ def validate_postability
30
+ case self
31
+ when ArchivedJournalEntry
32
+ raise PostableError, 'Cannot post an archived journal entry'
33
+ when ActiveJournalEntry
34
+
35
+ reason = if lines.length.zero?
36
+ 'Cannot post a journal entry with no lines'
37
+ elsif not balanced?
38
+ 'Cannot post an unbalanced journal entry'
39
+ end
40
+
41
+ unless reason.nil?
42
+ set_reason reason
43
+ raise PostableError, reason
44
+ end
45
+ when PostingJournalEntry
46
+ # All is fine
47
+ when PostedJournalEntry
48
+ # All is fine
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,11 @@
1
+ module Subledger
2
+ module Domain
3
+ module Roles
4
+ module Progressable
5
+ def progress
6
+ store.progress self
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,34 @@
1
+ module Subledger
2
+ module Domain
3
+ module Roles
4
+ module Readable
5
+ def self.included base
6
+ base.extend ReadableClass
7
+ end
8
+
9
+ module ReadableClass
10
+ def read args
11
+ unless args[:id]
12
+ raise ReadableError, 'Cannot read without :id'
13
+ end
14
+
15
+ readable = args[:client].send collection_name, args
16
+
17
+ args[:store].read readable
18
+ end
19
+ end
20
+
21
+ def read
22
+ readable = self.class.read attributes
23
+
24
+ if self.class == readable.class
25
+ initialize readable.attributes
26
+ self
27
+ else
28
+ readable
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,69 @@
1
+ module Subledger
2
+ module Domain
3
+ module Roles
4
+ module Restable
5
+ def post_hash
6
+ hash = { }
7
+
8
+ self.class.post_keys.each do |key|
9
+ value = self.send key
10
+
11
+ hash[key] = translate( key, value ) unless exclude? value
12
+ end
13
+
14
+ hash
15
+ end
16
+
17
+ def patch_hash
18
+ hash = { }
19
+
20
+ self.class.patch_keys.each do |key|
21
+ value = self.send key
22
+
23
+ hash[key] = translate( key, value ) unless exclude? value
24
+ end
25
+
26
+ hash
27
+ end
28
+
29
+ def serializable_hash
30
+ entity = self.class::Entity.new self
31
+
32
+ hash = entity.serializable_hash
33
+
34
+ hash.each do |key, value|
35
+ hash.delete( key ) if exclude? value
36
+ end
37
+
38
+ hash
39
+ end
40
+
41
+ def to_json
42
+ MultiJson.dump serializable_hash
43
+ end
44
+
45
+ private
46
+
47
+ # TODO elimate this using serializable hash
48
+ def translate key, value
49
+ case key.to_s
50
+ when /at$/
51
+ value.iso8601(3)
52
+ when 'org', 'book', 'account', 'journal_entry', 'identity', 'report'
53
+ value.id
54
+ when 'value'
55
+ value.rest_hash
56
+ when 'normal_balance'
57
+ value.type
58
+ else
59
+ value
60
+ end
61
+ end
62
+
63
+ def exclude? value
64
+ value.nil? or ( value.kind_of?( String ) and value.length.zero? )
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,30 @@
1
+ module Subledger
2
+ module Domain
3
+ module Roles
4
+ module Storable
5
+ attr_reader :store, :client
6
+
7
+ def self.raise_unless_creatable args
8
+ store = args[:store]
9
+
10
+ if store.nil? or not store.kind_of? Store
11
+ raise StorableError, ':store is required and must be a Store'
12
+ end
13
+
14
+ client = args[:client]
15
+
16
+ if client.nil? or not client.kind_of? Interface::Client
17
+ raise StorableError, ':client is required and must be a Client'
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def storable args
24
+ @store = args[:store]
25
+ @client = args[:client]
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,18 @@
1
+ module Subledger
2
+ module Domain
3
+ module Roles
4
+ module Timeable
5
+
6
+ private
7
+
8
+ def utc_or_nil time
9
+ time.nil? ? nil : time.utc
10
+ end
11
+
12
+ def utc_or_now time
13
+ time.nil? ? Time.now.utc : time.utc
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,35 @@
1
+ module Subledger
2
+ module Domain
3
+ module Roles
4
+ module Updatable
5
+ def self.included base
6
+ base.extend UpdatableClass
7
+ end
8
+
9
+ module UpdatableClass
10
+ def update args
11
+ client = args[:client]
12
+
13
+ updatable = client.send collection_name, args
14
+
15
+ args[:store].update updatable
16
+ end
17
+ end
18
+
19
+ def update
20
+ updatable = dup
21
+
22
+ updatable.send :increment_version
23
+
24
+ args = updatable.attributes
25
+
26
+ updated = self.class.update args
27
+
28
+ initialize updated.attributes
29
+
30
+ self
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,35 @@
1
+ module Subledger
2
+ module Domain
3
+ module Roles
4
+ module Versionable
5
+ def self.included base
6
+ base.extend VersionableClass
7
+ end
8
+
9
+ module VersionableClass
10
+ def self.raise_unless_creatable args
11
+ version = args[:version]
12
+
13
+ if version.nil? or not version.kind_of? Fixnum
14
+ raise VersionableError, ':version is required and must be a Fixnum'
15
+ end
16
+ end
17
+ end
18
+
19
+ attr_reader :version
20
+
21
+ protected
22
+
23
+ private
24
+
25
+ def versionable args
26
+ @version = args[:version] || 1
27
+ end
28
+
29
+ def increment_version
30
+ @version += 1
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,16 @@
1
+ require 'subledger/domain/roles/activatable'
2
+ require 'subledger/domain/roles/archivable'
3
+ require 'subledger/domain/roles/attributable'
4
+ require 'subledger/domain/roles/collectable'
5
+ require 'subledger/domain/roles/creatable'
6
+ require 'subledger/domain/roles/describable'
7
+ require 'subledger/domain/roles/describable_report_rendering'
8
+ require 'subledger/domain/roles/identifiable'
9
+ require 'subledger/domain/roles/postable'
10
+ require 'subledger/domain/roles/progressable'
11
+ require 'subledger/domain/roles/readable'
12
+ require 'subledger/domain/roles/restable'
13
+ require 'subledger/domain/roles/storable'
14
+ require 'subledger/domain/roles/timeable'
15
+ require 'subledger/domain/roles/updatable'
16
+ require 'subledger/domain/roles/versionable'
@@ -0,0 +1,16 @@
1
+ module Subledger
2
+ module Domain
3
+ class Credit
4
+
5
+ include Value
6
+
7
+ def self.type
8
+ 'credit'
9
+ end
10
+
11
+ def rest_hash
12
+ { 'type' => 'credit', 'amount' => amount_to_s(@amount) }
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ module Subledger
2
+ module Domain
3
+ class Debit
4
+
5
+ include Value
6
+
7
+ def self.type
8
+ 'debit'
9
+ end
10
+
11
+ def rest_hash
12
+ { 'type' => 'debit', 'amount' => amount_to_s(@amount) }
13
+ end
14
+ end
15
+ end
16
+ end