instance_accountant 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 659797969092d787410c4789f8379c33b9b10ccb
4
+ data.tar.gz: 0c858e6a0c0a6ae6f7bec5877cbbf450f6de665c
5
+ SHA512:
6
+ metadata.gz: a442d471310a881effe735f4b7d6ff5118af02e238e20e908178e5e845409cc51a4c123e08e9f0acebac655badec7cc0fbd4278a7e8a21d20c95d03132015af6
7
+ data.tar.gz: cb5df07e2c58e9905134bf3d1683846f1ddeaa41cffb503b68e6dc6c99d5fb36a7119693c31e86553cf64a0e2b71ac3093636bf527a796528a68c17e0141f872
data/LICENSE.txt ADDED
@@ -0,0 +1,12 @@
1
+ Copyright (c) 2014, Subledger, Inc.
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
5
+
6
+ 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
7
+
8
+ 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
9
+
10
+ 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
11
+
12
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
data/README.md ADDED
@@ -0,0 +1,76 @@
1
+ Subledger instance accountant Ruby gem
2
+
3
+ Installation:
4
+
5
+ gem install instance_accountant
6
+
7
+ Gemfile:
8
+
9
+ gem 'instance_accountant'
10
+
11
+ The gem provides an executable that can be invoked two ways:
12
+
13
+ If you want to account for the hourly cost of instances, use this form:
14
+
15
+ instance_accountant account \
16
+ --description 'instance usage for: %s' \
17
+ --reference 'http://test.com/journal_entry' \
18
+ --cost 0.1 \
19
+ --cost_description 'cost for: %s' \
20
+ --cost_reference 'http://test.com/cost' \
21
+ --expense_acct subledger_account_id \
22
+ --payable_acct subledger_account_id \
23
+ --key_id subledger_key_id \
24
+ --secret subledger_secret \
25
+ --org_id subledger_org_id \
26
+ --book_id subledger_book_id \
27
+ --daemon
28
+
29
+ If you want to account for both the hourly cost of instances, and hourly income instances, use this form:
30
+
31
+ instance_accountant account \
32
+ --description 'instance usage for: %s' \
33
+ --reference 'http://test.com/journal_entry' \
34
+ --cost 0.1 \
35
+ --cost_description 'cost for: %s' \
36
+ --cost_reference 'http://test.com/cost' \
37
+ --expense_acct subledger_account_id \
38
+ --payable_acct subledger_account_id \
39
+ --price 0.2 \
40
+ --price_description 'price for: %s' \
41
+ --price_reference 'http://test.com/price' \
42
+ --receivable_acct subledger_account_id \
43
+ --income_acct subledger_account_id \
44
+ --key_id subledger_key_id \
45
+ --secret subledger_secret \
46
+ --org_id subledger_org_id \
47
+ --book_id subledger_book_id \
48
+ --daemon
49
+
50
+ Note: %s in descriptions will be replaced by the ISO 8601 of the hour in UTC
51
+
52
+ Here's a complete set of options:
53
+
54
+ Options:
55
+ f, [--filepath=FILEPATH]
56
+ # Default: ~/.instance_accountant
57
+ d, --description=DESCRIPTION
58
+ # Default: instance usage for
59
+ [--reference=REFERENCE]
60
+ c, --cost=COST
61
+ [--cost-description=COST_DESCRIPTION]
62
+ # Default: cost for
63
+ [--cost-reference=COST_REFERENCE]
64
+ e, --expense-acct=EXPENSE_ACCT
65
+ p, --payable-acct=PAYABLE_ACCT
66
+ p, [--price=PRICE]
67
+ [--price-description=PRICE_DESCRIPTION]
68
+ # Default: price for
69
+ [--price-reference=PRICE_REFERENCE]
70
+ i, [--income-acct=INCOME_ACCT]
71
+ r, [--receivable-acct=RECEIVABLE_ACCT]
72
+ k, --key-id=KEY_ID
73
+ s, --secret=SECRET
74
+ o, --org-id=ORG_ID
75
+ b, --book-id=BOOK_ID
76
+ [--daemon], [--no-daemon]
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ # -*- encoding : utf-8 -*-
2
+ # DO NOT EDIT THIS FILE! (Create new task(s) using lib/tasks/*.rake convention
3
+
4
+ Dir[File.expand_path(__FILE__ + '/..') + '/tasks/*.rake' ].each do |f|
5
+ load f
6
+ end
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env ruby
2
+ # -*- encoding : utf-8 -*-
3
+
4
+ $LOAD_PATH.unshift 'lib'
5
+
6
+ require 'rubygems'
7
+
8
+ require 'instance_accountant/cli'
9
+
10
+ InstanceAccountant::CLI.start
@@ -0,0 +1,43 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ $LOAD_PATH.unshift 'lib'
4
+
5
+ require 'instance_accountant'
6
+
7
+ Gem::Specification.new do |gem|
8
+ gem.authors = [ 'Tom Mornini', 'John McKee' ]
9
+
10
+ gem.files = Dir.glob( 'lib/**/*.rb' )
11
+
12
+ gem.files << 'instance_accountant.gemspec'
13
+ gem.files << 'LICENSE.txt'
14
+ gem.files << 'Rakefile'
15
+ gem.files << 'README.md'
16
+
17
+ gem.test_files = Dir.glob( 'spec/**/*.rb' )
18
+
19
+ gem.name = 'instance_accountant'
20
+ gem.description = 'account for hourly instance usage using Subledger'
21
+ gem.summary = 'account for hourly instance usage using Subledger'
22
+ gem.version = InstanceAccountant::VERSION
23
+ gem.email = 'admin@subledger.com'
24
+ gem.licenses = ['BSD 3-Clause']
25
+
26
+ gem.homepage = 'https://github.com/subledger/instance_accountant'
27
+
28
+ gem.bindir = 'bin'
29
+ gem.executables = 'instance_accountant'
30
+
31
+ gem.add_runtime_dependency 'subledger', '~> 0.7'
32
+ gem.add_runtime_dependency 'thor', '~> 0.19'
33
+
34
+ gem.add_development_dependency 'guard', '~> 2.8'
35
+ gem.add_development_dependency 'guard-minitest', '~> 2.3'
36
+ gem.add_development_dependency 'guard-rubocop', '~> 1.1'
37
+ gem.add_development_dependency 'minitest', '~> 5.4'
38
+ gem.add_development_dependency 'rake', '~> 10.3'
39
+ gem.add_development_dependency 'rb-fsevent', '~> 0.9'
40
+ gem.add_development_dependency 'rspec-expectations', '~> 3.1'
41
+ gem.add_development_dependency 'rspec-mocks', '~> 3.1'
42
+ gem.add_development_dependency 'rubocop', '~> 0.27'
43
+ end
@@ -0,0 +1,123 @@
1
+ # -*- encoding : utf-8 -*-
2
+ require 'thor'
3
+
4
+ require 'instance_accountant'
5
+
6
+ module InstanceAccountant
7
+ class CLI < Thor
8
+ desc 'account', 'Accounts for hours in Subledger'
9
+
10
+ method_option :filepath,
11
+ aliases: :f,
12
+ default: '~/.instance_accountant'
13
+
14
+ method_option :description,
15
+ aliases: :d,
16
+ required: true,
17
+ default: 'instance usage for'
18
+
19
+ method_option :reference
20
+
21
+ method_option :cost,
22
+ aliases: :c,
23
+ required: true
24
+ method_option :cost_description,
25
+ default: 'cost for'
26
+
27
+ method_option :cost_reference
28
+
29
+ method_option :expense_acct,
30
+ aliases: :e,
31
+ required: true
32
+
33
+ method_option :payable_acct,
34
+ aliases: :p,
35
+ required: true
36
+
37
+ method_option :price,
38
+ aliases: :p
39
+
40
+ method_option :price_description,
41
+ default: 'price for'
42
+
43
+ method_option :price_reference
44
+
45
+ method_option :income_acct,
46
+ aliases: :i
47
+
48
+ method_option :receivable_acct,
49
+ aliases: :r
50
+
51
+ method_option :key_id,
52
+ aliases: :k,
53
+ required: true
54
+
55
+ method_option :secret,
56
+ aliases: :s,
57
+ required: true
58
+
59
+ method_option :org_id,
60
+ aliases: :o,
61
+ required: true
62
+
63
+ method_option :book_id,
64
+ aliases: :b,
65
+ required: true
66
+
67
+ method_option :daemon, type: :boolean
68
+
69
+ SIGNALS = []
70
+
71
+ TWENTY_MINUTES = 20 * 60
72
+
73
+ %w(ABRT ALRM HUP INT STOP TERM QUIT).each do |signal|
74
+ Signal.trap signal do
75
+ SIGNALS << signal
76
+ end
77
+ end
78
+
79
+ def account
80
+ if price_but_not_income_and_receivable options
81
+ STDERR.puts '--price requires --income_acct and --receivable_acct'
82
+ exit 0
83
+ end
84
+
85
+ i = 0
86
+
87
+ loop do
88
+ consider_posting i, options
89
+ consider_exiting
90
+ sleep 1
91
+ i += 1
92
+ end
93
+ end
94
+
95
+ private
96
+
97
+ def consider_posting i, options
98
+ Poster.new(options).post(Hour.new, Time.now) if posting_required? i
99
+ end
100
+
101
+ def posting_required? i
102
+ (i % TWENTY_MINUTES).zero? or signal?
103
+ end
104
+
105
+ def consider_exiting
106
+ exit 1 if signal?
107
+ exit 0 if not_daemon?
108
+ end
109
+
110
+ def signal?
111
+ SIGNALS.length > 0
112
+ end
113
+
114
+ def not_daemon?
115
+ options[:daemon].nil?
116
+ end
117
+
118
+ def price_but_not_income_and_receivable options
119
+ options[:price] and
120
+ ! (options[:income_acct] and options[:receivable_acct])
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,37 @@
1
+ # -*- encoding : utf-8 -*-
2
+
3
+ module InstanceAccountant
4
+ class Filer
5
+ def initialize options = { }
6
+ @file_klass = options[:file_klass] || File
7
+ @time_klass = options[:time_klass] || Time
8
+
9
+ @filepath = @file_klass.expand_path options[:filepath]
10
+ end
11
+
12
+ def read
13
+ @time_klass.parse @file_klass.open( @filepath,
14
+ File::RDWR | File::CREAT,
15
+ 0644 ) do |file|
16
+ file.flock File::LOCK_EX
17
+ file.read
18
+ end
19
+ rescue
20
+ nil
21
+ end
22
+
23
+ def write hour
24
+ hour_string = hour.to_s
25
+
26
+ @file_klass.open( @filepath,
27
+ File::RDWR | File::CREAT,
28
+ 0644 ) do |file|
29
+ file.flock File::LOCK_EX
30
+ file.rewind
31
+ file.write hour_string
32
+ end
33
+
34
+ hour_string
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,28 @@
1
+ # -*- encoding : utf-8 -*-
2
+ require 'time'
3
+
4
+ module InstanceAccountant
5
+ class Hour
6
+ def initialize options = { }
7
+ @time = options[:time] ? options[:time].utc : now(options)
8
+ end
9
+
10
+ def == other
11
+ hour_string == other.hour_string
12
+ end
13
+
14
+ def to_s
15
+ @time.iso8601
16
+ end
17
+
18
+ def hour_string
19
+ to_s[0..12] + ':00:00Z'
20
+ end
21
+
22
+ private
23
+
24
+ def now options
25
+ (options[:time_klass] || ::Time).now.utc
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,116 @@
1
+ # -*- encoding : utf-8 -*-
2
+ require 'subledger'
3
+
4
+ module InstanceAccountant
5
+ class JournalEntry
6
+ def initialize options = { }
7
+ @options = options
8
+
9
+ @subledger = options[:subledger] || Subledger.new(@options.dup)
10
+ end
11
+
12
+ def hash_for hour, now
13
+ hour_string = hour.hour_string
14
+
15
+ lines = cost_lines hour_string
16
+
17
+ lines += price_lines(hour_string) if include_price_lines?
18
+
19
+ hash = {
20
+ effective_at: now,
21
+ description: description(hour_string),
22
+ lines: lines
23
+ }
24
+
25
+ hash.merge!(reference: @options[:reference]) if @options[:reference]
26
+
27
+ hash
28
+ end
29
+
30
+ private
31
+
32
+ def include_price_lines?
33
+ @options[:receivable_acct] and
34
+ @options[:income_acct] and
35
+ @options[:price]
36
+ end
37
+
38
+ def description hour_string
39
+ format @options[:description], hour_string
40
+ end
41
+
42
+ def cost_lines hour_string
43
+ lines = [ ]
44
+
45
+ common = { description: @options[:cost_description],
46
+ reference: @options[:cost_reference] }
47
+
48
+ lines << complete(
49
+ hour_string,
50
+ common.merge( account: expense_acct,
51
+ value: @subledger.debit(@options[:cost]) ) )
52
+
53
+ lines << complete(
54
+ hour_string,
55
+ common.merge( account: payable_acct,
56
+ value: @subledger.credit(@options[:cost]) ) )
57
+
58
+ lines
59
+ end
60
+
61
+ def price_lines hour_string
62
+ lines = [ ]
63
+
64
+ common = { description: @options[:price_description],
65
+ reference: @options[:price_reference] }
66
+
67
+ lines << complete(
68
+ hour_string,
69
+ common.merge( account: receivable_acct,
70
+ value: @subledger.debit(@options[:price]) ) )
71
+
72
+ lines << complete(
73
+ hour_string,
74
+ common.merge( account: income_acct,
75
+ value: @subledger.credit(@options[:price]) ) )
76
+
77
+ lines
78
+ end
79
+
80
+ def complete hour_string, args
81
+ description = args[:description].nil? ? nil : description(hour_string)
82
+
83
+ if description
84
+ args.merge! description: description
85
+ else
86
+ args.delete :description
87
+ end
88
+
89
+ reference = args[:reference]
90
+
91
+ if reference
92
+ args.merge! reference: reference
93
+ else
94
+ args.delete :reference
95
+ end
96
+
97
+ args
98
+ end
99
+
100
+ def expense_acct
101
+ @subledger.account id: @options[:expense_acct]
102
+ end
103
+
104
+ def payable_acct
105
+ @subledger.account id: @options[:payable_acct]
106
+ end
107
+
108
+ def receivable_acct
109
+ @subledger.account id: @options[:receivable_acct]
110
+ end
111
+
112
+ def income_acct
113
+ @subledger.account id: @options[:income_acct]
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,52 @@
1
+ # -*- encoding : utf-8 -*-
2
+ require 'subledger'
3
+
4
+ require 'time'
5
+
6
+ require 'instance_accountant/filer'
7
+ require 'instance_accountant/hour'
8
+ require 'instance_accountant/journal_entry'
9
+
10
+ module InstanceAccountant
11
+ class Poster
12
+ def initialize options = { }
13
+ @options = options
14
+
15
+ @time_klass = @options[:time_klass] || Time
16
+ @hour_klass = @options[:hour_klass] || Hour
17
+
18
+ @subledger = @options[:subledger] || Subledger.new(@options.dup)
19
+ @filer = @options[:filer] || Filer.new(@options)
20
+ @journal_entry = @options[:journal_entry] || JournalEntry.new(@options)
21
+ end
22
+
23
+ def post hour, now
24
+ if posted? hour
25
+ false
26
+ else
27
+ write hour, now
28
+ true
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def posted? hour
35
+ file_time = @filer.read
36
+
37
+ if file_time.nil?
38
+ false
39
+ else
40
+ @hour_klass.new( time: file_time ) == hour
41
+ end
42
+ end
43
+
44
+ def write hour, now
45
+ @subledger
46
+ .journal_entry
47
+ .create_and_post( @journal_entry.hash_for hour, now )
48
+
49
+ @filer.write hour
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,8 @@
1
+ # -*- encoding : utf-8 -*-
2
+ require 'instance_accountant/filer'
3
+ require 'instance_accountant/hour'
4
+ require 'instance_accountant/poster'
5
+
6
+ module InstanceAccountant
7
+ VERSION = '0.0.1'
8
+ end
@@ -0,0 +1,83 @@
1
+ # -*- encoding : utf-8 -*-
2
+ require 'singleton'
3
+
4
+ gem 'minitest'
5
+
6
+ require 'minitest/spec'
7
+
8
+ require 'minitest/pride' unless ENV['SUBLEDGER_ENV'] == 'ci'
9
+
10
+ require 'minitest/autorun'
11
+
12
+ MiniTest::Test.parallelize_me!
13
+
14
+ module Kernel
15
+ def Class thing, &block
16
+ describe_set_subject thing, &block
17
+ end
18
+
19
+ def Constant thing, &block
20
+ describe_set_subject thing, &block
21
+ end
22
+
23
+ def Module thing, &block
24
+ describe_set_subject thing, &block
25
+ end
26
+
27
+ def Instance &block
28
+ describe 'instance', &block
29
+ end
30
+
31
+ def RespondsTo desc, &block
32
+ describe "responds to #{desc}", &block
33
+ end
34
+
35
+ def When desc, &block
36
+ describe "when #{desc}", &block
37
+ end
38
+
39
+ def And desc, &block
40
+ describe "and #{desc}", &block
41
+ end
42
+
43
+ def RoleTest desc
44
+ load Subledger::ROOT + "/spec/roles/#{desc}_spec.rb"
45
+ end
46
+
47
+ private
48
+
49
+ # Thanks Anton Lindqvist!
50
+ # https://github.com/mptre/minitest-implicit-subject
51
+
52
+ def describe_set_subject *args, &block
53
+ cls = describe *args, &block
54
+
55
+ subject = args.first
56
+
57
+ if subject.respond_to?(:included_modules) &&
58
+ Array(subject.included_modules).include?(Singleton)
59
+
60
+ subject = subject.instance
61
+ end
62
+
63
+ cls.subject { subject }
64
+
65
+ cls
66
+ end
67
+ end
68
+
69
+ module MiniTest
70
+ class Spec
71
+ def self.ByRaising desc, &block
72
+ it "by raising #{desc}", &block
73
+ end
74
+
75
+ def self.ByReturning desc, &block
76
+ it "by returning #{desc}", &block
77
+ end
78
+
79
+ def self.ByYielding desc, &block
80
+ it "by yielding #{desc}", &block
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,25 @@
1
+ # -*- encoding : utf-8 -*-
2
+ require 'rspec/expectations'
3
+ require 'rspec/mocks'
4
+
5
+ module MiniTest
6
+ class Test
7
+ include ::RSpec::Mocks::ExampleMethods
8
+
9
+ def before_setup
10
+ ::RSpec::Mocks.setup
11
+ super
12
+ end
13
+
14
+ def after_teardown
15
+ super
16
+ ::RSpec::Mocks.verify
17
+ ensure
18
+ ::RSpec::Mocks.teardown
19
+ end
20
+ end
21
+
22
+ class Spec
23
+ include RSpec::Matchers
24
+ end
25
+ end
data/spec/helpers.rb ADDED
@@ -0,0 +1,9 @@
1
+ # -*- encoding : utf-8 -*-
2
+
3
+ require 'rubygems'
4
+ require 'bundler'
5
+
6
+ Bundler.setup
7
+
8
+ require 'helpers/minitest'
9
+ require 'helpers/rspec'
@@ -0,0 +1,7 @@
1
+ # -*- encoding : utf-8 -*-
2
+ require 'helpers'
3
+
4
+ require 'instance_accountant/cli'
5
+
6
+ module InstanceAccountant
7
+ end
@@ -0,0 +1,72 @@
1
+ # -*- encoding : utf-8 -*-
2
+ require 'helpers'
3
+
4
+ require 'instance_accountant/filer'
5
+
6
+ module InstanceAccountant
7
+ Class File do
8
+ Instance do
9
+ subject { Filer.new options }
10
+
11
+ let(:options) do
12
+ {
13
+ filepath: filepath,
14
+ file_klass: file_klass,
15
+ time_klass: time_klass
16
+ }
17
+ end
18
+
19
+ let(:filepath) { double 'filepath' }
20
+ let(:expanded_filepath) { double 'expanded_filepath' }
21
+ let(:file_klass) { double 'file_klass' }
22
+ let(:time_klass) { double 'time_klass' }
23
+ let(:time_inst) { double 'time_inst' }
24
+
25
+ let(:time_string) { double 'time_string' }
26
+ let(:hour) { double 'hour' }
27
+
28
+ RespondsTo :read do
29
+ When 'it has been written before' do
30
+ ByReturning 'a Time' do
31
+ expect(file_klass)
32
+ .to receive(:expand_path)
33
+ .with(filepath)
34
+ .and_return(expanded_filepath)
35
+
36
+ expect(file_klass)
37
+ .to receive(:open)
38
+ .with(expanded_filepath, File::RDWR | File::CREAT, 0644)
39
+ .and_return(time_string)
40
+
41
+ expect(time_klass)
42
+ .to receive(:parse)
43
+ .with(time_string)
44
+ .and_return(time_inst)
45
+
46
+ subject.read.must_be_same_as time_inst
47
+ end
48
+ end
49
+ end
50
+
51
+ RespondsTo :write do
52
+ ByReturning 'the hour' do
53
+ expect(file_klass)
54
+ .to receive(:expand_path)
55
+ .with(filepath)
56
+ .and_return(expanded_filepath)
57
+
58
+ expect(hour)
59
+ .to receive(:to_s)
60
+ .and_return(time_string)
61
+
62
+ expect(file_klass)
63
+ .to receive(:open)
64
+ .with(expanded_filepath, File::RDWR | File::CREAT, 0644)
65
+ .and_return(time_string)
66
+
67
+ subject.write(hour).must_be_same_as time_string
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,136 @@
1
+ # -*- encoding : utf-8 -*-
2
+ require 'helpers'
3
+
4
+ require 'instance_accountant/hour'
5
+
6
+ module InstanceAccountant
7
+ Class Hour do
8
+ Instance do
9
+ subject { Hour.new options }
10
+
11
+ let(:options) do
12
+ {
13
+ time_klass: time_klass
14
+ }
15
+ end
16
+
17
+ let(:time_klass) { double 'time_klass' }
18
+
19
+ let(:time_local) { double 'time_local' }
20
+ let(:time_utc) { double 'time_utc' }
21
+ let(:time_iso) { double 'time_iso' }
22
+ let(:hour_string) { double 'hour_string' }
23
+
24
+ let(:other_hour) { double 'other_hour' }
25
+ let(:other_hour_string) { double 'other_hour_string' }
26
+
27
+ let(:other_time_local) { double 'other_time_local' }
28
+ let(:other_time_utc) { double 'other_time_utc' }
29
+ let(:other_time_iso) { double 'other_time_iso' }
30
+
31
+ RespondsTo :to_s do
32
+ ByReturning 'an hour string' do
33
+ expect(time_klass)
34
+ .to receive(:now)
35
+ .and_return(time_local)
36
+
37
+ expect(time_local)
38
+ .to receive(:utc)
39
+ .and_return(time_utc)
40
+
41
+ expect(time_utc)
42
+ .to receive(:iso8601)
43
+ .and_return(time_iso)
44
+
45
+ subject.to_s.must_equal time_iso
46
+ end
47
+ end
48
+
49
+ RespondsTo :hour_string do
50
+ ByReturning 'an hour string' do
51
+ expect(time_klass)
52
+ .to receive(:now)
53
+ .and_return(time_local)
54
+
55
+ expect(time_local)
56
+ .to receive(:utc)
57
+ .and_return(time_utc)
58
+
59
+ expect(time_utc)
60
+ .to receive(:iso8601)
61
+ .and_return('2014-11-05T00')
62
+
63
+ subject.hour_string.must_equal '2014-11-05T00:00:00Z'
64
+ end
65
+ end
66
+
67
+ RespondsTo :== do
68
+ When 'other is equal' do
69
+ ByReturning true do
70
+
71
+ expect(time_klass)
72
+ .to receive(:now)
73
+ .and_return(time_local)
74
+
75
+ expect(time_local)
76
+ .to receive(:utc)
77
+ .and_return(time_utc)
78
+
79
+ expect(time_utc)
80
+ .to receive(:iso8601)
81
+ .and_return(time_iso)
82
+
83
+ expect(time_iso)
84
+ .to receive(:[])
85
+ .with(0..12)
86
+ .and_return(hour_string)
87
+
88
+ expect(other_hour)
89
+ .to receive(:hour_string)
90
+ .and_return(hour_string)
91
+
92
+ expect(hour_string)
93
+ .to receive(:+)
94
+ .with(':00:00Z')
95
+ .and_return(hour_string)
96
+
97
+ (subject == other_hour).must_equal true
98
+ end
99
+ end
100
+
101
+ When 'other is not equal' do
102
+ ByReturning false do
103
+
104
+ expect(time_klass)
105
+ .to receive(:now)
106
+ .and_return(time_local)
107
+
108
+ expect(time_local)
109
+ .to receive(:utc)
110
+ .and_return(time_utc)
111
+
112
+ expect(time_utc)
113
+ .to receive(:iso8601)
114
+ .and_return(time_iso)
115
+
116
+ expect(time_iso)
117
+ .to receive(:[])
118
+ .with(0..12)
119
+ .and_return(hour_string)
120
+
121
+ expect(hour_string)
122
+ .to receive(:+)
123
+ .with(':00:00Z')
124
+ .and_return(hour_string)
125
+
126
+ expect(other_hour)
127
+ .to receive(:hour_string)
128
+ .and_return(other_hour_string)
129
+
130
+ (subject == other_hour).must_equal false
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,178 @@
1
+ # -*- encoding : utf-8 -*-
2
+ require 'helpers'
3
+
4
+ require 'instance_accountant/journal_entry'
5
+
6
+ module InstanceAccountant
7
+ Class JournalEntry do
8
+ Instance do
9
+ let(:options) do
10
+ {
11
+ subledger: subledger,
12
+ description: description,
13
+ cost: cost,
14
+ expense_acct: expense_acct,
15
+ payable_acct: payable_acct
16
+ }
17
+ end
18
+
19
+ let(:price_options) do
20
+ {
21
+ price: price,
22
+ income_acct: income_acct,
23
+ receivable_acct: receivable_acct
24
+ }
25
+ end
26
+
27
+ let(:subledger) { double 'subledger' }
28
+ let(:description) { 'description with time %s' }
29
+
30
+ let(:cost) { double 'cost' }
31
+ let(:expense_acct) { double 'expense_acct' }
32
+ let(:payable_acct) { double 'payable_acct' }
33
+
34
+ let(:price) { double 'price' }
35
+ let(:income_acct) { double 'income_acct' }
36
+ let(:receivable_acct) { double 'receivable_acct' }
37
+
38
+ let(:expense_debit) { double 'expense_debit' }
39
+ let(:payable_credit) { double 'payable_credit' }
40
+
41
+ let(:receivable_debit) { double 'receivable_debit' }
42
+ let(:income_credit) { double 'income_credit' }
43
+
44
+ let(:concat_desc) { 'description with time 2014-11-05T18:00:00Z' }
45
+
46
+ let(:hour) { double 'hour' }
47
+ let(:hour_string) { '2014-11-05T18:00:00Z' }
48
+
49
+ let(:now) { double 'now' }
50
+
51
+ RespondsTo :hash_for do
52
+ When 'price lines are not included' do
53
+ subject { JournalEntry.new options }
54
+
55
+ ByReturning true do
56
+ expect(hour)
57
+ .to receive(:hour_string)
58
+ .and_return(hour_string)
59
+
60
+ expect(subledger)
61
+ .to receive(:account)
62
+ .with(id: expense_acct)
63
+ .and_return(expense_acct)
64
+
65
+ expect(subledger)
66
+ .to receive(:debit)
67
+ .with(cost)
68
+ .and_return(expense_debit)
69
+
70
+ expect(subledger)
71
+ .to receive(:account)
72
+ .with(id: payable_acct)
73
+ .and_return(payable_acct)
74
+
75
+ expect(subledger)
76
+ .to receive(:credit)
77
+ .with(cost)
78
+ .and_return(payable_credit)
79
+
80
+ # expect(description)
81
+ # .to receive(:+)
82
+ # .and_return(concat_desc)
83
+
84
+ subject.hash_for(hour, now)
85
+ .must_equal effective_at: now,
86
+ description: concat_desc,
87
+ lines: [
88
+ {
89
+ account: expense_acct,
90
+ value: expense_debit
91
+ },
92
+ {
93
+ account: payable_acct,
94
+ value: payable_credit
95
+ }
96
+ ]
97
+ end
98
+ end
99
+
100
+ When 'price lines are included' do
101
+ subject { JournalEntry.new options.merge price_options }
102
+
103
+ ByReturning true do
104
+ expect(hour)
105
+ .to receive(:hour_string)
106
+ .and_return(hour_string)
107
+
108
+ expect(subledger)
109
+ .to receive(:account)
110
+ .with(id: expense_acct)
111
+ .and_return(expense_acct)
112
+
113
+ expect(subledger)
114
+ .to receive(:debit)
115
+ .with(cost)
116
+ .and_return(expense_debit)
117
+
118
+ expect(subledger)
119
+ .to receive(:account)
120
+ .with(id: payable_acct)
121
+ .and_return(payable_acct)
122
+
123
+ expect(subledger)
124
+ .to receive(:credit)
125
+ .with(cost)
126
+ .and_return(payable_credit)
127
+
128
+ expect(subledger)
129
+ .to receive(:account)
130
+ .with(id: receivable_acct)
131
+ .and_return(receivable_acct)
132
+
133
+ expect(subledger)
134
+ .to receive(:debit)
135
+ .with(price)
136
+ .and_return(receivable_debit)
137
+
138
+ expect(subledger)
139
+ .to receive(:account)
140
+ .with(id: income_acct)
141
+ .and_return(income_acct)
142
+
143
+ expect(subledger)
144
+ .to receive(:credit)
145
+ .with(price)
146
+ .and_return(income_credit)
147
+
148
+ # expect(description)
149
+ # .to receive(:+)
150
+ # .and_return(concat_desc)
151
+
152
+ subject.hash_for(hour, now)
153
+ .must_equal effective_at: now,
154
+ description: concat_desc,
155
+ lines: [
156
+ {
157
+ account: expense_acct,
158
+ value: expense_debit
159
+ },
160
+ {
161
+ account: payable_acct,
162
+ value: payable_credit
163
+ },
164
+ {
165
+ account: receivable_acct,
166
+ value: receivable_debit
167
+ },
168
+ {
169
+ account: income_acct,
170
+ value: income_credit
171
+ }
172
+ ]
173
+ end
174
+ end
175
+ end
176
+ end
177
+ end
178
+ end
@@ -0,0 +1,109 @@
1
+ # -*- encoding : utf-8 -*-
2
+ require 'helpers'
3
+
4
+ require 'instance_accountant/poster'
5
+
6
+ module InstanceAccountant
7
+ Class Poster do
8
+ Instance do
9
+ subject { Poster.new options }
10
+
11
+ let(:options) do
12
+ {
13
+ subledger: subledger,
14
+ filer: filer,
15
+ hour_klass: hour_klass,
16
+ journal_entry: journal_entry
17
+ }
18
+ end
19
+
20
+ let(:subledger) { double 'subledger' }
21
+ let(:sl_journal_entry) { double 'sl_journal_entry' }
22
+
23
+ let(:filer) { double 'filer' }
24
+ let(:hour_klass) { double 'hour_klass' }
25
+ let(:journal_entry) { double 'journal_entry' }
26
+
27
+ let(:journal_entry_hash) { double 'journal_entry_hash' }
28
+ let(:hour) { double 'hour' }
29
+ let(:different_hour) { double 'different_hour' }
30
+ let(:now) { double 'now' }
31
+
32
+ RespondsTo :post do
33
+ When 'no hours have been posted' do
34
+ ByReturning true do
35
+ expect(filer)
36
+ .to receive(:read)
37
+ .and_return(nil)
38
+
39
+ expect(journal_entry)
40
+ .to receive(:hash_for)
41
+ .with(hour, now)
42
+ .and_return(journal_entry_hash)
43
+
44
+ expect(filer)
45
+ .to receive(:write)
46
+ .with(hour)
47
+
48
+ expect(subledger)
49
+ .to receive(:journal_entry)
50
+ .and_return(sl_journal_entry)
51
+
52
+ expect(sl_journal_entry)
53
+ .to receive(:create_and_post)
54
+ .with(journal_entry_hash)
55
+
56
+ subject.post(hour, now).must_equal true
57
+ end
58
+ end
59
+
60
+ When 'the hour has not been posted' do
61
+ ByReturning true do
62
+ expect(filer)
63
+ .to receive(:read)
64
+ .and_return(different_hour)
65
+
66
+ expect(hour_klass)
67
+ .to receive(:new)
68
+ .with(time: different_hour)
69
+ .and_return(different_hour)
70
+
71
+ expect(journal_entry)
72
+ .to receive(:hash_for)
73
+ .with(hour, now)
74
+ .and_return(journal_entry_hash)
75
+
76
+ expect(subledger)
77
+ .to receive(:journal_entry)
78
+ .and_return(sl_journal_entry)
79
+
80
+ expect(sl_journal_entry)
81
+ .to receive(:create_and_post)
82
+ .with(journal_entry_hash)
83
+
84
+ expect(filer)
85
+ .to receive(:write)
86
+ .with(hour)
87
+
88
+ subject.post(hour, now).must_equal true
89
+ end
90
+ end
91
+
92
+ When 'the hour has been posted' do
93
+ ByReturning false do
94
+ expect(filer)
95
+ .to receive(:read)
96
+ .and_return(hour)
97
+
98
+ expect(hour_klass)
99
+ .to receive(:new)
100
+ .with(time: hour)
101
+ .and_return(hour)
102
+
103
+ subject.post(hour, now).must_equal false
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
metadata ADDED
@@ -0,0 +1,226 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: instance_accountant
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Tom Mornini
8
+ - John McKee
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2014-11-05 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: subledger
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - "~>"
19
+ - !ruby/object:Gem::Version
20
+ version: '0.7'
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - "~>"
26
+ - !ruby/object:Gem::Version
27
+ version: '0.7'
28
+ - !ruby/object:Gem::Dependency
29
+ name: thor
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - "~>"
33
+ - !ruby/object:Gem::Version
34
+ version: '0.19'
35
+ type: :runtime
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - "~>"
40
+ - !ruby/object:Gem::Version
41
+ version: '0.19'
42
+ - !ruby/object:Gem::Dependency
43
+ name: guard
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - "~>"
47
+ - !ruby/object:Gem::Version
48
+ version: '2.8'
49
+ type: :development
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - "~>"
54
+ - !ruby/object:Gem::Version
55
+ version: '2.8'
56
+ - !ruby/object:Gem::Dependency
57
+ name: guard-minitest
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - "~>"
61
+ - !ruby/object:Gem::Version
62
+ version: '2.3'
63
+ type: :development
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - "~>"
68
+ - !ruby/object:Gem::Version
69
+ version: '2.3'
70
+ - !ruby/object:Gem::Dependency
71
+ name: guard-rubocop
72
+ requirement: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - "~>"
75
+ - !ruby/object:Gem::Version
76
+ version: '1.1'
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - "~>"
82
+ - !ruby/object:Gem::Version
83
+ version: '1.1'
84
+ - !ruby/object:Gem::Dependency
85
+ name: minitest
86
+ requirement: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - "~>"
89
+ - !ruby/object:Gem::Version
90
+ version: '5.4'
91
+ type: :development
92
+ prerelease: false
93
+ version_requirements: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - "~>"
96
+ - !ruby/object:Gem::Version
97
+ version: '5.4'
98
+ - !ruby/object:Gem::Dependency
99
+ name: rake
100
+ requirement: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - "~>"
103
+ - !ruby/object:Gem::Version
104
+ version: '10.3'
105
+ type: :development
106
+ prerelease: false
107
+ version_requirements: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - "~>"
110
+ - !ruby/object:Gem::Version
111
+ version: '10.3'
112
+ - !ruby/object:Gem::Dependency
113
+ name: rb-fsevent
114
+ requirement: !ruby/object:Gem::Requirement
115
+ requirements:
116
+ - - "~>"
117
+ - !ruby/object:Gem::Version
118
+ version: '0.9'
119
+ type: :development
120
+ prerelease: false
121
+ version_requirements: !ruby/object:Gem::Requirement
122
+ requirements:
123
+ - - "~>"
124
+ - !ruby/object:Gem::Version
125
+ version: '0.9'
126
+ - !ruby/object:Gem::Dependency
127
+ name: rspec-expectations
128
+ requirement: !ruby/object:Gem::Requirement
129
+ requirements:
130
+ - - "~>"
131
+ - !ruby/object:Gem::Version
132
+ version: '3.1'
133
+ type: :development
134
+ prerelease: false
135
+ version_requirements: !ruby/object:Gem::Requirement
136
+ requirements:
137
+ - - "~>"
138
+ - !ruby/object:Gem::Version
139
+ version: '3.1'
140
+ - !ruby/object:Gem::Dependency
141
+ name: rspec-mocks
142
+ requirement: !ruby/object:Gem::Requirement
143
+ requirements:
144
+ - - "~>"
145
+ - !ruby/object:Gem::Version
146
+ version: '3.1'
147
+ type: :development
148
+ prerelease: false
149
+ version_requirements: !ruby/object:Gem::Requirement
150
+ requirements:
151
+ - - "~>"
152
+ - !ruby/object:Gem::Version
153
+ version: '3.1'
154
+ - !ruby/object:Gem::Dependency
155
+ name: rubocop
156
+ requirement: !ruby/object:Gem::Requirement
157
+ requirements:
158
+ - - "~>"
159
+ - !ruby/object:Gem::Version
160
+ version: '0.27'
161
+ type: :development
162
+ prerelease: false
163
+ version_requirements: !ruby/object:Gem::Requirement
164
+ requirements:
165
+ - - "~>"
166
+ - !ruby/object:Gem::Version
167
+ version: '0.27'
168
+ description: account for hourly instance usage using Subledger
169
+ email: admin@subledger.com
170
+ executables:
171
+ - instance_accountant
172
+ extensions: []
173
+ extra_rdoc_files: []
174
+ files:
175
+ - LICENSE.txt
176
+ - README.md
177
+ - Rakefile
178
+ - bin/instance_accountant
179
+ - instance_accountant.gemspec
180
+ - lib/instance_accountant.rb
181
+ - lib/instance_accountant/cli.rb
182
+ - lib/instance_accountant/filer.rb
183
+ - lib/instance_accountant/hour.rb
184
+ - lib/instance_accountant/journal_entry.rb
185
+ - lib/instance_accountant/poster.rb
186
+ - spec/helpers.rb
187
+ - spec/helpers/minitest.rb
188
+ - spec/helpers/rspec.rb
189
+ - spec/instance_accountant/cli_spec.rb
190
+ - spec/instance_accountant/filer_spec.rb
191
+ - spec/instance_accountant/hour_spec.rb
192
+ - spec/instance_accountant/journal_entry_spec.rb
193
+ - spec/instance_accountant/poster_spec.rb
194
+ homepage: https://github.com/subledger/instance_accountant
195
+ licenses:
196
+ - BSD 3-Clause
197
+ metadata: {}
198
+ post_install_message:
199
+ rdoc_options: []
200
+ require_paths:
201
+ - lib
202
+ required_ruby_version: !ruby/object:Gem::Requirement
203
+ requirements:
204
+ - - ">="
205
+ - !ruby/object:Gem::Version
206
+ version: '0'
207
+ required_rubygems_version: !ruby/object:Gem::Requirement
208
+ requirements:
209
+ - - ">="
210
+ - !ruby/object:Gem::Version
211
+ version: '0'
212
+ requirements: []
213
+ rubyforge_project:
214
+ rubygems_version: 2.4.2
215
+ signing_key:
216
+ specification_version: 4
217
+ summary: account for hourly instance usage using Subledger
218
+ test_files:
219
+ - spec/helpers/minitest.rb
220
+ - spec/helpers/rspec.rb
221
+ - spec/helpers.rb
222
+ - spec/instance_accountant/cli_spec.rb
223
+ - spec/instance_accountant/filer_spec.rb
224
+ - spec/instance_accountant/hour_spec.rb
225
+ - spec/instance_accountant/journal_entry_spec.rb
226
+ - spec/instance_accountant/poster_spec.rb