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.
- data/History.txt +8 -0
- data/LICENSE +10 -0
- data/Manifest.txt +45 -0
- data/README +44 -0
- data/Rakefile +27 -0
- data/lib/freshbooks.rb +94 -0
- data/lib/freshbooks/base.rb +169 -0
- data/lib/freshbooks/category.rb +11 -0
- data/lib/freshbooks/client.rb +22 -0
- data/lib/freshbooks/connection.rb +153 -0
- data/lib/freshbooks/estimate.rb +15 -0
- data/lib/freshbooks/expense.rb +12 -0
- data/lib/freshbooks/invoice.rb +18 -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 +80 -0
- data/lib/freshbooks/payment.rb +13 -0
- data/lib/freshbooks/project.rb +12 -0
- data/lib/freshbooks/recurring.rb +15 -0
- data/lib/freshbooks/response.rb +25 -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 +109 -0
- data/script/console +10 -0
- data/script/destroy +14 -0
- data/script/generate +14 -0
- data/test/fixtures/freshbooks_credentials.sample.yml +3 -0
- data/test/fixtures/invoice_create_response.xml +4 -0
- data/test/fixtures/invoice_get_response.xml +54 -0
- data/test/fixtures/invoice_list_response.xml +109 -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 +145 -0
- data/test/test_helper.rb +48 -0
- data/test/test_invoice.rb +125 -0
- data/test/test_list_proxy.rb +60 -0
- data/test/test_page.rb +50 -0
- 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,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 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
|