diff --git a/.gitignore b/.gitignore index b0302f2..0da7fad 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ .rvmrc example.rb resource_list + +Gemfile.lock diff --git a/Gemfile b/Gemfile index df2e81c..66e32a6 100644 --- a/Gemfile +++ b/Gemfile @@ -2,5 +2,3 @@ source "http://rubygems.org" # Specify gem dependencies in freeagent-api-ruby.gemspec gemspec - - diff --git a/Gemfile.lock b/Gemfile.lock deleted file mode 100644 index 44127b6..0000000 --- a/Gemfile.lock +++ /dev/null @@ -1,45 +0,0 @@ -PATH - remote: . - specs: - freeagent-api-ruby (0.0.1) - multi_json - oauth2 - -GEM - remote: http://rubygems.org/ - specs: - coderay (1.0.6) - diff-lcs (1.1.3) - faraday (0.8.0) - multipart-post (~> 1.1) - httpauth (0.1) - method_source (0.7.1) - multi_json (1.3.6) - multipart-post (1.1.5) - oauth2 (0.7.1) - faraday (~> 0.8) - httpauth (~> 0.1) - multi_json (~> 1.0) - rack (~> 1.4) - pry (0.9.9.6) - coderay (~> 1.0.5) - method_source (~> 0.7.1) - slop (>= 2.4.4, < 3) - rack (1.4.1) - rspec (2.10.0) - rspec-core (~> 2.10.0) - rspec-expectations (~> 2.10.0) - rspec-mocks (~> 2.10.0) - rspec-core (2.10.0) - rspec-expectations (2.10.0) - diff-lcs (~> 1.1.3) - rspec-mocks (2.10.1) - slop (2.4.4) - -PLATFORMS - ruby - -DEPENDENCIES - freeagent-api-ruby! - pry - rspec diff --git a/freeagent-api-ruby.gemspec b/freeagent-api-ruby.gemspec index 0e54e27..fc1c6ec 100644 --- a/freeagent-api-ruby.gemspec +++ b/freeagent-api-ruby.gemspec @@ -22,4 +22,4 @@ Gem::Specification.new do |s| s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } s.require_paths = ["lib"] -end \ No newline at end of file +end diff --git a/lib/freeagent.rb b/lib/freeagent.rb index 55963e7..328c161 100644 --- a/lib/freeagent.rb +++ b/lib/freeagent.rb @@ -23,10 +23,14 @@ class << self attr_accessor :environment attr_accessor :debug attr_reader :client - + def access_details(client_id, client_secret, access_token=nil) @client = Client.new(client_id, client_secret) @client.access_token = access_token if access_token end + + def authorize(options) + @client.authorize(options) + end end end diff --git a/lib/freeagent/client.rb b/lib/freeagent/client.rb index 05e2291..5191222 100644 --- a/lib/freeagent/client.rb +++ b/lib/freeagent/client.rb @@ -41,7 +41,15 @@ def self.token_url def self.connection_opts { :headers => { :user_agent => "freeagent-api-rb", :accept => "application/json", :content_type => "application/json" } } end - + + def authorize(options) + if options[:redirect_uri] + @client.auth_code.authorize_url(options) + else + raise FreeAgent::ClientError.new('Redirect uri not specified') + end + end + def fetch_access_token(auth_code, options) if options[:redirect_uri] @access_token = @client.auth_code.get_token(auth_code, options) @@ -58,8 +66,50 @@ def access_token=(token) @access_token = OAuth2::AccessToken.new(@client, token) end + def refresh_token + @access_token.try(:refresh_token) + end + + def refresh_token=(refresh_token) + @access_token = OAuth2::AccessToken.new( + @client, + nil, + refresh_token: refresh_token + ) + @access_token = @access_token.refresh! + end + + def get_default(params) + { + auto_paginate: true, + per_page: 100 + }.merge params + end + def get(path, params={}) - request(:get, "#{Client.site}#{path}", :params => params).parsed + params = get_default(params) + response = request(:get, "#{Client.site}#{path}", :params => params) + + if params[:auto_paginate] + auto_paginate(response, params) + else + response.parsed + end + end + + def auto_paginate(response, params) + rels = process_rels(response) + items = response.parsed + + while rels[:next] + response = request(:get, rels[:next], :params => params) + rels = process_rels(response) + items.merge response.parsed do |_, current, new| + current.concat new + end + end + + items end def post(path, data={}) @@ -74,7 +124,21 @@ def delete(path, data={}) request(:delete, "#{Client.site}#{path}", :data => data).parsed end - private + private + + # Finds link relations from 'Link' response header + # + # Returns an array of Relations + # https://github.com/lostisland/sawyer/blob/master/lib/sawyer/response.rb + def process_rels(response) + links = (response.headers["Link"] || "" ).split(', ').map do |link| + href, name = link.match(/<(.*?)>; rel=['"](\w+)["']/).captures + [name.to_sym, href] + end + + Hash[*links.flatten] + end + def request(method, path, options = {}) if @access_token diff --git a/lib/freeagent/contact.rb b/lib/freeagent/contact.rb index bba6637..28d4052 100644 --- a/lib/freeagent/contact.rb +++ b/lib/freeagent/contact.rb @@ -6,7 +6,7 @@ class Contact < Resource attr_accessor :first_name, :last_name, :contact_name_on_invoice, :country, :locale, :sales_tax_registration_number, :uses_contact_invoice_sequence - attr_accessor :organisation_name, :email, :phone_number + attr_accessor :organisation_name, :email, :billing_email, :phone_number attr_accessor :address1, :town, :region, :postcode, :address2, :address3, :country diff --git a/lib/freeagent/invoice.rb b/lib/freeagent/invoice.rb index 5e35924..d217dba 100644 --- a/lib/freeagent/invoice.rb +++ b/lib/freeagent/invoice.rb @@ -4,13 +4,13 @@ class Invoice < Resource resource_methods :default - attr_accessor :contact, :reference, :currency, :status, :omit_header, :payment_terms_in_days, :ec_status, :invoice_items + attr_accessor :contact, :reference, :currency, :status, :omit_header, :payment_terms_in_days, :ec_status, :place_of_supply, :invoice_items attr_accessor :project, :discount_percent, :written_off_date decimal_accessor :exchange_rate, :net_value, :sales_tax_value - date_accessor :dated_on, :due_on + date_accessor :dated_on, :due_on, :paid_on # TODO FIXME Need to rename this better def self.all_with_nested_items @@ -22,27 +22,27 @@ def self.recent_open_or_overdue end def self.open_or_overdue - Invoice.filter(:view => 'recent_open_or_overdue') + Invoice.filter(:view => 'open_or_overdue') end def self.draft - Invoice.filter(:view => 'recent_open_or_overdue') + Invoice.filter(:view => 'draft') end def self.scheduled_to_email - Invoice.filter(:view => 'recent_open_or_overdue') + Invoice.filter(:view => 'scheduled_to_email') end def self.thank_you_emails - Invoice.filter(:view => 'recent_open_or_overdue') + Invoice.filter(:view => 'thank_you_emails') end def self.reminder_emails - Invoice.filter(:view => 'recent_open_or_overdue') + Invoice.filter(:view => 'reminder_emails') end def self.last_month(n) - Invoice.filter(:view => 'recent_open_or_overdue') + Invoice.filter(:view => "last_#{n}_months") end def self.find_all_by_contact(contact) @@ -58,6 +58,18 @@ def self.find_all_by_project(project) # FreeAgent.client.post("invoices/#{id}/send_email", email) #end + def send_email(from, to, subject, msg) + FreeAgent.client.post("invoices/#{id}/send_email",{ + :invoice => { + :email => { + :to => to, + :from => from, + :subject => subject, + :body => msg + } + }}) + end + def mark_as_sent FreeAgent.client.put("invoices/#{id}/transitions/mark_as_sent", nil) end @@ -69,10 +81,14 @@ def mark_as_draft def mark_as_cancelled FreeAgent.client.put("invoices/#{id}/transitions/mark_as_cancelled", nil) end + + def delete_invoice + FreeAgent.client.delete("invoices/#{id}") + end # TODO Write invoice timeline wrapper #def timeline # #end end -end \ No newline at end of file +end diff --git a/lib/freeagent/resource.rb b/lib/freeagent/resource.rb index 1e33c8b..6140174 100644 --- a/lib/freeagent/resource.rb +++ b/lib/freeagent/resource.rb @@ -28,17 +28,17 @@ def inspect vars = to_hash.collect { |k,v| "#{k}=#{v.inspect}" } "#<#{self.class}: #{vars.join(', ')}>" end - + def to_hash hash = {} instance_variables.each {|var| hash[var.to_s.delete("@")] = instance_variable_get(var) } hash end - + def to_json MultiJson.encode(to_hash) end - + class << self attr_accessor :endpoint end @@ -64,7 +64,7 @@ def self.resource_methods(*args) define_delete if args.include? :delete end end - + def self.decimal_accessor(*args) decimal_reader(*args) decimal_writer(*args) @@ -123,7 +123,7 @@ def self.define_find response = FreeAgent.client.get("#{endpoint[:plural]}/#{id}") self.new(response[endpoint[:single]]) rescue FreeAgent::ApiError => error - raise error if FreeAgent.debug + raise error if FreeAgent.debug nil end end @@ -136,19 +136,18 @@ def self.define_create_and_save self.new(response[endpoint[:single]]) end - define_method(:save) do - begin - data = { self.class.endpoint[:single].to_sym => self.to_hash } - if persisted? - FreeAgent.client.put("#{self.class.endpoint[:plural]}/#{id}", data) - else - FreeAgent.client.post(self.class.endpoint[:plural], data) - end - true - rescue FreeAgent::ApiError => error - false - end + define_method(:save) do + data = { self.class.endpoint[:single].to_sym => self.to_hash } + response = nil + if persisted? + response = FreeAgent.client.put("#{self.class.endpoint[:plural]}/#{id}", data) + response = FreeAgent.client.get("#{self.class.endpoint[:plural]}/#{id}") + else + response = FreeAgent.client.post(self.class.endpoint[:plural], data) + end + self.class.new(response[self.class.endpoint[:single]]) end + end def self.define_update diff --git a/lib/freeagent/timeslip.rb b/lib/freeagent/timeslip.rb index 8c89494..94d8ada 100644 --- a/lib/freeagent/timeslip.rb +++ b/lib/freeagent/timeslip.rb @@ -6,7 +6,19 @@ class Timeslip < Resource attr_accessor :user, :project, :task, :comment + def follow_user + FreeAgent::User.find(extract_id(@user)) + end + + def follow_project + FreeAgent::Project.find(extract_id(@project)) + end + + def follow_task + FreeAgent::Task.find(extract_id(@task)) + end + decimal_accessor :hours date_accessor :dated_on, :created_at, :updated_at end -end \ No newline at end of file +end diff --git a/spec/client_spec.rb b/spec/client_spec.rb index a5f5621..a28c9d9 100644 --- a/spec/client_spec.rb +++ b/spec/client_spec.rb @@ -9,9 +9,9 @@ end it 'should raise client error when no client id or secret specified' do - expect { FreeAgent::Client.new(nil, '') }.should raise_error(FreeAgent::ClientError) - expect { FreeAgent::Client.new('', nil) }.should raise_error(FreeAgent::ClientError) - expect { FreeAgent::Client.new(nil, nil) }.should raise_error(FreeAgent::ClientError) + expect { FreeAgent::Client.new(nil, '') }.to raise_error(FreeAgent::ClientError) + expect { FreeAgent::Client.new('', nil) }.to raise_error(FreeAgent::ClientError) + expect { FreeAgent::Client.new(nil, nil) }.to raise_error(FreeAgent::ClientError) end end @@ -114,7 +114,7 @@ it 'should raise client error when redirect not specified' do fetch_access_token = expect {@client.fetch_access_token('auth_code', {})} - fetch_access_token.should raise_error(FreeAgent::ClientError) + fetch_access_token.to raise_error(FreeAgent::ClientError) end end