freeagent 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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