subledger 0.7.7

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