├── lib ├── harvesting │ ├── version.rb │ ├── models │ │ ├── harvest_record.rb │ │ ├── line_item.rb │ │ ├── tasks.rb │ │ ├── users.rb │ │ ├── invoices.rb │ │ ├── projects.rb │ │ ├── client.rb │ │ ├── clients.rb │ │ ├── time_entries.rb │ │ ├── task.rb │ │ ├── project_task_assignments.rb │ │ ├── project_user_assignments.rb │ │ ├── project_user_assignment.rb │ │ ├── contact.rb │ │ ├── harvest_record_collection.rb │ │ ├── project_task_assignment.rb │ │ ├── user.rb │ │ ├── invoice.rb │ │ ├── time_entry.rb │ │ ├── project.rb │ │ └── base.rb │ ├── errors.rb │ ├── enumerable.rb │ └── client.rb └── harvesting.rb ├── Dockerfile ├── Rakefile ├── TODO.md ├── spec ├── harvesting_spec.rb ├── harvesting │ ├── models │ │ ├── project_user_assignment_spec.rb │ │ ├── project_task_assignment_spec.rb │ │ ├── contact_spec.rb │ │ ├── project_spec.rb │ │ └── time_entry_spec.rb │ └── harvest_data_setup.rb └── spec_helper.rb ├── Gemfile ├── docker-compose.yml ├── bin ├── setup └── console ├── .travis.yml ├── .gitignore ├── .env.sample ├── pull_request_template.md ├── LICENSE.txt ├── harvesting.gemspec ├── fixtures └── vcr_cassettes │ ├── Harvesting_Client │ ├── authentication_when_client_is_not_authenticated_raises_a_Harvesting_AuthenticationError.yml │ └── throttling_when_client_reaches_the_API_rate_limit_raises_a_Harvesting_RateLimitExceeded.yml │ ├── Harvesting_Client_clients │ ├── when_user_is_not_an_administrator_returns_the_clients_associated_with_the_account.yml │ └── when_user_is_an_administrator_returns_the_clients_associated_with_the_account.yml │ ├── Harvesting_Client_contacts │ ├── when_user_is_not_an_administrator_returns_the_contacts_associated_with_the_account.yml │ └── when_user_is_an_administrator_returns_the_contacts_associated_with_the_account.yml │ ├── Harvesting_Models_Contact_get │ └── provides_direct_access_to_a_specific_contact.yml │ ├── Harvesting_Models_TimeEntry_create │ ├── when_trying_to_create_a_time_entry_without_required_attributes_fails.yml │ └── when_trying_to_create_a_time_entry_with_the_required_attributes_automatically_sets_the_id_of_the_time_entry.yml │ ├── harvest_data_setup │ ├── client_pepe.yml │ ├── client_toto.yml │ ├── task_coding.yml │ ├── task_writing.yml │ ├── contact_jon_snow.yml │ ├── contact_cersei_lannister.yml │ ├── user_me.yml │ ├── project_assignment_road_building.yml │ ├── task_assigment_castle_building_coding.yml │ ├── task_assignment_roading_building_writing.yml │ ├── project_assignment_castle_building.yml │ ├── user_john_smith.yml │ ├── user_jane_doe.yml │ ├── project_road_building.yml │ └── project_castle_building.yml │ ├── Harvesting_Client_time_entries │ └── when_account_has_no_entries_returns_the_time_entries_associated_with_the_account.yml │ ├── Harvesting_Client_invoices │ ├── when_account_has_no_invoices_returns_the_invoices_associated_with_the_account.yml │ ├── when_account_has_invoices_with_custom_options_only_returns_the_invoices_mathing_the_options.yml │ ├── when_account_has_invoices_has_line_item_for_invoices.yml │ ├── when_account_has_invoices_builds_line_items_for_invoices.yml │ └── when_account_has_invoices_returns_the_invoices_associated_with_the_account.yml │ ├── Harvesting_Models_ProjectUserAssignment_create │ ├── with_a_project_id_creates_the_user_assignment.yml │ └── without_a_project_id_raises_Harvesting_RequestNotFound_error.yml │ ├── Harvesting_Client_me │ └── returns_the_authenticated_user.yml │ ├── Harvesting_Models_Project_get │ └── provides_direct_access_to_a_specific_project.yml │ ├── Harvesting_Models_Project_task_assignments │ └── as_an_admin_user_retrieves_project_task_assignments_for_the_castle_building_project.yml │ ├── Harvesting_Client_task_assignments │ └── as_an_admin_user_retrieves_the_accounts_task_assignments.yml │ ├── Harvesting_Models_Project_user_assignments │ └── as_an_admin_user_retrieves_project_user_assignments_for_castle_building_project.yml │ ├── Harvesting_Client_user_assignments │ └── as_an_admin_user_retreives_the_accounts_user_assignments.yml │ └── Harvesting_Client_delete │ └── raises_a_UnprocessableRequest_exception_if_entity_is_not_removable.yml ├── Guardfile ├── CODE_OF_CONDUCT.md └── RELEASE_NOTES.md /lib/harvesting/version.rb: -------------------------------------------------------------------------------- 1 | module Harvesting 2 | VERSION = "0.6.0".freeze 3 | end 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ruby:2.4.1 2 | 3 | RUN mkdir /gem 4 | WORKDIR /gem 5 | 6 | RUN gem install bundler -v 1.16.1 7 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rspec/core/rake_task" 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task :default => :spec 7 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | * Implement methods like `project.time_entries` or `user.time_entries` 2 | * Implement methods like `project.time_entries.create(attribute: value)` 3 | -------------------------------------------------------------------------------- /spec/harvesting_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe Harvesting do 4 | it "has a version number" do 5 | expect(Harvesting::VERSION).not_to be nil 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } 4 | 5 | # Specify your gem's dependencies in harvesting.gemspec 6 | gemspec 7 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | gem: 4 | build: . 5 | volumes: 6 | - .:/gem 7 | - bundler:/usr/local/bundle 8 | 9 | volumes: 10 | bundler: 11 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | 10 | if [ ! -f .env ]; then 11 | cp .env.sample .env 12 | echo "Make sure that .env has valid values." 13 | fi 14 | -------------------------------------------------------------------------------- /lib/harvesting/models/harvest_record.rb: -------------------------------------------------------------------------------- 1 | module Harvesting 2 | module Models 3 | class HarvestRecord < Base 4 | 5 | def save 6 | id.nil? ? create : update 7 | end 8 | 9 | def create 10 | harvest_client.create(self) 11 | end 12 | 13 | def update 14 | harvest_client.update(self) 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: ruby 3 | rvm: 4 | - 2.5.8 5 | - 2.6.6 6 | - 2.7.1 7 | - 3.0 8 | env: 9 | - HARVEST_FIRST_NAME=Aaron HARVEST_LAST_NAME=Burr HARVEST_ACCOUNT_ID=112341234 HARVEST_NON_ADMIN_ACCOUNT_ID=112341234 HARVEST_ACCESS_TOKEN=112341234 HARVEST_NON_ADMIN_ACCESS_TOKEN=112341234 HARVEST_ADMIN_FULL_NAME=112341234 10 | 11 | before_install: gem install bundler -v 2.1.4 12 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "dotenv/load" 5 | require "harvesting" 6 | 7 | # You can add fixtures and/or initialization code here to make experimenting 8 | # with your gem easier. You can also use a different console, if you like. 9 | 10 | # (If you use this, don't forget to add pry to your Gemfile!) 11 | # require "pry" 12 | # Pry.start 13 | 14 | require "irb" 15 | IRB.start(__FILE__) 16 | -------------------------------------------------------------------------------- /lib/harvesting/errors.rb: -------------------------------------------------------------------------------- 1 | module Harvesting 2 | class AuthenticationError < StandardError 3 | end 4 | 5 | class UnprocessableRequest < StandardError 6 | end 7 | 8 | class RequestNotFound < StandardError 9 | def initialize(uri) 10 | super("The page you were looking for may have been moved or the address misspelled: #{uri}") 11 | end 12 | end 13 | 14 | class RateLimitExceeded < StandardError 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | 10 | # bundler 11 | Gemfile.lock 12 | 13 | # environment variables 14 | .env 15 | .env.* 16 | !.env.sample 17 | 18 | # rspec failure tracking 19 | .rspec_status 20 | 21 | # byebug 22 | .byebug_history 23 | 24 | # rspec 25 | .rspec 26 | 27 | # ruby versions 28 | .ruby-version 29 | 30 | .idea/ 31 | harvesting.iml 32 | 33 | .DS_Store 34 | -------------------------------------------------------------------------------- /lib/harvesting/models/line_item.rb: -------------------------------------------------------------------------------- 1 | module Harvesting 2 | module Models 3 | # A line item on an invoice from your Harvest account. 4 | # 5 | # For more information: https://help.getharvest.com/api-v2/invoices-api/invoices/invoices/ 6 | class LineItem < HarvestRecord 7 | attributed :id, 8 | :kind, 9 | :description, 10 | :quantity, 11 | :unit_price, 12 | :amount, 13 | :taxed, 14 | :taxed2 15 | 16 | modeled project: Project 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/harvesting/models/tasks.rb: -------------------------------------------------------------------------------- 1 | module Harvesting 2 | module Models 3 | class Tasks < HarvestRecordCollection 4 | 5 | def initialize(attrs, query_opts = {}, opts = {}) 6 | super(attrs.reject {|k,v| k == "tasks" }, query_opts, opts) 7 | @entries = attrs["tasks"].map do |entry| 8 | Task.new(entry, harvest_client: opts[:harvest_client]) 9 | end 10 | end 11 | 12 | def fetch_next_page 13 | @entries += harvest_client.tasks(next_page_query_opts).entries 14 | @attributes['page'] = page + 1 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/harvesting/models/users.rb: -------------------------------------------------------------------------------- 1 | module Harvesting 2 | module Models 3 | class Users < HarvestRecordCollection 4 | 5 | def initialize(attrs, query_opts = {}, opts = {}) 6 | super(attrs.reject {|k,v| k == "users" }, query_opts, opts) 7 | @entries = attrs["users"].map do |entry| 8 | User.new(entry, harvest_client: opts[:harvest_client]) 9 | end 10 | end 11 | 12 | def fetch_next_page 13 | @entries += harvest_client.users(next_page_query_opts).entries 14 | @attributes['page'] = page + 1 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /.env.sample: -------------------------------------------------------------------------------- 1 | HARVEST_FIRST_NAME= 2 | HARVEST_LAST_NAME= 3 | HARVEST_ACCOUNT_ID= 4 | HARVEST_NON_ADMIN_ACCOUNT_ID= 5 | HARVEST_ACCESS_TOKEN= 6 | HARVEST_NON_ADMIN_ACCESS_TOKEN= 7 | HARVEST_ADMIN_FULL_NAME= 8 | HARVEST_NON_ADMIN_FULL_NAME= 9 | HARVEST_ADMIN_FIRST_NAME= 10 | HARVEST_ADMIN_LAST_NAME= 11 | -------------------------------------------------------------------------------- /lib/harvesting/models/invoices.rb: -------------------------------------------------------------------------------- 1 | module Harvesting 2 | module Models 3 | class Invoices < HarvestRecordCollection 4 | def initialize(attrs, query_opts = {}, opts = {}) 5 | super(attrs.reject {|k,v| k == "invoices" }, query_opts, opts) 6 | @entries = attrs["invoices"].map do |entry| 7 | Invoice.new(entry, harvest_client: opts[:harvest_client]) 8 | end 9 | end 10 | 11 | def fetch_next_page 12 | @entries += harvest_client.invoices(next_page_query_opts).entries 13 | @attributes['page'] = page + 1 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/harvesting/models/projects.rb: -------------------------------------------------------------------------------- 1 | module Harvesting 2 | module Models 3 | class Projects < HarvestRecordCollection 4 | 5 | def initialize(attrs, query_opts = {}, opts = {}) 6 | super(attrs.reject {|k,v| k == "projects" }, query_opts, opts) 7 | @entries = attrs["projects"].map do |entry| 8 | Project.new(entry, harvest_client: opts[:harvest_client]) 9 | end 10 | end 11 | 12 | def fetch_next_page 13 | @entries += harvest_client.projects(next_page_query_opts).entries 14 | @attributes['page'] = page + 1 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/harvesting/models/client.rb: -------------------------------------------------------------------------------- 1 | module Harvesting 2 | module Models 3 | # A client record from your Harvest account. 4 | # 5 | # For more information: https://help.getharvest.com/api-v2/clients-api/clients/clients/ 6 | class Client < HarvestRecord 7 | attributed :id, 8 | :name, 9 | :is_active, 10 | :address, 11 | :created_at, 12 | :updated_at, 13 | :currency 14 | 15 | def path 16 | @attributes['id'].nil? ? "clients" : "clients/#{@attributes['id']}" 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/harvesting/models/clients.rb: -------------------------------------------------------------------------------- 1 | module Harvesting 2 | module Models 3 | class Clients < HarvestRecordCollection 4 | 5 | def initialize(attrs, query_opts = {}, opts = {}) 6 | super(attrs.reject {|k,v| k == "clients" }, query_opts, opts) 7 | @entries = attrs["clients"].map do |entry| 8 | Harvesting::Models::Client.new(entry, harvest_client: opts[:harvest_client]) 9 | end 10 | end 11 | 12 | def fetch_next_page 13 | @entries += harvest_client.clients(next_page_query_opts).entries 14 | @attributes['page'] = page + 1 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/harvesting/models/time_entries.rb: -------------------------------------------------------------------------------- 1 | module Harvesting 2 | module Models 3 | class TimeEntries < HarvestRecordCollection 4 | 5 | def initialize(attrs, query_opts = {}, opts = {}) 6 | super(attrs.reject {|k,v| k == "time_entries" }, query_opts, opts) 7 | @entries = attrs["time_entries"].map do |entry| 8 | TimeEntry.new(entry, harvest_client: opts[:harvest_client]) 9 | end 10 | end 11 | 12 | def fetch_next_page 13 | @entries += harvest_client.time_entries(next_page_query_opts).entries 14 | @attributes['page'] = page + 1 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/harvesting/models/task.rb: -------------------------------------------------------------------------------- 1 | module Harvesting 2 | module Models 3 | # A task record from your Harvest account. 4 | # 5 | # For more information: https://help.getharvest.com/api-v2/tasks-api/tasks/tasks/ 6 | class Task < HarvestRecord 7 | attributed :id, 8 | :name, 9 | :billable_by_default, 10 | :default_hourly_rate, 11 | :is_default, 12 | :is_active, 13 | :created_at, 14 | :updated_at 15 | 16 | def path 17 | @attributes['id'].nil? ? "tasks" : "tasks/#{@attributes['id']}" 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /pull_request_template.md: -------------------------------------------------------------------------------- 1 | **IMPORTANT: Please read the README before submitting pull requests for this project. Additionally, if your PR closes any open GitHub issues, make sure you include _Closes #XXXX_ in your comment or use the option on the PR's sidebar to add related issues to auto-close the issue that your PR fixes. ** 2 | 3 | **Description:** 4 | 5 | Please include a summary of the change and which issue is fixed or which feature is introduced. If changes to the behavior are made, clearly describe what changes. 6 | If changes to the UI are made, please include screenshots of the before and after. 7 | 8 | 9 | I will abide by the [code of conduct](CODE_OF_CONDUCT.md). 10 | -------------------------------------------------------------------------------- /lib/harvesting/models/project_task_assignments.rb: -------------------------------------------------------------------------------- 1 | module Harvesting 2 | module Models 3 | class ProjectTaskAssignments < HarvestRecordCollection 4 | def initialize(attrs, query_opts = {}, opts = {}) 5 | super(attrs.reject {|k,v| k == "task_assignments" }, query_opts, opts) 6 | @entries = attrs["task_assignments"].map do |entry| 7 | ProjectTaskAssignment.new(entry, harvest_client: opts[:harvest_client]) 8 | end 9 | end 10 | 11 | def fetch_next_page 12 | @entries += harvest_client.task_assignments(next_page_query_opts).entries 13 | @attributes['page'] = page + 1 14 | end 15 | 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/harvesting/models/project_user_assignments.rb: -------------------------------------------------------------------------------- 1 | module Harvesting 2 | module Models 3 | class ProjectUserAssignments < HarvestRecordCollection 4 | 5 | def initialize(attrs, query_opts = {}, opts = {}) 6 | super(attrs.reject {|k,v| k == "user_assignments" }, query_opts, opts) 7 | @entries = attrs["user_assignments"].map do |entry| 8 | ProjectUserAssignment.new(entry, harvest_client: opts[:harvest_client]) 9 | end 10 | end 11 | 12 | def fetch_next_page 13 | @entries += harvest_client.user_assignments(next_page_query_opts).entries 14 | @attributes['page'] = page + 1 15 | end 16 | 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/harvesting/models/project_user_assignment.rb: -------------------------------------------------------------------------------- 1 | module Harvesting 2 | module Models 3 | class ProjectUserAssignment < HarvestRecord 4 | attributed :id, 5 | :is_active, 6 | :is_project_manager, 7 | :hourly_rate, 8 | :budget, 9 | :created_at, 10 | :updated_at 11 | 12 | modeled project: Project, 13 | user: User 14 | 15 | def path 16 | base_url = "projects/#{project.id}/user_assignments" 17 | @attributes['id'].nil? ? base_url : "#{base_url}/#{@attributes['id']}" 18 | end 19 | 20 | def to_hash 21 | { project_id: project.id, user_id: user.id }.merge(super) 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/harvesting/models/contact.rb: -------------------------------------------------------------------------------- 1 | module Harvesting 2 | module Models 3 | # A contact record from your Harvest account. 4 | # 5 | # For more information: https://help.getharvest.com/api-v2/clients-api/clients/contacts/ 6 | class Contact < HarvestRecord 7 | attributed :id, 8 | :title, 9 | :first_name, 10 | :last_name, 11 | :email, 12 | :phone_office, 13 | :phone_mobile, 14 | :fax, 15 | :created_at, 16 | :updated_at 17 | 18 | modeled client: Client 19 | 20 | def path 21 | @attributes['id'].nil? ? "contacts" : "contacts/#{@attributes['id']}" 22 | end 23 | 24 | def to_hash 25 | { client_id: client.id }.merge(super) 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/harvesting/enumerable.rb: -------------------------------------------------------------------------------- 1 | # `Enumerable` extends the stdlib `Enumerable` to provide pagination for paged 2 | # API requests. 3 | # 4 | # @see https://github.com/sferik/twitter/blob/aa909b3b7733ca619d80f1c8cba961033d1fc7e6/lib/twitter/enumerable.rb 5 | module Harvesting 6 | module Enumerable 7 | include ::Enumerable 8 | 9 | # @return [Enumerator] 10 | def each(start = 0, &block) 11 | @cursor = start 12 | return to_enum(:each, start) unless block_given? 13 | Array(@entries[start..-1]).each_with_index do |element, index| 14 | @cursor = index 15 | yield(element) 16 | end 17 | 18 | unless last? 19 | start = [@entries.size, start].max 20 | fetch_next_page 21 | each(start, &block) 22 | end 23 | self 24 | end 25 | 26 | private 27 | 28 | # @return [Boolean] 29 | def last? 30 | (((page - 1) * per_page) + @cursor) >= (total_entries - 1) 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/harvesting/models/harvest_record_collection.rb: -------------------------------------------------------------------------------- 1 | require 'forwardable' 2 | 3 | module Harvesting 4 | module Models 5 | class HarvestRecordCollection < Base 6 | include Harvesting::Enumerable 7 | extend Forwardable 8 | 9 | attributed :per_page, 10 | :total_pages, 11 | :total_entries, 12 | :next_page, 13 | :previous_page, 14 | :page, 15 | :links 16 | 17 | attr_reader :entries 18 | 19 | def initialize(attrs, query_opts = {}, opts = {}) 20 | super(attrs, opts) 21 | @query_opts = query_opts 22 | @api_page = attrs 23 | end 24 | 25 | def page 26 | @attributes['page'] 27 | end 28 | 29 | def size 30 | total_entries 31 | end 32 | 33 | def next_page_query_opts 34 | @query_opts.merge(page: page + 1) 35 | end 36 | 37 | def fetch_next_page 38 | raise NotImplementedError 39 | end 40 | 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /spec/harvesting/models/project_user_assignment_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe Harvesting::Models::ProjectUserAssignment, :vcr do 4 | let(:attrs) { {} } 5 | let(:user_assignment) { Harvesting::Models::ProjectUserAssignment.new(attrs) } 6 | 7 | describe '#create' do 8 | context 'without a project id' do 9 | let(:attrs) do 10 | { 11 | 'project' => { 'id' => nil }, 12 | 'user' => { 'id' => '5678' } 13 | } 14 | end 15 | 16 | it 'raises Harvesting::RequestNotFound error' do 17 | expect { user_assignment.create }.to raise_error(Harvesting::RequestNotFound) 18 | end 19 | end 20 | 21 | context 'with a project id' do 22 | let(:attrs) do 23 | { 24 | 'project' => { 'id' => '1234' }, 25 | 'user' => { 'id' => '5678' } 26 | } 27 | end 28 | it 'creates the user assignment' do 29 | user_assignment.create 30 | expect(user_assignment.id).to eq(125_068_758) 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/harvesting/models/project_task_assignment.rb: -------------------------------------------------------------------------------- 1 | module Harvesting 2 | module Models 3 | # A task assignment record from your Harvest account. 4 | # 5 | # For more information: https://help.getharvest.com/api-v2/projects-api/projects/task-assignments/ 6 | class ProjectTaskAssignment < HarvestRecord 7 | attributed :id, 8 | :is_active, 9 | :billable, 10 | :hourly_rate, 11 | :budget, 12 | :created_at, 13 | :updated_at 14 | 15 | modeled project: Project, 16 | task: Task 17 | 18 | def path 19 | base_url = "projects/#{project.id}/task_assignments" 20 | id.nil? ? base_url : "#{base_url}/#{id}" 21 | end 22 | 23 | # def project_id 24 | # # TODO: handle case where project's id is part of json object 25 | # @attributes["project_id"] 26 | # end 27 | 28 | def to_hash 29 | { project_id: project.id, task_id: task.id }.merge(super) 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/harvesting/models/project_task_assignment_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe Harvesting::Models::ProjectTaskAssignment, :vcr do 4 | let(:attrs) { Hash.new } 5 | let(:task_assignment) { Harvesting::Models::ProjectTaskAssignment.new(attrs) } 6 | 7 | describe '#path' do 8 | context 'without a task assignment id' do 9 | let(:attrs) do 10 | { 11 | 'project' => { 12 | 'id' => '1234' 13 | } 14 | } 15 | end 16 | it 'includes the project id' do 17 | expect(task_assignment.path).to eq('projects/1234/task_assignments') 18 | end 19 | end 20 | 21 | context 'with a task assignment id' do 22 | let(:attrs) do 23 | { 24 | 'id' => '1111', 25 | 'project' => { 26 | 'id' => '2222' 27 | } 28 | } 29 | end 30 | it 'includes project id and task assignment id' do 31 | expect(task_assignment.path).to eq( 32 | 'projects/2222/task_assignments/1111' 33 | ) 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Ernesto Tagwerker 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/harvesting.rb: -------------------------------------------------------------------------------- 1 | # framework 2 | require "harvesting/version" 3 | require "harvesting/enumerable" 4 | require "harvesting/errors" 5 | require "harvesting/models/base" 6 | require "harvesting/models/harvest_record" 7 | require "harvesting/models/harvest_record_collection" 8 | # harvest records 9 | require "harvesting/models/client" 10 | require "harvesting/models/user" 11 | require "harvesting/models/project" 12 | require "harvesting/models/task" 13 | require "harvesting/models/project_user_assignment" 14 | require "harvesting/models/project_task_assignment" 15 | require "harvesting/models/invoice" 16 | require "harvesting/models/line_item" 17 | require "harvesting/models/time_entry" 18 | # harvest record collections 19 | require "harvesting/models/clients" 20 | require "harvesting/models/tasks" 21 | require "harvesting/models/users" 22 | require "harvesting/models/contact" 23 | require "harvesting/models/time_entries" 24 | require "harvesting/models/projects" 25 | require "harvesting/models/project_user_assignments" 26 | require "harvesting/models/project_task_assignments" 27 | require "harvesting/models/invoices" 28 | # API client 29 | require "harvesting/client" 30 | 31 | module Harvesting 32 | end 33 | -------------------------------------------------------------------------------- /spec/harvesting/models/contact_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe Harvesting::Models::Contact, :vcr do 4 | let(:attrs) { Hash.new } 5 | let(:contact) { Harvesting::Models::Contact.new(attrs) } 6 | 7 | include_context "harvest data setup" 8 | 9 | describe '.new' do 10 | context 'with client attributes in attrs' do 11 | let(:contact_id) { '1235' } 12 | let(:contact_name) { 'Lannister Co' } 13 | let(:client_attrs) { { "id" => contact_id, "name" => contact_name } } 14 | let(:attrs) { { "client" => client_attrs } } 15 | 16 | it 'provides access to a client object with the specified attributes' do 17 | expect(contact.client.id).to eq(contact_id) 18 | expect(contact.client.name).to eq(contact_name) 19 | end 20 | end 21 | end 22 | 23 | describe '.get' do 24 | it 'provides direct access to a specific contact' do 25 | contact = Harvesting::Models::Contact.get(contact_jon_snow.id) 26 | expect(contact.id).to eq(contact_jon_snow.id) 27 | expect(contact.first_name).to eq(contact_jon_snow.first_name) 28 | expect(contact.last_name).to eq(contact_jon_snow.last_name) 29 | expect(contact.client.id.to_i).to eq(contact_jon_snow.client.id.to_i) 30 | end 31 | end 32 | 33 | end 34 | -------------------------------------------------------------------------------- /lib/harvesting/models/user.rb: -------------------------------------------------------------------------------- 1 | module Harvesting 2 | module Models 3 | # An user record from your Harvest account. 4 | # 5 | # For more information: https://help.getharvest.com/api-v2/users-api/users/users/ 6 | class User < HarvestRecord 7 | attributed :id, 8 | :name, 9 | :first_name, 10 | :last_name, 11 | :email, 12 | :telephone, 13 | :timezone, 14 | :has_access_to_all_future_projects, 15 | :is_contractor, 16 | :is_admin, 17 | :is_project_manager, 18 | :can_see_rates, 19 | :can_create_invoices, 20 | :can_create_projects, 21 | :is_active, 22 | :weekly_capacity, 23 | :default_hourly_rate, 24 | :cost_rate, 25 | :roles, 26 | :avatar_url, 27 | :created_at, 28 | :updated_at 29 | 30 | def path 31 | @attributes['id'].nil? ? "users" : "users/#{@attributes['id']}" 32 | end 33 | 34 | def name 35 | @attributes['name'].nil? ? "#{first_name} #{last_name}" : @attributes['name'] 36 | end 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/harvesting/models/invoice.rb: -------------------------------------------------------------------------------- 1 | module Harvesting 2 | module Models 3 | # An invoice record from your Harvest account. 4 | # 5 | # For more information: https://help.getharvest.com/api-v2/invoices-api/invoices/invoices/ 6 | class Invoice < HarvestRecord 7 | attributed :id, 8 | :client_key, 9 | :number, 10 | :purchase_order, 11 | :amount, 12 | :due_amount, 13 | :tax, 14 | :tax_amount, 15 | :tax2, 16 | :tax2_amount, 17 | :discount, 18 | :discount_amount, 19 | :subject, 20 | :notes, 21 | :currency, 22 | :state, 23 | :period_start, 24 | :period_end, 25 | :issue_date, 26 | :due_date, 27 | :payment_term, 28 | :sent_at, 29 | :paid_at, 30 | :paid_date, 31 | :closed_at, 32 | :created_at, 33 | :updated_at 34 | 35 | def line_items 36 | @line_items ||= @attributes['line_items'].map { |line_item_attributes| LineItem.new line_item_attributes, { harvest_client: harvest_client } } 37 | end 38 | 39 | def path 40 | @attributes['id'].nil? ? "invoices" : "invoices/#{@attributes['id']}" 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/harvesting/models/time_entry.rb: -------------------------------------------------------------------------------- 1 | module Harvesting 2 | module Models 3 | # A time entry record from your Harvest account. 4 | # 5 | # For more information: https://help.getharvest.com/api-v2/timesheets-api/timesheets/time-entries/ 6 | class TimeEntry < HarvestRecord 7 | attributed :id, 8 | :spent_date, 9 | :hours, 10 | :notes, 11 | :is_locked, 12 | :locked_reason, 13 | :is_closed, 14 | :is_billed, 15 | :timer_started_at, 16 | :started_time, 17 | :ended_time, 18 | :is_running, 19 | :billable, 20 | :budgeted, 21 | :billable_rate, 22 | :cost_rate, 23 | :invoice, 24 | :external_reference, 25 | :created_at, 26 | :updated_at 27 | 28 | modeled project: Project, 29 | user: User, 30 | task: Task, 31 | client: Client, 32 | task_assignment: ProjectTaskAssignment, 33 | user_assignment: ProjectUserAssignment 34 | 35 | 36 | def path 37 | @attributes['id'].nil? ? "time_entries" : "time_entries/#{@attributes['id']}" 38 | end 39 | 40 | def to_hash 41 | { project_id: project.id, task_id: task.id, user_id: user.id }.merge(super) 42 | end 43 | 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | if ENV["COVERAGE"] 2 | require "simplecov" 3 | SimpleCov.start do 4 | track_files "{lib}/**/*.rb" 5 | add_filter "/spec/" 6 | end 7 | if ENV["CODECOV_TOKEN"] 8 | require "codecov" 9 | SimpleCov.formatter = SimpleCov::Formatter::Codecov 10 | end 11 | end 12 | 13 | require "bundler/setup" 14 | require 'dotenv/load' 15 | require "harvesting" 16 | require "webmock" 17 | require "webmock/rspec" 18 | require "vcr" 19 | 20 | VCR.configure do |config| 21 | config.cassette_library_dir = "fixtures/vcr_cassettes" 22 | config.hook_into :webmock # or :fakeweb 23 | 24 | config.filter_sensitive_data('$HARVEST_ACCESS_TOKEN') { ENV['HARVEST_ACCESS_TOKEN'] } 25 | config.filter_sensitive_data('$HARVEST_NON_ADMIN_ACCESS_TOKEN') { ENV['HARVEST_NON_ADMIN_ACCESS_TOKEN'] } 26 | config.filter_sensitive_data('$HARVEST_ACCOUNT_ID') { ENV['HARVEST_ACCOUNT_ID'] } 27 | config.filter_sensitive_data('$HARVEST_NON_ADMIN_ACCOUNT_ID') { ENV['HARVEST_NON_ADMIN_ACCOUNT_ID'] } 28 | end 29 | 30 | RSpec.configure do |config| 31 | # Enable flags like --only-failures and --next-failure 32 | config.example_status_persistence_file_path = ".rspec_status" 33 | 34 | # Disable RSpec exposing methods globally on `Module` and `main` 35 | config.disable_monkey_patching! 36 | 37 | config.expect_with :rspec do |c| 38 | c.syntax = :expect 39 | end 40 | 41 | config.around(:each, :vcr) do |example| 42 | name = example.metadata[:full_description].split(/\s+/, 2).join("/").gsub(/[^\w\/]+/, "_") 43 | 44 | VCR.use_cassette(name) { example.call } 45 | end 46 | end 47 | 48 | require_relative './harvesting/harvest_data_setup' 49 | -------------------------------------------------------------------------------- /harvesting.gemspec: -------------------------------------------------------------------------------- 1 | 2 | lib = File.expand_path("../lib", __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require "harvesting/version" 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "harvesting" 8 | spec.version = Harvesting::VERSION 9 | spec.authors = ["Ernesto Tagwerker", "M. Scott Ford"] 10 | spec.email = ["ernesto+github@ombulabs.com", "scott@mscottford.com"] 11 | 12 | spec.summary = %q{Ruby wrapper for the Harvest API v2.0} 13 | spec.description = %q{Interact with the Harvest API v2.0 from your Ruby application} 14 | spec.homepage = "https://github.com/fastruby/harvesting" 15 | spec.license = "MIT" 16 | 17 | spec.files = `git ls-files -z`.split("\x0").reject do |f| 18 | f.match(%r{^(test|spec|fixtures|features)/}) 19 | end 20 | spec.bindir = "exe" 21 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 22 | spec.require_paths = ["lib"] 23 | spec.add_dependency "http", ">= 3.3", "< 5.1" 24 | 25 | spec.add_development_dependency "bundler", ">= 2.0", "< 3.0" 26 | spec.add_development_dependency "rake", "~> 13.0" 27 | spec.add_development_dependency "rspec", "~> 3.0" 28 | spec.add_development_dependency "guard-rspec", "~> 4.7", ">= 4.7" 29 | spec.add_development_dependency "byebug", ">= 10.0", "< 12.0" 30 | spec.add_development_dependency "vcr", "~> 4.0", ">= 4.0" 31 | spec.add_development_dependency "webmock", "~> 3.4", ">= 3.4" 32 | spec.add_development_dependency "dotenv", "~> 2.5", ">= 2.5" 33 | spec.add_development_dependency "simplecov", "~> 0.19.0" 34 | spec.add_development_dependency "codecov", "~> 0.2.9" 35 | end 36 | -------------------------------------------------------------------------------- /fixtures/vcr_cassettes/Harvesting_Client/authentication_when_client_is_not_authenticated_raises_a_Harvesting_AuthenticationError.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: https://api.harvestapp.com/v2/users/me 6 | body: 7 | encoding: UTF-8 8 | string: '' 9 | headers: 10 | User-Agent: 11 | - Harvesting Ruby Gem 12 | Authorization: 13 | - Bearer foo 14 | Harvest-Account-Id: 15 | - bar 16 | Connection: 17 | - close 18 | Host: 19 | - api.harvestapp.com 20 | response: 21 | status: 22 | code: 401 23 | message: Unauthorized 24 | headers: 25 | Server: 26 | - nginx 27 | Date: 28 | - Fri, 25 Jan 2019 18:32:03 GMT 29 | Content-Type: 30 | - application/json 31 | Content-Length: 32 | - '134' 33 | Connection: 34 | - close 35 | Status: 36 | - 401 Unauthorized 37 | Www-Authenticate: 38 | - Bearer realm="Rack::OAuth2 Protected Resources", error="invalid_token", error_description="The 39 | access token provided is expired, revoked, malformed or invalid for other 40 | reasons." 41 | Cache-Control: 42 | - no-cache 43 | X-Request-Id: 44 | - de3649500e5cbf9bfc035b2695be0ccf 45 | X-Runtime: 46 | - '0.049054' 47 | Strict-Transport-Security: 48 | - max-age=31536000; includeSubDomains 49 | body: 50 | encoding: ASCII-8BIT 51 | string: '{"error":"invalid_token","error_description":"The access token provided 52 | is expired, revoked, malformed or invalid for other reasons."}' 53 | http_version: 54 | recorded_at: Fri, 25 Jan 2019 18:32:03 GMT 55 | recorded_with: VCR 4.0.0 56 | -------------------------------------------------------------------------------- /lib/harvesting/models/project.rb: -------------------------------------------------------------------------------- 1 | module Harvesting 2 | module Models 3 | # A project record from your Harvest account. 4 | # 5 | # For more information: https://help.getharvest.com/api-v2/projects-api/projects/projects/ 6 | class Project < HarvestRecord 7 | attributed :id, 8 | :name, 9 | :code, 10 | :is_active, 11 | :is_billable, 12 | :is_fixed_fee, 13 | :bill_by, 14 | :hourly_rate, 15 | :budget, 16 | :budget_by, 17 | :budget_is_monthly, 18 | :notify_when_over_budget, 19 | :over_budget_notification_percentage, 20 | :over_budget_notification_date, 21 | :show_budget_to_all, 22 | :cost_budget, 23 | :cost_budget_include_expenses, 24 | :fee, 25 | :notes, 26 | :starts_on, 27 | :ends_on, 28 | :created_at, 29 | :updated_at 30 | 31 | modeled client: Client 32 | 33 | def path 34 | @attributes['id'].nil? ? "projects" : "projects/#{@attributes['id']}" 35 | end 36 | 37 | def to_hash 38 | { client_id: client.id }.merge(super) 39 | end 40 | 41 | def time_entries 42 | harvest_client.time_entries(project_id: self.id) 43 | end 44 | 45 | # Provides access to the user assignments that are associated with this 46 | # project. 47 | def user_assignments 48 | harvest_client.user_assignments(project_id: self.id) 49 | end 50 | 51 | # Provides access to the task assignments that are associated with this 52 | # project. 53 | def task_assignments 54 | harvest_client.task_assignments(project_id: self.id) 55 | end 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /spec/harvesting/models/project_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe Harvesting::Models::Project, :vcr do 4 | let(:attrs) { Hash.new } 5 | let(:project) { Harvesting::Models::Project.new(attrs) } 6 | let(:project_id) { 19815868 } 7 | 8 | include_context "harvest data setup" 9 | 10 | describe '.new' do 11 | context 'with client attributes in attrs' do 12 | let(:project_id) { '1235' } 13 | let(:project_name) { 'Lannister Co' } 14 | let(:client_attrs) { { "id" => project_id, "name" => project_name } } 15 | let(:attrs) { { "client" => client_attrs } } 16 | 17 | it 'provides access to a client object with the specified attributes' do 18 | expect(project.client.id).to eq(project_id) 19 | expect(project.client.name).to eq(project_name) 20 | end 21 | end 22 | end 23 | 24 | describe '.get' do 25 | it 'provides direct access to a specific project' do 26 | project = Harvesting::Models::Project.get(project_id) 27 | expect(project.id).to eq(project_id) 28 | expect(project.name).to eq("X") 29 | end 30 | end 31 | 32 | describe "#time_entries" do 33 | it "loads associated time entries" do 34 | project = Harvesting::Models::Project.get(project_id) 35 | 36 | expect(project.time_entries.size).to eq(1) 37 | expect(project.time_entries.first.hours).to eq(1.25) 38 | end 39 | end 40 | 41 | describe "#user_assignments", :vcr do 42 | context "as an admin user" do 43 | subject { Harvesting::Client.new(access_token: admin_access_token, account_id: account_id) } 44 | 45 | it 'retrieves project user assignments for castle building project' do 46 | user_assignments = project_castle_building.user_assignments 47 | users = user_assignments.map { |ua| ua.user.id }.uniq 48 | expect(users).to contain_exactly(user_john_smith.id, user_me.id) 49 | end 50 | end 51 | end 52 | 53 | describe '#task_assignments' do 54 | context "as an admin user" do 55 | subject { Harvesting::Client.new(access_token: admin_access_token, account_id: account_id) } 56 | 57 | it 'retrieves project task assignments for the castle building project' do 58 | task_assignments = project_castle_building.task_assignments 59 | tasks = task_assignments.map { |ta| ta.task.id }.uniq 60 | expect(tasks).to contain_exactly(task_coding.id) 61 | end 62 | end 63 | end 64 | 65 | 66 | end 67 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | # A sample Guardfile 2 | # More info at https://github.com/guard/guard#readme 3 | 4 | ## Uncomment and set this to only include directories you want to watch 5 | # directories %w(app lib config test spec features) \ 6 | # .select{|d| Dir.exists?(d) ? d : UI.warning("Directory #{d} does not exist")} 7 | 8 | ## Note: if you are using the `directories` clause above and you are not 9 | ## watching the project directory ('.'), then you will want to move 10 | ## the Guardfile to a watched dir and symlink it back, e.g. 11 | # 12 | # $ mkdir config 13 | # $ mv Guardfile config/ 14 | # $ ln -s config/Guardfile . 15 | # 16 | # and, you'll have to watch "config/Guardfile" instead of "Guardfile" 17 | 18 | # Note: The cmd option is now required due to the increasing number of ways 19 | # rspec may be run, below are examples of the most common uses. 20 | # * bundler: 'bundle exec rspec' 21 | # * bundler binstubs: 'bin/rspec' 22 | # * spring: 'bin/rspec' (This will use spring if running and you have 23 | # installed the spring binstubs per the docs) 24 | # * zeus: 'zeus rspec' (requires the server to be started separately) 25 | # * 'just' rspec: 'rspec' 26 | 27 | guard :rspec, cmd: "bundle exec rspec" do 28 | require "guard/rspec/dsl" 29 | dsl = Guard::RSpec::Dsl.new(self) 30 | 31 | # Feel free to open issues for suggestions and improvements 32 | 33 | # RSpec files 34 | rspec = dsl.rspec 35 | watch(rspec.spec_helper) { rspec.spec_dir } 36 | watch(rspec.spec_support) { rspec.spec_dir } 37 | watch(rspec.spec_files) 38 | 39 | # Ruby files 40 | ruby = dsl.ruby 41 | dsl.watch_spec_files_for(ruby.lib_files) 42 | 43 | # Rails files 44 | rails = dsl.rails(view_extensions: %w(erb haml slim)) 45 | dsl.watch_spec_files_for(rails.app_files) 46 | dsl.watch_spec_files_for(rails.views) 47 | 48 | watch(rails.controllers) do |m| 49 | [ 50 | rspec.spec.call("routing/#{m[1]}_routing"), 51 | rspec.spec.call("controllers/#{m[1]}_controller"), 52 | rspec.spec.call("acceptance/#{m[1]}") 53 | ] 54 | end 55 | 56 | # Rails config changes 57 | watch(rails.spec_helper) { rspec.spec_dir } 58 | watch(rails.routes) { "#{rspec.spec_dir}/routing" } 59 | watch(rails.app_controller) { "#{rspec.spec_dir}/controllers" } 60 | 61 | # Capybara features specs 62 | watch(rails.view_dirs) { |m| rspec.spec.call("features/#{m[1]}") } 63 | watch(rails.layouts) { |m| rspec.spec.call("features/#{m[1]}") } 64 | 65 | # Turnip features and steps 66 | watch(%r{^spec/acceptance/(.+)\.feature$}) 67 | watch(%r{^spec/acceptance/steps/(.+)_steps\.rb$}) do |m| 68 | Dir[File.join("**/#{m[1]}.feature")][0] || "spec/acceptance" 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /fixtures/vcr_cassettes/Harvesting_Client/throttling_when_client_reaches_the_API_rate_limit_raises_a_Harvesting_RateLimitExceeded.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: https://api.harvestapp.com/v2/users/me 6 | body: 7 | encoding: UTF-8 8 | string: '' 9 | headers: 10 | User-Agent: 11 | - Harvesting Ruby Gem 12 | Authorization: 13 | - Bearer $HARVEST_NON_ADMIN_ACCESS_TOKEN 14 | Harvest-Account-Id: 15 | - "$HARVEST_ACCOUNT_ID" 16 | Connection: 17 | - close 18 | Host: 19 | - api.harvestapp.com 20 | response: 21 | status: 22 | code: 429 23 | message: Too Many Requests 24 | headers: 25 | Server: 26 | - nginx 27 | Date: 28 | - Thu, 03 Sep 2020 10:31:23 GMT 29 | Content-Type: 30 | - text/html 31 | Connection: 32 | - close 33 | Status: 34 | - 429 Too Many Requests 35 | X-Frame-Options: 36 | - SAMEORIGIN 37 | X-Xss-Protection: 38 | - 1; mode=block 39 | X-Content-Type-Options: 40 | - nosniff 41 | X-Download-Options: 42 | - noopen 43 | X-Permitted-Cross-Domain-Policies: 44 | - none 45 | Referrer-Policy: 46 | - strict-origin-when-cross-origin 47 | Cache-Control: 48 | - no-cache, no-store 49 | P3p: 50 | - 'CP="Our privacy policy is available online: https://www.getharvest.com/services/privacy-policy"' 51 | X-App-Server: 52 | - app9 53 | X-Robots-Tag: 54 | - noindex, nofollow 55 | Content-Security-Policy: 56 | - 'report-uri /csp_reports; default-src *; img-src * data:; font-src data: cache.harvestapp.com 57 | https://js.intercomcdn.com; script-src ''self'' ''unsafe-inline'' ''unsafe-eval'' 58 | https://*.google-analytics.com https://*.nr-data.net https://ajax.googleapis.com 59 | cache.harvestapp.com https://js-agent.newrelic.com https://platform.twitter.com 60 | https://www.google.com https://www.googleadservices.com https://www.googletagmanager.com 61 | https://connect.facebook.net https://googleads.g.doubleclick.net https://app.intercom.io 62 | https://widget.intercom.io https://js.intercomcdn.com https://cdn.plaid.com; 63 | style-src ''self'' ''unsafe-inline'' cache.harvestapp.com https://www.google.com' 64 | X-Request-Id: 65 | - 5a38ccf03cddc57d226a0ce3680784e9 66 | X-Runtime: 67 | - '0.021738' 68 | Strict-Transport-Security: 69 | - max-age=31536000; includeSubDomains 70 | X-Server: 71 | - lb4 72 | body: 73 | encoding: UTF-8 74 | string: "" 75 | http_version: 76 | recorded_at: Thu, 03 Sep 2020 10:31:23 GMT 77 | recorded_with: VCR 4.0.0 78 | -------------------------------------------------------------------------------- /fixtures/vcr_cassettes/Harvesting_Client_clients/when_user_is_not_an_administrator_returns_the_clients_associated_with_the_account.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: https://api.harvestapp.com/v2/clients 6 | body: 7 | encoding: UTF-8 8 | string: '' 9 | headers: 10 | User-Agent: 11 | - Harvesting Ruby Gem 12 | Authorization: 13 | - Bearer $HARVEST_NON_ADMIN_ACCESS_TOKEN 14 | Harvest-Account-Id: 15 | - "$HARVEST_ACCOUNT_ID" 16 | Connection: 17 | - close 18 | Host: 19 | - api.harvestapp.com 20 | response: 21 | status: 22 | code: 403 23 | message: Forbidden 24 | headers: 25 | Server: 26 | - nginx 27 | Date: 28 | - Fri, 25 Jan 2019 18:32:07 GMT 29 | Content-Type: 30 | - application/json; charset=utf-8 31 | Transfer-Encoding: 32 | - chunked 33 | Connection: 34 | - close 35 | Status: 36 | - 403 Forbidden 37 | X-Frame-Options: 38 | - SAMEORIGIN 39 | X-Xss-Protection: 40 | - 1; mode=block 41 | X-Content-Type-Options: 42 | - nosniff 43 | X-Download-Options: 44 | - noopen 45 | X-Permitted-Cross-Domain-Policies: 46 | - none 47 | Referrer-Policy: 48 | - strict-origin-when-cross-origin 49 | Cache-Control: 50 | - no-cache, no-store 51 | P3p: 52 | - 'CP="Our privacy policy is available online: https://www.getharvest.com/services/privacy-policy"' 53 | X-App-Server: 54 | - app8 55 | X-Robots-Tag: 56 | - noindex, nofollow 57 | Content-Security-Policy: 58 | - 'report-uri /csp_reports; default-src *; img-src * data:; font-src data: cache.harvestapp.com 59 | https://js.intercomcdn.com; script-src ''self'' ''unsafe-inline'' ''unsafe-eval'' 60 | https://*.google-analytics.com https://*.nr-data.net https://ajax.googleapis.com 61 | cache.harvestapp.com https://js-agent.newrelic.com https://platform.twitter.com 62 | https://www.google.com https://www.googleadservices.com https://www.googletagmanager.com 63 | https://connect.facebook.net https://googleads.g.doubleclick.net https://app.intercom.io 64 | https://widget.intercom.io https://js.intercomcdn.com https://cdn.plaid.com; 65 | style-src ''self'' ''unsafe-inline'' cache.harvestapp.com https://www.google.com' 66 | Hint: 67 | - Not authorized! 68 | X-Request-Id: 69 | - 73b545ad8aef05af1a3c44cae7a298ae 70 | X-Runtime: 71 | - '0.089403' 72 | Strict-Transport-Security: 73 | - max-age=31536000; includeSubDomains 74 | body: 75 | encoding: UTF-8 76 | string: '{"message":"Not authorized!"}' 77 | http_version: 78 | recorded_at: Fri, 25 Jan 2019 18:32:07 GMT 79 | recorded_with: VCR 4.0.0 80 | -------------------------------------------------------------------------------- /fixtures/vcr_cassettes/Harvesting_Client_contacts/when_user_is_not_an_administrator_returns_the_contacts_associated_with_the_account.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: https://api.harvestapp.com/v2/contacts 6 | body: 7 | encoding: UTF-8 8 | string: '' 9 | headers: 10 | User-Agent: 11 | - Harvesting Ruby Gem 12 | Authorization: 13 | - Bearer $HARVEST_NON_ADMIN_ACCESS_TOKEN 14 | Harvest-Account-Id: 15 | - "$HARVEST_ACCOUNT_ID" 16 | Connection: 17 | - close 18 | Host: 19 | - api.harvestapp.com 20 | response: 21 | status: 22 | code: 403 23 | message: Forbidden 24 | headers: 25 | Server: 26 | - nginx 27 | Date: 28 | - Fri, 25 Jan 2019 18:32:04 GMT 29 | Content-Type: 30 | - application/json; charset=utf-8 31 | Transfer-Encoding: 32 | - chunked 33 | Connection: 34 | - close 35 | Status: 36 | - 403 Forbidden 37 | X-Frame-Options: 38 | - SAMEORIGIN 39 | X-Xss-Protection: 40 | - 1; mode=block 41 | X-Content-Type-Options: 42 | - nosniff 43 | X-Download-Options: 44 | - noopen 45 | X-Permitted-Cross-Domain-Policies: 46 | - none 47 | Referrer-Policy: 48 | - strict-origin-when-cross-origin 49 | Cache-Control: 50 | - no-cache, no-store 51 | P3p: 52 | - 'CP="Our privacy policy is available online: https://www.getharvest.com/services/privacy-policy"' 53 | X-App-Server: 54 | - app10 55 | X-Robots-Tag: 56 | - noindex, nofollow 57 | Content-Security-Policy: 58 | - 'report-uri /csp_reports; default-src *; img-src * data:; font-src data: cache.harvestapp.com 59 | https://js.intercomcdn.com; script-src ''self'' ''unsafe-inline'' ''unsafe-eval'' 60 | https://*.google-analytics.com https://*.nr-data.net https://ajax.googleapis.com 61 | cache.harvestapp.com https://js-agent.newrelic.com https://platform.twitter.com 62 | https://www.google.com https://www.googleadservices.com https://www.googletagmanager.com 63 | https://connect.facebook.net https://googleads.g.doubleclick.net https://app.intercom.io 64 | https://widget.intercom.io https://js.intercomcdn.com https://cdn.plaid.com; 65 | style-src ''self'' ''unsafe-inline'' cache.harvestapp.com https://www.google.com' 66 | Hint: 67 | - Not authorized! 68 | X-Request-Id: 69 | - d7908a63cf2e2e309c22550e71cfbe24 70 | X-Runtime: 71 | - '0.078422' 72 | Strict-Transport-Security: 73 | - max-age=31536000; includeSubDomains 74 | body: 75 | encoding: UTF-8 76 | string: '{"message":"Not authorized!"}' 77 | http_version: 78 | recorded_at: Fri, 25 Jan 2019 18:32:04 GMT 79 | recorded_with: VCR 4.0.0 80 | -------------------------------------------------------------------------------- /fixtures/vcr_cassettes/Harvesting_Models_Contact_get/provides_direct_access_to_a_specific_contact.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: https://api.harvestapp.com/v2/contacts/7140797 6 | body: 7 | encoding: UTF-8 8 | string: '' 9 | headers: 10 | User-Agent: 11 | - Harvesting Ruby Gem 12 | Authorization: 13 | - Bearer $HARVEST_ACCESS_TOKEN 14 | Harvest-Account-Id: 15 | - "$HARVEST_ACCOUNT_ID" 16 | Connection: 17 | - close 18 | Host: 19 | - api.harvestapp.com 20 | response: 21 | status: 22 | code: 200 23 | message: OK 24 | headers: 25 | Server: 26 | - nginx 27 | Date: 28 | - Fri, 25 Jan 2019 18:32:29 GMT 29 | Content-Type: 30 | - application/json; charset=utf-8 31 | Transfer-Encoding: 32 | - chunked 33 | Connection: 34 | - close 35 | Status: 36 | - 200 OK 37 | X-Frame-Options: 38 | - SAMEORIGIN 39 | X-Xss-Protection: 40 | - 1; mode=block 41 | X-Content-Type-Options: 42 | - nosniff 43 | X-Download-Options: 44 | - noopen 45 | X-Permitted-Cross-Domain-Policies: 46 | - none 47 | Referrer-Policy: 48 | - strict-origin-when-cross-origin 49 | Cache-Control: 50 | - no-cache, no-store 51 | P3p: 52 | - 'CP="Our privacy policy is available online: https://www.getharvest.com/services/privacy-policy"' 53 | X-App-Server: 54 | - app9 55 | X-Robots-Tag: 56 | - noindex, nofollow 57 | Content-Security-Policy: 58 | - 'report-uri /csp_reports; default-src *; img-src * data:; font-src data: cache.harvestapp.com 59 | https://js.intercomcdn.com; script-src ''self'' ''unsafe-inline'' ''unsafe-eval'' 60 | https://*.google-analytics.com https://*.nr-data.net https://ajax.googleapis.com 61 | cache.harvestapp.com https://js-agent.newrelic.com https://platform.twitter.com 62 | https://www.google.com https://www.googleadservices.com https://www.googletagmanager.com 63 | https://connect.facebook.net https://googleads.g.doubleclick.net https://app.intercom.io 64 | https://widget.intercom.io https://js.intercomcdn.com https://cdn.plaid.com; 65 | style-src ''self'' ''unsafe-inline'' cache.harvestapp.com https://www.google.com' 66 | X-Request-Id: 67 | - 1a5980fbd3636bae7583516320d0ee0d 68 | X-Runtime: 69 | - '0.273141' 70 | Strict-Transport-Security: 71 | - max-age=31536000; includeSubDomains 72 | X-Server: 73 | - lb4 74 | body: 75 | encoding: UTF-8 76 | string: '{"id":7140797,"title":null,"first_name":"Jon","last_name":"Snow","email":"","phone_office":"","phone_mobile":"","fax":"","created_at":"2019-01-25T18:31:10Z","updated_at":"2019-01-25T18:31:10Z","client":{"id":7722246,"name":"Pepe"}}' 77 | http_version: 78 | recorded_at: Fri, 25 Jan 2019 18:32:29 GMT 79 | recorded_with: VCR 4.0.0 80 | -------------------------------------------------------------------------------- /fixtures/vcr_cassettes/Harvesting_Models_TimeEntry_create/when_trying_to_create_a_time_entry_without_required_attributes_fails.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: post 5 | uri: https://api.harvestapp.com/v2/time_entries 6 | body: 7 | encoding: UTF-8 8 | string: '{"project_id":null,"task_id":null,"user_id":null}' 9 | headers: 10 | User-Agent: 11 | - Harvesting Ruby Gem 12 | Authorization: 13 | - Bearer $HARVEST_ACCESS_TOKEN 14 | Harvest-Account-Id: 15 | - "$HARVEST_ACCOUNT_ID" 16 | Connection: 17 | - close 18 | Content-Type: 19 | - application/json; charset=UTF-8 20 | Host: 21 | - api.harvestapp.com 22 | response: 23 | status: 24 | code: 422 25 | message: Unprocessable Entity 26 | headers: 27 | Server: 28 | - nginx 29 | Date: 30 | - Fri, 25 Jan 2019 18:32:32 GMT 31 | Content-Type: 32 | - application/json; charset=utf-8 33 | Transfer-Encoding: 34 | - chunked 35 | Connection: 36 | - close 37 | Status: 38 | - 422 Unprocessable Entity 39 | X-Frame-Options: 40 | - SAMEORIGIN 41 | X-Xss-Protection: 42 | - 1; mode=block 43 | X-Content-Type-Options: 44 | - nosniff 45 | X-Download-Options: 46 | - noopen 47 | X-Permitted-Cross-Domain-Policies: 48 | - none 49 | Referrer-Policy: 50 | - strict-origin-when-cross-origin 51 | Cache-Control: 52 | - no-cache, no-store 53 | P3p: 54 | - 'CP="Our privacy policy is available online: https://www.getharvest.com/services/privacy-policy"' 55 | X-App-Server: 56 | - app1 57 | X-Robots-Tag: 58 | - noindex, nofollow 59 | Content-Security-Policy: 60 | - 'report-uri /csp_reports; default-src *; img-src * data:; font-src data: cache.harvestapp.com 61 | https://js.intercomcdn.com; script-src ''self'' ''unsafe-inline'' ''unsafe-eval'' 62 | https://*.google-analytics.com https://*.nr-data.net https://ajax.googleapis.com 63 | cache.harvestapp.com https://js-agent.newrelic.com https://platform.twitter.com 64 | https://www.google.com https://www.googleadservices.com https://www.googletagmanager.com 65 | https://connect.facebook.net https://googleads.g.doubleclick.net https://app.intercom.io 66 | https://widget.intercom.io https://js.intercomcdn.com https://cdn.plaid.com; 67 | style-src ''self'' ''unsafe-inline'' cache.harvestapp.com https://www.google.com' 68 | X-Request-Id: 69 | - 8489a0157ae517fc7849f6385223ccde 70 | X-Runtime: 71 | - '0.130450' 72 | Strict-Transport-Security: 73 | - max-age=31536000; includeSubDomains 74 | body: 75 | encoding: UTF-8 76 | string: '{"message":"Project can''t be blank, Task can''t be blank, Spent date 77 | can''t be blank, Spent date is not a valid date"}' 78 | http_version: 79 | recorded_at: Fri, 25 Jan 2019 18:32:32 GMT 80 | recorded_with: VCR 4.0.0 81 | -------------------------------------------------------------------------------- /fixtures/vcr_cassettes/harvest_data_setup/client_pepe.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: post 5 | uri: https://api.harvestapp.com/v2/clients 6 | body: 7 | encoding: UTF-8 8 | string: '{"name":"Pepe"}' 9 | headers: 10 | User-Agent: 11 | - Harvesting Ruby Gem 12 | Authorization: 13 | - Bearer $HARVEST_ACCESS_TOKEN 14 | Harvest-Account-Id: 15 | - "$HARVEST_ACCOUNT_ID" 16 | Connection: 17 | - close 18 | Content-Type: 19 | - application/json; charset=UTF-8 20 | Host: 21 | - api.harvestapp.com 22 | response: 23 | status: 24 | code: 201 25 | message: Created 26 | headers: 27 | Server: 28 | - nginx 29 | Date: 30 | - Fri, 25 Jan 2019 18:31:09 GMT 31 | Content-Type: 32 | - application/json; charset=utf-8 33 | Transfer-Encoding: 34 | - chunked 35 | Connection: 36 | - close 37 | Status: 38 | - 201 Created 39 | X-Frame-Options: 40 | - SAMEORIGIN 41 | X-Xss-Protection: 42 | - 1; mode=block 43 | X-Content-Type-Options: 44 | - nosniff 45 | X-Download-Options: 46 | - noopen 47 | X-Permitted-Cross-Domain-Policies: 48 | - none 49 | Referrer-Policy: 50 | - strict-origin-when-cross-origin 51 | Cache-Control: 52 | - no-cache, no-store 53 | P3p: 54 | - 'CP="Our privacy policy is available online: https://www.getharvest.com/services/privacy-policy"' 55 | X-App-Server: 56 | - app20 57 | X-Robots-Tag: 58 | - noindex, nofollow 59 | Content-Security-Policy: 60 | - 'report-uri /csp_reports; default-src *; img-src * data:; font-src data: cache.harvestapp.com 61 | https://js.intercomcdn.com; script-src ''self'' ''unsafe-inline'' ''unsafe-eval'' 62 | https://*.google-analytics.com https://*.nr-data.net https://ajax.googleapis.com 63 | cache.harvestapp.com https://js-agent.newrelic.com https://platform.twitter.com 64 | https://www.google.com https://www.googleadservices.com https://www.googletagmanager.com 65 | https://connect.facebook.net https://googleads.g.doubleclick.net https://app.intercom.io 66 | https://widget.intercom.io https://js.intercomcdn.com https://cdn.plaid.com; 67 | style-src ''self'' ''unsafe-inline'' cache.harvestapp.com https://www.google.com' 68 | Location: 69 | - https://api.harvestapp.com/api/v2/clients/7722246 70 | X-Request-Id: 71 | - 11fa5d27cb09eaa66c2c416350bb107e 72 | X-Runtime: 73 | - '0.509430' 74 | Strict-Transport-Security: 75 | - max-age=31536000; includeSubDomains 76 | X-Server: 77 | - lb2 78 | body: 79 | encoding: UTF-8 80 | string: '{"id":7722246,"name":"Pepe","is_active":true,"address":null,"created_at":"2019-01-25T18:31:09Z","updated_at":"2019-01-25T18:31:09Z","currency":"USD"}' 81 | http_version: 82 | recorded_at: Fri, 25 Jan 2019 18:31:09 GMT 83 | recorded_with: VCR 4.0.0 84 | -------------------------------------------------------------------------------- /fixtures/vcr_cassettes/harvest_data_setup/client_toto.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: post 5 | uri: https://api.harvestapp.com/v2/clients 6 | body: 7 | encoding: UTF-8 8 | string: '{"name":"Toto"}' 9 | headers: 10 | User-Agent: 11 | - Harvesting Ruby Gem 12 | Authorization: 13 | - Bearer $HARVEST_ACCESS_TOKEN 14 | Harvest-Account-Id: 15 | - "$HARVEST_ACCOUNT_ID" 16 | Connection: 17 | - close 18 | Content-Type: 19 | - application/json; charset=UTF-8 20 | Host: 21 | - api.harvestapp.com 22 | response: 23 | status: 24 | code: 201 25 | message: Created 26 | headers: 27 | Server: 28 | - nginx 29 | Date: 30 | - Fri, 25 Jan 2019 18:31:09 GMT 31 | Content-Type: 32 | - application/json; charset=utf-8 33 | Transfer-Encoding: 34 | - chunked 35 | Connection: 36 | - close 37 | Status: 38 | - 201 Created 39 | X-Frame-Options: 40 | - SAMEORIGIN 41 | X-Xss-Protection: 42 | - 1; mode=block 43 | X-Content-Type-Options: 44 | - nosniff 45 | X-Download-Options: 46 | - noopen 47 | X-Permitted-Cross-Domain-Policies: 48 | - none 49 | Referrer-Policy: 50 | - strict-origin-when-cross-origin 51 | Cache-Control: 52 | - no-cache, no-store 53 | P3p: 54 | - 'CP="Our privacy policy is available online: https://www.getharvest.com/services/privacy-policy"' 55 | X-App-Server: 56 | - app19 57 | X-Robots-Tag: 58 | - noindex, nofollow 59 | Content-Security-Policy: 60 | - 'report-uri /csp_reports; default-src *; img-src * data:; font-src data: cache.harvestapp.com 61 | https://js.intercomcdn.com; script-src ''self'' ''unsafe-inline'' ''unsafe-eval'' 62 | https://*.google-analytics.com https://*.nr-data.net https://ajax.googleapis.com 63 | cache.harvestapp.com https://js-agent.newrelic.com https://platform.twitter.com 64 | https://www.google.com https://www.googleadservices.com https://www.googletagmanager.com 65 | https://connect.facebook.net https://googleads.g.doubleclick.net https://app.intercom.io 66 | https://widget.intercom.io https://js.intercomcdn.com https://cdn.plaid.com; 67 | style-src ''self'' ''unsafe-inline'' cache.harvestapp.com https://www.google.com' 68 | Location: 69 | - https://api.harvestapp.com/api/v2/clients/7722247 70 | X-Request-Id: 71 | - 0da9c29178f54aa642af96f245ddc326 72 | X-Runtime: 73 | - '0.039715' 74 | Strict-Transport-Security: 75 | - max-age=31536000; includeSubDomains 76 | X-Server: 77 | - lb2 78 | body: 79 | encoding: UTF-8 80 | string: '{"id":7722247,"name":"Toto","is_active":true,"address":null,"created_at":"2019-01-25T18:31:09Z","updated_at":"2019-01-25T18:31:09Z","currency":"USD"}' 81 | http_version: 82 | recorded_at: Fri, 25 Jan 2019 18:31:09 GMT 83 | recorded_with: VCR 4.0.0 84 | -------------------------------------------------------------------------------- /fixtures/vcr_cassettes/harvest_data_setup/task_coding.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: post 5 | uri: https://api.harvestapp.com/v2/tasks 6 | body: 7 | encoding: UTF-8 8 | string: '{"name":"Coding"}' 9 | headers: 10 | User-Agent: 11 | - Harvesting Ruby Gem 12 | Authorization: 13 | - Bearer $HARVEST_ACCESS_TOKEN 14 | Harvest-Account-Id: 15 | - "$HARVEST_ACCOUNT_ID" 16 | Connection: 17 | - close 18 | Content-Type: 19 | - application/json; charset=UTF-8 20 | Host: 21 | - api.harvestapp.com 22 | response: 23 | status: 24 | code: 201 25 | message: Created 26 | headers: 27 | Server: 28 | - nginx 29 | Date: 30 | - Fri, 25 Jan 2019 18:31:11 GMT 31 | Content-Type: 32 | - application/json; charset=utf-8 33 | Transfer-Encoding: 34 | - chunked 35 | Connection: 36 | - close 37 | Status: 38 | - 201 Created 39 | X-Frame-Options: 40 | - SAMEORIGIN 41 | X-Xss-Protection: 42 | - 1; mode=block 43 | X-Content-Type-Options: 44 | - nosniff 45 | X-Download-Options: 46 | - noopen 47 | X-Permitted-Cross-Domain-Policies: 48 | - none 49 | Referrer-Policy: 50 | - strict-origin-when-cross-origin 51 | Cache-Control: 52 | - no-cache, no-store 53 | P3p: 54 | - 'CP="Our privacy policy is available online: https://www.getharvest.com/services/privacy-policy"' 55 | X-App-Server: 56 | - app9 57 | X-Robots-Tag: 58 | - noindex, nofollow 59 | Content-Security-Policy: 60 | - 'report-uri /csp_reports; default-src *; img-src * data:; font-src data: cache.harvestapp.com 61 | https://js.intercomcdn.com; script-src ''self'' ''unsafe-inline'' ''unsafe-eval'' 62 | https://*.google-analytics.com https://*.nr-data.net https://ajax.googleapis.com 63 | cache.harvestapp.com https://js-agent.newrelic.com https://platform.twitter.com 64 | https://www.google.com https://www.googleadservices.com https://www.googletagmanager.com 65 | https://connect.facebook.net https://googleads.g.doubleclick.net https://app.intercom.io 66 | https://widget.intercom.io https://js.intercomcdn.com https://cdn.plaid.com; 67 | style-src ''self'' ''unsafe-inline'' cache.harvestapp.com https://www.google.com' 68 | Location: 69 | - https://api.harvestapp.com/api/v2/tasks/11399145 70 | X-Request-Id: 71 | - f27ddbdd49cb53010b5fb18ae1d90135 72 | X-Runtime: 73 | - '0.109184' 74 | Strict-Transport-Security: 75 | - max-age=31536000; includeSubDomains 76 | X-Server: 77 | - lb2 78 | body: 79 | encoding: UTF-8 80 | string: '{"id":11399145,"name":"Coding","billable_by_default":true,"default_hourly_rate":null,"is_default":false,"is_active":true,"created_at":"2019-01-25T18:31:11Z","updated_at":"2019-01-25T18:31:11Z"}' 81 | http_version: 82 | recorded_at: Fri, 25 Jan 2019 18:31:11 GMT 83 | recorded_with: VCR 4.0.0 84 | -------------------------------------------------------------------------------- /fixtures/vcr_cassettes/harvest_data_setup/task_writing.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: post 5 | uri: https://api.harvestapp.com/v2/tasks 6 | body: 7 | encoding: UTF-8 8 | string: '{"name":"Writing"}' 9 | headers: 10 | User-Agent: 11 | - Harvesting Ruby Gem 12 | Authorization: 13 | - Bearer $HARVEST_ACCESS_TOKEN 14 | Harvest-Account-Id: 15 | - "$HARVEST_ACCOUNT_ID" 16 | Connection: 17 | - close 18 | Content-Type: 19 | - application/json; charset=UTF-8 20 | Host: 21 | - api.harvestapp.com 22 | response: 23 | status: 24 | code: 201 25 | message: Created 26 | headers: 27 | Server: 28 | - nginx 29 | Date: 30 | - Fri, 25 Jan 2019 19:05:24 GMT 31 | Content-Type: 32 | - application/json; charset=utf-8 33 | Transfer-Encoding: 34 | - chunked 35 | Connection: 36 | - close 37 | Status: 38 | - 201 Created 39 | X-Frame-Options: 40 | - SAMEORIGIN 41 | X-Xss-Protection: 42 | - 1; mode=block 43 | X-Content-Type-Options: 44 | - nosniff 45 | X-Download-Options: 46 | - noopen 47 | X-Permitted-Cross-Domain-Policies: 48 | - none 49 | Referrer-Policy: 50 | - strict-origin-when-cross-origin 51 | Cache-Control: 52 | - no-cache, no-store 53 | P3p: 54 | - 'CP="Our privacy policy is available online: https://www.getharvest.com/services/privacy-policy"' 55 | X-App-Server: 56 | - app4 57 | X-Robots-Tag: 58 | - noindex, nofollow 59 | Content-Security-Policy: 60 | - 'report-uri /csp_reports; default-src *; img-src * data:; font-src data: cache.harvestapp.com 61 | https://js.intercomcdn.com; script-src ''self'' ''unsafe-inline'' ''unsafe-eval'' 62 | https://*.google-analytics.com https://*.nr-data.net https://ajax.googleapis.com 63 | cache.harvestapp.com https://js-agent.newrelic.com https://platform.twitter.com 64 | https://www.google.com https://www.googleadservices.com https://www.googletagmanager.com 65 | https://connect.facebook.net https://googleads.g.doubleclick.net https://app.intercom.io 66 | https://widget.intercom.io https://js.intercomcdn.com https://cdn.plaid.com; 67 | style-src ''self'' ''unsafe-inline'' cache.harvestapp.com https://www.google.com' 68 | Location: 69 | - https://api.harvestapp.com/api/v2/tasks/11399470 70 | X-Request-Id: 71 | - 360afc8704a4c05423bc40eaeffc8b17 72 | X-Runtime: 73 | - '0.130892' 74 | Strict-Transport-Security: 75 | - max-age=31536000; includeSubDomains 76 | X-Server: 77 | - lb2 78 | body: 79 | encoding: UTF-8 80 | string: '{"id":11399470,"name":"Writing","billable_by_default":true,"default_hourly_rate":null,"is_default":false,"is_active":true,"created_at":"2019-01-25T19:05:24Z","updated_at":"2019-01-25T19:05:24Z"}' 81 | http_version: 82 | recorded_at: Fri, 25 Jan 2019 19:05:24 GMT 83 | recorded_with: VCR 4.0.0 84 | -------------------------------------------------------------------------------- /fixtures/vcr_cassettes/Harvesting_Client_time_entries/when_account_has_no_entries_returns_the_time_entries_associated_with_the_account.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: https://api.harvestapp.com/v2/time_entries 6 | body: 7 | encoding: UTF-8 8 | string: '' 9 | headers: 10 | User-Agent: 11 | - Harvesting Ruby Gem 12 | Authorization: 13 | - Bearer $HARVEST_NON_ADMIN_ACCESS_TOKEN 14 | Harvest-Account-Id: 15 | - "$HARVEST_ACCOUNT_ID" 16 | Connection: 17 | - close 18 | Host: 19 | - api.harvestapp.com 20 | response: 21 | status: 22 | code: 200 23 | message: OK 24 | headers: 25 | Server: 26 | - nginx 27 | Date: 28 | - Fri, 25 Jan 2019 18:32:10 GMT 29 | Content-Type: 30 | - application/json; charset=utf-8 31 | Transfer-Encoding: 32 | - chunked 33 | Connection: 34 | - close 35 | Status: 36 | - 200 OK 37 | X-Frame-Options: 38 | - SAMEORIGIN 39 | X-Xss-Protection: 40 | - 1; mode=block 41 | X-Content-Type-Options: 42 | - nosniff 43 | X-Download-Options: 44 | - noopen 45 | X-Permitted-Cross-Domain-Policies: 46 | - none 47 | Referrer-Policy: 48 | - strict-origin-when-cross-origin 49 | Cache-Control: 50 | - no-cache, no-store 51 | P3p: 52 | - 'CP="Our privacy policy is available online: https://www.getharvest.com/services/privacy-policy"' 53 | X-App-Server: 54 | - app20 55 | X-Robots-Tag: 56 | - noindex, nofollow 57 | Content-Security-Policy: 58 | - 'report-uri /csp_reports; default-src *; img-src * data:; font-src data: cache.harvestapp.com 59 | https://js.intercomcdn.com; script-src ''self'' ''unsafe-inline'' ''unsafe-eval'' 60 | https://*.google-analytics.com https://*.nr-data.net https://ajax.googleapis.com 61 | cache.harvestapp.com https://js-agent.newrelic.com https://platform.twitter.com 62 | https://www.google.com https://www.googleadservices.com https://www.googletagmanager.com 63 | https://connect.facebook.net https://googleads.g.doubleclick.net https://app.intercom.io 64 | https://widget.intercom.io https://js.intercomcdn.com https://cdn.plaid.com; 65 | style-src ''self'' ''unsafe-inline'' cache.harvestapp.com https://www.google.com' 66 | X-Request-Id: 67 | - '091fc0d42eaa40fd41e66a1478c3863c' 68 | X-Runtime: 69 | - '0.045590' 70 | Strict-Transport-Security: 71 | - max-age=31536000; includeSubDomains 72 | X-Server: 73 | - lb4 74 | body: 75 | encoding: UTF-8 76 | string: '{"time_entries":[],"per_page":100,"total_pages":1,"total_entries":0,"next_page":null,"previous_page":null,"page":1,"links":{"first":"https://api.harvestapp.com/v2/time_entries?page=1&per_page=100","next":null,"previous":null,"last":"https://api.harvestapp.com/v2/time_entries?page=1&per_page=100"}}' 77 | http_version: 78 | recorded_at: Fri, 25 Jan 2019 18:32:10 GMT 79 | recorded_with: VCR 4.0.0 80 | -------------------------------------------------------------------------------- /fixtures/vcr_cassettes/harvest_data_setup/contact_jon_snow.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: post 5 | uri: https://api.harvestapp.com/v2/contacts 6 | body: 7 | encoding: UTF-8 8 | string: '{"client_id":"7722246","first_name":"Jon","last_name":"Snow","client":{"id":"7722246"}}' 9 | headers: 10 | User-Agent: 11 | - Harvesting Ruby Gem 12 | Authorization: 13 | - Bearer $HARVEST_ACCESS_TOKEN 14 | Harvest-Account-Id: 15 | - "$HARVEST_ACCOUNT_ID" 16 | Connection: 17 | - close 18 | Content-Type: 19 | - application/json; charset=UTF-8 20 | Host: 21 | - api.harvestapp.com 22 | response: 23 | status: 24 | code: 201 25 | message: Created 26 | headers: 27 | Server: 28 | - nginx 29 | Date: 30 | - Fri, 25 Jan 2019 18:31:10 GMT 31 | Content-Type: 32 | - application/json; charset=utf-8 33 | Transfer-Encoding: 34 | - chunked 35 | Connection: 36 | - close 37 | Status: 38 | - 201 Created 39 | X-Frame-Options: 40 | - SAMEORIGIN 41 | X-Xss-Protection: 42 | - 1; mode=block 43 | X-Content-Type-Options: 44 | - nosniff 45 | X-Download-Options: 46 | - noopen 47 | X-Permitted-Cross-Domain-Policies: 48 | - none 49 | Referrer-Policy: 50 | - strict-origin-when-cross-origin 51 | Cache-Control: 52 | - no-cache, no-store 53 | P3p: 54 | - 'CP="Our privacy policy is available online: https://www.getharvest.com/services/privacy-policy"' 55 | X-App-Server: 56 | - app3 57 | X-Robots-Tag: 58 | - noindex, nofollow 59 | Content-Security-Policy: 60 | - 'report-uri /csp_reports; default-src *; img-src * data:; font-src data: cache.harvestapp.com 61 | https://js.intercomcdn.com; script-src ''self'' ''unsafe-inline'' ''unsafe-eval'' 62 | https://*.google-analytics.com https://*.nr-data.net https://ajax.googleapis.com 63 | cache.harvestapp.com https://js-agent.newrelic.com https://platform.twitter.com 64 | https://www.google.com https://www.googleadservices.com https://www.googletagmanager.com 65 | https://connect.facebook.net https://googleads.g.doubleclick.net https://app.intercom.io 66 | https://widget.intercom.io https://js.intercomcdn.com https://cdn.plaid.com; 67 | style-src ''self'' ''unsafe-inline'' cache.harvestapp.com https://www.google.com' 68 | Location: 69 | - https://api.harvestapp.com/api/v2/contacts/7140797 70 | X-Request-Id: 71 | - a385b739bf88869d3066ccefb1cd873b 72 | X-Runtime: 73 | - '0.174258' 74 | Strict-Transport-Security: 75 | - max-age=31536000; includeSubDomains 76 | X-Server: 77 | - lb2 78 | body: 79 | encoding: UTF-8 80 | string: '{"id":7140797,"title":null,"first_name":"Jon","last_name":"Snow","email":"","phone_office":"","phone_mobile":"","fax":"","created_at":"2019-01-25T18:31:10Z","updated_at":"2019-01-25T18:31:10Z","client":{"id":7722246,"name":"Pepe"}}' 81 | http_version: 82 | recorded_at: Fri, 25 Jan 2019 18:31:10 GMT 83 | recorded_with: VCR 4.0.0 84 | -------------------------------------------------------------------------------- /fixtures/vcr_cassettes/harvest_data_setup/contact_cersei_lannister.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: post 5 | uri: https://api.harvestapp.com/v2/contacts 6 | body: 7 | encoding: UTF-8 8 | string: '{"client_id":"7722247","first_name":"Cersei","last_name":"Lannister","client":{"id":"7722247"}}' 9 | headers: 10 | User-Agent: 11 | - Harvesting Ruby Gem 12 | Authorization: 13 | - Bearer $HARVEST_ACCESS_TOKEN 14 | Harvest-Account-Id: 15 | - "$HARVEST_ACCOUNT_ID" 16 | Connection: 17 | - close 18 | Content-Type: 19 | - application/json; charset=UTF-8 20 | Host: 21 | - api.harvestapp.com 22 | response: 23 | status: 24 | code: 201 25 | message: Created 26 | headers: 27 | Server: 28 | - nginx 29 | Date: 30 | - Fri, 25 Jan 2019 18:31:12 GMT 31 | Content-Type: 32 | - application/json; charset=utf-8 33 | Transfer-Encoding: 34 | - chunked 35 | Connection: 36 | - close 37 | Status: 38 | - 201 Created 39 | X-Frame-Options: 40 | - SAMEORIGIN 41 | X-Xss-Protection: 42 | - 1; mode=block 43 | X-Content-Type-Options: 44 | - nosniff 45 | X-Download-Options: 46 | - noopen 47 | X-Permitted-Cross-Domain-Policies: 48 | - none 49 | Referrer-Policy: 50 | - strict-origin-when-cross-origin 51 | Cache-Control: 52 | - no-cache, no-store 53 | P3p: 54 | - 'CP="Our privacy policy is available online: https://www.getharvest.com/services/privacy-policy"' 55 | X-App-Server: 56 | - app6 57 | X-Robots-Tag: 58 | - noindex, nofollow 59 | Content-Security-Policy: 60 | - 'report-uri /csp_reports; default-src *; img-src * data:; font-src data: cache.harvestapp.com 61 | https://js.intercomcdn.com; script-src ''self'' ''unsafe-inline'' ''unsafe-eval'' 62 | https://*.google-analytics.com https://*.nr-data.net https://ajax.googleapis.com 63 | cache.harvestapp.com https://js-agent.newrelic.com https://platform.twitter.com 64 | https://www.google.com https://www.googleadservices.com https://www.googletagmanager.com 65 | https://connect.facebook.net https://googleads.g.doubleclick.net https://app.intercom.io 66 | https://widget.intercom.io https://js.intercomcdn.com https://cdn.plaid.com; 67 | style-src ''self'' ''unsafe-inline'' cache.harvestapp.com https://www.google.com' 68 | Location: 69 | - https://api.harvestapp.com/api/v2/contacts/7140798 70 | X-Request-Id: 71 | - 52dc839751ebb20d58fffc52c60caa1c 72 | X-Runtime: 73 | - '0.191520' 74 | Strict-Transport-Security: 75 | - max-age=31536000; includeSubDomains 76 | X-Server: 77 | - lb2 78 | body: 79 | encoding: UTF-8 80 | string: '{"id":7140798,"title":null,"first_name":"Cersei","last_name":"Lannister","email":"","phone_office":"","phone_mobile":"","fax":"","created_at":"2019-01-25T18:31:12Z","updated_at":"2019-01-25T18:31:12Z","client":{"id":7722247,"name":"Toto"}}' 81 | http_version: 82 | recorded_at: Fri, 25 Jan 2019 18:31:12 GMT 83 | recorded_with: VCR 4.0.0 84 | -------------------------------------------------------------------------------- /fixtures/vcr_cassettes/Harvesting_Client_invoices/when_account_has_no_invoices_returns_the_invoices_associated_with_the_account.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: https://api.harvestapp.com/v2/invoices 6 | body: 7 | encoding: UTF-8 8 | string: '' 9 | headers: 10 | User-Agent: 11 | - Harvesting Ruby Gem 12 | Authorization: 13 | - Bearer $HARVEST_ACCESS_TOKEN 14 | Harvest-Account-Id: 15 | - "$HARVEST_ACCOUNT_ID" 16 | Connection: 17 | - close 18 | Host: 19 | - api.harvestapp.com 20 | response: 21 | status: 22 | code: 200 23 | message: OK 24 | headers: 25 | Server: 26 | - nginx 27 | Date: 28 | - Tue, 07 Apr 2020 07:22:11 GMT 29 | Content-Type: 30 | - application/json; charset=utf-8 31 | Transfer-Encoding: 32 | - chunked 33 | Connection: 34 | - close 35 | Status: 36 | - 200 OK 37 | X-Frame-Options: 38 | - SAMEORIGIN 39 | X-Xss-Protection: 40 | - 1; mode=block 41 | X-Content-Type-Options: 42 | - nosniff 43 | X-Download-Options: 44 | - noopen 45 | X-Permitted-Cross-Domain-Policies: 46 | - none 47 | Referrer-Policy: 48 | - strict-origin-when-cross-origin 49 | Cache-Control: 50 | - no-cache, no-store 51 | P3p: 52 | - 'CP="Our privacy policy is available online: https://www.getharvest.com/services/privacy-policy"' 53 | X-App-Server: 54 | - app4 55 | X-Robots-Tag: 56 | - noindex, nofollow 57 | Content-Security-Policy: 58 | - 'report-uri /csp_reports; default-src *; img-src * data:; font-src data: cache.harvestapp.com 59 | https://js.intercomcdn.com https://fonts.gstatic.com; script-src ''self'' 60 | ''unsafe-inline'' ''unsafe-eval'' https://*.google-analytics.com https://*.nr-data.net 61 | https://ajax.googleapis.com cache.harvestapp.com https://platform.twitter.com 62 | https://www.google.com https://www.googleadservices.com https://www.googletagmanager.com 63 | https://connect.facebook.net https://googleads.g.doubleclick.net https://app.intercom.io 64 | https://widget.intercom.io https://js.intercomcdn.com https://cdn.plaid.com 65 | https://tagmanager.google.com https://bat.bing.com https://ct.capterra.com; 66 | style-src ''self'' ''unsafe-inline'' cache.harvestapp.com https://www.google.com 67 | https://tagmanager.google.com https://fonts.googleapis.com' 68 | X-Request-Id: 69 | - e0809334fe373e041de63b1486b89854 70 | X-Runtime: 71 | - '0.069310' 72 | Strict-Transport-Security: 73 | - max-age=31536000; includeSubDomains 74 | X-Server: 75 | - lb4 76 | body: 77 | encoding: UTF-8 78 | string: '{"invoices":[],"per_page":100,"total_pages":1,"total_entries":0,"next_page":null,"previous_page":null,"page":1,"links":{"first":"https://api.harvestapp.com/v2/invoices?page=1&per_page=100","next":null,"previous":null,"last":"https://api.harvestapp.com/v2/invoices?page=1&per_page=100"}}' 79 | http_version: 80 | recorded_at: Tue, 07 Apr 2020 07:22:11 GMT 81 | recorded_with: VCR 4.0.0 82 | -------------------------------------------------------------------------------- /fixtures/vcr_cassettes/Harvesting_Models_ProjectUserAssignment_create/with_a_project_id_creates_the_user_assignment.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: post 5 | uri: https://api.harvestapp.com/v2/projects/1234/user_assignments 6 | body: 7 | encoding: UTF-8 8 | string: '{"project_id":"1234","user_id":"5678","project":{"id":"1234"},"user":{"id":"5678"}}' 9 | headers: 10 | User-Agent: 11 | - Harvesting Ruby Gem 12 | Authorization: 13 | - Bearer $HARVEST_ACCESS_TOKEN 14 | Harvest-Account-Id: 15 | - "$HARVEST_ACCOUNT_ID" 16 | Connection: 17 | - close 18 | Content-Type: 19 | - application/json; charset=UTF-8 20 | Host: 21 | - api.harvestapp.com 22 | response: 23 | status: 24 | code: 201 25 | message: '' 26 | headers: 27 | Server: 28 | - nginx 29 | Date: 30 | - Fri, 21 Aug 2020 20:25:17 GMT 31 | Content-Type: 32 | - application/json; charset=utf-8 33 | Transfer-Encoding: 34 | - chunked 35 | Connection: 36 | - close 37 | Status: 38 | - 404 Not Found 39 | X-Frame-Options: 40 | - SAMEORIGIN 41 | X-Xss-Protection: 42 | - 1; mode=block 43 | X-Content-Type-Options: 44 | - nosniff 45 | X-Download-Options: 46 | - noopen 47 | X-Permitted-Cross-Domain-Policies: 48 | - none 49 | Referrer-Policy: 50 | - strict-origin-when-cross-origin 51 | Cache-Control: 52 | - no-cache, no-store 53 | P3p: 54 | - 'CP="Our privacy policy is available online: https://www.getharvest.com/services/privacy-policy"' 55 | X-App-Server: 56 | - app1 57 | X-Robots-Tag: 58 | - noindex, nofollow 59 | Content-Security-Policy: 60 | - 'report-uri /csp_reports; default-src *; img-src * data:; font-src data: cache.harvestapp.com 61 | https://fonts.gstatic.com; script-src ''self'' ''unsafe-inline'' ''unsafe-eval'' 62 | https://*.google-analytics.com https://*.nr-data.net https://ajax.googleapis.com 63 | cache.harvestapp.com https://platform.twitter.com https://www.google.com https://www.googleadservices.com 64 | https://www.googletagmanager.com https://connect.facebook.net https://googleads.g.doubleclick.net 65 | https://cdn.plaid.com https://tagmanager.google.com https://bat.bing.com https://ct.capterra.com; 66 | style-src ''self'' ''unsafe-inline'' cache.harvestapp.com https://www.google.com 67 | https://tagmanager.google.com https://fonts.googleapis.com' 68 | Hint: 69 | - '' 70 | X-Request-Id: 71 | - 2efcf4ee8159d6a10335bc006dd1a214 72 | X-Runtime: 73 | - '0.044372' 74 | Strict-Transport-Security: 75 | - max-age=31536000; includeSubDomains 76 | body: 77 | encoding: UTF-8 78 | string: '{"id":125068758,"is_project_manager":false,"is_active":true,"use_default_rates":false,"budget":null,"created_at":"2017-06-26T22:36:01Z","updated_at":"2017-06-26T22:36:01Z","hourly_rate":75.5,"project":{"id":1234,"name":"Online Store - Phase 1","code":"OS1"},"user":{"id":5678,"name":"Jim Allen"}}' 79 | http_version: 80 | recorded_at: Fri, 21 Aug 2020 20:25:17 GMT 81 | recorded_with: VCR 4.0.0 82 | -------------------------------------------------------------------------------- /fixtures/vcr_cassettes/harvest_data_setup/user_me.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: https://api.harvestapp.com/v2/users/me 6 | body: 7 | encoding: UTF-8 8 | string: '' 9 | headers: 10 | User-Agent: 11 | - Harvesting Ruby Gem 12 | Authorization: 13 | - Bearer $HARVEST_ACCESS_TOKEN 14 | Harvest-Account-Id: 15 | - "$HARVEST_ACCOUNT_ID" 16 | Connection: 17 | - close 18 | Host: 19 | - api.harvestapp.com 20 | response: 21 | status: 22 | code: 200 23 | message: OK 24 | headers: 25 | Server: 26 | - nginx 27 | Date: 28 | - Fri, 25 Jan 2019 18:31:09 GMT 29 | Content-Type: 30 | - application/json; charset=utf-8 31 | Transfer-Encoding: 32 | - chunked 33 | Connection: 34 | - close 35 | Status: 36 | - 200 OK 37 | X-Frame-Options: 38 | - SAMEORIGIN 39 | X-Xss-Protection: 40 | - 1; mode=block 41 | X-Content-Type-Options: 42 | - nosniff 43 | X-Download-Options: 44 | - noopen 45 | X-Permitted-Cross-Domain-Policies: 46 | - none 47 | Referrer-Policy: 48 | - strict-origin-when-cross-origin 49 | Cache-Control: 50 | - no-cache, no-store 51 | P3p: 52 | - 'CP="Our privacy policy is available online: https://www.getharvest.com/services/privacy-policy"' 53 | X-App-Server: 54 | - app21 55 | X-Robots-Tag: 56 | - noindex, nofollow 57 | Content-Security-Policy: 58 | - 'report-uri /csp_reports; default-src *; img-src * data:; font-src data: cache.harvestapp.com 59 | https://js.intercomcdn.com; script-src ''self'' ''unsafe-inline'' ''unsafe-eval'' 60 | https://*.google-analytics.com https://*.nr-data.net https://ajax.googleapis.com 61 | cache.harvestapp.com https://js-agent.newrelic.com https://platform.twitter.com 62 | https://www.google.com https://www.googleadservices.com https://www.googletagmanager.com 63 | https://connect.facebook.net https://googleads.g.doubleclick.net https://app.intercom.io 64 | https://widget.intercom.io https://js.intercomcdn.com https://cdn.plaid.com; 65 | style-src ''self'' ''unsafe-inline'' cache.harvestapp.com https://www.google.com' 66 | X-Request-Id: 67 | - fdfbaa402859e2128927bbcefea56e37 68 | X-Runtime: 69 | - '0.029421' 70 | Strict-Transport-Security: 71 | - max-age=31536000; includeSubDomains 72 | X-Server: 73 | - lb2 74 | body: 75 | encoding: UTF-8 76 | string: '{"id":2348607,"first_name":"Alexander","last_name":"Hamilton","email":"scott+harvested@corgibytes.com","telephone":"","timezone":"Eastern 77 | Time (US & Canada)","weekly_capacity":126000,"has_access_to_all_future_projects":false,"is_contractor":false,"is_admin":true,"is_project_manager":false,"can_see_rates":true,"can_create_projects":true,"can_create_invoices":true,"is_active":true,"created_at":"2018-09-13T15:00:54Z","updated_at":"2019-01-14T03:42:13Z","default_hourly_rate":null,"cost_rate":null,"roles":[],"avatar_url":"https://cache.harvestapp.com/assets/profile_images/big_ben.png?1536850854"}' 78 | http_version: 79 | recorded_at: Fri, 25 Jan 2019 18:31:09 GMT 80 | recorded_with: VCR 4.0.0 81 | -------------------------------------------------------------------------------- /fixtures/vcr_cassettes/Harvesting_Client_me/returns_the_authenticated_user.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: https://api.harvestapp.com/v2/users/me 6 | body: 7 | encoding: UTF-8 8 | string: '' 9 | headers: 10 | User-Agent: 11 | - Harvesting Ruby Gem 12 | Authorization: 13 | - Bearer $HARVEST_NON_ADMIN_ACCESS_TOKEN 14 | Harvest-Account-Id: 15 | - "$HARVEST_ACCOUNT_ID" 16 | Connection: 17 | - close 18 | Host: 19 | - api.harvestapp.com 20 | response: 21 | status: 22 | code: 200 23 | message: OK 24 | headers: 25 | Server: 26 | - nginx 27 | Date: 28 | - Fri, 25 Jan 2019 18:32:09 GMT 29 | Content-Type: 30 | - application/json; charset=utf-8 31 | Transfer-Encoding: 32 | - chunked 33 | Connection: 34 | - close 35 | Status: 36 | - 200 OK 37 | X-Frame-Options: 38 | - SAMEORIGIN 39 | X-Xss-Protection: 40 | - 1; mode=block 41 | X-Content-Type-Options: 42 | - nosniff 43 | X-Download-Options: 44 | - noopen 45 | X-Permitted-Cross-Domain-Policies: 46 | - none 47 | Referrer-Policy: 48 | - strict-origin-when-cross-origin 49 | Cache-Control: 50 | - no-cache, no-store 51 | P3p: 52 | - 'CP="Our privacy policy is available online: https://www.getharvest.com/services/privacy-policy"' 53 | X-App-Server: 54 | - app9 55 | X-Robots-Tag: 56 | - noindex, nofollow 57 | Content-Security-Policy: 58 | - 'report-uri /csp_reports; default-src *; img-src * data:; font-src data: cache.harvestapp.com 59 | https://js.intercomcdn.com; script-src ''self'' ''unsafe-inline'' ''unsafe-eval'' 60 | https://*.google-analytics.com https://*.nr-data.net https://ajax.googleapis.com 61 | cache.harvestapp.com https://js-agent.newrelic.com https://platform.twitter.com 62 | https://www.google.com https://www.googleadservices.com https://www.googletagmanager.com 63 | https://connect.facebook.net https://googleads.g.doubleclick.net https://app.intercom.io 64 | https://widget.intercom.io https://js.intercomcdn.com https://cdn.plaid.com; 65 | style-src ''self'' ''unsafe-inline'' cache.harvestapp.com https://www.google.com' 66 | X-Request-Id: 67 | - fbefb0ec67ea29ac293218dd24debdbe 68 | X-Runtime: 69 | - '0.057106' 70 | Strict-Transport-Security: 71 | - max-age=31536000; includeSubDomains 72 | X-Server: 73 | - lb4 74 | body: 75 | encoding: UTF-8 76 | string: '{"id":2516167,"first_name":"Aaron","last_name":"Burr","email":"scott+harvested-employee@corgibytes.com","telephone":"","timezone":"Eastern 77 | Time (US & Canada)","weekly_capacity":126000,"has_access_to_all_future_projects":false,"is_contractor":false,"is_admin":false,"is_project_manager":false,"can_see_rates":false,"can_create_projects":false,"can_create_invoices":false,"is_active":true,"created_at":"2019-01-18T21:21:18Z","updated_at":"2019-01-18T21:25:16Z","roles":[],"avatar_url":"https://cache.harvestapp.com/assets/profile_images/abraj_albait_towers.png?1547846478"}' 78 | http_version: 79 | recorded_at: Fri, 25 Jan 2019 18:32:09 GMT 80 | recorded_with: VCR 4.0.0 81 | -------------------------------------------------------------------------------- /fixtures/vcr_cassettes/Harvesting_Models_Project_get/provides_direct_access_to_a_specific_project.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: https://api.harvestapp.com/v2/projects/19815868 6 | body: 7 | encoding: UTF-8 8 | string: '' 9 | headers: 10 | User-Agent: 11 | - Harvesting Ruby Gem 12 | Authorization: 13 | - Bearer $HARVEST_ACCESS_TOKEN 14 | Harvest-Account-Id: 15 | - "$HARVEST_ACCOUNT_ID" 16 | Connection: 17 | - close 18 | Host: 19 | - api.harvestapp.com 20 | response: 21 | status: 22 | code: 200 23 | message: OK 24 | headers: 25 | Server: 26 | - nginx 27 | Date: 28 | - Thu, 17 Jan 2019 15:42:32 GMT 29 | Content-Type: 30 | - application/json; charset=utf-8 31 | Transfer-Encoding: 32 | - chunked 33 | Connection: 34 | - close 35 | Status: 36 | - 200 OK 37 | X-Frame-Options: 38 | - SAMEORIGIN 39 | X-Xss-Protection: 40 | - 1; mode=block 41 | X-Content-Type-Options: 42 | - nosniff 43 | X-Download-Options: 44 | - noopen 45 | X-Permitted-Cross-Domain-Policies: 46 | - none 47 | Referrer-Policy: 48 | - strict-origin-when-cross-origin 49 | Cache-Control: 50 | - no-cache, no-store 51 | P3p: 52 | - 'CP="Our privacy policy is available online: https://www.getharvest.com/services/privacy-policy"' 53 | X-App-Server: 54 | - app4 55 | X-Robots-Tag: 56 | - noindex, nofollow 57 | Content-Security-Policy: 58 | - 'report-uri /csp_reports; default-src *; img-src * data:; font-src data: cache.harvestapp.com 59 | https://js.intercomcdn.com; script-src ''self'' ''unsafe-inline'' ''unsafe-eval'' 60 | https://*.google-analytics.com https://*.nr-data.net https://ajax.googleapis.com 61 | cache.harvestapp.com https://js-agent.newrelic.com https://platform.twitter.com 62 | https://www.google.com https://www.googleadservices.com https://www.googletagmanager.com 63 | https://connect.facebook.net https://googleads.g.doubleclick.net https://app.intercom.io 64 | https://widget.intercom.io https://js.intercomcdn.com https://cdn.plaid.com; 65 | style-src ''self'' ''unsafe-inline'' cache.harvestapp.com https://www.google.com' 66 | X-Request-Id: 67 | - c6fed7230654f9df792cfc40132dfd63 68 | X-Runtime: 69 | - '0.123959' 70 | Strict-Transport-Security: 71 | - max-age=31536000; includeSubDomains 72 | X-Server: 73 | - lb1 74 | body: 75 | encoding: UTF-8 76 | string: '{"id":19815868,"name":"X","code":"","is_active":true,"is_billable":true,"is_fixed_fee":false,"bill_by":"none","budget":null,"budget_by":"none","budget_is_monthly":false,"notify_when_over_budget":false,"over_budget_notification_percentage":80.0,"show_budget_to_all":false,"created_at":"2019-01-17T15:41:13Z","updated_at":"2019-01-17T15:41:13Z","starts_on":null,"ends_on":null,"over_budget_notification_date":null,"notes":"","cost_budget":null,"cost_budget_include_expenses":false,"hourly_rate":null,"fee":null,"client":{"id":6760579,"name":"Pepe","currency":"USD"}}' 77 | http_version: 78 | recorded_at: Thu, 17 Jan 2019 15:42:32 GMT 79 | recorded_with: VCR 4.0.0 80 | -------------------------------------------------------------------------------- /fixtures/vcr_cassettes/harvest_data_setup/project_assignment_road_building.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: post 5 | uri: https://api.harvestapp.com/v2/projects/19919747/user_assignments 6 | body: 7 | encoding: UTF-8 8 | string: '{"project_id":"19919747","user_id":"2527916","project":{"id":"19919747"},"user":{"id":"2527916"}}' 9 | headers: 10 | User-Agent: 11 | - Harvesting Ruby Gem 12 | Authorization: 13 | - Bearer $HARVEST_ACCESS_TOKEN 14 | Harvest-Account-Id: 15 | - "$HARVEST_ACCOUNT_ID" 16 | Connection: 17 | - close 18 | Content-Type: 19 | - application/json; charset=UTF-8 20 | Host: 21 | - api.harvestapp.com 22 | response: 23 | status: 24 | code: 201 25 | message: Created 26 | headers: 27 | Server: 28 | - nginx 29 | Date: 30 | - Fri, 25 Jan 2019 18:31:12 GMT 31 | Content-Type: 32 | - application/json; charset=utf-8 33 | Transfer-Encoding: 34 | - chunked 35 | Connection: 36 | - close 37 | Status: 38 | - 201 Created 39 | X-Frame-Options: 40 | - SAMEORIGIN 41 | X-Xss-Protection: 42 | - 1; mode=block 43 | X-Content-Type-Options: 44 | - nosniff 45 | X-Download-Options: 46 | - noopen 47 | X-Permitted-Cross-Domain-Policies: 48 | - none 49 | Referrer-Policy: 50 | - strict-origin-when-cross-origin 51 | Cache-Control: 52 | - no-cache, no-store 53 | P3p: 54 | - 'CP="Our privacy policy is available online: https://www.getharvest.com/services/privacy-policy"' 55 | X-App-Server: 56 | - app3 57 | X-Robots-Tag: 58 | - noindex, nofollow 59 | Content-Security-Policy: 60 | - 'report-uri /csp_reports; default-src *; img-src * data:; font-src data: cache.harvestapp.com 61 | https://js.intercomcdn.com; script-src ''self'' ''unsafe-inline'' ''unsafe-eval'' 62 | https://*.google-analytics.com https://*.nr-data.net https://ajax.googleapis.com 63 | cache.harvestapp.com https://js-agent.newrelic.com https://platform.twitter.com 64 | https://www.google.com https://www.googleadservices.com https://www.googletagmanager.com 65 | https://connect.facebook.net https://googleads.g.doubleclick.net https://app.intercom.io 66 | https://widget.intercom.io https://js.intercomcdn.com https://cdn.plaid.com; 67 | style-src ''self'' ''unsafe-inline'' cache.harvestapp.com https://www.google.com' 68 | Location: 69 | - https://api.harvestapp.com/api/v2/projects/19919747/user_assignments/186878708 70 | X-Request-Id: 71 | - 0d7f0dbd3a748c8ce0d6970eac0fd8ae 72 | X-Runtime: 73 | - '0.200388' 74 | Strict-Transport-Security: 75 | - max-age=31536000; includeSubDomains 76 | X-Server: 77 | - lb2 78 | body: 79 | encoding: UTF-8 80 | string: '{"id":186878708,"is_project_manager":false,"is_active":true,"budget":null,"created_at":"2019-01-25T18:31:12Z","updated_at":"2019-01-25T18:31:12Z","hourly_rate":null,"project":{"id":19919747,"name":"Road 81 | Building","code":null},"user":{"id":2527916,"name":"Jane Doe"}}' 82 | http_version: 83 | recorded_at: Fri, 25 Jan 2019 18:31:12 GMT 84 | recorded_with: VCR 4.0.0 85 | -------------------------------------------------------------------------------- /fixtures/vcr_cassettes/harvest_data_setup/task_assigment_castle_building_coding.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: post 5 | uri: https://api.harvestapp.com/v2/projects/19919748/task_assignments 6 | body: 7 | encoding: UTF-8 8 | string: '{"project_id":"19919748","task_id":"11399145","project":{"id":"19919748"},"task":{"id":"11399145"}}' 9 | headers: 10 | User-Agent: 11 | - Harvesting Ruby Gem 12 | Authorization: 13 | - Bearer $HARVEST_ACCESS_TOKEN 14 | Harvest-Account-Id: 15 | - "$HARVEST_ACCOUNT_ID" 16 | Connection: 17 | - close 18 | Content-Type: 19 | - application/json; charset=UTF-8 20 | Host: 21 | - api.harvestapp.com 22 | response: 23 | status: 24 | code: 201 25 | message: Created 26 | headers: 27 | Server: 28 | - nginx 29 | Date: 30 | - Fri, 25 Jan 2019 18:31:11 GMT 31 | Content-Type: 32 | - application/json; charset=utf-8 33 | Transfer-Encoding: 34 | - chunked 35 | Connection: 36 | - close 37 | Status: 38 | - 201 Created 39 | X-Frame-Options: 40 | - SAMEORIGIN 41 | X-Xss-Protection: 42 | - 1; mode=block 43 | X-Content-Type-Options: 44 | - nosniff 45 | X-Download-Options: 46 | - noopen 47 | X-Permitted-Cross-Domain-Policies: 48 | - none 49 | Referrer-Policy: 50 | - strict-origin-when-cross-origin 51 | Cache-Control: 52 | - no-cache, no-store 53 | P3p: 54 | - 'CP="Our privacy policy is available online: https://www.getharvest.com/services/privacy-policy"' 55 | X-App-Server: 56 | - app1 57 | X-Robots-Tag: 58 | - noindex, nofollow 59 | Content-Security-Policy: 60 | - 'report-uri /csp_reports; default-src *; img-src * data:; font-src data: cache.harvestapp.com 61 | https://js.intercomcdn.com; script-src ''self'' ''unsafe-inline'' ''unsafe-eval'' 62 | https://*.google-analytics.com https://*.nr-data.net https://ajax.googleapis.com 63 | cache.harvestapp.com https://js-agent.newrelic.com https://platform.twitter.com 64 | https://www.google.com https://www.googleadservices.com https://www.googletagmanager.com 65 | https://connect.facebook.net https://googleads.g.doubleclick.net https://app.intercom.io 66 | https://widget.intercom.io https://js.intercomcdn.com https://cdn.plaid.com; 67 | style-src ''self'' ''unsafe-inline'' cache.harvestapp.com https://www.google.com' 68 | Location: 69 | - https://api.harvestapp.com/api/v2/projects/19919748/task_assignments/215014748 70 | X-Request-Id: 71 | - a41e44a4e52ef52223fa8b9eb9bda942 72 | X-Runtime: 73 | - '0.118818' 74 | Strict-Transport-Security: 75 | - max-age=31536000; includeSubDomains 76 | X-Server: 77 | - lb2 78 | body: 79 | encoding: UTF-8 80 | string: '{"id":215014748,"billable":true,"is_active":true,"created_at":"2019-01-25T18:31:11Z","updated_at":"2019-01-25T18:31:11Z","hourly_rate":null,"budget":null,"project":{"id":19919748,"name":"Castle 81 | Building","code":null},"task":{"id":11399145,"name":"Coding"}}' 82 | http_version: 83 | recorded_at: Fri, 25 Jan 2019 18:31:11 GMT 84 | recorded_with: VCR 4.0.0 85 | -------------------------------------------------------------------------------- /fixtures/vcr_cassettes/harvest_data_setup/task_assignment_roading_building_writing.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: post 5 | uri: https://api.harvestapp.com/v2/projects/19919747/task_assignments 6 | body: 7 | encoding: UTF-8 8 | string: '{"project_id":"19919747","task_id":"11399470","project":{"id":"19919747"},"task":{"id":"11399470"}}' 9 | headers: 10 | User-Agent: 11 | - Harvesting Ruby Gem 12 | Authorization: 13 | - Bearer $HARVEST_ACCESS_TOKEN 14 | Harvest-Account-Id: 15 | - "$HARVEST_ACCOUNT_ID" 16 | Connection: 17 | - close 18 | Content-Type: 19 | - application/json; charset=UTF-8 20 | Host: 21 | - api.harvestapp.com 22 | response: 23 | status: 24 | code: 201 25 | message: Created 26 | headers: 27 | Server: 28 | - nginx 29 | Date: 30 | - Fri, 25 Jan 2019 19:05:24 GMT 31 | Content-Type: 32 | - application/json; charset=utf-8 33 | Transfer-Encoding: 34 | - chunked 35 | Connection: 36 | - close 37 | Status: 38 | - 201 Created 39 | X-Frame-Options: 40 | - SAMEORIGIN 41 | X-Xss-Protection: 42 | - 1; mode=block 43 | X-Content-Type-Options: 44 | - nosniff 45 | X-Download-Options: 46 | - noopen 47 | X-Permitted-Cross-Domain-Policies: 48 | - none 49 | Referrer-Policy: 50 | - strict-origin-when-cross-origin 51 | Cache-Control: 52 | - no-cache, no-store 53 | P3p: 54 | - 'CP="Our privacy policy is available online: https://www.getharvest.com/services/privacy-policy"' 55 | X-App-Server: 56 | - app5 57 | X-Robots-Tag: 58 | - noindex, nofollow 59 | Content-Security-Policy: 60 | - 'report-uri /csp_reports; default-src *; img-src * data:; font-src data: cache.harvestapp.com 61 | https://js.intercomcdn.com; script-src ''self'' ''unsafe-inline'' ''unsafe-eval'' 62 | https://*.google-analytics.com https://*.nr-data.net https://ajax.googleapis.com 63 | cache.harvestapp.com https://js-agent.newrelic.com https://platform.twitter.com 64 | https://www.google.com https://www.googleadservices.com https://www.googletagmanager.com 65 | https://connect.facebook.net https://googleads.g.doubleclick.net https://app.intercom.io 66 | https://widget.intercom.io https://js.intercomcdn.com https://cdn.plaid.com; 67 | style-src ''self'' ''unsafe-inline'' cache.harvestapp.com https://www.google.com' 68 | Location: 69 | - https://api.harvestapp.com/api/v2/projects/19919747/task_assignments/215018912 70 | X-Request-Id: 71 | - 9d2fde089050ebf8e41e713cb3dd5c14 72 | X-Runtime: 73 | - '0.120101' 74 | Strict-Transport-Security: 75 | - max-age=31536000; includeSubDomains 76 | X-Server: 77 | - lb2 78 | body: 79 | encoding: UTF-8 80 | string: '{"id":215018912,"billable":true,"is_active":true,"created_at":"2019-01-25T19:05:24Z","updated_at":"2019-01-25T19:05:24Z","hourly_rate":null,"budget":null,"project":{"id":19919747,"name":"Road 81 | Building","code":null},"task":{"id":11399470,"name":"Writing"}}' 82 | http_version: 83 | recorded_at: Fri, 25 Jan 2019 19:05:24 GMT 84 | recorded_with: VCR 4.0.0 85 | -------------------------------------------------------------------------------- /fixtures/vcr_cassettes/harvest_data_setup/project_assignment_castle_building.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: post 5 | uri: https://api.harvestapp.com/v2/projects/19919748/user_assignments 6 | body: 7 | encoding: UTF-8 8 | string: '{"project_id":"19919748","user_id":"2527915","project":{"id":"19919748"},"user":{"id":"2527915"}}' 9 | headers: 10 | User-Agent: 11 | - Harvesting Ruby Gem 12 | Authorization: 13 | - Bearer $HARVEST_ACCESS_TOKEN 14 | Harvest-Account-Id: 15 | - "$HARVEST_ACCOUNT_ID" 16 | Connection: 17 | - close 18 | Content-Type: 19 | - application/json; charset=UTF-8 20 | Host: 21 | - api.harvestapp.com 22 | response: 23 | status: 24 | code: 201 25 | message: Created 26 | headers: 27 | Server: 28 | - nginx 29 | Date: 30 | - Fri, 25 Jan 2019 18:31:11 GMT 31 | Content-Type: 32 | - application/json; charset=utf-8 33 | Transfer-Encoding: 34 | - chunked 35 | Connection: 36 | - close 37 | Status: 38 | - 201 Created 39 | X-Frame-Options: 40 | - SAMEORIGIN 41 | X-Xss-Protection: 42 | - 1; mode=block 43 | X-Content-Type-Options: 44 | - nosniff 45 | X-Download-Options: 46 | - noopen 47 | X-Permitted-Cross-Domain-Policies: 48 | - none 49 | Referrer-Policy: 50 | - strict-origin-when-cross-origin 51 | Cache-Control: 52 | - no-cache, no-store 53 | P3p: 54 | - 'CP="Our privacy policy is available online: https://www.getharvest.com/services/privacy-policy"' 55 | X-App-Server: 56 | - app7 57 | X-Robots-Tag: 58 | - noindex, nofollow 59 | Content-Security-Policy: 60 | - 'report-uri /csp_reports; default-src *; img-src * data:; font-src data: cache.harvestapp.com 61 | https://js.intercomcdn.com; script-src ''self'' ''unsafe-inline'' ''unsafe-eval'' 62 | https://*.google-analytics.com https://*.nr-data.net https://ajax.googleapis.com 63 | cache.harvestapp.com https://js-agent.newrelic.com https://platform.twitter.com 64 | https://www.google.com https://www.googleadservices.com https://www.googletagmanager.com 65 | https://connect.facebook.net https://googleads.g.doubleclick.net https://app.intercom.io 66 | https://widget.intercom.io https://js.intercomcdn.com https://cdn.plaid.com; 67 | style-src ''self'' ''unsafe-inline'' cache.harvestapp.com https://www.google.com' 68 | Location: 69 | - https://api.harvestapp.com/api/v2/projects/19919748/user_assignments/186878707 70 | X-Request-Id: 71 | - b5bb2aa35e08230229b999e3c8c28222 72 | X-Runtime: 73 | - '0.096077' 74 | Strict-Transport-Security: 75 | - max-age=31536000; includeSubDomains 76 | X-Server: 77 | - lb2 78 | body: 79 | encoding: UTF-8 80 | string: '{"id":186878707,"is_project_manager":false,"is_active":true,"budget":null,"created_at":"2019-01-25T18:31:11Z","updated_at":"2019-01-25T18:31:11Z","hourly_rate":null,"project":{"id":19919748,"name":"Castle 81 | Building","code":null},"user":{"id":2527915,"name":"John Smith"}}' 82 | http_version: 83 | recorded_at: Fri, 25 Jan 2019 18:31:11 GMT 84 | recorded_with: VCR 4.0.0 85 | -------------------------------------------------------------------------------- /fixtures/vcr_cassettes/Harvesting_Client_clients/when_user_is_an_administrator_returns_the_clients_associated_with_the_account.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: https://api.harvestapp.com/v2/clients 6 | body: 7 | encoding: UTF-8 8 | string: '' 9 | headers: 10 | User-Agent: 11 | - Harvesting Ruby Gem 12 | Authorization: 13 | - Bearer $HARVEST_ACCESS_TOKEN 14 | Harvest-Account-Id: 15 | - "$HARVEST_ACCOUNT_ID" 16 | Connection: 17 | - close 18 | Host: 19 | - api.harvestapp.com 20 | response: 21 | status: 22 | code: 200 23 | message: OK 24 | headers: 25 | Server: 26 | - nginx 27 | Date: 28 | - Fri, 25 Jan 2019 18:32:08 GMT 29 | Content-Type: 30 | - application/json; charset=utf-8 31 | Transfer-Encoding: 32 | - chunked 33 | Connection: 34 | - close 35 | Status: 36 | - 200 OK 37 | X-Frame-Options: 38 | - SAMEORIGIN 39 | X-Xss-Protection: 40 | - 1; mode=block 41 | X-Content-Type-Options: 42 | - nosniff 43 | X-Download-Options: 44 | - noopen 45 | X-Permitted-Cross-Domain-Policies: 46 | - none 47 | Referrer-Policy: 48 | - strict-origin-when-cross-origin 49 | Cache-Control: 50 | - no-cache, no-store 51 | P3p: 52 | - 'CP="Our privacy policy is available online: https://www.getharvest.com/services/privacy-policy"' 53 | X-App-Server: 54 | - app1 55 | X-Robots-Tag: 56 | - noindex, nofollow 57 | Content-Security-Policy: 58 | - 'report-uri /csp_reports; default-src *; img-src * data:; font-src data: cache.harvestapp.com 59 | https://js.intercomcdn.com; script-src ''self'' ''unsafe-inline'' ''unsafe-eval'' 60 | https://*.google-analytics.com https://*.nr-data.net https://ajax.googleapis.com 61 | cache.harvestapp.com https://js-agent.newrelic.com https://platform.twitter.com 62 | https://www.google.com https://www.googleadservices.com https://www.googletagmanager.com 63 | https://connect.facebook.net https://googleads.g.doubleclick.net https://app.intercom.io 64 | https://widget.intercom.io https://js.intercomcdn.com https://cdn.plaid.com; 65 | style-src ''self'' ''unsafe-inline'' cache.harvestapp.com https://www.google.com' 66 | X-Request-Id: 67 | - d125e70e64791763595a010cd4e11135 68 | X-Runtime: 69 | - '0.067018' 70 | Strict-Transport-Security: 71 | - max-age=31536000; includeSubDomains 72 | X-Server: 73 | - lb4 74 | body: 75 | encoding: UTF-8 76 | string: '{"clients":[{"id":7722247,"name":"Toto","is_active":true,"address":null,"created_at":"2019-01-25T18:31:09Z","updated_at":"2019-01-25T18:31:12Z","currency":"USD"},{"id":7722246,"name":"Pepe","is_active":true,"address":null,"created_at":"2019-01-25T18:31:09Z","updated_at":"2019-01-25T18:31:10Z","currency":"USD"}],"per_page":100,"total_pages":1,"total_entries":2,"next_page":null,"previous_page":null,"page":1,"links":{"first":"https://api.harvestapp.com/v2/clients?page=1&per_page=100","next":null,"previous":null,"last":"https://api.harvestapp.com/v2/clients?page=1&per_page=100"}}' 77 | http_version: 78 | recorded_at: Fri, 25 Jan 2019 18:32:08 GMT 79 | recorded_with: VCR 4.0.0 80 | -------------------------------------------------------------------------------- /fixtures/vcr_cassettes/Harvesting_Models_Project_task_assignments/as_an_admin_user_retrieves_project_task_assignments_for_the_castle_building_project.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: https://api.harvestapp.com/v2/projects/19919748/task_assignments 6 | body: 7 | encoding: UTF-8 8 | string: '' 9 | headers: 10 | User-Agent: 11 | - Harvesting Ruby Gem 12 | Authorization: 13 | - Bearer $HARVEST_ACCESS_TOKEN 14 | Harvest-Account-Id: 15 | - "$HARVEST_ACCOUNT_ID" 16 | Connection: 17 | - close 18 | Host: 19 | - api.harvestapp.com 20 | response: 21 | status: 22 | code: 200 23 | message: OK 24 | headers: 25 | Server: 26 | - nginx 27 | Date: 28 | - Tue, 29 Jan 2019 03:02:03 GMT 29 | Content-Type: 30 | - application/json; charset=utf-8 31 | Transfer-Encoding: 32 | - chunked 33 | Connection: 34 | - close 35 | Status: 36 | - 200 OK 37 | X-Frame-Options: 38 | - SAMEORIGIN 39 | X-Xss-Protection: 40 | - 1; mode=block 41 | X-Content-Type-Options: 42 | - nosniff 43 | X-Download-Options: 44 | - noopen 45 | X-Permitted-Cross-Domain-Policies: 46 | - none 47 | Referrer-Policy: 48 | - strict-origin-when-cross-origin 49 | Cache-Control: 50 | - no-cache, no-store 51 | P3p: 52 | - 'CP="Our privacy policy is available online: https://www.getharvest.com/services/privacy-policy"' 53 | X-App-Server: 54 | - app11 55 | X-Robots-Tag: 56 | - noindex, nofollow 57 | Content-Security-Policy: 58 | - 'report-uri /csp_reports; default-src *; img-src * data:; font-src data: cache.harvestapp.com 59 | https://js.intercomcdn.com; script-src ''self'' ''unsafe-inline'' ''unsafe-eval'' 60 | https://*.google-analytics.com https://*.nr-data.net https://ajax.googleapis.com 61 | cache.harvestapp.com https://js-agent.newrelic.com https://platform.twitter.com 62 | https://www.google.com https://www.googleadservices.com https://www.googletagmanager.com 63 | https://connect.facebook.net https://googleads.g.doubleclick.net https://app.intercom.io 64 | https://widget.intercom.io https://js.intercomcdn.com https://cdn.plaid.com; 65 | style-src ''self'' ''unsafe-inline'' cache.harvestapp.com https://www.google.com' 66 | X-Request-Id: 67 | - a5e8566218ca18f2fbb01b4c12f88507 68 | X-Runtime: 69 | - '0.035194' 70 | Strict-Transport-Security: 71 | - max-age=31536000; includeSubDomains 72 | X-Server: 73 | - lb1 74 | body: 75 | encoding: UTF-8 76 | string: '{"task_assignments":[{"id":215014748,"billable":true,"is_active":true,"created_at":"2019-01-25T18:31:11Z","updated_at":"2019-01-25T18:31:11Z","hourly_rate":null,"budget":null,"project":{"id":19919748,"name":"Castle 77 | Building","code":null},"task":{"id":11399145,"name":"Coding"}}],"per_page":100,"total_pages":1,"total_entries":1,"next_page":null,"previous_page":null,"page":1,"links":{"first":"https://api.harvestapp.com/v2/projects/19919748/task_assignments?page=1&per_page=100","next":null,"previous":null,"last":"https://api.harvestapp.com/v2/projects/19919748/task_assignments?page=1&per_page=100"}}' 78 | http_version: 79 | recorded_at: Tue, 29 Jan 2019 03:02:03 GMT 80 | recorded_with: VCR 4.0.0 81 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at ernesto+github@ombulabs.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /fixtures/vcr_cassettes/Harvesting_Client_contacts/when_user_is_an_administrator_returns_the_contacts_associated_with_the_account.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: https://api.harvestapp.com/v2/contacts 6 | body: 7 | encoding: UTF-8 8 | string: '' 9 | headers: 10 | User-Agent: 11 | - Harvesting Ruby Gem 12 | Authorization: 13 | - Bearer $HARVEST_ACCESS_TOKEN 14 | Harvest-Account-Id: 15 | - "$HARVEST_ACCOUNT_ID" 16 | Connection: 17 | - close 18 | Host: 19 | - api.harvestapp.com 20 | response: 21 | status: 22 | code: 200 23 | message: OK 24 | headers: 25 | Server: 26 | - nginx 27 | Date: 28 | - Fri, 25 Jan 2019 18:32:05 GMT 29 | Content-Type: 30 | - application/json; charset=utf-8 31 | Transfer-Encoding: 32 | - chunked 33 | Connection: 34 | - close 35 | Status: 36 | - 200 OK 37 | X-Frame-Options: 38 | - SAMEORIGIN 39 | X-Xss-Protection: 40 | - 1; mode=block 41 | X-Content-Type-Options: 42 | - nosniff 43 | X-Download-Options: 44 | - noopen 45 | X-Permitted-Cross-Domain-Policies: 46 | - none 47 | Referrer-Policy: 48 | - strict-origin-when-cross-origin 49 | Cache-Control: 50 | - no-cache, no-store 51 | P3p: 52 | - 'CP="Our privacy policy is available online: https://www.getharvest.com/services/privacy-policy"' 53 | X-App-Server: 54 | - app4 55 | X-Robots-Tag: 56 | - noindex, nofollow 57 | Content-Security-Policy: 58 | - 'report-uri /csp_reports; default-src *; img-src * data:; font-src data: cache.harvestapp.com 59 | https://js.intercomcdn.com; script-src ''self'' ''unsafe-inline'' ''unsafe-eval'' 60 | https://*.google-analytics.com https://*.nr-data.net https://ajax.googleapis.com 61 | cache.harvestapp.com https://js-agent.newrelic.com https://platform.twitter.com 62 | https://www.google.com https://www.googleadservices.com https://www.googletagmanager.com 63 | https://connect.facebook.net https://googleads.g.doubleclick.net https://app.intercom.io 64 | https://widget.intercom.io https://js.intercomcdn.com https://cdn.plaid.com; 65 | style-src ''self'' ''unsafe-inline'' cache.harvestapp.com https://www.google.com' 66 | X-Request-Id: 67 | - 4c52ece40992f26ff4df2ba69dd26c5f 68 | X-Runtime: 69 | - '0.101059' 70 | Strict-Transport-Security: 71 | - max-age=31536000; includeSubDomains 72 | X-Server: 73 | - lb4 74 | body: 75 | encoding: UTF-8 76 | string: '{"contacts":[{"id":7140798,"title":null,"first_name":"Cersei","last_name":"Lannister","email":"","phone_office":"","phone_mobile":"","fax":"","created_at":"2019-01-25T18:31:12Z","updated_at":"2019-01-25T18:31:12Z","client":{"id":7722247,"name":"Toto"}},{"id":7140797,"title":null,"first_name":"Jon","last_name":"Snow","email":"","phone_office":"","phone_mobile":"","fax":"","created_at":"2019-01-25T18:31:10Z","updated_at":"2019-01-25T18:31:10Z","client":{"id":7722246,"name":"Pepe"}}],"per_page":100,"total_pages":1,"total_entries":2,"next_page":null,"previous_page":null,"page":1,"links":{"first":"https://api.harvestapp.com/v2/contacts?page=1&per_page=100","next":null,"previous":null,"last":"https://api.harvestapp.com/v2/contacts?page=1&per_page=100"}}' 77 | http_version: 78 | recorded_at: Fri, 25 Jan 2019 18:32:05 GMT 79 | recorded_with: VCR 4.0.0 80 | -------------------------------------------------------------------------------- /fixtures/vcr_cassettes/harvest_data_setup/user_john_smith.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: post 5 | uri: https://api.harvestapp.com/v2/users 6 | body: 7 | encoding: UTF-8 8 | string: '{"first_name":"John","last_name":"Smith","email":"john.smith@example.com"}' 9 | headers: 10 | User-Agent: 11 | - Harvesting Ruby Gem 12 | Authorization: 13 | - Bearer $HARVEST_ACCESS_TOKEN 14 | Harvest-Account-Id: 15 | - "$HARVEST_ACCOUNT_ID" 16 | Connection: 17 | - close 18 | Content-Type: 19 | - application/json; charset=UTF-8 20 | Host: 21 | - api.harvestapp.com 22 | response: 23 | status: 24 | code: 201 25 | message: Created 26 | headers: 27 | Server: 28 | - nginx 29 | Date: 30 | - Fri, 25 Jan 2019 18:31:08 GMT 31 | Content-Type: 32 | - application/json; charset=utf-8 33 | Transfer-Encoding: 34 | - chunked 35 | Connection: 36 | - close 37 | Status: 38 | - 201 Created 39 | X-Frame-Options: 40 | - SAMEORIGIN 41 | X-Xss-Protection: 42 | - 1; mode=block 43 | X-Content-Type-Options: 44 | - nosniff 45 | X-Download-Options: 46 | - noopen 47 | X-Permitted-Cross-Domain-Policies: 48 | - none 49 | Referrer-Policy: 50 | - strict-origin-when-cross-origin 51 | Cache-Control: 52 | - no-cache, no-store 53 | P3p: 54 | - 'CP="Our privacy policy is available online: https://www.getharvest.com/services/privacy-policy"' 55 | X-App-Server: 56 | - app1 57 | X-Robots-Tag: 58 | - noindex, nofollow 59 | Content-Security-Policy: 60 | - 'report-uri /csp_reports; default-src *; img-src * data:; font-src data: cache.harvestapp.com 61 | https://js.intercomcdn.com; script-src ''self'' ''unsafe-inline'' ''unsafe-eval'' 62 | https://*.google-analytics.com https://*.nr-data.net https://ajax.googleapis.com 63 | cache.harvestapp.com https://js-agent.newrelic.com https://platform.twitter.com 64 | https://www.google.com https://www.googleadservices.com https://www.googletagmanager.com 65 | https://connect.facebook.net https://googleads.g.doubleclick.net https://app.intercom.io 66 | https://widget.intercom.io https://js.intercomcdn.com https://cdn.plaid.com; 67 | style-src ''self'' ''unsafe-inline'' cache.harvestapp.com https://www.google.com' 68 | Location: 69 | - https://api.harvestapp.com/api/v2/users/2527915 70 | X-Request-Id: 71 | - 9a8875cac7d8f0f7ce04a92f232f7868 72 | X-Runtime: 73 | - '0.146816' 74 | Strict-Transport-Security: 75 | - max-age=31536000; includeSubDomains 76 | X-Server: 77 | - lb2 78 | body: 79 | encoding: UTF-8 80 | string: '{"id":2527915,"first_name":"John","last_name":"Smith","email":"john.smith@example.com","telephone":"","timezone":"Eastern 81 | Time (US & Canada)","weekly_capacity":126000,"has_access_to_all_future_projects":false,"is_contractor":false,"is_admin":false,"is_project_manager":false,"can_see_rates":false,"can_create_projects":false,"can_create_invoices":false,"is_active":true,"created_at":"2019-01-25T18:31:08Z","updated_at":"2019-01-25T18:31:08Z","default_hourly_rate":null,"cost_rate":null,"roles":[],"avatar_url":"https://cache.harvestapp.com/assets/profile_images/big_ben.png?1548441068"}' 82 | http_version: 83 | recorded_at: Fri, 25 Jan 2019 18:31:08 GMT 84 | recorded_with: VCR 4.0.0 85 | -------------------------------------------------------------------------------- /fixtures/vcr_cassettes/harvest_data_setup/user_jane_doe.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: post 5 | uri: https://api.harvestapp.com/v2/users 6 | body: 7 | encoding: UTF-8 8 | string: '{"first_name":"Jane","last_name":"Doe","email":"jane.doe@example.com"}' 9 | headers: 10 | User-Agent: 11 | - Harvesting Ruby Gem 12 | Authorization: 13 | - Bearer $HARVEST_ACCESS_TOKEN 14 | Harvest-Account-Id: 15 | - "$HARVEST_ACCOUNT_ID" 16 | Connection: 17 | - close 18 | Content-Type: 19 | - application/json; charset=UTF-8 20 | Host: 21 | - api.harvestapp.com 22 | response: 23 | status: 24 | code: 201 25 | message: Created 26 | headers: 27 | Server: 28 | - nginx 29 | Date: 30 | - Fri, 25 Jan 2019 18:31:08 GMT 31 | Content-Type: 32 | - application/json; charset=utf-8 33 | Transfer-Encoding: 34 | - chunked 35 | Connection: 36 | - close 37 | Status: 38 | - 201 Created 39 | X-Frame-Options: 40 | - SAMEORIGIN 41 | X-Xss-Protection: 42 | - 1; mode=block 43 | X-Content-Type-Options: 44 | - nosniff 45 | X-Download-Options: 46 | - noopen 47 | X-Permitted-Cross-Domain-Policies: 48 | - none 49 | Referrer-Policy: 50 | - strict-origin-when-cross-origin 51 | Cache-Control: 52 | - no-cache, no-store 53 | P3p: 54 | - 'CP="Our privacy policy is available online: https://www.getharvest.com/services/privacy-policy"' 55 | X-App-Server: 56 | - app20 57 | X-Robots-Tag: 58 | - noindex, nofollow 59 | Content-Security-Policy: 60 | - 'report-uri /csp_reports; default-src *; img-src * data:; font-src data: cache.harvestapp.com 61 | https://js.intercomcdn.com; script-src ''self'' ''unsafe-inline'' ''unsafe-eval'' 62 | https://*.google-analytics.com https://*.nr-data.net https://ajax.googleapis.com 63 | cache.harvestapp.com https://js-agent.newrelic.com https://platform.twitter.com 64 | https://www.google.com https://www.googleadservices.com https://www.googletagmanager.com 65 | https://connect.facebook.net https://googleads.g.doubleclick.net https://app.intercom.io 66 | https://widget.intercom.io https://js.intercomcdn.com https://cdn.plaid.com; 67 | style-src ''self'' ''unsafe-inline'' cache.harvestapp.com https://www.google.com' 68 | Location: 69 | - https://api.harvestapp.com/api/v2/users/2527916 70 | X-Request-Id: 71 | - dbf12f6a972e70bedf395767014113d4 72 | X-Runtime: 73 | - '0.097340' 74 | Strict-Transport-Security: 75 | - max-age=31536000; includeSubDomains 76 | X-Server: 77 | - lb2 78 | body: 79 | encoding: UTF-8 80 | string: '{"id":2527916,"first_name":"Jane","last_name":"Doe","email":"jane.doe@example.com","telephone":"","timezone":"Eastern 81 | Time (US & Canada)","weekly_capacity":126000,"has_access_to_all_future_projects":false,"is_contractor":false,"is_admin":false,"is_project_manager":false,"can_see_rates":false,"can_create_projects":false,"can_create_invoices":false,"is_active":true,"created_at":"2019-01-25T18:31:08Z","updated_at":"2019-01-25T18:31:08Z","default_hourly_rate":null,"cost_rate":null,"roles":[],"avatar_url":"https://cache.harvestapp.com/assets/profile_images/allen_bradley_clock_tower.png?1548441068"}' 82 | http_version: 83 | recorded_at: Fri, 25 Jan 2019 18:31:08 GMT 84 | recorded_with: VCR 4.0.0 85 | -------------------------------------------------------------------------------- /fixtures/vcr_cassettes/harvest_data_setup/project_road_building.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: post 5 | uri: https://api.harvestapp.com/v2/projects 6 | body: 7 | encoding: UTF-8 8 | string: '{"client_id":"7722247","client":{"id":"7722247"},"name":"Road Building","is_billable":"true","bill_by":"Tasks","budget_by":"person"}' 9 | headers: 10 | User-Agent: 11 | - Harvesting Ruby Gem 12 | Authorization: 13 | - Bearer $HARVEST_ACCESS_TOKEN 14 | Harvest-Account-Id: 15 | - "$HARVEST_ACCOUNT_ID" 16 | Connection: 17 | - close 18 | Content-Type: 19 | - application/json; charset=UTF-8 20 | Host: 21 | - api.harvestapp.com 22 | response: 23 | status: 24 | code: 201 25 | message: Created 26 | headers: 27 | Server: 28 | - nginx 29 | Date: 30 | - Fri, 25 Jan 2019 18:31:10 GMT 31 | Content-Type: 32 | - application/json; charset=utf-8 33 | Transfer-Encoding: 34 | - chunked 35 | Connection: 36 | - close 37 | Status: 38 | - 201 Created 39 | X-Frame-Options: 40 | - SAMEORIGIN 41 | X-Xss-Protection: 42 | - 1; mode=block 43 | X-Content-Type-Options: 44 | - nosniff 45 | X-Download-Options: 46 | - noopen 47 | X-Permitted-Cross-Domain-Policies: 48 | - none 49 | Referrer-Policy: 50 | - strict-origin-when-cross-origin 51 | Cache-Control: 52 | - no-cache, no-store 53 | P3p: 54 | - 'CP="Our privacy policy is available online: https://www.getharvest.com/services/privacy-policy"' 55 | X-App-Server: 56 | - app2 57 | X-Robots-Tag: 58 | - noindex, nofollow 59 | Content-Security-Policy: 60 | - 'report-uri /csp_reports; default-src *; img-src * data:; font-src data: cache.harvestapp.com 61 | https://js.intercomcdn.com; script-src ''self'' ''unsafe-inline'' ''unsafe-eval'' 62 | https://*.google-analytics.com https://*.nr-data.net https://ajax.googleapis.com 63 | cache.harvestapp.com https://js-agent.newrelic.com https://platform.twitter.com 64 | https://www.google.com https://www.googleadservices.com https://www.googletagmanager.com 65 | https://connect.facebook.net https://googleads.g.doubleclick.net https://app.intercom.io 66 | https://widget.intercom.io https://js.intercomcdn.com https://cdn.plaid.com; 67 | style-src ''self'' ''unsafe-inline'' cache.harvestapp.com https://www.google.com' 68 | Location: 69 | - https://api.harvestapp.com/api/v2/projects/19919747 70 | X-Request-Id: 71 | - e519b8d9029b6332e44069f73706ba64 72 | X-Runtime: 73 | - '0.231503' 74 | Strict-Transport-Security: 75 | - max-age=31536000; includeSubDomains 76 | X-Server: 77 | - lb2 78 | body: 79 | encoding: UTF-8 80 | string: '{"id":19919747,"name":"Road Building","code":null,"is_active":true,"is_billable":true,"is_fixed_fee":false,"bill_by":"Tasks","budget":null,"budget_by":"person","budget_is_monthly":false,"notify_when_over_budget":false,"over_budget_notification_percentage":80.0,"show_budget_to_all":false,"created_at":"2019-01-25T18:31:10Z","updated_at":"2019-01-25T18:31:10Z","starts_on":null,"ends_on":null,"over_budget_notification_date":null,"notes":null,"cost_budget":null,"cost_budget_include_expenses":false,"hourly_rate":null,"fee":null,"client":{"id":7722247,"name":"Toto","currency":"USD"}}' 81 | http_version: 82 | recorded_at: Fri, 25 Jan 2019 18:31:10 GMT 83 | recorded_with: VCR 4.0.0 84 | -------------------------------------------------------------------------------- /fixtures/vcr_cassettes/harvest_data_setup/project_castle_building.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: post 5 | uri: https://api.harvestapp.com/v2/projects 6 | body: 7 | encoding: UTF-8 8 | string: '{"client_id":"7722246","client":{"id":"7722246"},"name":"Castle Building","is_billable":"true","bill_by":"Tasks","budget_by":"person"}' 9 | headers: 10 | User-Agent: 11 | - Harvesting Ruby Gem 12 | Authorization: 13 | - Bearer $HARVEST_ACCESS_TOKEN 14 | Harvest-Account-Id: 15 | - "$HARVEST_ACCOUNT_ID" 16 | Connection: 17 | - close 18 | Content-Type: 19 | - application/json; charset=UTF-8 20 | Host: 21 | - api.harvestapp.com 22 | response: 23 | status: 24 | code: 201 25 | message: Created 26 | headers: 27 | Server: 28 | - nginx 29 | Date: 30 | - Fri, 25 Jan 2019 18:31:10 GMT 31 | Content-Type: 32 | - application/json; charset=utf-8 33 | Transfer-Encoding: 34 | - chunked 35 | Connection: 36 | - close 37 | Status: 38 | - 201 Created 39 | X-Frame-Options: 40 | - SAMEORIGIN 41 | X-Xss-Protection: 42 | - 1; mode=block 43 | X-Content-Type-Options: 44 | - nosniff 45 | X-Download-Options: 46 | - noopen 47 | X-Permitted-Cross-Domain-Policies: 48 | - none 49 | Referrer-Policy: 50 | - strict-origin-when-cross-origin 51 | Cache-Control: 52 | - no-cache, no-store 53 | P3p: 54 | - 'CP="Our privacy policy is available online: https://www.getharvest.com/services/privacy-policy"' 55 | X-App-Server: 56 | - app21 57 | X-Robots-Tag: 58 | - noindex, nofollow 59 | Content-Security-Policy: 60 | - 'report-uri /csp_reports; default-src *; img-src * data:; font-src data: cache.harvestapp.com 61 | https://js.intercomcdn.com; script-src ''self'' ''unsafe-inline'' ''unsafe-eval'' 62 | https://*.google-analytics.com https://*.nr-data.net https://ajax.googleapis.com 63 | cache.harvestapp.com https://js-agent.newrelic.com https://platform.twitter.com 64 | https://www.google.com https://www.googleadservices.com https://www.googletagmanager.com 65 | https://connect.facebook.net https://googleads.g.doubleclick.net https://app.intercom.io 66 | https://widget.intercom.io https://js.intercomcdn.com https://cdn.plaid.com; 67 | style-src ''self'' ''unsafe-inline'' cache.harvestapp.com https://www.google.com' 68 | Location: 69 | - https://api.harvestapp.com/api/v2/projects/19919748 70 | X-Request-Id: 71 | - 7451b5d5f857f2814fdbfbf846db4a08 72 | X-Runtime: 73 | - '0.072091' 74 | Strict-Transport-Security: 75 | - max-age=31536000; includeSubDomains 76 | X-Server: 77 | - lb2 78 | body: 79 | encoding: UTF-8 80 | string: '{"id":19919748,"name":"Castle Building","code":null,"is_active":true,"is_billable":true,"is_fixed_fee":false,"bill_by":"Tasks","budget":null,"budget_by":"person","budget_is_monthly":false,"notify_when_over_budget":false,"over_budget_notification_percentage":80.0,"show_budget_to_all":false,"created_at":"2019-01-25T18:31:10Z","updated_at":"2019-01-25T18:31:10Z","starts_on":null,"ends_on":null,"over_budget_notification_date":null,"notes":null,"cost_budget":null,"cost_budget_include_expenses":false,"hourly_rate":null,"fee":null,"client":{"id":7722246,"name":"Pepe","currency":"USD"}}' 81 | http_version: 82 | recorded_at: Fri, 25 Jan 2019 18:31:10 GMT 83 | recorded_with: VCR 4.0.0 84 | -------------------------------------------------------------------------------- /fixtures/vcr_cassettes/Harvesting_Client_task_assignments/as_an_admin_user_retrieves_the_accounts_task_assignments.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: https://api.harvestapp.com/v2/task_assignments 6 | body: 7 | encoding: UTF-8 8 | string: '' 9 | headers: 10 | User-Agent: 11 | - Harvesting Ruby Gem 12 | Authorization: 13 | - Bearer $HARVEST_ACCESS_TOKEN 14 | Harvest-Account-Id: 15 | - "$HARVEST_ACCOUNT_ID" 16 | Connection: 17 | - close 18 | Host: 19 | - api.harvestapp.com 20 | response: 21 | status: 22 | code: 200 23 | message: OK 24 | headers: 25 | Server: 26 | - nginx 27 | Date: 28 | - Fri, 25 Jan 2019 19:05:45 GMT 29 | Content-Type: 30 | - application/json; charset=utf-8 31 | Transfer-Encoding: 32 | - chunked 33 | Connection: 34 | - close 35 | Status: 36 | - 200 OK 37 | X-Frame-Options: 38 | - SAMEORIGIN 39 | X-Xss-Protection: 40 | - 1; mode=block 41 | X-Content-Type-Options: 42 | - nosniff 43 | X-Download-Options: 44 | - noopen 45 | X-Permitted-Cross-Domain-Policies: 46 | - none 47 | Referrer-Policy: 48 | - strict-origin-when-cross-origin 49 | Cache-Control: 50 | - no-cache, no-store 51 | P3p: 52 | - 'CP="Our privacy policy is available online: https://www.getharvest.com/services/privacy-policy"' 53 | X-App-Server: 54 | - app3 55 | X-Robots-Tag: 56 | - noindex, nofollow 57 | Content-Security-Policy: 58 | - 'report-uri /csp_reports; default-src *; img-src * data:; font-src data: cache.harvestapp.com 59 | https://js.intercomcdn.com; script-src ''self'' ''unsafe-inline'' ''unsafe-eval'' 60 | https://*.google-analytics.com https://*.nr-data.net https://ajax.googleapis.com 61 | cache.harvestapp.com https://js-agent.newrelic.com https://platform.twitter.com 62 | https://www.google.com https://www.googleadservices.com https://www.googletagmanager.com 63 | https://connect.facebook.net https://googleads.g.doubleclick.net https://app.intercom.io 64 | https://widget.intercom.io https://js.intercomcdn.com https://cdn.plaid.com; 65 | style-src ''self'' ''unsafe-inline'' cache.harvestapp.com https://www.google.com' 66 | X-Request-Id: 67 | - 80859416f9595cc38ef73e5baaf452f7 68 | X-Runtime: 69 | - '0.110058' 70 | Strict-Transport-Security: 71 | - max-age=31536000; includeSubDomains 72 | X-Server: 73 | - lb2 74 | body: 75 | encoding: UTF-8 76 | string: '{"task_assignments":[{"id":215018912,"billable":true,"is_active":true,"created_at":"2019-01-25T19:05:24Z","updated_at":"2019-01-25T19:05:24Z","hourly_rate":null,"budget":null,"project":{"id":19919747,"name":"Road 77 | Building","code":null},"task":{"id":11399470,"name":"Writing"}},{"id":215014748,"billable":true,"is_active":true,"created_at":"2019-01-25T18:31:11Z","updated_at":"2019-01-25T18:31:11Z","hourly_rate":null,"budget":null,"project":{"id":19919748,"name":"Castle 78 | Building","code":null},"task":{"id":11399145,"name":"Coding"}}],"per_page":100,"total_pages":1,"total_entries":2,"next_page":null,"previous_page":null,"page":1,"links":{"first":"https://api.harvestapp.com/v2/task_assignments?page=1&per_page=100","next":null,"previous":null,"last":"https://api.harvestapp.com/v2/task_assignments?page=1&per_page=100"}}' 79 | http_version: 80 | recorded_at: Fri, 25 Jan 2019 19:05:45 GMT 81 | recorded_with: VCR 4.0.0 82 | -------------------------------------------------------------------------------- /fixtures/vcr_cassettes/Harvesting_Models_Project_user_assignments/as_an_admin_user_retrieves_project_user_assignments_for_castle_building_project.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: https://api.harvestapp.com/v2/projects/19919748/user_assignments 6 | body: 7 | encoding: UTF-8 8 | string: '' 9 | headers: 10 | User-Agent: 11 | - Harvesting Ruby Gem 12 | Authorization: 13 | - Bearer $HARVEST_ACCESS_TOKEN 14 | Harvest-Account-Id: 15 | - "$HARVEST_ACCOUNT_ID" 16 | Connection: 17 | - close 18 | Host: 19 | - api.harvestapp.com 20 | response: 21 | status: 22 | code: 200 23 | message: OK 24 | headers: 25 | Server: 26 | - nginx 27 | Date: 28 | - Tue, 29 Jan 2019 03:02:02 GMT 29 | Content-Type: 30 | - application/json; charset=utf-8 31 | Transfer-Encoding: 32 | - chunked 33 | Connection: 34 | - close 35 | Status: 36 | - 200 OK 37 | X-Frame-Options: 38 | - SAMEORIGIN 39 | X-Xss-Protection: 40 | - 1; mode=block 41 | X-Content-Type-Options: 42 | - nosniff 43 | X-Download-Options: 44 | - noopen 45 | X-Permitted-Cross-Domain-Policies: 46 | - none 47 | Referrer-Policy: 48 | - strict-origin-when-cross-origin 49 | Cache-Control: 50 | - no-cache, no-store 51 | P3p: 52 | - 'CP="Our privacy policy is available online: https://www.getharvest.com/services/privacy-policy"' 53 | X-App-Server: 54 | - app5 55 | X-Robots-Tag: 56 | - noindex, nofollow 57 | Content-Security-Policy: 58 | - 'report-uri /csp_reports; default-src *; img-src * data:; font-src data: cache.harvestapp.com 59 | https://js.intercomcdn.com; script-src ''self'' ''unsafe-inline'' ''unsafe-eval'' 60 | https://*.google-analytics.com https://*.nr-data.net https://ajax.googleapis.com 61 | cache.harvestapp.com https://js-agent.newrelic.com https://platform.twitter.com 62 | https://www.google.com https://www.googleadservices.com https://www.googletagmanager.com 63 | https://connect.facebook.net https://googleads.g.doubleclick.net https://app.intercom.io 64 | https://widget.intercom.io https://js.intercomcdn.com https://cdn.plaid.com; 65 | style-src ''self'' ''unsafe-inline'' cache.harvestapp.com https://www.google.com' 66 | X-Request-Id: 67 | - 1866fea3f6eeccdf82364bffa2c7a782 68 | X-Runtime: 69 | - '0.087824' 70 | Strict-Transport-Security: 71 | - max-age=31536000; includeSubDomains 72 | X-Server: 73 | - lb1 74 | body: 75 | encoding: UTF-8 76 | string: '{"user_assignments":[{"id":186878707,"is_project_manager":false,"is_active":true,"budget":null,"created_at":"2019-01-25T18:31:11Z","updated_at":"2019-01-25T18:31:11Z","hourly_rate":null,"project":{"id":19919748,"name":"Castle 77 | Building","code":null},"user":{"id":2527915,"name":"John Smith"}},{"id":186878706,"is_project_manager":true,"is_active":true,"budget":null,"created_at":"2019-01-25T18:31:10Z","updated_at":"2019-01-25T18:31:10Z","hourly_rate":null,"project":{"id":19919748,"name":"Castle 78 | Building","code":null},"user":{"id":2348607,"name":"Alexander Hamilton"}}],"per_page":100,"total_pages":1,"total_entries":2,"next_page":null,"previous_page":null,"page":1,"links":{"first":"https://api.harvestapp.com/v2/projects/19919748/user_assignments?page=1&per_page=100","next":null,"previous":null,"last":"https://api.harvestapp.com/v2/projects/19919748/user_assignments?page=1&per_page=100"}}' 79 | http_version: 80 | recorded_at: Tue, 29 Jan 2019 03:02:02 GMT 81 | recorded_with: VCR 4.0.0 82 | -------------------------------------------------------------------------------- /RELEASE_NOTES.md: -------------------------------------------------------------------------------- 1 | # RELEASE NOTES 2 | 3 | ### main 4 | 5 | **Notes** 6 | 7 | **Bug Fixes** 8 | 9 | ### Version 0.6.0 - July 17, 2023 10 | 11 | **Notes** 12 | 13 | **Bug Fixes** 14 | 15 | - Fix incompatibility with Ruby 3.0 and kwargs: https://github.com/fastruby/harvesting/pull/68 16 | 17 | ### Version 0.5.1 - January 14, 2021 18 | 19 | **Notes** 20 | - Add support for Codecov so that we can track code coverage over time in the 21 | library: https://github.com/fastruby/harvesting/pull/59 22 | 23 | **Bug Fixes** 24 | - Fix incompatibility with Ruby 3.0: https://github.com/fastruby/harvesting/pull/64 25 | - Relax `http` dependency declaration so that we can `bundle install` with more 26 | modern versions of that gem: https://github.com/fastruby/harvesting/pull/63 27 | - Fix issue when trying to remove an entity that is not removable: 28 | https://github.com/fastruby/harvesting/pull/61 29 | 30 | ### Version 0.5.0 - September 3, 2020 31 | 32 | **Notes** 33 | - Changed behavior of `client.clients` so that it returns an instance of `Harvesting::Models::Clients` instead of an `Array`: https://github.com/fastruby/harvesting/pull/39 34 | 35 | **Bug Fixes** 36 | - Add support for Harvesting::RateLimitExceeded instead of a JSON::ParserError: https://github.com/fastruby/harvesting/pull/57 37 | - Add support for RequestNotFound instead of a JSON::ParserError: https://github.com/fastruby/harvesting/pull/54 38 | 39 | ### Version 0.4.0 - June 6, 2020 40 | 41 | **Notes** 42 | - Added Ruby 2.5.1 to version matrix in Travis: https://github.com/fastruby/harvesting/pull/31 43 | - Associated time entries for project: https://github.com/fastruby/harvesting/pull/32 44 | - Add require forwardable in havest_record_collection model: https://github.com/fastruby/harvesting/pull/40 45 | - Add syntax highlighting to readme examples: https://github.com/fastruby/harvesting/pull/41 46 | - Rename the client key as harvest_client to avoid confusion: https://github.com/fastruby/harvesting/pull/43 47 | - Update rake requirement from ~> 10.0 to ~> 13.0: https://github.com/fastruby/harvesting/pull/44 48 | - Bump ffi from 1.9.23 to 1.12.2: https://github.com/fastruby/harvesting/pull/45 49 | - Ability to supply filter options to the invoice end point and a model for line items on an invoice.: https://github.com/fastruby/harvesting/pull/46 50 | 51 | **Bug Fixes** 52 | 53 | - Complete pending test: https://github.com/fastruby/harvesting/pull/28 54 | - Fixed Code Climate link: https://github.com/fastruby/harvesting/pull/38 55 | 56 | 57 | ### Version 0.3.0 - Jan 22, 2019 58 | 59 | **Notes** 60 | 61 | - Support for users: https://github.com/fastruby/harvesting/pull/9 62 | - Support for fetching single records: https://github.com/fastruby/harvesting/pull/14 and https://github.com/fastruby/harvesting/pull/22 63 | - Nested models make it easier to access data: https://github.com/fastruby/harvesting/pull/15 64 | - Better architecture for collections: https://github.com/fastruby/harvesting/pull/18 65 | 66 | **Bug Fixes** 67 | 68 | - Correct pagination support: https://github.com/fastruby/harvesting/pull/17 69 | - Added documentation: https://github.com/fastruby/harvesting/pull/30 70 | 71 | ### Version 0.2.0 - Oct 18, 2018 72 | 73 | **Notes** 74 | 75 | - More documentation in README.md 76 | - Adds ability to access user associated with time entries: https://github.com/fastruby/harvesting/pull/3 77 | - Adds support for Docker for development: https://github.com/fastruby/harvesting/pull/1 78 | 79 | **Bug Fixes** 80 | 81 | - Fixes issues with specs: https://github.com/fastruby/harvesting/pull/2 82 | - Fixed https://github.com/fastruby/harvesting/issues/6 with https://github.com/fastruby/harvesting/pull/7 83 | 84 | ## Version 0.1.0 - Sep 04, 2018 85 | 86 | **Notes** 87 | 88 | - Initial release 89 | -------------------------------------------------------------------------------- /lib/harvesting/models/base.rb: -------------------------------------------------------------------------------- 1 | module Harvesting 2 | module Models 3 | class Base 4 | # @return [Hash] 5 | attr_accessor :attributes 6 | # @return [Harvesting::Model::Client] 7 | attr_reader :harvest_client 8 | 9 | def initialize(attrs, opts = {}) 10 | @models = {} 11 | @attributes = attrs.dup 12 | @harvest_client = opts[:harvest_client] || Harvesting::Client.new(**opts) 13 | end 14 | 15 | # It calls `create` or `update` depending on the record's ID. If the ID 16 | # is present, then it calls `update`. Otherwise it calls `create` 17 | # 18 | # @see Client#create 19 | # @see Client#update 20 | def save 21 | id.nil? ? create : update 22 | end 23 | 24 | # It creates the record. 25 | # 26 | # @see Client#create 27 | # @return [Harvesting::Models::Base] 28 | def create 29 | @harvest_client.create(self) 30 | end 31 | 32 | # It updates the record. 33 | # 34 | # @see Client#update 35 | # @return [Harvesting::Models::Base] 36 | def update 37 | @harvest_client.update(self) 38 | end 39 | 40 | # It removes the record. 41 | # 42 | # @see Client#delete 43 | # @return [Harvesting::Models::Base] 44 | def delete 45 | @harvest_client.delete(self) 46 | end 47 | 48 | # It returns keys and values for all the attributes of this record. 49 | # 50 | # @return [Hash] 51 | def to_hash 52 | @attributes 53 | end 54 | 55 | # It loads a new record from your Harvest account. 56 | # 57 | # @return [Harvesting::Models::Base] 58 | def fetch 59 | self.class.new(@harvest_client.get(path), harvest_client: @harvest_client) 60 | end 61 | 62 | # Retrieves an instance of the object by ID 63 | # 64 | # @param id [Integer] the id of the object to retrieve 65 | # @param opts [Hash] options to pass along to the `Harvesting::Client` 66 | # instance 67 | def self.get(id, opts = {}) 68 | client = opts[:harvest_client] || Harvesting::Client.new(**opts) 69 | self.new({ 'id' => id }, opts).fetch 70 | end 71 | 72 | protected 73 | 74 | # Class method to define attribute methods for accessing attributes for 75 | # a record 76 | # 77 | # It needs to be used like this: 78 | # 79 | # class Contact < HarvestRecord 80 | # attributed :id, 81 | # :title, 82 | # :first_name 83 | # ... 84 | # end 85 | # 86 | # @param attribute_names [Array] A list of attributes 87 | def self.attributed(*attribute_names) 88 | attribute_names.each do |attribute_name| 89 | define_method(attribute_name) do 90 | @attributes[__method__.to_s] 91 | end 92 | define_method("#{attribute_name}=") do |value| 93 | @attributes[__method__.to_s.chop] = value 94 | end 95 | end 96 | end 97 | 98 | # Class method to define nested resources for a record. 99 | # 100 | # It needs to be used like this: 101 | # 102 | # class Contact < HarvestRecord 103 | # modeled client: Client 104 | # ... 105 | # end 106 | # 107 | # @param opts [Hash] key = symbol that needs to be the same as the one returned by the Harvest API. value = model class for the nested resource. 108 | def self.modeled(opts = {}) 109 | opts.each do |attribute_name, model| 110 | attribute_name_string = attribute_name.to_s 111 | define_method(attribute_name_string) do 112 | @models[attribute_name_string] ||= model.new(@attributes[attribute_name_string] || {}, harvest_client: harvest_client) 113 | end 114 | end 115 | end 116 | end 117 | end 118 | end 119 | -------------------------------------------------------------------------------- /fixtures/vcr_cassettes/Harvesting_Client_user_assignments/as_an_admin_user_retreives_the_accounts_user_assignments.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: https://api.harvestapp.com/v2/user_assignments 6 | body: 7 | encoding: UTF-8 8 | string: '' 9 | headers: 10 | User-Agent: 11 | - Harvesting Ruby Gem 12 | Authorization: 13 | - Bearer $HARVEST_ACCESS_TOKEN 14 | Harvest-Account-Id: 15 | - "$HARVEST_ACCOUNT_ID" 16 | Connection: 17 | - close 18 | Host: 19 | - api.harvestapp.com 20 | response: 21 | status: 22 | code: 200 23 | message: OK 24 | headers: 25 | Server: 26 | - nginx 27 | Date: 28 | - Fri, 25 Jan 2019 18:32:26 GMT 29 | Content-Type: 30 | - application/json; charset=utf-8 31 | Transfer-Encoding: 32 | - chunked 33 | Connection: 34 | - close 35 | Status: 36 | - 200 OK 37 | X-Frame-Options: 38 | - SAMEORIGIN 39 | X-Xss-Protection: 40 | - 1; mode=block 41 | X-Content-Type-Options: 42 | - nosniff 43 | X-Download-Options: 44 | - noopen 45 | X-Permitted-Cross-Domain-Policies: 46 | - none 47 | Referrer-Policy: 48 | - strict-origin-when-cross-origin 49 | Cache-Control: 50 | - no-cache, no-store 51 | P3p: 52 | - 'CP="Our privacy policy is available online: https://www.getharvest.com/services/privacy-policy"' 53 | X-App-Server: 54 | - app5 55 | X-Robots-Tag: 56 | - noindex, nofollow 57 | Content-Security-Policy: 58 | - 'report-uri /csp_reports; default-src *; img-src * data:; font-src data: cache.harvestapp.com 59 | https://js.intercomcdn.com; script-src ''self'' ''unsafe-inline'' ''unsafe-eval'' 60 | https://*.google-analytics.com https://*.nr-data.net https://ajax.googleapis.com 61 | cache.harvestapp.com https://js-agent.newrelic.com https://platform.twitter.com 62 | https://www.google.com https://www.googleadservices.com https://www.googletagmanager.com 63 | https://connect.facebook.net https://googleads.g.doubleclick.net https://app.intercom.io 64 | https://widget.intercom.io https://js.intercomcdn.com https://cdn.plaid.com; 65 | style-src ''self'' ''unsafe-inline'' cache.harvestapp.com https://www.google.com' 66 | X-Request-Id: 67 | - fb0e054e3356577cbd0835e63179c373 68 | X-Runtime: 69 | - '0.146212' 70 | Strict-Transport-Security: 71 | - max-age=31536000; includeSubDomains 72 | X-Server: 73 | - lb4 74 | body: 75 | encoding: UTF-8 76 | string: '{"user_assignments":[{"id":186878708,"is_project_manager":false,"is_active":true,"budget":null,"created_at":"2019-01-25T18:31:12Z","updated_at":"2019-01-25T18:31:12Z","hourly_rate":null,"project":{"id":19919747,"name":"Road 77 | Building","code":null},"user":{"id":2527916,"name":"Jane Doe"}},{"id":186878707,"is_project_manager":false,"is_active":true,"budget":null,"created_at":"2019-01-25T18:31:11Z","updated_at":"2019-01-25T18:31:11Z","hourly_rate":null,"project":{"id":19919748,"name":"Castle 78 | Building","code":null},"user":{"id":2527915,"name":"John Smith"}},{"id":186878706,"is_project_manager":true,"is_active":true,"budget":null,"created_at":"2019-01-25T18:31:10Z","updated_at":"2019-01-25T18:31:10Z","hourly_rate":null,"project":{"id":19919748,"name":"Castle 79 | Building","code":null},"user":{"id":2348607,"name":"Alexander Hamilton"}},{"id":186878705,"is_project_manager":true,"is_active":true,"budget":null,"created_at":"2019-01-25T18:31:10Z","updated_at":"2019-01-25T18:31:10Z","hourly_rate":null,"project":{"id":19919747,"name":"Road 80 | Building","code":null},"user":{"id":2348607,"name":"Alexander Hamilton"}}],"per_page":100,"total_pages":1,"total_entries":4,"next_page":null,"previous_page":null,"page":1,"links":{"first":"https://api.harvestapp.com/v2/user_assignments?page=1&per_page=100","next":null,"previous":null,"last":"https://api.harvestapp.com/v2/user_assignments?page=1&per_page=100"}}' 81 | http_version: 82 | recorded_at: Fri, 25 Jan 2019 18:32:26 GMT 83 | recorded_with: VCR 4.0.0 84 | -------------------------------------------------------------------------------- /spec/harvesting/models/time_entry_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe Harvesting::Models::TimeEntry, :vcr do 4 | include_context "harvest data setup" 5 | 6 | let(:attrs) { Hash.new } 7 | let(:time_entry) { Harvesting::Models::TimeEntry.new(attrs, harvest_client: harvest_client) } 8 | let(:date) { "2018-05-14" } 9 | let(:started_time) { "8:00am" } 10 | let(:ended_time) { "9:00am" } 11 | let(:project_id) { project_castle_building.id } 12 | let(:task_id) { task_coding.id } 13 | let(:user_id) { user_me.id } 14 | 15 | describe "#save" do 16 | context "when id is nil" do 17 | it "calls create" do 18 | allow(time_entry).to receive(:create) 19 | 20 | time_entry.save 21 | 22 | expect(time_entry).to have_received(:create) 23 | end 24 | end 25 | 26 | context "when id is not nil" do 27 | let(:attrs) { Hash.new('id' => 123) } 28 | 29 | it "calls update" do 30 | allow(time_entry).to receive(:update) 31 | 32 | time_entry.save 33 | 34 | expect(time_entry).to have_received(:update) 35 | end 36 | end 37 | end 38 | 39 | describe "#create" do 40 | context "when trying to create a time entry without required attributes" do 41 | let(:error) do 42 | { message: "Project can't be blank, Task can't be blank, Spent date can't be blank, Spent date is not a valid date" }.to_json 43 | end 44 | 45 | it "fails" do 46 | expect do 47 | result = time_entry.save 48 | end.to raise_error(Harvesting::UnprocessableRequest, error) 49 | end 50 | end 51 | 52 | context "when trying to create a time entry with the required attributes" do 53 | let(:attrs) do 54 | { 55 | "project" => { 56 | "id" => project_id.to_s 57 | }, 58 | "task" => { 59 | "id" => task_id.to_s 60 | }, 61 | "spent_date" => date, 62 | "hours" => '1.0', 63 | "user" => { 64 | "id" => user_id.to_s 65 | }, 66 | "is_running" => 'false', 67 | "notes" => 'hacked the things' 68 | } 69 | end 70 | 71 | it "automatically sets the id of the time entry" do 72 | result = time_entry.save 73 | 74 | expect(time_entry.id).not_to be_nil 75 | expect(time_entry.hours).to eq 1 76 | 77 | time_entry.delete 78 | end 79 | end 80 | end 81 | 82 | describe "#update" do 83 | let(:attrs) do 84 | { 85 | "project" => { 86 | "id" => project_id.to_s 87 | }, 88 | "task" => { 89 | "id" => task_id.to_s 90 | }, 91 | "spent_date" => date, 92 | "hours" => '1.0', 93 | "user" => { 94 | "id" => user_id.to_s 95 | }, 96 | "is_running" => 'false', 97 | "notes" => 'hacked the things' 98 | } 99 | end 100 | 101 | context "when updating an existing time entry" do 102 | it "updates the amount of hours" do 103 | # trigger time entry creation 104 | time_entry.save 105 | 106 | # update with a different number of hours 107 | time_entry.hours = '4.0' 108 | time_entry.save 109 | 110 | final_time_entry = Harvesting::Models::TimeEntry.get(time_entry.id) 111 | expect(final_time_entry.hours).to eq(4.0) 112 | end 113 | end 114 | end 115 | 116 | describe "initialize" do 117 | let(:project_name) { 'Harvesting' } 118 | let(:attrs) do 119 | { 120 | 'spent_date' => date, 121 | 'project' => { 122 | 'name' => project_name, 123 | 'id' => project_id 124 | } 125 | } 126 | end 127 | 128 | it 'creates accessors for top-level attributes' do 129 | expect(time_entry.spent_date).to eq(date) 130 | end 131 | 132 | it 'creates accessors for nested attributes' do 133 | expect(time_entry.project.name).to eq(project_name) 134 | expect(time_entry.project.id).to eq(project_id) 135 | end 136 | 137 | it 'does not throw when parent is nil' do 138 | expect(time_entry.user.id).to eq(nil) 139 | end 140 | 141 | it 'creates accessors on instances of this class' do 142 | expect(time_entry).to respond_to(:spent_date) 143 | end 144 | 145 | it 'does not create accessors on instances of other classes' do 146 | expect(time_entry.class.superclass.new(attrs)).not_to respond_to(:spent_date) 147 | end 148 | end 149 | end 150 | -------------------------------------------------------------------------------- /fixtures/vcr_cassettes/Harvesting_Client_invoices/when_account_has_invoices_with_custom_options_only_returns_the_invoices_mathing_the_options.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: https://api.harvestapp.com/v2/invoices?state=draft 6 | body: 7 | encoding: UTF-8 8 | string: '' 9 | headers: 10 | User-Agent: 11 | - Harvesting Ruby Gem 12 | Authorization: 13 | - Bearer $HARVEST_ACCESS_TOKEN 14 | Harvest-Account-Id: 15 | - "$HARVEST_ACCOUNT_ID" 16 | Connection: 17 | - close 18 | Host: 19 | - api.harvestapp.com 20 | response: 21 | status: 22 | code: 200 23 | message: OK 24 | headers: 25 | Server: 26 | - nginx 27 | Date: 28 | - Tue, 07 Apr 2020 07:34:47 GMT 29 | Content-Type: 30 | - application/json; charset=utf-8 31 | Transfer-Encoding: 32 | - chunked 33 | Connection: 34 | - close 35 | Status: 36 | - 200 OK 37 | X-Frame-Options: 38 | - SAMEORIGIN 39 | X-Xss-Protection: 40 | - 1; mode=block 41 | X-Content-Type-Options: 42 | - nosniff 43 | X-Download-Options: 44 | - noopen 45 | X-Permitted-Cross-Domain-Policies: 46 | - none 47 | Referrer-Policy: 48 | - strict-origin-when-cross-origin 49 | Cache-Control: 50 | - no-cache, no-store 51 | P3p: 52 | - 'CP="Our privacy policy is available online: https://www.getharvest.com/services/privacy-policy"' 53 | X-App-Server: 54 | - app11 55 | X-Robots-Tag: 56 | - noindex, nofollow 57 | Content-Security-Policy: 58 | - 'report-uri /csp_reports; default-src *; img-src * data:; font-src data: cache.harvestapp.com 59 | https://js.intercomcdn.com https://fonts.gstatic.com; script-src ''self'' 60 | ''unsafe-inline'' ''unsafe-eval'' https://*.google-analytics.com https://*.nr-data.net 61 | https://ajax.googleapis.com cache.harvestapp.com https://platform.twitter.com 62 | https://www.google.com https://www.googleadservices.com https://www.googletagmanager.com 63 | https://connect.facebook.net https://googleads.g.doubleclick.net https://app.intercom.io 64 | https://widget.intercom.io https://js.intercomcdn.com https://cdn.plaid.com 65 | https://tagmanager.google.com https://bat.bing.com https://ct.capterra.com; 66 | style-src ''self'' ''unsafe-inline'' cache.harvestapp.com https://www.google.com 67 | https://tagmanager.google.com https://fonts.googleapis.com' 68 | X-Request-Id: 69 | - ebba0451a1c81c9aefdd30921365557b 70 | X-Runtime: 71 | - '0.127891' 72 | Strict-Transport-Security: 73 | - max-age=31536000; includeSubDomains 74 | X-Server: 75 | - lb3 76 | body: 77 | encoding: UTF-8 78 | string: '{"invoices":[{"id":23831208,"client_key":"73688e97a43ed497ace45939eb76db6b18427b80","number":"3","purchase_order":"","amount":750.0,"due_amount":750.0,"tax":null,"tax_amount":0.0,"tax2":null,"tax2_amount":0.0,"discount":null,"discount_amount":0.0,"subject":"","notes":"","state":"draft","period_start":null,"period_end":null,"issue_date":"2020-04-07","due_date":"2020-04-07","payment_term":"upon 79 | receipt","sent_at":null,"paid_at":null,"closed_at":null,"recurring_invoice_id":null,"created_at":"2020-04-07T07:27:13Z","updated_at":"2020-04-07T07:27:13Z","paid_date":null,"currency":"DKK","client":{"id":9343060,"name":"Traels.it"},"estimate":null,"retainer":null,"creator":{"id":3211810,"name":"Nicolai 80 | Bach Woller"},"line_items":[{"id":109677268,"kind":"Service","description":"","quantity":3.0,"unit_price":250.0,"amount":750.0,"taxed":false,"taxed2":false,"project":{"id":24566828,"name":"Harvest 81 | Billing Automation","code":""}}]},{"id":23831195,"client_key":"ef41ef4e8ce6475fce47948bfc0aca88a9ab29e9","number":"1","purchase_order":"","amount":4500.0,"due_amount":4500.0,"tax":null,"tax_amount":0.0,"tax2":null,"tax2_amount":0.0,"discount":null,"discount_amount":0.0,"subject":"","notes":"","state":"draft","period_start":"2020-04-04","period_end":"2020-04-06","issue_date":"2020-04-07","due_date":"2020-04-07","payment_term":"upon 82 | receipt","sent_at":null,"paid_at":null,"closed_at":null,"recurring_invoice_id":null,"created_at":"2020-04-07T07:25:33Z","updated_at":"2020-04-07T07:25:33Z","paid_date":null,"currency":"DKK","client":{"id":9343060,"name":"Traels.it"},"estimate":null,"retainer":null,"creator":{"id":3211810,"name":"Nicolai 83 | Bach Woller"},"line_items":[{"id":109677238,"kind":"Service","description":"Harvest 84 | Billing Automation - A note","quantity":3.0,"unit_price":300.0,"amount":900.0,"taxed":false,"taxed2":false,"project":{"id":24566828,"name":"Harvest 85 | Billing Automation","code":""}},{"id":109677239,"kind":"Service","description":"Harvest 86 | Billing Automation - Another note","quantity":2.0,"unit_price":300.0,"amount":600.0,"taxed":false,"taxed2":false,"project":{"id":24566828,"name":"Harvest 87 | Billing Automation","code":""}},{"id":109677240,"kind":"Service","description":"Harvest 88 | Billing Automation - A third note","quantity":2.0,"unit_price":300.0,"amount":600.0,"taxed":false,"taxed2":false,"project":{"id":24566828,"name":"Harvest 89 | Billing Automation","code":""}},{"id":109677241,"kind":"Service","description":"Harvest 90 | Billing Automation","quantity":5.0,"unit_price":300.0,"amount":1500.0,"taxed":false,"taxed2":false,"project":{"id":24566828,"name":"Harvest 91 | Billing Automation","code":""}},{"id":109677242,"kind":"Service","description":"Harvest 92 | Billing Automation - A note\r\nAnother note\r\nA third note","quantity":3.0,"unit_price":300.0,"amount":900.0,"taxed":false,"taxed2":false,"project":{"id":24566828,"name":"Harvest 93 | Billing Automation","code":""}}]}],"per_page":100,"total_pages":1,"total_entries":2,"next_page":null,"previous_page":null,"page":1,"links":{"first":"https://api.harvestapp.com/v2/invoices?page=1&per_page=100&state=draft","next":null,"previous":null,"last":"https://api.harvestapp.com/v2/invoices?page=1&per_page=100&state=draft"}}' 94 | http_version: 95 | recorded_at: Tue, 07 Apr 2020 07:34:48 GMT 96 | recorded_with: VCR 4.0.0 97 | -------------------------------------------------------------------------------- /lib/harvesting/client.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require "http" 3 | require "json" 4 | 5 | module Harvesting 6 | 7 | # A client for the Harvest API (version 2.0) 8 | class Client 9 | DEFAULT_HOST = "https://api.harvestapp.com/v2" 10 | 11 | attr_accessor :access_token, :account_id 12 | 13 | # Returns a new instance of `Client` 14 | # 15 | # client = Client.new({ access_token: "12345678", account_id: "98764" }) 16 | # 17 | # @param [Hash] opts the options to create an API client 18 | # @option opts [String] :access_token Harvest access token 19 | # @option opts [String] :account_id Harvest account id 20 | def initialize(access_token: ENV['HARVEST_ACCESS_TOKEN'], account_id: ENV['HARVEST_ACCOUNT_ID']) 21 | @access_token = access_token.to_s 22 | @account_id = account_id.to_s 23 | 24 | if @account_id.length == 0 || @access_token.length == 0 25 | raise ArgumentError.new("Access token and account id are required. Access token: '#{@access_token}'. Account ID: '#{@account_id}'.") 26 | end 27 | end 28 | 29 | # @return [Harvesting::Models::User] 30 | def me 31 | Harvesting::Models::User.new(get("users/me"), harvest_client: self) 32 | end 33 | 34 | # @return [Harvesting::Models::Clients] 35 | def clients(opts = {}) 36 | Harvesting::Models::Clients.new(get("clients", opts), opts, harvest_client: self) 37 | end 38 | 39 | # @return [Array] 40 | def contacts 41 | get("contacts")["contacts"].map do |result| 42 | Harvesting::Models::Contact.new(result, harvest_client: self) 43 | end 44 | end 45 | 46 | # @return [Harvesting::Models::TimeEntries] 47 | def time_entries(opts = {}) 48 | Harvesting::Models::TimeEntries.new(get("time_entries", opts), opts, harvest_client: self) 49 | end 50 | 51 | # @return [Harvesting::Models::Projects] 52 | def projects(opts = {}) 53 | Harvesting::Models::Projects.new(get("projects", opts), opts, harvest_client: self) 54 | end 55 | 56 | # @return [Harvesting::Models::Tasks] 57 | def tasks(opts = {}) 58 | Harvesting::Models::Tasks.new(get("tasks", opts), opts, harvest_client: self) 59 | end 60 | 61 | # @return [Harvesting::Models::Users] 62 | def users(opts = {}) 63 | Harvesting::Models::Users.new(get("users", opts), opts, harvest_client: self) 64 | end 65 | 66 | # @return [Array] 67 | def invoices(opts = {}) 68 | Harvesting::Models::Invoices.new(get("invoices", opts), opts, harvest_client: self) 69 | end 70 | 71 | # @return [Harvesting::Models::ProjectUserAssignments] 72 | def user_assignments(opts = {}) 73 | project_id = opts.delete(:project_id) 74 | path = project_id.nil? ? "user_assignments" : "projects/#{project_id}/user_assignments" 75 | Harvesting::Models::ProjectUserAssignments.new(get(path, opts), opts, harvest_client: self) 76 | end 77 | 78 | # @return [Harvesting::Models::ProjectTaskAssignments] 79 | def task_assignments(opts = {}) 80 | project_id = opts.delete(:project_id) 81 | path = project_id.nil? ? "task_assignments" : "projects/#{project_id}/task_assignments" 82 | Harvesting::Models::ProjectTaskAssignments.new(get(path, opts), opts, harvest_client: self) 83 | end 84 | 85 | # Creates an `entity` in your Harvest account. 86 | # 87 | # @param entity [Harvesting::Models::Base] A new record in your Harvest account 88 | # @return [Harvesting::Models::Base] A subclass of `Harvesting::Models::Base` updated with the response from Harvest 89 | def create(entity) 90 | url = "#{DEFAULT_HOST}/#{entity.path}" 91 | uri = URI(url) 92 | response = http_response(:post, uri, body: entity.to_hash) 93 | entity.attributes = JSON.parse(response.body) 94 | entity 95 | end 96 | 97 | # Updates an `entity` in your Harvest account. 98 | # 99 | # @param entity [Harvesting::Models::Base] An existing record in your Harvest account 100 | # @return [Harvesting::Models::Base] A subclass of `Harvesting::Models::Base` updated with the response from Harvest 101 | def update(entity) 102 | url = "#{DEFAULT_HOST}/#{entity.path}" 103 | uri = URI(url) 104 | response = http_response(:patch, uri, body: entity.to_hash) 105 | entity.attributes = JSON.parse(response.body) 106 | entity 107 | end 108 | 109 | # It removes an `entity` from your Harvest account. 110 | # 111 | # @param entity [Harvesting::Models::Base] A record to be removed from your Harvest account 112 | # @return [Hash] 113 | # @raise [UnprocessableRequest] When HTTP response is not 200 OK 114 | def delete(entity) 115 | url = "#{DEFAULT_HOST}/#{entity.path}" 116 | uri = URI(url) 117 | response = http_response(:delete, uri) 118 | raise UnprocessableRequest.new(response.to_s) unless response.code.to_i == 200 119 | 120 | JSON.parse(response.body) 121 | end 122 | 123 | # Performs a GET request and returned the parsed JSON as a Hash. 124 | # 125 | # @param path [String] path to be combined with `DEFAULT_HOST` 126 | # @param opts [Hash] key/values will get passed as HTTP (GET) parameters 127 | # @return [Hash] 128 | def get(path, opts = {}) 129 | url = "#{DEFAULT_HOST}/#{path}" 130 | url += "?#{opts.map {|k, v| "#{k}=#{v}"}.join("&")}" if opts.any? 131 | uri = URI(url) 132 | response = http_response(:get, uri) 133 | JSON.parse(response.body) 134 | end 135 | 136 | private 137 | 138 | def http_response(method, uri, opts = {}) 139 | response = nil 140 | 141 | http = HTTP["User-Agent" => "Harvesting Ruby Gem", 142 | "Authorization" => "Bearer #{@access_token}", 143 | "Harvest-Account-ID" => @account_id] 144 | params = {} 145 | if opts[:body] 146 | params[:json] = opts[:body] 147 | end 148 | response = http.send(method, uri, params) 149 | 150 | raise Harvesting::AuthenticationError.new(response.to_s) if auth_error?(response) 151 | raise Harvesting::UnprocessableRequest.new(response.to_s) if response.code.to_i == 422 152 | raise Harvesting::RequestNotFound.new(uri) if response.code.to_i == 404 153 | raise Harvesting::RateLimitExceeded.new(response.to_s) if response.code.to_i == 429 154 | 155 | response 156 | end 157 | 158 | def auth_error?(response) 159 | response.code.to_i == 403 || response.code.to_i == 401 160 | end 161 | end 162 | end 163 | -------------------------------------------------------------------------------- /fixtures/vcr_cassettes/Harvesting_Models_ProjectUserAssignment_create/without_a_project_id_raises_Harvesting_RequestNotFound_error.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: post 5 | uri: https://api.harvestapp.com/v2/projects//user_assignments 6 | body: 7 | encoding: UTF-8 8 | string: '{"project_id":null,"user_id":"5678","project":{"id":null},"user":{"id":"5678"}}' 9 | headers: 10 | User-Agent: 11 | - Harvesting Ruby Gem 12 | Authorization: 13 | - Bearer $HARVEST_ACCESS_TOKEN 14 | Harvest-Account-Id: 15 | - "$HARVEST_ACCOUNT_ID" 16 | Connection: 17 | - close 18 | Content-Type: 19 | - application/json; charset=UTF-8 20 | Host: 21 | - api.harvestapp.com 22 | response: 23 | status: 24 | code: 404 25 | message: Not Found 26 | headers: 27 | Server: 28 | - nginx 29 | Date: 30 | - Fri, 21 Aug 2020 20:25:16 GMT 31 | Content-Type: 32 | - text/html; charset=UTF-8 33 | Content-Length: 34 | - '4044' 35 | Connection: 36 | - close 37 | Status: 38 | - 404 Not Found 39 | X-Request-Id: 40 | - 47a13ab18fb6e5f8f85c2827e9b782da 41 | X-Runtime: 42 | - '0.028918' 43 | Strict-Transport-Security: 44 | - max-age=31536000; includeSubDomains 45 | body: 46 | encoding: UTF-8 47 | string: | 48 | 49 | 50 | 51 | 52 | 53 | HARVEST: Canʼt find page 54 | 55 | 56 | 57 | 64 | 65 | 90 | 115 | 116 | 117 |
118 |
119 | 130 |

We canʼt find the page youʼre looking for.

131 | 132 |

133 | Sorry about that! The page you were looking for may have been moved or the address misspelled. 134 |

135 | 136 | 140 | 145 |
146 |
147 | 148 | 149 | http_version: 150 | recorded_at: Fri, 21 Aug 2020 20:25:16 GMT 151 | recorded_with: VCR 4.0.0 152 | -------------------------------------------------------------------------------- /fixtures/vcr_cassettes/Harvesting_Client_invoices/when_account_has_invoices_has_line_item_for_invoices.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: https://api.harvestapp.com/v2/invoices 6 | body: 7 | encoding: UTF-8 8 | string: '' 9 | headers: 10 | User-Agent: 11 | - Harvesting Ruby Gem 12 | Authorization: 13 | - Bearer $HARVEST_ACCESS_TOKEN 14 | Harvest-Account-Id: 15 | - "$HARVEST_ACCOUNT_ID" 16 | Connection: 17 | - close 18 | Host: 19 | - api.harvestapp.com 20 | response: 21 | status: 22 | code: 200 23 | message: OK 24 | headers: 25 | Server: 26 | - nginx 27 | Date: 28 | - Tue, 07 Apr 2020 07:36:09 GMT 29 | Content-Type: 30 | - application/json; charset=utf-8 31 | Transfer-Encoding: 32 | - chunked 33 | Connection: 34 | - close 35 | Status: 36 | - 200 OK 37 | X-Frame-Options: 38 | - SAMEORIGIN 39 | X-Xss-Protection: 40 | - 1; mode=block 41 | X-Content-Type-Options: 42 | - nosniff 43 | X-Download-Options: 44 | - noopen 45 | X-Permitted-Cross-Domain-Policies: 46 | - none 47 | Referrer-Policy: 48 | - strict-origin-when-cross-origin 49 | Cache-Control: 50 | - no-cache, no-store 51 | P3p: 52 | - 'CP="Our privacy policy is available online: https://www.getharvest.com/services/privacy-policy"' 53 | X-App-Server: 54 | - app3 55 | X-Robots-Tag: 56 | - noindex, nofollow 57 | Content-Security-Policy: 58 | - 'report-uri /csp_reports; default-src *; img-src * data:; font-src data: cache.harvestapp.com 59 | https://js.intercomcdn.com https://fonts.gstatic.com; script-src ''self'' 60 | ''unsafe-inline'' ''unsafe-eval'' https://*.google-analytics.com https://*.nr-data.net 61 | https://ajax.googleapis.com cache.harvestapp.com https://platform.twitter.com 62 | https://www.google.com https://www.googleadservices.com https://www.googletagmanager.com 63 | https://connect.facebook.net https://googleads.g.doubleclick.net https://app.intercom.io 64 | https://widget.intercom.io https://js.intercomcdn.com https://cdn.plaid.com 65 | https://tagmanager.google.com https://bat.bing.com https://ct.capterra.com; 66 | style-src ''self'' ''unsafe-inline'' cache.harvestapp.com https://www.google.com 67 | https://tagmanager.google.com https://fonts.googleapis.com' 68 | X-Request-Id: 69 | - 2bf206cbc99dd054031b28b1ba33f368 70 | X-Runtime: 71 | - '0.115310' 72 | Strict-Transport-Security: 73 | - max-age=31536000; includeSubDomains 74 | X-Server: 75 | - lb3 76 | body: 77 | encoding: UTF-8 78 | string: '{"invoices":[{"id":23831208,"client_key":"73688e97a43ed497ace45939eb76db6b18427b80","number":"3","purchase_order":"","amount":750.0,"due_amount":750.0,"tax":null,"tax_amount":0.0,"tax2":null,"tax2_amount":0.0,"discount":null,"discount_amount":0.0,"subject":"","notes":"","state":"draft","period_start":null,"period_end":null,"issue_date":"2020-04-07","due_date":"2020-04-07","payment_term":"upon 79 | receipt","sent_at":null,"paid_at":null,"closed_at":null,"recurring_invoice_id":null,"created_at":"2020-04-07T07:27:13Z","updated_at":"2020-04-07T07:27:13Z","paid_date":null,"currency":"DKK","client":{"id":9343060,"name":"Traels.it"},"estimate":null,"retainer":null,"creator":{"id":3211810,"name":"Nicolai 80 | Bach Woller"},"line_items":[{"id":109677268,"kind":"Service","description":"","quantity":3.0,"unit_price":250.0,"amount":750.0,"taxed":false,"taxed2":false,"project":{"id":24566828,"name":"Harvest 81 | Billing Automation","code":""}}]},{"id":23831206,"client_key":"1da2f2458310563da46daebe4b732d2417458a05","number":"2","purchase_order":"","amount":750.0,"due_amount":0.0,"tax":null,"tax_amount":0.0,"tax2":null,"tax2_amount":0.0,"discount":null,"discount_amount":0.0,"subject":"","notes":"","state":"paid","period_start":null,"period_end":null,"issue_date":"2020-04-07","due_date":"2020-04-07","payment_term":"upon 82 | receipt","sent_at":null,"paid_at":"2020-04-07T00:00:00Z","closed_at":null,"recurring_invoice_id":null,"created_at":"2020-04-07T07:26:55Z","updated_at":"2020-04-07T07:27:37Z","paid_date":"2020-04-07","currency":"DKK","client":{"id":9343060,"name":"Traels.it"},"estimate":null,"retainer":null,"creator":{"id":3211810,"name":"Nicolai 83 | Bach Woller"},"line_items":[{"id":109677264,"kind":"Service","description":"","quantity":3.0,"unit_price":250.0,"amount":750.0,"taxed":false,"taxed2":false,"project":{"id":24566828,"name":"Harvest 84 | Billing Automation","code":""}}]},{"id":23831195,"client_key":"ef41ef4e8ce6475fce47948bfc0aca88a9ab29e9","number":"1","purchase_order":"","amount":4500.0,"due_amount":4500.0,"tax":null,"tax_amount":0.0,"tax2":null,"tax2_amount":0.0,"discount":null,"discount_amount":0.0,"subject":"","notes":"","state":"draft","period_start":"2020-04-04","period_end":"2020-04-06","issue_date":"2020-04-07","due_date":"2020-04-07","payment_term":"upon 85 | receipt","sent_at":null,"paid_at":null,"closed_at":null,"recurring_invoice_id":null,"created_at":"2020-04-07T07:25:33Z","updated_at":"2020-04-07T07:25:33Z","paid_date":null,"currency":"DKK","client":{"id":9343060,"name":"Traels.it"},"estimate":null,"retainer":null,"creator":{"id":3211810,"name":"Nicolai 86 | Bach Woller"},"line_items":[{"id":109677238,"kind":"Service","description":"Harvest 87 | Billing Automation - A note","quantity":3.0,"unit_price":300.0,"amount":900.0,"taxed":false,"taxed2":false,"project":{"id":24566828,"name":"Harvest 88 | Billing Automation","code":""}},{"id":109677239,"kind":"Service","description":"Harvest 89 | Billing Automation - Another note","quantity":2.0,"unit_price":300.0,"amount":600.0,"taxed":false,"taxed2":false,"project":{"id":24566828,"name":"Harvest 90 | Billing Automation","code":""}},{"id":109677240,"kind":"Service","description":"Harvest 91 | Billing Automation - A third note","quantity":2.0,"unit_price":300.0,"amount":600.0,"taxed":false,"taxed2":false,"project":{"id":24566828,"name":"Harvest 92 | Billing Automation","code":""}},{"id":109677241,"kind":"Service","description":"Harvest 93 | Billing Automation","quantity":5.0,"unit_price":300.0,"amount":1500.0,"taxed":false,"taxed2":false,"project":{"id":24566828,"name":"Harvest 94 | Billing Automation","code":""}},{"id":109677242,"kind":"Service","description":"Harvest 95 | Billing Automation - A note\r\nAnother note\r\nA third note","quantity":3.0,"unit_price":300.0,"amount":900.0,"taxed":false,"taxed2":false,"project":{"id":24566828,"name":"Harvest 96 | Billing Automation","code":""}}]}],"per_page":100,"total_pages":1,"total_entries":3,"next_page":null,"previous_page":null,"page":1,"links":{"first":"https://api.harvestapp.com/v2/invoices?page=1&per_page=100","next":null,"previous":null,"last":"https://api.harvestapp.com/v2/invoices?page=1&per_page=100"}}' 97 | http_version: 98 | recorded_at: Tue, 07 Apr 2020 07:36:09 GMT 99 | recorded_with: VCR 4.0.0 100 | -------------------------------------------------------------------------------- /fixtures/vcr_cassettes/Harvesting_Client_invoices/when_account_has_invoices_builds_line_items_for_invoices.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: https://api.harvestapp.com/v2/invoices 6 | body: 7 | encoding: UTF-8 8 | string: '' 9 | headers: 10 | User-Agent: 11 | - Harvesting Ruby Gem 12 | Authorization: 13 | - Bearer $HARVEST_ACCESS_TOKEN 14 | Harvest-Account-Id: 15 | - "$HARVEST_ACCOUNT_ID" 16 | Connection: 17 | - close 18 | Host: 19 | - api.harvestapp.com 20 | response: 21 | status: 22 | code: 200 23 | message: OK 24 | headers: 25 | Server: 26 | - nginx 27 | Date: 28 | - Tue, 07 Apr 2020 07:48:27 GMT 29 | Content-Type: 30 | - application/json; charset=utf-8 31 | Transfer-Encoding: 32 | - chunked 33 | Connection: 34 | - close 35 | Status: 36 | - 200 OK 37 | X-Frame-Options: 38 | - SAMEORIGIN 39 | X-Xss-Protection: 40 | - 1; mode=block 41 | X-Content-Type-Options: 42 | - nosniff 43 | X-Download-Options: 44 | - noopen 45 | X-Permitted-Cross-Domain-Policies: 46 | - none 47 | Referrer-Policy: 48 | - strict-origin-when-cross-origin 49 | Cache-Control: 50 | - no-cache, no-store 51 | P3p: 52 | - 'CP="Our privacy policy is available online: https://www.getharvest.com/services/privacy-policy"' 53 | X-App-Server: 54 | - app12 55 | X-Robots-Tag: 56 | - noindex, nofollow 57 | Content-Security-Policy: 58 | - 'report-uri /csp_reports; default-src *; img-src * data:; font-src data: cache.harvestapp.com 59 | https://js.intercomcdn.com https://fonts.gstatic.com; script-src ''self'' 60 | ''unsafe-inline'' ''unsafe-eval'' https://*.google-analytics.com https://*.nr-data.net 61 | https://ajax.googleapis.com cache.harvestapp.com https://platform.twitter.com 62 | https://www.google.com https://www.googleadservices.com https://www.googletagmanager.com 63 | https://connect.facebook.net https://googleads.g.doubleclick.net https://app.intercom.io 64 | https://widget.intercom.io https://js.intercomcdn.com https://cdn.plaid.com 65 | https://tagmanager.google.com https://bat.bing.com https://ct.capterra.com; 66 | style-src ''self'' ''unsafe-inline'' cache.harvestapp.com https://www.google.com 67 | https://tagmanager.google.com https://fonts.googleapis.com' 68 | X-Request-Id: 69 | - 8b74dc6dcb057b9e64db2a76ff1ef503 70 | X-Runtime: 71 | - '0.110585' 72 | Strict-Transport-Security: 73 | - max-age=31536000; includeSubDomains 74 | X-Server: 75 | - lb1 76 | body: 77 | encoding: UTF-8 78 | string: '{"invoices":[{"id":23831208,"client_key":"73688e97a43ed497ace45939eb76db6b18427b80","number":"3","purchase_order":"","amount":750.0,"due_amount":750.0,"tax":null,"tax_amount":0.0,"tax2":null,"tax2_amount":0.0,"discount":null,"discount_amount":0.0,"subject":"","notes":"","state":"draft","period_start":null,"period_end":null,"issue_date":"2020-04-07","due_date":"2020-04-07","payment_term":"upon 79 | receipt","sent_at":null,"paid_at":null,"closed_at":null,"recurring_invoice_id":null,"created_at":"2020-04-07T07:27:13Z","updated_at":"2020-04-07T07:27:13Z","paid_date":null,"currency":"DKK","client":{"id":9343060,"name":"Traels.it"},"estimate":null,"retainer":null,"creator":{"id":3211810,"name":"Nicolai 80 | Bach Woller"},"line_items":[{"id":109677268,"kind":"Service","description":"","quantity":3.0,"unit_price":250.0,"amount":750.0,"taxed":false,"taxed2":false,"project":{"id":24566828,"name":"Harvest 81 | Billing Automation","code":""}}]},{"id":23831206,"client_key":"1da2f2458310563da46daebe4b732d2417458a05","number":"2","purchase_order":"","amount":750.0,"due_amount":0.0,"tax":null,"tax_amount":0.0,"tax2":null,"tax2_amount":0.0,"discount":null,"discount_amount":0.0,"subject":"","notes":"","state":"paid","period_start":null,"period_end":null,"issue_date":"2020-04-07","due_date":"2020-04-07","payment_term":"upon 82 | receipt","sent_at":null,"paid_at":"2020-04-07T00:00:00Z","closed_at":null,"recurring_invoice_id":null,"created_at":"2020-04-07T07:26:55Z","updated_at":"2020-04-07T07:27:37Z","paid_date":"2020-04-07","currency":"DKK","client":{"id":9343060,"name":"Traels.it"},"estimate":null,"retainer":null,"creator":{"id":3211810,"name":"Nicolai 83 | Bach Woller"},"line_items":[{"id":109677264,"kind":"Service","description":"","quantity":3.0,"unit_price":250.0,"amount":750.0,"taxed":false,"taxed2":false,"project":{"id":24566828,"name":"Harvest 84 | Billing Automation","code":""}}]},{"id":23831195,"client_key":"ef41ef4e8ce6475fce47948bfc0aca88a9ab29e9","number":"1","purchase_order":"","amount":4500.0,"due_amount":4500.0,"tax":null,"tax_amount":0.0,"tax2":null,"tax2_amount":0.0,"discount":null,"discount_amount":0.0,"subject":"","notes":"","state":"draft","period_start":"2020-04-04","period_end":"2020-04-06","issue_date":"2020-04-07","due_date":"2020-04-07","payment_term":"upon 85 | receipt","sent_at":null,"paid_at":null,"closed_at":null,"recurring_invoice_id":null,"created_at":"2020-04-07T07:25:33Z","updated_at":"2020-04-07T07:25:33Z","paid_date":null,"currency":"DKK","client":{"id":9343060,"name":"Traels.it"},"estimate":null,"retainer":null,"creator":{"id":3211810,"name":"Nicolai 86 | Bach Woller"},"line_items":[{"id":109677238,"kind":"Service","description":"Harvest 87 | Billing Automation - A note","quantity":3.0,"unit_price":300.0,"amount":900.0,"taxed":false,"taxed2":false,"project":{"id":24566828,"name":"Harvest 88 | Billing Automation","code":""}},{"id":109677239,"kind":"Service","description":"Harvest 89 | Billing Automation - Another note","quantity":2.0,"unit_price":300.0,"amount":600.0,"taxed":false,"taxed2":false,"project":{"id":24566828,"name":"Harvest 90 | Billing Automation","code":""}},{"id":109677240,"kind":"Service","description":"Harvest 91 | Billing Automation - A third note","quantity":2.0,"unit_price":300.0,"amount":600.0,"taxed":false,"taxed2":false,"project":{"id":24566828,"name":"Harvest 92 | Billing Automation","code":""}},{"id":109677241,"kind":"Service","description":"Harvest 93 | Billing Automation","quantity":5.0,"unit_price":300.0,"amount":1500.0,"taxed":false,"taxed2":false,"project":{"id":24566828,"name":"Harvest 94 | Billing Automation","code":""}},{"id":109677242,"kind":"Service","description":"Harvest 95 | Billing Automation - A note\r\nAnother note\r\nA third note","quantity":3.0,"unit_price":300.0,"amount":900.0,"taxed":false,"taxed2":false,"project":{"id":24566828,"name":"Harvest 96 | Billing Automation","code":""}}]}],"per_page":100,"total_pages":1,"total_entries":3,"next_page":null,"previous_page":null,"page":1,"links":{"first":"https://api.harvestapp.com/v2/invoices?page=1&per_page=100","next":null,"previous":null,"last":"https://api.harvestapp.com/v2/invoices?page=1&per_page=100"}}' 97 | http_version: 98 | recorded_at: Tue, 07 Apr 2020 07:48:27 GMT 99 | recorded_with: VCR 4.0.0 100 | -------------------------------------------------------------------------------- /fixtures/vcr_cassettes/Harvesting_Client_invoices/when_account_has_invoices_returns_the_invoices_associated_with_the_account.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: https://api.harvestapp.com/v2/invoices 6 | body: 7 | encoding: UTF-8 8 | string: '' 9 | headers: 10 | User-Agent: 11 | - Harvesting Ruby Gem 12 | Authorization: 13 | - Bearer $HARVEST_ACCESS_TOKEN 14 | Harvest-Account-Id: 15 | - "$HARVEST_ACCOUNT_ID" 16 | Connection: 17 | - close 18 | Host: 19 | - api.harvestapp.com 20 | response: 21 | status: 22 | code: 200 23 | message: OK 24 | headers: 25 | Server: 26 | - nginx 27 | Date: 28 | - Tue, 07 Apr 2020 07:28:25 GMT 29 | Content-Type: 30 | - application/json; charset=utf-8 31 | Transfer-Encoding: 32 | - chunked 33 | Connection: 34 | - close 35 | Status: 36 | - 200 OK 37 | X-Frame-Options: 38 | - SAMEORIGIN 39 | X-Xss-Protection: 40 | - 1; mode=block 41 | X-Content-Type-Options: 42 | - nosniff 43 | X-Download-Options: 44 | - noopen 45 | X-Permitted-Cross-Domain-Policies: 46 | - none 47 | Referrer-Policy: 48 | - strict-origin-when-cross-origin 49 | Cache-Control: 50 | - no-cache, no-store 51 | P3p: 52 | - 'CP="Our privacy policy is available online: https://www.getharvest.com/services/privacy-policy"' 53 | X-App-Server: 54 | - app7 55 | X-Robots-Tag: 56 | - noindex, nofollow 57 | Content-Security-Policy: 58 | - 'report-uri /csp_reports; default-src *; img-src * data:; font-src data: cache.harvestapp.com 59 | https://js.intercomcdn.com https://fonts.gstatic.com; script-src ''self'' 60 | ''unsafe-inline'' ''unsafe-eval'' https://*.google-analytics.com https://*.nr-data.net 61 | https://ajax.googleapis.com cache.harvestapp.com https://platform.twitter.com 62 | https://www.google.com https://www.googleadservices.com https://www.googletagmanager.com 63 | https://connect.facebook.net https://googleads.g.doubleclick.net https://app.intercom.io 64 | https://widget.intercom.io https://js.intercomcdn.com https://cdn.plaid.com 65 | https://tagmanager.google.com https://bat.bing.com https://ct.capterra.com; 66 | style-src ''self'' ''unsafe-inline'' cache.harvestapp.com https://www.google.com 67 | https://tagmanager.google.com https://fonts.googleapis.com' 68 | X-Request-Id: 69 | - 8f78b1d818f61cab8b7531ae69f4de1a 70 | X-Runtime: 71 | - '0.137932' 72 | Strict-Transport-Security: 73 | - max-age=31536000; includeSubDomains 74 | X-Server: 75 | - lb1 76 | body: 77 | encoding: UTF-8 78 | string: '{"invoices":[{"id":23831208,"client_key":"73688e97a43ed497ace45939eb76db6b18427b80","number":"3","purchase_order":"","amount":750.0,"due_amount":750.0,"tax":null,"tax_amount":0.0,"tax2":null,"tax2_amount":0.0,"discount":null,"discount_amount":0.0,"subject":"","notes":"","state":"draft","period_start":null,"period_end":null,"issue_date":"2020-04-07","due_date":"2020-04-07","payment_term":"upon 79 | receipt","sent_at":null,"paid_at":null,"closed_at":null,"recurring_invoice_id":null,"created_at":"2020-04-07T07:27:13Z","updated_at":"2020-04-07T07:27:13Z","paid_date":null,"currency":"DKK","client":{"id":9343060,"name":"Traels.it"},"estimate":null,"retainer":null,"creator":{"id":3211810,"name":"Nicolai 80 | Bach Woller"},"line_items":[{"id":109677268,"kind":"Service","description":"","quantity":3.0,"unit_price":250.0,"amount":750.0,"taxed":false,"taxed2":false,"project":{"id":24566828,"name":"Harvest 81 | Billing Automation","code":""}}]},{"id":23831206,"client_key":"1da2f2458310563da46daebe4b732d2417458a05","number":"2","purchase_order":"","amount":750.0,"due_amount":0.0,"tax":null,"tax_amount":0.0,"tax2":null,"tax2_amount":0.0,"discount":null,"discount_amount":0.0,"subject":"","notes":"","state":"paid","period_start":null,"period_end":null,"issue_date":"2020-04-07","due_date":"2020-04-07","payment_term":"upon 82 | receipt","sent_at":null,"paid_at":"2020-04-07T00:00:00Z","closed_at":null,"recurring_invoice_id":null,"created_at":"2020-04-07T07:26:55Z","updated_at":"2020-04-07T07:27:37Z","paid_date":"2020-04-07","currency":"DKK","client":{"id":9343060,"name":"Traels.it"},"estimate":null,"retainer":null,"creator":{"id":3211810,"name":"Nicolai 83 | Bach Woller"},"line_items":[{"id":109677264,"kind":"Service","description":"","quantity":3.0,"unit_price":250.0,"amount":750.0,"taxed":false,"taxed2":false,"project":{"id":24566828,"name":"Harvest 84 | Billing Automation","code":""}}]},{"id":23831195,"client_key":"ef41ef4e8ce6475fce47948bfc0aca88a9ab29e9","number":"1","purchase_order":"","amount":4500.0,"due_amount":4500.0,"tax":null,"tax_amount":0.0,"tax2":null,"tax2_amount":0.0,"discount":null,"discount_amount":0.0,"subject":"","notes":"","state":"draft","period_start":"2020-04-04","period_end":"2020-04-06","issue_date":"2020-04-07","due_date":"2020-04-07","payment_term":"upon 85 | receipt","sent_at":null,"paid_at":null,"closed_at":null,"recurring_invoice_id":null,"created_at":"2020-04-07T07:25:33Z","updated_at":"2020-04-07T07:25:33Z","paid_date":null,"currency":"DKK","client":{"id":9343060,"name":"Traels.it"},"estimate":null,"retainer":null,"creator":{"id":3211810,"name":"Nicolai 86 | Bach Woller"},"line_items":[{"id":109677238,"kind":"Service","description":"Harvest 87 | Billing Automation - A note","quantity":3.0,"unit_price":300.0,"amount":900.0,"taxed":false,"taxed2":false,"project":{"id":24566828,"name":"Harvest 88 | Billing Automation","code":""}},{"id":109677239,"kind":"Service","description":"Harvest 89 | Billing Automation - Another note","quantity":2.0,"unit_price":300.0,"amount":600.0,"taxed":false,"taxed2":false,"project":{"id":24566828,"name":"Harvest 90 | Billing Automation","code":""}},{"id":109677240,"kind":"Service","description":"Harvest 91 | Billing Automation - A third note","quantity":2.0,"unit_price":300.0,"amount":600.0,"taxed":false,"taxed2":false,"project":{"id":24566828,"name":"Harvest 92 | Billing Automation","code":""}},{"id":109677241,"kind":"Service","description":"Harvest 93 | Billing Automation","quantity":5.0,"unit_price":300.0,"amount":1500.0,"taxed":false,"taxed2":false,"project":{"id":24566828,"name":"Harvest 94 | Billing Automation","code":""}},{"id":109677242,"kind":"Service","description":"Harvest 95 | Billing Automation - A note\r\nAnother note\r\nA third note","quantity":3.0,"unit_price":300.0,"amount":900.0,"taxed":false,"taxed2":false,"project":{"id":24566828,"name":"Harvest 96 | Billing Automation","code":""}}]}],"per_page":100,"total_pages":1,"total_entries":3,"next_page":null,"previous_page":null,"page":1,"links":{"first":"https://api.harvestapp.com/v2/invoices?page=1&per_page=100","next":null,"previous":null,"last":"https://api.harvestapp.com/v2/invoices?page=1&per_page=100"}}' 97 | http_version: 98 | recorded_at: Tue, 07 Apr 2020 07:28:26 GMT 99 | recorded_with: VCR 4.0.0 100 | -------------------------------------------------------------------------------- /fixtures/vcr_cassettes/Harvesting_Models_TimeEntry_create/when_trying_to_create_a_time_entry_with_the_required_attributes_automatically_sets_the_id_of_the_time_entry.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: post 5 | uri: https://api.harvestapp.com/v2/time_entries 6 | body: 7 | encoding: UTF-8 8 | string: '{"project_id":"19919748","task_id":"11399145","user_id":"2348607","project":{"id":"19919748"},"task":{"id":"11399145"},"spent_date":"2018-05-14","hours":"1.0","user":{"id":"2348607"},"is_running":"false","notes":"hacked 9 | the things"}' 10 | headers: 11 | User-Agent: 12 | - Harvesting Ruby Gem 13 | Authorization: 14 | - Bearer $HARVEST_ACCESS_TOKEN 15 | Harvest-Account-Id: 16 | - "$HARVEST_ACCOUNT_ID" 17 | Connection: 18 | - close 19 | Content-Type: 20 | - application/json; charset=UTF-8 21 | Host: 22 | - api.harvestapp.com 23 | response: 24 | status: 25 | code: 201 26 | message: Created 27 | headers: 28 | Server: 29 | - nginx 30 | Date: 31 | - Fri, 25 Jan 2019 18:32:33 GMT 32 | Content-Type: 33 | - application/json; charset=utf-8 34 | Transfer-Encoding: 35 | - chunked 36 | Connection: 37 | - close 38 | Status: 39 | - 201 Created 40 | X-Frame-Options: 41 | - SAMEORIGIN 42 | X-Xss-Protection: 43 | - 1; mode=block 44 | X-Content-Type-Options: 45 | - nosniff 46 | X-Download-Options: 47 | - noopen 48 | X-Permitted-Cross-Domain-Policies: 49 | - none 50 | Referrer-Policy: 51 | - strict-origin-when-cross-origin 52 | Cache-Control: 53 | - no-cache, no-store 54 | P3p: 55 | - 'CP="Our privacy policy is available online: https://www.getharvest.com/services/privacy-policy"' 56 | X-App-Server: 57 | - app1 58 | X-Robots-Tag: 59 | - noindex, nofollow 60 | Content-Security-Policy: 61 | - 'report-uri /csp_reports; default-src *; img-src * data:; font-src data: cache.harvestapp.com 62 | https://js.intercomcdn.com; script-src ''self'' ''unsafe-inline'' ''unsafe-eval'' 63 | https://*.google-analytics.com https://*.nr-data.net https://ajax.googleapis.com 64 | cache.harvestapp.com https://js-agent.newrelic.com https://platform.twitter.com 65 | https://www.google.com https://www.googleadservices.com https://www.googletagmanager.com 66 | https://connect.facebook.net https://googleads.g.doubleclick.net https://app.intercom.io 67 | https://widget.intercom.io https://js.intercomcdn.com https://cdn.plaid.com; 68 | style-src ''self'' ''unsafe-inline'' cache.harvestapp.com https://www.google.com' 69 | Location: 70 | - https://api.harvestapp.com/api/v2/time_entries/933385752 71 | X-Request-Id: 72 | - 4cdff68bfe10ff824efe21022c9100fa 73 | X-Runtime: 74 | - '0.282239' 75 | Strict-Transport-Security: 76 | - max-age=31536000; includeSubDomains 77 | X-Server: 78 | - lb4 79 | body: 80 | encoding: UTF-8 81 | string: '{"id":933385752,"spent_date":"2018-05-14","hours":1.0,"notes":"hacked 82 | the things","is_locked":false,"locked_reason":null,"is_closed":false,"is_billed":false,"timer_started_at":null,"started_time":null,"ended_time":null,"is_running":false,"billable":true,"budgeted":false,"billable_rate":null,"cost_rate":null,"created_at":"2019-01-25T18:32:33Z","updated_at":"2019-01-25T18:32:33Z","user":{"id":2348607,"name":"Alexander 83 | Hamilton"},"client":{"id":7722246,"name":"Pepe","currency":"USD"},"project":{"id":19919748,"name":"Castle 84 | Building","code":null},"task":{"id":11399145,"name":"Coding"},"user_assignment":{"id":186878706,"is_project_manager":true,"is_active":true,"budget":null,"created_at":"2019-01-25T18:31:10Z","updated_at":"2019-01-25T18:31:10Z","hourly_rate":null},"task_assignment":{"id":215014748,"billable":true,"is_active":true,"created_at":"2019-01-25T18:31:11Z","updated_at":"2019-01-25T18:31:11Z","hourly_rate":null,"budget":null},"invoice":null,"external_reference":null}' 85 | http_version: 86 | recorded_at: Fri, 25 Jan 2019 18:32:33 GMT 87 | - request: 88 | method: delete 89 | uri: https://api.harvestapp.com/v2/time_entries/933385752 90 | body: 91 | encoding: UTF-8 92 | string: '' 93 | headers: 94 | User-Agent: 95 | - Harvesting Ruby Gem 96 | Authorization: 97 | - Bearer $HARVEST_ACCESS_TOKEN 98 | Harvest-Account-Id: 99 | - "$HARVEST_ACCOUNT_ID" 100 | Connection: 101 | - close 102 | Host: 103 | - api.harvestapp.com 104 | response: 105 | status: 106 | code: 200 107 | message: OK 108 | headers: 109 | Server: 110 | - nginx 111 | Date: 112 | - Fri, 25 Jan 2019 18:32:34 GMT 113 | Content-Type: 114 | - application/json; charset=utf-8 115 | Transfer-Encoding: 116 | - chunked 117 | Connection: 118 | - close 119 | Status: 120 | - 200 OK 121 | X-Frame-Options: 122 | - SAMEORIGIN 123 | X-Xss-Protection: 124 | - 1; mode=block 125 | X-Content-Type-Options: 126 | - nosniff 127 | X-Download-Options: 128 | - noopen 129 | X-Permitted-Cross-Domain-Policies: 130 | - none 131 | Referrer-Policy: 132 | - strict-origin-when-cross-origin 133 | Cache-Control: 134 | - no-cache, no-store 135 | P3p: 136 | - 'CP="Our privacy policy is available online: https://www.getharvest.com/services/privacy-policy"' 137 | X-App-Server: 138 | - app21 139 | X-Robots-Tag: 140 | - noindex, nofollow 141 | Content-Security-Policy: 142 | - 'report-uri /csp_reports; default-src *; img-src * data:; font-src data: cache.harvestapp.com 143 | https://js.intercomcdn.com; script-src ''self'' ''unsafe-inline'' ''unsafe-eval'' 144 | https://*.google-analytics.com https://*.nr-data.net https://ajax.googleapis.com 145 | cache.harvestapp.com https://js-agent.newrelic.com https://platform.twitter.com 146 | https://www.google.com https://www.googleadservices.com https://www.googletagmanager.com 147 | https://connect.facebook.net https://googleads.g.doubleclick.net https://app.intercom.io 148 | https://widget.intercom.io https://js.intercomcdn.com https://cdn.plaid.com; 149 | style-src ''self'' ''unsafe-inline'' cache.harvestapp.com https://www.google.com' 150 | X-Request-Id: 151 | - 69daa6f4622aaed559401a8c91037314 152 | X-Runtime: 153 | - '0.290300' 154 | Strict-Transport-Security: 155 | - max-age=31536000; includeSubDomains 156 | X-Server: 157 | - lb4 158 | body: 159 | encoding: UTF-8 160 | string: '{"id":933385752,"spent_date":"2018-05-14","hours":1.0,"notes":"hacked 161 | the things","is_locked":false,"locked_reason":null,"is_closed":false,"is_billed":false,"timer_started_at":null,"started_time":null,"ended_time":null,"is_running":false,"billable":true,"budgeted":false,"billable_rate":null,"cost_rate":null,"created_at":"2019-01-25T18:32:33Z","updated_at":"2019-01-25T18:32:33Z","user":{"id":2348607,"name":"Alexander 162 | Hamilton"},"client":{"id":7722246,"name":"Pepe","currency":"USD"},"project":{"id":19919748,"name":"Castle 163 | Building","code":null},"task":{"id":11399145,"name":"Coding"},"user_assignment":{"id":186878706,"is_project_manager":true,"is_active":true,"budget":null,"created_at":"2019-01-25T18:31:10Z","updated_at":"2019-01-25T18:31:10Z","hourly_rate":null},"task_assignment":{"id":215014748,"billable":true,"is_active":true,"created_at":"2019-01-25T18:31:11Z","updated_at":"2019-01-25T18:31:11Z","hourly_rate":null,"budget":null},"invoice":null,"external_reference":null}' 164 | http_version: 165 | recorded_at: Fri, 25 Jan 2019 18:32:34 GMT 166 | recorded_with: VCR 4.0.0 167 | -------------------------------------------------------------------------------- /fixtures/vcr_cassettes/Harvesting_Client_delete/raises_a_UnprocessableRequest_exception_if_entity_is_not_removable.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: post 5 | uri: https://api.harvestapp.com/v2/clients 6 | body: 7 | encoding: UTF-8 8 | string: '{"name":"Mr. Robot"}' 9 | headers: 10 | User-Agent: 11 | - Harvesting Ruby Gem 12 | Authorization: 13 | - Bearer $HARVEST_ACCESS_TOKEN 14 | Harvest-Account-Id: 15 | - "$HARVEST_ACCOUNT_ID" 16 | Connection: 17 | - close 18 | Content-Type: 19 | - application/json; charset=UTF-8 20 | Host: 21 | - api.harvestapp.com 22 | response: 23 | status: 24 | code: 201 25 | message: Created 26 | headers: 27 | Server: 28 | - nginx 29 | Date: 30 | - Fri, 15 Jan 2021 02:39:10 GMT 31 | Content-Type: 32 | - application/json; charset=utf-8 33 | Transfer-Encoding: 34 | - chunked 35 | Connection: 36 | - close 37 | X-Frame-Options: 38 | - SAMEORIGIN 39 | X-Xss-Protection: 40 | - 1; mode=block 41 | X-Content-Type-Options: 42 | - nosniff 43 | X-Download-Options: 44 | - noopen 45 | X-Permitted-Cross-Domain-Policies: 46 | - none 47 | Referrer-Policy: 48 | - strict-origin-when-cross-origin 49 | Cache-Control: 50 | - no-cache, no-store 51 | P3p: 52 | - 'CP="Our privacy policy is available online: https://www.getharvest.com/services/privacy-policy"' 53 | X-App-Server: 54 | - app3 55 | X-Robots-Tag: 56 | - noindex, nofollow 57 | Content-Security-Policy: 58 | - 'report-uri /csp_reports; default-src *; img-src * data:; font-src data: cache.harvestapp.com 59 | https://fonts.gstatic.com; script-src ''self'' ''unsafe-inline'' ''unsafe-eval'' 60 | https://*.google-analytics.com https://*.nr-data.net https://ajax.googleapis.com 61 | cache.harvestapp.com https://platform.twitter.com https://www.google.com https://www.googleadservices.com 62 | https://www.googletagmanager.com https://connect.facebook.net https://googleads.g.doubleclick.net 63 | https://cdn.plaid.com https://tagmanager.google.com https://bat.bing.com https://ct.capterra.com; 64 | style-src ''self'' ''unsafe-inline'' cache.harvestapp.com https://www.google.com 65 | https://tagmanager.google.com https://fonts.googleapis.com' 66 | Location: 67 | - https://api.harvestapp.com/api/v2/clients/10331953 68 | Etag: 69 | - W/"98188a5156309a3babc9ff7e0c4f98b0" 70 | X-Request-Id: 71 | - 237a3ab5d3d40ebf6b887bf18418ed3e 72 | X-Runtime: 73 | - '0.034833' 74 | Strict-Transport-Security: 75 | - max-age=31536000; includeSubDomains 76 | X-Server: 77 | - lb4 78 | body: 79 | encoding: UTF-8 80 | string: '{"id":10331953,"name":"Mr. Robot","is_active":true,"address":null,"statement_key":"b0e8f3951e5cd5b28ad3ea7a8f6311ab","created_at":"2021-01-15T02:39:10Z","updated_at":"2021-01-15T02:39:10Z","currency":"USD"}' 81 | http_version: 82 | recorded_at: Fri, 15 Jan 2021 02:39:10 GMT 83 | - request: 84 | method: post 85 | uri: https://api.harvestapp.com/v2/projects 86 | body: 87 | encoding: UTF-8 88 | string: '{"client_id":10331953,"name":"E-Corp","client":{"id":10331953,"name":"Mr. 89 | Robot","is_active":true,"address":null,"statement_key":"b0e8f3951e5cd5b28ad3ea7a8f6311ab","created_at":"2021-01-15T02:39:10Z","updated_at":"2021-01-15T02:39:10Z","currency":"USD"}}' 90 | headers: 91 | User-Agent: 92 | - Harvesting Ruby Gem 93 | Authorization: 94 | - Bearer $HARVEST_ACCESS_TOKEN 95 | Harvest-Account-Id: 96 | - "$HARVEST_ACCOUNT_ID" 97 | Connection: 98 | - close 99 | Content-Type: 100 | - application/json; charset=UTF-8 101 | Host: 102 | - api.harvestapp.com 103 | response: 104 | status: 105 | code: 201 106 | message: Created 107 | headers: 108 | Server: 109 | - nginx 110 | Date: 111 | - Fri, 15 Jan 2021 02:39:27 GMT 112 | Content-Type: 113 | - application/json; charset=utf-8 114 | Transfer-Encoding: 115 | - chunked 116 | Connection: 117 | - close 118 | X-Frame-Options: 119 | - SAMEORIGIN 120 | X-Xss-Protection: 121 | - 1; mode=block 122 | X-Content-Type-Options: 123 | - nosniff 124 | X-Download-Options: 125 | - noopen 126 | X-Permitted-Cross-Domain-Policies: 127 | - none 128 | Referrer-Policy: 129 | - strict-origin-when-cross-origin 130 | Cache-Control: 131 | - no-cache, no-store 132 | P3p: 133 | - 'CP="Our privacy policy is available online: https://www.getharvest.com/services/privacy-policy"' 134 | X-App-Server: 135 | - app2 136 | X-Robots-Tag: 137 | - noindex, nofollow 138 | Content-Security-Policy: 139 | - 'report-uri /csp_reports; default-src *; img-src * data:; font-src data: cache.harvestapp.com 140 | https://fonts.gstatic.com; script-src ''self'' ''unsafe-inline'' ''unsafe-eval'' 141 | https://*.google-analytics.com https://*.nr-data.net https://ajax.googleapis.com 142 | cache.harvestapp.com https://platform.twitter.com https://www.google.com https://www.googleadservices.com 143 | https://www.googletagmanager.com https://connect.facebook.net https://googleads.g.doubleclick.net 144 | https://cdn.plaid.com https://tagmanager.google.com https://bat.bing.com https://ct.capterra.com; 145 | style-src ''self'' ''unsafe-inline'' cache.harvestapp.com https://www.google.com 146 | https://tagmanager.google.com https://fonts.googleapis.com' 147 | Location: 148 | - https://api.harvestapp.com/api/v2/projects/27411968 149 | Etag: 150 | - W/"a4a511a182aa9acc5271d74d6c47f7d5" 151 | X-Request-Id: 152 | - e794b68cc5e45a379f640666ca0cc253 153 | X-Runtime: 154 | - '0.142711' 155 | Strict-Transport-Security: 156 | - max-age=31536000; includeSubDomains 157 | X-Server: 158 | - lb2 159 | body: 160 | encoding: UTF-8 161 | string: '{"id":27411968,"name":"E-Corp","code":null,"is_active":true,"is_billable":true,"is_fixed_fee":false,"bill_by":"none","budget":null,"budget_by":"none","budget_is_monthly":false,"notify_when_over_budget":false,"over_budget_notification_percentage":80.0,"show_budget_to_all":false,"created_at":"2021-01-15T02:39:27Z","updated_at":"2021-01-15T02:39:27Z","starts_on":null,"ends_on":null,"over_budget_notification_date":null,"notes":null,"cost_budget":null,"cost_budget_include_expenses":false,"hourly_rate":null,"fee":null,"client":{"id":10331953,"name":"Mr. 162 | Robot","currency":"USD"}}' 163 | http_version: 164 | recorded_at: Fri, 15 Jan 2021 02:39:27 GMT 165 | - request: 166 | method: delete 167 | uri: https://api.harvestapp.com/v2/clients/10331953 168 | body: 169 | encoding: UTF-8 170 | string: '' 171 | headers: 172 | User-Agent: 173 | - Harvesting Ruby Gem 174 | Authorization: 175 | - Bearer $HARVEST_ACCESS_TOKEN 176 | Harvest-Account-Id: 177 | - "$HARVEST_ACCOUNT_ID" 178 | Connection: 179 | - close 180 | Host: 181 | - api.harvestapp.com 182 | response: 183 | status: 184 | code: 422 185 | message: Unprocessable Entity 186 | headers: 187 | Server: 188 | - nginx 189 | Date: 190 | - Fri, 15 Jan 2021 02:39:27 GMT 191 | Content-Type: 192 | - application/json; charset=utf-8 193 | Transfer-Encoding: 194 | - chunked 195 | Connection: 196 | - close 197 | X-Frame-Options: 198 | - SAMEORIGIN 199 | X-Xss-Protection: 200 | - 1; mode=block 201 | X-Content-Type-Options: 202 | - nosniff 203 | X-Download-Options: 204 | - noopen 205 | X-Permitted-Cross-Domain-Policies: 206 | - none 207 | Referrer-Policy: 208 | - strict-origin-when-cross-origin 209 | Cache-Control: 210 | - no-cache, no-store 211 | P3p: 212 | - 'CP="Our privacy policy is available online: https://www.getharvest.com/services/privacy-policy"' 213 | X-App-Server: 214 | - app3 215 | X-Robots-Tag: 216 | - noindex, nofollow 217 | Content-Security-Policy: 218 | - 'report-uri /csp_reports; default-src *; img-src * data:; font-src data: cache.harvestapp.com 219 | https://fonts.gstatic.com; script-src ''self'' ''unsafe-inline'' ''unsafe-eval'' 220 | https://*.google-analytics.com https://*.nr-data.net https://ajax.googleapis.com 221 | cache.harvestapp.com https://platform.twitter.com https://www.google.com https://www.googleadservices.com 222 | https://www.googletagmanager.com https://connect.facebook.net https://googleads.g.doubleclick.net 223 | https://cdn.plaid.com https://tagmanager.google.com https://bat.bing.com https://ct.capterra.com; 224 | style-src ''self'' ''unsafe-inline'' cache.harvestapp.com https://www.google.com 225 | https://tagmanager.google.com https://fonts.googleapis.com' 226 | Hint: 227 | - This client is not removable. It still has projects and/or invoices. 228 | X-Request-Id: 229 | - 03add25275ef5a884f8de7c5b28a2986 230 | X-Runtime: 231 | - '0.048734' 232 | Strict-Transport-Security: 233 | - max-age=31536000; includeSubDomains 234 | body: 235 | encoding: UTF-8 236 | string: '{"message":"This client is not removable. It still has projects and/or 237 | invoices."}' 238 | http_version: 239 | recorded_at: Fri, 15 Jan 2021 02:39:27 GMT 240 | recorded_with: VCR 4.0.0 241 | -------------------------------------------------------------------------------- /spec/harvesting/harvest_data_setup.rb: -------------------------------------------------------------------------------- 1 | require 'rspec/core' 2 | 3 | module RSpec::Core::MemoizedHelpers::ClassMethods 4 | # Helper method to simplify defining let methods that are wrapped in a VCR 5 | # cassette. Calling this method is equivalent to calling: 6 | # ``` 7 | # let!(variable_name) do 8 | # VCR.use_cassette('harvest_data_setup/variable_name', :record => :once, :allow_playback_repeats => true) do 9 | # contents 10 | # end 11 | # end 12 | # ``` 13 | # The equivalent call would look like this: 14 | # ``` 15 | # cassette_let!(variable_name) do 16 | # contents 17 | # end 18 | # ``` 19 | def cassette_let!(name, &block) 20 | let!(name, &block) 21 | 22 | # partially duplicates the implementation found at: 23 | # https://github.com/rspec/rspec-core/blob/58f38210492cd369784d3fe1a849c0e81342a2f2/lib/rspec/core/memoized_helpers.rb 24 | define_method(name) { 25 | __memoized.fetch_or_store(name) { 26 | VCR.insert_cassette("harvest_data_setup/#{name}", :record => :once, :allow_playback_repeats => true) 27 | result = super(&nil) 28 | VCR.eject_cassette 29 | result 30 | } 31 | } 32 | end 33 | end 34 | 35 | RSpec.shared_context "harvest data setup" do 36 | 37 | let(:account_first_name) { ENV["HARVEST_FIRST_NAME"] } 38 | let(:account_last_name) { ENV["HARVEST_LAST_NAME"] } 39 | 40 | let(:admin_account_id) { ENV["HARVEST_ACCOUNT_ID"] } 41 | let(:non_admin_account_id) { ENV["HARVEST_NON_ADMIN_ACCOUNT_ID"] } 42 | 43 | let(:admin_access_token) { ENV["HARVEST_ACCESS_TOKEN"] } 44 | let(:non_admin_access_token) { ENV["HARVEST_NON_ADMIN_ACCESS_TOKEN"] } 45 | 46 | let(:admin_full_name) { ENV["HARVEST_ADMIN_FULL_NAME"] } 47 | let(:non_admin_full_name) { ENV["HARVEST_NON_ADMIN_FULL_NAME"] } 48 | 49 | let(:access_token) { non_admin_access_token } 50 | let(:account_id) { non_admin_account_id } 51 | 52 | let(:one_day) { 24 * 60 * 60 } 53 | 54 | let!(:harvest_client) do 55 | VCR.use_cassette('harvest_data_setup/harvest_client', :record => :once, :allow_playback_repeats => true) do 56 | harvest_client = Harvesting::Client.new( 57 | access_token: admin_access_token, 58 | account_id: admin_account_id 59 | ) 60 | harvest_client 61 | end 62 | end 63 | 64 | before do 65 | # VCR.use_cassette('harvest_data_setup/clear_data', :record => :once, :allow_playback_repeats => true) do 66 | # harvest_client.time_entries.to_a.each do |time_entry| 67 | # time_entry.delete 68 | # end 69 | # 70 | # harvest_client.tasks.to_a.each do |task| 71 | # task.delete 72 | # end 73 | # 74 | # harvest_client.projects.to_a.each do |project| 75 | # project.delete 76 | # end 77 | # 78 | # harvest_client.invoices.to_a.each do |invoice| 79 | # invoice.delete 80 | # end 81 | # 82 | # harvest_client.clients.to_a.each do |client| 83 | # client.delete 84 | # end 85 | # 86 | # harvest_client.users.to_a.each do |user| 87 | # unless [admin_full_name, non_admin_full_name].include?(user.name) 88 | # user.delete 89 | # end 90 | # end 91 | # end 92 | end 93 | 94 | cassette_let!(:user_john_smith) do 95 | john_smith = Harvesting::Models::User.new( 96 | { 97 | "first_name" => "John", 98 | "last_name" => "Smith", 99 | "email" => "john.smith@example.com" 100 | }, 101 | harvest_client: harvest_client 102 | ) 103 | john_smith.save 104 | john_smith 105 | end 106 | 107 | cassette_let!(:user_jane_doe) do 108 | jane_doe = Harvesting::Models::User.new( 109 | { 110 | "first_name" => "Jane", 111 | "last_name" => "Doe", 112 | "email" => "jane.doe@example.com" 113 | }, 114 | harvest_client: harvest_client 115 | ) 116 | jane_doe.save 117 | jane_doe 118 | end 119 | 120 | let!(:client_pepe) do 121 | VCR.use_cassette('harvest_data_setup/client_pepe', :record => :once, :allow_playback_repeats => true) do 122 | pepe = Harvesting::Models::Client.new( 123 | { 124 | "name" => "Pepe" 125 | }, 126 | harvest_client: harvest_client 127 | ) 128 | pepe.save 129 | pepe 130 | end 131 | end 132 | 133 | let!(:client_toto) do 134 | VCR.use_cassette('harvest_data_setup/client_toto', :record => :once, :allow_playback_repeats => true) do 135 | toto = Harvesting::Models::Client.new( 136 | { 137 | "name" => "Toto" 138 | }, 139 | harvest_client: harvest_client 140 | ) 141 | toto.save 142 | toto 143 | end 144 | end 145 | 146 | let!(:user_me) do 147 | VCR.use_cassette('harvest_data_setup/user_me', :record => :once, :allow_playback_repeats => true) do 148 | harvest_client.me 149 | end 150 | end 151 | 152 | let!(:contact_jon_snow) do 153 | VCR.use_cassette('harvest_data_setup/contact_jon_snow', :record => :once, :allow_playback_repeats => true) do 154 | jon_snow = Harvesting::Models::Contact.new( 155 | { 156 | "first_name" => "Jon", 157 | "last_name" => "Snow", 158 | "client" => { 159 | "id" => client_pepe.id.to_s 160 | } 161 | }, 162 | harvest_client: harvest_client 163 | ) 164 | jon_snow.save 165 | jon_snow 166 | end 167 | end 168 | 169 | let!(:project_road_building) do 170 | VCR.use_cassette('harvest_data_setup/project_road_building', :record => :once, :allow_playback_repeats => true) do 171 | castle_building = Harvesting::Models::Project.new( 172 | { 173 | "client" => { 174 | "id" => client_toto.id.to_s 175 | }, 176 | "name" => "Road Building", 177 | "is_billable" => "true", 178 | "bill_by" => "Tasks", 179 | "budget_by" => "person" 180 | }, 181 | harvest_client: harvest_client 182 | ) 183 | castle_building.save 184 | castle_building 185 | end 186 | end 187 | 188 | let!(:project_castle_building) do 189 | VCR.use_cassette('harvest_data_setup/project_castle_building', :record => :once, :allow_playback_repeats => true) do 190 | castle_building = Harvesting::Models::Project.new( 191 | { 192 | "client" => { 193 | "id" => client_pepe.id.to_s 194 | }, 195 | "name" => "Castle Building", 196 | "is_billable" => "true", 197 | "bill_by" => "Tasks", 198 | "budget_by" => "person" 199 | }, 200 | harvest_client: harvest_client 201 | ) 202 | castle_building.save 203 | castle_building 204 | end 205 | end 206 | 207 | let!(:task_coding) do 208 | VCR.use_cassette('harvest_data_setup/task_coding', :record => :once, :allow_playback_repeats => true) do 209 | coding = Harvesting::Models::Task.new( 210 | { 211 | "name" => "Coding" 212 | }, 213 | harvest_client: harvest_client 214 | ) 215 | coding.save 216 | coding 217 | end 218 | end 219 | 220 | cassette_let!(:task_writing) do 221 | writing = Harvesting::Models::Task.new( 222 | { 223 | "name" => "Writing" 224 | }, 225 | harvest_client: harvest_client 226 | ) 227 | writing.save 228 | writing 229 | end 230 | 231 | let!(:task_assigment_castle_building_coding) do 232 | VCR.use_cassette('harvest_data_setup/task_assigment_castle_building_coding', :record => :once, :allow_playback_repeats => true) do 233 | castle_building_coding = Harvesting::Models::ProjectTaskAssignment.new( 234 | { 235 | "project" => { 236 | "id" => project_castle_building.id.to_s 237 | }, 238 | "task" => { 239 | "id" => task_coding.id.to_s 240 | } 241 | }, 242 | harvest_client: harvest_client 243 | ) 244 | castle_building_coding.save 245 | castle_building_coding 246 | end 247 | end 248 | 249 | cassette_let!(:task_assignment_roading_building_writing) do 250 | road_building_writing = Harvesting::Models::ProjectTaskAssignment.new( 251 | { 252 | "project" => { 253 | "id" => project_road_building.id.to_s 254 | }, 255 | "task" => { 256 | "id" => task_writing.id.to_s 257 | } 258 | }, 259 | harvest_client: harvest_client 260 | ) 261 | road_building_writing.save 262 | road_building_writing 263 | end 264 | 265 | let!(:project_assignment_castle_building) do 266 | VCR.use_cassette('harvest_data_setup/project_assignment_castle_building', :record => :once, :allow_playback_repeats => true) do 267 | project_assignment = Harvesting::Models::ProjectUserAssignment.new( 268 | { 269 | "project" => { 270 | "id" => project_castle_building.id.to_s 271 | }, 272 | "user" => { 273 | "id" => user_john_smith.id.to_s 274 | } 275 | }, 276 | harvest_client: harvest_client 277 | ) 278 | project_assignment.save 279 | project_assignment 280 | end 281 | end 282 | 283 | cassette_let!(:project_assignment_road_building) do 284 | project_assignment = Harvesting::Models::ProjectUserAssignment.new( 285 | { 286 | "project" => { 287 | "id" => project_road_building.id.to_s 288 | }, 289 | "user" => { 290 | "id" => user_jane_doe.id.to_s 291 | } 292 | }, 293 | harvest_client: harvest_client 294 | ) 295 | project_assignment.save 296 | project_assignment 297 | end 298 | 299 | let!(:contact_cersei_lannister) do 300 | VCR.use_cassette('harvest_data_setup/contact_cersei_lannister', :record => :once, :allow_playback_repeats => true) do 301 | cersei_lannister = Harvesting::Models::Contact.new( 302 | { 303 | "first_name" => "Cersei", 304 | "last_name" => "Lannister", 305 | "client" => { 306 | "id" => client_toto.id.to_s 307 | } 308 | }, 309 | harvest_client: harvest_client 310 | ) 311 | cersei_lannister.save 312 | cersei_lannister 313 | end 314 | end 315 | 316 | let!(:time_entries) do 317 | result = [] 318 | VCR.use_cassette('harvest_data_setup/time_entries', :record => :once, :allow_playback_repeats => true) do 319 | 119.times do |iteration| 320 | time = Harvesting::Models::TimeEntry.new( 321 | { 322 | "project" => { 323 | "id" => project_castle_building.id.to_s 324 | }, 325 | "task" => { 326 | "id" => task_coding.id.to_s 327 | }, 328 | "spent_date" => (Time.now - one_day * (iteration + 1)).iso8601.to_s, 329 | "hours" => 6.to_s 330 | }, 331 | harvest_client: harvest_client 332 | ) 333 | time.save 334 | result << time 335 | end 336 | end 337 | result 338 | end 339 | end 340 | --------------------------------------------------------------------------------