rubill 0.1.2

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: 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: []