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