harvested2 5.0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (106) hide show
  1. checksums.yaml +7 -0
  2. data/.document +3 -0
  3. data/.gitignore +35 -0
  4. data/.rspec +1 -0
  5. data/.rubocop.yml +34 -0
  6. data/.ruby-version +1 -0
  7. data/.travis.yml +12 -0
  8. data/Gemfile +20 -0
  9. data/HISTORY.md +118 -0
  10. data/MIT-LICENSE +21 -0
  11. data/README.md +66 -0
  12. data/Rakefile +24 -0
  13. data/harvested2.gemspec +30 -0
  14. data/lib/ext/array.rb +52 -0
  15. data/lib/ext/date.rb +9 -0
  16. data/lib/ext/hash.rb +17 -0
  17. data/lib/ext/time.rb +5 -0
  18. data/lib/harvest/account.rb +13 -0
  19. data/lib/harvest/api/account.rb +25 -0
  20. data/lib/harvest/api/base.rb +72 -0
  21. data/lib/harvest/api/clients.rb +10 -0
  22. data/lib/harvest/api/company.rb +12 -0
  23. data/lib/harvest/api/contacts.rb +9 -0
  24. data/lib/harvest/api/expense_categories.rb +9 -0
  25. data/lib/harvest/api/expenses.rb +26 -0
  26. data/lib/harvest/api/invoice_categories.rb +9 -0
  27. data/lib/harvest/api/invoice_messages.rb +86 -0
  28. data/lib/harvest/api/invoice_payments.rb +41 -0
  29. data/lib/harvest/api/invoices.rb +9 -0
  30. data/lib/harvest/api/projects.rb +9 -0
  31. data/lib/harvest/api/task_assignments.rb +75 -0
  32. data/lib/harvest/api/tasks.rb +9 -0
  33. data/lib/harvest/api/time_entry.rb +19 -0
  34. data/lib/harvest/api/user_assignments.rb +75 -0
  35. data/lib/harvest/api/users.rb +10 -0
  36. data/lib/harvest/base.rb +333 -0
  37. data/lib/harvest/behavior/activatable.rb +31 -0
  38. data/lib/harvest/behavior/crud.rb +80 -0
  39. data/lib/harvest/client.rb +23 -0
  40. data/lib/harvest/company.rb +8 -0
  41. data/lib/harvest/contact.rb +20 -0
  42. data/lib/harvest/credentials.rb +34 -0
  43. data/lib/harvest/errors.rb +27 -0
  44. data/lib/harvest/expense.rb +54 -0
  45. data/lib/harvest/expense_category.rb +10 -0
  46. data/lib/harvest/hardy_client.rb +80 -0
  47. data/lib/harvest/invoice.rb +75 -0
  48. data/lib/harvest/invoice_category.rb +8 -0
  49. data/lib/harvest/invoice_message.rb +8 -0
  50. data/lib/harvest/invoice_payment.rb +8 -0
  51. data/lib/harvest/line_item.rb +21 -0
  52. data/lib/harvest/model.rb +133 -0
  53. data/lib/harvest/project.rb +41 -0
  54. data/lib/harvest/receipt.rb +12 -0
  55. data/lib/harvest/task.rb +21 -0
  56. data/lib/harvest/task_assignment.rb +27 -0
  57. data/lib/harvest/time_entry.rb +57 -0
  58. data/lib/harvest/timezones.rb +130 -0
  59. data/lib/harvest/user.rb +58 -0
  60. data/lib/harvest/user_assignment.rb +27 -0
  61. data/lib/harvest/version.rb +3 -0
  62. data/lib/harvested2.rb +96 -0
  63. data/spec/factories/client.rb +14 -0
  64. data/spec/factories/contact.rb +8 -0
  65. data/spec/factories/expense.rb +10 -0
  66. data/spec/factories/expenses_category.rb +7 -0
  67. data/spec/factories/invoice.rb +25 -0
  68. data/spec/factories/invoice_category.rb +5 -0
  69. data/spec/factories/invoice_message.rb +9 -0
  70. data/spec/factories/invoice_payment.rb +7 -0
  71. data/spec/factories/line_item.rb +9 -0
  72. data/spec/factories/project.rb +15 -0
  73. data/spec/factories/task.rb +8 -0
  74. data/spec/factories/task_assignment.rb +8 -0
  75. data/spec/factories/time_entry.rb +13 -0
  76. data/spec/factories/user.rb +19 -0
  77. data/spec/factories/user_assigment.rb +7 -0
  78. data/spec/functional/clients_spec.rb +105 -0
  79. data/spec/functional/errors_spec.rb +42 -0
  80. data/spec/functional/expenses_spec.rb +97 -0
  81. data/spec/functional/invoice_messages_spec.rb +48 -0
  82. data/spec/functional/invoice_payments_spec.rb +51 -0
  83. data/spec/functional/invoice_spec.rb +138 -0
  84. data/spec/functional/project_spec.rb +76 -0
  85. data/spec/functional/tasks_spec.rb +119 -0
  86. data/spec/functional/time_entries_spec.rb +87 -0
  87. data/spec/functional/users_spec.rb +72 -0
  88. data/spec/harvest/base_spec.rb +10 -0
  89. data/spec/harvest/basic_auth_credentials_spec.rb +12 -0
  90. data/spec/harvest/expense_category_spec.rb +5 -0
  91. data/spec/harvest/expense_spec.rb +18 -0
  92. data/spec/harvest/invoice_message_spec.rb +5 -0
  93. data/spec/harvest/invoice_payment_spec.rb +5 -0
  94. data/spec/harvest/invoice_spec.rb +5 -0
  95. data/spec/harvest/oauth_credentials_spec.rb +11 -0
  96. data/spec/harvest/project_spec.rb +5 -0
  97. data/spec/harvest/task_assignment_spec.rb +5 -0
  98. data/spec/harvest/task_spec.rb +5 -0
  99. data/spec/harvest/time_entry_spec.rb +23 -0
  100. data/spec/harvest/user_assignment_spec.rb +5 -0
  101. data/spec/harvest/user_spec.rb +34 -0
  102. data/spec/spec_helper.rb +22 -0
  103. data/spec/support/factory_bot.rb +5 -0
  104. data/spec/support/harvested_helpers.rb +28 -0
  105. data/spec/support/json_examples.rb +9 -0
  106. metadata +238 -0
@@ -0,0 +1,17 @@
1
+ # Shamelessly ripped from https://github.com/rails/rails/blob/master/activesupport/lib/active_support/core_ext/hash/keys.rb
2
+
3
+ unless Hash.respond_to?(:stringify_keys)
4
+ class Hash
5
+ # Return a new hash with all keys converted to strings.
6
+ def stringify_keys
7
+ dup.stringify_keys!
8
+ end
9
+
10
+ def stringify_keys!
11
+ keys.each do |key|
12
+ self[key.to_s] = delete(key)
13
+ end
14
+ self
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,5 @@
1
+ unless Time.respond_to?(:to_time)
2
+ class Time
3
+ def to_time; self; end
4
+ end
5
+ end
@@ -0,0 +1,13 @@
1
+ module Harvest
2
+ # The model that contains information about a client
3
+ #
4
+ # == Fields
5
+ # [+user+] user attributes
6
+ # [+accounts+] accounts attributes
7
+ class Account < Hashie::Mash
8
+ include Harvest::Model
9
+
10
+ skip_json_root true
11
+ api_path '/accounts'
12
+ end
13
+ end
@@ -0,0 +1,25 @@
1
+ module Harvest
2
+ module API
3
+
4
+ # API Methods to contain all account actions
5
+ class Account < Base
6
+
7
+ # Returns the current rate limit information
8
+ # @return [Harvest::RateLimitStatus]
9
+ def rate_limit_status
10
+ response = request(:get, credentials, '/account/rate_limit_status')
11
+ Harvest::RateLimitStatus.parse(response.body).first
12
+ end
13
+
14
+ # Returns the current logged in user
15
+ # @return [Harvest::User]
16
+ def who_am_i
17
+ response = request(:get, credentials, '/account/who_am_i')
18
+ parsed = JSON.parse(response.body)
19
+ Harvest::User.parse(parsed).first.tap do |user|
20
+ user.company = parsed["company"]
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,72 @@
1
+ module Harvest
2
+ module API
3
+ class Base
4
+ attr_reader :credentials
5
+
6
+ def initialize(credentials)
7
+ @credentials = credentials
8
+ end
9
+
10
+ class << self
11
+ def api_model(klass)
12
+ class_eval <<-END
13
+ def api_model
14
+ #{klass}
15
+ end
16
+ END
17
+ end
18
+ end
19
+
20
+ protected
21
+
22
+ def request(method, credentials, path, options = {})
23
+ params = {
24
+ path: path,
25
+ options: options,
26
+ method: method
27
+ }
28
+
29
+ httparty_options = {
30
+ query: options[:query],
31
+ body: options[:body],
32
+ format: :plain,
33
+ headers: {
34
+ "Accept" => "application/json",
35
+ "Content-Type" => "application/json; charset=utf-8",
36
+ "User-Agent" => "Harvested/#{Harvest::VERSION}"
37
+ }.update(options[:headers] || {})
38
+ }
39
+
40
+ credentials.set_authentication(httparty_options)
41
+ response = HTTParty.send(method, "#{credentials.host}#{path}",
42
+ httparty_options)
43
+
44
+ params[:response] = response.inspect.to_s
45
+
46
+ case response.code
47
+ when 200..201
48
+ response
49
+ when 400
50
+ raise Harvest::BadRequest.new(response, params)
51
+ when 401
52
+ raise Harvest::AuthenticationFailed.new(response, params)
53
+ when 404
54
+ raise Harvest::NotFound.new(response, params,
55
+ 'Do you have sufficient privileges?')
56
+ when 500
57
+ raise Harvest::ServerError.new(response, params)
58
+ when 502
59
+ raise Harvest::Unavailable.new(response, params)
60
+ when 503
61
+ raise Harvest::RateLimited.new(response, params)
62
+ else
63
+ raise Harvest::InformHarvest.new(response, params)
64
+ end
65
+ end
66
+
67
+ def to_json(json)
68
+ parsed = String === json ? JSON.parse(json) : json
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,10 @@
1
+ module Harvest
2
+ module API
3
+ class Clients < Base
4
+ include Harvest::Behavior::Crud
5
+ include Harvest::Behavior::Activatable
6
+
7
+ api_model Harvest::Client
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,12 @@
1
+ module Harvest
2
+ module API
3
+ class Company < Base
4
+ api_model Harvest::Company
5
+
6
+ def info
7
+ response = request(:get, credentials, api_model.api_path)
8
+ api_model.parse(response.parsed_response)
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,9 @@
1
+ module Harvest
2
+ module API
3
+ class Contacts < Base
4
+ include Harvest::Behavior::Crud
5
+
6
+ api_model Harvest::Contact
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module Harvest
2
+ module API
3
+ class ExpenseCategories < Base
4
+ include Harvest::Behavior::Crud
5
+
6
+ api_model Harvest::ExpenseCategory
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,26 @@
1
+ module Harvest
2
+ module API
3
+ class Expenses < Base
4
+ api_model Harvest::Expense
5
+ include Harvest::Behavior::Crud
6
+
7
+ def attach(expense, filename, receipt)
8
+ body = ""
9
+ body << "--__X_ATTACH_BOUNDARY__\r\n"
10
+ body << %Q{Content-Disposition: form-data; name="expense[receipt]"; filename="#{filename}"\r\n}
11
+ body << "\r\n#{receipt.read}"
12
+ body << "\r\n--__X_ATTACH_BOUNDARY__--\r\n\r\n"
13
+
14
+ request(
15
+ :post,
16
+ credentials,
17
+ "#{api_model.api_path}/#{expense.id}/receipt",
18
+ :headers => {
19
+ 'Content-Type' => 'multipart/form-data; charset=utf-8; boundary=__X_ATTACH_BOUNDARY__',
20
+ 'Content-Length' => body.length.to_s,
21
+ },
22
+ :body => body)
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,9 @@
1
+ module Harvest
2
+ module API
3
+ class InvoiceCategories < Base
4
+ include Harvest::Behavior::Crud
5
+
6
+ api_model Harvest::InvoiceCategory
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,86 @@
1
+ module Harvest
2
+ module API
3
+ class InvoiceMessages < Base
4
+ api_model Harvest::InvoiceMessage
5
+
6
+ def all(invoice, query = {})
7
+ response = request(:get, credentials, "/invoices/#{invoice.id}/messages", query: query)
8
+ response_parsed = api_model.to_json(response.parsed_response)
9
+
10
+ if response_parsed['total_pages'] > 1
11
+ counter = response_parsed['page']
12
+
13
+ while counter <= response_parsed['total_pages'] do
14
+ counter += 1
15
+ query = { 'page' => counter }
16
+
17
+ response_page = request(:get, credentials,
18
+ "/invoices/#{invoice.id}/messages",
19
+ query: query)
20
+ invoice_messages = api_model.to_json(response.parsed_response)
21
+ response_parsed['invoice_messages']
22
+ .concat(invoice_messages['invoice_messages'])
23
+ end
24
+ end
25
+
26
+ api_model.parse(response_parsed)
27
+ end
28
+
29
+ def create(invoice, invoice_message)
30
+ invoice_message = api_model.wrap(invoice_message)
31
+ response = request(:post, credentials, "/invoices/#{invoice.id}/messages", body: invoice_message.to_json)
32
+ find(invoice_message.id)
33
+ end
34
+
35
+ def delete(invoice, invoice_message)
36
+ request(:delete, credentials, "/invoices/#{invoice.id}/messages/#{invoice_message.id}")
37
+ message.id
38
+ end
39
+
40
+ # Create a message for marking an invoice as sent.
41
+ #
42
+ # @param [Harvest::InvoiceMessage] The message you want to send
43
+ # @return [Harvest::InvoiceMessage] The sent message
44
+ def mark_as_sent(invoice, invoice_message)
45
+ send_status_message(invoice, invoice_message, 'send')
46
+ end
47
+
48
+ # Create a message and mark an open invoice as closed (writing an invoice off)
49
+ #
50
+ # @param [Harvest::InvoiceMessage] The message you want to send
51
+ # @return [Harvest::InvoiceMessage] The sent message
52
+ def mark_as_closed(invoice, invoice_message)
53
+ send_status_message(invoice, invoice_message, 'close')
54
+ end
55
+
56
+ # Create a message and mark a closed (written-off) invoice as open
57
+ #
58
+ # @param [Harvest::InvoiceMessage] The message you want to send
59
+ # @return [Harvest::InvoiceMessage] The sent message
60
+ def re_open(invoice, invoice_message)
61
+ send_status_message(invoice, invoice_message, 're-open')
62
+ end
63
+
64
+ # Create a message for marking an open invoice as draft
65
+ #
66
+ # @param [Harvest::InvoiceMessage] The message you want to send
67
+ # @return [Harvest::InvoiceMessage] The sent message
68
+ def mark_as_draft(invoice_message)
69
+ send_status_message(invoice, invoice_message, 'draft')
70
+ end
71
+
72
+ private
73
+
74
+ def send_status_message(invoice, invoice_message, action)
75
+ invoice_message = api_model.wrap(invoice_message)
76
+ response = request( :post,
77
+ credentials,
78
+ "/invoices/#{invoice.id}/messages",
79
+ event_type: action,
80
+ body: invoice_message.to_json)
81
+
82
+ invoice_message
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,41 @@
1
+ module Harvest
2
+ module API
3
+ class InvoicePayments < Base
4
+ api_model Harvest::InvoicePayment
5
+
6
+ def all(invoice, query = {})
7
+ response = request(:get, credentials, "/invoices/#{invoice.id}/payments", query: query)
8
+ response_parsed = api_model.to_json(response.parsed_response)
9
+
10
+ if response_parsed['total_pages'].to_i > 1
11
+ counter = response_parsed['page'].to_i
12
+
13
+ while counter <= response_parsed['total_pages'].to_i do
14
+ counter += 1
15
+ query = { 'page' => counter }
16
+
17
+ response_page = request(:get, credentials,
18
+ "/invoices/#{invoice.id}/payments",
19
+ query: query)
20
+ invoice_payments = api_model.to_json(response.parsed_response)
21
+ response_parsed['invoice_payments']
22
+ .concat(invoice_payments['invoice_payments'])
23
+ end
24
+ end
25
+
26
+ api_model.parse(response_parsed)
27
+ end
28
+
29
+ def create(invoice, invoice_payment)
30
+ invoice_payment = api_model.wrap(invoice_payment)
31
+ response = request(:post, credentials, "/invoices/#{invoice.id}/payments", body: invoice_payment.to_json)
32
+ find(invoice_payment.id)
33
+ end
34
+
35
+ def delete(invoice, invoice_payment)
36
+ request(:delete, credentials, "/invoices/#{invoice.id}/payments/#{invoice_payment.id}")
37
+ invoice_payment.id
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,9 @@
1
+ module Harvest
2
+ module API
3
+ class Invoices < Base
4
+ api_model Harvest::Invoice
5
+
6
+ include Harvest::Behavior::Crud
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module Harvest
2
+ module API
3
+ class Projects < Base
4
+ api_model Harvest::Project
5
+
6
+ include Harvest::Behavior::Crud
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,75 @@
1
+ module Harvest
2
+ module API
3
+ class TaskAssignments < Base
4
+ api_model Harvest::TaskAssignment
5
+
6
+ def all(project, query = {})
7
+ api_path = "/projects/#{project.id}/task_assignments"
8
+ response = request(:get, credentials, api_path, query: query)
9
+ response_parsed = api_model.to_json(response.parsed_response)
10
+
11
+ if response_parsed['total_pages'].to_i > 1
12
+ counter = response_parsed['page'].to_i
13
+
14
+ while counter <= response_parsed['total_pages'].to_i do
15
+ counter += 1
16
+ query = { 'page' => counter }
17
+ response_page = request(:get, credentials, api_path, query: query)
18
+ project_task_assignments = api_model.to_json(response.parsed_response)
19
+ response_parsed['task_assignments']
20
+ .concat(project_task_assignments['task_assignments'])
21
+ end
22
+ end
23
+
24
+ api_model.parse(response_parsed)
25
+ end
26
+
27
+ def find(project, task_assignment)
28
+ api_path = "/projects/#{project.id}/task_assignments/#{task_assignment.id}"
29
+ response = request(:get, credentials, api_path)
30
+ api_model.parse(response.parsed_response)
31
+ end
32
+
33
+ def create(project, task_assignment)
34
+ task_assignment = api_model.wrap(task_assignment)
35
+ api_path = "/projects/#{project.id}/task_assignments"
36
+ response = request(:post, credentials, api_path, body: task_assignment.to_json)
37
+ find(task_assignment.project_id, task_assignment.id)
38
+ end
39
+
40
+ def update(project, task_assignment)
41
+ task_assignment = api_model.wrap(task_assignment)
42
+ api_path = "/projects/#{project.id}/task_assignments/#{task_assignment.id}"
43
+ request(:put, credentials, api_path, body: task_assignment.to_json)
44
+ find(task_assignment.project_id, task_assignment.id)
45
+ end
46
+
47
+ def delete(task_assignment)
48
+ api_path = "/projects/#{project.id}/task_assignments/#{task_assignment.id}"
49
+ response = request(:delete, credentials, api_path)
50
+ task_assignment.id
51
+ end
52
+
53
+ def me(query = {})
54
+ api_path = "/task_assignments"
55
+ response = request(:get, credentials, api_path, query: query)
56
+ response_parsed = api_model.to_json(response.parsed_response)
57
+
58
+ if response_parsed['total_pages'].to_i > 1
59
+ counter = response_parsed['page'].to_i
60
+
61
+ while counter <= response_parsed['total_pages'].to_i do
62
+ counter += 1
63
+ query = { 'page' => counter }
64
+ response_page = request(:get, credentials, api_path, query: query)
65
+ task_assignments = api_model.to_json(response.parsed_response)
66
+ response_parsed['task_assignments']
67
+ .concat(task_assignments['task_assignments'])
68
+ end
69
+ end
70
+
71
+ api_model.parse(response_parsed)
72
+ end
73
+ end
74
+ end
75
+ end