xeroizer 0.3.5 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. data/.bundle/config +2 -2
  2. data/Gemfile +5 -0
  3. data/Rakefile +17 -1
  4. data/VERSION +1 -1
  5. data/lib/xeroizer.rb +5 -1
  6. data/lib/xeroizer/configuration.rb +19 -0
  7. data/lib/xeroizer/generic_application.rb +2 -1
  8. data/lib/xeroizer/logging.rb +8 -0
  9. data/lib/xeroizer/models/account.rb +2 -1
  10. data/lib/xeroizer/models/bank_account.rb +12 -0
  11. data/lib/xeroizer/models/bank_transaction.rb +74 -0
  12. data/lib/xeroizer/models/invoice.rb +17 -12
  13. data/lib/xeroizer/models/item.rb +3 -3
  14. data/lib/xeroizer/models/item_purchase_details.rb +19 -0
  15. data/lib/xeroizer/models/{item_purchase_sale_details.rb → item_sales_details.rb} +5 -3
  16. data/lib/xeroizer/models/line_amount_type.rb +11 -0
  17. data/lib/xeroizer/models/line_item.rb +2 -12
  18. data/lib/xeroizer/models/line_item_sum.rb +21 -0
  19. data/lib/xeroizer/models/payment.rb +2 -6
  20. data/lib/xeroizer/oauth.rb +1 -1
  21. data/lib/xeroizer/record/base.rb +21 -2
  22. data/lib/xeroizer/record/validation_helper.rb +14 -2
  23. data/lib/xeroizer/record/validators/block_validator.rb +22 -0
  24. data/lib/xeroizer/record/validators/validator.rb +14 -5
  25. data/lib/xeroizer/record/xml_helper.rb +24 -7
  26. data/test/acceptance/about_creating_bank_transactions_test.rb +162 -0
  27. data/test/acceptance/about_fetching_bank_transactions_test.rb +56 -0
  28. data/test/acceptance/acceptance_test.rb +53 -0
  29. data/test/acceptance/bank_transaction_reference_data.rb +31 -0
  30. data/test/test_helper.rb +11 -1
  31. data/test/unit/models/bank_transaction_model_parsing_test.rb +131 -0
  32. data/test/unit/models/bank_transaction_test.rb +47 -0
  33. data/test/unit/models/bank_transaction_validation_test.rb +87 -0
  34. data/test/unit/models/contact_test.rb +2 -2
  35. data/test/unit/models/credit_note_test.rb +2 -2
  36. data/test/unit/models/invoice_test.rb +43 -17
  37. data/test/unit/models/line_item_sum_test.rb +24 -0
  38. data/test/unit/models/line_item_test.rb +54 -0
  39. data/test/unit/oauth_config_test.rb +20 -0
  40. data/test/unit/oauth_test.rb +1 -1
  41. data/test/unit/private_application_test.rb +2 -2
  42. data/test/unit/record/base_model_test.rb +2 -2
  43. data/test/unit/record/base_test.rb +38 -1
  44. data/test/unit/record/block_validator_test.rb +125 -0
  45. data/test/unit/record/model_definition_test.rb +2 -2
  46. data/test/unit/record/parse_where_hash_test.rb +2 -2
  47. data/test/unit/record/record_association_test.rb +1 -1
  48. data/test/unit/record/validators_test.rb +51 -3
  49. data/test/unit/record_definition_test.rb +2 -2
  50. data/test/unit/report_definition_test.rb +2 -2
  51. data/test/unit/report_test.rb +1 -1
  52. data/xeroizer.gemspec +60 -6
  53. metadata +124 -66
  54. data/lib/.DS_Store +0 -0
@@ -14,6 +14,7 @@ module Xeroizer
14
14
  # Adds a validator config for each attribute specified in args.
15
15
  def validates_with_validator(validator, args)
16
16
  options = args.extract_options!
17
+
17
18
  self.validators ||= []
18
19
  args.flatten.each do | attribute |
19
20
  self.validators << validator.new(attribute, options)
@@ -31,7 +32,18 @@ module Xeroizer
31
32
  def validates_presence_of(*args)
32
33
  validates_with_validator(Validator::PresenceOfValidator, args)
33
34
  end
34
-
35
+
36
+ def validates(*args, &block)
37
+ fail "Block required" unless block_given?
38
+
39
+ if args.last.is_a? Hash
40
+ args.last[:block] = block
41
+ else
42
+ args << { :block => block }
43
+ end
44
+
45
+ validates_with_validator(Validator::BlockValidator, args)
46
+ end
35
47
  end
36
48
 
37
49
  module InstanceMethods
@@ -56,4 +68,4 @@ module Xeroizer
56
68
 
57
69
  end
58
70
  end
59
- end
71
+ end
@@ -0,0 +1,22 @@
1
+ module Xeroizer
2
+ module Record
3
+ class Validator
4
+ class BlockValidator < Validator
5
+ def valid?(record)
6
+ fail "No block provided" unless options[:block]
7
+
8
+ result = record.instance_eval &options[:block]
9
+
10
+ record.errors << [attribute, message] unless result == true
11
+ end
12
+
13
+ private
14
+
15
+ def message
16
+ supplied_message = options[:message] || ""
17
+ supplied_message.empty? ? "block condition failed" : supplied_message
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -12,12 +12,21 @@ module Xeroizer
12
12
  end
13
13
 
14
14
  def validate(record)
15
- run_validator = true
16
- run_validator = false if options[:if] && !options[:if].call(record)
17
- run_validator = false if options[:unless] && options[:unless].call(record)
18
- valid?(record) if run_validator
15
+ valid?(record) if run_validator?(record, options)
19
16
  end
20
-
17
+
18
+ def run_validator?(record, options)
19
+ return false if options[:if] && !condition?(record, options[:if])
20
+ return false if options[:unless] && condition?(record, options[:unless])
21
+ true
22
+ end
23
+
24
+ def condition?(record, condition)
25
+ return condition.call(record) if condition.respond_to? :call
26
+ return record.send(condition) if condition.is_a? Symbol
27
+ raise "Validation condition must be a Symbol or an Object that responds to call"
28
+ end
29
+
21
30
  end
22
31
 
23
32
  end
@@ -56,17 +56,34 @@ module Xeroizer
56
56
 
57
57
  # Turn a record into its XML representation.
58
58
  def to_xml(b = Builder::XmlMarkup.new(:indent => 2))
59
- b.tag!(parent.class.xml_node_name || parent.model_name) {
60
- attributes.each do | key, value |
61
- field = self.class.fields[key]
62
- value = self.send(key) if field[:calculated]
63
- xml_value_from_field(b, field, value) unless value.nil?
64
- end
65
- }
59
+ optional_root_tag(parent.class.xml_root_name, b) do |b|
60
+ b.tag!(parent.class.xml_node_name || parent.model_name) {
61
+ attributes.each do | key, value |
62
+ field = self.class.fields[key]
63
+ value = self.send(key) if field[:calculated]
64
+ xml_value_from_field(b, field, value) unless value.nil?
65
+ end
66
+ }
67
+ end
66
68
  end
67
69
 
68
70
  protected
69
71
 
72
+ # Add top-level root name if required.
73
+ # E.g. Payments need specifying in the form:
74
+ # <Payments>
75
+ # <Payment>
76
+ # ...
77
+ # </Payment>
78
+ # </Payments>
79
+ def optional_root_tag(root_name, b, &block)
80
+ if root_name
81
+ b.tag!(root_name) { |b| yield(b) }
82
+ else
83
+ yield(b)
84
+ end
85
+ end
86
+
70
87
  # Format a attribute for use in the XML passed to Xero.
71
88
  def xml_value_from_field(b, field, value)
72
89
  case field[:type]
@@ -0,0 +1,162 @@
1
+ require "test_helper"
2
+ require "acceptance_test"
3
+
4
+ class AboutCreatingBankTransactions < Test::Unit::TestCase
5
+ include AcceptanceTest
6
+
7
+ let :client do
8
+ Xeroizer::PrivateApplication.new(@consumer_key, @consumer_secret, @key_file)
9
+ end
10
+
11
+ def setup
12
+ super
13
+ all_accounts = client.Account.all
14
+ @account = all_accounts.select{|account| account.status == "ACTIVE" && account.type == "REVENUE"}.first
15
+ @bank_account = all_accounts.select{|account| account.status == "ACTIVE" && account.type == "BANK"}.first
16
+ end
17
+
18
+ can "create a new SPEND bank transaction" do
19
+ new_transaction = client.BankTransaction.build(
20
+ :type => "SPEND",
21
+ :contact => { :name => "Jazz Kang" },
22
+ :line_items => any_line_items(@account),
23
+ :bank_account => { :code => @bank_account.code }
24
+ )
25
+
26
+ assert new_transaction.save, "Save failed with the following errors: #{new_transaction.errors.inspect}"
27
+ assert_exists new_transaction
28
+ end
29
+
30
+ can "update a SPEND bank transaction, for example by setting its status" do
31
+ new_transaction = client.BankTransaction.build(
32
+ :type => "SPEND",
33
+ :contact => { :name => "Jazz Kang" },
34
+ :line_items => any_line_items(@account),
35
+ :bank_account => { :code => @bank_account.code }
36
+ )
37
+
38
+ assert new_transaction.save, "Save failed with the following errors: #{new_transaction.errors.inspect}"
39
+
40
+ assert_exists new_transaction
41
+
42
+ the_new_type = "RECEIVE"
43
+
44
+ expected_id = new_transaction.id
45
+
46
+ new_transaction.type = the_new_type
47
+
48
+ assert new_transaction.save, "Update failed with the following errors: #{new_transaction.errors.inspect}"
49
+
50
+ assert_equal expected_id, new_transaction.id, "Expected the id to be the same because it has been updated"
51
+
52
+ refreshed_bank_transaction = client.BankTransaction.find expected_id
53
+
54
+ assert_equal the_new_type, refreshed_bank_transaction.type,
55
+ "Expected the bank transaction to've had its type updated"
56
+ end
57
+
58
+ can "update a bank transaction by adding line items provided you calculate the tax_amount correctly" do
59
+ new_transaction = client.BankTransaction.build(
60
+ :type => "SPEND",
61
+ :contact => { :name => "Jazz Kang" },
62
+ :line_items => any_line_items(@account),
63
+ :bank_account => { :code => @bank_account.code },
64
+ :line_amount_types => "Exclusive"
65
+ )
66
+
67
+ assert new_transaction.save, "Save failed with the following errors: #{new_transaction.errors.inspect}"
68
+ assert_exists new_transaction
69
+
70
+ expected_id = new_transaction.id
71
+
72
+ tax_rate = get_tax_rate(@account.tax_type).effective_rate
73
+
74
+ unit_price = BigDecimal("1337.00")
75
+
76
+ the_new_line_items = [
77
+ {
78
+ :description => "Burrito skin",
79
+ :quantity => 1,
80
+ :unit_amount => unit_price,
81
+ :account_code => @account.code,
82
+ :tax_type => @account.tax_type,
83
+ :tax_amount => get_exclusive_tax(unit_price, tax_rate)
84
+ }
85
+ ]
86
+
87
+ new_transaction.line_items = the_new_line_items
88
+
89
+ assert new_transaction.save, "Update failed with the following errors: #{new_transaction.errors.inspect}"
90
+
91
+ refreshed_bank_transaction = client.BankTransaction.find expected_id
92
+
93
+ assert_equal expected_id, new_transaction.id,
94
+ "Expected the id to be the same because it has been updated"
95
+
96
+ assert_equal 1, refreshed_bank_transaction.line_items.size,
97
+ "Expected the bank transaction to've had its line items updated to just one"
98
+
99
+ the_first_line_item = refreshed_bank_transaction.line_items.first
100
+
101
+ assert_equal "Burrito skin", the_first_line_item.description,
102
+ "Expected the bank transaction to've had its line items updated, " +
103
+ "but the first one's description does not match: #{the_first_line_item.inspect}"
104
+ end
105
+
106
+ def get_inclusive_tax(amount, tax_rate)
107
+ inclusive_tax = amount * (1 - (100/(100 + tax_rate)))
108
+ BigDecimal(inclusive_tax.to_s).round(2)
109
+ end
110
+
111
+ def get_exclusive_tax(amount, tax_rate)
112
+ exclusive_tax = amount * (tax_rate/100)
113
+ BigDecimal(exclusive_tax.to_s).round(2)
114
+ end
115
+
116
+ def get_tax_rate tax_type
117
+ @all_tax_types ||= client.TaxRate.all
118
+ @all_tax_types.select{|tax_rate| tax_rate.tax_type == tax_type}.first
119
+ end
120
+
121
+ can "create a new RECEIVE bank transaction" do
122
+ new_transaction = client.BankTransaction.build(
123
+ :type => "RECEIVE",
124
+ :contact => { :name => "Jazz Kang" },
125
+ :line_items => any_line_items(@account),
126
+ :bank_account => { :code => @bank_account.code }
127
+ )
128
+
129
+ assert new_transaction.save, "Save failed with the following errors: #{new_transaction.errors.inspect}"
130
+ assert_exists new_transaction
131
+ end
132
+
133
+ it "treats line item unit_amounts as tax EXCLUSIVE"
134
+ must "not set the tax_amount manually on line items"
135
+
136
+ def assert_exists(bank_transaction)
137
+ assert_not_nil bank_transaction.id,
138
+ "Cannot check for exitence unless the bank transaction has non-null identifier"
139
+ assert_not_nil client.BankTransaction.find bank_transaction.id
140
+ end
141
+
142
+ def any_line_items(account)
143
+ [{
144
+ :description => "Clingfilm bike shorts",
145
+ :quantity => 1,
146
+ :unit_amount => "17.00",
147
+ :account_code => account.code,
148
+ :tax_type => account.tax_type
149
+ }]
150
+ end
151
+
152
+ it "fails with RuntimeError when you try and create a new bank account" do
153
+ new_account = client.Account.build(
154
+ :name => "Example bank account",
155
+ :code => "ACC-001"
156
+ )
157
+
158
+ assert_raise RuntimeError do
159
+ new_account.save
160
+ end
161
+ end
162
+ end
@@ -0,0 +1,56 @@
1
+ require "test_helper"
2
+ require "acceptance_test"
3
+ require "bank_transaction_reference_data"
4
+
5
+ class AboutFetchingBankTransactions < Test::Unit::TestCase
6
+ include AcceptanceTest
7
+
8
+ def client
9
+ @client ||= Xeroizer::PrivateApplication.new(@consumer_key, @consumer_secret, @key_file)
10
+ end
11
+
12
+ context "when requesting all bank transactions (i.e., without filter)" do
13
+ setup do
14
+ @the_first_bank_transaction = client.BankTransaction.all.first
15
+ end
16
+
17
+ it "returns line items empty" do
18
+ assert_empty(@the_first_bank_transaction.line_items, "Expected line items to've been excluded")
19
+ end
20
+
21
+ it "returns contact with name and and id ONLY (no addresses or phones)" do
22
+ the_contact = @the_first_bank_transaction.contact
23
+ assert_not_nil(the_contact.contact_id, "Expected contact id to be present")
24
+ assert_not_nil(the_contact.name, "Expected contact name to be present")
25
+ assert_empty the_contact.phones, "Expected empty contact phones"
26
+ assert_empty the_contact.addresses, "Expected empty contact addresses"
27
+ end
28
+
29
+ it "returns the bank account" do
30
+ assert_not_nil @the_first_bank_transaction.bank_account
31
+ end
32
+ end
33
+
34
+ context "when requesting a single bank transaction for example" do
35
+ setup do
36
+ @a_new_bank_transaction = BankTransactionReferenceData.new(client).bank_transaction
37
+ end
38
+
39
+ it "returns contact with addresses and phones" do
40
+ single_bank_transaction = client.BankTransaction.find @a_new_bank_transaction.id
41
+
42
+ assert_not_empty single_bank_transaction.contact.addresses,
43
+ "expected the contact's addresses to have been included"
44
+
45
+ assert_not_empty single_bank_transaction.contact.phones,
46
+ "expected the contact's phone numbers to have been included"
47
+ end
48
+
49
+ it "returns full line item details" do
50
+ single_bank_transaction = client.BankTransaction.find @a_new_bank_transaction.id
51
+
52
+ assert_not_empty single_bank_transaction.line_items,
53
+ "expected the bank transaction's line items to have been included"
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,53 @@
1
+ module AcceptanceTest
2
+ class << self
3
+ def included(klass)
4
+ klass.class_eval do
5
+ def self.log_to_console
6
+ Xeroizer::Logging.const_set :Log, Xeroizer::Logging::StdOutLog
7
+ end
8
+
9
+ def self.no_log
10
+ Xeroizer::Logging.const_set :Log, Xeroizer::Logging::DevNullLog
11
+ end
12
+
13
+ def self.let(symbol, &block)
14
+ return unless block_given?
15
+
16
+ unless respond_to? symbol
17
+ define_method symbol, do
18
+ cached_method_result = instance_variable_get ivar_name = "@#{symbol}"
19
+ instance_variable_set(ivar_name, instance_eval(&block)) if cached_method_result.nil?
20
+ instance_variable_get ivar_name
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+
28
+ def setup
29
+ config = load_config_from_file || load_config_from_env
30
+
31
+ @key_file = config.key_file
32
+ @consumer_key = config.consumer_key
33
+ @consumer_secret = config.consumer_secret
34
+ end
35
+
36
+ private
37
+
38
+ def load_config_from_file
39
+ the_file_name = ".oauth"
40
+
41
+ return nil unless File.exists? the_file_name
42
+
43
+ Xeroizer::OAuthConfig.load IO.read the_file_name
44
+ end
45
+
46
+ def load_config_from_env
47
+ assert_not_nil ENV["CONSUMER_KEY"], "No CONSUMER_KEY environment variable specified."
48
+ assert_not_nil ENV["CONSUMER_SECRET"], "No CONSUMER_SECRET environment variable specified."
49
+ assert_not_nil ENV["KEY_FILE"], "No KEY_FILE environment variable specified."
50
+ assert File.exists?(ENV["KEY_FILE"]), "The file <#{ENV["KEY_FILE"]}> does not exist."
51
+ OAuthCredentials.new ENV["CONSUMER_KEY"], ENV["CONSUMER_SECRET"], ENV["KEY_FILE"]
52
+ end
53
+ end
@@ -0,0 +1,31 @@
1
+ class BankTransactionReferenceData
2
+ def initialize(client); @client = client; end
3
+ def bank_transaction; @bank_transaction ||= new_bank_transaction; end
4
+
5
+ private
6
+
7
+ def new_bank_transaction
8
+ all_accounts = @client.Account.all
9
+
10
+ account = all_accounts.select{|account| account.status == "ACTIVE" && account.type == "REVENUE"}.first
11
+ bank_account = all_accounts.select{|account| account.status == "ACTIVE" && account.type == "BANK"}.first
12
+
13
+ result = @client.BankTransaction.build(
14
+ :type => "SPEND",
15
+ :contact => { :name => "Jazz Kang" },
16
+ :line_items => [
17
+ :item_code => "Clingfilm bike shorts",
18
+ :description => "Bike shorts made of clear, unbreathable material",
19
+ :quantity => 1,
20
+ :unit_amount => 39.99,
21
+ :account_code => account.code,
22
+ :tax_type => account.tax_type
23
+ ],
24
+ :bank_account => { :code => bank_account.code }
25
+ )
26
+
27
+ fail("Expected save to have succeeded, but it failed. #{result.errors.inspect}") unless result.save
28
+
29
+ result
30
+ end
31
+ end
data/test/test_helper.rb CHANGED
@@ -7,9 +7,11 @@ require 'pp'
7
7
 
8
8
  require File.dirname(__FILE__) + '/../lib/xeroizer.rb'
9
9
 
10
+ $: << File.join(File.dirname(__FILE__), "acceptance")
11
+
10
12
  module TestHelper
11
13
 
12
- # The integration tests can be run against the Xero test environment. You mush have a company set up in the test
14
+ # The integration tests can be run against the Xero test environment. You must have a company set up in the test
13
15
  # environment, and you must have set up a customer key for that account.
14
16
  #
15
17
  # You can then run the tests against the test environment using the commands (linux or mac):
@@ -55,3 +57,11 @@ module TestHelper
55
57
  end
56
58
 
57
59
  end
60
+
61
+ Shoulda::ClassMethods.class_eval do
62
+ %w{it must can}.each do |m|
63
+ alias_method m, :should
64
+ end
65
+
66
+ alias_method :must_eventually, :should_eventually
67
+ end