jpablobr-freshbooks 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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