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.
- data/.gitignore +5 -0
- data/History.txt +8 -0
- data/LICENSE +10 -0
- data/Manifest.txt +44 -0
- data/README +65 -0
- data/Rakefile +19 -0
- data/lib/freshbooks.rb +94 -0
- data/lib/freshbooks/base.rb +168 -0
- data/lib/freshbooks/category.rb +11 -0
- data/lib/freshbooks/client.rb +20 -0
- data/lib/freshbooks/connection.rb +112 -0
- data/lib/freshbooks/estimate.rb +23 -0
- data/lib/freshbooks/expense.rb +12 -0
- data/lib/freshbooks/invoice.rb +25 -0
- data/lib/freshbooks/item.rb +11 -0
- data/lib/freshbooks/line.rb +10 -0
- data/lib/freshbooks/links.rb +7 -0
- data/lib/freshbooks/list_proxy.rb +70 -0
- data/lib/freshbooks/payment.rb +12 -0
- data/lib/freshbooks/project.rb +12 -0
- data/lib/freshbooks/recurring.rb +15 -0
- data/lib/freshbooks/response.rb +27 -0
- data/lib/freshbooks/schema/definition.rb +20 -0
- data/lib/freshbooks/schema/mixin.rb +40 -0
- data/lib/freshbooks/staff.rb +13 -0
- data/lib/freshbooks/task.rb +12 -0
- data/lib/freshbooks/time_entry.rb +12 -0
- data/lib/freshbooks/xml_serializer.rb +17 -0
- data/lib/freshbooks/xml_serializer/serializers.rb +107 -0
- data/script/console +10 -0
- data/script/destroy +14 -0
- data/script/generate +14 -0
- data/test/fixtures/invoice_create_response.xml +4 -0
- data/test/fixtures/invoice_get_response.xml +51 -0
- data/test/fixtures/invoice_list_response.xml +27 -0
- data/test/fixtures/success_response.xml +2 -0
- data/test/mock_connection.rb +13 -0
- data/test/schema/test_definition.rb +36 -0
- data/test/schema/test_mixin.rb +39 -0
- data/test/test_base.rb +97 -0
- data/test/test_connection.rb +83 -0
- data/test/test_helper.rb +32 -0
- data/test/test_invoice.rb +122 -0
- data/test/test_list_proxy.rb +41 -0
- data/test/test_page.rb +50 -0
- 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,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 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
|