freshbooks 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt ADDED
@@ -0,0 +1,4 @@
1
+ == 1.0.0 / 2007-09-16
2
+
3
+ * 1 Initial release on RubyForge
4
+
data/Manifest.txt ADDED
@@ -0,0 +1,7 @@
1
+ History.txt
2
+ Manifest.txt
3
+ README.txt
4
+ Rakefile
5
+ bin/freshbooks
6
+ lib/freshbooks.rb
7
+ test/test_freshbooks.rb
data/README.txt ADDED
@@ -0,0 +1,48 @@
1
+ FreshBooks.rb
2
+ by Ben Vinegar
3
+ http://www.benlog.org
4
+
5
+ == DESCRIPTION:
6
+
7
+ FreshBooks.rb: FreshBooks API wrapper for Ruby
8
+
9
+ == FEATURES/PROBLEMS:
10
+
11
+ * TODO
12
+
13
+ == SYNOPSIS:
14
+
15
+ FreshBooks.rb: FreshBooks API wrapper for Ruby
16
+
17
+ == REQUIREMENTS:
18
+
19
+ * REXML
20
+
21
+ == INSTALL:
22
+
23
+ * sudo gem install freshbooks
24
+
25
+ == LICENSE:
26
+
27
+ (The MIT License)
28
+
29
+ Copyright (c) 2007 FIX
30
+
31
+ Permission is hereby granted, free of charge, to any person obtaining
32
+ a copy of this software and associated documentation files (the
33
+ 'Software'), to deal in the Software without restriction, including
34
+ without limitation the rights to use, copy, modify, merge, publish,
35
+ distribute, sublicense, and/or sell copies of the Software, and to
36
+ permit persons to whom the Software is furnished to do so, subject to
37
+ the following conditions:
38
+
39
+ The above copyright notice and this permission notice shall be
40
+ included in all copies or substantial portions of the Software.
41
+
42
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
43
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
44
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
45
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
46
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
47
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
48
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,17 @@
1
+ # -*- ruby -*-
2
+
3
+ require 'rubygems'
4
+ require 'hoe'
5
+ require './lib/freshbooks.rb'
6
+
7
+ Hoe.new('freshbooks', FreshBooks::VERSION) do |p|
8
+ p.rubyforge_name = 'freshbooks'
9
+ p.author = 'Ben Vinegar'
10
+ # p.email = 'FIX'
11
+ # p.summary = 'FIX'
12
+ # p.description = p.paragraphs_of('README.txt', 2..5).join("\n\n")
13
+ # p.url = p.paragraphs_of('README.txt', 0).first.split(/\n/)[1..-1]
14
+ p.changes = p.paragraphs_of('History.txt', 0..1).join("\n\n")
15
+ end
16
+
17
+ # vim: syntax=Ruby
data/bin/freshbooks ADDED
File without changes
data/lib/freshbooks.rb ADDED
@@ -0,0 +1,472 @@
1
+ #------------------------------------------------------------------------------
2
+ # Freshbooks API - Ruby Library
3
+ #
4
+ # Copyright (c) 2007 Ben Vinegar (http://www.benlog.org)
5
+ #
6
+ # This work is distributed under an MIT License:
7
+ # http://www.opensource.org/licenses/mit-license.php
8
+ #
9
+ #------------------------------------------------------------------------------
10
+ # Usage:
11
+ #
12
+ # FreshBooks.setup('sample.freshbooks.com', 'mytoken')
13
+ #
14
+ # clients = FreshBooks::Client.list
15
+ # client = clients[0]
16
+ # client.first_name = 'Suzy'
17
+ # client.update
18
+ #
19
+ # invoice = FreshBooks::Invoice.get(4)
20
+ # invoice.lines[0].quantity += 1
21
+ # invoice.update
22
+ #
23
+ # item = FreshBooks::Item.new
24
+ # item.name = 'A sample item'
25
+ # item.create
26
+ #
27
+ #==============================================================================
28
+
29
+ require 'net/https'
30
+ require 'rexml/document'
31
+ include REXML
32
+
33
+ module FreshBooks
34
+ VERSION = '1.0.0'
35
+
36
+ API_PATH = "/api/xml-in"
37
+
38
+ class InternalError < Exception; end;
39
+ class AuthenticationError < Exception; end;
40
+ class UnknownSystemError < Exception; end;
41
+
42
+ @@account_url, @@auth_token = ''
43
+ @@response = nil
44
+
45
+ def self.setup(account_url, auth_token)
46
+ @@account_url = account_url
47
+ @@auth_token = auth_token
48
+
49
+ true
50
+ end
51
+
52
+ def self.last_response
53
+ @@response
54
+ end
55
+
56
+ def self.call_api(method, elems = [])
57
+ doc = Document.new '<?xml version="1.0" encoding="UTF-8"?>'
58
+ request = doc.add_element 'request'
59
+ request.attributes['method'] = method
60
+
61
+ elems.each do |key, value|
62
+ if value.is_a?(BaseObject)
63
+ elem = value.to_xml
64
+ request.add_element elem
65
+ else
66
+ request.add_element(Element.new(key)).text = value.to_s
67
+ end
68
+ end
69
+
70
+ result = self.post(request.to_s)
71
+
72
+ @@response = Response.new(result)
73
+
74
+ #
75
+ # Failure
76
+ #
77
+ if @@response.fail?
78
+ error_msg = @@response.error_msg
79
+
80
+ # Raise an exception for unexpected errors
81
+
82
+ raise InternalError.new if error_msg =~ /not formatted correctly/
83
+ raise AuthenticationError.new if error_msg =~ /[Aa]uthentication failed/
84
+ raise UnknownSystemError.new if error_msg =~ /does not exist/
85
+ end
86
+
87
+ @@response
88
+ end
89
+
90
+ def self.post(body)
91
+ connection = Net::HTTP.new(@@account_url, 443)
92
+ connection.use_ssl = true
93
+ connection.verify_mode = OpenSSL::SSL::VERIFY_NONE
94
+
95
+ request = Net::HTTP::Post.new(FreshBooks::API_PATH)
96
+ request.basic_auth @@auth_token, 'X'
97
+ request.body = body
98
+ request.content_type = 'application/xml'
99
+
100
+ result = connection.start { |http| http.request(request) }
101
+
102
+ result.body
103
+ end
104
+
105
+ class Response
106
+ attr_accessor :doc
107
+ def initialize(xml_raw)
108
+ @doc = Document.new xml_raw
109
+ end
110
+
111
+ def elements
112
+ @doc.root.elements
113
+ end
114
+
115
+ def success?
116
+ @doc.root.attributes['status'] == 'ok'
117
+ end
118
+
119
+ def fail?
120
+ !success?
121
+ end
122
+
123
+ def error_msg
124
+ return @doc.root.elements['error'].text
125
+ end
126
+ end
127
+
128
+
129
+ #--------------------------------------------------------------------------
130
+ # BaseObject
131
+ #==========================================================================
132
+
133
+ class BaseObject < Struct
134
+ attr_accessor :resp
135
+
136
+ # Rails-like accessor to member variables
137
+ #def attributes; return members; end;
138
+
139
+ # Maps field names ('invoice_id') to Ruby types (Fixnum)
140
+ TYPE_MAPPINGS = {}
141
+
142
+ # Anonymous methods for converting an XML element to its
143
+ # corresponding Ruby type
144
+ MAPPING_FNS = {
145
+ Fixnum => lambda { |xml_val| xml_val.text.to_i },
146
+ Float => lambda { |xml_val| xml_val.text.to_f },
147
+ BaseObject => lambda { |xml_val| BaseObject.class::new_from_xml },
148
+ Array => lambda do |xml_val|
149
+ xml_val.elements.map do |elem|
150
+ FreshBooks::const_get(elem.name.capitalize)::new_from_xml(elem)
151
+ end
152
+ end
153
+ }
154
+
155
+ # Create a new instance of this class from an XML element
156
+ def self.new_from_xml(xml_root)
157
+ object = self.new
158
+
159
+ self.members.each do |field_name|
160
+ node = xml_root.elements[field_name]
161
+
162
+ next if node.nil?
163
+
164
+ mapping = self::TYPE_MAPPINGS[field_name]
165
+ if mapping
166
+ object[field_name] = self::MAPPING_FNS[mapping].call(node)
167
+ else
168
+ object[field_name] = node.text.to_s
169
+ end
170
+ end
171
+ return object
172
+ end
173
+
174
+ # Convert an instance of this class to an XML element
175
+ def to_xml
176
+ # The root element is the class name, downcased
177
+ elem_name = self.class.to_s.split('::').last.downcase
178
+ root = Element.new elem_name
179
+
180
+ # Add each BaseObject member to the root elem
181
+ self.members.each do |field_name|
182
+
183
+ value = self.send(field_name)
184
+
185
+ if value.is_a?(Array)
186
+ node = root.add_element(field_name)
187
+ value.each { |array_elem| node.add_element(array_elem.to_xml) }
188
+ elsif !value.nil?
189
+ root.add_element(field_name).text = value
190
+ end
191
+ end
192
+ root
193
+ end
194
+
195
+ end
196
+
197
+ #--------------------------------------------------------------------------
198
+ # Clients
199
+ #==========================================================================
200
+
201
+ Client = BaseObject.new(:client_id, :first_name, :last_name, :organization,
202
+ :email, :username, :password, :work_phone, :home_phone, :mobile, :fax,
203
+ :notes, :p_street1, :p_street2, :p_city, :p_state, :p_country, :p_code,
204
+ :s_street1, :s_street2, :s_city, :s_state, :s_country, :s_code, :url)
205
+
206
+ class Client
207
+ TYPE_MAPPINGS = { 'client_id' => Fixnum }
208
+ def create
209
+ resp = FreshBooks::call_api('client.create', 'client' => self)
210
+ if resp.success?
211
+ self.client_id = resp.elements[1].to_i
212
+ end
213
+
214
+ resp.success? ? self.client_id : nil
215
+ end
216
+
217
+ def update
218
+ resp = FreshBooks::call_api('client.update', 'client' => self)
219
+
220
+ resp.success?
221
+ end
222
+
223
+ def delete
224
+ Client::delete(self.client_id)
225
+ end
226
+
227
+ def self.get(client_id)
228
+ resp = FreshBooks::call_api('client.get', 'client_id' => client_id)
229
+
230
+ resp.success? ? self.new_from_xml(resp.elements[1]) : nil
231
+ end
232
+
233
+ def self.list(options = {})
234
+ resp = FreshBooks::call_api('client.list', options)
235
+
236
+ resp.success? ? resp.elements.map { |elem| self.new_from_xml(elem) } : nil
237
+ end
238
+
239
+ def self.delete(client_id)
240
+ resp = FreshBooks::call_api('client.delete', 'client_id' => client_id)
241
+
242
+ resp.success?
243
+ end
244
+
245
+ def invoices(options = {})
246
+ options.merge( 'client_id' => self.client_id )
247
+
248
+ Invoice::list(options)
249
+ end
250
+ end
251
+
252
+ #--------------------------------------------------------------------------
253
+ # Invoices
254
+ #==========================================================================
255
+
256
+ Invoice = BaseObject.new(:invoice_id, :client_id, :date, :po_number,
257
+ :terms, :first_name, :last_name, :organization, :p_street1, :p_street2, :p_city,
258
+ :p_state, :p_country, :p_code, :amount, :lines, :discount, :status, :notes)
259
+
260
+
261
+ class Invoice
262
+ TYPE_MAPPINGS = { 'client_id' => Fixnum, 'lines' => Array,
263
+ 'po_number' => Fixnum, 'discount' => Float, 'amount' => Float }
264
+
265
+ def initialize
266
+ super
267
+ self.lines ||= []
268
+ end
269
+
270
+ def create
271
+ resp = FreshBooks::call_api('invoice.create', 'invoice' => self)
272
+ if resp.success?
273
+ self.invoice_id = resp.elements[1].text.to_i
274
+ end
275
+
276
+ resp.success? ? self.invoice_id : nil
277
+ end
278
+
279
+ def update
280
+ resp = FreshBooks::call_api('invoice.update', 'invoice' => self)
281
+
282
+ resp.success?
283
+ end
284
+
285
+ def delete; Invoice::delete(self.invoice_id); end;
286
+ def send_by_email; Invoice::send_by_email(self.invoice_id); end;
287
+ def send_by_snail_mail; Invoice::send_by_snail_mail(self.invoice_id); end;
288
+
289
+ def self.get(invoice_id)
290
+ resp = FreshBooks::call_api('invoice.get', 'invoice_id' => invoice_id)
291
+
292
+ resp.success? ? self.new_from_xml(resp.elements[1]) : nil
293
+ end
294
+
295
+ def self.delete(invoice_id)
296
+ resp = FreshBooks::call_api('invoice.delete', 'invoice_id' => invoice_id)
297
+
298
+ resp.success?
299
+ end
300
+
301
+ def self.list(options = {})
302
+ resp = FreshBooks::call_api('invoice.list', options)
303
+
304
+ resp.success? ? resp.elements.map { |elem| self.new_from_xml(elem) } : nil
305
+ end
306
+
307
+ def self.send_by_email(invoice_id)
308
+ resp = FreshBooks::call_api('invoice.sendByEmail', 'invoice_id' => invoice_id)
309
+
310
+ resp.success?
311
+ end
312
+
313
+ def self.send_by_snail_mail(invoice_id)
314
+ resp = FreshBooks::call_api('invoice.sendBySnailMail', 'invoice_id' => invoice_id)
315
+
316
+ resp.success?
317
+ end
318
+
319
+ end
320
+
321
+ Line = BaseObject.new(:name, :description, :unit_cost, :quantity, :tax1_name,
322
+ :tax2_name, :tax1_percent, :tax2_percent, :amount)
323
+
324
+ class Line
325
+ TYPE_MAPPINGS = { 'unit_cost' => Float, 'quantity' => Fixnum,
326
+ 'tax1_percent' => Float, 'tax2_percent' => Float, 'amount' => Float }
327
+ end
328
+
329
+ #--------------------------------------------------------------------------
330
+ # Items
331
+ #==========================================================================
332
+
333
+ Item = BaseObject.new(:item_id, :name, :description, :unit_cost,
334
+ :quantity, :inventory)
335
+ class Item
336
+ TYPE_MAPPINGS = { 'item_id' => Fixnum, 'unit_cost' => Float,
337
+ 'quantity' => Fixnum, 'inventory' => Fixnum }
338
+
339
+ def create
340
+ resp = FreshBooks::call_api('item.create', 'item' => self)
341
+ if resp.success?
342
+ self.item_id = resp.elements[1].text.to_i
343
+ end
344
+
345
+ resp.success? ? self.item_id : nil
346
+ end
347
+
348
+ def update
349
+ resp = FreshBooks::call_api('item.update', 'item' => self)
350
+
351
+ resp.success?
352
+ end
353
+
354
+ def delete
355
+ Item::delete(self.item_id)
356
+ end
357
+
358
+ def self.get(item_id)
359
+ resp = FreshBooks::call_api('item.get', 'item_id' => item_id)
360
+
361
+ resp.success? ? self.new_from_xml(resp.elements[1]) : nil
362
+ end
363
+
364
+ def self.delete(item_id)
365
+ resp = FreshBooks::call_api('item.delete', 'item_id' => item_id)
366
+
367
+ resp.success?
368
+ end
369
+
370
+ def self.list(options = {})
371
+ resp = FreshBooks::call_api('item.list', options)
372
+
373
+ resp.success? ? resp.elements.map { |elem| self.new_from_xml(elem) } : nil
374
+ end
375
+ end
376
+
377
+ #--------------------------------------------------------------------------
378
+ # Payments
379
+ #==========================================================================
380
+
381
+ Payment = BaseObject.new(:payment_id, :client_id, :invoice_id, :date, :amount, :type, :notes)
382
+ class Payment
383
+ TYPE_MAPPINGS = { 'client_id' => Fixnum, 'invoice_id' => Fixnum, 'amount' => Float }
384
+
385
+ def create
386
+ resp = FreshBooks::call_api('payment.create', 'payment' => self)
387
+ if resp.success?
388
+ self.payment_id = resp.elements[1].text.to_i
389
+ end
390
+
391
+ resp.success? ? self.payment_id : nil
392
+ end
393
+
394
+ def update
395
+ resp = FreshBooks::call_api('payment.update', 'payment' => self)
396
+
397
+ resp.success?
398
+ end
399
+
400
+ def self.get(payment)
401
+ resp = FreshBooks::call_api('payment.get', 'payment_id' => payment_id)
402
+
403
+ resp.success? ? self.new_from_xml(resp.elements[1]) : nil
404
+ end
405
+
406
+ def self.list(options = {})
407
+ resp = FreshBooks::call_api('payment.list', options)
408
+
409
+ resp.success? ? resp.elements.map { |elem| self.new_from_xml(elem) } : nil
410
+ end
411
+ end
412
+
413
+ #--------------------------------------------------------------------------
414
+ # Recurring Profiles
415
+ #==========================================================================
416
+
417
+ Recurring = BaseObject.new(:recurring_id, :client_id, :date, :po_number,
418
+ :terms, :first_name, :last_name, :organization, :p_street1, :p_street2, :p_city,
419
+ :p_state, :p_country, :p_code, :amount, :lines, :discount, :status, :notes,
420
+ :occurrences, :frequency, :send_email, :send_snail_mail)
421
+
422
+
423
+ class Recurring
424
+ TYPE_MAPPINGS = { 'client_id' => Fixnum, 'lines' => Array,
425
+ 'po_number' => Fixnum, 'discount' => Float, 'amount' => Float,
426
+ 'occurrences' => Fixnum }
427
+
428
+ def initialize
429
+ super
430
+ self.lines ||= []
431
+ end
432
+
433
+ def create
434
+ resp = FreshBooks::call_api('recurring.create', 'recurring' => self)
435
+ if resp.success?
436
+ self.invoice_id = resp.elements[1].text.to_i
437
+ end
438
+
439
+ resp.success? ? self.invoice_id : nil
440
+ end
441
+
442
+ def update
443
+ resp = FreshBooks::call_api('recurring.update', 'recurring' => self)
444
+
445
+ resp.success?
446
+ end
447
+
448
+ def self.get(recurring_id)
449
+ resp = FreshBooks::call_api('recurring.get', 'recurring_id' => recurring_id)
450
+
451
+ resp.success? ? self.new_from_xml(resp.elements[1]) : nil
452
+ end
453
+
454
+ def delete
455
+ Recurring::delete(self.recurring_id)
456
+ end
457
+
458
+ def self.delete(recurring_id)
459
+ resp = FreshBooks::call_api('recurring.delete', 'recurring_id' => recurring_id)
460
+
461
+ resp.success?
462
+ end
463
+
464
+ def self.list(options = {})
465
+ resp = FreshBooks::call_api('recurring.list', options)
466
+
467
+ resp.success? ? resp.elements.map { |elem| self.new_from_xml(elem) } : nil
468
+ end
469
+
470
+ end
471
+ end
472
+
File without changes
metadata ADDED
@@ -0,0 +1,63 @@
1
+ --- !ruby/object:Gem::Specification
2
+ rubygems_version: 0.9.0
3
+ specification_version: 1
4
+ name: freshbooks
5
+ version: !ruby/object:Gem::Version
6
+ version: 1.0.0
7
+ date: 2007-09-16 00:00:00 -04:00
8
+ summary: The author was too lazy to write a summary
9
+ require_paths:
10
+ - lib
11
+ email: ryand-ruby@zenspider.com
12
+ homepage: http://www.zenspider.com/ZSS/Products/freshbooks/
13
+ rubyforge_project: freshbooks
14
+ description: The author was too lazy to write a description
15
+ autorequire:
16
+ default_executable:
17
+ bindir: bin
18
+ has_rdoc: true
19
+ required_ruby_version: !ruby/object:Gem::Version::Requirement
20
+ requirements:
21
+ - - ">"
22
+ - !ruby/object:Gem::Version
23
+ version: 0.0.0
24
+ version:
25
+ platform: ruby
26
+ signing_key:
27
+ cert_chain:
28
+ post_install_message:
29
+ authors:
30
+ - Ben Vinegar
31
+ files:
32
+ - History.txt
33
+ - Manifest.txt
34
+ - README.txt
35
+ - Rakefile
36
+ - bin/freshbooks
37
+ - lib/freshbooks.rb
38
+ - test/test_freshbooks.rb
39
+ test_files:
40
+ - test/test_freshbooks.rb
41
+ rdoc_options:
42
+ - --main
43
+ - README.txt
44
+ extra_rdoc_files:
45
+ - History.txt
46
+ - Manifest.txt
47
+ - README.txt
48
+ executables:
49
+ - freshbooks
50
+ extensions: []
51
+
52
+ requirements: []
53
+
54
+ dependencies:
55
+ - !ruby/object:Gem::Dependency
56
+ name: hoe
57
+ version_requirement:
58
+ version_requirements: !ruby/object:Gem::Version::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: 1.3.0
63
+ version: