harvested2 5.0.3
Sign up to get free protection for your applications and to get access to all the features.
- 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
|