freshbooks.rb 3.0.13

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. data/History.txt +8 -0
  2. data/LICENSE +10 -0
  3. data/Manifest.txt +45 -0
  4. data/README +44 -0
  5. data/Rakefile +27 -0
  6. data/lib/freshbooks.rb +94 -0
  7. data/lib/freshbooks/base.rb +169 -0
  8. data/lib/freshbooks/category.rb +11 -0
  9. data/lib/freshbooks/client.rb +22 -0
  10. data/lib/freshbooks/connection.rb +153 -0
  11. data/lib/freshbooks/estimate.rb +15 -0
  12. data/lib/freshbooks/expense.rb +12 -0
  13. data/lib/freshbooks/invoice.rb +18 -0
  14. data/lib/freshbooks/item.rb +11 -0
  15. data/lib/freshbooks/line.rb +10 -0
  16. data/lib/freshbooks/links.rb +7 -0
  17. data/lib/freshbooks/list_proxy.rb +80 -0
  18. data/lib/freshbooks/payment.rb +13 -0
  19. data/lib/freshbooks/project.rb +12 -0
  20. data/lib/freshbooks/recurring.rb +15 -0
  21. data/lib/freshbooks/response.rb +25 -0
  22. data/lib/freshbooks/schema/definition.rb +20 -0
  23. data/lib/freshbooks/schema/mixin.rb +40 -0
  24. data/lib/freshbooks/staff.rb +13 -0
  25. data/lib/freshbooks/task.rb +12 -0
  26. data/lib/freshbooks/time_entry.rb +12 -0
  27. data/lib/freshbooks/xml_serializer.rb +17 -0
  28. data/lib/freshbooks/xml_serializer/serializers.rb +109 -0
  29. data/script/console +10 -0
  30. data/script/destroy +14 -0
  31. data/script/generate +14 -0
  32. data/test/fixtures/freshbooks_credentials.sample.yml +3 -0
  33. data/test/fixtures/invoice_create_response.xml +4 -0
  34. data/test/fixtures/invoice_get_response.xml +54 -0
  35. data/test/fixtures/invoice_list_response.xml +109 -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 +145 -0
  42. data/test/test_helper.rb +48 -0
  43. data/test/test_invoice.rb +125 -0
  44. data/test/test_list_proxy.rb +60 -0
  45. data/test/test_page.rb +50 -0
  46. metadata +148 -0
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env ruby
2
+ # File: script/console
3
+ irb = RUBY_PLATFORM =~ /(:?mswin|mingw)/ ? 'irb.bat' : 'irb'
4
+
5
+ libs = " -r irb/completion"
6
+ # Perhaps use a console_lib to store any extra methods I may want available in the cosole
7
+ # libs << " -r #{File.dirname(__FILE__) + '/../lib/console_lib/console_logger.rb'}"
8
+ libs << " -r #{File.dirname(__FILE__) + '/../lib/freshbooks.rb'}"
9
+ puts "Loading freshbooks.rb gem"
10
+ exec "#{irb} #{libs} --simple-prompt"
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+ APP_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..'))
3
+
4
+ begin
5
+ require 'rubigen'
6
+ rescue LoadError
7
+ require 'rubygems'
8
+ require 'rubigen'
9
+ end
10
+ require 'rubigen/scripts/destroy'
11
+
12
+ ARGV.shift if ['--help', '-h'].include?(ARGV[0])
13
+ RubiGen::Base.use_component_sources! [:rubygems, :newgem, :newgem_theme, :test_unit]
14
+ RubiGen::Scripts::Destroy.new.run(ARGV)
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+ APP_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..'))
3
+
4
+ begin
5
+ require 'rubigen'
6
+ rescue LoadError
7
+ require 'rubygems'
8
+ require 'rubigen'
9
+ end
10
+ require 'rubigen/scripts/generate'
11
+
12
+ ARGV.shift if ['--help', '-h'].include?(ARGV[0])
13
+ RubiGen::Base.use_component_sources! [:rubygems, :newgem, :newgem_theme, :test_unit]
14
+ RubiGen::Scripts::Generate.new.run(ARGV)
@@ -0,0 +1,3 @@
1
+ fresh_books_test_account:
2
+ account_url: <insert your account url>
3
+ api_key: <insert your api key>
@@ -0,0 +1,4 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <response status="ok">
3
+ <invoice_id>1</invoice_id>
4
+ </response>
@@ -0,0 +1,54 @@
1
+ <?xml version="1.0"?>
2
+ <response status="ok">
3
+ <invoice>
4
+ <invoice_id>1</invoice_id>
5
+ <client_id>1</client_id>
6
+ <number>number1</number>
7
+ <recurring_id>1</recurring_id>
8
+ <organization>Organization 1</organization>
9
+ <status>draft</status>
10
+ <amount>100.00</amount>
11
+ <amount_outstanding>50.00</amount_outstanding>
12
+ <date>2009-02-01</date>
13
+
14
+ <po_number>1</po_number>
15
+ <discount>1</discount>
16
+ <notes>notes1</notes>
17
+ <terms>terms1</terms>
18
+
19
+ <first_name>first_name1</first_name>
20
+ <last_name>last_name1</last_name>
21
+ <p_street1>p_street11</p_street1>
22
+ <p_street2>p_street21</p_street2>
23
+ <p_city>p_city1</p_city>
24
+ <p_state>p_state1</p_state>
25
+ <p_country>p_country1</p_country>
26
+ <p_code>p_code1</p_code>
27
+
28
+ <return_uri>return_uri1</return_uri>
29
+ <updated>2009-08-1 01:00:00</updated>
30
+
31
+ <links>
32
+ <client_view>client_view1</client_view>
33
+ <view>view1</view>
34
+ <edit>edit1</edit>
35
+ </links>
36
+
37
+ <lines>
38
+ <line>
39
+ <amount>1</amount>
40
+ <name>name1</name>
41
+ <description>description1</description>
42
+ <unit_cost>1</unit_cost>
43
+ <quantity>1</quantity>
44
+ <tax1_name>tax1_name1</tax1_name>
45
+ <tax2_name>tax2_name1</tax2_name>
46
+ <tax1_percent>1</tax1_percent>
47
+ <tax2_percent>1</tax2_percent>
48
+ </line>
49
+ </lines>
50
+
51
+ <url deprecated="true">https://sample.freshbooks.com/inv/12345-1-6d30b</url>
52
+ <auth_url deprecated="true">https://sample.freshbooks.com/inv/12345-1-6d30b-z</auth_url>
53
+ </invoice>
54
+ </response>
@@ -0,0 +1,109 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <response status="ok">
3
+ <invoices page="1" per_page="2" pages="2" total="3">
4
+ <invoice>
5
+ <invoice_id>1</invoice_id>
6
+ <number>number1</number>
7
+ <client_id>1</client_id>
8
+ <recurring_id>1</recurring_id>
9
+ <organization>Organization 1</organization>
10
+ <status>draft</status>
11
+ <amount>100.00</amount>
12
+ <amount_outstanding>50.00</amount_outstanding>
13
+ <date>2009-02-01</date>
14
+
15
+ <po_number>1</po_number>
16
+ <discount>1</discount>
17
+ <notes>notes1</notes>
18
+ <terms>terms1</terms>
19
+
20
+ <first_name>first_name1</first_name>
21
+ <last_name>last_name1</last_name>
22
+ <p_street1>p_street11</p_street1>
23
+ <p_street2>p_street21</p_street2>
24
+ <p_city>p_city1</p_city>
25
+ <p_state>p_state1</p_state>
26
+ <p_country>p_country1</p_country>
27
+ <p_code>p_code1</p_code>
28
+
29
+ <return_uri>return_uri1</return_uri>
30
+ <updated>2009-08-1 01:00:00</updated>
31
+
32
+ <links>
33
+ <client_view>client_view1</client_view>
34
+ <view>view1</view>
35
+ <edit>edit1</edit>
36
+ </links>
37
+
38
+ <lines>
39
+ <line>
40
+ <amount>1</amount>
41
+ <name>name1</name>
42
+ <description>description1</description>
43
+ <unit_cost>1</unit_cost>
44
+ <quantity>1</quantity>
45
+ <tax1_name>tax1_name1</tax1_name>
46
+ <tax2_name>tax2_name1</tax2_name>
47
+ <tax1_percent>1</tax1_percent>
48
+ <tax2_percent>1</tax2_percent>
49
+ </line>
50
+ </lines>
51
+
52
+ <url deprecated="true">https://sample.freshbooks.com/inv/12345-1-6d30b</url>
53
+ <auth_url deprecated="true">https://sample.freshbooks.com/inv/12345-1-6d30b-z</auth_url>
54
+
55
+ </invoice>
56
+ <invoice>
57
+ <invoice_id>2</invoice_id>
58
+ <number>number2</number>
59
+ <client_id>2</client_id>
60
+ <recurring_id>2</recurring_id>
61
+ <organization>Organization 2</organization>
62
+ <status>draft</status>
63
+ <amount>200.00</amount>
64
+ <amount_outstanding>100.00</amount_outstanding>
65
+ <date>2009-02-02</date>
66
+
67
+ <po_number>2</po_number>
68
+ <discount>2</discount>
69
+ <notes>notes2</notes>
70
+ <terms>terms2</terms>
71
+
72
+ <first_name>first_name2</first_name>
73
+ <last_name>last_name2</last_name>
74
+ <p_street1>p_street12</p_street1>
75
+ <p_street2>p_street22</p_street2>
76
+ <p_city>p_city2</p_city>
77
+ <p_state>p_state2</p_state>
78
+ <p_country>p_country2</p_country>
79
+ <p_code>p_code2</p_code>
80
+
81
+ <return_uri>return_uri2</return_uri>
82
+ <updated>2009-08-2 02:00:00</updated>
83
+
84
+ <links>
85
+ <client_view>client_view2</client_view>
86
+ <view>view2</view>
87
+ <edit>edit2</edit>
88
+ </links>
89
+
90
+ <lines>
91
+ <line>
92
+ <amount>1</amount>
93
+ <name>name1</name>
94
+ <description>description1</description>
95
+ <unit_cost>1</unit_cost>
96
+ <quantity>1</quantity>
97
+ <tax1_name>tax1_name1</tax1_name>
98
+ <tax2_name>tax2_name1</tax2_name>
99
+ <tax1_percent>1</tax1_percent>
100
+ <tax2_percent>1</tax2_percent>
101
+ </line>
102
+ </lines>
103
+
104
+ <url deprecated="true">https://sample.freshbooks.com/inv/12345-1-6d30b</url>
105
+ <auth_url deprecated="true">https://sample.freshbooks.com/inv/12345-1-6d30b-z</auth_url>
106
+
107
+ </invoice>
108
+ </invoices>
109
+ </response>
@@ -0,0 +1,2 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <response status="ok"/>
@@ -0,0 +1,13 @@
1
+ require 'freshbooks/connection'
2
+
3
+ class MockConnection < FreshBooks::Connection
4
+ def initialize(response_body)
5
+ @response_body = response_body
6
+ end
7
+
8
+ protected
9
+
10
+ def post(request_body)
11
+ @response_body
12
+ end
13
+ end
@@ -0,0 +1,36 @@
1
+ require File.dirname(__FILE__) + '/../test_helper.rb'
2
+
3
+ module Schema
4
+ class TestDefinition < Test::Unit::TestCase
5
+ def setup
6
+ @definition = FreshBooks::Schema::Definition.new
7
+ end
8
+
9
+ def test_method_missing
10
+ # Empty
11
+ assert_equal 0, @definition.members.size
12
+
13
+ # One type
14
+ @definition.string :name
15
+ assert_equal 1, @definition.members.size
16
+ assert_equal({ :type => :string, :read_only => false }, @definition.members["name"])
17
+
18
+ # Multiple attributes
19
+ @definition.fixnum :version, :po_number
20
+ assert_equal 3, @definition.members.size
21
+ assert_equal({ :type => :fixnum, :read_only => false }, @definition.members["version"])
22
+ assert_equal({ :type => :fixnum, :read_only => false }, @definition.members["po_number"])
23
+
24
+ # Multiple times
25
+ @definition.fixnum :lock_number
26
+ assert_equal 4, @definition.members.size
27
+ assert_equal({ :type => :fixnum, :read_only => false }, @definition.members["lock_number"])
28
+ end
29
+
30
+ def test_method_missing_extra_options
31
+ @definition.fixnum :version, :po_number, :read_only => true
32
+ assert_equal({ :type => :fixnum, :read_only => true }, @definition.members["version"])
33
+ assert_equal({ :type => :fixnum, :read_only => true }, @definition.members["po_number"])
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,39 @@
1
+ require File.dirname(__FILE__) + '/../test_helper.rb'
2
+
3
+ module Schema
4
+ class TestMixin < Test::Unit::TestCase
5
+ def test_define_schema__unique_definition_per_class
6
+ assert MyItem.schema_definition.members.include?("name")
7
+ assert !MyItem.schema_definition.members.include?("name2")
8
+
9
+ assert MyItem2.schema_definition.members.include?("name2")
10
+ assert !MyItem2.schema_definition.members.include?("name")
11
+ end
12
+
13
+ def test_define_schema__creates_attr_accessors
14
+ assert MyItem.public_method_defined?("name")
15
+ assert MyItem.public_method_defined?("name=")
16
+
17
+ assert MyItem2.public_method_defined?("name2")
18
+ assert MyItem2.public_method_defined?("name2=")
19
+ end
20
+
21
+ def test_define_schema__creates_read_only_attr_accessors
22
+ assert MyItem.public_method_defined?("read_only_name")
23
+ assert MyItem.protected_method_defined?("read_only_name=")
24
+ end
25
+ end
26
+
27
+ class MyItem < FreshBooks::Base
28
+ define_schema do |s|
29
+ s.string :name
30
+ s.string :read_only_name, :read_only => true
31
+ end
32
+ end
33
+
34
+ class MyItem2 < FreshBooks::Base
35
+ define_schema do |s|
36
+ s.string :name2
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,97 @@
1
+ require File.dirname(__FILE__) + '/test_helper.rb'
2
+
3
+ class TestBase < Test::Unit::TestCase
4
+ def test_establish_connection
5
+ assert_nil FreshBooks::Base.connection
6
+
7
+ FreshBooks::Base.establish_connection("company.freshbooks.com", "auth_token")
8
+ connection = FreshBooks::Base.connection
9
+ assert_not_nil connection
10
+ assert_equal "company.freshbooks.com", connection.account_url
11
+ assert_equal "auth_token", connection.auth_token
12
+
13
+ FreshBooks::Base.establish_connection("company2.freshbooks.com", "auth_token2")
14
+ connection = FreshBooks::Base.connection
15
+ assert_not_nil connection
16
+ assert_equal "company2.freshbooks.com", connection.account_url
17
+ assert_equal "auth_token2", connection.auth_token
18
+ end
19
+
20
+ def test_new_from_xml_to_xml__round_trip
21
+ xml = <<-EOS
22
+ <my_item>
23
+ <name>name1</name>
24
+ <amount>4.5</amount>
25
+ <read_only_number>5</read_only_number>
26
+ <number>6</number>
27
+ <visible>1</visible>
28
+ <date>2008-02-01</date>
29
+ <created_at>2008-10-22 13:57:00</created_at>
30
+ <my_address>
31
+ <street1>street1</street1>
32
+ </my_address>
33
+ <my_lines>
34
+ <my_line>
35
+ <description>description</description>
36
+ </my_line>
37
+ </my_lines>
38
+ </my_item>
39
+ EOS
40
+ doc = REXML::Document.new(xml)
41
+
42
+ item = FreshBooks::MyItem.new_from_xml(doc.root)
43
+ assert_equal "name1", item.name
44
+ assert_equal 5, item.read_only_number
45
+ assert_equal 6, item.number
46
+ assert_equal 4.5, item.amount
47
+ assert_equal true, item.visible
48
+ assert_equal Date.new(2008, 2, 1), item.date
49
+ assert_equal DateTime.parse("2008-10-22 13:57:00 -04:00"), item.created_at
50
+ assert_equal "street1", item.my_address.street1
51
+ assert_equal 1, item.my_lines.size
52
+ assert_equal "description", item.my_lines.first.description
53
+
54
+ # If someone knows a good way to compare xml docs where ordering doesn't matter
55
+ # let me know. This can be refactored and improved greatly.
56
+ xml_out = item.to_xml.to_s.strip
57
+ assert_equal "<my_item>", xml_out.first(9)
58
+ assert xml_out.include?("<name>name1</name>")
59
+ assert xml_out.include?("<amount>4.5</amount>")
60
+ assert xml_out.include?("<visible>1</visible>")
61
+ assert !xml_out.include?("<read_only_number>5</read_only_number>") # this is read only
62
+ assert xml_out.include?("<number>6</number>")
63
+ assert xml_out.include?("<date>2008-02-01</date>")
64
+ assert xml_out.include?("<created_at>2008-10-22 13:57:00</created_at>")
65
+ assert xml_out.include?("<my_address><street1>street1</street1></my_address>")
66
+ assert xml_out.include?("<my_lines><my_line><description>description</description></my_line></my_lines>")
67
+ end
68
+
69
+ end
70
+
71
+ module FreshBooks
72
+ class MyItem < FreshBooks::Base
73
+ define_schema do |s|
74
+ s.string :name
75
+ s.fixnum :read_only_number, :read_only => true
76
+ s.fixnum :number
77
+ s.float :amount
78
+ s.boolean :visible
79
+ s.date :date
80
+ s.date_time :created_at
81
+ s.object :my_address
82
+ s.array :my_lines
83
+ end
84
+ end
85
+
86
+ class MyAddress < FreshBooks::Base
87
+ define_schema do |s|
88
+ s.string :street1
89
+ end
90
+ end
91
+
92
+ class MyLine < FreshBooks::Base
93
+ define_schema do |s|
94
+ s.string :description
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,145 @@
1
+ require File.dirname(__FILE__) + '/test_helper.rb'
2
+
3
+ class TestConnection < Test::Unit::TestCase
4
+ def setup
5
+ @connection = FreshBooks::Connection.new("company.freshbooks.com", "auth_token")
6
+ end
7
+
8
+ def test_connection_accessors
9
+ assert_equal "company.freshbooks.com", @connection.account_url
10
+ assert_equal "auth_token", @connection.auth_token
11
+ end
12
+
13
+ def test_connection_request_headers
14
+ request_headers = { "key" => "value" }
15
+ connection = FreshBooks::Connection.new("company.freshbooks.com", "auth_token", request_headers)
16
+ assert_equal request_headers, connection.request_headers
17
+ end
18
+
19
+
20
+ def test_create_request__array_of_elements
21
+ request = @connection.send(:create_request, 'mymethod', [['element1', 'value1'], [:element2, :value2]])
22
+ assert_equal "<?xml version='1.0' encoding='UTF-8'?><request method='mymethod'><element1>value1</element1><element2>value2</element2></request>", request
23
+ end
24
+
25
+ def test_create_request__base_object_element
26
+ invoice = FreshBooks::Invoice.new
27
+ invoice.expects(:to_xml).with().returns("<invoice><number>23</number></invoice>")
28
+
29
+ request = @connection.send(:create_request, 'mymethod', 'invoice' => invoice)
30
+ assert_equal "<?xml version='1.0' encoding='UTF-8'?><request method='mymethod'><invoice><number>23</number></invoice></request>", request
31
+ end
32
+
33
+ def test_check_for_api_error__success
34
+ body = "body xml"
35
+ response = Net::HTTPSuccess.new("1.1", "200", "message")
36
+ response.expects(:body).with().returns(body)
37
+ assert_equal body, @connection.send(:check_for_api_error, response)
38
+ end
39
+
40
+ def test_check_for_api_error__unknown_system
41
+ response = Net::HTTPMovedPermanently.new("1.1", "301", "message")
42
+ response.stubs(:[]).with("location").returns("loginSearch")
43
+ assert_raise(FreshBooks::UnknownSystemError) do
44
+ @connection.send(:check_for_api_error, response)
45
+ end
46
+ end
47
+
48
+ def test_check_for_api_error__deactivated
49
+ response = Net::HTTPMovedPermanently.new("1.1", "301", "message")
50
+ response.stubs(:[]).with("location").returns("deactivated")
51
+ assert_raise(FreshBooks::AccountDeactivatedError) do
52
+ @connection.send(:check_for_api_error, response)
53
+ end
54
+ end
55
+
56
+ def test_check_for_api_error__unauthorized
57
+ response = Net::HTTPUnauthorized.new("1.1", "401", "message")
58
+ assert_raise(FreshBooks::AuthenticationError) do
59
+ @connection.send(:check_for_api_error, response)
60
+ end
61
+ end
62
+
63
+ def test_check_for_api_error__bad_request
64
+ response = Net::HTTPBadRequest.new("1.1", "401", "message")
65
+ assert_raise(FreshBooks::ApiAccessNotEnabledError) do
66
+ @connection.send(:check_for_api_error, response)
67
+ end
68
+ end
69
+
70
+ def test_check_for_api_error__internal_error
71
+ response = Net::HTTPBadGateway.new("1.1", "502", "message")
72
+ assert_raise(FreshBooks::InternalError) do
73
+ @connection.send(:check_for_api_error, response)
74
+ end
75
+
76
+ response = Net::HTTPMovedPermanently.new("1.1", "301", "message")
77
+ response.stubs(:[]).with("location").returns("somePage")
78
+ assert_raise(FreshBooks::InternalError) do
79
+ @connection.send(:check_for_api_error, response)
80
+ end
81
+ end
82
+
83
+ def test_close_is_only_called_once_in_ntexted_start_sessions
84
+ @connection.expects(:obtain_connection)
85
+ @connection.expects(:close)
86
+
87
+ @connection.start_session { @connection.start_session { } }
88
+ end
89
+
90
+ def test_reconnect
91
+ connection = stub()
92
+
93
+ @connection.expects(:close).with()
94
+ @connection.expects(:obtain_connection).with(true).returns(connection)
95
+
96
+ assert_equal connection, @connection.send(:reconnect)
97
+ end
98
+
99
+ def test_post_request_successfull_request
100
+ request = "<request></request>"
101
+ response = "<response></response>"
102
+
103
+ http_connection = stub()
104
+ http_connection.expects(:request).with(request).returns(response)
105
+ @connection.expects(:start_session).with().yields(http_connection)
106
+
107
+ assert_equal response, @connection.send(:post_request, request)
108
+ end
109
+
110
+ def test_post_request_eof_error_retry
111
+ request = "<request></request>"
112
+ response = "<response></response>"
113
+ eof_error = EOFError.new("End of file error")
114
+
115
+ bad_http_connection = stub()
116
+ bad_http_connection.expects(:request).with(request).raises(eof_error)
117
+
118
+ new_http_connection = stub()
119
+ new_http_connection.expects(:request).with(request).returns(response)
120
+
121
+ @connection.expects(:start_session).with().yields(bad_http_connection)
122
+ @connection.expects(:reconnect).with().returns(new_http_connection)
123
+
124
+ assert_equal response, @connection.send(:post_request, request)
125
+ end
126
+
127
+ def test_post_request_eof_error_retry_only_retry_once
128
+ request = "<request></request>"
129
+ response = "<response></response>"
130
+ eof_error = EOFError.new("End of file error")
131
+
132
+ bad_http_connection = stub()
133
+ bad_http_connection.expects(:request).with(request).raises(eof_error)
134
+
135
+ new_http_connection = stub()
136
+ new_http_connection.expects(:request).with(request).raises(eof_error)
137
+
138
+ @connection.expects(:start_session).with().yields(bad_http_connection)
139
+ @connection.expects(:reconnect).with().returns(new_http_connection)
140
+
141
+ assert_raises(EOFError, eof_error.message) do
142
+ @connection.send(:post_request, request)
143
+ end
144
+ end
145
+ end