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