quickbooks-ruby-oauth2 0.6.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (169) hide show
  1. checksums.yaml +7 -0
  2. data/lib/quickbooks/faraday/middleware/gzip.rb +72 -0
  3. data/lib/quickbooks/model/access_token_response.rb +29 -0
  4. data/lib/quickbooks/model/account.rb +78 -0
  5. data/lib/quickbooks/model/account_based_expense_line_detail.rb +15 -0
  6. data/lib/quickbooks/model/attachable.rb +33 -0
  7. data/lib/quickbooks/model/attachable_ref.rb +22 -0
  8. data/lib/quickbooks/model/base_model.rb +120 -0
  9. data/lib/quickbooks/model/base_model_json.rb +16 -0
  10. data/lib/quickbooks/model/base_reference.rb +23 -0
  11. data/lib/quickbooks/model/batch_request.rb +45 -0
  12. data/lib/quickbooks/model/batch_response.rb +33 -0
  13. data/lib/quickbooks/model/bill.rb +45 -0
  14. data/lib/quickbooks/model/bill_line_item.rb +42 -0
  15. data/lib/quickbooks/model/bill_payment.rb +40 -0
  16. data/lib/quickbooks/model/bill_payment_check.rb +12 -0
  17. data/lib/quickbooks/model/bill_payment_credit_card.rb +10 -0
  18. data/lib/quickbooks/model/bill_payment_line_item.rb +21 -0
  19. data/lib/quickbooks/model/budget.rb +27 -0
  20. data/lib/quickbooks/model/budget_line_item.rb +15 -0
  21. data/lib/quickbooks/model/change_data_capture.rb +42 -0
  22. data/lib/quickbooks/model/change_model.rb +10 -0
  23. data/lib/quickbooks/model/check_payment.rb +11 -0
  24. data/lib/quickbooks/model/class.rb +31 -0
  25. data/lib/quickbooks/model/company_info.rb +69 -0
  26. data/lib/quickbooks/model/credit_card_payment.rb +17 -0
  27. data/lib/quickbooks/model/credit_memo.rb +59 -0
  28. data/lib/quickbooks/model/credit_memo_change.rb +8 -0
  29. data/lib/quickbooks/model/custom_field.rb +13 -0
  30. data/lib/quickbooks/model/customer.rb +76 -0
  31. data/lib/quickbooks/model/customer_change.rb +8 -0
  32. data/lib/quickbooks/model/definition.rb +46 -0
  33. data/lib/quickbooks/model/delivery_info.rb +8 -0
  34. data/lib/quickbooks/model/department.rb +31 -0
  35. data/lib/quickbooks/model/deposit.rb +44 -0
  36. data/lib/quickbooks/model/deposit_line_detail.rb +17 -0
  37. data/lib/quickbooks/model/deposit_line_item.rb +35 -0
  38. data/lib/quickbooks/model/description_line_detail.rb +7 -0
  39. data/lib/quickbooks/model/discount_line_detail.rb +15 -0
  40. data/lib/quickbooks/model/discount_override.rb +13 -0
  41. data/lib/quickbooks/model/document_numbering.rb +18 -0
  42. data/lib/quickbooks/model/effective_tax_rate.rb +15 -0
  43. data/lib/quickbooks/model/email_address.rb +19 -0
  44. data/lib/quickbooks/model/employee.rb +47 -0
  45. data/lib/quickbooks/model/entity.rb +16 -0
  46. data/lib/quickbooks/model/entity_ref.rb +8 -0
  47. data/lib/quickbooks/model/estimate.rb +76 -0
  48. data/lib/quickbooks/model/exchange_rate.rb +23 -0
  49. data/lib/quickbooks/model/fault.rb +16 -0
  50. data/lib/quickbooks/model/global_tax_calculation.rb +13 -0
  51. data/lib/quickbooks/model/group_line_detail.rb +13 -0
  52. data/lib/quickbooks/model/has_line_items.rb +14 -0
  53. data/lib/quickbooks/model/invoice.rb +107 -0
  54. data/lib/quickbooks/model/invoice_change.rb +9 -0
  55. data/lib/quickbooks/model/invoice_group_line_detail.rb +14 -0
  56. data/lib/quickbooks/model/invoice_line_item.rb +88 -0
  57. data/lib/quickbooks/model/item.rb +87 -0
  58. data/lib/quickbooks/model/item_based_expense_line_detail.rb +20 -0
  59. data/lib/quickbooks/model/item_change.rb +8 -0
  60. data/lib/quickbooks/model/item_group_detail.rb +9 -0
  61. data/lib/quickbooks/model/item_group_line.rb +20 -0
  62. data/lib/quickbooks/model/journal_entry.rb +36 -0
  63. data/lib/quickbooks/model/journal_entry_line_detail.rb +27 -0
  64. data/lib/quickbooks/model/line.rb +95 -0
  65. data/lib/quickbooks/model/line_ex.rb +9 -0
  66. data/lib/quickbooks/model/linked_transaction.rb +9 -0
  67. data/lib/quickbooks/model/markup_info.rb +12 -0
  68. data/lib/quickbooks/model/meta_data.rb +24 -0
  69. data/lib/quickbooks/model/name_value.rb +8 -0
  70. data/lib/quickbooks/model/other_contact_info.rb +8 -0
  71. data/lib/quickbooks/model/payment.rb +43 -0
  72. data/lib/quickbooks/model/payment_change.rb +8 -0
  73. data/lib/quickbooks/model/payment_line_detail.rb +12 -0
  74. data/lib/quickbooks/model/payment_method.rb +25 -0
  75. data/lib/quickbooks/model/physical_address.rb +40 -0
  76. data/lib/quickbooks/model/preferences.rb +49 -0
  77. data/lib/quickbooks/model/purchase.rb +57 -0
  78. data/lib/quickbooks/model/purchase_line_item.rb +44 -0
  79. data/lib/quickbooks/model/purchase_order.rb +50 -0
  80. data/lib/quickbooks/model/purchase_tax_rate_list.rb +11 -0
  81. data/lib/quickbooks/model/refund_receipt.rb +62 -0
  82. data/lib/quickbooks/model/refund_receipt_change.rb +8 -0
  83. data/lib/quickbooks/model/report.rb +52 -0
  84. data/lib/quickbooks/model/sales_item_line_detail.rb +16 -0
  85. data/lib/quickbooks/model/sales_receipt.rb +65 -0
  86. data/lib/quickbooks/model/sales_tax_rate_list.rb +11 -0
  87. data/lib/quickbooks/model/sub_total_line_detail.rb +13 -0
  88. data/lib/quickbooks/model/tax_agency.rb +18 -0
  89. data/lib/quickbooks/model/tax_code.rb +21 -0
  90. data/lib/quickbooks/model/tax_line.rb +15 -0
  91. data/lib/quickbooks/model/tax_line_detail.rb +16 -0
  92. data/lib/quickbooks/model/tax_rate.rb +28 -0
  93. data/lib/quickbooks/model/tax_rate_detail.rb +15 -0
  94. data/lib/quickbooks/model/tax_rate_detail_line.rb +23 -0
  95. data/lib/quickbooks/model/tax_service.rb +52 -0
  96. data/lib/quickbooks/model/telephone_number.rb +13 -0
  97. data/lib/quickbooks/model/term.rb +28 -0
  98. data/lib/quickbooks/model/time_activity.rb +79 -0
  99. data/lib/quickbooks/model/transaction_tax_detail.rb +12 -0
  100. data/lib/quickbooks/model/transfer.rb +21 -0
  101. data/lib/quickbooks/model/upload.rb +35 -0
  102. data/lib/quickbooks/model/validator.rb +10 -0
  103. data/lib/quickbooks/model/vendor.rb +53 -0
  104. data/lib/quickbooks/model/vendor_change.rb +8 -0
  105. data/lib/quickbooks/model/vendor_credit.rb +40 -0
  106. data/lib/quickbooks/model/web_site_address.rb +16 -0
  107. data/lib/quickbooks/service/access_token.rb +38 -0
  108. data/lib/quickbooks/service/account.rb +17 -0
  109. data/lib/quickbooks/service/attachable.rb +12 -0
  110. data/lib/quickbooks/service/base_service.rb +445 -0
  111. data/lib/quickbooks/service/base_service_json.rb +39 -0
  112. data/lib/quickbooks/service/batch.rb +11 -0
  113. data/lib/quickbooks/service/bill.rb +16 -0
  114. data/lib/quickbooks/service/bill_payment.rb +16 -0
  115. data/lib/quickbooks/service/budget.rb +12 -0
  116. data/lib/quickbooks/service/change_data_capture.rb +24 -0
  117. data/lib/quickbooks/service/change_service.rb +28 -0
  118. data/lib/quickbooks/service/class.rb +23 -0
  119. data/lib/quickbooks/service/company_info.rb +12 -0
  120. data/lib/quickbooks/service/credit_memo.rb +16 -0
  121. data/lib/quickbooks/service/credit_memo_change.rb +16 -0
  122. data/lib/quickbooks/service/customer.rb +18 -0
  123. data/lib/quickbooks/service/customer_change.rb +16 -0
  124. data/lib/quickbooks/service/department.rb +23 -0
  125. data/lib/quickbooks/service/deposit.rb +16 -0
  126. data/lib/quickbooks/service/employee.rb +24 -0
  127. data/lib/quickbooks/service/estimate.rb +22 -0
  128. data/lib/quickbooks/service/exchange_rate.rb +25 -0
  129. data/lib/quickbooks/service/invoice.rb +45 -0
  130. data/lib/quickbooks/service/invoice_change.rb +16 -0
  131. data/lib/quickbooks/service/item.rb +32 -0
  132. data/lib/quickbooks/service/item_change.rb +16 -0
  133. data/lib/quickbooks/service/journal_entry.rb +16 -0
  134. data/lib/quickbooks/service/payment.rb +29 -0
  135. data/lib/quickbooks/service/payment_change.rb +16 -0
  136. data/lib/quickbooks/service/payment_method.rb +30 -0
  137. data/lib/quickbooks/service/preferences.rb +12 -0
  138. data/lib/quickbooks/service/purchase.rb +16 -0
  139. data/lib/quickbooks/service/purchase_order.rb +16 -0
  140. data/lib/quickbooks/service/refund_receipt.rb +16 -0
  141. data/lib/quickbooks/service/refund_receipt_change.rb +16 -0
  142. data/lib/quickbooks/service/reports.rb +33 -0
  143. data/lib/quickbooks/service/responses/methods.rb +17 -0
  144. data/lib/quickbooks/service/responses/oauth1_http_response.rb +42 -0
  145. data/lib/quickbooks/service/responses/oauth2_http_response.rb +43 -0
  146. data/lib/quickbooks/service/responses/oauth_http_response.rb +21 -0
  147. data/lib/quickbooks/service/sales_receipt.rb +46 -0
  148. data/lib/quickbooks/service/service_crud.rb +75 -0
  149. data/lib/quickbooks/service/service_crud_json.rb +31 -0
  150. data/lib/quickbooks/service/tax_agency.rb +12 -0
  151. data/lib/quickbooks/service/tax_code.rb +12 -0
  152. data/lib/quickbooks/service/tax_rate.rb +12 -0
  153. data/lib/quickbooks/service/tax_service.rb +11 -0
  154. data/lib/quickbooks/service/term.rb +17 -0
  155. data/lib/quickbooks/service/time_activity.rb +16 -0
  156. data/lib/quickbooks/service/transfer.rb +16 -0
  157. data/lib/quickbooks/service/upload.rb +39 -0
  158. data/lib/quickbooks/service/vendor.rb +24 -0
  159. data/lib/quickbooks/service/vendor_change.rb +16 -0
  160. data/lib/quickbooks/service/vendor_credit.rb +16 -0
  161. data/lib/quickbooks/util/collection.rb +18 -0
  162. data/lib/quickbooks/util/http_encoding_helper.rb +54 -0
  163. data/lib/quickbooks/util/logging.rb +19 -0
  164. data/lib/quickbooks/util/multipart.rb +85 -0
  165. data/lib/quickbooks/util/name_entity.rb +85 -0
  166. data/lib/quickbooks/util/query_builder.rb +35 -0
  167. data/lib/quickbooks/version.rb +5 -0
  168. data/lib/quickbooks-ruby.rb +255 -0
  169. metadata +378 -0
@@ -0,0 +1,40 @@
1
+ # === Business Rules
2
+ # * The VendorRef attribute must be specified.
3
+ # * At lease one Line with Line.Amount must be specified.
4
+
5
+ module Quickbooks
6
+ module Model
7
+ class VendorCredit < BaseModel
8
+ include GlobalTaxCalculation
9
+ include HasLineItems
10
+
11
+ #== Constants
12
+ REST_RESOURCE = 'vendorcredit'
13
+ XML_COLLECTION_NODE = "VendorCredit"
14
+ XML_NODE = "VendorCredit"
15
+
16
+ xml_accessor :id, :from => 'Id'
17
+ xml_accessor :sync_token, :from => 'SyncToken', :as => Integer
18
+ xml_accessor :meta_data, :from => 'MetaData', :as => MetaData
19
+ xml_accessor :doc_number, :from => 'DocNumber'
20
+ xml_accessor :txn_date, :from => 'TxnDate', :as => Date
21
+ xml_accessor :private_note, :from => 'PrivateNote'
22
+
23
+ xml_accessor :line_items, :from => 'Line', :as => [PurchaseLineItem]
24
+ xml_accessor :department_ref, :from => 'DepartmentRef', :as => BaseReference
25
+ xml_accessor :ap_account_ref, :from => 'APAccountRef', :as => BaseReference
26
+ xml_accessor :vendor_ref, :from => 'VendorRef', :as => BaseReference
27
+ xml_accessor :total, :from => 'TotalAmt', :as => BigDecimal, :to_xml => Proc.new { |val| val.to_f }
28
+
29
+ xml_accessor :currency_ref, :from => 'CurrencyRef', :as => BaseReference
30
+ xml_accessor :exchange_rate, :from => 'ExchangeRate', :as => BigDecimal, :to_xml => to_xml_big_decimal
31
+
32
+ reference_setters
33
+
34
+ #== This adds aliases for backwards compatability to old attributes names
35
+ alias_method :total_amount, :total
36
+ alias_method :total_amount=, :total=
37
+
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,16 @@
1
+ module Quickbooks
2
+ module Model
3
+ class WebSiteAddress < BaseModel
4
+ xml_accessor :uri, :from => 'URI'
5
+
6
+ def initialize(uri = nil)
7
+ self.uri = uri if uri
8
+ end
9
+
10
+ def to_xml(options = {})
11
+ return "" if uri.to_s.empty?
12
+ super(options)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,38 @@
1
+ module Quickbooks
2
+ module Service
3
+ class AccessToken < BaseService
4
+
5
+ RENEW_URL = "https://appcenter.intuit.com/api/v1/connection/reconnect"
6
+ DISCONNECT_URL = "https://appcenter.intuit.com/api/v1/connection/disconnect"
7
+
8
+ # https://developer.intuit.com/docs/0025_quickbooksapi/0053_auth_auth/oauth_management_api#Reconnect
9
+ def renew
10
+ result = nil
11
+ response = do_http_get(RENEW_URL)
12
+ if response
13
+ code = response.code.to_i
14
+ if code == 200
15
+ result = Quickbooks::Model::AccessTokenResponse.from_xml(response.plain_body)
16
+ end
17
+ end
18
+
19
+ result
20
+ end
21
+
22
+ # https://developer.intuit.com/docs/0025_quickbooksapi/0053_auth_auth/oauth_management_api#Disconnect
23
+ def disconnect
24
+ result = nil
25
+ response = do_http_get(DISCONNECT_URL)
26
+ if response
27
+ code = response.code.to_i
28
+ if code == 200
29
+ result = Quickbooks::Model::AccessTokenResponse.from_xml(response.plain_body)
30
+ end
31
+ end
32
+
33
+ result
34
+ end
35
+
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,17 @@
1
+ module Quickbooks
2
+ module Service
3
+ class Account < BaseService
4
+
5
+ def delete(account)
6
+ account.active = false
7
+ update(account, :sparse => true)
8
+ end
9
+
10
+ private
11
+
12
+ def model
13
+ Quickbooks::Model::Account
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,12 @@
1
+ module Quickbooks
2
+ module Service
3
+ class Attachable < BaseService
4
+
5
+ private
6
+
7
+ def model
8
+ Quickbooks::Model::Attachable
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,445 @@
1
+ module Quickbooks
2
+ module Service
3
+ class BaseService
4
+ include Quickbooks::Util::Logging
5
+ include ServiceCrud
6
+
7
+ attr_accessor :company_id
8
+ attr_accessor :oauth
9
+ attr_reader :base_uri
10
+ attr_reader :last_response_body
11
+ attr_reader :last_response_xml
12
+
13
+ XML_NS = %{xmlns="http://schema.intuit.com/finance/v3"}
14
+ HTTP_CONTENT_TYPE = 'application/xml'
15
+ HTTP_ACCEPT = 'application/xml'
16
+ HTTP_ACCEPT_ENCODING = 'gzip, deflate'
17
+ BASE_DOMAIN = 'quickbooks.api.intuit.com'
18
+ SANDBOX_DOMAIN = 'sandbox-quickbooks.api.intuit.com'
19
+
20
+ def initialize(attributes = {})
21
+ domain = Quickbooks.sandbox_mode ? SANDBOX_DOMAIN : BASE_DOMAIN
22
+ @base_uri = "https://#{domain}/v3/company"
23
+ attributes.each {|key, value| public_send("#{key}=", value) }
24
+ end
25
+
26
+ def access_token=(token)
27
+ @oauth = token
28
+ rebuild_connection!
29
+ end
30
+
31
+ def company_id=(company_id)
32
+ @company_id = company_id
33
+ end
34
+
35
+ # realm & company are synonymous
36
+ def realm_id=(company_id)
37
+ @company_id = company_id
38
+ end
39
+
40
+ def oauth_v1?
41
+ @oauth.is_a? OAuth::AccessToken
42
+ end
43
+
44
+ def oauth_v2?
45
+ @oauth.is_a? OAuth2::AccessToken
46
+ end
47
+
48
+ # [OAuth2] The default Faraday connection does not have gzip or multipart support.
49
+ # We need to reset the existing connection and build a new one.
50
+ def rebuild_connection!
51
+ return unless oauth_v2?
52
+ @oauth.client.connection = nil
53
+ @oauth.client.connection.build do |builder|
54
+ builder.use :gzip
55
+ builder.request :multipart
56
+ builder.request :url_encoded
57
+ builder.adapter :net_http
58
+ end
59
+ end
60
+
61
+ def url_for_resource(resource)
62
+ "#{url_for_base}/#{resource}"
63
+ end
64
+
65
+ def url_for_base
66
+ raise MissingRealmError.new unless @company_id
67
+ "#{@base_uri}/#{@company_id}"
68
+ end
69
+
70
+ def is_json?
71
+ self.class::HTTP_CONTENT_TYPE == "application/json"
72
+ end
73
+
74
+ def is_pdf?
75
+ self.class::HTTP_CONTENT_TYPE == "application/pdf"
76
+ end
77
+
78
+ def default_model_query
79
+ "SELECT * FROM #{self.class.name.split("::").last}"
80
+ end
81
+
82
+ def url_for_query(query = nil, start_position = 1, max_results = 20, options = {})
83
+ query ||= default_model_query
84
+ query = "#{query} STARTPOSITION #{start_position} MAXRESULTS #{max_results}"
85
+
86
+ "#{url_for_base}/query?query=#{CGI.escape(query)}"
87
+ end
88
+
89
+ private
90
+
91
+ def parse_xml(xml)
92
+ @last_response_xml = Nokogiri::XML(xml)
93
+ end
94
+
95
+ def valid_xml_document(xml)
96
+ %Q{<?xml version="1.0" encoding="utf-8"?>\n#{xml.strip}}
97
+ end
98
+
99
+ # A single object response is the same as a collection response except
100
+ # it just has a single main element
101
+ def fetch_object(model, url, params = {})
102
+ raise ArgumentError, "missing model to instantiate" if model.nil?
103
+ response = do_http_get(url, params)
104
+ collection = parse_collection(response, model)
105
+ if collection.is_a?(Quickbooks::Collection)
106
+ collection.entries.first
107
+ else
108
+ nil
109
+ end
110
+ end
111
+
112
+ def fetch_collection(query, model, options = {})
113
+ page = options.fetch(:page, 1)
114
+ per_page = options.fetch(:per_page, 20)
115
+
116
+ start_position = ((page - 1) * per_page) + 1 # page=2, per_page=10 then we want to start at 11
117
+ max_results = per_page
118
+
119
+ response = do_http_get(url_for_query(query, start_position, max_results))
120
+
121
+ parse_collection(response, model)
122
+ end
123
+
124
+ def parse_collection(response, model)
125
+ if response
126
+ collection = Quickbooks::Collection.new
127
+ xml = @last_response_xml
128
+ begin
129
+ results = []
130
+
131
+ query_response = xml.xpath("//xmlns:IntuitResponse/xmlns:QueryResponse")[0]
132
+ if query_response
133
+
134
+ start_pos_attr = query_response.attributes['startPosition']
135
+ if start_pos_attr
136
+ collection.start_position = start_pos_attr.value.to_i
137
+ end
138
+
139
+ max_results_attr = query_response.attributes['maxResults']
140
+ if max_results_attr
141
+ collection.max_results = max_results_attr.value.to_i
142
+ end
143
+
144
+ total_count_attr = query_response.attributes['totalCount']
145
+ if total_count_attr
146
+ collection.total_count = total_count_attr.value.to_i
147
+ end
148
+ end
149
+
150
+ path_to_nodes = "//xmlns:IntuitResponse//xmlns:#{model::XML_NODE}"
151
+ collection.count = xml.xpath(path_to_nodes).count
152
+ if collection.count > 0
153
+ xml.xpath(path_to_nodes).each do |xa|
154
+ results << model.from_xml(xa)
155
+ end
156
+ end
157
+
158
+ collection.entries = results
159
+ rescue => ex
160
+ raise Quickbooks::IntuitRequestException.new("Error parsing XML: #{ex.message}")
161
+ end
162
+ collection
163
+ else
164
+ nil
165
+ end
166
+ end
167
+
168
+ # Given an IntuitResponse which is expected to wrap a single
169
+ # Entity node, e.g.
170
+ # <IntuitResponse xmlns="http://schema.intuit.com/finance/v3" time="2013-11-16T10:26:42.762-08:00">
171
+ # <Customer domain="QBO" sparse="false">
172
+ # <Id>1</Id>
173
+ # ...
174
+ # </Customer>
175
+ # </IntuitResponse>
176
+ def parse_singular_entity_response(model, xml, node_xpath_prefix = nil)
177
+ xmldoc = Nokogiri(xml)
178
+ prefix = node_xpath_prefix || model::XML_NODE
179
+ xmldoc.xpath("//xmlns:IntuitResponse/xmlns:#{prefix}")[0]
180
+ end
181
+
182
+ # A successful delete request returns a XML packet like:
183
+ # <IntuitResponse xmlns="http://schema.intuit.com/finance/v3" time="2013-04-23T08:30:33.626-07:00">
184
+ # <Payment domain="QBO" status="Deleted">
185
+ # <Id>8748</Id>
186
+ # </Payment>
187
+ # </IntuitResponse>
188
+ def parse_singular_entity_response_for_delete(model, xml)
189
+ xmldoc = Nokogiri(xml)
190
+ xmldoc.xpath("//xmlns:IntuitResponse/xmlns:#{model::XML_NODE}[@status='Deleted']").length == 1
191
+ end
192
+
193
+ def do_http_post(url, body = "", params = {}, headers = {}) # throws IntuitRequestException
194
+ url = add_query_string_to_url(url, params)
195
+ do_http(:post, url, body, headers)
196
+ end
197
+
198
+ def do_http_get(url, params = {}, headers = {}) # throws IntuitRequestException
199
+ url = add_query_string_to_url(url, params)
200
+ do_http(:get, url, {}, headers)
201
+ end
202
+
203
+ def do_http_raw_get(url, params = {}, headers = {})
204
+ url = add_query_string_to_url(url, params)
205
+ unless headers.has_key?('Content-Type')
206
+ headers['Content-Type'] = self.class::HTTP_CONTENT_TYPE
207
+ end
208
+ unless headers.has_key?('Accept')
209
+ headers['Accept'] = self.class::HTTP_ACCEPT
210
+ end
211
+ unless headers.has_key?('Accept-Encoding')
212
+ headers['Accept-Encoding'] = HTTP_ACCEPT_ENCODING
213
+ end
214
+ raw_response = oauth_get(url, headers)
215
+ Quickbooks::Service::Responses::OAuthHttpResponse.wrap(raw_response)
216
+ end
217
+
218
+ def do_http_file_upload(uploadIO, url, metadata = nil)
219
+ headers = {
220
+ 'Content-Type' => 'multipart/form-data'
221
+ }
222
+ body = {}
223
+ body['file_content_0'] = uploadIO
224
+
225
+ if metadata
226
+ standalone_prefix = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
227
+ meta_data_xml = "#{standalone_prefix}\n#{metadata.to_xml_ns.to_s}"
228
+ param_part = UploadIO.new(StringIO.new(meta_data_xml), "application/xml")
229
+ body['file_metadata_0'] = param_part
230
+ end
231
+
232
+ do_http(:upload, url, body, headers)
233
+ end
234
+
235
+ def do_http(method, url, body, headers) # throws IntuitRequestException
236
+ if @oauth.nil?
237
+ raise "OAuth client has not been initialized. Initialize with setter access_token="
238
+ end
239
+ unless headers.has_key?('Content-Type')
240
+ headers['Content-Type'] = self.class::HTTP_CONTENT_TYPE
241
+ end
242
+ unless headers.has_key?('Accept')
243
+ headers['Accept'] = self.class::HTTP_ACCEPT
244
+ end
245
+ unless headers.has_key?('Accept-Encoding')
246
+ headers['Accept-Encoding'] = HTTP_ACCEPT_ENCODING
247
+ end
248
+
249
+ log "------ QUICKBOOKS-RUBY REQUEST ------"
250
+ log "METHOD = #{method}"
251
+ log "RESOURCE = #{url}"
252
+ log_request_body(body)
253
+ log "REQUEST HEADERS = #{headers.inspect}"
254
+
255
+ raw_response = case method
256
+ when :get
257
+ oauth_get(url, headers)
258
+ when :post
259
+ oauth_post(url, body, headers)
260
+ when :upload
261
+ oauth_post_with_multipart(url, body, headers)
262
+ else
263
+ raise "Do not know how to perform that HTTP operation"
264
+ end
265
+ response = Quickbooks::Service::Responses::OAuthHttpResponse.wrap(raw_response)
266
+ check_response(response, :request => body)
267
+ end
268
+
269
+ def oauth_get(url, headers)
270
+ if oauth_v1?
271
+ @oauth.get(url, headers)
272
+ elsif oauth_v2?
273
+ @oauth.get(url, headers: headers, raise_errors: false)
274
+ else
275
+ raise InvalidOauthAccessTokenObject.new(@oauth)
276
+ end
277
+ end
278
+
279
+ def oauth_post(url, body, headers)
280
+ if oauth_v1?
281
+ @oauth.post(url, body, headers)
282
+ elsif oauth_v2?
283
+ @oauth.post(url, headers: headers, body: body, raise_errors: false)
284
+ else
285
+ raise InvalidOauthAccessTokenObject.new(@oauth)
286
+ end
287
+ end
288
+
289
+ def oauth_post_with_multipart(url, body, headers)
290
+ raw_response = if oauth_v1?
291
+ oauth.post_with_multipart(url, body, headers)
292
+ elsif oauth_v2?
293
+ oauth.post_with_multipart(url, headers: headers, body: body, raise_errors: false)
294
+ else
295
+ raise InvalidOauthAccessTokenObject.new(@oauth)
296
+ end
297
+ response = Quickbooks::Service::Responses::OAuthHttpResponse.wrap(raw_response)
298
+ check_response(response, :request => body)
299
+ end
300
+
301
+ def add_query_string_to_url(url, params)
302
+ if params.is_a?(Hash) && !params.empty?
303
+ url + "?" + params.collect { |k| "#{k.first}=#{k.last}" }.join("&")
304
+ else
305
+ url
306
+ end
307
+ end
308
+
309
+ def check_response(response, options = {})
310
+ log "------ QUICKBOOKS-RUBY RESPONSE ------"
311
+ log "RESPONSE CODE = #{response.code}"
312
+ if response.respond_to?(:headers)
313
+ log "RESPONSE HEADERS = #{response.headers}"
314
+ end
315
+ log_response_body(response)
316
+ status = response.code.to_i
317
+ case status
318
+ when 200
319
+ # even HTTP 200 can contain an error, so we always have to peek for an Error
320
+ if response_is_error?
321
+ parse_and_raise_exception(options)
322
+ else
323
+ response
324
+ end
325
+ when 302
326
+ raise "Unhandled HTTP Redirect"
327
+ when 401
328
+ raise Quickbooks::AuthorizationFailure
329
+ when 403
330
+ message = parse_intuit_error[:message]
331
+ if message.include?('ThrottleExceeded')
332
+ raise Quickbooks::ThrottleExceeded, message
333
+ end
334
+ raise Quickbooks::Forbidden, message
335
+ when 404
336
+ raise Quickbooks::NotFound
337
+ when 413
338
+ raise Quickbooks::RequestTooLarge
339
+ when 400, 500
340
+ parse_and_raise_exception(options)
341
+ when 429
342
+ message = parse_intuit_error[:message]
343
+ raise Quickbooks::TooManyRequests, message
344
+ when 503, 504
345
+ raise Quickbooks::ServiceUnavailable
346
+ else
347
+ raise "HTTP Error Code: #{status}, Msg: #{response.plain_body}"
348
+ end
349
+ end
350
+
351
+ def log_response_body(response)
352
+ log "RESPONSE BODY:"
353
+ if is_json?
354
+ log ">>>>#{response.plain_body.inspect}"
355
+ parse_json(response.plain_body)
356
+ elsif is_pdf?
357
+ log("BODY is a PDF : not dumping")
358
+ else
359
+ log(log_xml(response.plain_body))
360
+ parse_xml(response.plain_body)
361
+ end
362
+ end
363
+
364
+ def log_request_body(body)
365
+ log "REQUEST BODY:"
366
+ if is_json?
367
+ log(body.inspect)
368
+ elsif is_pdf?
369
+ log("BODY is a PDF : not dumping")
370
+ else
371
+ #multipart request for uploads arrive here in a Hash with UploadIO vals
372
+ if body.is_a?(Hash)
373
+ body.each do |k,v|
374
+ log('BODY PART:')
375
+ val_content = v.inspect
376
+ if v.is_a?(UploadIO)
377
+ if v.content_type == 'application/xml'
378
+ if v.io.is_a?(StringIO)
379
+ val_content = log_xml(v.io.string)
380
+ end
381
+ end
382
+ end
383
+ log("#{k}: #{val_content}")
384
+ end
385
+ else
386
+ log(log_xml(body))
387
+ end
388
+ end
389
+ end
390
+
391
+ def parse_and_raise_exception(options = {})
392
+ err = parse_intuit_error
393
+ ex = Quickbooks::IntuitRequestException.new("#{err[:message]}:\n\t#{err[:detail]}")
394
+ ex.code = err[:code]
395
+ ex.detail = err[:detail]
396
+ ex.type = err[:type]
397
+ if is_json?
398
+ ex.request_json = options[:request]
399
+ else
400
+ ex.request_xml = options[:request]
401
+ end
402
+ raise ex
403
+ end
404
+
405
+ def response_is_error?
406
+ begin
407
+ @last_response_xml.xpath("//xmlns:IntuitResponse/xmlns:Fault")[0] != nil
408
+ rescue Nokogiri::XML::XPath::SyntaxError => exception
409
+ #puts @last_response_xml.to_xml.to_s
410
+ #puts "WTF: #{exception.inspect}:#{exception.backtrace.join("\n")}"
411
+ true
412
+ end
413
+ end
414
+
415
+ def parse_intuit_error
416
+ error = {:message => "", :detail => "", :type => nil, :code => 0}
417
+ fault = @last_response_xml.xpath("//xmlns:IntuitResponse/xmlns:Fault")[0]
418
+ if fault
419
+ error[:type] = fault.attributes['type'].value
420
+
421
+ error_element = fault.xpath("//xmlns:Error")[0]
422
+ if error_element
423
+ code_attr = error_element.attributes['code']
424
+ if code_attr
425
+ error[:code] = code_attr.value
426
+ end
427
+ element_attr = error_element.attributes['element']
428
+ if code_attr
429
+ error[:element] = code_attr.value
430
+ end
431
+ error[:message] = error_element.xpath("//xmlns:Message").text
432
+ error[:detail] = error_element.xpath("//xmlns:Detail").text
433
+ end
434
+ end
435
+
436
+ error
437
+ rescue Nokogiri::XML::XPath::SyntaxError => exception
438
+ error[:detail] = @last_response_xml.to_s
439
+
440
+ error
441
+ end
442
+
443
+ end
444
+ end
445
+ end
@@ -0,0 +1,39 @@
1
+ module Quickbooks
2
+ module Service
3
+ class BaseServiceJSON < BaseService
4
+ include ServiceCrudJSON
5
+ HTTP_CONTENT_TYPE = 'application/json'
6
+ HTTP_ACCEPT = 'application/json'
7
+ attr_reader :last_response_json
8
+
9
+ private
10
+
11
+ def parse_json(json)
12
+ @last_response_json = json
13
+ end
14
+
15
+ def response_is_error?
16
+ @last_response_json['Fault'].present?
17
+ end
18
+
19
+ def parse_intuit_error
20
+ error = {:message => "", :detail => "", :type => nil, :code => 0}
21
+ resp = JSON.parse(@last_response_json)
22
+ fault = resp['Fault']
23
+ if fault.present?
24
+ error[:type] = fault['type'] if fault.has_key?('type')
25
+ if fault_error = fault['Error'].first
26
+ error[:message] = fault_error['Message']
27
+ error[:detail] = fault_error['Detail']
28
+ error[:code] = fault_error['code']
29
+ end
30
+ end
31
+ error
32
+ rescue Exception => exception
33
+ error[:detail] = @last_response_json.to_s
34
+ error
35
+ end
36
+
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,11 @@
1
+ module Quickbooks
2
+ module Service
3
+ class Batch < BaseService
4
+
5
+ def make_request(entity, options = {})
6
+ response = do_http_post(url_for_resource('batch'), valid_xml_document(entity.to_xml_ns), options[:query])
7
+ Quickbooks::Model::BatchResponse.from_xml(response.plain_body)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,16 @@
1
+ module Quickbooks
2
+ module Service
3
+ class Bill < BaseService
4
+
5
+ def delete(bill)
6
+ delete_by_query_string(bill)
7
+ end
8
+
9
+ private
10
+
11
+ def model
12
+ Quickbooks::Model::Bill
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ module Quickbooks
2
+ module Service
3
+ class BillPayment < BaseService
4
+
5
+ def delete(bill_payment)
6
+ delete_by_query_string(bill_payment)
7
+ end
8
+
9
+ private
10
+
11
+ def model
12
+ Quickbooks::Model::BillPayment
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,12 @@
1
+ module Quickbooks
2
+ module Service
3
+ class Budget < BaseService
4
+
5
+ private
6
+
7
+ def model
8
+ Quickbooks::Model::Budget
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,24 @@
1
+ module Quickbooks
2
+ module Service
3
+ class ChangeDataCapture < BaseService
4
+
5
+ def url_for_query(entity_list, query=nil)
6
+ q = entity_list.join(",")
7
+ q = "#{q}&#{query}" if query.present?
8
+ return "#{url_for_base}/cdc?entities=#{q}"
9
+ end
10
+
11
+ def since(entity_list, timestamp)
12
+ do_http_get(url_for_query(entity_list, "changedSince=#{URI.encode_www_form_component(timestamp.iso8601)}"))
13
+ model.new(:xml => @last_response_xml)
14
+ end
15
+
16
+ private
17
+
18
+ def model
19
+ Quickbooks::Model::ChangeDataCapture
20
+ end
21
+
22
+ end
23
+ end
24
+ end