xeroizer 0.3.5 → 0.4.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 (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