rubill 0.1.2

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: d9b71f0d60dd41ff424f2e915cb7ed2def75c2c1
4
+ data.tar.gz: 51aef8f5bed4e369786e51d655e9afa65ab7ea2e
5
+ SHA512:
6
+ metadata.gz: 8f57a53a8388bbea6fd2b515bef3c30dd4e7a0675dfc3828ed541fe1aa162e4b7ce1eec365e28acf8105e672da1f5fb7de05bb2833df5633de84b81c9fc7fc6a
7
+ data.tar.gz: eb04ddf63780996914870264f0bf58a420abe1d1e88abf911684ff956aa1ecf7bbc39d03fd9866d49bb48f3cbaf8819aad7c49a00631f7d8130a1bd740e3d652
@@ -0,0 +1,90 @@
1
+ module Rubill
2
+ class Base
3
+ attr_accessor :remote_record
4
+
5
+ class NotFound < StandardError; end
6
+
7
+ def initialize(remote)
8
+ self.remote_record = remote
9
+ end
10
+
11
+ def [](key)
12
+ remote_record.send(:[], key)
13
+ end
14
+
15
+ def []=(key, value)
16
+ remote_record.send(:[]=, key, value)
17
+ end
18
+
19
+ def self.active
20
+ where([Query::Filter.new("isActive", "=", "1")])
21
+ end
22
+
23
+ def id
24
+ remote_record[:id]
25
+ end
26
+
27
+ def save
28
+ self.class.update(remote_record)
29
+ end
30
+
31
+ def delete
32
+ self.class.delete(id)
33
+ end
34
+
35
+ def self.find(id)
36
+ new(Query.read(remote_class_name, id))
37
+ end
38
+
39
+ def self.create(data)
40
+ new(Query.create(remote_class_name, data.merge({entity: remote_class_name})))
41
+ end
42
+
43
+ def self.update(data)
44
+ Query.update(remote_class_name, data.merge({entity: remote_class_name}))
45
+ end
46
+
47
+ def self.delete(id)
48
+ Query.delete(remote_class_name, id)
49
+ end
50
+
51
+ def self.where(filters=[])
52
+ raise ArgumentError unless filters.is_a?(Enumerable)
53
+ raise ArgumentError if !filters.is_a?(Hash) && !filters.all? { |f| f.is_a?(Query::Filter) }
54
+
55
+ if filters.is_a?(Hash)
56
+ filters = filters.map do |field, value|
57
+ Query::Filter.new(field.to_s, "=", value)
58
+ end
59
+ end
60
+
61
+ result = []
62
+ start = 0
63
+ step = 999
64
+ loop do
65
+ chunk = Query.list(remote_class_name, start, step, filters)
66
+
67
+ if !chunk.empty?
68
+ records = chunk.map { |r| new(r) }
69
+ result += records
70
+ start += step
71
+ end
72
+
73
+ break if chunk.length < step
74
+ end
75
+
76
+ result
77
+ end
78
+
79
+ def self.all
80
+ # Note: this method returns ALL of desired entity, including inactive
81
+ where([])
82
+ end
83
+
84
+ private
85
+
86
+ def self.remote_class_name
87
+ raise NotImplementedError
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,11 @@
1
+ module Rubill
2
+ class Bill < Base
3
+ def self.send_payment(opts)
4
+ SentPayment.create(opts)
5
+ end
6
+
7
+ def self.remote_class_name
8
+ "Bill"
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,15 @@
1
+ module Rubill
2
+ class Customer < Base
3
+ def self.find_by_name(name)
4
+ where([Query::Filter.new("name", "=", name)]).first
5
+ end
6
+
7
+ def contacts
8
+ CustomerContact.active_by_customer(id)
9
+ end
10
+
11
+ def self.remote_class_name
12
+ "Customer"
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,20 @@
1
+ module Rubill
2
+ class CustomerContact < Base
3
+ def self.find_by_customer(customer_id)
4
+ where([Query::Filter.new("customerId", "=", customer_id)])
5
+ end
6
+
7
+ def self.active_by_customer(customer_id)
8
+ where(
9
+ [
10
+ Query::Filter.new("customerId", "=", customer_id),
11
+ Query::Filter.new("isActive", "=", "1"),
12
+ ]
13
+ )
14
+ end
15
+
16
+ def self.remote_class_name
17
+ "CustomerContact"
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,46 @@
1
+ module Rubill
2
+ class Invoice < Base
3
+ def amount_paid
4
+ amount - amount_due
5
+ end
6
+
7
+ def amount
8
+ remote_record[:amount]
9
+ end
10
+
11
+ def send_email(subject, body, contact_emails)
12
+ Query.execute(
13
+ "/SendInvoice.json",
14
+ {
15
+ invoiceId: id,
16
+ headers: {
17
+ subject: subject,
18
+ toEmailAddresses: contact_emails
19
+ },
20
+ content: {
21
+ body: body
22
+ }
23
+ }
24
+ )
25
+ end
26
+
27
+ def amount_due
28
+ remote_record[:amountDue]
29
+ end
30
+
31
+ def self.line_item(amount, description, item_id)
32
+ {
33
+ entity: "InvoiceLineItem",
34
+ quantity: 1,
35
+ itemId: item_id,
36
+ # must to_f amount otherwise decimal will be converted to string in JSON
37
+ price: amount.to_f,
38
+ description: description,
39
+ }
40
+ end
41
+
42
+ def self.remote_class_name
43
+ "Invoice"
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,86 @@
1
+ module Rubill
2
+ class Query
3
+ attr_accessor :url
4
+ attr_accessor :options
5
+
6
+ def initialize(url, opts={})
7
+ self.url = url
8
+ self.options = opts
9
+ end
10
+
11
+ def self.list(entity, start=0, step=999, filters=[])
12
+ execute(
13
+ "/List/#{entity}.json",
14
+ start: start,
15
+ max: step,
16
+ filters: filters.map(&:to_hash),
17
+ )
18
+ end
19
+
20
+ def self.read(entity, id)
21
+ execute("/Crud/Read/#{entity}.json", id: id)
22
+ end
23
+
24
+ def self.create(entity, object={})
25
+ execute("/Crud/Create/#{entity}.json", obj: object)
26
+ end
27
+
28
+ def self.update(entity, object={})
29
+ execute("/Crud/Update/#{entity}.json", obj: object)
30
+ end
31
+
32
+ def self.delete(entity, id)
33
+ execute("/Crud/Delete/#{entity}.json", id: id)
34
+ end
35
+
36
+ def self.pay_bills(opts={})
37
+ execute("/PayBills.json", opts)
38
+ end
39
+
40
+ def self.receive_payment(opts={})
41
+ execute("/RecordARPayment.json", opts)
42
+ end
43
+
44
+ def self.send_payment(opts={})
45
+ execute("/RecordAPPayment.json", opts)
46
+ end
47
+
48
+ def self.upload_attachment(opts={})
49
+ execute("/UploadAttachment.json", opts)
50
+ end
51
+
52
+ def self.void_sent_payment(id)
53
+ execute("/VoidAPPayment.json", sentPayId: id)
54
+ end
55
+
56
+ def self.void_received_payment(id)
57
+ execute("/VoidARPayment.json", id: id)
58
+ end
59
+
60
+ def execute
61
+ Session.instance.execute(self)
62
+ end
63
+
64
+ def self.execute(url, options)
65
+ new(url, options).execute
66
+ end
67
+
68
+ class Filter
69
+ attr_accessor :field
70
+ attr_accessor :op
71
+ attr_accessor :value
72
+
73
+ def initialize(field, op, value)
74
+ self.field, self.op, self.value = field, op, value
75
+ end
76
+
77
+ def to_hash
78
+ {
79
+ field: field,
80
+ op: op,
81
+ value: value,
82
+ }
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,32 @@
1
+ module Rubill
2
+ class ReceivedPayment < Base
3
+ def self.create(opts)
4
+ Query.receive_payment(opts)
5
+ end
6
+
7
+ def self.active
8
+ where([Query::Filter.new("status", "!=", "1")])
9
+ end
10
+
11
+ def void
12
+ delete
13
+ end
14
+
15
+ def delete
16
+ self.class.delete(id)
17
+ end
18
+
19
+ def self.delete(id)
20
+ # To overwrite delete method in superclass
21
+ void(id)
22
+ end
23
+
24
+ def self.void(id)
25
+ Query.void_received_payment(id)
26
+ end
27
+
28
+ def self.remote_class_name
29
+ "ReceivedPay"
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,32 @@
1
+ module Rubill
2
+ class SentPayment < Base
3
+ def self.create(opts)
4
+ Query.send_payment(opts)
5
+ end
6
+
7
+ def self.active
8
+ where([Query::Filter.new("status", "!=", "4")])
9
+ end
10
+
11
+ def void
12
+ delete
13
+ end
14
+
15
+ def delete
16
+ self.class.delete(id)
17
+ end
18
+
19
+ def self.delete(id)
20
+ # To overwrite delete method in superclass
21
+ void(id)
22
+ end
23
+
24
+ def self.void(id)
25
+ Query.void_sent_payment(id)
26
+ end
27
+
28
+ def self.remote_class_name
29
+ "SentPay"
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,103 @@
1
+ require "httmultiparty"
2
+ require "json"
3
+ require "singleton"
4
+
5
+ module Rubill
6
+ class APIError < StandardError; end
7
+
8
+ class Session
9
+ include HTTParty
10
+ include Singleton
11
+
12
+ attr_accessor :id
13
+
14
+ base_uri "https://api.bill.com/api/v2"
15
+
16
+ def initialize
17
+ config = self.class.configuration
18
+ if missing = (!config.missing_keys.empty? && config.missing_keys)
19
+ raise "Missing key(s) in configuration: #{missing}"
20
+ end
21
+
22
+ if config.sandbox
23
+ self.class.base_uri "https://api-stage.bill.com/api/v2"
24
+ end
25
+
26
+ login
27
+ end
28
+
29
+ def execute(query)
30
+ _post(query.url, query.options)
31
+ end
32
+
33
+ def login
34
+ self.id = self.class.login
35
+ end
36
+
37
+ def self.login
38
+ login_options = {
39
+ password: configuration.password,
40
+ userName: configuration.user_name,
41
+ devKey: configuration.dev_key,
42
+ orgId: configuration.org_id,
43
+ }
44
+ login = _post("/Login.json", login_options)
45
+ login[:sessionId]
46
+ end
47
+
48
+ def options(data={})
49
+ {
50
+ sessionId: id,
51
+ devKey: self.class.configuration.dev_key,
52
+ data: data.to_json,
53
+ }
54
+ end
55
+
56
+ def self.default_headers
57
+ {"Content-Type" => "application/x-www-form-urlencoded"}
58
+ end
59
+
60
+ def _post(url, data, retries=0)
61
+ begin
62
+ self.class._post(url, options(data))
63
+ rescue APIError => e
64
+ if e.message =~ /Session is invalid/ && retries < 3
65
+ login
66
+ _post(url, data, retries + 1)
67
+ else
68
+ raise
69
+ end
70
+ end
71
+ end
72
+
73
+ def self._post(url, options)
74
+ if options.key?(:fileName)
75
+ file = StringIO.new(options.delete(:content))
76
+ end
77
+
78
+ post_options = {
79
+ body: options,
80
+ headers: default_headers,
81
+ }
82
+
83
+ post_options[:file] = file if file
84
+
85
+ if self.configuration.debug
86
+ post_options[:debug_output] = $stdout
87
+ end
88
+
89
+ response = post(url, post_options)
90
+ result = JSON.parse(response.body, symbolize_names: true)
91
+
92
+ unless result[:response_status] == 0
93
+ raise APIError.new(result[:response_data][:error_message])
94
+ end
95
+
96
+ result[:response_data]
97
+ end
98
+
99
+ def self.configuration
100
+ Rubill::configuration
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,14 @@
1
+ module Rubill
2
+ class Vendor < Base
3
+ def self.remote_class_name
4
+ "Vendor"
5
+ end
6
+
7
+ def bills
8
+ Bill.where([
9
+ Query::Filter.new("isActive", "=", "1"),
10
+ Query::Filter.new("vendorId", "=", id),
11
+ ])
12
+ end
13
+ end
14
+ end
data/lib/rubill.rb ADDED
@@ -0,0 +1,58 @@
1
+ module Rubill
2
+ attr_writer :configuration
3
+
4
+ def self.configure(&block)
5
+ yield(configuration)
6
+ end
7
+
8
+ def self.configuration
9
+ @configuration ||= Configuration.new
10
+ end
11
+
12
+ class Configuration
13
+ attr_accessor :user_name
14
+ attr_accessor :password
15
+ attr_accessor :dev_key
16
+ attr_accessor :org_id
17
+ attr_writer :debug
18
+ attr_writer :sandbox
19
+
20
+ def required_keys
21
+ %w(user_name password dev_key org_id)
22
+ end
23
+
24
+ def to_hash
25
+ required_keys.each_with_object({}) do |k, h|
26
+ h[k] = send(k.to_sym)
27
+ end
28
+ end
29
+
30
+ def missing_keys
31
+ required_keys.reject do |k|
32
+ to_hash[k]
33
+ end
34
+ end
35
+
36
+ def debug
37
+ @debug || false
38
+ end
39
+
40
+ def sandbox
41
+ @sandbox || false
42
+ end
43
+ end
44
+ end
45
+
46
+ require "rubill/session"
47
+ require "rubill/query"
48
+ require "rubill/base"
49
+ require "rubill/bill"
50
+ require "rubill/bill_payment"
51
+ require "rubill/invoice"
52
+ require "rubill/attachment"
53
+ require "rubill/sent_payment"
54
+ require "rubill/sent_bill_payment"
55
+ require "rubill/received_payment"
56
+ require "rubill/vendor"
57
+ require "rubill/customer"
58
+ require "rubill/customer_contact"
metadata ADDED
@@ -0,0 +1,110 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rubill
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.2
5
+ platform: ruby
6
+ authors:
7
+ - Andrew Taber
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-10-01 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: httmultiparty
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: json
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ description: A Ruby interface to Bill.com's API
70
+ email: andrew.e.taber@gmail.com
71
+ executables: []
72
+ extensions: []
73
+ extra_rdoc_files: []
74
+ files:
75
+ - lib/rubill.rb
76
+ - lib/rubill/base.rb
77
+ - lib/rubill/bill.rb
78
+ - lib/rubill/customer.rb
79
+ - lib/rubill/customer_contact.rb
80
+ - lib/rubill/invoice.rb
81
+ - lib/rubill/query.rb
82
+ - lib/rubill/received_payment.rb
83
+ - lib/rubill/sent_payment.rb
84
+ - lib/rubill/session.rb
85
+ - lib/rubill/vendor.rb
86
+ homepage: http://rubygems.org/gems/rubill
87
+ licenses:
88
+ - MIT
89
+ metadata: {}
90
+ post_install_message:
91
+ rdoc_options: []
92
+ require_paths:
93
+ - lib
94
+ required_ruby_version: !ruby/object:Gem::Requirement
95
+ requirements:
96
+ - - ">="
97
+ - !ruby/object:Gem::Version
98
+ version: '0'
99
+ required_rubygems_version: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ requirements: []
105
+ rubyforge_project:
106
+ rubygems_version: 2.4.6
107
+ signing_key:
108
+ specification_version: 4
109
+ summary: Interface with Bill.com
110
+ test_files: []