effective_qb_sync 1.0.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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +94 -0
- data/Rakefile +21 -0
- data/app/controllers/admin/qb_syncs_controller.rb +60 -0
- data/app/controllers/effective/qb_sync_controller.rb +40 -0
- data/app/models/effective/access_denied.rb +17 -0
- data/app/models/effective/datatables/qb_syncs.rb +30 -0
- data/app/models/effective/qb_log.rb +13 -0
- data/app/models/effective/qb_machine.rb +281 -0
- data/app/models/effective/qb_order_item.rb +13 -0
- data/app/models/effective/qb_order_items_form.rb +55 -0
- data/app/models/effective/qb_request.rb +262 -0
- data/app/models/effective/qb_ticket.rb +55 -0
- data/app/models/effective/qbwc_supervisor.rb +89 -0
- data/app/views/admin/qb_syncs/_actions.html.haml +2 -0
- data/app/views/admin/qb_syncs/_qb_item_names.html.haml +9 -0
- data/app/views/admin/qb_syncs/index.html.haml +24 -0
- data/app/views/admin/qb_syncs/instructions.html.haml +136 -0
- data/app/views/admin/qb_syncs/show.html.haml +52 -0
- data/app/views/effective/orders_mailer/qb_sync_error.html.haml +56 -0
- data/app/views/effective/qb_sync/authenticate.erb +12 -0
- data/app/views/effective/qb_sync/clientVersion.erb +8 -0
- data/app/views/effective/qb_sync/closeConnection.erb +8 -0
- data/app/views/effective/qb_sync/connectionError.erb +9 -0
- data/app/views/effective/qb_sync/getLastError.erb +9 -0
- data/app/views/effective/qb_sync/receiveResponseXML.erb +8 -0
- data/app/views/effective/qb_sync/sendRequestXML.erb +8 -0
- data/app/views/effective/qb_sync/serverVersion.erb +8 -0
- data/app/views/effective/qb_web_connector/quickbooks.qwc.erb +12 -0
- data/config/routes.rb +16 -0
- data/db/migrate/01_create_effective_qb_sync.rb.erb +68 -0
- data/lib/effective_qb_sync/engine.rb +42 -0
- data/lib/effective_qb_sync/version.rb +3 -0
- data/lib/effective_qb_sync.rb +42 -0
- data/lib/generators/effective_qb_sync/install_generator.rb +42 -0
- data/lib/generators/templates/effective_qb_sync.rb +61 -0
- data/lib/generators/templates/effective_qb_sync_mailer_preview.rb +39 -0
- data/spec/dummy/README.rdoc +8 -0
- data/spec/dummy/Rakefile +6 -0
- data/spec/dummy/app/assets/javascripts/application.js +13 -0
- data/spec/dummy/app/assets/stylesheets/application.css +15 -0
- data/spec/dummy/app/controllers/application_controller.rb +5 -0
- data/spec/dummy/app/helpers/application_helper.rb +2 -0
- data/spec/dummy/app/models/product.rb +14 -0
- data/spec/dummy/app/models/product_with_float_price.rb +13 -0
- data/spec/dummy/app/models/user.rb +14 -0
- data/spec/dummy/app/views/layouts/application.html.erb +14 -0
- data/spec/dummy/bin/bundle +3 -0
- data/spec/dummy/bin/rails +4 -0
- data/spec/dummy/bin/rake +4 -0
- data/spec/dummy/config/application.rb +32 -0
- data/spec/dummy/config/boot.rb +5 -0
- data/spec/dummy/config/database.yml +25 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/development.rb +37 -0
- data/spec/dummy/config/environments/production.rb +80 -0
- data/spec/dummy/config/environments/test.rb +36 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/cookies_serializer.rb +3 -0
- data/spec/dummy/config/initializers/devise.rb +254 -0
- data/spec/dummy/config/initializers/effective_addresses.rb +15 -0
- data/spec/dummy/config/initializers/effective_orders.rb +154 -0
- data/spec/dummy/config/initializers/effective_qb_sync.rb +41 -0
- data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/spec/dummy/config/initializers/inflections.rb +16 -0
- data/spec/dummy/config/initializers/mime_types.rb +4 -0
- data/spec/dummy/config/initializers/session_store.rb +3 -0
- data/spec/dummy/config/initializers/simple_form.rb +189 -0
- data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/spec/dummy/config/locales/en.yml +23 -0
- data/spec/dummy/config/routes.rb +3 -0
- data/spec/dummy/config/secrets.yml +22 -0
- data/spec/dummy/config.ru +4 -0
- data/spec/dummy/db/schema.rb +208 -0
- data/spec/dummy/db/test.sqlite3 +0 -0
- data/spec/dummy/log/development.log +90 -0
- data/spec/dummy/log/test.log +1 -0
- data/spec/dummy/public/404.html +67 -0
- data/spec/dummy/public/422.html +67 -0
- data/spec/dummy/public/500.html +66 -0
- data/spec/dummy/public/favicon.ico +0 -0
- data/spec/fixtures/qbxml_response_error.xml +6 -0
- data/spec/fixtures/qbxml_response_success.xml +621 -0
- data/spec/models/acts_as_purchasable_spec.rb +131 -0
- data/spec/models/factories_spec.rb +32 -0
- data/spec/models/qb_machine_spec.rb +554 -0
- data/spec/models/qb_request_spec.rb +327 -0
- data/spec/models/qb_ticket_spec.rb +62 -0
- data/spec/spec_helper.rb +45 -0
- data/spec/support/factories.rb +97 -0
- metadata +397 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# This is a form object for the admin_qb_syncs#update action
|
|
2
|
+
|
|
3
|
+
module Effective
|
|
4
|
+
class QbOrderItemsForm
|
|
5
|
+
include ActiveModel::Model
|
|
6
|
+
|
|
7
|
+
attr_accessor :id, :orders
|
|
8
|
+
|
|
9
|
+
def initialize(id:, orders:)
|
|
10
|
+
@id = id
|
|
11
|
+
@orders = Array(orders)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def qb_order_items
|
|
15
|
+
@qb_order_items ||= orders.flat_map { |order| order.order_items.map { |oi| oi.qb_item_name; oi.qb_order_item } }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# This is required by SimpleForm and Rails for non-ActiveRecord nested attributes
|
|
19
|
+
def qb_order_items_attributes=(qb_order_item_atts)
|
|
20
|
+
qb_order_item_atts.each do |attributes|
|
|
21
|
+
qb_order_item = qb_order_items.find { |qb_order_item| qb_order_item.order_item_id.to_s == attributes[:order_item_id] }
|
|
22
|
+
raise "unable to find qb_order_item with order_item_id #{attributes[:order_item_id]}" unless qb_order_item.present?
|
|
23
|
+
|
|
24
|
+
qb_order_item.attributes = attributes.except(:id, :order_item_id)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def save
|
|
29
|
+
qb_order_items.each { |qb_order_item| qb_order_item.valid? }
|
|
30
|
+
return false unless qb_order_items.all? { |qb_order_item| qb_order_item.valid? }
|
|
31
|
+
|
|
32
|
+
success = false
|
|
33
|
+
|
|
34
|
+
Effective::QbOrderItem.transaction do
|
|
35
|
+
begin
|
|
36
|
+
qb_order_items.each { |qb_order_item| qb_order_item.save! }
|
|
37
|
+
success = true
|
|
38
|
+
rescue => e
|
|
39
|
+
raise ActiveRecord::Rollback
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
success
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def to_param
|
|
47
|
+
id
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def persisted?
|
|
51
|
+
true
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
module Effective
|
|
2
|
+
class QbRequest < ActiveRecord::Base
|
|
3
|
+
belongs_to :qb_ticket
|
|
4
|
+
belongs_to :order
|
|
5
|
+
|
|
6
|
+
# NOTE: Anything that is 'raise' here finds its way to qb_ticket#error!
|
|
7
|
+
|
|
8
|
+
# these are the states that signal a request is finished
|
|
9
|
+
COMPLETED_STATES = ['Finished', 'Error']
|
|
10
|
+
PROCESSING_STATES = ['Processing', 'CustomerQuery', 'CreateCustomer', 'OrderSync']
|
|
11
|
+
|
|
12
|
+
# structure do
|
|
13
|
+
# request_qbxml :text
|
|
14
|
+
# response_qbxml :text
|
|
15
|
+
|
|
16
|
+
# request_sent_at :datetime
|
|
17
|
+
# response_received_at :datetime
|
|
18
|
+
|
|
19
|
+
# state :string, :validates => [:presence, :inclusion => { :in => COMPLETED_STATES + PROCESSING_STATES}]
|
|
20
|
+
# error :text
|
|
21
|
+
|
|
22
|
+
# site_id :integer # ActsAsSiteSpecific
|
|
23
|
+
|
|
24
|
+
# timestamps
|
|
25
|
+
# end
|
|
26
|
+
|
|
27
|
+
validates :state, inclusion: { in: COMPLETED_STATES + PROCESSING_STATES }
|
|
28
|
+
validates :qb_ticket, presence: true
|
|
29
|
+
validates :order, presence: true
|
|
30
|
+
|
|
31
|
+
# creates (does not persist) QbRequests for outstanding orders. The caller may choose to
|
|
32
|
+
# persist a request when that request starts communicating with QuickBooks
|
|
33
|
+
def self.new_requests_for_unsynced_items
|
|
34
|
+
finished_order_ids = Effective::QbRequest.where(state: 'Finished').pluck(:order_id)
|
|
35
|
+
Effective::Order.purchased.includes(order_items: [:purchasable, :qb_order_item])
|
|
36
|
+
.where.not(id: finished_order_ids).map { |order| Effective::QbRequest.new(order: order) }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Finds a QbRequest using response qb_xml. If the response could not be parsed, or if there was no
|
|
40
|
+
# corresponding record, nil will be returned.
|
|
41
|
+
def self.find_using_response_qbxml(xml)
|
|
42
|
+
return nil if xml.blank?
|
|
43
|
+
element = Effective::QbRequest.find_first_response_having_a_request_id(xml)
|
|
44
|
+
|
|
45
|
+
Effective::QbRequest.find_by_id(element.attr('requestID').to_i) if element
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def has_more_work?
|
|
49
|
+
PROCESSING_STATES.include?(state)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def state
|
|
53
|
+
self[:state] || 'Processing'
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# searches the XML and returns the first element having a requestID attribute. Since the
|
|
57
|
+
# application does not bundle requests (yet), this should work.
|
|
58
|
+
def self.find_first_response_having_a_request_id(xml)
|
|
59
|
+
doc = Nokogiri::XML(xml)
|
|
60
|
+
doc.xpath('//*[@requestID]')[0]
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# parses the response XML and processes it.
|
|
64
|
+
# returns true if the responseXML indicates success, false otherwise
|
|
65
|
+
def consume_response_xml(xml)
|
|
66
|
+
update_attributes!(response_qbxml: xml)
|
|
67
|
+
handle_response_xml(xml)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# handle response xml
|
|
71
|
+
def handle_response_xml(xml)
|
|
72
|
+
case state
|
|
73
|
+
when 'CustomerQuery'
|
|
74
|
+
handle_customer_query_response_xml(xml)
|
|
75
|
+
when 'CreateCustomer'
|
|
76
|
+
handle_create_customer_response_xml(xml)
|
|
77
|
+
when 'OrderSync'
|
|
78
|
+
handle_order_sync_response_xml(xml)
|
|
79
|
+
else
|
|
80
|
+
raise "Request in state #{state} was not expecting a response from the server"
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# outputs this request in qb_xml_format
|
|
85
|
+
def to_qb_xml
|
|
86
|
+
if state == 'Processing'
|
|
87
|
+
# this is a dummy state -- we need to transition to the CustomerQuery state before any XML goes out.
|
|
88
|
+
transition_state 'CustomerQuery'
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
xml = generate_request_xml
|
|
92
|
+
wrap_qbxml_request(xml)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# generates the actual request XML that will be wrapped in a qbxml_request
|
|
96
|
+
def generate_request_xml
|
|
97
|
+
# safety checks to make sure we are linked in to the order
|
|
98
|
+
raise 'Missing Order' unless order
|
|
99
|
+
|
|
100
|
+
if order.order_items.any? { |order_item| order_item.qb_item_name.blank? }
|
|
101
|
+
raise 'expected .qb_item_name() to be present on Effective::OrderItem'
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
case self.state
|
|
105
|
+
when 'CustomerQuery'
|
|
106
|
+
generate_customer_query_request_xml
|
|
107
|
+
when 'OrderSync'
|
|
108
|
+
generate_order_sync_request_xml
|
|
109
|
+
when 'CreateCustomer'
|
|
110
|
+
generate_create_customer_request_xml
|
|
111
|
+
else
|
|
112
|
+
raise "Unsupported state for generating request XML: #{state}"
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# transitions the request state and also outputs a log statement
|
|
117
|
+
def transition_state(state)
|
|
118
|
+
old_state = self.state
|
|
119
|
+
update_attributes!(state: state)
|
|
120
|
+
log "Transitioned request state from [#{old_state}] to [#{state}]"
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def transition_to_finished
|
|
124
|
+
# We create one QbOrderItem for each OrderItem here.
|
|
125
|
+
order.order_items.each do |order_item|
|
|
126
|
+
order_item.qb_item_name
|
|
127
|
+
order_item.qb_order_item.save
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
transition_state('Finished')
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# This should be private too, but test needs it
|
|
134
|
+
def handle_create_customer_response_xml(xml)
|
|
135
|
+
queryResponse = Nokogiri::XML(xml).xpath('//CustomerAddRs').first['statusCode']
|
|
136
|
+
statusMessage = Nokogiri::XML(xml).xpath('//CustomerAddRs').first['statusMessage']
|
|
137
|
+
|
|
138
|
+
if '0' == queryResponse
|
|
139
|
+
# the customer was created
|
|
140
|
+
log "Customer #{order.billing_name} created successfully"
|
|
141
|
+
transition_state 'OrderSync'
|
|
142
|
+
else
|
|
143
|
+
raise "[Order ##{order.id}] Customer #{order.billing_name} could not be created in QuickBooks: #{statusMessage}"
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
true # indicate success
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
private
|
|
150
|
+
|
|
151
|
+
# ensures that the total amount includes two decimal places
|
|
152
|
+
def qb_amount(amount)
|
|
153
|
+
raise 'amount should be an Integer representing the price in number of cents' unless amount.kind_of?(Integer)
|
|
154
|
+
sprintf('%0.2f', (amount / 100.0))
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def truncate(str, max_chars)
|
|
158
|
+
str[(0...max_chars)] rescue ''
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def generate_create_customer_request_xml
|
|
162
|
+
Nokogiri::XML::Builder.new do |xml|
|
|
163
|
+
xml.CustomerAddRq(requestID: id) {
|
|
164
|
+
xml.CustomerAdd {
|
|
165
|
+
xml.Name(truncate(order.billing_name, 41))
|
|
166
|
+
xml.FirstName(truncate(order.billing_name.split(' ').first, 25))
|
|
167
|
+
xml.LastName(truncate(order.billing_name.split(' ')[1..-1].join(' '), 25))
|
|
168
|
+
xml.BillAddress {
|
|
169
|
+
xml.Addr1(truncate(order.billing_name, 41))
|
|
170
|
+
xml.Addr2(truncate(order.billing_address.address1, 41))
|
|
171
|
+
xml.Addr3(truncate(order.billing_address.address2, 41))
|
|
172
|
+
xml.City(truncate(order.billing_address.city, 31))
|
|
173
|
+
xml.PostalCode(truncate(order.billing_address.postal_code, 13))
|
|
174
|
+
}
|
|
175
|
+
xml.Phone(truncate((order.user.try(:phone) || order.user.try(:cell_phone)), 21))
|
|
176
|
+
xml.Email(truncate(order.user.try(:email), 1023))
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
end.doc.root.to_s
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def generate_customer_query_request_xml
|
|
183
|
+
Nokogiri::XML::Builder.new do |xml|
|
|
184
|
+
xml.CustomerQueryRq(requestID: id) {
|
|
185
|
+
xml.FullName(truncate(order.billing_name, 209))
|
|
186
|
+
}
|
|
187
|
+
end.doc.root.to_s
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def handle_customer_query_response_xml(xml)
|
|
191
|
+
queryResponse = Nokogiri::XML(xml).xpath('//CustomerQueryRs').first['statusCode']
|
|
192
|
+
|
|
193
|
+
if '500' == queryResponse # the user was not found.
|
|
194
|
+
log "Customer #{order.billing_name} was not found"
|
|
195
|
+
transition_state 'CreateCustomer'
|
|
196
|
+
else # the user was found
|
|
197
|
+
log "Customer #{order.billing_name} exists"
|
|
198
|
+
transition_state 'OrderSync'
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
true # indicate success
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# delegates logging to the ticket
|
|
205
|
+
def log(message)
|
|
206
|
+
qb_ticket.log("Request: #{message}") if qb_ticket
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def generate_order_sync_request_xml
|
|
210
|
+
Nokogiri::XML::Builder.new do |xml|
|
|
211
|
+
xml.SalesReceiptAddRq(requestID: id) {
|
|
212
|
+
xml.SalesReceiptAdd {
|
|
213
|
+
xml.CustomerRef { xml.FullName(truncate(order.billing_name, 209)) }
|
|
214
|
+
xml.TxnDate(order.purchased_at.strftime("%Y-%m-%d"))
|
|
215
|
+
xml.Memo("Order ##{order.to_param} from website")
|
|
216
|
+
xml.IsToBePrinted('false')
|
|
217
|
+
xml.IsToBeEmailed('false')
|
|
218
|
+
|
|
219
|
+
order.order_items.each do |order_item|
|
|
220
|
+
xml.SalesReceiptLineAdd {
|
|
221
|
+
xml.ItemRef { xml.FullName(order_item.qb_item_name) }
|
|
222
|
+
xml.Desc(order_item.title)
|
|
223
|
+
xml.Amount(qb_amount(order_item.subtotal))
|
|
224
|
+
}
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
if EffectiveQbSync.quickbooks_tax_name.present?
|
|
228
|
+
xml.SalesReceiptLineAdd {
|
|
229
|
+
xml.ItemRef { xml.FullName(EffectiveQbSync.quickbooks_tax_name) }
|
|
230
|
+
xml.Desc(EffectiveQbSync.quickbooks_tax_name)
|
|
231
|
+
xml.Amount(qb_amount(order.tax))
|
|
232
|
+
}
|
|
233
|
+
end
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
end.doc.root.to_s
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def handle_order_sync_response_xml(xml)
|
|
240
|
+
queryResponse = Nokogiri::XML(xml).xpath('//SalesReceiptAddRs').first['statusCode']
|
|
241
|
+
statusMessage = Nokogiri::XML(xml).xpath('//SalesReceiptAddRs').first['statusMessage']
|
|
242
|
+
|
|
243
|
+
if '0' == queryResponse
|
|
244
|
+
log "Order #{order.to_param} successfully syncronized"
|
|
245
|
+
transition_to_finished
|
|
246
|
+
elsif '3180' == queryResponse
|
|
247
|
+
log "Order #{order.to_param} was not recorded by quickbooks because it was an empty transaction"
|
|
248
|
+
transition_to_finished
|
|
249
|
+
else
|
|
250
|
+
raise "[Order ##{order.to_param}] could not be synchronized with QuickBooks: #{statusMessage}"
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
true # indicate success
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# Simple wrapping helper
|
|
257
|
+
|
|
258
|
+
def wrap_qbxml_request(body)
|
|
259
|
+
'<?xml version="1.0" ?><?qbxml version="6.0" ?><QBXML><QBXMLMsgsRq onError="continueOnError">' + (body || '') + '</QBXMLMsgsRq></QBXML>'
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
module Effective
|
|
2
|
+
class QbTicket < ActiveRecord::Base
|
|
3
|
+
belongs_to :qb_request # the current request
|
|
4
|
+
has_many :qb_requests
|
|
5
|
+
has_many :orders, through: :qb_requests
|
|
6
|
+
has_many :qb_logs
|
|
7
|
+
|
|
8
|
+
STATES = ['Ready', 'Authenticated', 'Processing', 'Finished', 'ConnectionError', 'RequestError']
|
|
9
|
+
|
|
10
|
+
# structure do
|
|
11
|
+
# username :string
|
|
12
|
+
# company_file_name :string
|
|
13
|
+
# country :string
|
|
14
|
+
|
|
15
|
+
# qbxml_major_version :string
|
|
16
|
+
# qbxml_minor_version :string
|
|
17
|
+
|
|
18
|
+
# state :string, :default => 'Ready', :validates => [:presence, :inclusion => { :in => STATES}]
|
|
19
|
+
# percent :integer, :default => 0
|
|
20
|
+
|
|
21
|
+
# hpc_response :text
|
|
22
|
+
# connection_error_hresult :text
|
|
23
|
+
# connection_error_message :text
|
|
24
|
+
# last_error :text
|
|
25
|
+
|
|
26
|
+
# site_id :integer # ActsAsSiteSpecific
|
|
27
|
+
|
|
28
|
+
# timestamps
|
|
29
|
+
# end
|
|
30
|
+
|
|
31
|
+
validates :state, inclusion: { in: STATES }
|
|
32
|
+
|
|
33
|
+
def request_error!(error, atts={})
|
|
34
|
+
self.error!(error, atts.reverse_merge({state: 'RequestError'}))
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# This is the entry point for a standard error.
|
|
38
|
+
def error!(error, atts={})
|
|
39
|
+
Effective::OrdersMailer.order_error(
|
|
40
|
+
order: qb_request.try(:order),
|
|
41
|
+
error: error,
|
|
42
|
+
to: EffectiveQbSync.error_email,
|
|
43
|
+
subject: "Quickbooks failed to synchronize order ##{qb_request.try(:order).try(:to_param) || 'unknown'}",
|
|
44
|
+
template: 'qb_sync_error'
|
|
45
|
+
).try(:deliver_now).try(:deliver)
|
|
46
|
+
|
|
47
|
+
self.update_attributes!(atts.reverse_merge({last_error: error}))
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# persists a new log message to this ticket
|
|
51
|
+
def log(message)
|
|
52
|
+
qb_logs.create(message: message, qb_ticket: self)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
module Effective
|
|
2
|
+
class QbwcSupervisor
|
|
3
|
+
QBXML = 'http://developer.intuit.com/'
|
|
4
|
+
|
|
5
|
+
def authenticate(doc)
|
|
6
|
+
username = doc.at_xpath('//qbxml:strUserName', 'qbxml' => QBXML).content
|
|
7
|
+
password = doc.at_xpath('//qbxml:strPassword', 'qbxml' => QBXML).content
|
|
8
|
+
|
|
9
|
+
attempt do |m|
|
|
10
|
+
return [m.ticket.id.to_s, m.op_authenticate(username, password)]
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def sendRequestXML(doc)
|
|
15
|
+
ticket = doc.at_xpath('//qbxml:ticket', 'qbxml' => QBXML).content
|
|
16
|
+
strHCPResponse = doc.at_xpath('//qbxml:strHCPResponse', 'qbxml' => QBXML).content
|
|
17
|
+
strCompanyFileName = doc.at_xpath('//qbxml:strCompanyFileName', 'qbxml' => QBXML).content
|
|
18
|
+
qbXMLCountry = doc.at_xpath('//qbxml:qbXMLCountry', 'qbxml' => QBXML).content
|
|
19
|
+
qbXMLMajorVers = doc.at_xpath('//qbxml:qbXMLMajorVers', 'qbxml' => QBXML).content
|
|
20
|
+
qbXMLMinorVers = doc.at_xpath('//qbxml:qbXMLMinorVers', 'qbxml' => QBXML).content
|
|
21
|
+
|
|
22
|
+
params = {
|
|
23
|
+
ticket: ticket,
|
|
24
|
+
hcpresponse: strHCPResponse,
|
|
25
|
+
company: strCompanyFileName,
|
|
26
|
+
country: qbXMLCountry,
|
|
27
|
+
major_ver: qbXMLMajorVers,
|
|
28
|
+
minor_ver: qbXMLMinorVers
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
attempt(ticket) do |m|
|
|
32
|
+
return m.op_send_request_xml(params)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def receiveResponseXML(doc)
|
|
37
|
+
ticket = doc.at_xpath('//qbxml:ticket', 'qbxml' => QBXML).content
|
|
38
|
+
response = doc.at_xpath('//qbxml:response', 'qbxml' => QBXML).content
|
|
39
|
+
hresult = doc.at_xpath('//qbxml:hresult', 'qbxml' => QBXML).content
|
|
40
|
+
message = doc.at_xpath('//qbxml:message', 'qbxml' => QBXML).content
|
|
41
|
+
|
|
42
|
+
params = { ticket: ticket, response: response, hresult: hresult, message: message }
|
|
43
|
+
|
|
44
|
+
attempt(ticket) do |m|
|
|
45
|
+
return m.op_receive_response_xml(params)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def connectionError(doc)
|
|
50
|
+
ticket = doc.at_xpath('//qbxml:ticket', 'qbxml' => QBXML).content
|
|
51
|
+
hresult = doc.at_xpath('//qbxml:hresult', 'qbxml' => QBXML).content
|
|
52
|
+
message = doc.at_xpath('//qbxml:message', 'qbxml' => QBXML).content
|
|
53
|
+
|
|
54
|
+
attempt(ticket) do |m|
|
|
55
|
+
return m.op_connection_error(hresult, message)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def closeConnection(doc)
|
|
60
|
+
ticket = doc.at_xpath('//qbxml:ticket', 'qbxml' => QBXML).content
|
|
61
|
+
|
|
62
|
+
attempt(ticket) do |m|
|
|
63
|
+
return m.op_close_connection
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def getLastError(doc)
|
|
68
|
+
ticket = doc.at_xpath('//qbxml:ticket', 'qbxml' => QBXML).content
|
|
69
|
+
|
|
70
|
+
attempt(ticket) do |m|
|
|
71
|
+
return m.op_last_error
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
# executes an operation on a machine safely, recording the failure if one occurs
|
|
78
|
+
def attempt(ticket=nil)
|
|
79
|
+
@qb_machine = Effective::QbMachine.new(ticket)
|
|
80
|
+
begin
|
|
81
|
+
return yield(@qb_machine)
|
|
82
|
+
rescue
|
|
83
|
+
@qb_machine.fail_unexpectedly($!)
|
|
84
|
+
end
|
|
85
|
+
false
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
end
|
|
89
|
+
end
|