harvested2 5.0.3

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.
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