shushu 0.1.5

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.
@@ -0,0 +1,46 @@
1
+ module Shushu
2
+ module HttpHelpers
3
+ extend self
4
+
5
+ def build_q_params(hash)
6
+ "?" + hash.map do |k, v|
7
+ CGI.escape("#{k}=#{v}")
8
+ end.join("&")
9
+ end
10
+
11
+ def headers
12
+ {:content_type => :json, :accept => :json}
13
+ end
14
+
15
+ def enc_json(hash)
16
+ Yajl::Encoder.encode(hash)
17
+ end
18
+
19
+ def dec_json(json)
20
+ Yajl::Parser.parse(json)
21
+ end
22
+
23
+ def handle_req(&blk)
24
+ begin
25
+ resp = yield
26
+ dec_json(resp.body)
27
+ rescue RestClient::Exception => e
28
+ body = dec_json(e.http_body)
29
+ case e.http_code
30
+ when 404
31
+ when 401
32
+ raise(AuthorizationError, "Response: #{body.inspect}")
33
+ when 403
34
+ when 409
35
+ when 500
36
+ raise(UnexpectedError, "Response: #{body.inspect}")
37
+ when 503
38
+ raise(ServiceDownError, "#{Shushu.url} is down for maintenance.")
39
+ else
40
+ raise(UnexpectedError, "Response: #{body.inspect}")
41
+ end
42
+ end
43
+ end
44
+
45
+ end
46
+ end
@@ -0,0 +1,13 @@
1
+ module Shushu
2
+ module Account
3
+ extend self
4
+
5
+ def create
6
+ Shushu.handle_req {RestClient.post(accounts_url, {}, Shushu.headers)}
7
+ end
8
+
9
+ def accounts_url
10
+ [Shushu.url, "/accounts"].join
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,31 @@
1
+ module Shushu
2
+ module AcctOwn
3
+ extend self
4
+
5
+ def act(args)
6
+ pmid, eid = args.delete(:payment_method_id), args.delete(:entity_id)
7
+ Shushu.handle_req {RestClient.post(acct_own_url(pmid, eid), args, Shushu.headers)}
8
+ end
9
+
10
+ def xfr(args)
11
+ prev_pmid, prev_eid = args.delete(:prev_payment_method_id), args.delete(:prev_entity_id)
12
+ Shushu.handle_req {RestClient.put(acct_own_url(prev_pmid, prev_eid), args, Shushu.headers)}
13
+ end
14
+
15
+ def deact(args)
16
+ pmid = args.delete(:payment_method_id)
17
+ eid = args.delete(:entity_id)
18
+ account_id = args[:account_id]
19
+ time = args[:time]
20
+ Shushu.handle_req {RestClient.delete([acct_own_url(pmid, eid), CGI.encode("?account_id=#{account_id}&time=#{time}")].join)}
21
+ end
22
+
23
+ def query(args)
24
+ Shushu.handle_req {RestClient.get([Shushu.url, "/accounts/#{args[:account_id]}/resource_ownerships"].join)}
25
+ end
26
+
27
+ def acct_own_url(payment_method_id, entity_id)
28
+ [Shushu.url, "/payment_methods/#{payment_method_id}/account_ownerships/#{entity_id}"].join
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,31 @@
1
+ module Shushu
2
+ module BEvent
3
+ extend self
4
+
5
+ OPEN = "open"
6
+ CLOSE = "close"
7
+
8
+ def all
9
+ Shushu.handle_req {RestClient.get([Shushu.url, "/billable_events"].join, Shushu.headers)}
10
+ end
11
+
12
+ def open(args)
13
+ hid = args.delete(:hid)
14
+ entity_id = args.delete(:entity_id)
15
+ args[:state] = OPEN
16
+ Shushu.handle_req {RestClient.put(events_url(hid, entity_id), args, Shushu.headers)}
17
+ end
18
+
19
+ def close(args)
20
+ hid = args.delete(:hid)
21
+ entity_id = args.delete(:entity_id)
22
+ args[:state] = CLOSE
23
+ Shushu.handle_req {RestClient.put(events_url(hid, entity_id), args, Shushu.headers)}
24
+ end
25
+
26
+ def events_url(hid, entity_id)
27
+ [Shushu.url, "/resources/#{hid}/billable_events/#{entity_id}"].join
28
+ end
29
+
30
+ end
31
+ end
@@ -0,0 +1,35 @@
1
+ module Shushu
2
+ class BillableUnit
3
+ attr_accessor(
4
+ :hid,
5
+ :from,
6
+ :to,
7
+ :qty,
8
+ :rate,
9
+ :rate_period,
10
+ :product_group,
11
+ :product_name
12
+ )
13
+
14
+ def initialize(args_hash)
15
+ @hid = args_hash["hid"]
16
+ @from = args_hash["from"]
17
+ @to = args_hash["to"]
18
+ @qty = args_hash["qty"].to_f
19
+ @rate = args_hash["rate"].to_i
20
+ @rate_period = args_hash["rate_period"]
21
+ @product_name = args_hash["product_name"]
22
+ @product_group = args_hash["product_group"]
23
+ end
24
+
25
+ def id
26
+ #TODO remove stub
27
+ Time.now.to_i
28
+ end
29
+
30
+ def total
31
+ @rate * @qty
32
+ end
33
+
34
+ end
35
+ end
@@ -0,0 +1,10 @@
1
+ module Shushu
2
+ module HeartBeat
3
+ extend self
4
+
5
+ def alive?
6
+ Shushu.handle_req {RestClient.get([Shushu.url, "/heartbeat"].join, Shushu.headers)}
7
+ end
8
+
9
+ end
10
+ end
@@ -0,0 +1,19 @@
1
+ module Shushu
2
+ class LineItem
3
+
4
+ attr_reader :unit_groups
5
+
6
+ def initialize(unit_groups)
7
+ @unit_groups = unit_groups
8
+ end
9
+
10
+ def app_name
11
+ @unit_groups.first.hid
12
+ end
13
+
14
+ def total
15
+ @unit_groups.map(&:total).reduce(:+)
16
+ end
17
+
18
+ end
19
+ end
@@ -0,0 +1,14 @@
1
+ module Shushu
2
+ module RCode
3
+ extend self
4
+
5
+ def create(args)
6
+ Shushu.handle_req {RestClient.post(rate_codes_url, args, Shushu.headers)}
7
+ end
8
+
9
+ def rate_codes_url
10
+ [Shushu.url, "/rate_codes"].join
11
+ end
12
+
13
+ end
14
+ end
@@ -0,0 +1,51 @@
1
+ module Shushu
2
+ class Report
3
+ attr_accessor(
4
+ :from,
5
+ :to,
6
+ :billable_units
7
+ )
8
+
9
+ def fetch
10
+ Shushu.handle_req do
11
+ RestClient.get([Shushu.url, resource].join, {:params => {:from => from.utc.to_s, :to => to.utc.to_s}})
12
+ end
13
+ end
14
+
15
+ def billable_units
16
+ @billable_units ||= report["billable_units"].map {|buh| BillableUnit.new(buh)}
17
+ end
18
+
19
+ def total
20
+ report["total"]
21
+ end
22
+
23
+ def report
24
+ @report ||= fetch
25
+ end
26
+ end
27
+
28
+ class UsageReport < Report
29
+ attr_accessor :account_id
30
+
31
+ def initialize(account_id, from, to)
32
+ @account_id, @from, @to = account_id, from, to
33
+ end
34
+
35
+ def resource
36
+ "/accounts/#{@account_id}/usage_reports"
37
+ end
38
+ end
39
+
40
+ class Invoice < Report
41
+ attr_accessor :payment_method_id
42
+
43
+ def initialize(payment_method_id, from, to)
44
+ @payment_method_id, @from, @to = payment_method_id, from, to
45
+ end
46
+
47
+ def resource
48
+ "/payment_methods/#{payment_method_id}/invoices"
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,31 @@
1
+ module Shushu
2
+ module ResOwn
3
+ extend self
4
+
5
+ def act(args)
6
+ aid, eid = args.delete(:account_id), args.delete(:entity_id)
7
+ Shushu.handle_req {RestClient.post(res_own_url(aid, eid), args, Shushu.headers)}
8
+ end
9
+
10
+ def xfr(args)
11
+ prev_aid, prev_eid = args.delete(:prev_account_id), args.delete(:prev_entity_id)
12
+ Shushu.handle_req {RestClient.put(res_own_url(prev_aid, prev_eid), args, Shushu.headers)}
13
+ end
14
+
15
+ def deact(args)
16
+ aid = args.delete(:account_id)
17
+ eid = args.delete(:entity_id)
18
+ hid = args[:hid]
19
+ time = args[:time]
20
+ Shushu.handle_req {RestClient.delete([res_own_url(aid, eid), CGI.encode("?hid=#{hid}&time=#{time}")].join)}
21
+ end
22
+
23
+ def query(args)
24
+ Shushu.handle_req {RestClient.get([Shushu.url, "/accounts/#{args[:account_id]}/resource_ownerships"].join)}
25
+ end
26
+
27
+ def res_own_url(account_id, entity_id)
28
+ [Shushu.url, "/accounts/#{account_id}/resource_ownerships/#{entity_id}"].join
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,13 @@
1
+ module Shushu
2
+ module RevRep
3
+ extend self
4
+
5
+ def get(from, to)
6
+ Shushu.handle_req do
7
+ RestClient.get([Shushu.url, "/rev_report"].join, {:params => {:from => from.utc.to_s, :to => to.utc.to_s}})
8
+ end
9
+ end
10
+
11
+ end
12
+ end
13
+
@@ -0,0 +1,59 @@
1
+ module Shushu
2
+ class UnitGroup
3
+ InvalidUnitGroup = Class.new(ShushuException)
4
+
5
+ attr_reader :units
6
+
7
+ def initialize(units)
8
+ @units = units
9
+ check_rates
10
+ end
11
+
12
+ def check_rates
13
+ if @units.map(&:rate).uniq.length > 1
14
+ raise(InvalidUnitGroup, "Rates must be homogenius.")
15
+ end
16
+ end
17
+
18
+ def total
19
+ rate * qty
20
+ end
21
+
22
+ def qty
23
+ @units.map(&:qty).reduce(:+)
24
+ end
25
+
26
+ def rate
27
+ sample_unit.rate
28
+ end
29
+
30
+ def rate_period
31
+ sample_unit.rate_period
32
+ end
33
+
34
+ def name
35
+ sample_unit.product_name
36
+ end
37
+
38
+ def hid
39
+ sample_unit.hid
40
+ end
41
+
42
+ def product_name
43
+ sample_unit.product_name
44
+ end
45
+
46
+ def product_group
47
+ sample_unit.product_group
48
+ end
49
+
50
+ def description
51
+ sample_unit.product_name
52
+ end
53
+
54
+ def sample_unit
55
+ @sample_unit ||= @units.sample
56
+ end
57
+
58
+ end
59
+ end
@@ -0,0 +1,25 @@
1
+ module Shushu
2
+ class BasePresenter
3
+
4
+ def money(cents)
5
+ add_commas(pennies_to_dollar(cents.to_i))
6
+ end
7
+
8
+ def add_commas(str)
9
+ str.reverse.scan(/(?:\d*\.)?\d{1,3}-?/).join(',').reverse
10
+ end
11
+
12
+ def pennies_to_dollar(qty)
13
+ sprintf("%.2f", qty / 100.0)
14
+ end
15
+
16
+ def trunc_hours(hrs)
17
+ sprintf("%.3f", hrs)
18
+ end
19
+
20
+ def date_range(s, f)
21
+ s.strftime('%d %b %H:%M ') + " - " + f.strftime('%d %b %H:%M ')
22
+ end
23
+
24
+ end
25
+ end
@@ -0,0 +1,49 @@
1
+ module Shushu
2
+ class LineItemPresenter < BasePresenter
3
+
4
+ def initialize(line_item)
5
+ @line_item = line_item
6
+ @unit_groups = @line_item.unit_groups
7
+ @unit_group_presenters = @unit_groups.map {|ug| UnitGroupPresenter.new(ug)}
8
+ end
9
+
10
+ def total
11
+ money(@line_item.total)
12
+ end
13
+
14
+ def name
15
+ @line_item.app_name
16
+ end
17
+
18
+ def unit_group_rate(product_group)
19
+ unit_group_presenters(product_group).sample.rate
20
+ end
21
+
22
+ def unit_group_total(product_group)
23
+ money(
24
+ unit_group_presenters(product_group).
25
+ map(&:unit_group).
26
+ map(&:total).
27
+ reduce(:+)
28
+ )
29
+ end
30
+
31
+ def unit_group_qty(product_group)
32
+ trunc_hours(
33
+ unit_group_presenters(product_group).
34
+ map(&:unit_group).
35
+ map(&:qty).
36
+ reduce(:+)
37
+ )
38
+ end
39
+
40
+ def unit_group_presenters(product_group=nil)
41
+ if product_group
42
+ @unit_group_presenters.select {|ugp| ugp.product_group == product_group}
43
+ else
44
+ @unit_group_presenters
45
+ end
46
+ end
47
+
48
+ end
49
+ end
@@ -0,0 +1,23 @@
1
+ module Shushu
2
+ class ReportPresenter < BasePresenter
3
+
4
+ def initialize(report, line_item_builder=LineItemBuilder)
5
+ @report = report
6
+ @units = @report.billable_units
7
+ @builder = line_item_builder
8
+ end
9
+
10
+ def total
11
+ money(@report.total)
12
+ end
13
+
14
+ def line_item_presenters
15
+ @line_item_presenters ||= begin
16
+ @builder.build(@units).map do |li|
17
+ LineItemPresenter.new(li)
18
+ end
19
+ end
20
+ end
21
+
22
+ end
23
+ end
@@ -0,0 +1,63 @@
1
+ module Shushu
2
+ class UnitGroupPresenter < BasePresenter
3
+
4
+ attr_reader :unit_group
5
+
6
+ def initialize(unit_group)
7
+ @unit_group = unit_group
8
+ end
9
+
10
+ def id
11
+ [:hid, :name].map {|m| unit_group.send(m)}.join("_").gsub("-", "_")
12
+ end
13
+
14
+ def unit_presenters
15
+ @unit_presenters ||= @unit_group.units.map {|u| UnitPresenter.new(u)}
16
+ end
17
+
18
+ def daily_report_url
19
+ #TODO remove chart stub
20
+ #payments_uri("/billable_units/daily")
21
+ "/billable_units/daily"
22
+ end
23
+
24
+ def compacted_reports
25
+ #TODO remove chart stub
26
+ #BillableUnitReporter.compacted(@unit_group.units)
27
+ []
28
+ end
29
+
30
+ def total
31
+ money(@unit_group.total)
32
+ end
33
+
34
+ def qty
35
+ trunc_hours(@unit_group.qty)
36
+ end
37
+
38
+ def rate
39
+ @unit_group.rate
40
+ end
41
+
42
+ def name
43
+ @unit_group.name
44
+ end
45
+
46
+ def description
47
+ @unit_group.description
48
+ end
49
+
50
+ def product_name
51
+ @unit_group.product_name
52
+ end
53
+
54
+ def product_group
55
+ @unit_group.product_group
56
+ end
57
+
58
+ def unit_of_measure
59
+ @unit_group.rate_period
60
+ end
61
+
62
+ end
63
+ end
@@ -0,0 +1,34 @@
1
+ module Shushu
2
+ class UnitPresenter < BasePresenter
3
+
4
+ def initialize(unit)
5
+ @unit = unit
6
+ end
7
+
8
+ def total
9
+ money(@unit.total)
10
+ end
11
+
12
+ def rate
13
+ [money(@unit.rate), @unit.rate_period].join("/")
14
+ end
15
+
16
+ def qty
17
+ trunc_hours(@unit.qty)
18
+ end
19
+
20
+ def start
21
+ @unit.from
22
+ end
23
+
24
+ def end
25
+ @unit.to
26
+ end
27
+
28
+ def description
29
+ @unit.product_name
30
+ end
31
+
32
+ end
33
+ end
34
+
@@ -0,0 +1,17 @@
1
+ module Shushu
2
+ module LineItemBuilder
3
+ extend self
4
+
5
+ # Give build() a collection of units and it will return a
6
+ # collection of line_items. Each line_item will have a collection
7
+ # of unit_groups wich will hold a collection of units
8
+
9
+ def build(units)
10
+ units.group_by(&:hid).map do |hid, units_by_hid|
11
+ units_by_hid.group_by(&:product_name).map do |product_name, units_by_name|
12
+ UnitGroup.new(units_by_name)
13
+ end
14
+ end.map {|unit_groups| LineItem.new(unit_groups)}.flatten
15
+ end
16
+ end
17
+ end
data/lib/shushu.rb ADDED
@@ -0,0 +1,64 @@
1
+ require "yajl"
2
+ require "rest-client"
3
+ require "helpers/http_helpers"
4
+
5
+ require "active_support/inflector"
6
+ Inflector = ActiveSupport::Inflector.send(:extend, ActiveSupport::Inflector)
7
+
8
+ module Shushu
9
+ extend self
10
+ extend HttpHelpers
11
+
12
+ ShushuException = Class.new(Exception)
13
+ AuthenticationError = Class.new(ShushuException)
14
+ AuthorizationError = Class.new(ShushuException)
15
+ ServiceDownError = Class.new(ShushuException)
16
+ UnexpectedError = Class.new(ShushuException)
17
+ SymanticError = Class.new(ShushuException)
18
+
19
+ def url
20
+ Client.url
21
+ end
22
+
23
+ def url=(url)
24
+ Client.url = url
25
+ end
26
+
27
+ class Client
28
+ def self.url=(url)
29
+ @@url = url
30
+ end
31
+
32
+ def self.url
33
+ @@url || ENV["SHUSHU_URL"]
34
+ end
35
+
36
+ def initialize(url)
37
+ self.class.url = url
38
+ end
39
+
40
+ def [](api)
41
+ Shushu.const_get(Inflector.camelize(api))
42
+ end
43
+ end
44
+ end
45
+
46
+
47
+ require "models/account"
48
+ require "models/billable_unit"
49
+ require "models/report"
50
+ require "models/unit_group"
51
+ require "models/line_item"
52
+ require "models/b_event"
53
+ require "models/res_own"
54
+ require "models/r_code"
55
+ require "models/rev_rep"
56
+ require "models/heart_beat"
57
+
58
+ require "services/line_item_builder"
59
+
60
+ require "presenters/base_presenter"
61
+ require "presenters/unit_group_presenter"
62
+ require "presenters/unit_presenter"
63
+ require "presenters/line_item_presenter"
64
+ require "presenters/report_presenter"
data/readme.md ADDED
@@ -0,0 +1,252 @@
1
+ # Shushu Client
2
+
3
+ ## Purpose
4
+
5
+ This gem wraps the APIs defined [here](https://github.com/heroku/shushu/tree/master/doc).
6
+ The shushu client also provides a set of objects that help with the presentation
7
+ of an invoice.
8
+
9
+ ## Setup
10
+
11
+ ```bash
12
+ $ gem install shushu
13
+ ```
14
+
15
+ ```bash
16
+ # Optional. When set, shushu will ignore bad responses from Shushu's API.
17
+ # Default: false
18
+ $SHUSHU_CLIENT_UNSAFE=true
19
+
20
+ # Required
21
+ # Default: nil
22
+ $SHUSHU_URL=https://shushu-stg.heroku.com
23
+ ```
24
+
25
+ **Ruby configuration will take precedence over environment variables.**
26
+
27
+ ```ruby
28
+ Shushu.url = "https://core:secret@shushu-stg.heroku.com"
29
+ ```
30
+
31
+ ## Usage
32
+
33
+ ### PaymentMethods
34
+
35
+ This API deals primarily with credit cards. PaymentMethods can be created
36
+ indipendintly of Accounts. You will need a payment_method to generate an
37
+ invoice.
38
+
39
+ ```ruby
40
+ #TODO Build API
41
+ ```
42
+
43
+ ### Accounts
44
+
45
+ This API deals with accounts which is a primitive for grouping resources. You
46
+ will need an account to generate a usage report.
47
+
48
+ ```ruby
49
+ Shushu::Account.create
50
+ #=> {:id => "001"}
51
+ ```
52
+
53
+ ### AccountOwnerships
54
+
55
+ Use this API when you want to setup associations between Vault accounts and
56
+ Vault payment_methods.
57
+
58
+ For complete details on the semantics of this API, read the [AccountOwnerships
59
+ API docs.](https://github.com/heroku/shushu/blob/master/doc/account_ownership_api.md)
60
+
61
+ ```ruby
62
+ # To associate an account with a payment_method
63
+ Shushu::AcctOwn.act(
64
+ :account_id => vault_account_id,
65
+ :payment_method_id => payment_method_id,
66
+ :entity_id => entity_id,
67
+ :time => time
68
+ )
69
+ #=> {"payment_method_id"=>"123", "account_id"=>"1", "entity_id"=>"entity123"}
70
+
71
+ # Now we need to change the payment_method on an account
72
+ Shushu::AcctOwn.xfr(
73
+ :prev_payment_method_id => prev_payment_method_id,
74
+ :payment_method_id => new_payment_method_id,
75
+ :account_id => account_id,
76
+ :prev_entity_id => prev_entity_id,
77
+ :entity_id => entity_id,
78
+ :time => time
79
+ )
80
+ #=> {"payment_method_id"=>"456", "account_id"=>"1", "entity_id"=>"event124"}
81
+ ```
82
+
83
+ ### RateCodes
84
+
85
+ Before you can send BillableEvents to Shushu, you will need to provision a
86
+ RateCode.
87
+
88
+ * rate - int. units should be pennies.
89
+ * product_group - string.
90
+ * product_name - string. optional. you can also pass the product name in an event.
91
+
92
+ [Shushu API Docs](https://github.com/heroku/shushu/blob/master/doc/rate_code_api.md)
93
+
94
+ ```ruby
95
+ Shushu::RCode.create(
96
+ :rate => 5,
97
+ :product_group => "addon",
98
+ :product_name => "postgres"
99
+ )
100
+ #=> {:slug => "AO01", :rate => 5, :product_group => "addon", :product_name => "postgres"}
101
+ ```
102
+
103
+ ### ResourceOwnerships
104
+
105
+ Use this API when dealing with resources and Vault accounts. For instance:
106
+ Heroku apps and Teams.
107
+
108
+ For complete details on the semantics of this API, read the [ResourceOwnerships
109
+ API docs.](https://github.com/heroku/shushu/blob/master/doc/resource_ownership_api.md)
110
+
111
+ ```ruby
112
+ # When a new app is created, activate a new resource_ownership record.
113
+ Shushu::ResOwn.act(
114
+ :hid => hid,
115
+ :entity_id => entity_id,
116
+ :account_id => vault_account_id,
117
+ :time => time
118
+ )
119
+ #=> {"hid"=>"123", "account_id"=>"1", "entity_id"=>"event123"}
120
+
121
+
122
+ # When an app is transfered to another vault account, transfer the resource_ownership record.
123
+ Shushu::ResOwn.xfr(
124
+ :hid => hid,
125
+ :prev_entity_id => prev_entity_id,
126
+ :prev_vault_account_id => prev_vault_account_id,
127
+ :entity_id => entity_id,
128
+ :account_id => new_vault_account_id,
129
+ :time => time
130
+ )
131
+ #=> {"hid"=>"123", "account_id"=>"1", "entity_id"=>"event123"}
132
+
133
+ # When an app is destroyed, deactivate the resource_ownership record.
134
+ Shushu::ResOwn.deact(
135
+ :hid => hid,
136
+ :entity_id => entity_id,
137
+ :account_id => vault_account_id,
138
+ :time => time
139
+ )
140
+ #=> {"hid"=>"123", "account_id"=>"1", "entity_id"=>"event123"}
141
+ ```
142
+
143
+ ### BillableEvents
144
+
145
+ Use this API when you want to start billing for a resource. You can start
146
+ emitting events prior to setting up relationships between accounts and
147
+ payment_methods. (Although, usage reports and invoices will not be available
148
+ until account_ownerships and resource_ownerships have been established.)
149
+
150
+ For complete details on the semantics of this API, read the [BillableEvents
151
+ API docs.](https://github.com/heroku/shushu/blob/master/doc/events_api.md)
152
+
153
+ ```ruby
154
+ # Open an event when you would like to start billing.
155
+ Shushu::BEvent.open(
156
+ :entity_id => entity_id,
157
+ :hid => hid,
158
+ :time => time,
159
+ :rate_code => rate_code,
160
+ :product_name => product_name,
161
+ :qty => qty
162
+ )
163
+
164
+ # Don't forget to close it.
165
+ Shushu::BEvent.close(
166
+ :entity_id => entity_id,
167
+ :time => time
168
+ )
169
+ ```
170
+
171
+ ### UsageReports
172
+
173
+ ```ruby
174
+ report = Shushu::UsageReport.new(account_id, from, to)
175
+ report.billable_units
176
+ ```
177
+
178
+
179
+ ### Invoices
180
+
181
+ ```ruby
182
+ invoice = Shushu::UsageReport.new(account_id, from, to)
183
+ invoice.billable_units
184
+ ```
185
+
186
+
187
+ ### Report Generation
188
+
189
+ Invoices and UsageReports can be used for report generation. Basically, the
190
+ report generation code expects a collection of BillableUnits. BillableUnits are
191
+ returned from both the Invoice API and the UsageReport API. However, the details
192
+ of the billable_units may be different with respect to the type of the report.
193
+
194
+ #### Presenters
195
+
196
+ Clients of this library will want to generate some sort of view for the reports, the
197
+ presenter objects were created to aid with that effort. You should only need to
198
+ use the presenters while building views. Each view wraps a simple model object.
199
+ All of the models and presenters are derived from the billable_unit which is
200
+ retreived from the remote API.
201
+
202
+ Report --> LineItem --> UnitGroup --> Billable Unit
203
+
204
+
205
+ #### ReportPresenter
206
+
207
+ The report presenter is how you will kick off the process of generating a
208
+ report. You hand it a report object, either a UsageReport or and Invoice, and
209
+ using the reports billable_units, it will build the line_items and a set of
210
+ line_item_presenters for the line_items.
211
+
212
+ #### LineItemBuilder
213
+
214
+ The ReportPresenter will create a set of line_items based upon the
215
+ billable_units in the report. The default is to group things by HID. Thus there
216
+ will be a line_item for each distinct HID in the set of billable_units. The
217
+ builder will also give a collection of unit_groups to the line_item. By default,
218
+ the unit_groups will be partitioned by the product_group. (i.e. dyno, addon)
219
+
220
+ You can customize the LineItemBuilder by creating a new class that responds to
221
+ build and passing it to the ReportPresenter.
222
+
223
+ ```ruby
224
+ ReportPresenter.new(report, CustomLineItemBuilder)
225
+ ```
226
+
227
+ #### LineItemPresenter
228
+
229
+ The LineItemPresenter is responsible for handling the total and names of the
230
+ line_items. It also manages the set of unit_groups. Since the default
231
+ LineItemBuilder partitioned unit_groups based upon product_group, you can ask
232
+ the LineItemPresenter for infomation about subsets of unit_groups. For instance:
233
+
234
+ ```ruby
235
+ line_item_presenter.unit_group_presenters("dyno")
236
+ line_item_presenter.unit_group_total("dyno")
237
+ line_item_presenter.unit_group_qty("dyno")
238
+ ```
239
+
240
+
241
+ #### UnitGroupPresenter
242
+
243
+ UnitGroups are collections of billable_units that are partitioned by
244
+ product_name. So if there exists a set of billable_units that all have
245
+ product_group = "dyno" and product_name= "worker", then this presenter
246
+ will give you information about that group.
247
+
248
+
249
+ #### UnitPresenter
250
+
251
+ Finally, the UnitPresenter wraps a billable_unit and exposes methods to show
252
+ totals, quantities and other meta-data.
data/test/fixtures.rb ADDED
@@ -0,0 +1,29 @@
1
+ module Fixtures
2
+ extend self
3
+
4
+ def billable_units
5
+ [
6
+ {
7
+ :hid => 123,
8
+ :from => Time.mktime(2000,1),
9
+ :to => Time.mktime(2000,2),
10
+ :qty => 744,
11
+ :rate => 5,
12
+ :rate_period => "hour",
13
+ :product_group => "dyno",
14
+ :product_name => "web"
15
+ },
16
+ {
17
+ :hid => 123,
18
+ :from => Time.mktime(2000,1),
19
+ :to => Time.mktime(2000,2),
20
+ :qty => 744,
21
+ :rate => 5,
22
+ :rate_period => "hour",
23
+ :product_group => "dyno",
24
+ :product_name => "worker"
25
+ }
26
+ ]
27
+ end
28
+
29
+ end
@@ -0,0 +1,20 @@
1
+ require File.expand_path('../../test_helper', __FILE__)
2
+
3
+ class BEventTest < ShushuTest
4
+ def test_open
5
+ our_params = {
6
+ :hid => "app123",
7
+ :entity_id => 1,
8
+ :rate_code => "RT01",
9
+ :time => Time.utc(2012,1).to_s,
10
+ :product_name => "web",
11
+ :qty => 1
12
+ }
13
+ api_resp_body = Shushu::HttpHelpers.enc_json(our_params)
14
+ Shushu.url = "http://provider:password@shushu.heroku.com"
15
+ FakeWeb.register_uri(:put, (Shushu.url + "/resources/app123/billable_events/1"), :body => api_resp_body)
16
+ event = Shushu::BEvent.open(our_params)
17
+ assert_equal(1, event["entity_id"])
18
+ end
19
+ end
20
+
@@ -0,0 +1,8 @@
1
+ require File.expand_path('../../test_helper', __FILE__)
2
+
3
+ class BillableUnitTest < ShushuTest
4
+ def test_total
5
+ unit1 = Shushu::BillableUnit.new("rate" => 10, "qty" => 10)
6
+ assert_equal(100, unit1.total)
7
+ end
8
+ end
@@ -0,0 +1,10 @@
1
+ require File.expand_path('../../test_helper', __FILE__)
2
+
3
+ class LineItemTest < ShushuTest
4
+ def test_total
5
+ unit1 = Shushu::BillableUnit.new("rate" => 10, "qty" => 10)
6
+ unit_group1 = Shushu::UnitGroup.new([unit1])
7
+ line_item = Shushu::LineItem.new([unit_group1])
8
+ assert_equal(100, line_item.total)
9
+ end
10
+ end
@@ -0,0 +1,18 @@
1
+ require File.expand_path('../../test_helper', __FILE__)
2
+
3
+ class RCodeTest < ShushuTest
4
+ def test_open
5
+ our_params = {
6
+ :rate => 5,
7
+ :product_group => "addon",
8
+ :product_name => "postgres",
9
+ :description => "A database service."
10
+ }
11
+ api_resp_body = Shushu::HttpHelpers.enc_json(our_params.merge(:slug => "AO01"))
12
+ Shushu.url = "http://provider:password@shushu.heroku.com"
13
+ FakeWeb.register_uri(:post, (Shushu.url + "/rate_codes"), :body => api_resp_body)
14
+ rate_code = Shushu::RCode.create(our_params)
15
+ assert_equal("AO01", rate_code["slug"])
16
+ end
17
+ end
18
+
@@ -0,0 +1,12 @@
1
+ require File.expand_path('../../test_helper', __FILE__)
2
+
3
+ class RevRepTest < ShushuTest
4
+ def test_for_month
5
+ api_resp_body = Shushu::HttpHelpers.enc_json({:ok => true})
6
+ Shushu.url = "http://provider:password@shushu.heroku.com"
7
+ url = Shushu.url + "/rev_rep?from%3D2011-01-01+00%3A00%3A00+UTC&to%3D2011-02-01+00%3A00%3A00+UTC"
8
+ FakeWeb.register_uri(:get, url, :body => api_resp_body)
9
+ Shushu::RevRep.get(Time.utc(2011,1), Time.utc(2011,2))
10
+ end
11
+ end
12
+
@@ -0,0 +1,17 @@
1
+ require File.expand_path('../../test_helper', __FILE__)
2
+
3
+ class UnitGroupTest < ShushuTest
4
+ def test_total
5
+ unit1 = Shushu::BillableUnit.new("rate" => 10, "qty" => 10)
6
+ unit_group1 = Shushu::UnitGroup.new([unit1])
7
+ assert_equal(100, unit_group1.total)
8
+ end
9
+
10
+ def test_check_rates
11
+ unit1 = Shushu::BillableUnit.new("rate" => 10, "qty" => 10)
12
+ unit2 = Shushu::BillableUnit.new("rate" => 9, "qty" => 10)
13
+ unit_group1 = Shushu::UnitGroup.new([unit1])
14
+ unit_group2 = Shushu::UnitGroup.new([unit2])
15
+ assert_raises(Shushu::UnitGroup::InvalidUnitGroup) {Shushu::UnitGroup.new([unit1, unit2])}
16
+ end
17
+ end
@@ -0,0 +1,36 @@
1
+ require File.expand_path('../../test_helper', __FILE__)
2
+
3
+ class LineItemPresenterTest < ShushuTest
4
+ def test_unit_group_total
5
+ unit1 = Shushu::BillableUnit.new("rate" => 1000, "qty" => 10, "product_group" => "dyno")
6
+ unit_group1 = Shushu::UnitGroup.new([unit1])
7
+ line_item = Shushu::LineItem.new([unit_group1])
8
+ lip = Shushu::LineItemPresenter.new(line_item)
9
+ assert_equal("100.00", lip.unit_group_total("dyno"))
10
+ end
11
+
12
+ def test_unit_group_qty
13
+ unit1 = Shushu::BillableUnit.new("rate" => 1000, "qty" => 10, "product_group" => "dyno")
14
+ unit2 = Shushu::BillableUnit.new("rate" => 1000, "qty" => 1, "product_group" => "dyno")
15
+ unit_group1 = Shushu::UnitGroup.new([unit1, unit2])
16
+ line_item = Shushu::LineItem.new([unit_group1])
17
+ lip = Shushu::LineItemPresenter.new(line_item)
18
+ assert_equal("11.000", lip.unit_group_qty("dyno"))
19
+ end
20
+
21
+ def test_unit_group_presenters
22
+ unit1 = Shushu::BillableUnit.new("product_group" => "dyno")
23
+ unit2 = Shushu::BillableUnit.new("product_group" => "dyno")
24
+ unit3 = Shushu::BillableUnit.new("product_group" => "addon")
25
+ unit_group1 = Shushu::UnitGroup.new([unit1, unit2])
26
+ unit_group2 = Shushu::UnitGroup.new([unit3])
27
+ line_item = Shushu::LineItem.new([unit_group1, unit_group2])
28
+ lip = Shushu::LineItemPresenter.new(line_item)
29
+
30
+ assert_equal(2, lip.unit_group_presenters.length)
31
+ assert_equal(1, lip.unit_group_presenters("dyno").map(&:product_group).uniq.length)
32
+ assert_equal("addon", lip.unit_group_presenters("addon").map(&:product_group).uniq.first)
33
+ end
34
+ end
35
+
36
+
@@ -0,0 +1,22 @@
1
+ require File.expand_path('../../test_helper', __FILE__)
2
+
3
+ class ReportPresenterTest < ShushuTest
4
+
5
+ def report
6
+ Shushu::Report.new.tap do |r|
7
+ r.billable_units = Fixtures.billable_units.map {|h| Shushu::BillableUnit.new(h)}
8
+ end
9
+ end
10
+
11
+ def test_init
12
+ presenter = Shushu::ReportPresenter.new(report)
13
+ assert(!presenter.nil?)
14
+ end
15
+
16
+ def test_returns_line_item_presenters
17
+ presenter = Shushu::ReportPresenter.new(report)
18
+ lip = presenter.line_item_presenters
19
+ assert_equal(Shushu::LineItemPresenter, lip.first.class)
20
+ end
21
+
22
+ end
@@ -0,0 +1,30 @@
1
+ require File.expand_path('../../test_helper', __FILE__)
2
+
3
+ class LineItemBuilderTest < ShushuTest
4
+
5
+ def units
6
+ Fixtures.billable_units.map {|h| Shushu::BillableUnit.new(h)}
7
+ end
8
+
9
+ def test_build
10
+ line_items = Shushu::LineItemBuilder.build(units)
11
+ assert(!line_items.nil?)
12
+ end
13
+
14
+ def test_build_creates_line_item_by_hid
15
+ line_items = Shushu::LineItemBuilder.build(units)
16
+ assert_equal(1, line_items.length)
17
+ end
18
+
19
+ def test_build_creates_one_unit_group_for_line_item
20
+ line_item = Shushu::LineItemBuilder.build(units).pop
21
+ assert_equal(1, line_item.unit_groups.length)
22
+ end
23
+
24
+ def test_build_puts_both_units_on_unit_group
25
+ line_item = Shushu::LineItemBuilder.build(units).pop
26
+ unit_group = line_item.unit_groups.pop
27
+ assert_equal(2, unit_group.units.length)
28
+ end
29
+
30
+ end
@@ -0,0 +1,19 @@
1
+ $:.unshift("lib")
2
+ $:.unshift("test")
3
+
4
+ ENV["RACK_ENV"] = "test"
5
+
6
+ require "rubygems"
7
+ require "bundler"
8
+ Bundler.require(:default, :test)
9
+
10
+ require "minitest/autorun"
11
+ require "shushu"
12
+ require "fakeweb"
13
+ require "fixtures"
14
+
15
+ puts "Blocking network connectivity"
16
+ FakeWeb.allow_net_connect = false
17
+
18
+ class ShushuTest < MiniTest::Unit::TestCase
19
+ end
metadata ADDED
@@ -0,0 +1,117 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: shushu
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.5
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Ryan Smith
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2011-12-29 00:00:00.000000000Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rest-client
16
+ requirement: &26931360 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 1.6.7
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *26931360
25
+ - !ruby/object:Gem::Dependency
26
+ name: yajl-ruby
27
+ requirement: &26930880 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ~>
31
+ - !ruby/object:Gem::Version
32
+ version: 1.1.0
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: *26930880
36
+ - !ruby/object:Gem::Dependency
37
+ name: active_support
38
+ requirement: &26930500 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ type: :runtime
45
+ prerelease: false
46
+ version_requirements: *26930500
47
+ description: A ruby wrapper around Shushu's HTTP API.
48
+ email: ryan@heroku.com
49
+ executables: []
50
+ extensions: []
51
+ extra_rdoc_files: []
52
+ files:
53
+ - readme.md
54
+ - lib/presenters/base_presenter.rb
55
+ - lib/presenters/unit_presenter.rb
56
+ - lib/presenters/unit_group_presenter.rb
57
+ - lib/presenters/report_presenter.rb
58
+ - lib/presenters/line_item_presenter.rb
59
+ - lib/shushu.rb
60
+ - lib/models/account.rb
61
+ - lib/models/acct_own.rb
62
+ - lib/models/report.rb
63
+ - lib/models/unit_group.rb
64
+ - lib/models/rev_rep.rb
65
+ - lib/models/line_item.rb
66
+ - lib/models/heart_beat.rb
67
+ - lib/models/r_code.rb
68
+ - lib/models/res_own.rb
69
+ - lib/models/b_event.rb
70
+ - lib/models/billable_unit.rb
71
+ - lib/helpers/http_helpers.rb
72
+ - lib/services/line_item_builder.rb
73
+ - test/presenters/line_item_presenter_test.rb
74
+ - test/presenters/report_presenter_test.rb
75
+ - test/fixtures.rb
76
+ - test/models/r_code_test.rb
77
+ - test/models/billable_unit_test.rb
78
+ - test/models/b_event_test.rb
79
+ - test/models/unit_group_test.rb
80
+ - test/models/rev_rep_test.rb
81
+ - test/models/line_item_test.rb
82
+ - test/services/line_item_builder_test.rb
83
+ - test/test_helper.rb
84
+ homepage: http://github.com/heroku/shushu
85
+ licenses: []
86
+ post_install_message:
87
+ rdoc_options: []
88
+ require_paths:
89
+ - lib
90
+ required_ruby_version: !ruby/object:Gem::Requirement
91
+ none: false
92
+ requirements:
93
+ - - ! '>='
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
96
+ required_rubygems_version: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ! '>='
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ requirements: []
103
+ rubyforge_project:
104
+ rubygems_version: 1.8.10
105
+ signing_key:
106
+ specification_version: 3
107
+ summary: Shushu client lib
108
+ test_files:
109
+ - test/presenters/line_item_presenter_test.rb
110
+ - test/presenters/report_presenter_test.rb
111
+ - test/models/r_code_test.rb
112
+ - test/models/billable_unit_test.rb
113
+ - test/models/b_event_test.rb
114
+ - test/models/unit_group_test.rb
115
+ - test/models/rev_rep_test.rb
116
+ - test/models/line_item_test.rb
117
+ - test/services/line_item_builder_test.rb