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,58 @@
1
+ module Harvest
2
+ # The model that contains information about a task
3
+ #
4
+ # == Fields
5
+ # [+id+] (READONLY) the id of the user
6
+ # [+email+] the email of the user
7
+ # [+first_name+] the first name for the user
8
+ # [+last_name+] the last name for the user
9
+ # [+telephone+] the telephone for the user
10
+ # [+department] the department for the user
11
+ # [+has_access_to_all_future_projects+] whether the user should be added to future projects by default
12
+ # [+hourly_rate+] what the default hourly rate for the user is
13
+ # [+admin?+] whether the user is an admin
14
+ # [+contractor?+] whether the user is a contractor
15
+ # [+contractor?+] whether the user is a contractor
16
+ # [+timezone+] the timezone for the user.
17
+ class User < Hashie::Mash
18
+ include Harvest::Model
19
+
20
+ skip_json_root true
21
+ api_path '/users'
22
+
23
+ delegate_methods(active?: :is_active,
24
+ admin?: :is_admin,
25
+ contractor?: :is_contractor)
26
+
27
+ def initialize(args = {}, _ = nil)
28
+ args = args.stringify_keys
29
+ args['is_admin'] = args.delete('admin') if args['admin']
30
+ self.timezone = args.delete('timezone') if args['timezone']
31
+ super
32
+ end
33
+
34
+ # Sets the timezone for the user. This can be done in a variety of ways.
35
+ #
36
+ # == Examples
37
+ # user.timezone = :cst # the easiest way. CST, EST, MST, and PST are supported
38
+ #
39
+ # user.timezone = 'america/chicago' # a little more verbose
40
+ #
41
+ # user.timezone = 'Central Time (US & Canada)' # the most explicit way
42
+ def timezone=(timezone)
43
+ tz = timezone.to_s.downcase
44
+ case tz
45
+ when 'cst', 'cdt' then self.timezone = 'america/chicago'
46
+ when 'est', 'edt' then self.timezone = 'america/new_york'
47
+ when 'mst', 'mdt' then self.timezone = 'america/denver'
48
+ when 'pst', 'pdt' then self.timezone = 'america/los_angeles'
49
+ else
50
+ if Harvest::Timezones::MAPPING[tz]
51
+ self['timezone'] = Harvest::Timezones::MAPPING[tz]
52
+ else
53
+ self['timezone'] = timezone
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,27 @@
1
+ module Harvest
2
+ class UserAssignment < Hashie::Mash
3
+ include Harvest::Model
4
+
5
+ skip_json_root true
6
+
7
+ def initialize(args = {}, _ = nil)
8
+ args = args.stringify_keys
9
+ self.user = args.delete('user') if args['user']
10
+ self.project = args.delete('project') if args['project']
11
+
12
+ super
13
+ end
14
+
15
+ def user=(user)
16
+ self['user_id'] = user['id']
17
+ end
18
+
19
+ def project=(project)
20
+ self['project_id'] = project['id']
21
+ end
22
+
23
+ def active?
24
+ !deactivated
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,3 @@
1
+ module Harvest
2
+ VERSION = '5.0.3'
3
+ end
@@ -0,0 +1,96 @@
1
+ require 'httparty'
2
+ require 'base64'
3
+ require 'delegate'
4
+ require 'hashie'
5
+ require 'json'
6
+ require 'time'
7
+ require 'csv'
8
+
9
+ require 'ext/array'
10
+ require 'ext/hash'
11
+ require 'ext/date'
12
+ require 'ext/time'
13
+
14
+ require 'harvest/version'
15
+ require 'harvest/credentials'
16
+ require 'harvest/errors'
17
+ require 'harvest/hardy_client'
18
+ require 'harvest/timezones'
19
+
20
+ require 'harvest/base'
21
+
22
+ %w(crud activatable).each { |a| require "harvest/behavior/#{a}" }
23
+
24
+ %w(model client contact project task user task_assignment
25
+ user_assignment expense_category expense time_entry invoice_category
26
+ line_item invoice invoice_payment
27
+ invoice_message).each { |a| require "harvest/#{a}" }
28
+
29
+ %w(base account clients contacts projects tasks users task_assignments
30
+ user_assignments expense_categories expenses time_entry
31
+ invoice_categories invoices invoice_payments
32
+ invoice_messages).each { |a| require "harvest/api/#{a}" }
33
+
34
+ module Harvest
35
+ class << self
36
+
37
+ # Creates a standard client that will raise all errors it encounters
38
+ #
39
+ # == Options
40
+ # * Basic Authentication V2
41
+ # * +:access_token+ - Your Harvest access_token
42
+ # * +:account_id+ - Your Harvest account_id
43
+ # * +:client_id+ - An OAuth 2.0 access token
44
+ #
45
+ # == Examples
46
+ # Harvest.client(access_token: 'token', account_id: '123')
47
+ # Harvest.client(access_token: 'myaccesstoken', client_id: '1')
48
+ #
49
+ # @return [Harvest::Base]
50
+ def client(access_token: nil, account_id: nil, client_id: nil)
51
+ Harvest::Base.new(access_token: access_token, account_id: account_id,
52
+ client_id: client_id)
53
+ end
54
+
55
+ # Creates a hardy client that will retry common HTTP errors
56
+ # it encounters and sleep() if it determines it is over your rate limit
57
+ #
58
+ # == Options
59
+ # * Basic Authentication
60
+ # * +:access_token+ - Your Harvest access_token
61
+ # * +:account_id+ - Your Harvest account_id
62
+ # * +:client_id+ - An OAuth 2.0 access token
63
+ # * +:retry+ - How many times the hardy client should retry errors.
64
+ # Set to +5+ by default.
65
+ #
66
+ # == Examples
67
+ # Harvest.hardy_client(access_token: 'token', account_id: '123', retry: 3)
68
+ #
69
+ # Harvest.hardy_client(access_token: 'myaccesstoken', client_id: '1',
70
+ # retries: 3)
71
+ #
72
+ # == Errors
73
+ # The hardy client will retry the following errors
74
+ # * Harvest::Unavailable
75
+ # * Harvest::InformHarvest
76
+ # * Net::HTTPError
77
+ # * Net::HTTPFatalError
78
+ # * Errno::ECONNRESET
79
+ #
80
+ # == Rate Limits
81
+ # The hardy client will make as many requests as it can until it detects it
82
+ # has gone over the rate limit. Then it will +sleep()+ for the how ever
83
+ # long it takes for the limit to reset. You can find more information about
84
+ # the Rate Limiting at http://www.getharvest.com/api
85
+ #
86
+ # @return [Harvest::HardyClient] a Harvest::Base wrapped in a Harvest::HardyClient
87
+ # @see Harvest::Base
88
+ def hardy_client(access_token: nil, account_id: nil, client_id: nil, retries: 5)
89
+ Harvest::HardyClient.new(client(
90
+ access_token: access_token,
91
+ account_id: account_id,
92
+ client_id: client_id),
93
+ retries)
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,14 @@
1
+ FactoryBot.define do
2
+ factory :client, class: Harvest::Client do
3
+ name 'John Doe'
4
+ details "Steam Cleaning across the country"
5
+
6
+ trait :active do
7
+ active true
8
+ end
9
+
10
+ trait :deactive do
11
+ active false
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,8 @@
1
+ FactoryBot.define do
2
+ factory :contact, class: Harvest::Contact do
3
+ client
4
+ email 'jane@example.com'
5
+ first_name 'Jane'
6
+ last_name 'Doe'
7
+ end
8
+ end
@@ -0,0 +1,10 @@
1
+ FactoryBot.define do
2
+ factory :expense, class: Harvest::Expense do
3
+ notes 'Drive to Chicago'
4
+ total_cost 75.0
5
+ spent_at Time.utc(2009, 12, 28)
6
+ expense_category
7
+ project
8
+ end
9
+ end
10
+
@@ -0,0 +1,7 @@
1
+ FactoryBot.define do
2
+ factory :expense_category, class: Harvest::ExpenseCategory do
3
+ name 'Mileage'
4
+ unit_price 100
5
+ unit_name 'mile'
6
+ end
7
+ end
@@ -0,0 +1,25 @@
1
+ FactoryBot.define do
2
+ factory :invoice, class: Harvest::Invoice do
3
+ sequence(:id)
4
+ subject "Invoice for Joe's Stream Cleaning"
5
+ amount 2400.0
6
+ issued_at '2014-01-01'
7
+ due_at "2011-03-31"
8
+ due_at_human_format 'upon receipt'
9
+ currency 'United States Dollars - USD'
10
+ number '1000'
11
+ notes 'Some notes go here'
12
+ period_end '2013-03-31'
13
+ period_start '2013-02-26'
14
+ kind 'free_form'
15
+ state 'draft'
16
+ purchase_order nil
17
+ tax nil
18
+ tax2 nil
19
+ import_hours 'no'
20
+ import_expenses 'no'
21
+
22
+ client
23
+ end
24
+ end
25
+
@@ -0,0 +1,5 @@
1
+ FactoryBot.define do
2
+ factory :invoice_category, class: Harvest::InvoiceCategory do
3
+ name 'New Category'
4
+ end
5
+ end
@@ -0,0 +1,9 @@
1
+ FactoryBot.define do
2
+ factory :invoice_message, class: Harvest::InvoiceMessage do
3
+ body 'The message body goes here'
4
+ recipients 'john@example.com, jane@example.com'
5
+ attach_pdf true
6
+ send_me_a_copy true
7
+ include_pay_pal_link true
8
+ end
9
+ end
@@ -0,0 +1,7 @@
1
+ FactoryBot.define do
2
+ factory :invoice_payment, class: Harvest::InvoicePayment do
3
+ paid_at Time.now
4
+ amount 0.00
5
+ notes 'Payment received'
6
+ end
7
+ end
@@ -0,0 +1,9 @@
1
+ FactoryBot.define do
2
+ factory :line_item, class: Harvest::LineItem do
3
+ kind 'Service'
4
+ description 'Description'
5
+ quantity 200
6
+ unit_price '12.00'
7
+ project
8
+ end
9
+ end
@@ -0,0 +1,15 @@
1
+ FactoryBot.define do
2
+ factory :project, class: Harvest::Project do
3
+ name 'Project Test'
4
+ notes 'project to test the api'
5
+ client
6
+
7
+ trait :active do
8
+ active true
9
+ end
10
+
11
+ trait :deactive do
12
+ active false
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,8 @@
1
+ FactoryBot.define do
2
+ factory :task, class: Harvest::Task do
3
+ name 'A crud task'
4
+ billable_by_default true
5
+ default_hourly_rate 120
6
+ end
7
+ end
8
+
@@ -0,0 +1,8 @@
1
+ FactoryBot.define do
2
+ factory :task_assignment, class: Harvest::TaskAssignment do
3
+ hourly_rate 120
4
+ project
5
+ task
6
+ end
7
+ end
8
+
@@ -0,0 +1,13 @@
1
+ FactoryBot.define do
2
+ factory :time_entry, class: Harvest::TimeEntry do
3
+ notes 'Test api support'
4
+ hours 3
5
+ spent_date '2009/12/28'
6
+ task
7
+ project
8
+
9
+ trait :started do
10
+ timer_started_at '2009/12/28'
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,19 @@
1
+ FactoryBot.define do
2
+ factory :user, class: Harvest::User do
3
+ sequence(:id)
4
+ first_name 'Edgar'
5
+ last_name 'Ruth'
6
+ email 'edgar@ruth.com'
7
+ timezone 'cst'
8
+ is_admin 'false'
9
+ telephone '444-4444'
10
+
11
+ trait :active do
12
+ is_active true
13
+ end
14
+
15
+ trait :deactive do
16
+ is_active false
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,7 @@
1
+ FactoryBot.define do
2
+ factory :user_assignment, class: Harvest::UserAssignment do
3
+ hourly_rate 120
4
+ project
5
+ user
6
+ end
7
+ end
@@ -0,0 +1,105 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'harvest clients' do
4
+ let(:harvest) { Harvest.client(access_token: 'mytoken', account_id: '123') }
5
+
6
+ describe 'clients' do
7
+ let(:client) { create(:client) }
8
+ let(:client_attributes) { FactoryBot.attributes_for(:client) }
9
+
10
+ context 'allows to add clients' do
11
+ before do
12
+ allow(harvest.clients).to receive(:create).and_return(client)
13
+ end
14
+
15
+ it 'returns true' do
16
+ expect(client.name).to eql(client_attributes[:name])
17
+ end
18
+ end
19
+
20
+ context 'allows to update clients' do
21
+ before do
22
+ allow(harvest.clients).to receive(:update).and_return(client)
23
+ client.name = "Joe and Frank's Steam Cleaning"
24
+ client = harvest.clients.update(client)
25
+ end
26
+
27
+ it 'returns true' do
28
+ expect(client.name).to eql("Joe and Frank's Steam Cleaning")
29
+ end
30
+ end
31
+
32
+ context 'allows to remove clients' do
33
+ before do
34
+ allow(harvest.clients).to receive(:delete).and_return([])
35
+ allow(harvest.clients).to receive(:all).and_return([])
36
+ harvest.clients.delete(client)
37
+ end
38
+
39
+ it 'returns true' do
40
+ expect(harvest.clients.all.select do |p|
41
+ p.name == "Joe and Frank's Steam Cleaning"
42
+ end).to eql([])
43
+ end
44
+ end
45
+
46
+ context 'allows activating and deactivating clients' do
47
+ let(:active_client) { create(:client, :active) }
48
+ let(:deactive_client) { create(:client, :deactive) }
49
+
50
+ before do
51
+ allow(harvest.clients).to receive(:deactivate)
52
+ .and_return(deactive_client)
53
+ allow(harvest.clients).to receive(:activate)
54
+ .and_return(active_client)
55
+ end
56
+
57
+ it 'should be active' do
58
+ expect(active_client).to be_active
59
+ end
60
+
61
+ it 'should be deactive' do
62
+ client = harvest.clients.deactivate(client)
63
+ expect(deactive_client).not_to be_active
64
+ end
65
+
66
+ it 'should reactive a client' do
67
+ client = harvest.clients.activate(client)
68
+ expect(active_client).to be_active
69
+ end
70
+ end
71
+ end
72
+
73
+ describe 'contacts' do
74
+ let(:client) { create(:client) }
75
+ let(:contact) { create(:contact, client: client) }
76
+ let(:contact_attributes) { FactoryBot.attributes_for(:contact) }
77
+
78
+ context 'allows to add contacts' do
79
+ before do
80
+ allow(harvest.contacts).to receive(:create)
81
+ .and_return(contact)
82
+ contact = harvest.contacts.create(contact_attributes)
83
+ end
84
+
85
+ it 'should be true' do
86
+ expect(contact.client_id).to eql(client.id)
87
+ expect(contact.email).to eql('jane@example.com')
88
+ end
89
+ end
90
+
91
+ context 'allows to delete contacts' do
92
+ before do
93
+ allow(harvest.contacts).to receive(:delete).and_return([])
94
+ allow(harvest.contacts).to receive(:all).and_return([])
95
+ contact = harvest.contacts.delete(contact)
96
+ end
97
+
98
+ it 'should return empty' do
99
+ expect(harvest.contacts.all.select do |e|
100
+ e.email == 'jane@example.com'
101
+ end).to eql([])
102
+ end
103
+ end
104
+ end
105
+ end