turingstudio-freshbooks-rb 3.0.1
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.
- data/History.txt +4 -0
- data/README.rdoc +65 -0
- data/Rakefile +20 -0
- data/lib/freshbooks.rb +41 -0
- data/lib/freshbooks/base.rb +286 -0
- data/lib/freshbooks/category.rb +11 -0
- data/lib/freshbooks/client.rb +32 -0
- data/lib/freshbooks/estimate.rb +30 -0
- data/lib/freshbooks/expense.rb +16 -0
- data/lib/freshbooks/invoice.rb +34 -0
- data/lib/freshbooks/item.rb +12 -0
- data/lib/freshbooks/line.rb +13 -0
- data/lib/freshbooks/payment.rb +22 -0
- data/lib/freshbooks/project.rb +39 -0
- data/lib/freshbooks/recurring.rb +51 -0
- data/lib/freshbooks/response.rb +25 -0
- data/lib/freshbooks/staff.rb +24 -0
- data/lib/freshbooks/task.rb +11 -0
- data/lib/freshbooks/time_entry.rb +13 -0
- data/lib/freshbooks/version.rb +6 -0
- data/spec/lib/freshbooks/base_spec.rb +385 -0
- data/spec/lib/freshbooks/category_spec.rb +64 -0
- data/spec/lib/freshbooks/client_spec.rb +91 -0
- data/spec/lib/freshbooks/estimate_spec.rb +80 -0
- data/spec/lib/freshbooks/expense_spec.rb +86 -0
- data/spec/lib/freshbooks/invoice_spec.rb +89 -0
- data/spec/lib/freshbooks/item_spec.rb +75 -0
- data/spec/lib/freshbooks/payment_spec.rb +77 -0
- data/spec/lib/freshbooks/project_spec.rb +83 -0
- data/spec/lib/freshbooks/recurring_spec.rb +82 -0
- data/spec/lib/freshbooks/staff_spec.rb +21 -0
- data/spec/lib/freshbooks/task_spec.rb +61 -0
- data/spec/lib/freshbooks/time_entry_spec.rb +94 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +11 -0
- data/tasks/rspec.rake +33 -0
- metadata +109 -0
data/History.txt
ADDED
data/README.rdoc
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
= freshbooks-rb
|
2
|
+
|
3
|
+
http://github.com/turingstudio/freshbooks-rb/
|
4
|
+
|
5
|
+
== DESCRIPTION:
|
6
|
+
|
7
|
+
A Ruby interface to the FreshBooks API. It exposes easy-to-use classes and methods for interacting with your FreshBooks account.
|
8
|
+
|
9
|
+
== SYNOPSIS:
|
10
|
+
|
11
|
+
Initialization:
|
12
|
+
|
13
|
+
FreshBooks::Base.setup('sample.freshbooks.com', 'mytoken')
|
14
|
+
|
15
|
+
Updating a client name:
|
16
|
+
|
17
|
+
clients = FreshBooks::Client.list
|
18
|
+
client = clients[0]
|
19
|
+
client.first_name = 'Suzy'
|
20
|
+
client.update
|
21
|
+
|
22
|
+
Updating an invoice:
|
23
|
+
|
24
|
+
invoice = FreshBooks::Invoice.get(4)
|
25
|
+
invoice.lines[0].quantity += 1
|
26
|
+
invoice.update
|
27
|
+
|
28
|
+
Creating a new item
|
29
|
+
|
30
|
+
item = FreshBooks::Item.new
|
31
|
+
item.name = 'A sample item'
|
32
|
+
item.create
|
33
|
+
|
34
|
+
== REQUIREMENTS:
|
35
|
+
|
36
|
+
activesupport >= 2.2.2
|
37
|
+
|
38
|
+
== INSTALL:
|
39
|
+
|
40
|
+
gem install turingstudio-freshbooks-rb
|
41
|
+
|
42
|
+
== LICENSE:
|
43
|
+
|
44
|
+
(The MIT License)
|
45
|
+
|
46
|
+
Copyright (c) 2009 The Turing Studio, Inc.
|
47
|
+
|
48
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
49
|
+
a copy of this software and associated documentation files (the
|
50
|
+
'Software'), to deal in the Software without restriction, including
|
51
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
52
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
53
|
+
permit persons to whom the Software is furnished to do so, subject to
|
54
|
+
the following conditions:
|
55
|
+
|
56
|
+
The above copyright notice and this permission notice shall be
|
57
|
+
included in all copies or substantial portions of the Software.
|
58
|
+
|
59
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
60
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
61
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
62
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
63
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
64
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
65
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
%w[rubygems rake rake/clean fileutils newgem rubigen].each { |f| require f }
|
2
|
+
require File.dirname(__FILE__) + '/lib/freshbooks/version'
|
3
|
+
|
4
|
+
# Generate all the Rake tasks
|
5
|
+
# Run 'rake -T' to see list of generated tasks (from gem root directory)
|
6
|
+
$hoe = Hoe.new('freshbooks-rb', FreshBooks::VERSION) do |p|
|
7
|
+
p.developer('The Turing Studio, Inc.', 'support@turingstudio.com')
|
8
|
+
p.changes = p.paragraphs_of("History.txt", 0..1).join("\n\n")
|
9
|
+
p.extra_deps = [
|
10
|
+
['xml-simple','>= 1.0.11'],
|
11
|
+
['activesupport','>= 2.2.2']
|
12
|
+
]
|
13
|
+
p.extra_dev_deps = [
|
14
|
+
['newgem', ">= #{::Newgem::VERSION}"]
|
15
|
+
]
|
16
|
+
p.clean_globs |= %w[**/.DS_Store tmp *.log]
|
17
|
+
end
|
18
|
+
|
19
|
+
require 'newgem/tasks' # load /tasks/*.rake
|
20
|
+
Dir['tasks/**/*.rake'].each { |t| load t }
|
data/lib/freshbooks.rb
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
#
|
2
|
+
# Usage:
|
3
|
+
#
|
4
|
+
# FreshBooks::Base.setup('sample.freshbooks.com', 'mytoken')
|
5
|
+
#
|
6
|
+
# clients = FreshBooks::Client.list
|
7
|
+
# client = clients[0]
|
8
|
+
# client.first_name = 'Suzy'
|
9
|
+
# client.update
|
10
|
+
#
|
11
|
+
# invoice = FreshBooks::Invoice.get(4)
|
12
|
+
# invoice.lines[0].quantity += 1
|
13
|
+
# invoice.update
|
14
|
+
#
|
15
|
+
# item = FreshBooks::Item.new
|
16
|
+
# item.name = 'A sample item'
|
17
|
+
# item.create
|
18
|
+
#
|
19
|
+
|
20
|
+
$:.unshift(File.dirname(__FILE__))
|
21
|
+
|
22
|
+
require 'net/https'
|
23
|
+
require 'rexml/document'
|
24
|
+
require 'activesupport'
|
25
|
+
|
26
|
+
require 'freshbooks/version'
|
27
|
+
require 'freshbooks/response'
|
28
|
+
require 'freshbooks/base'
|
29
|
+
require 'freshbooks/category'
|
30
|
+
require 'freshbooks/client'
|
31
|
+
require 'freshbooks/estimate'
|
32
|
+
require 'freshbooks/expense'
|
33
|
+
require 'freshbooks/invoice'
|
34
|
+
require 'freshbooks/item'
|
35
|
+
require 'freshbooks/line'
|
36
|
+
require 'freshbooks/payment'
|
37
|
+
require 'freshbooks/project'
|
38
|
+
require 'freshbooks/recurring'
|
39
|
+
require 'freshbooks/staff'
|
40
|
+
require 'freshbooks/task'
|
41
|
+
require 'freshbooks/time_entry'
|
@@ -0,0 +1,286 @@
|
|
1
|
+
module FreshBooks
|
2
|
+
class Base
|
3
|
+
class InternalError < Exception; end;
|
4
|
+
class AuthenticationError < Exception; end;
|
5
|
+
class UnknownSystemError < Exception; end;
|
6
|
+
class InvalidParameterError < Exception; end;
|
7
|
+
|
8
|
+
attr_accessor :resp
|
9
|
+
|
10
|
+
def initialize(data = {})
|
11
|
+
@old_values = {}
|
12
|
+
@changed_fields = []
|
13
|
+
data.each do |name, value|
|
14
|
+
send("#{name}=", value) if self.class.attributes[name]
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# Convert an instance of this class to an XML element
|
19
|
+
def to_xml(elem_name = nil)
|
20
|
+
root = REXML::Element.new(node_name)
|
21
|
+
|
22
|
+
self.class.attributes.each do |name, options|
|
23
|
+
next if options[:read_only] || !send("#{name}_changed?")
|
24
|
+
|
25
|
+
value = send(name)
|
26
|
+
|
27
|
+
if self.class.method_defined?("#{name}_to_xml")
|
28
|
+
send("#{name}_to_xml", root)
|
29
|
+
elsif value.is_a?(Array)
|
30
|
+
unless value.empty?
|
31
|
+
node = root.add_element(name.to_s)
|
32
|
+
value.each do |item|
|
33
|
+
node.add_element(item.to_xml)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
elsif !value.nil?
|
37
|
+
root.add_element(name.to_s).text = value
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
root
|
42
|
+
end
|
43
|
+
|
44
|
+
def create
|
45
|
+
self.class.ensure_allowed! :create
|
46
|
+
resp = Base.call_api("#{node_name}.create", node_name => self)
|
47
|
+
if resp.success?
|
48
|
+
self.send("#{node_name}_id=", resp.elements[1].text.to_i)
|
49
|
+
end
|
50
|
+
resp.success? ? self.send("#{node_name}_id") : nil
|
51
|
+
end
|
52
|
+
|
53
|
+
def update
|
54
|
+
self.class.ensure_allowed! :update
|
55
|
+
resp = Base.call_api("#{node_name}.update", node_name => self)
|
56
|
+
resp.success?
|
57
|
+
end
|
58
|
+
|
59
|
+
def delete
|
60
|
+
self.class.delete(self.send("#{node_name}_id"))
|
61
|
+
end
|
62
|
+
|
63
|
+
def send_by_email
|
64
|
+
self.class.send_by_email(self.send("#{node_name}_id"))
|
65
|
+
end
|
66
|
+
|
67
|
+
def send_by_snail_mail
|
68
|
+
self.class.send_by_snail_mail(self.send("#{node_name}_id"))
|
69
|
+
end
|
70
|
+
|
71
|
+
def node_name
|
72
|
+
self.class.node_name
|
73
|
+
end
|
74
|
+
|
75
|
+
class << self
|
76
|
+
def setup(account_url, auth_token, request_headers = {})
|
77
|
+
@@account_url = account_url
|
78
|
+
@@auth_token = auth_token
|
79
|
+
@@request_headers = request_headers
|
80
|
+
@@response = nil
|
81
|
+
end
|
82
|
+
|
83
|
+
def list(options = {})
|
84
|
+
ensure_allowed! :list
|
85
|
+
resp = Base.call_api("#{node_name}.list", options)
|
86
|
+
return nil unless resp.success?
|
87
|
+
|
88
|
+
elems = resp.elements[1].elements
|
89
|
+
elems.map { |elem| self.new_from_xml(elem) }
|
90
|
+
end
|
91
|
+
|
92
|
+
def get(id)
|
93
|
+
ensure_allowed! :get
|
94
|
+
resp = Base.call_api("#{node_name}.get", "#{node_name}_id" => id)
|
95
|
+
resp.success? ? self.new_from_xml(resp.elements[1]) : nil
|
96
|
+
end
|
97
|
+
|
98
|
+
def delete(id)
|
99
|
+
ensure_allowed! :delete
|
100
|
+
resp = Base.call_api("#{node_name}.delete", "#{node_name}_id" => id)
|
101
|
+
resp.success?
|
102
|
+
end
|
103
|
+
|
104
|
+
def send_by_email(id)
|
105
|
+
ensure_allowed! :send_by_email
|
106
|
+
resp = Base.call_api("#{node_name}.sendByEmail", "#{node_name}_id" => id)
|
107
|
+
resp.success?
|
108
|
+
end
|
109
|
+
|
110
|
+
def send_by_snail_mail(id)
|
111
|
+
ensure_allowed! :send_by_snail_mail
|
112
|
+
resp = Base.call_api("#{node_name}.sendBySnailMail", "#{node_name}_id" => id)
|
113
|
+
resp.success?
|
114
|
+
end
|
115
|
+
|
116
|
+
def node_name
|
117
|
+
self.to_s.split('::').last.underscore
|
118
|
+
end
|
119
|
+
|
120
|
+
#
|
121
|
+
# Defines structure attribute. Generates attr_accessor and #{name}_changed? method
|
122
|
+
#
|
123
|
+
# class Test < Base
|
124
|
+
# attribute :test_id, :integer
|
125
|
+
# end
|
126
|
+
#
|
127
|
+
# test = Test.new
|
128
|
+
# test.test_id_changed? # false
|
129
|
+
# test.test_id = 1
|
130
|
+
# test.test_id_changed? # true
|
131
|
+
# test.test_id # 1
|
132
|
+
#
|
133
|
+
def attribute(name, type, options = {})
|
134
|
+
self.attributes[name] = options.update(:type => type)
|
135
|
+
|
136
|
+
define_method("#{name}=") do |value|
|
137
|
+
@changed_fields << name
|
138
|
+
@old_values[name] = send(name) if @old_values[name].nil?
|
139
|
+
instance_variable_set("@#{name}", value)
|
140
|
+
end
|
141
|
+
|
142
|
+
define_method("#{name}_changed?") do
|
143
|
+
@changed_fields.include?(name)
|
144
|
+
end
|
145
|
+
|
146
|
+
self.send(:attr_reader, name)
|
147
|
+
end
|
148
|
+
|
149
|
+
def attributes
|
150
|
+
@attributes ||= {}
|
151
|
+
end
|
152
|
+
|
153
|
+
#
|
154
|
+
# Generates association methods.
|
155
|
+
#
|
156
|
+
# class Test < Base
|
157
|
+
# belongs_to :project
|
158
|
+
# has_many :items, :lines
|
159
|
+
# end
|
160
|
+
#
|
161
|
+
# test = Test.new
|
162
|
+
# test.project # Project.get(test.project_id)
|
163
|
+
# test.items # Item.list(:test_id => test.test_id)
|
164
|
+
# test.lines # Line.list(:test_id => test.test_id)
|
165
|
+
#
|
166
|
+
def has_many(*args)
|
167
|
+
args.each do |name|
|
168
|
+
class_eval "def #{name}(options = {}); @#{name} ||= #{name.to_s.classify}.list(options.merge('#{node_name}_id' => #{node_name}_id)); end"
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
def belongs_to(*args)
|
173
|
+
args.each do |name|
|
174
|
+
class_eval "def #{name}(options = {}); @#{name} ||= #{name.to_s.classify}.get(#{name}_id); end"
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
#
|
179
|
+
# Defines allowed API methods.
|
180
|
+
#
|
181
|
+
# class Test < Base
|
182
|
+
# method :list
|
183
|
+
# end
|
184
|
+
#
|
185
|
+
# Test.list # [...]
|
186
|
+
# Test.get(1) # "Error: Method get is not allowed for Test"
|
187
|
+
#
|
188
|
+
def method(*args)
|
189
|
+
@methods = (methods + args).uniq
|
190
|
+
end
|
191
|
+
|
192
|
+
def methods
|
193
|
+
@methods ||= []
|
194
|
+
end
|
195
|
+
|
196
|
+
def ensure_allowed!(name)
|
197
|
+
unless methods.include?(name)
|
198
|
+
raise "Error: Method #{name} is not allowed for #{self}"
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
# Create a new instance of this class from an XML element
|
203
|
+
def new_from_xml(xml_root)
|
204
|
+
object = self.new
|
205
|
+
attributes.each do |name, options|
|
206
|
+
node = xml_root.elements[name.to_s]
|
207
|
+
next if node.nil?
|
208
|
+
|
209
|
+
case options[:type]
|
210
|
+
when :integer
|
211
|
+
value = node.text.to_i
|
212
|
+
when :float
|
213
|
+
value = node.text.to_f
|
214
|
+
when :array
|
215
|
+
value = node.elements.map do |item|
|
216
|
+
FreshBooks::const_get(item.name.classify)::new_from_xml(item)
|
217
|
+
end
|
218
|
+
else
|
219
|
+
value = node.text.to_s
|
220
|
+
end
|
221
|
+
|
222
|
+
object.send("#{name}=", value)
|
223
|
+
end
|
224
|
+
object
|
225
|
+
end
|
226
|
+
|
227
|
+
def call_api(method, elems = [])
|
228
|
+
doc = REXML::Document.new '<?xml version="1.0" encoding="UTF-8"?>'
|
229
|
+
request = doc.add_element 'request'
|
230
|
+
request.attributes['method'] = method
|
231
|
+
|
232
|
+
elems.each do |key, value|
|
233
|
+
if value.is_a?(Base)
|
234
|
+
elem = value.to_xml
|
235
|
+
request.add_element(elem)
|
236
|
+
else
|
237
|
+
request.add_element(REXML::Element.new(key)).text = value.to_s
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
result = self.post(request.to_s)
|
242
|
+
|
243
|
+
@@response = Response.new(result)
|
244
|
+
|
245
|
+
#
|
246
|
+
# Failure
|
247
|
+
#
|
248
|
+
if @@response.fail?
|
249
|
+
error_msg = @@response.error_msg
|
250
|
+
|
251
|
+
raise InternalError.new, error_msg if error_msg =~ /not formatted correctly/
|
252
|
+
raise AuthenticationError.new, error_msg if error_msg =~ /[Aa]uthentication failed/
|
253
|
+
raise UnknownSystemError.new, error_msg if error_msg =~ /does not exist/
|
254
|
+
raise InvalidParameterError.new, error_msg if error_msg =~ /Invalid parameter: (.*)/
|
255
|
+
|
256
|
+
# Raise an exception for unexpected errors
|
257
|
+
raise error_msg
|
258
|
+
end
|
259
|
+
|
260
|
+
@@response
|
261
|
+
end
|
262
|
+
|
263
|
+
def response
|
264
|
+
@@response
|
265
|
+
end
|
266
|
+
|
267
|
+
def post(body)
|
268
|
+
connection = Net::HTTP.new(@@account_url, 443)
|
269
|
+
connection.use_ssl = true
|
270
|
+
connection.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
271
|
+
|
272
|
+
request = Net::HTTP::Post.new(FreshBooks::SERVICE_URL)
|
273
|
+
request.basic_auth @@auth_token, 'X'
|
274
|
+
request.body = body
|
275
|
+
request.content_type = 'application/xml'
|
276
|
+
@@request_headers.each_pair do |name, value|
|
277
|
+
request[name.to_s] = value
|
278
|
+
end
|
279
|
+
|
280
|
+
result = connection.start { |http| http.request(request) }
|
281
|
+
|
282
|
+
result.body
|
283
|
+
end
|
284
|
+
end
|
285
|
+
end
|
286
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module FreshBooks
|
2
|
+
class Client < Base
|
3
|
+
attribute :client_id, :integer
|
4
|
+
attribute :first_name, :string
|
5
|
+
attribute :last_name, :string
|
6
|
+
attribute :organization, :string
|
7
|
+
attribute :email, :string
|
8
|
+
attribute :username, :string
|
9
|
+
attribute :password, :string
|
10
|
+
attribute :work_phone, :string
|
11
|
+
attribute :home_phone, :string
|
12
|
+
attribute :mobile, :string
|
13
|
+
attribute :fax, :string
|
14
|
+
attribute :notes, :string
|
15
|
+
attribute :p_street1, :string
|
16
|
+
attribute :p_street2, :string
|
17
|
+
attribute :p_city, :string
|
18
|
+
attribute :p_state, :string
|
19
|
+
attribute :p_country, :string
|
20
|
+
attribute :p_code, :string
|
21
|
+
attribute :s_street1, :string
|
22
|
+
attribute :s_street2, :string
|
23
|
+
attribute :s_city, :string
|
24
|
+
attribute :s_state, :string
|
25
|
+
attribute :s_country, :string
|
26
|
+
attribute :s_code, :string
|
27
|
+
attribute :url, :string, :read_only => true
|
28
|
+
|
29
|
+
has_many :expenses, :estimates, :invoices, :payments, :projects, :recurrings
|
30
|
+
method :list, :get, :create, :update, :delete
|
31
|
+
end
|
32
|
+
end
|