mirror42-freshbooks.rb 3.0.25

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 (47) hide show
  1. checksums.yaml +15 -0
  2. data/History.txt +8 -0
  3. data/LICENSE +10 -0
  4. data/Manifest.txt +45 -0
  5. data/README +58 -0
  6. data/Rakefile +27 -0
  7. data/lib/freshbooks.rb +95 -0
  8. data/lib/freshbooks/base.rb +176 -0
  9. data/lib/freshbooks/category.rb +11 -0
  10. data/lib/freshbooks/client.rb +23 -0
  11. data/lib/freshbooks/connection.rb +162 -0
  12. data/lib/freshbooks/estimate.rb +15 -0
  13. data/lib/freshbooks/expense.rb +12 -0
  14. data/lib/freshbooks/invoice.rb +22 -0
  15. data/lib/freshbooks/item.rb +11 -0
  16. data/lib/freshbooks/line.rb +11 -0
  17. data/lib/freshbooks/links.rb +7 -0
  18. data/lib/freshbooks/list_proxy.rb +80 -0
  19. data/lib/freshbooks/payment.rb +13 -0
  20. data/lib/freshbooks/project.rb +12 -0
  21. data/lib/freshbooks/recurring.rb +16 -0
  22. data/lib/freshbooks/response.rb +25 -0
  23. data/lib/freshbooks/schema/definition.rb +20 -0
  24. data/lib/freshbooks/schema/mixin.rb +40 -0
  25. data/lib/freshbooks/staff.rb +13 -0
  26. data/lib/freshbooks/task.rb +12 -0
  27. data/lib/freshbooks/time_entry.rb +12 -0
  28. data/lib/freshbooks/xml_serializer.rb +17 -0
  29. data/lib/freshbooks/xml_serializer/serializers.rb +116 -0
  30. data/script/console +10 -0
  31. data/script/destroy +14 -0
  32. data/script/generate +14 -0
  33. data/test/fixtures/freshbooks_credentials.sample.yml +3 -0
  34. data/test/fixtures/invoice_create_response.xml +4 -0
  35. data/test/fixtures/invoice_get_response.xml +54 -0
  36. data/test/fixtures/invoice_list_response.xml +109 -0
  37. data/test/fixtures/success_response.xml +2 -0
  38. data/test/mock_connection.rb +13 -0
  39. data/test/schema/test_definition.rb +36 -0
  40. data/test/schema/test_mixin.rb +39 -0
  41. data/test/test_base.rb +151 -0
  42. data/test/test_connection.rb +145 -0
  43. data/test/test_helper.rb +48 -0
  44. data/test/test_invoice.rb +125 -0
  45. data/test/test_list_proxy.rb +60 -0
  46. data/test/test_page.rb +50 -0
  47. metadata +157 -0
@@ -0,0 +1,23 @@
1
+ module FreshBooks
2
+ class Client < FreshBooks::Base
3
+ define_schema do |s|
4
+ s.string :first_name, :last_name, :organization, :email
5
+ s.string :username, :password, :work_phone, :home_phone
6
+ s.string :mobile, :fax, :notes, :p_street1, :p_street2, :p_city
7
+ s.string :p_state, :p_country, :p_code, :s_street1, :s_street2
8
+ s.string :s_city, :s_state, :s_country, :s_code
9
+ s.string :vat_name, :vat_number
10
+ s.float :credit, :read_only => true
11
+ s.date_time :updated, :read_only => true
12
+ s.fixnum :client_id
13
+ s.object :links, :read_only => true
14
+ end
15
+
16
+ actions :list, :get, :create, :update, :delete
17
+
18
+ def invoices(options = {})
19
+ options.merge!('client_id' => self.client_id)
20
+ Invoice::list(options)
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,162 @@
1
+ require 'net/https'
2
+ require 'rexml/document'
3
+ require 'logger'
4
+
5
+ module FreshBooks
6
+ class Connection
7
+ attr_reader :account_url, :auth_token, :utc_offset, :request_headers
8
+
9
+ @@logger = Logger.new(STDOUT)
10
+ def logger
11
+ @@logger
12
+ end
13
+
14
+ def logger=(value)
15
+ @@logger = value
16
+ end
17
+
18
+ def self.log_level=(level)
19
+ @@logger.level = level
20
+ end
21
+ self.log_level = Logger::WARN
22
+
23
+ def initialize(account_url, auth_token, request_headers = {}, options = {})
24
+ raise InvalidAccountUrlError.new unless account_url =~ /^[0-9a-zA-Z\-_]+\.freshbooks\.com$/
25
+
26
+ @account_url = account_url
27
+ @auth_token = auth_token
28
+ @request_headers = request_headers
29
+ @utc_offset = options[:utc_offset] || -4
30
+ @start_session_count = 0
31
+ end
32
+
33
+ def call_api(method, elements = [])
34
+ request = create_request(method, elements)
35
+ result = post(request)
36
+ Response.new(result)
37
+ end
38
+
39
+ def direct_post(xml)
40
+ result = post(xml)
41
+ Response.new(result)
42
+ end
43
+
44
+ def start_session(&block)
45
+ @connection = obtain_connection if @start_session_count == 0
46
+ @start_session_count = @start_session_count + 1
47
+
48
+ begin
49
+ block.call(@connection)
50
+ ensure
51
+ @start_session_count = @start_session_count - 1
52
+ close if @start_session_count == 0
53
+ end
54
+ end
55
+
56
+ protected
57
+
58
+ def create_request(method, elements = [])
59
+ doc = REXML::Document.new '<?xml version="1.0" encoding="UTF-8"?>'
60
+ request = doc.add_element('request')
61
+ request.attributes['method'] = method
62
+
63
+ elements.each do |element|
64
+ if element.kind_of?(Hash)
65
+ element = element.to_a
66
+ end
67
+ key = element.first
68
+ value = element.last
69
+
70
+ if value.kind_of?(Base)
71
+ request.add_element(REXML::Document.new(value.to_xml))
72
+ else
73
+ request.add_element(REXML::Element.new(key.to_s)).text = value.to_s
74
+ end
75
+ end
76
+
77
+ doc.to_s
78
+ end
79
+
80
+ def obtain_connection(force = false)
81
+ return @connection if @connection && !force
82
+
83
+ @connection = Net::HTTP.new(@account_url, 443)
84
+ @connection.use_ssl = true
85
+ @connection.verify_mode = OpenSSL::SSL::VERIFY_NONE
86
+ @connection.start
87
+ end
88
+
89
+ def reconnect
90
+ close
91
+ obtain_connection(true)
92
+ end
93
+
94
+ def close
95
+ begin
96
+ @connection.finish if @connection
97
+ rescue => e
98
+ logger.error("Error closing connection: " + e.message)
99
+ end
100
+ @connection = nil
101
+ end
102
+
103
+ def post(request_body)
104
+ result = nil
105
+ request = Net::HTTP::Post.new(FreshBooks::SERVICE_URL)
106
+ request.basic_auth @auth_token, 'X'
107
+ request.body = request_body
108
+ request.content_type = 'application/xml'
109
+ @request_headers.each_pair do |name, value|
110
+ request[name.to_s] = value
111
+ end
112
+
113
+ result = post_request(request)
114
+
115
+ if logger.debug?
116
+ logger.debug "Request:"
117
+ logger.debug request_body
118
+ logger.debug "Response:"
119
+ logger.debug result.body
120
+ end
121
+
122
+ check_for_api_error(result)
123
+ end
124
+
125
+ # For connections that take a long time, we catch EOFError's and reconnect seamlessly
126
+ def post_request(request)
127
+ response = nil
128
+ has_reconnected = false
129
+ start_session do |connection|
130
+ begin
131
+ response = connection.request(request)
132
+ rescue EOFError => e
133
+ raise e if has_reconnected
134
+
135
+ has_reconnected = true
136
+ connection = reconnect
137
+ retry
138
+ end
139
+ end
140
+ response
141
+ end
142
+
143
+ def check_for_api_error(result)
144
+ return result.body if result.kind_of?(Net::HTTPSuccess)
145
+
146
+ case result
147
+ when Net::HTTPRedirection
148
+ if result["location"] =~ /loginSearch/
149
+ raise UnknownSystemError.new("Account does not exist")
150
+ elsif result["location"] =~ /deactivated/
151
+ raise AccountDeactivatedError.new("Account is deactivated")
152
+ end
153
+ when Net::HTTPUnauthorized
154
+ raise AuthenticationError.new("Invalid API key.")
155
+ when Net::HTTPBadRequest
156
+ raise ApiAccessNotEnabledError.new("API not enabled.")
157
+ end
158
+
159
+ raise InternalError.new("Invalid HTTP code: #{result.class}")
160
+ end
161
+ end
162
+ end
@@ -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, :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,22 @@
1
+ module FreshBooks
2
+ class Invoice < FreshBooks::Base
3
+
4
+
5
+
6
+ define_schema do |s|
7
+ s.fixnum :invoice_id, :client_id, :po_number
8
+ s.fixnum :recurring_id, :read_only => true
9
+ s.float :amount, :discount
10
+ s.float :amount_outstanding, :paid, :read_only => true
11
+ s.date :date
12
+ s.date_time :updated, :read_only => true
13
+ s.array :lines
14
+ s.object :links, :read_only => true
15
+ s.string :number, :organization, :status, :notes, :terms, :first_name, :last_name, :currency_code
16
+ s.string :p_street1, :p_street2, :p_city, :p_state, :p_country, :p_code
17
+ s.string :return_uri
18
+ end
19
+
20
+ actions :list, :get, :create, :update, :delete, :send_by_email, :send_by_snail_mail
21
+ end
22
+ 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,11 @@
1
+ module FreshBooks
2
+ class Line < FreshBooks::Base
3
+
4
+ define_schema do |s|
5
+ s.string :name, :description, :tax1_name, :tax2_name
6
+ s.float :unit_cost, :tax1_percent, :tax2_percent
7
+ s.float :amount, :read_only => true
8
+ s.float :quantity
9
+ end
10
+ end
11
+ 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, :read_only => true
8
+ s.string :type, :notes
9
+ end
10
+
11
+ actions :list, :get, :create, :update, :delete
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,16 @@
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, :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
+ s.string :return_uri
12
+ end
13
+
14
+ actions :list, :get, :create, :update, :delete
15
+ end
16
+ 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 = ActiveSupport::OrderedHash.new
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