freshbooks.rb 3.0.13

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.
Files changed (46) hide show
  1. data/History.txt +8 -0
  2. data/LICENSE +10 -0
  3. data/Manifest.txt +45 -0
  4. data/README +44 -0
  5. data/Rakefile +27 -0
  6. data/lib/freshbooks.rb +94 -0
  7. data/lib/freshbooks/base.rb +169 -0
  8. data/lib/freshbooks/category.rb +11 -0
  9. data/lib/freshbooks/client.rb +22 -0
  10. data/lib/freshbooks/connection.rb +153 -0
  11. data/lib/freshbooks/estimate.rb +15 -0
  12. data/lib/freshbooks/expense.rb +12 -0
  13. data/lib/freshbooks/invoice.rb +18 -0
  14. data/lib/freshbooks/item.rb +11 -0
  15. data/lib/freshbooks/line.rb +10 -0
  16. data/lib/freshbooks/links.rb +7 -0
  17. data/lib/freshbooks/list_proxy.rb +80 -0
  18. data/lib/freshbooks/payment.rb +13 -0
  19. data/lib/freshbooks/project.rb +12 -0
  20. data/lib/freshbooks/recurring.rb +15 -0
  21. data/lib/freshbooks/response.rb +25 -0
  22. data/lib/freshbooks/schema/definition.rb +20 -0
  23. data/lib/freshbooks/schema/mixin.rb +40 -0
  24. data/lib/freshbooks/staff.rb +13 -0
  25. data/lib/freshbooks/task.rb +12 -0
  26. data/lib/freshbooks/time_entry.rb +12 -0
  27. data/lib/freshbooks/xml_serializer.rb +17 -0
  28. data/lib/freshbooks/xml_serializer/serializers.rb +109 -0
  29. data/script/console +10 -0
  30. data/script/destroy +14 -0
  31. data/script/generate +14 -0
  32. data/test/fixtures/freshbooks_credentials.sample.yml +3 -0
  33. data/test/fixtures/invoice_create_response.xml +4 -0
  34. data/test/fixtures/invoice_get_response.xml +54 -0
  35. data/test/fixtures/invoice_list_response.xml +109 -0
  36. data/test/fixtures/success_response.xml +2 -0
  37. data/test/mock_connection.rb +13 -0
  38. data/test/schema/test_definition.rb +36 -0
  39. data/test/schema/test_mixin.rb +39 -0
  40. data/test/test_base.rb +97 -0
  41. data/test/test_connection.rb +145 -0
  42. data/test/test_helper.rb +48 -0
  43. data/test/test_invoice.rb +125 -0
  44. data/test/test_list_proxy.rb +60 -0
  45. data/test/test_page.rb +50 -0
  46. metadata +148 -0
@@ -0,0 +1,15 @@
1
+ module FreshBooks
2
+ class Estimate < FreshBooks::Base
3
+ define_schema do |s|
4
+ s.string :estimate_id, :status, :date, :notes, :terms, :first_name
5
+ s.string :last_name, :organization, :p_street1, :p_street2, :p_city
6
+ s.string :p_state, :p_country, :p_code
7
+ s.fixnum :client_id, :po_number
8
+ s.float :discount, :amount
9
+ s.array :lines
10
+ s.object :links, :read_only => true
11
+ end
12
+
13
+ actions :list, :get, :create, :update, :delete, :send_by_email
14
+ end
15
+ end
@@ -0,0 +1,12 @@
1
+ module FreshBooks
2
+ class Expense < FreshBooks::Base
3
+ define_schema do |s|
4
+ s.fixnum :expense_id, :staff_id, :category_id, :project_id, :client_id
5
+ s.float :amount, :tax1_amount, :tax1_percent, :tax2_amount, :tax2_percent
6
+ s.date :date
7
+ s.string :notes, :vendor, :status, :tax1_name, :tax2_name
8
+ end
9
+
10
+ actions :list, :get, :create, :update, :delete
11
+ end
12
+ end
@@ -0,0 +1,18 @@
1
+ module FreshBooks
2
+ class Invoice < FreshBooks::Base
3
+ define_schema do |s|
4
+ s.fixnum :invoice_id, :client_id, :po_number
5
+ s.fixnum :recurring_id, :read_only => true
6
+ s.float :amount, :amount_outstanding, :discount, :paid
7
+ s.date :date
8
+ s.date_time :updated
9
+ s.array :lines
10
+ s.object :links, :read_only => true
11
+ s.string :number, :organization, :status, :notes, :terms, :first_name, :last_name
12
+ s.string :p_street1, :p_street2, :p_city, :p_state, :p_country, :p_code
13
+ s.string :return_uri
14
+ end
15
+
16
+ actions :list, :get, :create, :update, :delete, :send_by_email, :send_by_snail_mail
17
+ end
18
+ end
@@ -0,0 +1,11 @@
1
+ module FreshBooks
2
+ class Item < FreshBooks::Base
3
+ define_schema do |s|
4
+ s.fixnum :item_id, :quantity, :inventory
5
+ s.float :unit_cost
6
+ s.string :name, :description
7
+ end
8
+
9
+ actions :create, :update, :get, :delete, :list
10
+ end
11
+ end
@@ -0,0 +1,10 @@
1
+ module FreshBooks
2
+ class Line < FreshBooks::Base
3
+ define_schema do |s|
4
+ s.string :name, :description, :tax1_name, :tax2_name
5
+ s.float :unit_cost, :tax1_percent, :tax2_percent
6
+ s.float :amount, :read_only => true
7
+ s.fixnum :quantity
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,7 @@
1
+ module FreshBooks
2
+ class Links < FreshBooks::Base
3
+ define_schema do |s|
4
+ s.string :client_view, :view, :edit, :read_only => true
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,80 @@
1
+ module FreshBooks
2
+ class ListProxy
3
+ include Enumerable
4
+
5
+ def initialize(list_page_proc)
6
+ @list_page_proc = list_page_proc
7
+ move_to_page(1)
8
+ end
9
+
10
+ def each(&block)
11
+ move_to_page(1)
12
+
13
+ begin
14
+ @array.each(&block)
15
+ end while @current_page.next_page? && next_page
16
+ end
17
+
18
+ def [](position)
19
+ move_to_page(@current_page.page_number(position))
20
+ @array[@current_page.position_number(position)]
21
+ end
22
+
23
+ def size
24
+ @current_page.total
25
+ end
26
+
27
+ private
28
+
29
+ def next_page
30
+ move_to_page(@current_page.page + 1)
31
+ end
32
+
33
+ def move_to_page(page)
34
+ return true if @current_page && @current_page.page == page
35
+ @array, @current_page = @list_page_proc.call(page)
36
+ end
37
+ end
38
+
39
+ class Page
40
+ attr_reader :page, :per_page, :total
41
+
42
+ def initialize(page, per_page, total, total_in_array = total)
43
+ @page = page.to_i
44
+ @per_page = per_page.to_i
45
+ @total = total.to_i
46
+
47
+ # Detect if response has pagination
48
+ if @per_page == 0 && @total == 0 && total_in_array != 0
49
+ # No pagination so fake it
50
+ @page = 1
51
+ @per_page = @total = total_in_array
52
+ end
53
+ end
54
+
55
+ # Get the page number that this element is on given the number of elements per page
56
+ def page_number(position)
57
+ (position / per_page) + 1
58
+ end
59
+
60
+ # Get the position number of this element based relative to the current page
61
+ def position_number(position)
62
+ position - ((page - 1) * per_page)
63
+ end
64
+
65
+ def pages
66
+ return 0 if per_page == 0
67
+
68
+ pages = total / per_page
69
+
70
+ if (total % per_page) != 0
71
+ pages += 1
72
+ end
73
+ pages
74
+ end
75
+
76
+ def next_page?
77
+ page < pages
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,13 @@
1
+ module FreshBooks
2
+ class Payment < FreshBooks::Base
3
+ define_schema do |s|
4
+ s.fixnum :client_id, :invoice_id, :payment_id
5
+ s.float :amount
6
+ s.date :date
7
+ s.date_time :updated
8
+ s.string :type, :notes
9
+ end
10
+
11
+ actions :list, :get, :create, :update
12
+ end
13
+ end
@@ -0,0 +1,12 @@
1
+ module FreshBooks
2
+ class Project < FreshBooks::Base
3
+ define_schema do |s|
4
+ s.string :name, :bill_method, :description
5
+ s.fixnum :project_id, :client_id
6
+ s.float :rate
7
+ s.array :tasks
8
+ end
9
+
10
+ actions :list, :get, :create, :update, :delete
11
+ end
12
+ end
@@ -0,0 +1,15 @@
1
+ module FreshBooks
2
+ class Recurring < FreshBooks::Base
3
+ define_schema do |s|
4
+ s.string :first_name, :last_name, :organization, :p_street1, :p_street2, :p_city
5
+ s.string :p_state, :p_country, :p_code, :lines, :status, :notes, :terms, :frequency
6
+ s.date :date
7
+ s.fixnum :recurring_id, :client_id, :po_number, :occurrences
8
+ s.float :discount, :amount
9
+ s.array :lines
10
+ s.boolean :stopped, :send_email, :send_snail_mail
11
+ end
12
+
13
+ actions :list, :get, :create, :update, :delete
14
+ end
15
+ end
@@ -0,0 +1,25 @@
1
+ module FreshBooks
2
+ class Response
3
+ attr_accessor :doc
4
+
5
+ def initialize(xml_raw)
6
+ @doc = REXML::Document.new(xml_raw)
7
+ end
8
+
9
+ def elements
10
+ @doc.root.elements
11
+ end
12
+
13
+ def success?
14
+ @doc.root.attributes['status'] == 'ok'
15
+ end
16
+
17
+ def fail?
18
+ !success?
19
+ end
20
+
21
+ def error_msg
22
+ return @doc.root.elements['error'].text
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,20 @@
1
+ module FreshBooks
2
+ module Schema
3
+ class Definition
4
+ attr_reader :members
5
+
6
+ def initialize
7
+ @members = {}
8
+ end
9
+
10
+ def method_missing(method, *attributes)
11
+ options = attributes.extract_options!
12
+ options[:read_only] ||= false
13
+
14
+ attributes.each do |attribute|
15
+ @members[attribute.to_s] = options.merge({ :type => method.to_sym })
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,40 @@
1
+ require File.dirname(__FILE__) + '/definition'
2
+
3
+ module FreshBooks
4
+ module Schema
5
+ module Mixin
6
+ def self.included(base)
7
+ base.extend ClassMethods
8
+ end
9
+
10
+ module ClassMethods
11
+ def define_schema
12
+ # Create the class method accessor for the schema definition
13
+ cattr_accessor :schema_definition
14
+ self.schema_definition ||= FreshBooks::Schema::Definition.new
15
+
16
+ # Yield to the block for the user to define the schema
17
+ yield self.schema_definition
18
+
19
+ # Process the schema additions
20
+ schema_definition.members.each do |member|
21
+ process_schema_member(member)
22
+ end
23
+ end
24
+
25
+ def process_schema_member(member)
26
+ member_name = member.first
27
+ member_options = member.last
28
+
29
+ # Create accessor
30
+ attr_accessor member_name
31
+
32
+ # Protect write if read only
33
+ if member_options[:read_only]
34
+ protected "#{member_name}="
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,13 @@
1
+ module FreshBooks
2
+ class Staff < FreshBooks::Base
3
+ define_schema do |s|
4
+ s.fixnum :staff_id, :number_of_logins
5
+ s.string :username, :first_name, :last_name, :email, :business_phone, :mobile_phone
6
+ s.string :street1, :street2, :city, :state, :country, :code
7
+ s.float :rate
8
+ s.date_time :last_login, :signup_date
9
+ end
10
+
11
+ actions :list, :get
12
+ end
13
+ end
@@ -0,0 +1,12 @@
1
+ module FreshBooks
2
+ class Task < FreshBooks::Base
3
+ define_schema do |s|
4
+ s.fixnum :task_id
5
+ s.string :name, :description
6
+ s.float :rate
7
+ s.boolean :billable
8
+ end
9
+
10
+ actions :list, :get, :create, :update, :delete
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ module FreshBooks
2
+ class TimeEntry < FreshBooks::Base
3
+ define_schema do |s|
4
+ s.fixnum :time_entry_id, :project_id, :task_id, :staff_id
5
+ s.float :hours
6
+ s.date :date
7
+ s.string :notes
8
+ end
9
+
10
+ actions :list, :get, :create, :update, :delete
11
+ end
12
+ end
@@ -0,0 +1,17 @@
1
+ require File.dirname(__FILE__) + '/xml_serializer/serializers'
2
+
3
+ module FreshBooks
4
+ module XmlSerializer
5
+ def self.to_value(node, type)
6
+ create_serializer(type).to_value(node)
7
+ end
8
+
9
+ def self.to_node(member_name, value, type)
10
+ create_serializer(type).to_node(member_name, value)
11
+ end
12
+
13
+ def self.create_serializer(type)
14
+ "FreshBooks::XmlSerializer::#{type.to_s.classify}Serializer".constantize
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,109 @@
1
+ module FreshBooks
2
+ module XmlSerializer
3
+ class FixnumSerializer
4
+ def self.to_node(member_name, value)
5
+ element = REXML::Element.new(member_name)
6
+ element.text = value.to_s
7
+ element
8
+ end
9
+
10
+ def self.to_value(xml_val)
11
+ xml_val.text.to_i
12
+ end
13
+ end
14
+
15
+ class FloatSerializer
16
+ def self.to_node(member_name, value)
17
+ element = REXML::Element.new(member_name)
18
+ element.text = value.to_s
19
+ element
20
+ end
21
+
22
+ def self.to_value(xml_val)
23
+ xml_val.text.to_f
24
+ end
25
+ end
26
+
27
+ class DateSerializer
28
+ def self.to_node(member_name, value)
29
+ element = REXML::Element.new(member_name)
30
+ element.text = value.to_s
31
+ element
32
+ end
33
+
34
+ def self.to_value(xml_val)
35
+ begin
36
+ Date.parse(xml_val.text.to_s)
37
+ rescue ArgumentError => e
38
+ # Sometimes freshbooks gives dates that look like this 0000-00-00 00:00:00
39
+ # just default to todays date, you have any other suggestions?
40
+ Date.new
41
+ end
42
+ end
43
+ end
44
+
45
+ class StringSerializer
46
+ def self.to_node(member_name, value)
47
+ element = REXML::Element.new(member_name)
48
+ element.text = value.to_s
49
+ element
50
+ end
51
+
52
+ def self.to_value(xml_val)
53
+ xml_val.text.to_s
54
+ end
55
+ end
56
+
57
+ class BooleanSerializer
58
+ def self.to_node(member_name, value)
59
+ element = REXML::Element.new(member_name)
60
+ element.text = value ? '1' : '0'
61
+ element
62
+ end
63
+
64
+ def self.to_value(xml_val)
65
+ xml_val.text.to_s == "1"
66
+ end
67
+ end
68
+
69
+ class ObjectSerializer
70
+ def self.to_node(member_name, value)
71
+ REXML::Document.new(value.to_xml(member_name))
72
+ end
73
+
74
+ def self.to_value(xml_val)
75
+ FreshBooks::const_get(xml_val.name.camelize)::new_from_xml(xml_val)
76
+ end
77
+ end
78
+
79
+ class ArraySerializer
80
+ def self.to_node(member_name, value)
81
+ element = REXML::Element.new(member_name)
82
+ value.each { |array_elem|
83
+ element.add_element(REXML::Document.new(array_elem.to_xml))
84
+ }
85
+ element
86
+ end
87
+
88
+ def self.to_value(xml_val)
89
+ xml_val.elements.map { |elem|
90
+ FreshBooks::const_get(elem.name.camelize)::new_from_xml(elem)
91
+ }
92
+ end
93
+ end
94
+
95
+ # FreshBooks datetimes are specified in gmt-4. This library assumes utc and
96
+ # will convert to the appropriate timezone.
97
+ class DateTimeSerializer
98
+ def self.to_node(member_name, value)
99
+ element = REXML::Element.new(member_name)
100
+ element.text = (value.utc - 4.hours).to_s(:db) # hack to convert to gmt-4, any better way?
101
+ element
102
+ end
103
+
104
+ def self.to_value(xml_val)
105
+ DateTime.parse(xml_val.text.to_s + " -04:00").utc # hack to convert from gmt-4 to utc
106
+ end
107
+ end
108
+ end
109
+ end