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