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.
Files changed (92) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +94 -0
  4. data/Rakefile +21 -0
  5. data/app/controllers/admin/qb_syncs_controller.rb +60 -0
  6. data/app/controllers/effective/qb_sync_controller.rb +40 -0
  7. data/app/models/effective/access_denied.rb +17 -0
  8. data/app/models/effective/datatables/qb_syncs.rb +30 -0
  9. data/app/models/effective/qb_log.rb +13 -0
  10. data/app/models/effective/qb_machine.rb +281 -0
  11. data/app/models/effective/qb_order_item.rb +13 -0
  12. data/app/models/effective/qb_order_items_form.rb +55 -0
  13. data/app/models/effective/qb_request.rb +262 -0
  14. data/app/models/effective/qb_ticket.rb +55 -0
  15. data/app/models/effective/qbwc_supervisor.rb +89 -0
  16. data/app/views/admin/qb_syncs/_actions.html.haml +2 -0
  17. data/app/views/admin/qb_syncs/_qb_item_names.html.haml +9 -0
  18. data/app/views/admin/qb_syncs/index.html.haml +24 -0
  19. data/app/views/admin/qb_syncs/instructions.html.haml +136 -0
  20. data/app/views/admin/qb_syncs/show.html.haml +52 -0
  21. data/app/views/effective/orders_mailer/qb_sync_error.html.haml +56 -0
  22. data/app/views/effective/qb_sync/authenticate.erb +12 -0
  23. data/app/views/effective/qb_sync/clientVersion.erb +8 -0
  24. data/app/views/effective/qb_sync/closeConnection.erb +8 -0
  25. data/app/views/effective/qb_sync/connectionError.erb +9 -0
  26. data/app/views/effective/qb_sync/getLastError.erb +9 -0
  27. data/app/views/effective/qb_sync/receiveResponseXML.erb +8 -0
  28. data/app/views/effective/qb_sync/sendRequestXML.erb +8 -0
  29. data/app/views/effective/qb_sync/serverVersion.erb +8 -0
  30. data/app/views/effective/qb_web_connector/quickbooks.qwc.erb +12 -0
  31. data/config/routes.rb +16 -0
  32. data/db/migrate/01_create_effective_qb_sync.rb.erb +68 -0
  33. data/lib/effective_qb_sync/engine.rb +42 -0
  34. data/lib/effective_qb_sync/version.rb +3 -0
  35. data/lib/effective_qb_sync.rb +42 -0
  36. data/lib/generators/effective_qb_sync/install_generator.rb +42 -0
  37. data/lib/generators/templates/effective_qb_sync.rb +61 -0
  38. data/lib/generators/templates/effective_qb_sync_mailer_preview.rb +39 -0
  39. data/spec/dummy/README.rdoc +8 -0
  40. data/spec/dummy/Rakefile +6 -0
  41. data/spec/dummy/app/assets/javascripts/application.js +13 -0
  42. data/spec/dummy/app/assets/stylesheets/application.css +15 -0
  43. data/spec/dummy/app/controllers/application_controller.rb +5 -0
  44. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  45. data/spec/dummy/app/models/product.rb +14 -0
  46. data/spec/dummy/app/models/product_with_float_price.rb +13 -0
  47. data/spec/dummy/app/models/user.rb +14 -0
  48. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  49. data/spec/dummy/bin/bundle +3 -0
  50. data/spec/dummy/bin/rails +4 -0
  51. data/spec/dummy/bin/rake +4 -0
  52. data/spec/dummy/config/application.rb +32 -0
  53. data/spec/dummy/config/boot.rb +5 -0
  54. data/spec/dummy/config/database.yml +25 -0
  55. data/spec/dummy/config/environment.rb +5 -0
  56. data/spec/dummy/config/environments/development.rb +37 -0
  57. data/spec/dummy/config/environments/production.rb +80 -0
  58. data/spec/dummy/config/environments/test.rb +36 -0
  59. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  60. data/spec/dummy/config/initializers/cookies_serializer.rb +3 -0
  61. data/spec/dummy/config/initializers/devise.rb +254 -0
  62. data/spec/dummy/config/initializers/effective_addresses.rb +15 -0
  63. data/spec/dummy/config/initializers/effective_orders.rb +154 -0
  64. data/spec/dummy/config/initializers/effective_qb_sync.rb +41 -0
  65. data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  66. data/spec/dummy/config/initializers/inflections.rb +16 -0
  67. data/spec/dummy/config/initializers/mime_types.rb +4 -0
  68. data/spec/dummy/config/initializers/session_store.rb +3 -0
  69. data/spec/dummy/config/initializers/simple_form.rb +189 -0
  70. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  71. data/spec/dummy/config/locales/en.yml +23 -0
  72. data/spec/dummy/config/routes.rb +3 -0
  73. data/spec/dummy/config/secrets.yml +22 -0
  74. data/spec/dummy/config.ru +4 -0
  75. data/spec/dummy/db/schema.rb +208 -0
  76. data/spec/dummy/db/test.sqlite3 +0 -0
  77. data/spec/dummy/log/development.log +90 -0
  78. data/spec/dummy/log/test.log +1 -0
  79. data/spec/dummy/public/404.html +67 -0
  80. data/spec/dummy/public/422.html +67 -0
  81. data/spec/dummy/public/500.html +66 -0
  82. data/spec/dummy/public/favicon.ico +0 -0
  83. data/spec/fixtures/qbxml_response_error.xml +6 -0
  84. data/spec/fixtures/qbxml_response_success.xml +621 -0
  85. data/spec/models/acts_as_purchasable_spec.rb +131 -0
  86. data/spec/models/factories_spec.rb +32 -0
  87. data/spec/models/qb_machine_spec.rb +554 -0
  88. data/spec/models/qb_request_spec.rb +327 -0
  89. data/spec/models/qb_ticket_spec.rb +62 -0
  90. data/spec/spec_helper.rb +45 -0
  91. data/spec/support/factories.rb +97 -0
  92. metadata +397 -0
@@ -0,0 +1,13 @@
1
+ module Effective
2
+ class QbOrderItem < ActiveRecord::Base
3
+ belongs_to :order_item
4
+
5
+ # structure do
6
+ # name :string
7
+ # timestamps
8
+ # end
9
+
10
+ validates :order_item, presence: true
11
+ validates :name, presence: true
12
+ end
13
+ end
@@ -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