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.
- checksums.yaml +7 -0
- data/.document +3 -0
- data/.gitignore +35 -0
- data/.rspec +1 -0
- data/.rubocop.yml +34 -0
- data/.ruby-version +1 -0
- data/.travis.yml +12 -0
- data/Gemfile +20 -0
- data/HISTORY.md +118 -0
- data/MIT-LICENSE +21 -0
- data/README.md +66 -0
- data/Rakefile +24 -0
- data/harvested2.gemspec +30 -0
- data/lib/ext/array.rb +52 -0
- data/lib/ext/date.rb +9 -0
- data/lib/ext/hash.rb +17 -0
- data/lib/ext/time.rb +5 -0
- data/lib/harvest/account.rb +13 -0
- data/lib/harvest/api/account.rb +25 -0
- data/lib/harvest/api/base.rb +72 -0
- data/lib/harvest/api/clients.rb +10 -0
- data/lib/harvest/api/company.rb +12 -0
- data/lib/harvest/api/contacts.rb +9 -0
- data/lib/harvest/api/expense_categories.rb +9 -0
- data/lib/harvest/api/expenses.rb +26 -0
- data/lib/harvest/api/invoice_categories.rb +9 -0
- data/lib/harvest/api/invoice_messages.rb +86 -0
- data/lib/harvest/api/invoice_payments.rb +41 -0
- data/lib/harvest/api/invoices.rb +9 -0
- data/lib/harvest/api/projects.rb +9 -0
- data/lib/harvest/api/task_assignments.rb +75 -0
- data/lib/harvest/api/tasks.rb +9 -0
- data/lib/harvest/api/time_entry.rb +19 -0
- data/lib/harvest/api/user_assignments.rb +75 -0
- data/lib/harvest/api/users.rb +10 -0
- data/lib/harvest/base.rb +333 -0
- data/lib/harvest/behavior/activatable.rb +31 -0
- data/lib/harvest/behavior/crud.rb +80 -0
- data/lib/harvest/client.rb +23 -0
- data/lib/harvest/company.rb +8 -0
- data/lib/harvest/contact.rb +20 -0
- data/lib/harvest/credentials.rb +34 -0
- data/lib/harvest/errors.rb +27 -0
- data/lib/harvest/expense.rb +54 -0
- data/lib/harvest/expense_category.rb +10 -0
- data/lib/harvest/hardy_client.rb +80 -0
- data/lib/harvest/invoice.rb +75 -0
- data/lib/harvest/invoice_category.rb +8 -0
- data/lib/harvest/invoice_message.rb +8 -0
- data/lib/harvest/invoice_payment.rb +8 -0
- data/lib/harvest/line_item.rb +21 -0
- data/lib/harvest/model.rb +133 -0
- data/lib/harvest/project.rb +41 -0
- data/lib/harvest/receipt.rb +12 -0
- data/lib/harvest/task.rb +21 -0
- data/lib/harvest/task_assignment.rb +27 -0
- data/lib/harvest/time_entry.rb +57 -0
- data/lib/harvest/timezones.rb +130 -0
- data/lib/harvest/user.rb +58 -0
- data/lib/harvest/user_assignment.rb +27 -0
- data/lib/harvest/version.rb +3 -0
- data/lib/harvested2.rb +96 -0
- data/spec/factories/client.rb +14 -0
- data/spec/factories/contact.rb +8 -0
- data/spec/factories/expense.rb +10 -0
- data/spec/factories/expenses_category.rb +7 -0
- data/spec/factories/invoice.rb +25 -0
- data/spec/factories/invoice_category.rb +5 -0
- data/spec/factories/invoice_message.rb +9 -0
- data/spec/factories/invoice_payment.rb +7 -0
- data/spec/factories/line_item.rb +9 -0
- data/spec/factories/project.rb +15 -0
- data/spec/factories/task.rb +8 -0
- data/spec/factories/task_assignment.rb +8 -0
- data/spec/factories/time_entry.rb +13 -0
- data/spec/factories/user.rb +19 -0
- data/spec/factories/user_assigment.rb +7 -0
- data/spec/functional/clients_spec.rb +105 -0
- data/spec/functional/errors_spec.rb +42 -0
- data/spec/functional/expenses_spec.rb +97 -0
- data/spec/functional/invoice_messages_spec.rb +48 -0
- data/spec/functional/invoice_payments_spec.rb +51 -0
- data/spec/functional/invoice_spec.rb +138 -0
- data/spec/functional/project_spec.rb +76 -0
- data/spec/functional/tasks_spec.rb +119 -0
- data/spec/functional/time_entries_spec.rb +87 -0
- data/spec/functional/users_spec.rb +72 -0
- data/spec/harvest/base_spec.rb +10 -0
- data/spec/harvest/basic_auth_credentials_spec.rb +12 -0
- data/spec/harvest/expense_category_spec.rb +5 -0
- data/spec/harvest/expense_spec.rb +18 -0
- data/spec/harvest/invoice_message_spec.rb +5 -0
- data/spec/harvest/invoice_payment_spec.rb +5 -0
- data/spec/harvest/invoice_spec.rb +5 -0
- data/spec/harvest/oauth_credentials_spec.rb +11 -0
- data/spec/harvest/project_spec.rb +5 -0
- data/spec/harvest/task_assignment_spec.rb +5 -0
- data/spec/harvest/task_spec.rb +5 -0
- data/spec/harvest/time_entry_spec.rb +23 -0
- data/spec/harvest/user_assignment_spec.rb +5 -0
- data/spec/harvest/user_spec.rb +34 -0
- data/spec/spec_helper.rb +22 -0
- data/spec/support/factory_bot.rb +5 -0
- data/spec/support/harvested_helpers.rb +28 -0
- data/spec/support/json_examples.rb +9 -0
- metadata +238 -0
data/lib/ext/hash.rb
ADDED
|
@@ -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
|
data/lib/ext/time.rb
ADDED
|
@@ -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,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,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,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
|