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,110 @@
1
+ module Subledger
2
+ module Domain
3
+ class OrgError < Error; end
4
+
5
+ class Org
6
+
7
+ include Domain
8
+
9
+ include Roles::Attributable
10
+ include Roles::Describable
11
+ include Roles::Identifiable
12
+ include Roles::Storable
13
+ include Roles::Versionable
14
+
15
+ include Roles::Creatable
16
+ include Roles::Readable
17
+ include Roles::Updatable
18
+
19
+ include Roles::Activatable
20
+ include Roles::Archivable
21
+
22
+ include Roles::Restable
23
+
24
+ attr_accessor :bucket_name
25
+
26
+ def self.root_klass
27
+ Org
28
+ end
29
+
30
+ def self.post_keys
31
+ [ :description, :reference, :bucket_name ]
32
+ end
33
+
34
+ def self.patch_keys
35
+ [ :id, :description, :reference, :bucket_name, :version ]
36
+ end
37
+
38
+ def self.sub_klasses
39
+ [ active_klass, archived_klass ]
40
+ end
41
+
42
+ def self.active_klass
43
+ ActiveOrg
44
+ end
45
+
46
+ def self.archived_klass
47
+ ArchivedOrg
48
+ end
49
+
50
+ def self.create args
51
+ org = super
52
+
53
+ args[:store].add_initial_controls_for org
54
+
55
+ org
56
+ end
57
+
58
+ def initialize args
59
+ describable args
60
+ identifiable args
61
+ storable args
62
+ versionable args
63
+
64
+ @bucket_name = args[:bucket_name]
65
+ end
66
+
67
+ class Entity < Grape::Entity
68
+ root 'orgs', 'org'
69
+
70
+ expose :id, :documentation => { :type => 'string', :desc => 'org ID' }
71
+
72
+ expose :description, :documentation => { :type => 'string', :desc => 'description' }
73
+
74
+ expose :reference, :documentation => { :type => 'string', :desc => 'reference URI' }
75
+
76
+ expose :bucket_name, :documentation => { :type => 'string', :desc => 'backup URI' }
77
+
78
+ expose :version, :documentation => { :type => 'integer', :desc => 'version' }
79
+ end
80
+
81
+ private
82
+
83
+ def self.raise_unless_creatable args
84
+ store = args[:store]
85
+
86
+ store.raise_unless_bucket_name_valid args
87
+ end
88
+ end
89
+
90
+ class ArchivedOrg < Org
91
+ class Entity < Org::Entity
92
+ root 'archived_orgs', 'archived_org'
93
+ end
94
+
95
+ def self.sub_klasses
96
+ [ archived_klass ]
97
+ end
98
+ end
99
+
100
+ class ActiveOrg < Org
101
+ class Entity < Org::Entity
102
+ root 'active_orgs', 'active_org'
103
+ end
104
+
105
+ def self.sub_klasses
106
+ [ active_klass ]
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,247 @@
1
+ module Subledger
2
+ module Domain
3
+ class ReportError < Error; end
4
+
5
+ class Report
6
+
7
+ include Domain
8
+
9
+ include Roles::Attributable
10
+ include Roles::Describable
11
+ include Roles::Identifiable
12
+ include Roles::Storable
13
+ include Roles::Versionable
14
+
15
+ include Roles::Creatable
16
+ include Roles::Readable
17
+ include Roles::Updatable
18
+
19
+ include Roles::Collectable
20
+
21
+ include Roles::Activatable
22
+ include Roles::Archivable
23
+
24
+ include Roles::Restable
25
+
26
+ attr_reader :book
27
+
28
+ def self.post_keys
29
+ [ :description, :reference ]
30
+ end
31
+
32
+ def self.patch_keys
33
+ [ :id, :description, :reference, :version ]
34
+ end
35
+
36
+ def self.root_klass
37
+ Report
38
+ end
39
+
40
+ def self.sub_klasses
41
+ [ active_klass, archived_klass ]
42
+ end
43
+
44
+ def self.active_klass
45
+ ActiveReport
46
+ end
47
+
48
+ def self.archived_klass
49
+ ArchivedReport
50
+ end
51
+
52
+ def initialize args
53
+ describable args
54
+ identifiable args
55
+ storable args
56
+ versionable args
57
+
58
+ @book = args[:book]
59
+ end
60
+
61
+ class Entity < Grape::Entity
62
+ root 'reports', 'report'
63
+
64
+ expose :id, :documentation => { :type => 'string', :desc => 'report ID' }
65
+
66
+ expose :book, :format_with => :id,
67
+ :documentation => { :type => 'string', :desc => 'book ID' }
68
+
69
+ expose :description, :documentation => { :type => 'string', :desc => 'description' }
70
+
71
+ expose :reference, :documentation => { :type => 'string', :desc => 'reference URI' }
72
+
73
+ expose :version, :documentation => { :type => 'integer', :desc => 'version' }
74
+ end
75
+
76
+ private
77
+
78
+ def self.raise_unless_creatable args
79
+
80
+ book = args[:book]
81
+
82
+ if book.nil? or not book.kind_of? Book
83
+ raise BookError, ':book is required and must be a Book'
84
+ elsif UUID.invalid? book.id
85
+ raise BookError, ':book must have a valid :id'
86
+ end
87
+ end
88
+
89
+ def structure
90
+ store.report_structure_for self
91
+ end
92
+ end
93
+
94
+ class ArchivedReport < Report
95
+ class Entity < Report::Entity
96
+ root 'archived_reports', 'archived_report'
97
+ end
98
+
99
+ def self.sub_klasses
100
+ [ archived_klass ]
101
+ end
102
+ end
103
+
104
+ class ActiveReport < Report
105
+ class Entity < Report::Entity
106
+ root 'active_reports', 'active_report'
107
+ end
108
+
109
+ def self.sub_klasses
110
+ [ active_klass ]
111
+ end
112
+
113
+ def categories &block
114
+ begin
115
+ store.collect_categories_for_report self, &block
116
+ rescue Store::CollectError => e
117
+ raise ReportError, e
118
+ end
119
+ end
120
+
121
+ def attach args
122
+ validate_attach args
123
+
124
+ category = args[:category]
125
+ parent = args[:parent]
126
+
127
+ if category.nil? or category.id.nil?
128
+ raise ReportError, ':category is required and must have an id'
129
+ end
130
+
131
+ if not parent.nil? and parent.id.nil?
132
+ raise ReportError, ':parent must have an id'
133
+ end
134
+
135
+ begin
136
+ store.attach_category_to_report :report => self,
137
+ :category => category,
138
+ :parent => parent
139
+ rescue Store::AttachError => e
140
+ raise ReportError, e
141
+ end
142
+
143
+ category
144
+ end
145
+
146
+ def detach args
147
+ category = args[:category]
148
+
149
+ if category.nil? or category.id.nil?
150
+ raise ReportError, ':category is required and must have an id'
151
+ end
152
+
153
+ begin
154
+ store.detach_category_from_report :report => self,
155
+ :category => category
156
+ rescue Store::DetachError => e
157
+ raise ReportError, e
158
+ end
159
+
160
+ category
161
+ end
162
+
163
+ def render args
164
+ at = args[:at]
165
+
166
+ if at.nil? or not at.kind_of? Time
167
+ raise ReportError, ':at is required and must be a Time'
168
+ end
169
+
170
+ building_report_rendering = client.building_report_rendering :effective_at => at,
171
+ :report => self
172
+
173
+ store.render args.merge! :building_report_rendering => building_report_rendering
174
+ end
175
+
176
+ private
177
+
178
+ def add_categories_to report_hash, sub_structure, effective_at
179
+ categories_hash = report_hash[:categories] = { }
180
+
181
+ categories_balance = Balance.new
182
+
183
+ sub_structure.each do |category, category_hash|
184
+ categories_hash[category.id] = category.serializable_hash
185
+
186
+ category_balance = add_categories_to categories_hash[category.id], category_hash, effective_at
187
+
188
+ category_balance += add_accounts_to categories_hash[category.id], category, effective_at
189
+
190
+ categories_hash[category.id][:balance] = category_balance.serializable_hash
191
+
192
+ categories_balance += category_balance
193
+ end
194
+
195
+ categories_balance
196
+ end
197
+
198
+ def add_accounts_to report_category_hash, category, effective_at
199
+ accounts_hash = report_category_hash[:accounts] = { }
200
+
201
+ accounts_balance = Balance.new
202
+
203
+ category.accounts.each do |account|
204
+ accounts_hash[account.id] = account.serializable_hash
205
+
206
+ account_balance = add_balance_to accounts_hash[account.id], account, effective_at
207
+
208
+ accounts_balance += account_balance
209
+ end
210
+
211
+ accounts_balance
212
+ end
213
+
214
+ def add_balance_to report_category_account_hash, account, effective_at
215
+ balance = account.balance :at => effective_at
216
+
217
+ report_category_account_hash[:balance] = balance.serializable_hash
218
+
219
+ balance
220
+ end
221
+
222
+ def validate_attach args
223
+ if UUID.invalid? id
224
+ raise ReportError, ':report must have a valid :id'
225
+ end
226
+
227
+ category = args[:category]
228
+
229
+ if category.nil? or not category.kind_of? Category
230
+ raise ReportError, ':category is required and must be a Category'
231
+ elsif UUID.invalid? category.id
232
+ raise ReportError, ':category must have a valid :id'
233
+ end
234
+
235
+ parent = args[:parent]
236
+
237
+ unless parent.nil?
238
+ if not parent.kind_of? Category
239
+ raise ReportError, ':parent must be a Category'
240
+ elsif UUID.invalid? parent.id
241
+ raise ReportError, ':parent must have a valid :id'
242
+ end
243
+ end
244
+ end
245
+ end
246
+ end
247
+ end
@@ -0,0 +1,233 @@
1
+ module Subledger
2
+ module Domain
3
+ class ReportRenderingError < Error; end
4
+
5
+ class ReportRendering
6
+
7
+ include Domain
8
+
9
+ include Roles::Attributable
10
+ include Roles::DescribableReportRendering
11
+ include Roles::Identifiable
12
+ include Roles::Storable
13
+ include Roles::Timeable
14
+
15
+ include Roles::Creatable
16
+ include Roles::Readable
17
+
18
+ include Roles::Collectable
19
+
20
+ include Roles::Restable
21
+
22
+ attr_reader :book, :report, :effective_at, :rendered_at,
23
+ :balanced_accounts, :total_accounts
24
+
25
+ def self.post_keys
26
+ [ :description, :reference, :report, :effective_at ]
27
+ end
28
+
29
+ def self.root_klass
30
+ ReportRendering
31
+ end
32
+
33
+ def self.sub_klasses
34
+ [ building_klass, completed_klass ]
35
+ end
36
+
37
+ def self.building_klass
38
+ BuildingReportRendering
39
+ end
40
+
41
+ # TODO create uses active_klass, should use creation_klass
42
+ def self.active_klass
43
+ building_klass
44
+ end
45
+
46
+ def self.completed_klass
47
+ CompletedReportRendering
48
+ end
49
+
50
+ def initialize args
51
+ describable_report_rendering args
52
+ identifiable args
53
+ storable args
54
+
55
+ @book = args[:book]
56
+ @report = args[:report]
57
+ @effective_at = round args[:effective_at]
58
+ @rendered_at = utc_or_now args[:rendered_at]
59
+
60
+ @balanced_accounts = args[:balanced_accounts] || 0
61
+ @total_accounts = args[:total_accounts] || 0
62
+
63
+ specialized_initialization args
64
+ end
65
+
66
+ def progress
67
+ if total_accounts.zero?
68
+ 0
69
+ else
70
+ ( ( balanced_accounts.to_f / total_accounts ) * 99 ).to_i
71
+ end
72
+ end
73
+
74
+ class Entity < Grape::Entity
75
+ root 'report_renderings', 'report_rendering'
76
+
77
+ expose :id
78
+ expose :book, :format_with => :id
79
+ expose :report, :format_with => :id
80
+ expose :description
81
+ expose :reference
82
+ expose :effective_at, :format_with => :time_full_second
83
+ expose :total_accounts, :format_with => :integer
84
+ expose :progress, :format_with => :progress
85
+ expose :rendered_at, :format_with => :time
86
+ end
87
+
88
+ private
89
+
90
+ def self.raise_unless_creatable args
91
+
92
+ book = args[:book]
93
+
94
+ if book.nil? or not book.kind_of? Book
95
+ raise ReportRenderingError, ':book is required and must be a Book'
96
+ elsif UUID.invalid? book.id
97
+ raise ReportRenderingError, ':book must have a valid :id'
98
+ end
99
+
100
+ report = args[:report]
101
+
102
+ if report.nil? or not report.kind_of? Report
103
+ raise ReportRenderingError, ':report is required and must be a Report'
104
+ elsif UUID.invalid? report.id
105
+ raise ReportRenderingError, ':report must have a valid :id'
106
+ end
107
+
108
+ effective_at = args[:effective_at]
109
+
110
+ if effective_at.nil? or not effective_at.kind_of? Time
111
+ raise ReportRenderingError, ':effective_at is required and must be a Time'
112
+ end
113
+ end
114
+
115
+ def round effective_at
116
+ Time.at( utc_or_nil( effective_at ).to_i ).utc
117
+ end
118
+
119
+ TOTAL_ACCOUNTS_SEMAPHORE = Mutex.new
120
+
121
+ def increase_total_accounts_by count
122
+ TOTAL_ACCOUNTS_SEMAPHORE.synchronize do
123
+ @total_accounts += count
124
+ end
125
+ end
126
+
127
+ BALANCED_ACCOUNTS_SEMAPHORE = Mutex.new
128
+
129
+ def increase_balanced_accounts_by count
130
+ BALANCED_ACCOUNTS_SEMAPHORE.synchronize do
131
+ @balanced_accounts += count
132
+ end
133
+ end
134
+
135
+ def specialized_initialization args
136
+ end
137
+ end
138
+
139
+ class BuildingReportRendering < ReportRendering
140
+ class Entity < ReportRendering::Entity
141
+ root 'building_report_renderings', 'building_report_rendering'
142
+ end
143
+
144
+ def self.sub_klasses
145
+ [ building_klass ]
146
+ end
147
+
148
+ # TODO this should probably move to Domain module
149
+
150
+ def to_json
151
+ MultiJson.dump serializable_hash
152
+ end
153
+
154
+ private
155
+
156
+ def complete args
157
+ store.send :become, :becomable => self,
158
+ :klass => self.class.completed_klass,
159
+ :extra_args => args
160
+ end
161
+ end
162
+
163
+ class CompletedReportRendering < ReportRendering
164
+ attr_reader :completed_at, :categories, :balance, :warnings
165
+
166
+ def self.sub_klasses
167
+ [ completed_klass ]
168
+ end
169
+
170
+ def progress
171
+ 100
172
+ end
173
+
174
+ def to_json
175
+ @json ||= if location[0..4] == 's3://'
176
+ ExceptionHandler.new( :name => 'completed_report_rendering to_json' ).with_retry do
177
+ s3_bucket.objects[id].read
178
+ end
179
+ elsif location[0..6] == 'file://'
180
+ File.open( location[7..-1], 'r' ).read
181
+ else
182
+ raise ReportRenderingError, "unknown URL scheme: #{location}"
183
+ end
184
+ end
185
+
186
+ def categories
187
+ @categories ||= as_hash['categories']
188
+ end
189
+
190
+ def balance
191
+ balance_hash = { 'balance' => as_hash['balance'] }
192
+
193
+ @balance ||= Rest.to_balance balance_hash, client
194
+ end
195
+
196
+ def warnings
197
+ @warnings ||= as_hash['warnings']
198
+ end
199
+
200
+ class Entity < ReportRendering::Entity
201
+ root 'completed_report_renderings', 'completed_report_rendering'
202
+
203
+ expose :completed_at, :format_with => :time
204
+ expose :categories
205
+ expose :balance, :format_with => :balance
206
+ expose :warnings
207
+ end
208
+
209
+ private
210
+
211
+ attr_reader :location
212
+
213
+ def s3_bucket
214
+ @s3_bucket ||= Store::Aws.s3_client.buckets[RENDERED_REPORTS_BUCKET]
215
+ end
216
+
217
+ def as_hash
218
+ @hash ||= MultiJson.load to_json
219
+ end
220
+
221
+ def specialized_initialization args
222
+ @completed_at = utc_or_nil args[:completed_at]
223
+ @location = args[:location]
224
+ @json = args[:json]
225
+
226
+ @balance = nil
227
+ @categories = nil
228
+ @hash = nil
229
+ @warnings = nil
230
+ end
231
+ end
232
+ end
233
+ end
@@ -0,0 +1,11 @@
1
+ module Subledger
2
+ module Domain
3
+ module Roles
4
+ module Activatable
5
+ def activate
6
+ store.activate self
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ module Subledger
2
+ module Domain
3
+ module Roles
4
+ module Archivable
5
+ def archive
6
+ store.archive self
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,14 @@
1
+ module Subledger
2
+ module Domain
3
+ module Roles
4
+ module Attributable
5
+ def attributes
6
+ instance_variables.inject( { } ) do | attrs, ivar |
7
+ attrs[ ivar.to_s[1..-1].to_sym ] = instance_variable_get ivar
8
+ attrs
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end