freshbooks 1.0.0

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