effective_qb_sync 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
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,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
|