effective_qb_sync 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: ad8ca346bd7681f4e298eab0a873b9d18381c5a8
4
+ data.tar.gz: 052fda34af7a8a95fd6a578ea588bf3adc32acd7
5
+ SHA512:
6
+ metadata.gz: 2bf69833eb4ed99ac58b87470c693bd6435f83551e316e4fc0913507fe7ae0718f3346f6bf4ef8b13cf1f3eaa659b7e35f1ad4eb3e7126e8a17584f9d72aa385
7
+ data.tar.gz: b700597415767d5266091bf45dffbc7f5aa487ab3665994d068e583140cdd2a9702963d7fbf5523c7db7b6ba85b1c08e078946aeacb20b7fd558df59d58fa667
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2016 Code and Effect Inc.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,94 @@
1
+ # Effective Quickbooks Sync
2
+
3
+ Synchronize [effective_orders](https://github.com/code-and-effect/effective_orders) with [Quickbooks Web Connector](https://developer.intuit.com/docs/quickbooks_web_connector).
4
+
5
+ ## Getting Started
6
+
7
+ Make sure [effective_orders](https://github.com/code-and-effect/effective_orders) is installed.
8
+
9
+ Ensure all your `acts_as_purchasable` objects respond to `qb_item_name`.
10
+
11
+
12
+ Add to your Gemfile:
13
+
14
+ ```ruby
15
+ gem 'effective_qb_sync'
16
+ ```
17
+
18
+ Run the bundle command to install it:
19
+
20
+ ```console
21
+ bundle install
22
+ ```
23
+
24
+ Then run the generator:
25
+
26
+ ```ruby
27
+ rails generate effective_qb_sync:install
28
+ ```
29
+
30
+ The generator will install an initializer which describes all configuration options and creates a database migration.
31
+
32
+ If you want to tweak the table name (to use something other than the default 'qb_requests', 'qb_tickets', 'qb_logs' and 'qb_order_items'), manually adjust both the configuration file and the migration now.
33
+
34
+ Then migrate the database:
35
+
36
+ ```ruby
37
+ rake db:migrate
38
+ ```
39
+
40
+ ### Admin Screen
41
+
42
+ To use the Admin screen, please also install the effective_datatables gem:
43
+
44
+ ```ruby
45
+ gem 'effective_datatables'
46
+ ```
47
+
48
+ Then visit:
49
+
50
+ ```ruby
51
+ link_to 'Quickbooks', effective_qb_sync.admin_qb_syncs_path # /admin/qb_syncs
52
+ ```
53
+
54
+ ### Permissions
55
+
56
+ The Quickbooks synchronization controller does not require a logged in user, or any other rails permissions.
57
+
58
+ The sync itself checks that the Quickbooks user performing the sync has a username/password that matches the values in the config/initializers/effective_qb_sync.rb file.
59
+
60
+ For the admin screen, a logged in user is required (devise) and the user should be able to (CanCan):
61
+
62
+ ```ruby
63
+ can :admin, :effective_qb_sync
64
+ ```
65
+
66
+ Devise is a required dependency, but CanCan is not. Please see the authorization_method in the initializer.
67
+
68
+ ## Setting up Quickbooks
69
+
70
+ Once the gem is installed, visit the /admin/qb_syncs/instructions page for setup instructions.
71
+
72
+ ## License
73
+
74
+ MIT License. Copyright [Code and Effect Inc.](http://www.codeandeffect.com/)
75
+
76
+ Code and Effect is the product arm of [AgileStyle](http://www.agilestyle.com/), an Edmonton-based shop that specializes in building custom web applications with Ruby on Rails.
77
+
78
+ ## Testing
79
+
80
+ Run tests by:
81
+
82
+ ```ruby
83
+ rspec
84
+ ```
85
+
86
+ ## Contributing
87
+
88
+ 1. Fork it
89
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
90
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
91
+ 4. Push to the branch (`git push origin my-new-feature`)
92
+ 5. Bonus points for test coverage
93
+ 6. Create new Pull Request
94
+
data/Rakefile ADDED
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env rake
2
+ begin
3
+ require 'bundler/setup'
4
+ rescue LoadError
5
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
6
+ end
7
+
8
+ # Testing tasks
9
+ APP_RAKEFILE = File.expand_path("../spec/dummy/Rakefile", __FILE__)
10
+ load 'rails/tasks/engine.rake'
11
+
12
+ require "bundler/vendored_thor"
13
+ Bundler::GemHelper.install_tasks
14
+
15
+ require 'rspec/core'
16
+ require 'rspec/core/rake_task'
17
+
18
+ desc "Run all specs in spec directory (excluding plugin specs)"
19
+ RSpec::Core::RakeTask.new(:spec => 'app:db:test:prepare')
20
+
21
+ task :default => :spec
@@ -0,0 +1,60 @@
1
+ module Admin
2
+ class QbSyncsController < ApplicationController
3
+ before_filter :authenticate_user! # This is devise, ensure we're logged in.
4
+ before_filter :restrict_access
5
+
6
+ layout (EffectiveQbSync.layout.kind_of?(Hash) ? EffectiveQbSync.layout[:admin_qb_tickets] : EffectiveQbSync.layout)
7
+
8
+ def index
9
+ @datatable = Effective::Datatables::QbSyncs.new() if defined?(EffectiveDatatables)
10
+ @page_title = 'Quickbooks Synchronizations'
11
+ end
12
+
13
+ def show
14
+ @qb_ticket = Effective::QbTicket.includes(:qb_requests, :qb_logs).find(params[:id])
15
+ @page_title = "Quickbooks Sync ##{@qb_ticket.id}"
16
+
17
+ @qb_order_items_form = Effective::QbOrderItemsForm.new(id: @qb_ticket.id, orders: @qb_ticket.orders)
18
+ end
19
+
20
+ def update
21
+ @qb_ticket = Effective::QbTicket.includes(:qb_requests, :qb_logs).find(params[:id])
22
+ @page_title = "Quickbooks Sync ##{@qb_ticket.id}"
23
+
24
+ @qb_order_items_form = Effective::QbOrderItemsForm.new(id: @qb_ticket.id, orders: @qb_ticket.orders)
25
+ @qb_order_items_form.qb_order_items_attributes = permitted_qb_order_items_params[:qb_order_items_attributes].values
26
+
27
+ if @qb_order_items_form.save
28
+ flash[:success] = 'Successfully updated Quickbooks item names'
29
+ redirect_to effective_qb_sync.admin_qb_sync_path(@qb_ticket)
30
+ else
31
+ flash.now[:danger] = 'Unable to update Quickbooks item names'
32
+ render action: :show
33
+ end
34
+ end
35
+
36
+ def instructions
37
+ @page_title = 'Quickbooks Setup Instructions'
38
+ end
39
+
40
+ def qwc
41
+ @filename = Rails.application.class.parent_name.downcase + '.qwc'
42
+
43
+ response.headers['Content-Disposition'] = "attachment; filename=\"#{@filename}\""
44
+
45
+ render '/effective/qb_web_connector/quickbooks.qwc', layout: false
46
+ end
47
+
48
+ private
49
+
50
+ def restrict_access
51
+ EffectiveQbSync.authorized?(self, :admin, :effective_qb_sync)
52
+ end
53
+
54
+ def permitted_qb_order_items_params
55
+ params.require(:effective_qb_order_items_form).permit(:id, qb_order_items_attributes: [:name, :id, :order_item_id])
56
+ end
57
+
58
+
59
+ end
60
+ end
@@ -0,0 +1,40 @@
1
+ module Effective
2
+ class QbSyncController < ApplicationController
3
+ skip_authorization_check if defined?(CanCan)
4
+ skip_before_filter :verify_authenticity_token
5
+
6
+ def api
7
+ # respond successfully to a GET which some versions of the Web Connector send to verify the url
8
+ (render(nothing: true) and return) if request.get?
9
+
10
+ # Examine raw post and determine which API call to process
11
+ doc = Nokogiri::XML(request.raw_post)
12
+ @qbwcSupervisor = QbwcSupervisor.new
13
+
14
+ api_verb = (doc.at_xpath('//soap:Body').children.first.node_name rescue '')
15
+
16
+ case api_verb
17
+ when 'serverVersion'
18
+ @version = '1.0'
19
+ when 'clientVersion'
20
+ @version = nil
21
+ when 'authenticate'
22
+ @token, @message = @qbwcSupervisor.authenticate(doc)
23
+ when 'sendRequestXML'
24
+ @message = @qbwcSupervisor.sendRequestXML(doc)
25
+ when 'receiveResponseXML'
26
+ @message = @qbwcSupervisor.receiveResponseXML(doc)
27
+ when 'getLastError'
28
+ @message = @qbwcSupervisor.getLastError(doc)
29
+ when 'connectionError'
30
+ @message = @qbwcSupervisor.connectionError(doc)
31
+ when 'closeConnection'
32
+ @message = @qbwcSupervisor.closeConnection(doc)
33
+ else
34
+ ''
35
+ end
36
+
37
+ render(template: "/effective/qb_sync/#{api_verb}.erb", layout: false, content_type: 'text/xml')
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,17 @@
1
+ unless defined?(Effective::AccessDenied)
2
+ module Effective
3
+ class AccessDenied < StandardError
4
+ attr_reader :action, :subject
5
+
6
+ def initialize(message = nil, action = nil, subject = nil)
7
+ @message = message
8
+ @action = action
9
+ @subject = subject
10
+ end
11
+
12
+ def to_s
13
+ @message || I18n.t(:'unauthorized.default', :default => 'Access Denied')
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,30 @@
1
+ if defined?(EffectiveDatatables)
2
+ module Effective
3
+ module Datatables
4
+ class QbSyncs < Effective::Datatable
5
+ datatable do
6
+ default_order :created_at, :desc
7
+
8
+ table_column :created_at
9
+ table_column :state, filter: { values: QbTicket::STATES }
10
+
11
+ array_column :num_orders, visible: false do |qb_ticket|
12
+ qb_ticket.qb_requests.length
13
+ end
14
+
15
+ array_column :orders do |qb_ticket|
16
+ qb_ticket.qb_requests.select { |qb_request| qb_request.order.present? }
17
+ .map { |qb_request| link_to "##{qb_request.order.to_param}", effective_orders.admin_order_path(qb_request.order) }
18
+ .join('<br>').html_safe
19
+ end
20
+
21
+ table_column :actions, sortable: false, filter: false, partial: 'admin/qb_syncs/actions', partial_local: :qb_sync
22
+ end
23
+
24
+ def collection
25
+ Effective::QbTicket.includes(qb_requests: :order)
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,13 @@
1
+ module Effective
2
+ class QbLog < ActiveRecord::Base
3
+ belongs_to :qb_ticket
4
+
5
+ # structure do
6
+ # message :text
7
+ # timestamps
8
+ # end
9
+
10
+ validates :qb_ticket, presence: true
11
+ validates :message, presence: true
12
+ end
13
+ end
@@ -0,0 +1,281 @@
1
+ # a state controller that will help orchestrate the different state transitions in the dance with the QBWC
2
+ # Note that all methods that are prefixed with op_ should correspond directly to the QuickBooks web connector API.
3
+
4
+ module Effective
5
+ class QbMachine
6
+
7
+ attr_reader :ticket
8
+ attr_reader :last_log_message
9
+
10
+ # creates a new machine. this implicitly creates a new ticket unless one is supplied
11
+ def initialize(ticket_id = nil)
12
+ if ticket_id
13
+ @ticket = Effective::QbTicket.find_by_id(ticket_id)
14
+ else
15
+ @ticket = Effective::QbTicket.create
16
+ end
17
+ end
18
+
19
+ # authenticates a user for a particular session
20
+ #
21
+ # returns
22
+ # - 'nvu' if the user login was invalid
23
+ # - 'none' if the user login was valid but there is no work to be done
24
+ # - '' if the user login was valid and there is work to be done
25
+ def op_authenticate(username, password)
26
+ return 'nvu' unless valid?
27
+
28
+ unless authentication_valid?(username, password)
29
+ log "Authentication failed for user #{username}"
30
+ @ticket.update_attributes!(username: username, state: 'Finished', last_error: @last_log_message)
31
+ return 'nvu' # not valid user
32
+ end
33
+
34
+ if has_work?
35
+ log "Authentication successful. Reporting to QuickBooks that there is work to be done."
36
+ @ticket.update_attributes!(username: username, state: 'Authenticated')
37
+ '' # "Any other string value = use this name for company file"
38
+ else
39
+ log "Authentication successful, but there is no work to be done"
40
+ @ticket.update_attributes!(username: username, state: 'Finished')
41
+ 'none'
42
+ end
43
+ end
44
+
45
+ # processes a message from the QBWC that corresponds to
46
+ #
47
+ # string sendRequestXML(ticket, hcpresponse, company, country, major_ver, minor_ver)
48
+ #
49
+ # in the QBWC api
50
+ #
51
+ # The params input should have members that correspond to the above parameters except for ticket, since
52
+ # the ticket has already been used to build this state machine.
53
+ #
54
+ # Returns:
55
+ # - '' if there is no work to be done
56
+ # - Some qbXML if there is work to be done
57
+ def op_send_request_xml(params)
58
+ return '' unless valid?
59
+
60
+ # update the ticket with the metadata sent at the first request for XML (i.e. if not blank)
61
+ @ticket.update_attributes!(
62
+ hpc_response: (@ticket.hpc_response || params[:hcpresponse]),
63
+ company_file_name: (@ticket.company_file_name || params[:company]),
64
+ country: (@ticket.country || params[:country]),
65
+ qbxml_major_version: (@ticket.qbxml_major_version || params[:major_ver]),
66
+ qbxml_minor_version: (@ticket.qbxml_minor_version || params[:minor_ver])
67
+ )
68
+
69
+ # only process when in the Authenticated or Processing states
70
+ unless ['Authenticated', 'Processing'].include?(@ticket.state)
71
+ @ticket.request_error!(@last_log_message)
72
+ return ''
73
+ end
74
+
75
+ # either grab the current request or create a new one
76
+ request = @ticket.qb_request
77
+ unless request
78
+ request = create_request
79
+ @ticket.qb_request = request
80
+ end
81
+
82
+ # if we don't have a request, then we are done.
83
+ unless request
84
+ log "There is no more work to be done. Marking ticket state as finished"
85
+ @ticket.update_attributes!(state: 'Finished')
86
+ return ''
87
+ end
88
+
89
+ request.update_attributes!(qb_ticket: @ticket, request_sent_at: Time.zone.now)
90
+ qb_xml = request.to_qb_xml
91
+ request.update_attributes!(request_qbxml: qb_xml)
92
+
93
+ # set the ticket into a Processing state
94
+ @ticket.state = 'Processing'
95
+
96
+ # save the changes.
97
+ @ticket.save!
98
+
99
+ log "Sending request [#{request.state}] XML to QuickBooks"
100
+
101
+ qb_xml
102
+ end
103
+
104
+ # processes a message from the QBWC that corresponds to
105
+ #
106
+ # int receiveResponseXML(ticket, response, hresult, message)
107
+ #
108
+ # in the QBWC api
109
+ #
110
+ # The params input should have members that correspond to the above parameters except for ticket
111
+ #
112
+ # Returns:
113
+ # - negative value for error
114
+ # - postive value < 100 for percent complete (more work is to be done)
115
+ # - 100 if there is no more work
116
+ def op_receive_response_xml(params)
117
+ return -1 unless valid?
118
+
119
+ # only process when in the 'Processing' state
120
+ unless @ticket.state == 'Processing'
121
+ log "Ticket state #{@ticket.state} not valid for processing responses"
122
+ @ticket.request_error! @last_log_message
123
+ return -1
124
+ end
125
+
126
+ responseXML = params[:response]
127
+ log "Received response XML from QuickBooks"
128
+
129
+ # handle a connection error
130
+ unless params[:hresult].blank? and params[:message].blank?
131
+ log "Connection error with QuickBooks: #{params[:hresult]} : #{params[:message]}"
132
+
133
+ @ticket.request_error!(@last_log_message, connection_error_hresult: params[:hresult], connection_error_message: params[:message])
134
+
135
+ # also update the request if it is able to be found
136
+ request = find_outstanding_request(responseXML)
137
+ request.update_attributes!(response_qbxml: responseXML, state: 'Error') if request
138
+
139
+ return -1
140
+ end
141
+
142
+ # find the corresponding request
143
+ request = find_outstanding_request(responseXML)
144
+
145
+ unless request
146
+ log "Received response back from QuickBooks but it did not correspond to any outstanding ticket request"
147
+ @ticket.request_error! @last_log_message
148
+ return -1
149
+ end
150
+
151
+ log "Found corresponding request [#{request.state}]"
152
+
153
+ # safety check. we should always get a response back for the current request
154
+ unless request == @ticket.qb_request
155
+ log "Received response from QuickBooks but it references a request other than the current request"
156
+ @ticket.request_error! @last_log_message
157
+ return -1
158
+ end
159
+
160
+ # process the response XML now
161
+ unless request.consume_response_xml(responseXML)
162
+ # this request for some reason did not succeeed. Update the request and the ticket
163
+ log "Request [#{request.state}] could not process the QuickBooks response: #{request.error}"
164
+ request.update_attributes!(response_qbxml: responseXML, state: 'Error')
165
+ @ticket.error! @last_log_message
166
+ return -1
167
+ end
168
+
169
+ request.update_attributes!(response_qbxml: responseXML) # This was changed for effective_qb_sync
170
+
171
+ # the request has processed the response XML. if it does not have any more work to do, then detach it
172
+
173
+ if request.has_more_work?
174
+ log "Request [#{request.state}] has more work to do on the next request"
175
+ else
176
+ # detach the current request
177
+ @ticket.update_attributes!(qb_request: nil)
178
+ log "Request [#{request.state}] has completed its work"
179
+ end
180
+
181
+ work_done = @ticket.qb_requests.size
182
+ work_left = how_much_more_work
183
+ work_left = work_left + 1 if @ticket.qb_request # if there is still a current request we need to add that to the work_left
184
+
185
+ work_left == 0 ? 100 : (work_done * 100 / (work_done + work_left))
186
+ end
187
+
188
+ # processes a message from the QBWC that corresponds to
189
+ #
190
+ # string connectionError(string ticket, string hresult, string message)
191
+ #
192
+ # in the QBWC api, signfiying that the connection with QuickBooks was lost
193
+ #
194
+ # The params input should have members that correspond to the above parameters except for ticket
195
+ #
196
+ # Returns: 'done' to indicate that activity on this ticket should not be resumed
197
+ def op_connection_error(hresult,message)
198
+ if valid?
199
+ @ticket.connection_error_hresult = hresult
200
+ @ticket.connection_error_message = message
201
+ @ticket.state = 'ConnectionError'
202
+ end
203
+
204
+ 'done'
205
+ end
206
+
207
+ # processes a message from the QBWC that corresponds to
208
+ #
209
+ # string closeConnection(string ticket)
210
+ #
211
+ # in the QBWC api, signifying that this connection should be terminated.
212
+ #
213
+ # returns the connection closing result (e.g. 'OK')
214
+ def op_close_connection
215
+ return 'Close error: invalid ticket' unless valid?
216
+
217
+ @ticket.update_attributes!(state: 'Finished') unless ['ConnectionError', 'RequestError'].include?(@ticket.state)
218
+ log "Closed connection with QuickBooks"
219
+
220
+ 'OK'
221
+ end
222
+
223
+ # processes a message from the QBWC that corresponds to
224
+ #
225
+ # string getLastError(string ticket)
226
+ #
227
+ # in the QBWC api
228
+ #
229
+ # Returns: the last error recorded for that ticket
230
+ def op_last_error
231
+ return 'Invalid Ticket Id' unless valid?
232
+ @ticket.last_error || ''
233
+ end
234
+
235
+ # returns true if this machine is a valid machine
236
+ def valid?
237
+ @ticket.present?
238
+ end
239
+
240
+ # logs a message to the ticket if it exists
241
+ def log(message)
242
+ @ticket.log(message) unless message.blank?
243
+ # always save the last log message
244
+ @last_log_message = message
245
+ end
246
+
247
+ # sets the ticket as failed and records the message
248
+ def fail_unexpectedly(message)
249
+ log "An unexpected error occurred: #{message}"
250
+ @ticket.request_error! @last_log_message
251
+ end
252
+
253
+ protected
254
+
255
+ # determines if this username and password is valid
256
+ def authentication_valid?(username,password)
257
+ (username == EffectiveQbSync.quickbooks_username) && (password == EffectiveQbSync.quickbooks_password)
258
+ end
259
+
260
+ # returns how much more work is to be done. If there is no more work to be done, it will return 0, else,
261
+ # the number of requests that need to be processed.
262
+ def how_much_more_work
263
+ Effective::QbRequest.new_requests_for_unsynced_items.size
264
+ end
265
+
266
+ # returns true if there is work to be done
267
+ def has_work?
268
+ how_much_more_work > 0
269
+ end
270
+
271
+ # creates a new request object for a unit of work to be done. returns nil if no work can be found
272
+ def create_request
273
+ Effective::QbRequest.new_requests_for_unsynced_items.first
274
+ end
275
+
276
+ # returns a qb request that corresponds to the first element in the response with a requestID
277
+ def find_outstanding_request(responseXML)
278
+ Effective::QbRequest.find_using_response_qbxml(responseXML)
279
+ end
280
+ end
281
+ end