turingstudio-freshbooks-rb 3.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|