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
data/lib/subledger.rb ADDED
@@ -0,0 +1,234 @@
1
+ # encoding: UTF-8
2
+
3
+ Encoding.default_internal = Encoding.default_external = 'UTF-8'
4
+
5
+ require 'bigdecimal'
6
+ require 'logger'
7
+
8
+ require 'time'
9
+
10
+ require 'rubygems'
11
+
12
+ require 'multi_json'
13
+
14
+ require 'subledger/exception_handler'
15
+
16
+ module Subledger
17
+ class Error < StandardError; end
18
+ end
19
+
20
+ require 'subledger/collection_name'
21
+ require 'subledger/domain'
22
+ require 'subledger/interface'
23
+ require 'subledger/path'
24
+ require 'subledger/rest'
25
+ require 'subledger/uuid'
26
+ require 'subledger/version'
27
+
28
+ # additional requires at bottom
29
+
30
+ SUBLEDGER = 'SUBLEDGER'
31
+
32
+ module Subledger
33
+ class DateError < Error; end
34
+
35
+ ROOT = File.expand_path __FILE__ + '../../..'
36
+
37
+ ROOT_CONFIG = ROOT + '/config'
38
+ ROOT_LIB = ROOT + '/lib'
39
+ ROOT_SPEC = ROOT + '/spec'
40
+
41
+ LOG = ::Logger.new '/tmp/subledger.log', 10, 102_400_000
42
+ LOG.sev_threshold = Logger::DEBUG
43
+
44
+ case ::ENV['SUBLEDGER_ENV']
45
+ when nil
46
+ ENV = 'production'
47
+ COVER = ::ENV['SUBLEDGER_COVER' ] || false
48
+ FORMAT = ::ENV['SUBLEDGER_FORMAT'] || 'pride'
49
+ SCHEME = ::ENV['SUBLEDGER_SCHEME'] || 'https://'
50
+ DOMAIN = ::ENV['SUBLEDGER_DOMAIN'] || 'api.subledger.com'
51
+
52
+ STORE = ( ::ENV['SUBLEDGER_STORE'] || :api ).to_sym
53
+ AWS_CRED_FILE = nil
54
+
55
+ LOG.sev_threshold = Logger::WARN
56
+
57
+ when 'test'
58
+ ENV = 'test'
59
+ COVER = ::ENV['SUBLEDGER_COVER' ] || false
60
+ FORMAT = ::ENV['SUBLEDGER_FORMAT'] || 'pride'
61
+ SCHEME = ::ENV['SUBLEDGER_SCHEME'] || 'http://'
62
+ DOMAIN = ::ENV['SUBLEDGER_DOMAIN'] || 'api.subledger.dev'
63
+
64
+ STORE = ( ::ENV['SUBLEDGER_STORE'] || :memory ).to_sym
65
+ AWS_CRED_FILE = 'aws_creds_dev.yml'
66
+
67
+ when 'test_single'
68
+ ENV = 'test'
69
+ COVER = ::ENV['SUBLEDGER_COVER' ] || false
70
+ FORMAT = ::ENV['SUBLEDGER_FORMAT'] || 'turn'
71
+ SCHEME = ::ENV['SUBLEDGER_SCHEME'] || 'http://'
72
+ DOMAIN = ::ENV['SUBLEDGER_DOMAIN'] || 'api.subledger.dev'
73
+
74
+ STORE = ( ::ENV['SUBLEDGER_STORE'] || :memory ).to_sym
75
+ AWS_CRED_FILE = 'aws_creds_dev.yml'
76
+
77
+ when 'ci'
78
+ ENV = 'ci'
79
+ COVER = ::ENV['SUBLEDGER_COVER' ] || false
80
+ FORMAT = ::ENV['SUBLEDGER_FORMAT'] || 'normal'
81
+ SCHEME = ::ENV['SUBLEDGER_SCHEME'] || 'http://'
82
+ DOMAIN = ::ENV['SUBLEDGER_DOMAIN'] || 'localhost:9292'
83
+
84
+ STORE = ( ::ENV['SUBLEDGER_STORE'] || :memory ).to_sym
85
+ AWS_CRED_FILE = 'aws_creds_dev.yml'
86
+
87
+ when 'production'
88
+ ENV = 'production'
89
+ COVER = ::ENV['SUBLEDGER_COVER' ] || false
90
+ FORMAT = ::ENV['SUBLEDGER_FORMAT'] || 'pride'
91
+ SCHEME = ::ENV['SUBLEDGER_SCHEME'] || 'https://'
92
+ DOMAIN = ::ENV['SUBLEDGER_DOMAIN'] || 'api.subledger.com'
93
+
94
+ STORE = ( ::ENV['SUBLEDGER_STORE'] || :aws ).to_sym
95
+ AWS_CRED_FILE = 'aws_creds_prod.yml'
96
+ end
97
+
98
+ case STORE
99
+ when :memory
100
+ SLOW_POST_DELAY = ::ENV['SUBLEDGER_SLOW_POST_DELAY'] || 0.5
101
+ PROGRESS_INITIAL = ::ENV['SUBLEDGER_PROGRESS_INITIAL'] || 0.075
102
+ PROGRESS_RETRY = ::ENV['SUBLEDGER_PROGRESS_RETRY'] || 0.05
103
+ PROGRESS_FAIL = ::ENV['SUBLEDGER_PROGRESS_FAIL'] || 4
104
+ BALANCE_SETTLE_DELAY = ::ENV['SUBLEDGER_BALANCE_SETTLE_DELAY'] || 0
105
+ when :api
106
+ SLOW_POST_DELAY = ::ENV['SUBLEDGER_SLOW_POST_DELAY'] || 0.5
107
+ PROGRESS_INITIAL = ::ENV['SUBLEDGER_PROGRESS_INITIAL'] || 0.075
108
+ PROGRESS_RETRY = ::ENV['SUBLEDGER_PROGRESS_RETRY'] || 0.05
109
+ PROGRESS_FAIL = ::ENV['SUBLEDGER_PROGRESS_FAIL'] || 4
110
+ BALANCE_SETTLE_DELAY = ::ENV['SUBLEDGER_BALANCE_SETTLE_DELAY'] || 0
111
+ when :aws
112
+ SLOW_POST_DELAY = ::ENV['SUBLEDGER_SLOW_POST_DELAY'] || 10
113
+ PROGRESS_INITIAL = ::ENV['SUBLEDGER_PROGRESS_INITIAL'] || 1.5
114
+ PROGRESS_RETRY = ::ENV['SUBLEDGER_PROGRESS_RETRY'] || 0.5
115
+ PROGRESS_FAIL = ::ENV['SUBLEDGER_PROGRESS_FAIL'] || 20
116
+ BALANCE_SETTLE_DELAY = ::ENV['SUBLEDGER_BALANCE_SETTLE_DELAY'] || 3
117
+ end
118
+
119
+ unless AWS_CRED_FILE.nil?
120
+ AWS_PATH = File.expand_path( File.dirname( __FILE__ ) + '/../config/' + AWS_CRED_FILE )
121
+ end
122
+
123
+ RENDERED_REPORTS_BUCKET = "subledger_#{ENV}_rendered_reports"
124
+ RENDERED_REPORT_CATEGORIES_BUCKET = "subledger_#{ENV}_rendered_report_categories"
125
+ BACKUP_BUCKET = "subledger_#{ENV}_backups"
126
+
127
+ def self.new args = { }
128
+ args = args.dup
129
+
130
+ # auth args
131
+
132
+ identity = args[:identity]
133
+ identity_id = args[:identity_id]
134
+
135
+ if identity.nil? and not identity_id.nil?
136
+ args.merge! :identity => Domain::Identity.
137
+ new( :id => identity_id )
138
+
139
+ args.delete :identity_id
140
+ end
141
+
142
+ key = args[:key]
143
+ key_id = args[:key_id]
144
+
145
+ if key.nil? and not key_id.nil?
146
+ args.merge! :auth_key => Domain::Key.
147
+ new( :id => key_id,
148
+ :identity => args[:identity],
149
+ :secret => args[:secret] )
150
+
151
+ args.delete :key_id
152
+ args.delete :secret
153
+ end
154
+
155
+ # instance args
156
+
157
+ org = args[:org]
158
+ org_id = args[:org_id]
159
+
160
+ if org.nil? and not org_id.nil?
161
+ args.merge! :org => Domain::Org.
162
+ new( :id => org_id )
163
+
164
+ args.delete :org_id
165
+ end
166
+
167
+ book = args[:book]
168
+ book_id = args[:book_id]
169
+
170
+ if book.nil? and not book_id.nil?
171
+ args.merge! :book => Domain::Book.
172
+ new( :id => book_id,
173
+ :org => args[:org] )
174
+
175
+ args.delete :book_id
176
+ end
177
+
178
+ args.merge! :store => store( args )
179
+
180
+ Interface::Client.new args
181
+ end
182
+
183
+ def self.store args = { }
184
+ store_sym = args[:store] || STORE
185
+
186
+ begin
187
+ require "subledger/store/#{store_sym}"
188
+ rescue LoadError => e
189
+ raise Error, "Store: #{store_sym}: #{e}"
190
+ end
191
+
192
+ Object.const_get( 'Subledger' ).
193
+ const_get( 'Store' ).
194
+ const_get( store_sym.capitalize ).
195
+ const_get( 'Store' ).new args
196
+ end
197
+
198
+ # Thanks to Jan Goyvaerts, Steven Levithan, O'Reilly Media, Inc.
199
+ ISO8601_REGEXP = /^(?<year>[0-9]{4})-?(?<month>1[0-2]|0[1-9])-?(?<day>3[0-1]|0[1-9]|[1-2][0-9])T(?<hour>2[0-3]|[0-1][0-9]):?(?<minute>[0-5][0-9]):?(?<second>[0-5][0-9])(?<ms>\.[0-9]{1,3})??(?<timezone>Z|[+-](?:2[0-3]|[0-1][0-9]):?[0-5][0-9])$/
200
+ # http://my.safaribooksonline.com/book/programming/regular-expressions/9780596802837/4dot-validation-and-formatting/id2983571
201
+ # Modified to accept optional punctuation and fractional seconds of 1 to 3 digits
202
+
203
+ EARLIEST_TIME = Time.utc 1970, 1, 1, 0, 0, 0, 0
204
+ LATEST_TIME = EARLIEST_TIME + 2 ** 32
205
+
206
+ def self.parse_time parameter_name, time_string
207
+ if time_string.nil?
208
+ raise DateError, "#{parameter_name} is required"
209
+ end
210
+
211
+ unless time_string.match ISO8601_REGEXP
212
+ raise DateError, "#{parameter_name} must be ISO 8601 datetime with timezone, optional punctuation, optional fractional seconds (1-3 digits)"
213
+ end
214
+
215
+ time = begin
216
+ Time.parse( time_string ).utc
217
+ rescue ArgumentError => e
218
+ raise DateError, "#{parameter_name}: not a parsable time"
219
+ end
220
+
221
+ unless time >= EARLIEST_TIME and time <= LATEST_TIME
222
+ raise DateError, "#{parameter_name} must be between #{EARLIEST_TIME.iso8601(3)} and #{LATEST_TIME.iso8601(3)} inclusive"
223
+ end
224
+
225
+ time
226
+ end
227
+
228
+ def self.log_environment
229
+ LOG.info "SUBLEDGER_STORE: #{STORE}"
230
+ LOG.info "SUBLEDGER_ENV: #{ENV}"
231
+ end
232
+ end
233
+
234
+ require 'subledger/store'
@@ -0,0 +1,77 @@
1
+ # encoding: UTF-8
2
+
3
+ Encoding.default_internal = Encoding.default_external = 'UTF-8'
4
+
5
+ require 'pp'
6
+
7
+ require 'rubygems'
8
+
9
+ require 'subledger'
10
+ require 'subledger/server'
11
+
12
+ require 'minitest/autorun'
13
+ require 'minitest/mock'
14
+ require 'minitest/pride' unless Subledger::ENV == 'ci'
15
+
16
+ require 'rack/test'
17
+
18
+ require Subledger::ROOT_SPEC + '/support/minitest'
19
+ require Subledger::ROOT_SPEC + '/support/factory'
20
+
21
+ module Subledger
22
+ def self.scenarios
23
+ [
24
+ { :before => ['l', 'k'] },
25
+ { :ending => ['m', 'l'] },
26
+ { :starting => ['m', 'n'] },
27
+ { :after => ['n', 'o'] },
28
+ { :preceding => ['l', 'k'] },
29
+ { :following => ['n', 'o'] }
30
+ ]
31
+ end
32
+
33
+ def self.scenarios_order
34
+ [
35
+ { :before => ['l', 'k'] },
36
+ { :ending => ['m', 'l'] },
37
+ { :starting => ['m', 'n'] },
38
+ { :after => ['n', 'o'] }
39
+ ]
40
+ end
41
+
42
+ def self.scenarios_id
43
+ [
44
+ { :preceding => ['l', 'k'] },
45
+ { :following => ['n', 'o'] }
46
+ ]
47
+ end
48
+ end
49
+
50
+ # Factory namespace setup
51
+
52
+ ActiveBook = Subledger::Domain::ActiveBook
53
+ ArchivedBook = Subledger::Domain::ArchivedBook
54
+ ActiveJournalEntry = Subledger::Domain::ActiveJournalEntry
55
+ PostingJournalEntry = Subledger::Domain::PostingJournalEntry
56
+ PostedJournalEntry = Subledger::Domain::PostedJournalEntry
57
+ ArchivedJournalEntry = Subledger::Domain::PostingJournalEntry
58
+ ActiveAccount = Subledger::Domain::ActiveAccount
59
+ ArchivedAccount = Subledger::Domain::ArchivedAccount
60
+ ActiveLine = Subledger::Domain::ActiveLine
61
+ Debit = Subledger::Domain::Debit
62
+ Credit = Subledger::Domain::Credit
63
+ Zero = Subledger::Domain::Zero
64
+
65
+ module Kernel
66
+ private
67
+
68
+ def this_method
69
+ caller[0] =~ /`([^']*)'/ and $1
70
+ end
71
+
72
+ def calling_method
73
+ caller[1] =~ /`([^']*)'/ and $1
74
+ end
75
+ end
76
+
77
+ Minitest.autorun
@@ -0,0 +1,354 @@
1
+ require 'spec_helper'
2
+
3
+ Test 'Subledger' do
4
+
5
+ let(:factory ) { Subledger::Factory.create }
6
+ let(:amount ) { '1' }
7
+
8
+ balances_order = { :before => [ Subledger::Domain::Debit.new( '3' ),
9
+ Subledger::Domain::Debit.new( '1' ) ],
10
+ :ending => [ Subledger::Domain::Debit.new( '6' ),
11
+ Subledger::Domain::Debit.new( '3' ) ],
12
+ :starting => [ Subledger::Domain::Debit.new( '6' ),
13
+ Subledger::Domain::Debit.new( '10' ) ],
14
+ :after => [ Subledger::Domain::Debit.new( '10' ),
15
+ Subledger::Domain::Debit.new( '15' ) ] }
16
+
17
+ balances_id = { :preceding => [ Subledger::Domain::Debit.new( '3' ),
18
+ Subledger::Domain::Debit.new( '1' ) ],
19
+ :following => [ Subledger::Domain::Debit.new( '10' ),
20
+ Subledger::Domain::Debit.new( '15' ) ] }
21
+
22
+ Instance do
23
+ subject { factory.client }
24
+
25
+ Test 'Account' do
26
+
27
+ # POST /books/:book_id/accounts
28
+ RespondsTo 'account.create' do
29
+ ByReturning 'an instance of Subledger::Domain::ActiveAccount' do
30
+ subject.account.create( factory.account_args ).
31
+ must_be_instance_of Subledger::Domain::ActiveAccount
32
+ end
33
+ end
34
+
35
+ # GET /books/:book_id/accounts/:id/first_and_last_line
36
+ [ :archived, :active ].each do |state|
37
+ RespondsTo 'account.first_and_last_line' do
38
+ let(:account) { factory.create_account state }
39
+
40
+ When 'the account has no lines' do
41
+ ByReturning "an empty array" do
42
+ subject.
43
+ account.
44
+ first_and_last_line( :id => account.id ).must_be_instance_of Array
45
+
46
+ subject.
47
+ account.
48
+ first_and_last_line( :id => account.id ).empty?.must_equal true
49
+ end
50
+ end
51
+
52
+ When 'the account has lines' do
53
+ before { factory.create_posted_line_collection account }
54
+
55
+ ByReturning "an empty array" do
56
+ subject.
57
+ account.
58
+ first_and_last_line( :id => account.id ).must_be_instance_of Array
59
+
60
+ subject.
61
+ account.
62
+ first_and_last_line( :id => account.id ).
63
+ length.must_equal 2
64
+
65
+ subject.
66
+ account.
67
+ first_and_last_line( :id => account.id ).
68
+ first.must_be_instance_of Subledger::Domain::PostedLine
69
+
70
+ subject.
71
+ account.
72
+ first_and_last_line( :id => account.id ).
73
+ last.must_be_instance_of Subledger::Domain::PostedLine
74
+ end
75
+ end
76
+ end
77
+ end
78
+
79
+ [ :archived, :active ].each do |state|
80
+ RespondsTo 'account.read when description has a / in it' do
81
+ let(:description) { 'this has a / in it' }
82
+ let(:args ) { factory.account_args.
83
+ merge :description => description }
84
+ let(:account ) { subject.account.create args }
85
+
86
+ ByReturning "an instance of #{state} account" do
87
+ subject.account.read( :id => account.id ).id.must_equal account.id
88
+ subject.account.read( :id => account.id ).description.must_equal description
89
+ end
90
+ end
91
+ end
92
+
93
+ # PATCH /books/:book_id/accounts/:id
94
+ [ :archived, :active ].each do |state|
95
+ RespondsTo 'accounts.update' do
96
+ let(:account ) { factory.create_account state }
97
+ let(:args ) { account.attributes }
98
+ let(:description) { 'updated description' }
99
+ let(:update_args) { args.merge :description => description,
100
+ :version => 2 }
101
+
102
+ ByReturning "an instance of #{state} account" do
103
+ patched = subject.accounts.update update_args
104
+
105
+ patched.must_be_instance_of account.class
106
+ patched.description.must_equal description
107
+ end
108
+ end
109
+ end
110
+
111
+ # GET /books/:book_id/accounts ? before = :description & limit = :limit & state = :state
112
+ # ending
113
+ # starting
114
+ # after
115
+ [ :active, :archived ].each do |state|
116
+ Subledger.scenarios_order.each do |scenario|
117
+ action = scenario.keys.first
118
+ expectations = scenario[action]
119
+
120
+ RespondsTo "accounts.collect :action => #{action}, :state => #{state}" do
121
+ let(:accounts) { factory.create_account_collection state }
122
+ let(:item ) { accounts[2] }
123
+ let(:limit ) { 2 }
124
+ let(:klass ) { item.class }
125
+
126
+ let(:args) { { :action => action,
127
+ :description => item.description,
128
+ :state => state,
129
+ :limit => limit } }
130
+
131
+ When 'not given a block' do
132
+ ByReturning "a list of #{state} accounts" do
133
+ subject.accounts.collect( args ).length.must_equal limit
134
+ end
135
+ end
136
+
137
+ When 'given a block' do
138
+ ByYielding "a list of #{state} accounts" do
139
+ count = 0
140
+
141
+ subject.accounts.collect args do |account|
142
+ expectation = expectations[count]
143
+
144
+ account.description.must_equal expectation
145
+
146
+ account.must_be_instance_of klass
147
+
148
+ count += 1
149
+ end
150
+
151
+ count.must_equal limit
152
+ end
153
+ end
154
+ end
155
+ end
156
+ end
157
+
158
+ # GET /accounts ? preceding = :book_id & limit = :limit & state = :state
159
+ # following = :book_id & limit = :limit & state = :state
160
+ [ :active, :archived ].each do |state|
161
+ Subledger.scenarios_id.each do |scenario|
162
+ action = scenario.keys.first
163
+ expectations = scenario[action]
164
+
165
+ RespondsTo "accounts.collect :action => #{action}" do
166
+ let(:accounts) { factory.create_account_collection state }
167
+ let(:item ) { accounts[2] }
168
+ let(:limit ) { 2 }
169
+ let(:klass ) { item.class }
170
+
171
+ let(:args) { { :action => action,
172
+ :id => item.id,
173
+ :state => state,
174
+ :limit => limit } }
175
+
176
+ When 'not given a block' do
177
+ ByReturning "a list of #{state} accounts" do
178
+ subject.accounts.collect( args ).length.must_equal limit
179
+ end
180
+ end
181
+
182
+ When 'given a block' do
183
+ ByYielding "a list of #{state} accounts" do
184
+ count = 0
185
+
186
+ subject.accounts.collect args do |account|
187
+ expectation = expectations[count]
188
+
189
+ account.description.must_equal expectation
190
+
191
+ account.must_be_instance_of klass
192
+
193
+ count += 1
194
+ end
195
+
196
+ count.must_equal limit
197
+ end
198
+ end
199
+ end
200
+ end
201
+ end
202
+
203
+ # GET /books/:book_id/accounts/:id/lines ? before = :effective_at & limit = :limit
204
+ # ending
205
+ # starting
206
+ # after
207
+ Subledger.scenarios_order.each do |scenario|
208
+ action = scenario.keys.first
209
+ expectations = scenario[action]
210
+ balances = balances_order[action]
211
+
212
+ RespondsTo "account.read(:id => id).lines action => #{action}" do
213
+ let(:account) { factory.create_account :active }
214
+ let(:lines ) { factory.create_posted_line_collection account }
215
+ let(:item ) { lines[2] }
216
+ let(:limit ) { 2 }
217
+
218
+ let(:args) { { :action => action,
219
+ :effective_at => item.effective_at,
220
+ :limit => limit } }
221
+
222
+ When 'not given a block' do
223
+ ByReturning 'a list of account lines' do
224
+ account.lines( args ).length.must_equal expectations.length
225
+ end
226
+ end
227
+
228
+ When 'given a block' do
229
+ ByYielding 'a list of account lines' do
230
+ count = 0
231
+
232
+ account.lines args do |line|
233
+ expectation = expectations[count]
234
+ line.description.must_equal expectation
235
+
236
+ line.must_be_instance_of Subledger::Domain::PostedLine
237
+
238
+ line.posted_at.wont_be_nil
239
+ line.effective_at.wont_be_nil
240
+
241
+ line.balance.must_be_instance_of Subledger::Domain::Balance
242
+
243
+ line.balance.must_equal balances[count]
244
+
245
+ count += 1
246
+ end
247
+
248
+ count.must_equal expectations.length
249
+ end
250
+ end
251
+ end
252
+ end
253
+
254
+ # GET /books/:book_id/accounts/:id/lines ? preceding = :line_id & limit = :limit
255
+ # following = :line_id & limit = :limit
256
+ Subledger.scenarios_id.each do |scenario|
257
+ action = scenario.keys.first
258
+ expectations = scenario[action]
259
+ balances = balances_id[action]
260
+
261
+ RespondsTo "account_instance.lines action => #{action}" do
262
+ let(:account) { factory.create_account :active }
263
+ let(:lines ) { factory.create_posted_line_collection account }
264
+ let(:item ) { lines[2] }
265
+ let(:limit ) { 2 }
266
+
267
+ let(:args) { { :action => action,
268
+ :id => item.id,
269
+ :limit => limit } }
270
+
271
+ When 'not given a block' do
272
+ ByReturning 'a list of account lines' do
273
+ account.lines( args ).length.must_equal expectations.length
274
+ end
275
+ end
276
+
277
+ When 'given a block' do
278
+ ByYielding 'a list of account lines' do
279
+ count = 0
280
+
281
+ account.lines args do |line|
282
+ expectation = expectations[count]
283
+
284
+ line.description.must_equal expectation
285
+
286
+ line.must_be_instance_of Subledger::Domain::PostedLine
287
+
288
+ line.posted_at.wont_be_nil
289
+ line.effective_at.wont_be_nil
290
+
291
+ line.balance.must_be_instance_of Subledger::Domain::Balance
292
+
293
+ line.balance.must_equal balances[count]
294
+
295
+ count += 1
296
+ end
297
+
298
+ count.must_equal expectations.length
299
+ end
300
+ end
301
+ end
302
+ end
303
+
304
+ # GET /books/:book_id/accounts/:account_id/lines/:id
305
+
306
+ RespondsTo 'account.line.read' do
307
+ let(:account) { factory.create_account :active }
308
+ let(:lines ) { factory.create_posted_line_collection account }
309
+ let(:line ) { lines[2] }
310
+
311
+ ByReturning 'Subledger::Domain::PostedLine' do
312
+ posted_line = account.line( :id => line.id ).read
313
+ posted_line.must_be_instance_of Subledger::Domain::PostedLine
314
+ posted_line.posted_at.must_be_instance_of Time
315
+ posted_line.effective_at.must_be_instance_of Time
316
+
317
+ if Subledger::STORE == :api
318
+ posted_line.balance.must_be_instance_of Subledger::Domain::Balance
319
+ posted_line.balance.must_equal factory.
320
+ client.
321
+ debit( '6' )
322
+ end
323
+ end
324
+ end
325
+
326
+ # POST /books/:book_id/accounts/:id/archive
327
+ RespondsTo 'account_instance.archive' do
328
+ ByReturning 'an instance of Subledger::Domain::ArchivedAccount' do
329
+ factory.create_account(:active).archive.
330
+ must_be_instance_of Subledger::Domain::ArchivedAccount
331
+ end
332
+ end
333
+
334
+ # POST /books/:book_id/archived_accounts/:id/activate
335
+ RespondsTo 'archived_account_instance.activate' do
336
+ ByReturning 'an instance of Subledger::Domain::ActiveAccount' do
337
+ factory.create_account(:archived).activate.
338
+ must_be_instance_of Subledger::Domain::ActiveAccount
339
+ end
340
+ end
341
+
342
+ # GET /books/:book_id/accounts/:id/balance/:at
343
+ [ :archived, :active ].each do |state|
344
+ RespondsTo 'account_instance.balance' do
345
+ let(:account) { factory.create_account state }
346
+
347
+ ByReturning 'a Subledger::Domain::Balance' do
348
+ account.balance( :at => Time.now ).must_be_instance_of Subledger::Domain::Balance
349
+ end
350
+ end
351
+ end
352
+ end
353
+ end
354
+ end