jpablobr-freshbooks 0.1.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.
Files changed (46) hide show
  1. data/.gitignore +5 -0
  2. data/History.txt +8 -0
  3. data/LICENSE +10 -0
  4. data/Manifest.txt +44 -0
  5. data/README +65 -0
  6. data/Rakefile +19 -0
  7. data/lib/freshbooks.rb +94 -0
  8. data/lib/freshbooks/base.rb +168 -0
  9. data/lib/freshbooks/category.rb +11 -0
  10. data/lib/freshbooks/client.rb +20 -0
  11. data/lib/freshbooks/connection.rb +112 -0
  12. data/lib/freshbooks/estimate.rb +23 -0
  13. data/lib/freshbooks/expense.rb +12 -0
  14. data/lib/freshbooks/invoice.rb +25 -0
  15. data/lib/freshbooks/item.rb +11 -0
  16. data/lib/freshbooks/line.rb +10 -0
  17. data/lib/freshbooks/links.rb +7 -0
  18. data/lib/freshbooks/list_proxy.rb +70 -0
  19. data/lib/freshbooks/payment.rb +12 -0
  20. data/lib/freshbooks/project.rb +12 -0
  21. data/lib/freshbooks/recurring.rb +15 -0
  22. data/lib/freshbooks/response.rb +27 -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 +107 -0
  30. data/script/console +10 -0
  31. data/script/destroy +14 -0
  32. data/script/generate +14 -0
  33. data/test/fixtures/invoice_create_response.xml +4 -0
  34. data/test/fixtures/invoice_get_response.xml +51 -0
  35. data/test/fixtures/invoice_list_response.xml +27 -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 +83 -0
  42. data/test/test_helper.rb +32 -0
  43. data/test/test_invoice.rb +122 -0
  44. data/test/test_list_proxy.rb +41 -0
  45. data/test/test_page.rb +50 -0
  46. metadata +115 -0
@@ -0,0 +1,112 @@
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, :request_headers
8
+
9
+ @@logger = Logger.new(STDOUT)
10
+ def logger
11
+ @@logger
12
+ end
13
+
14
+ def self.log_level=(level)
15
+ @@logger.level = level
16
+ end
17
+ self.log_level = Logger::WARN
18
+
19
+ def initialize(account_url, auth_token, request_headers = {})
20
+ raise InvalidAccountUrlError.new unless account_url =~ /^[0-9a-zA-Z\-_]+\.freshbooks\.com$/
21
+
22
+ @account_url = account_url
23
+ @auth_token = auth_token
24
+ @request_headers = request_headers
25
+ end
26
+
27
+ def call_api(method, elements = [])
28
+ # puts "#{self.class}#call_api: Creating a request with method: #{method} and elements: #{elements.inspect}"
29
+ request = create_request(method, elements)
30
+ # puts "#{self.class}#call_api: Sending request \"#{request}\""
31
+ self.logger.debug request
32
+ result = post(request)
33
+ # puts "#{self.class}#call_api: Received: \"#{result}\""
34
+ self.logger.debug result
35
+ Response.new(result)
36
+ end
37
+
38
+ protected
39
+
40
+ def create_request(method, elements = [])
41
+ doc = REXML::Document.new '<?xml version="1.0" encoding="UTF-8"?>'
42
+ request = doc.add_element('request')
43
+ request.attributes['method'] = method
44
+
45
+ elements.each do |element|
46
+ # puts "Element: " + element.class.inspect
47
+ # puts " - " + element.inspect
48
+ if element.kind_of?(Hash)
49
+ element = element.to_a
50
+ end
51
+ key = element.first
52
+ value = element.last
53
+
54
+ if value.kind_of?(Base)
55
+ #puts "We thinks this is a kind of base. This is the value to_xml: " + value.to_xml
56
+ request << REXML::Document.new(value.to_xml)
57
+ # request.add_text(REXML::Text.new( value.to_xml, false, nil, false ))
58
+ else
59
+ #puts "We ain't thinkin this is a kind of base. This is the key to_xml: " + key.to_s
60
+ #puts "This is the value to_xml: " + value.to_s
61
+ request.add_element(REXML::Element.new(key.to_s)).text = value.to_s
62
+ end
63
+ end
64
+
65
+ doc.to_s
66
+ end
67
+
68
+ def post(request_body)
69
+ connection = Net::HTTP.new(@account_url, 443)
70
+ connection.use_ssl = true
71
+ connection.verify_mode = OpenSSL::SSL::VERIFY_NONE
72
+
73
+ request = Net::HTTP::Post.new(FreshBooks::SERVICE_URL)
74
+ request.basic_auth @auth_token, 'X'
75
+ request.body = request_body
76
+ request.content_type = 'application/xml'
77
+ @request_headers.each_pair do |name, value|
78
+ request[name.to_s] = value
79
+ end
80
+
81
+ result = connection.start { |http| http.request(request) }
82
+
83
+ if logger.debug?
84
+ logger.debug "Request:"
85
+ logger.debug request_body
86
+ logger.debug "Response:"
87
+ logger.debug result.body
88
+ end
89
+
90
+ check_for_api_error(result)
91
+ end
92
+
93
+ def check_for_api_error(result)
94
+ return result.body if result.kind_of?(Net::HTTPSuccess)
95
+
96
+ case result
97
+ when Net::HTTPRedirection
98
+ if result["location"] =~ /loginSearch/
99
+ raise UnknownSystemError.new("Account does not exist")
100
+ elsif result["location"] =~ /deactivated/
101
+ raise AccountDeactivatedError.new("Account is deactivated")
102
+ end
103
+ when Net::HTTPUnauthorized
104
+ raise AuthenticationError.new("Invalid API key.")
105
+ when Net::HTTPBadRequest
106
+ raise ApiAccessNotEnabledError.new("API not enabled.")
107
+ end
108
+
109
+ raise InternalError.new("Invalid HTTP code: #{result.class}")
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,23 @@
1
+ module FreshBooks
2
+ class Estimate < FreshBooks::Base
3
+ define_schema do |s|
4
+ s.string :estimate_id, :status, :notes, :terms, :first_name
5
+ s.string :number, :last_name, :organization, :p_street1, :p_street2, :p_city
6
+ s.string :p_state, :p_country, :p_code
7
+ s.date :date
8
+ s.fixnum :client_id, :po_number
9
+ s.float :discount, :amount
10
+ s.object :links, :read_only => true
11
+ s.array :lines
12
+ end
13
+ def name
14
+ "#{self.first_name} #{self.last_name}"
15
+ end
16
+ def email
17
+ client = Client.get(self.client_id)
18
+ client.email
19
+ end
20
+
21
+ actions :list, :get, :create, :update, :delete, :send_by_email
22
+ end
23
+ 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, :status, :tax1_name, :tax2_name
8
+ end
9
+
10
+ actions :list, :get, :create, :update, :delete
11
+ end
12
+ end
@@ -0,0 +1,25 @@
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, :discount
7
+ s.float :amount_outstanding, :read_only => true # Don't send in update call
8
+ s.float :paid, :read_only => true # Don't send in update call
9
+ s.date :date
10
+ s.array :lines
11
+ s.object :links, :read_only => true
12
+ s.string :number, :organization, :status, :notes, :terms, :first_name, :last_name
13
+ s.string :p_street1, :p_street2, :p_city, :p_state, :p_country, :p_code, :currency_code, :return_uri
14
+ end
15
+ def name
16
+ "#{self.first_name} #{self.last_name}"
17
+ end
18
+ def email
19
+ client = Client.get(self.client_id)
20
+ client.email
21
+ end
22
+
23
+ actions :list, :get, :create, :update, :delete, :send_by_email, :send_by_snail_mail
24
+ end
25
+ 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.float :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,70 @@
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)
43
+ @page = page.to_i
44
+ @per_page = per_page.to_i
45
+ @total = total.to_i
46
+ end
47
+
48
+ # Get the page number that this element is on given the number of elements per page
49
+ def page_number(position)
50
+ (position / per_page) + 1
51
+ end
52
+
53
+ # Get the position number of this element based relative to the current page
54
+ def position_number(position)
55
+ position - ((page - 1) * per_page)
56
+ end
57
+
58
+ def pages
59
+ pages = total / per_page
60
+ if (total % per_page) != 0
61
+ pages += 1
62
+ end
63
+ pages
64
+ end
65
+
66
+ def next_page?
67
+ page < pages
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,12 @@
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.string :type, :notes
8
+ end
9
+
10
+ actions :list, :get, :create, :update
11
+ end
12
+ 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,27 @@
1
+ module FreshBooks
2
+ class Response
3
+ attr_accessor :doc
4
+ attr_reader :raw_response
5
+
6
+ def initialize(xml_raw)
7
+ @raw_response = xml_raw
8
+ @doc = REXML::Document.new(xml_raw)
9
+ end
10
+
11
+ def elements
12
+ @doc.root.elements
13
+ end
14
+
15
+ def success?
16
+ @doc.root.attributes['status'] == 'ok'
17
+ end
18
+
19
+ def fail?
20
+ !success?
21
+ end
22
+
23
+ def error_msg
24
+ return @doc.root.elements['error'].text
25
+ end
26
+ end
27
+ 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
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