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,293 @@
1
+ module Subledger
2
+ module Domain
3
+ class JournalEntryError < Error; end
4
+
5
+ class JournalEntry
6
+
7
+ include Domain
8
+
9
+ include Roles::Attributable
10
+ include Roles::Describable
11
+ include Roles::Identifiable
12
+ include Roles::Storable
13
+ include Roles::Timeable
14
+ include Roles::Versionable
15
+
16
+ include Roles::Creatable
17
+ include Roles::Readable
18
+ include Roles::Updatable
19
+
20
+ include Roles::Progressable
21
+ include Roles::Collectable
22
+
23
+ include Roles::Restable
24
+
25
+ attr_reader :org, :book, :post_delay
26
+
27
+ attr_accessor :effective_at
28
+
29
+ def self.post_keys
30
+ [ :effective_at, :description, :reference ]
31
+ end
32
+
33
+ def self.patch_keys
34
+ [ :id, :effective_at, :description, :reference, :version ]
35
+ end
36
+
37
+ def self.root_klass
38
+ JournalEntry
39
+ end
40
+
41
+ def self.sub_klasses
42
+ [ active_klass, archived_klass, posting_klass, posted_klass ]
43
+ end
44
+
45
+ def self.active_klass
46
+ ActiveJournalEntry
47
+ end
48
+
49
+ def self.archived_klass
50
+ ArchivedJournalEntry
51
+ end
52
+
53
+ def self.posting_klass
54
+ PostingJournalEntry
55
+ end
56
+
57
+ def self.posted_klass
58
+ PostedJournalEntry
59
+ end
60
+
61
+ def self.create_and_post args
62
+ begin
63
+ client = args[:client]
64
+
65
+ active_journal_entry = client.active_journal_entry args
66
+
67
+ JournalEntry.send :validate_creatability, active_journal_entry.attributes
68
+
69
+ order = 1
70
+
71
+ arg_lines = args[:lines]
72
+
73
+ arg_lines.each do |line_args|
74
+ if line_args[:description].nil?
75
+ line_args.merge! :description => active_journal_entry.description
76
+ end
77
+
78
+ if line_args[:reference].nil?
79
+ line_args.merge! :reference => active_journal_entry.reference
80
+ end
81
+
82
+ line_args.merge! :order => '%07.2f' % order
83
+
84
+ order += 1
85
+ end
86
+
87
+ store = args[:store]
88
+
89
+ active_lines = []
90
+
91
+ arg_lines.each do |line_args|
92
+ line_args.merge! :journal_entry => active_journal_entry
93
+
94
+ line = client.active_line line_args.merge( :id => UUID.as_string )
95
+
96
+ Line.send :raise_unless_create_and_postable, line.attributes
97
+ Line.send :validate_creatability_modules, line.attributes
98
+
99
+ active_lines << line
100
+ end
101
+
102
+ store.create_and_post(
103
+ :active_journal_entry => active_journal_entry,
104
+ :active_lines => active_lines,
105
+ :posting_journal_entry => client.posting_journal_entry( { } ) )
106
+
107
+ rescue Exception => e
108
+ raise JournalEntryError, "Cannot create and post: #{e}"
109
+ end
110
+ end
111
+
112
+ def initialize args
113
+ describable args
114
+ identifiable args
115
+ storable args
116
+ versionable args
117
+
118
+ @org = args[:org]
119
+ @book = args[:book]
120
+ @effective_at = utc_or_nil args[:effective_at]
121
+ @post_delay = args[:post_delay] || 0
122
+
123
+ @reason = args[:reason] if args.has_key? :reason
124
+ end
125
+
126
+ def line args
127
+ client.lines args.merge( :journal_entry => self )
128
+ end
129
+
130
+ def lines args={ }, &block
131
+ args.merge! :action => args[:action] || :starting,
132
+ :state => args[:state] || line_state,
133
+ :order => args[:order] || '0',
134
+ :journal_entry => self
135
+
136
+ client.lines.collect args, &block
137
+ end
138
+
139
+ def balance
140
+ store.journal_entry_balance :store => store,
141
+ :client => client,
142
+ :journal_entry => self,
143
+ :state => line_state
144
+ end
145
+
146
+ def balanced?
147
+ balance.balanced?
148
+ end
149
+
150
+ class Entity < Grape::Entity
151
+ root 'journal_entries', 'journal_entry'
152
+
153
+ expose :id, :documentation => { :type => 'string', :desc => 'journal entry ID' }
154
+
155
+ expose :book, :format_with => :id,
156
+ :documentation => { :type => 'string', :desc => 'book ID' }
157
+
158
+ expose :effective_at, :format_with => :time,
159
+ :documentation => { :type => 'string', :desc => 'effective at date' }
160
+
161
+ expose :description, :documentation => { :type => 'string', :desc => 'description' }
162
+
163
+ expose :reference, :documentation => { :type => 'string', :desc => 'reference URI' }
164
+
165
+ expose :version, :documentation => { :type => 'integer', :desc => 'version' }
166
+ end
167
+
168
+ private
169
+
170
+ def self.raise_unless_creatable args
171
+ book = args[:book]
172
+
173
+ if book.nil? or not book.kind_of? Book
174
+ raise JournalEntryError, ':book is required and must be a Book'
175
+ elsif UUID.invalid? book.id
176
+ raise JournalEntryError, ':book must have a valid :id'
177
+ end
178
+
179
+ effective_at = args[:effective_at]
180
+
181
+ if effective_at.nil? or not effective_at.kind_of? Time
182
+ raise JournalEntryError, ':effective_at is required and must be a Time'
183
+ end
184
+ end
185
+
186
+ def line_state
187
+ :active
188
+ end
189
+ end
190
+
191
+ class ArchivedJournalEntry < JournalEntry
192
+ include Roles::Activatable
193
+ include Roles::Archivable
194
+
195
+ class Entity < JournalEntry::Entity
196
+ root 'archived_journal_entries', 'archived_journal_entry'
197
+ end
198
+
199
+ def self.sub_klasses
200
+ [ archived_klass ]
201
+ end
202
+ end
203
+
204
+ class ActiveJournalEntry < JournalEntry
205
+
206
+ include Roles::Postable
207
+ include Roles::Activatable
208
+ include Roles::Archivable
209
+
210
+ attr_reader :reason
211
+
212
+ class Entity < JournalEntry::Entity
213
+ root 'active_journal_entries', 'active_journal_entry'
214
+
215
+ expose :reason
216
+ end
217
+
218
+ def self.sub_klasses
219
+ [ active_klass ]
220
+ end
221
+
222
+ def create_line args
223
+ args.merge! :client => client,
224
+ :journal_entry => self,
225
+ :effective_at => effective_at
226
+
227
+ Line.raise_on_duplicate_orders args
228
+
229
+ client.active_lines.validate_creatability args
230
+
231
+ active_line = client.active_lines args
232
+
233
+ store.create_line :line => active_line
234
+ end
235
+
236
+ private
237
+
238
+ def set_reason reason
239
+ @reason = reason
240
+ end
241
+ end
242
+
243
+ class PostingJournalEntry < JournalEntry
244
+ include Roles::Postable
245
+
246
+ class Entity < JournalEntry::Entity
247
+ root 'posting_journal_entries', 'posting_journal_entry'
248
+ end
249
+
250
+ def self.sub_klasses
251
+ [ posting_klass ]
252
+ end
253
+
254
+ def self.update args
255
+ raise JournalEntryError, 'posting journal entries are not updatable'
256
+ end
257
+
258
+ def update
259
+ self.class.update( { } )
260
+ end
261
+
262
+ def balance
263
+ raise JournalEntryError, 'cannot call balance on a PostingJournalEntry'
264
+ end
265
+ end
266
+
267
+ class PostedJournalEntry < JournalEntry
268
+ include Roles::Postable
269
+
270
+ class Entity < JournalEntry::Entity
271
+ root 'posted_journal_entries', 'posted_journal_entry'
272
+ end
273
+
274
+ def self.sub_klasses
275
+ [ posted_klass ]
276
+ end
277
+
278
+ def self.update args
279
+ raise JournalEntryError, 'posted journal entries are not updatable'
280
+ end
281
+
282
+ def update
283
+ self.class.update( { } )
284
+ end
285
+
286
+ private
287
+
288
+ def line_state
289
+ :posted
290
+ end
291
+ end
292
+ end
293
+ end
@@ -0,0 +1,113 @@
1
+ module Subledger
2
+ module Domain
3
+ class KeyError < Error; end
4
+
5
+ class Key
6
+
7
+ include Domain
8
+
9
+ include Roles::Attributable
10
+ include Roles::Identifiable
11
+ include Roles::Storable
12
+
13
+ include Roles::Creatable
14
+ include Roles::Readable
15
+ include Roles::Collectable
16
+
17
+ include Roles::Activatable
18
+ include Roles::Archivable
19
+
20
+ include Roles::Restable
21
+
22
+ attr_reader :identity, :secret
23
+
24
+ def self.post_keys
25
+ [ :id, :identity ]
26
+ end
27
+
28
+ def self.root_klass
29
+ Key
30
+ end
31
+
32
+ def self.sub_klasses
33
+ [ archived_klass, active_klass ]
34
+ end
35
+
36
+ def self.active_klass
37
+ ActiveKey
38
+ end
39
+
40
+ def self.archived_klass
41
+ ArchivedKey
42
+ end
43
+
44
+ def initialize args
45
+ identifiable args
46
+ storable args
47
+
48
+ @identity = args[:identity]
49
+
50
+ # TODO separate new and create
51
+
52
+ if args[:bcrypt].nil?
53
+ @secret = args[:secret] || UUID.as_string
54
+
55
+ @bcrypt = BCrypt::Password.create( @secret, :cost => 5 )
56
+ elsif args[:bcrypt]
57
+ @secret = args[:secret]
58
+
59
+ @bcrypt = BCrypt::Password.new args[:bcrypt]
60
+ end
61
+ end
62
+
63
+ def authenticates? this_secret
64
+ bcrypt == this_secret
65
+ end
66
+
67
+ class Entity < Grape::Entity
68
+ root 'keys', 'key'
69
+
70
+ expose :id, :documentation => { :type => 'string', :desc => 'key ID' }
71
+
72
+ expose :identity, :format_with => :id,
73
+ :documentation => { :type => 'string', :desc => 'identity ID' }
74
+
75
+ expose :secret, :documentation => { :type => 'string', :desc => 'key secret' }
76
+ end
77
+
78
+ private
79
+
80
+ attr_reader :bcrypt
81
+
82
+ def self.raise_unless_creatable args
83
+ identity = args[:identity]
84
+
85
+ if identity.nil? or not identity.kind_of? Identity
86
+ raise KeyError, ':identity is required and must be an Identity'
87
+ elsif UUID.invalid? identity.id
88
+ raise KeyError, ':identity must have a valid :id'
89
+ end
90
+ end
91
+ end
92
+
93
+ class ArchivedKey < Key
94
+ class Entity < Key::Entity
95
+ root 'archived_keys', 'archived_key'
96
+ end
97
+
98
+ def self.sub_klasses
99
+ [ archived_klass ]
100
+ end
101
+ end
102
+
103
+ class ActiveKey < Key
104
+ class Entity < Key::Entity
105
+ root 'active_keys', 'active_key'
106
+ end
107
+
108
+ def self.sub_klasses
109
+ [ active_klass ]
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,272 @@
1
+ module Subledger
2
+ module Domain
3
+ class LineError < Error; end
4
+
5
+ class Line
6
+
7
+ include Domain::Value
8
+ include Domain
9
+
10
+ include Roles::Attributable
11
+ include Roles::Describable
12
+ include Roles::Identifiable
13
+ include Roles::Storable
14
+ include Roles::Timeable
15
+ include Roles::Versionable
16
+
17
+ include Roles::Creatable
18
+ include Roles::Readable
19
+
20
+ include Roles::Collectable
21
+
22
+ include Roles::Restable
23
+
24
+ attr_reader :journal_entry
25
+ attr_accessor :account, :value, :order
26
+
27
+ def self.root_klass
28
+ Line
29
+ end
30
+
31
+ def self.sub_klasses
32
+ [ archived_klass, active_klass, posted_klass ]
33
+ end
34
+
35
+ def self.archived_klass
36
+ ArchivedLine
37
+ end
38
+
39
+ def self.active_klass
40
+ ActiveLine
41
+ end
42
+
43
+ def self.posted_klass
44
+ PostedLine
45
+ end
46
+
47
+ def self.update args
48
+ raise_on_duplicate_orders args
49
+
50
+ super
51
+ end
52
+
53
+ def self.specialized_raise_unless_creatable args
54
+ end
55
+
56
+ def initialize args
57
+ describable args
58
+ identifiable args
59
+ storable args
60
+ versionable args
61
+
62
+ @journal_entry = args[:journal_entry]
63
+ @account = args[:account]
64
+ @value = args[:value]
65
+ @order = args[:order]
66
+
67
+ specialized_initialization args
68
+ end
69
+
70
+ def amount
71
+ value.amount
72
+ end
73
+
74
+ class Entity < Grape::Entity
75
+ root 'lines', 'line'
76
+
77
+ expose :id, :documentation => { :type => 'string', :desc => 'line ID' }
78
+
79
+ expose :journal_entry, :format_with => :id,
80
+ :documentation => { :type => 'string', :desc => 'journal entry ID' }
81
+
82
+ expose :account, :format_with => :id,
83
+ :documentation => { :type => 'string', :desc => 'account ID' }
84
+
85
+ expose :description, :documentation => { :type => 'string', :desc => 'description' }
86
+
87
+ expose :reference, :documentation => { :type => 'string', :desc => 'reference URI' }
88
+
89
+ expose :value, :format_with => :value,
90
+ :documentation => { :type => 'string', :desc => 'value' }
91
+
92
+ expose :order, :documentation => { :type => 'string', :desc => 'order' }
93
+
94
+ expose :version, :documentation => { :type => 'integer', :desc => 'version' }
95
+ end
96
+
97
+ private
98
+
99
+ def self.specialized_keys
100
+ []
101
+ end
102
+
103
+ def self.raise_on_duplicate_orders args
104
+ client = args[:client]
105
+
106
+ journal_entry = client.journal_entry :id => args[:journal_entry].id
107
+
108
+ order = BigDecimal args[:order]
109
+
110
+ journal_entry.lines.each do |line|
111
+ if order == BigDecimal( line.order )
112
+ raise LineError, "lines must have unique orders: #{args[:orders]}"
113
+ end
114
+ end
115
+ end
116
+
117
+ def self.raise_unless_creatable args
118
+ raise_on_duplicate_orders args
119
+
120
+ raise_unless_create_and_postable args
121
+
122
+ journal_entry = args[:journal_entry]
123
+
124
+ if UUID.invalid? journal_entry.id
125
+ raise LineError, ':journal_entry must have a valid :id'
126
+ end
127
+ end
128
+
129
+ def self.raise_unless_create_and_postable args
130
+ journal_entry = args[:journal_entry]
131
+
132
+ if journal_entry.nil? or not journal_entry.kind_of? JournalEntry
133
+ raise LineError, ':journal_entry is required and must be a JournalEntry'
134
+ end
135
+
136
+ account = args[:account]
137
+
138
+ if account.nil? or not account.kind_of? Account
139
+ raise LineError, ':account is required and must be an Account'
140
+ elsif UUID.invalid? account.id
141
+ raise LineError, ':account must have a valid :id'
142
+ end
143
+
144
+ value = args[:value]
145
+
146
+ if value.nil? or not value.kind_of? Value
147
+ raise LineError, ':value is required and must be a Value'
148
+ end
149
+
150
+ order = args[:order]
151
+
152
+ if order.nil? or
153
+ order !~ /^\d{1,4}[.]?\d{0,2}$/
154
+ raise LineError, ':order is required and in the form [###]#[.##]'
155
+ end
156
+
157
+ bd_order = BigDecimal order
158
+
159
+ if bd_order <= 0 or bd_order >= 10_000
160
+ raise LineError, ':order must be > 0 and < 10,000'
161
+ end
162
+
163
+ specialized_raise_unless_creatable args
164
+ end
165
+
166
+ def specialized_initialization args
167
+ end
168
+ end
169
+
170
+ class ArchivedLine < Line
171
+
172
+ include Roles::Updatable
173
+ include Roles::Archivable
174
+ include Roles::Activatable
175
+
176
+ class Entity < Line::Entity
177
+ root 'archived_lines', 'archived_line'
178
+ end
179
+
180
+ def self.sub_klasses
181
+ [ archived_klass ]
182
+ end
183
+
184
+ def self.post_keys
185
+ [ :account, :journal_entry, :description, :reference, :value, :order ]
186
+ end
187
+
188
+ def self.patch_keys
189
+ [ :id, :account, :description, :reference, :value, :order, :version ]
190
+ end
191
+
192
+ def posted?
193
+ false
194
+ end
195
+ end
196
+
197
+ class ActiveLine < Line
198
+
199
+ include Roles::Updatable
200
+ include Roles::Archivable
201
+ include Roles::Activatable
202
+
203
+ class Entity < Line::Entity
204
+ root 'active_lines', 'active_line'
205
+ end
206
+
207
+ def self.sub_klasses
208
+ [ active_klass ]
209
+ end
210
+
211
+ def self.post_keys
212
+ [ :account, :journal_entry, :description, :reference, :value, :order ]
213
+ end
214
+
215
+ def self.patch_keys
216
+ [ :id, :account, :description, :reference, :value, :order, :version ]
217
+ end
218
+
219
+ def posted?
220
+ false
221
+ end
222
+ end
223
+
224
+ class PostedLineError < Error; end
225
+
226
+ class PostedLine < Line
227
+ attr_reader :effective_at, :posted_at, :balance
228
+
229
+ class Entity < Line::Entity
230
+ root 'posted_lines', 'posted_line'
231
+
232
+ expose :effective_at, :format_with => :time,
233
+ :documentation => { :type => 'string', :desc => 'effective time' }
234
+
235
+ expose :posted_at, :format_with => :time,
236
+ :documentation => { :type => 'string', :desc => 'posted time' }
237
+
238
+ expose :balance, :format_with => :balance
239
+ end
240
+
241
+ def self.sub_klasses
242
+ [ posted_klass ]
243
+ end
244
+
245
+ def posted?
246
+ true
247
+ end
248
+
249
+ private
250
+
251
+ attr_writer :balance
252
+
253
+ def self.specialized_raise_unless_creatable args
254
+ effective_at = args[:effective_at]
255
+
256
+ if effective_at.nil? or not effective_at.kind_of? Time
257
+ raise PostedLineError, ':effective_at is required and must be a Time'
258
+ end
259
+
260
+ if args[:posted_at].nil?
261
+ raise PostedLineError, ':posted_at is required'
262
+ end
263
+ end
264
+
265
+ def specialized_initialization args
266
+ @effective_at = utc_or_nil args[:effective_at]
267
+ @posted_at = utc_or_nil args[:posted_at]
268
+ @balance = args[:balance]
269
+ end
270
+ end
271
+ end
272
+ end