freeagent 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. data/CHANGELOG.rdoc +17 -0
  2. data/Gemfile.lock +12 -10
  3. data/README.rdoc +13 -1
  4. data/TODO +22 -0
  5. data/freeagent.gemspec +4 -3
  6. data/lib/free_agent.rb +1 -0
  7. data/lib/free_agent/base.rb +132 -0
  8. data/lib/free_agent/contact.rb +68 -0
  9. data/lib/free_agent/invoice.rb +58 -0
  10. data/lib/free_agent/project.rb +20 -0
  11. data/lib/free_agent/task.rb +10 -0
  12. data/lib/free_agent/version.rb +1 -1
  13. data/spec/fixtures/bank_accounts/all.xml +24 -19
  14. data/spec/fixtures/bank_accounts/single.xml +6 -6
  15. data/spec/fixtures/contacts/all.xml +13 -13
  16. data/spec/fixtures/contacts/all_filter_all.xml +69 -0
  17. data/spec/fixtures/contacts/invoices.xml +75 -0
  18. data/spec/fixtures/contacts/single.xml +6 -6
  19. data/spec/fixtures/estimates/all.xml +38 -0
  20. data/spec/fixtures/expenses/all.xml +93 -0
  21. data/spec/fixtures/expenses/single.xml +31 -0
  22. data/spec/fixtures/expenses/single_with_project_id.xml +31 -0
  23. data/spec/fixtures/invoices/all.xml +112 -75
  24. data/spec/fixtures/invoices/single.xml +38 -37
  25. data/spec/fixtures/projects/invoices.xml +39 -0
  26. data/spec/fixtures/projects/single.xml +2 -2
  27. data/spec/fixtures/projects/tasks.xml +30 -0
  28. data/spec/fixtures/projects/timeslips.xml +25 -0
  29. data/spec/fixtures/tasks/single.xml +10 -0
  30. data/spec/fixtures/timeslips/all.xml +25 -0
  31. data/spec/fixtures/timeslips/single.xml +12 -0
  32. data/spec/spec_helper.rb +1 -1
  33. data/spec/support/helper.rb +4 -4
  34. data/spec/support/mimic.rb +40 -25
  35. data/spec/unit/bank_account_spec.rb +1 -1
  36. data/spec/unit/base_spec.rb +113 -0
  37. data/spec/unit/contact_spec.rb +87 -1
  38. data/spec/unit/invoice_item_spec.rb +1 -1
  39. data/spec/unit/invoice_spec.rb +48 -2
  40. data/spec/unit/project_spec.rb +34 -0
  41. data/spec/unit/task_spec.rb +77 -0
  42. metadata +45 -7
@@ -1,6 +1,23 @@
1
1
  = Changelog
2
2
 
3
3
 
4
+ == Release 0.2.0
5
+
6
+ * NEW: Added support for Project#invoices.
7
+
8
+ * NEW: Added support for Task records.
9
+
10
+ * NEW: Added Contact validations.
11
+
12
+ * NEW: Added Dynamic Finders feature.
13
+
14
+ * NEW: Added support for Contact#invoices.
15
+
16
+ * NEW: Added support for Invoice status methods.
17
+
18
+ * CHANGED: Added .gemspec description and summary.
19
+
20
+
4
21
  == Release 0.1.0
5
22
 
6
23
  * Initial version
@@ -6,25 +6,26 @@ PATH
6
6
  GEM
7
7
  remote: http://rubygems.org/
8
8
  specs:
9
- activemodel (3.0.6)
10
- activesupport (= 3.0.6)
9
+ activemodel (3.0.7)
10
+ activesupport (= 3.0.7)
11
11
  builder (~> 2.1.2)
12
12
  i18n (~> 0.5.0)
13
- activeresource (3.0.6)
14
- activemodel (= 3.0.6)
15
- activesupport (= 3.0.6)
16
- activesupport (3.0.6)
13
+ activeresource (3.0.7)
14
+ activemodel (= 3.0.7)
15
+ activesupport (= 3.0.7)
16
+ activesupport (3.0.7)
17
17
  builder (2.1.2)
18
18
  diff-lcs (1.1.2)
19
19
  i18n (0.5.0)
20
20
  json (1.5.1)
21
- mimic (0.4.1)
21
+ mimic (0.4.2)
22
22
  json
23
23
  plist
24
24
  rack
25
25
  sinatra
26
26
  plist (3.1.0)
27
27
  rack (1.2.2)
28
+ rr (1.0.2)
28
29
  rspec (2.5.0)
29
30
  rspec-core (~> 2.5.0)
30
31
  rspec-expectations (~> 2.5.0)
@@ -33,10 +34,10 @@ GEM
33
34
  rspec-expectations (2.5.0)
34
35
  diff-lcs (~> 1.1.2)
35
36
  rspec-mocks (2.5.0)
36
- sinatra (1.2.1)
37
+ sinatra (1.2.3)
37
38
  rack (~> 1.1)
38
- tilt (< 2.0, >= 1.2.2)
39
- tilt (1.2.2)
39
+ tilt (>= 1.2.2, < 2.0)
40
+ tilt (1.3)
40
41
  yard (0.6.7)
41
42
 
42
43
  PLATFORMS
@@ -46,5 +47,6 @@ DEPENDENCIES
46
47
  activeresource (>= 3.0.0)
47
48
  freeagent!
48
49
  mimic
50
+ rr
49
51
  rspec (~> 2.5.0)
50
52
  yard
@@ -1,6 +1,13 @@
1
1
  = FreeAgent Client
2
2
 
3
- (Unofficial) Ruby client for the {FreeAgent Central API}[http://www.freeagentcentral.com/developers/freeagent-api].
3
+ (Unofficial) Ruby client for the {FreeAgent}[http://fre.ag/4103hsny] accounting software.
4
+
5
+
6
+ == Status
7
+
8
+ This library is a work in progress.
9
+
10
+ Visit the {FreeAgent Central API}[http://www.freeagentcentral.com/developers/freeagent-api] page to learn more about FreeAgent API.
4
11
 
5
12
 
6
13
  == Requirements
@@ -19,6 +26,11 @@
19
26
  * {Simone Carletti}[http://www.simonecarletti.com] <weppos@weppos.net>
20
27
 
21
28
 
29
+ == Promo
30
+
31
+ Sign up with this {promo link}[http://fre.ag/4103hsny] and save 10% on the standard FreeAgent monthly price... forever!
32
+
33
+
22
34
  == License
23
35
 
24
36
  FreeAgent (the Client) is Copyright (c) 2011 Simone Carletti.
data/TODO ADDED
@@ -0,0 +1,22 @@
1
+ /verify
2
+
3
+ /company/invoice_timeline
4
+ /company/tax_timeline
5
+
6
+ /bills/types
7
+
8
+ /projects/project_id/timeslip
9
+
10
+ /timeslips
11
+
12
+ /company/users
13
+
14
+ /users/user_id/expenses
15
+
16
+ /expenses/types
17
+
18
+ ?
19
+
20
+ /projects/project_id/notes
21
+ /projects/project_id/expenses
22
+ /projects/project_id/estimates
@@ -5,15 +5,15 @@ require "free_agent/version"
5
5
  Gem::Specification.new do |s|
6
6
  s.name = "freeagent"
7
7
  s.version = FreeAgent::VERSION
8
- s.summary = ""
9
- s.description = ""
8
+ s.summary = "Ruby client for the FreeAgent API."
9
+ s.description = "Ruby client for the FreeAgent API."
10
10
 
11
11
  s.platform = Gem::Platform::RUBY
12
12
  s.required_ruby_version = ">= 1.8.7"
13
13
 
14
14
  s.authors = ["Simone Carletti"]
15
15
  s.email = ["weppos@weppos.net"]
16
- s.homepage = ""
16
+ s.homepage = "https://github.com/weppos/freeagent"
17
17
 
18
18
  s.files = `git ls-files`.split("\n")
19
19
  s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
@@ -24,4 +24,5 @@ Gem::Specification.new do |s|
24
24
  s.add_development_dependency("yard")
25
25
  s.add_development_dependency("activeresource", ">= 3.0.0")
26
26
  s.add_development_dependency("mimic")
27
+ s.add_development_dependency("rr")
27
28
  end
@@ -13,6 +13,7 @@ module FreeAgent
13
13
  autoload :Invoice, 'free_agent/invoice'
14
14
  autoload :InvoiceItem, 'free_agent/invoice_item'
15
15
  autoload :Project, 'free_agent/project'
16
+ autoload :Task, 'free_agent/task'
16
17
 
17
18
 
18
19
  class << self
@@ -1,6 +1,138 @@
1
1
  module FreeAgent
2
2
 
3
+ # The Base class for any FreeAgent resource.
4
+ #
5
+ # == Dynamic Finders
6
+ #
7
+ # Like ActiveRecord, this class support dynamic finders.
8
+ # You can use dynamic finders to search a resource by any of its attributes.
9
+ #
10
+ # class Contact < Base
11
+ # end
12
+ #
13
+ # Contact.find_by_email 'email@example.org'
14
+ # Contact.find_by_username_and_email 'weppos', 'email@example.org'
15
+ #
16
+ # However, there's one important caveat we should mention.
17
+ # This feature is not very efficient and the more records you have,
18
+ # the longer the query it takes.
19
+ #
20
+ # This is because record filtering is performed client-side.
21
+ # The library first loads and instantiate all records, then it filters them
22
+ # applying given criteria.
23
+ #
3
24
  class Base < ActiveResource::Base
25
+
26
+ # = Active Record Dynamic Finder Match
27
+ #
28
+ # Refer to ActiveRecord::Base documentation for Dynamic attribute-based finders for detailed info.
29
+ #
30
+ class DynamicFinderMatch
31
+ def self.match(method)
32
+ finder = :first
33
+ bang = false
34
+ instantiator = nil
35
+
36
+ case method.to_s
37
+ when /^find_(all_|last_)?by_([_a-zA-Z]\w*)$/
38
+ finder = :last if $1 == 'last_'
39
+ finder = :all if $1 == 'all_'
40
+ names = $2
41
+ when /^find_by_([_a-zA-Z]\w*)\!$/
42
+ bang = true
43
+ names = $1
44
+ when /^find_or_(initialize|create)_by_([_a-zA-Z]\w*)$/
45
+ instantiator = $1 == 'initialize' ? :new : :create
46
+ names = $2
47
+ else
48
+ return nil
49
+ end
50
+
51
+ new(finder, instantiator, bang, names.split('_and_'))
52
+ end
53
+
54
+ def initialize(finder, instantiator, bang, attribute_names)
55
+ @finder = finder
56
+ @instantiator = instantiator
57
+ @bang = bang
58
+ @attribute_names = attribute_names
59
+ end
60
+
61
+ attr_reader :finder, :attribute_names, :instantiator
62
+
63
+ def finder?
64
+ @finder && !@instantiator
65
+ end
66
+
67
+ def instantiator?
68
+ @finder == :first && @instantiator
69
+ end
70
+
71
+ def creator?
72
+ @finder == :first && @instantiator == :create
73
+ end
74
+
75
+ def bang?
76
+ @bang
77
+ end
78
+ end
79
+
80
+
81
+ class << self
82
+
83
+ protected
84
+
85
+ def method_missing(method_id, *arguments, &block)
86
+ if match = DynamicFinderMatch.match(method_id)
87
+ attribute_names = match.attribute_names
88
+ if match.finder?
89
+ find_by_attributes(match, attribute_names, *arguments)
90
+ elsif match.instantiator?
91
+ find_or_instantiator_by_attributes(match, attribute_names, *arguments, &block)
92
+ end
93
+ else
94
+ super
95
+ end
96
+ end
97
+
98
+ def find_by_attributes(match, attributes, *args)
99
+ conditions = Hash[attributes.map {|a| [a, args[attributes.index(a)]]}]
100
+ result = all.select do |record|
101
+ conditions.all? { |k,v| record.send(k) == v }
102
+ end
103
+ result = result.send(match.finder) if match.finder != :all
104
+
105
+ if match.bang? && result.blank?
106
+ raise ActiveResource::ResourceNotFound, 404, "Couldn't find #{self} with #{conditions.to_a.collect {|p| p.join(' = ')}.join(', ')}"
107
+ else
108
+ result
109
+ end
110
+ end
111
+
112
+ def find_or_instantiator_by_attributes(match, attributes, *args)
113
+ record = find_by_attributes(match, attributes, *args)
114
+
115
+ unless record
116
+ conditions = Hash[attributes.map {|a| [a, args[attributes.index(a)]]}]
117
+ record = new(conditions)
118
+ yield(record) if block_given?
119
+ record.save if match.instantiator == :create
120
+ end
121
+
122
+ record
123
+ end
124
+
125
+ end
126
+
127
+
128
+ def respond_to?(method_id, include_private = false)
129
+ if DynamicFinderMatch.match(method_id)
130
+ return true
131
+ end
132
+
133
+ super
134
+ end
135
+
4
136
  end
5
137
 
6
138
  end
@@ -1,7 +1,75 @@
1
1
  module FreeAgent
2
2
 
3
3
  # Represents a Contact in FreeAgent.
4
+ #
5
+ # @attr [String] first_name
6
+ # @attr [String] last_name
7
+ # @attr [String] organisation_name
8
+ #
4
9
  class Contact < Base
10
+
11
+ # == Methods
12
+ #
13
+ # * .all
14
+ # * .first
15
+ # * .last
16
+ # * .find(id)
17
+ # * .new
18
+ # * .create
19
+ #
20
+ # * #save
21
+ # * #update
22
+ # * #destroy
23
+ #
24
+ # == Relationships
25
+ #
26
+ # - has_many :bills
27
+ # - has_many :estimates
28
+ # - has_many :projects
29
+ # * has_many :invoices
30
+
31
+
32
+ validates_presence_of :first_name, :unless => :organisation_name?
33
+ validates_presence_of :last_name, :unless => :organisation_name?
34
+ validates_presence_of :organisation_name, :unless => :name?
35
+
36
+ schema do
37
+ attribute :first_name, :string
38
+ attribute :last_name, :string
39
+ attribute :organisation_name, :string
40
+ end
41
+
42
+
43
+ # Creates the name of the contact,
44
+ # composed by the interpolation of {#first_name} and {#last_name}.
45
+ #
46
+ # @return [String,nil] The Contact name.
47
+ def name
48
+ attrs = [first_name, last_name].reject(&:blank?)
49
+ attrs.empty? ? nil : attrs.join(" ")
50
+ end
51
+
52
+ alias :name? :name
53
+
54
+
55
+ # Gets all the invoices associated to this contact.
56
+ #
57
+ # @overload invoices(options = {})
58
+ # Gets all the invoices for this contact.
59
+ # @param [Hash] options Hash of options to customize the finder behavior.
60
+ # @return [Array<FreeAgent::Invoice>]
61
+ #
62
+ # @example Simple query
63
+ # contact.invoices
64
+ # # => [...]
65
+ # @example Query with custom find params
66
+ # contact.invoices(:params => { :foo => 'bar' }
67
+ # # => [...]
68
+ #
69
+ def invoices(*args)
70
+ options = args.extract_options!
71
+ Invoice.all(options.merge!(:from => "/contacts/#{id}/invoices.xml"))
72
+ end
5
73
  end
6
74
 
7
75
  end
@@ -2,6 +2,64 @@ module FreeAgent
2
2
 
3
3
  # Represents an Invoice in FreeAgent.
4
4
  class Invoice < Base
5
+
6
+ # == Methods
7
+ #
8
+ # * .all
9
+ # * .first
10
+ # * .last
11
+ # * .find(id)
12
+ # * .new
13
+ # * .create
14
+ #
15
+ # * #save
16
+ # * #update
17
+ # * #destroy
18
+ # * #mark_as_draft
19
+ # * #mark_as_sent
20
+ # * #mark_as_cancelled
21
+
22
+ # Marks the current invoice as draft.
23
+ #
24
+ # This method actually performs a new HTTP request
25
+ # to FreeAgent in order to execute the update.
26
+ #
27
+ # @return [void]
28
+ def mark_as_draft
29
+ # put(:mark_as_draft).tap do |response|
30
+ # load_attributes_from_response(response)
31
+ # end
32
+ put(:mark_as_draft) && reload
33
+ end
34
+
35
+ # Marks the current invoice as sent.
36
+ #
37
+ # This method actually performs a new HTTP request
38
+ # to FreeAgent in order to execute the update.
39
+ # It also triggers any automatic deliver if the
40
+ # {#send_new_invoice_emails} invoice attribute is set to true.
41
+ #
42
+ # @return [void]
43
+ def mark_as_sent
44
+ # put(:mark_as_sent).tap do |response|
45
+ # load_attributes_from_response(response)
46
+ # end
47
+ put(:mark_as_sent) && reload
48
+ end
49
+
50
+ # Marks the current invoice as cancelled.
51
+ #
52
+ # This method actually performs a new HTTP request
53
+ # to FreeAgent in order to execute the update.
54
+ #
55
+ # @return [void]
56
+ def mark_as_cancelled
57
+ # put(:mark_as_cancelled).tap do |response|
58
+ # load_attributes_from_response(response)
59
+ # end
60
+ put(:mark_as_cancelled) && reload
61
+ end
62
+
5
63
  end
6
64
 
7
65
  end
@@ -2,6 +2,26 @@ module FreeAgent
2
2
 
3
3
  # Represents a Project in FreeAgent.
4
4
  class Project < Base
5
+
6
+ # Gets all the invoices associated to this project.
7
+ #
8
+ # @overload invoices(options = {})
9
+ # Gets all the invoices for this project.
10
+ # @param [Hash] options Hash of options to customize the finder behavior.
11
+ # @return [Array<FreeAgent::Invoice>]
12
+ #
13
+ # @example Simple query
14
+ # project.invoices
15
+ # # => [...]
16
+ # @example Query with custom find params
17
+ # project.invoices(:params => { :foo => 'bar' }
18
+ # # => [...]
19
+ #
20
+ def invoices(*args)
21
+ options = args.extract_options!
22
+ Invoice.all(options.merge!(:from => "/projects/#{id}/invoices.xml"))
23
+ end
24
+
5
25
  end
6
26
 
7
27
  end