xero_gateway-float 2.0.15

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 (71) hide show
  1. data/Gemfile +12 -0
  2. data/LICENSE +14 -0
  3. data/README.textile +357 -0
  4. data/Rakefile +14 -0
  5. data/examples/oauth.rb +25 -0
  6. data/examples/partner_app.rb +36 -0
  7. data/init.rb +1 -0
  8. data/lib/oauth/oauth_consumer.rb +14 -0
  9. data/lib/xero_gateway.rb +39 -0
  10. data/lib/xero_gateway/account.rb +95 -0
  11. data/lib/xero_gateway/accounts_list.rb +87 -0
  12. data/lib/xero_gateway/address.rb +96 -0
  13. data/lib/xero_gateway/bank_transaction.rb +178 -0
  14. data/lib/xero_gateway/ca-certificates.crt +2560 -0
  15. data/lib/xero_gateway/contact.rb +206 -0
  16. data/lib/xero_gateway/credit_note.rb +222 -0
  17. data/lib/xero_gateway/currency.rb +56 -0
  18. data/lib/xero_gateway/dates.rb +30 -0
  19. data/lib/xero_gateway/error.rb +18 -0
  20. data/lib/xero_gateway/exceptions.rb +46 -0
  21. data/lib/xero_gateway/gateway.rb +622 -0
  22. data/lib/xero_gateway/http.rb +138 -0
  23. data/lib/xero_gateway/http_encoding_helper.rb +49 -0
  24. data/lib/xero_gateway/invoice.rb +236 -0
  25. data/lib/xero_gateway/line_item.rb +125 -0
  26. data/lib/xero_gateway/line_item_calculations.rb +55 -0
  27. data/lib/xero_gateway/money.rb +16 -0
  28. data/lib/xero_gateway/oauth.rb +87 -0
  29. data/lib/xero_gateway/organisation.rb +75 -0
  30. data/lib/xero_gateway/partner_app.rb +30 -0
  31. data/lib/xero_gateway/payment.rb +40 -0
  32. data/lib/xero_gateway/phone.rb +77 -0
  33. data/lib/xero_gateway/private_app.rb +17 -0
  34. data/lib/xero_gateway/response.rb +41 -0
  35. data/lib/xero_gateway/tax_rate.rb +63 -0
  36. data/lib/xero_gateway/tracking_category.rb +87 -0
  37. data/test/integration/accounts_list_test.rb +109 -0
  38. data/test/integration/create_bank_transaction_test.rb +38 -0
  39. data/test/integration/create_contact_test.rb +66 -0
  40. data/test/integration/create_credit_note_test.rb +49 -0
  41. data/test/integration/create_invoice_test.rb +49 -0
  42. data/test/integration/get_accounts_test.rb +23 -0
  43. data/test/integration/get_bank_transaction_test.rb +51 -0
  44. data/test/integration/get_bank_transactions_test.rb +88 -0
  45. data/test/integration/get_contact_test.rb +28 -0
  46. data/test/integration/get_contacts_test.rb +40 -0
  47. data/test/integration/get_credit_note_test.rb +48 -0
  48. data/test/integration/get_credit_notes_test.rb +90 -0
  49. data/test/integration/get_currencies_test.rb +25 -0
  50. data/test/integration/get_invoice_test.rb +48 -0
  51. data/test/integration/get_invoices_test.rb +92 -0
  52. data/test/integration/get_organisation_test.rb +24 -0
  53. data/test/integration/get_tax_rates_test.rb +25 -0
  54. data/test/integration/get_tracking_categories_test.rb +27 -0
  55. data/test/integration/update_bank_transaction_test.rb +31 -0
  56. data/test/integration/update_contact_test.rb +31 -0
  57. data/test/integration/update_invoice_test.rb +31 -0
  58. data/test/test_helper.rb +179 -0
  59. data/test/unit/account_test.rb +47 -0
  60. data/test/unit/bank_transaction_test.rb +126 -0
  61. data/test/unit/contact_test.rb +97 -0
  62. data/test/unit/credit_note_test.rb +284 -0
  63. data/test/unit/currency_test.rb +31 -0
  64. data/test/unit/gateway_test.rb +119 -0
  65. data/test/unit/invoice_test.rb +326 -0
  66. data/test/unit/oauth_test.rb +116 -0
  67. data/test/unit/organisation_test.rb +38 -0
  68. data/test/unit/tax_rate_test.rb +38 -0
  69. data/test/unit/tracking_category_test.rb +52 -0
  70. data/xero_gateway.gemspec +15 -0
  71. metadata +164 -0
@@ -0,0 +1,206 @@
1
+ module XeroGateway
2
+ class Contact
3
+ include Dates
4
+
5
+ class Error < RuntimeError; end
6
+ class NoGatewayError < Error; end
7
+
8
+ GUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/ unless defined?(GUID_REGEX)
9
+
10
+ CONTACT_STATUS = {
11
+ 'ACTIVE' => 'Active',
12
+ 'DELETED' => 'Deleted'
13
+ } unless defined?(CONTACT_STATUS)
14
+
15
+ # Xero::Gateway associated with this contact.
16
+ attr_accessor :gateway
17
+
18
+ # Any errors that occurred when the #valid? method called.
19
+ attr_reader :errors
20
+
21
+ attr_accessor :contact_id, :contact_number, :status, :name, :first_name, :last_name, :email, :addresses, :phones, :updated_at,
22
+ :bank_account_details, :tax_number, :accounts_receivable_tax_type, :accounts_payable_tax_type, :is_customer, :is_supplier,
23
+ :default_currency, :contact_groups
24
+
25
+
26
+ def initialize(params = {})
27
+ @errors ||= []
28
+
29
+ params = {}.merge(params)
30
+ params.each do |k,v|
31
+ self.send("#{k}=", v)
32
+ end
33
+
34
+ @phones ||= []
35
+ @addresses ||= []
36
+ end
37
+
38
+ def address=(address)
39
+ self.addresses = [address]
40
+ end
41
+
42
+ def address
43
+ self.addresses[0] ||= Address.new
44
+ end
45
+
46
+ # Helper method to add a new address object to this contact.
47
+ #
48
+ # Usage:
49
+ # contact.add_address({
50
+ # :address_type => 'STREET',
51
+ # :line_1 => '100 Queen Street',
52
+ # :city => 'Brisbane',
53
+ # :region => 'QLD',
54
+ # :post_code => '4000',
55
+ # :country => 'Australia'
56
+ # })
57
+ def add_address(address_params)
58
+ self.addresses << Address.new(address_params)
59
+ end
60
+
61
+ def phone=(phone)
62
+ self.phones = [phone]
63
+ end
64
+
65
+ def phone
66
+ if @phones.size > 1
67
+ @phones.detect {|p| p.phone_type == 'DEFAULT'} || phones[0]
68
+ else
69
+ @phones[0] ||= Phone.new
70
+ end
71
+ end
72
+
73
+ # Helper method to add a new phone object to this contact.
74
+ #
75
+ # Usage:
76
+ # contact.add_phone({
77
+ # :phone_type => 'MOBILE',
78
+ # :number => '0400123123'
79
+ # })
80
+ def add_phone(phone_params = {})
81
+ self.phones << Phone.new(phone_params)
82
+ end
83
+
84
+ # Validate the Contact record according to what will be valid by the gateway.
85
+ #
86
+ # Usage:
87
+ # contact.valid? # Returns true/false
88
+ #
89
+ # Additionally sets contact.errors array to an array of field/error.
90
+ def valid?
91
+ @errors = []
92
+
93
+ if !contact_id.nil? && contact_id !~ GUID_REGEX
94
+ @errors << ['contact_id', 'must be blank or a valid Xero GUID']
95
+ end
96
+
97
+ if status && !CONTACT_STATUS[status]
98
+ @errors << ['status', "must be one of #{CONTACT_STATUS.keys.join('/')}"]
99
+ end
100
+
101
+ unless name
102
+ @errors << ['name', "can't be blank"]
103
+ end
104
+
105
+ # Make sure all addresses are correct.
106
+ unless addresses.all? { | address | address.valid? }
107
+ @errors << ['addresses', 'at least one address is invalid']
108
+ end
109
+
110
+ # Make sure all phone numbers are correct.
111
+ unless phones.all? { | phone | phone.valid? }
112
+ @errors << ['phones', 'at least one phone is invalid']
113
+ end
114
+
115
+ @errors.size == 0
116
+ end
117
+
118
+ # General purpose create/save method.
119
+ # If contact_id and contact_number are nil then create, otherwise, attempt to save.
120
+ def save
121
+ if contact_id.nil? && contact_number.nil?
122
+ create
123
+ else
124
+ update
125
+ end
126
+ end
127
+
128
+ # Creates this contact record (using gateway.create_contact) with the associated gateway.
129
+ # If no gateway set, raise a Xero::Contact::NoGatewayError exception.
130
+ def create
131
+ raise NoGatewayError unless gateway
132
+ gateway.create_contact(self)
133
+ end
134
+
135
+ # Creates this contact record (using gateway.update_contact) with the associated gateway.
136
+ # If no gateway set, raise a Xero::Contact::NoGatewayError exception.
137
+ def update
138
+ raise NoGatewayError unless gateway
139
+ gateway.update_contact(self)
140
+ end
141
+
142
+ def to_xml(b = Builder::XmlMarkup.new)
143
+ b.Contact {
144
+ b.ContactID self.contact_id if self.contact_id
145
+ b.ContactNumber self.contact_number if self.contact_number
146
+ b.Name self.name
147
+ b.EmailAddress self.email if self.email
148
+ b.FirstName self.first_name if self.first_name
149
+ b.LastName self.last_name if self.last_name
150
+ b.BankAccountDetails self.bank_account_details if self.bank_account_details
151
+ b.TaxNumber self.tax_number if self.tax_number
152
+ b.AccountsReceivableTaxType self.accounts_receivable_tax_type if self.accounts_receivable_tax_type
153
+ b.AccountsPayableTaxType self.accounts_payable_tax_type if self.accounts_payable_tax_type
154
+ b.ContactGroups if self.contact_groups
155
+ b.IsCustomer true if self.is_customer
156
+ b.IsSupplier true if self.is_supplier
157
+ b.DefaultCurrency if self.default_currency
158
+ b.Addresses {
159
+ addresses.each { |address| address.to_xml(b) }
160
+ }
161
+ b.Phones {
162
+ phones.each { |phone| phone.to_xml(b) }
163
+ }
164
+ }
165
+ end
166
+
167
+ # Take a Contact element and convert it into an Contact object
168
+ def self.from_xml(contact_element, gateway = nil)
169
+ contact = Contact.new(:gateway => gateway)
170
+ contact_element.children.each do |element|
171
+ case(element.name)
172
+ when "ContactID" then contact.contact_id = element.text
173
+ when "ContactNumber" then contact.contact_number = element.text
174
+ when "ContactStatus" then contact.status = element.text
175
+ when "Name" then contact.name = element.text
176
+ when "FirstName" then contact.first_name = element.text
177
+ when "LastName" then contact.last_name = element.text
178
+ when "EmailAddress" then contact.email = element.text
179
+ when "Addresses" then element.children.each {|address_element| contact.addresses << Address.from_xml(address_element)}
180
+ when "Phones" then element.children.each {|phone_element| contact.phones << Phone.from_xml(phone_element)}
181
+ when "FirstName" then contact.first_name = element.text
182
+ when "LastName" then contact.last_name = element.text
183
+ when "BankAccountDetails" then contact.bank_account_details = element.text
184
+ when "TaxNumber" then contact.tax_number = element.text
185
+ when "AccountsReceivableTaxType" then contact.accounts_receivable_tax_type = element.text
186
+ when "AccountsPayableTaxType" then contact.accounts_payable_tax_type = element.text
187
+ when "ContactGroups" then contact.contact_groups = element.text
188
+ when "IsCustomer" then contact.is_customer = (element.text == "true")
189
+ when "IsSupplier" then contact.is_supplier = (element.text == "true")
190
+ when "DefaultCurrency" then contact.default_currency = element.text
191
+ end
192
+ end
193
+ contact
194
+ end
195
+
196
+ def ==(other)
197
+ [ :contact_id, :contact_number, :status, :name, :first_name, :last_name, :email, :addresses, :phones, :updated_at,
198
+ :bank_account_details, :tax_number, :accounts_receivable_tax_type, :accounts_payable_tax_type, :is_customer, :is_supplier,
199
+ :default_currency, :contact_groups ].each do |field|
200
+ return false if send(field) != other.send(field)
201
+ end
202
+ return true
203
+ end
204
+
205
+ end
206
+ end
@@ -0,0 +1,222 @@
1
+ module XeroGateway
2
+ class CreditNote
3
+ include Dates
4
+ include Money
5
+ include LineItemCalculations
6
+
7
+ class NoGatewayError < Error; end
8
+
9
+ CREDIT_NOTE_TYPE = {
10
+ 'ACCRECCREDIT' => 'Accounts Receivable',
11
+ 'ACCPAYCREDIT' => 'Accounts Payable'
12
+ } unless defined?(CREDIT_NOTE_TYPE)
13
+
14
+ LINE_AMOUNT_TYPES = {
15
+ "Inclusive" => 'CreditNote lines are inclusive tax',
16
+ "Exclusive" => 'CreditNote lines are exclusive of tax (default)',
17
+ "NoTax" => 'CreditNotes lines have no tax'
18
+ } unless defined?(LINE_AMOUNT_TYPES)
19
+
20
+ CREDIT_NOTE_STATUS = {
21
+ 'AUTHORISED' => 'Approved credit_notes awaiting payment',
22
+ 'DELETED' => 'Draft credit_notes that are deleted',
23
+ 'DRAFT' => 'CreditNotes saved as draft or entered via API',
24
+ 'PAID' => 'CreditNotes approved and fully paid',
25
+ 'SUBMITTED' => 'CreditNotes entered by an employee awaiting approval',
26
+ 'VOID' => 'Approved credit_notes that are voided'
27
+ } unless defined?(CREDIT_NOTE_STATUS)
28
+
29
+ GUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/ unless defined?(GUID_REGEX)
30
+
31
+ # Xero::Gateway associated with this credit_note.
32
+ attr_accessor :gateway
33
+
34
+ # Any errors that occurred when the #valid? method called.
35
+ attr_reader :errors
36
+
37
+ # Represents whether the line_items have been downloaded when getting from GET /API.XRO/2.0/CreditNotes
38
+ attr_accessor :line_items_downloaded
39
+
40
+ # All accessible fields
41
+ attr_accessor :credit_note_id, :credit_note_number, :type, :status, :date, :reference, :line_amount_types, :currency_code, :line_items, :contact, :payments, :fully_paid_on, :amount_credited
42
+
43
+
44
+ def initialize(params = {})
45
+ @errors ||= []
46
+ @payments ||= []
47
+
48
+ # Check if the line items have been downloaded.
49
+ @line_items_downloaded = (params.delete(:line_items_downloaded) == true)
50
+
51
+ params = {
52
+ :line_amount_types => "Inclusive"
53
+ }.merge(params)
54
+
55
+ params.each do |k,v|
56
+ self.send("#{k}=", v)
57
+ end
58
+
59
+ @line_items ||= []
60
+ end
61
+
62
+ # Validate the Address record according to what will be valid by the gateway.
63
+ #
64
+ # Usage:
65
+ # address.valid? # Returns true/false
66
+ #
67
+ # Additionally sets address.errors array to an array of field/error.
68
+ def valid?
69
+ @errors = []
70
+
71
+ if !credit_note_id.nil? && credit_note_id !~ GUID_REGEX
72
+ @errors << ['credit_note_id', 'must be blank or a valid Xero GUID']
73
+ end
74
+
75
+ if status && !CREDIT_NOTE_STATUS[status]
76
+ @errors << ['status', "must be one of #{CREDIT_NOTE_STATUS.keys.join('/')}"]
77
+ end
78
+
79
+ if line_amount_types && !LINE_AMOUNT_TYPES[line_amount_types]
80
+ @errors << ['line_amount_types', "must be one of #{LINE_AMOUNT_TYPES.keys.join('/')}"]
81
+ end
82
+
83
+ unless date
84
+ @errors << ['credit_note_date', "can't be blank"]
85
+ end
86
+
87
+ # Make sure contact is valid.
88
+ unless @contact && @contact.valid?
89
+ @errors << ['contact', 'is invalid']
90
+ end
91
+
92
+ # Make sure all line_items are valid.
93
+ unless line_items.all? { | line_item | line_item.valid? }
94
+ @errors << ['line_items', "at least one line item invalid"]
95
+ end
96
+
97
+ @errors.size == 0
98
+ end
99
+
100
+ # Helper method to create the associated contact object.
101
+ def build_contact(params = {})
102
+ self.contact = gateway ? gateway.build_contact(params) : Contact.new(params)
103
+ end
104
+
105
+ def contact
106
+ @contact ||= build_contact
107
+ end
108
+
109
+ # Helper method to check if the credit_note is accounts payable.
110
+ def accounts_payable?
111
+ type == 'ACCPAYCREDIT'
112
+ end
113
+
114
+ # Helper method to check if the credit_note is accounts receivable.
115
+ def accounts_receivable?
116
+ type == 'ACCRECCREDIT'
117
+ end
118
+
119
+ # Whether or not the line_items have been downloaded (GET/credit_notes does not download line items).
120
+ def line_items_downloaded?
121
+ @line_items_downloaded
122
+ end
123
+
124
+ # If line items are not downloaded, then attempt a download now (if this record was found to begin with).
125
+ def line_items
126
+ if line_items_downloaded?
127
+ @line_items
128
+
129
+ # There is an credit_note_is so we can assume this record was loaded from Xero.
130
+ # attempt to download the line_item records.
131
+ elsif credit_note_id =~ GUID_REGEX
132
+ raise NoGatewayError unless @gateway
133
+
134
+ response = @gateway.get_credit_note(credit_note_id)
135
+ raise CreditNoteNotFoundError, "CreditNote with ID #{credit_note_id} not found in Xero." unless response.success? && response.credit_note.is_a?(XeroGateway::CreditNote)
136
+
137
+ @line_items = response.credit_note.line_items
138
+ @line_items_downloaded = true
139
+
140
+ @line_items
141
+
142
+ # Otherwise, this is a new credit_note, so return the line_items reference.
143
+ else
144
+ @line_items
145
+ end
146
+ end
147
+
148
+ def ==(other)
149
+ ["credit_note_number", "type", "status", "reference", "currency_code", "line_amount_types", "contact", "line_items"].each do |field|
150
+ return false if send(field) != other.send(field)
151
+ end
152
+
153
+ ["date"].each do |field|
154
+ return false if send(field).to_s != other.send(field).to_s
155
+ end
156
+ return true
157
+ end
158
+
159
+ # General purpose createsave method.
160
+ # If contact_id and contact_number are nil then create, otherwise, attempt to save.
161
+ def save
162
+ create
163
+ end
164
+
165
+ # Creates this credit_note record (using gateway.create_credit_note) with the associated gateway.
166
+ # If no gateway set, raise a Xero::CreditNote::NoGatewayError exception.
167
+ def create
168
+ raise NoGatewayError unless gateway
169
+ gateway.create_credit_note(self)
170
+ end
171
+
172
+ # Alias create as save as this is currently the only write action.
173
+ alias_method :save, :create
174
+
175
+ def to_xml(b = Builder::XmlMarkup.new)
176
+ b.CreditNote {
177
+ b.Type self.type
178
+ contact.to_xml(b)
179
+ b.Date CreditNote.format_date(self.date || Date.today)
180
+ b.Status self.status if self.status
181
+ b.CreditNoteNumber self.credit_note_number if credit_note_number
182
+ b.Reference self.reference if self.reference
183
+ b.CurrencyCode self.currency_code if self.currency_code
184
+ b.LineAmountTypes self.line_amount_types
185
+ b.LineItems {
186
+ self.line_items.each do |line_item|
187
+ line_item.to_xml(b)
188
+ end
189
+ }
190
+ }
191
+ end
192
+
193
+ #TODO UpdatedDateUTC
194
+ def self.from_xml(credit_note_element, gateway = nil, options = {})
195
+ credit_note = CreditNote.new(options.merge({:gateway => gateway}))
196
+ credit_note_element.children.each do |element|
197
+ case(element.name)
198
+ when "CreditNoteID" then credit_note.credit_note_id = element.text
199
+ when "CreditNoteNumber" then credit_note.credit_note_number = element.text
200
+ when "Type" then credit_note.type = element.text
201
+ when "CurrencyCode" then credit_note.currency_code = element.text
202
+ when "Contact" then credit_note.contact = Contact.from_xml(element)
203
+ when "Date" then credit_note.date = parse_date(element.text)
204
+ when "Status" then credit_note.status = element.text
205
+ when "Reference" then credit_note.reference = element.text
206
+ when "LineAmountTypes" then credit_note.line_amount_types = element.text
207
+ when "LineItems" then element.children.each {|line_item| credit_note.line_items_downloaded = true; credit_note.line_items << LineItem.from_xml(line_item) }
208
+ when "SubTotal" then credit_note.sub_total = BigDecimal.new(element.text)
209
+ when "TotalTax" then credit_note.total_tax = BigDecimal.new(element.text)
210
+ when "Total" then credit_note.total = BigDecimal.new(element.text)
211
+ when "CreditNoteID" then credit_note.credit_note_id = element.text
212
+ when "CreditNoteNumber" then credit_note.credit_note_number = element.text
213
+ when "Payments" then element.children.each { | payment | credit_note.payments << Payment.from_xml(payment) }
214
+ when "AmountDue" then credit_note.amount_due = BigDecimal.new(element.text)
215
+ when "AmountPaid" then credit_note.amount_paid = BigDecimal.new(element.text)
216
+ when "AmountCredited" then credit_note.amount_credited = BigDecimal.new(element.text)
217
+ end
218
+ end
219
+ credit_note
220
+ end
221
+ end
222
+ end
@@ -0,0 +1,56 @@
1
+ module XeroGateway
2
+ class Currency
3
+
4
+ unless defined? ATTRS
5
+ ATTRS = {
6
+ "Code" => :string, # 3 letter alpha code for the currency – see list of currency codes
7
+ "Description" => :string, # Name of Currency
8
+ }
9
+ end
10
+
11
+ attr_accessor *ATTRS.keys.map(&:underscore)
12
+
13
+ def initialize(params = {})
14
+ params.each do |k,v|
15
+ self.send("#{k}=", v)
16
+ end
17
+ end
18
+
19
+ def ==(other)
20
+ ATTRS.keys.map(&:underscore).each do |field|
21
+ return false if send(field) != other.send(field)
22
+ end
23
+ return true
24
+ end
25
+
26
+ def to_xml
27
+ b = Builder::XmlMarkup.new
28
+
29
+ b.Currency do
30
+ ATTRS.keys.each do |attr|
31
+ eval("b.#{attr} '#{self.send(attr.underscore.to_sym)}'")
32
+ end
33
+ end
34
+ end
35
+
36
+ def self.from_xml(currency_element)
37
+ Currency.new.tap do |currency|
38
+ currency_element.children.each do |element|
39
+
40
+ attribute = element.name
41
+ underscored_attribute = element.name.underscore
42
+
43
+ raise "Unknown attribute: #{attribute}" unless ATTRS.keys.include?(attribute)
44
+
45
+ case (ATTRS[attribute])
46
+ when :boolean then currency.send("#{underscored_attribute}=", (element.text == "true"))
47
+ when :float then currency.send("#{underscored_attribute}=", element.text.to_f)
48
+ else currency.send("#{underscored_attribute}=", element.text)
49
+ end
50
+
51
+ end
52
+ end
53
+ end
54
+
55
+ end
56
+ end