effective_qb_online 0.1.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 (39) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +96 -0
  4. data/Rakefile +18 -0
  5. data/app/assets/config/effective_qb_online_manifest.js +3 -0
  6. data/app/assets/javascripts/effective_qb_online/base.js +0 -0
  7. data/app/assets/javascripts/effective_qb_online.js +1 -0
  8. data/app/assets/stylesheets/effective_qb_online/base.scss +0 -0
  9. data/app/assets/stylesheets/effective_qb_online.scss +1 -0
  10. data/app/controllers/admin/qb_online_controller.rb +20 -0
  11. data/app/controllers/admin/qb_realms_controller.rb +11 -0
  12. data/app/controllers/admin/qb_receipts_controller.rb +17 -0
  13. data/app/controllers/effective/qb_oauth_controller.rb +52 -0
  14. data/app/datatables/admin/effective_qb_receipts_datatable.rb +41 -0
  15. data/app/helpers/effective_qb_online_helper.rb +14 -0
  16. data/app/jobs/qb_sync_order_job.rb +13 -0
  17. data/app/models/effective/qb_api.rb +208 -0
  18. data/app/models/effective/qb_realm.rb +37 -0
  19. data/app/models/effective/qb_receipt.rb +98 -0
  20. data/app/models/effective/qb_receipt_item.rb +26 -0
  21. data/app/models/effective/qb_sales_receipt.rb +101 -0
  22. data/app/views/admin/qb_online/_company.html.haml +31 -0
  23. data/app/views/admin/qb_online/_test_credentials.html.haml +15 -0
  24. data/app/views/admin/qb_online/index.html.haml +14 -0
  25. data/app/views/admin/qb_online/instructions.html.haml +18 -0
  26. data/app/views/admin/qb_receipts/_form.html.haml +40 -0
  27. data/app/views/admin/qb_receipts/edit.html.haml +4 -0
  28. data/app/views/admin/qb_receipts/new.html.haml +4 -0
  29. data/config/effective_qb_online.rb +23 -0
  30. data/config/routes.rb +25 -0
  31. data/db/migrate/01_create_effective_qb_online.rb.erb +40 -0
  32. data/db/seeds.rb +1 -0
  33. data/lib/effective_qb_online/engine.rb +17 -0
  34. data/lib/effective_qb_online/version.rb +3 -0
  35. data/lib/effective_qb_online.rb +55 -0
  36. data/lib/generators/effective_qb_online/install_generator.rb +32 -0
  37. data/lib/generators/templates/effective_qb_online_mailer_preview.rb +4 -0
  38. data/lib/tasks/effective_qb_online_tasks.rake +8 -0
  39. metadata +262 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 41c564325389352ce139af8d840c39d3f5ffaa132efd7f8bb72c5d7fe28927af
4
+ data.tar.gz: 505c8393cf0e7a06b6a687ccebda7c510f9590a248cc111e4c524027cb9b8163
5
+ SHA512:
6
+ metadata.gz: 06fd5e50780c61176aaf4df670888855cd7590a5fb9f949d2a5d50686e2c077e8719c1a454327d2c6f82721ecf596fd0ed2f0c8d1ce4a056293d9f40c1bd11c5
7
+ data.tar.gz: da8ba5c9871f8988a4e2d74393779ebd9b0b4e97c5fa4b8d65304d264556cfe1d1d1078c4d0ec3d049d48d6232d416651054f52a8beb1a27450037cee52576fe
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2022 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,96 @@
1
+ # Effective Quickbooks Online
2
+
3
+ Create Quickbooks Online SalesReceipts for purchased effective orders.
4
+
5
+ ## Getting Started
6
+
7
+ This requires Rails 6+ and Twitter Bootstrap 4 and just works with Devise.
8
+
9
+ Please first install the [effective_datatables](https://github.com/code-and-effect/effective_datatables) gem.
10
+
11
+ Please download and install the [Twitter Bootstrap4](http://getbootstrap.com)
12
+
13
+ Add to your Gemfile:
14
+
15
+ ```ruby
16
+ gem 'haml-rails' # or try using gem 'hamlit-rails'
17
+ gem 'effective_qb_online'
18
+ ```
19
+
20
+ Run the bundle command to install it:
21
+
22
+ ```console
23
+ bundle install
24
+ ```
25
+
26
+ Then run the generator:
27
+
28
+ ```ruby
29
+ rails generate effective_qb_online:install
30
+ ```
31
+
32
+ The generator will install an initializer which describes all configuration options and creates a database migration.
33
+
34
+ If you want to tweak the table names, manually adjust both the configuration file and the migration now.
35
+
36
+ Then migrate the database:
37
+
38
+ ```ruby
39
+ rake db:migrate
40
+ ```
41
+
42
+ ```
43
+ Add a link to the admin menu:
44
+
45
+ ```haml
46
+ - if can? :admin, :effective_qb_online
47
+ = nav_link_to 'Quickbooks Online', effective_qb_online.admin_quickbooks_path
48
+ ```
49
+
50
+ and visit `/admin/quickbooks`.
51
+
52
+ ## Authorization
53
+
54
+ All authorization checks are handled via the effective_resources gem found in the `config/initializers/effective_resources.rb` file.
55
+
56
+ ## Permissions
57
+
58
+ The permissions you actually want to define are as follows (using CanCan):
59
+
60
+ ```ruby
61
+ if user.admin?
62
+ can :admin, :effective_qb_online
63
+
64
+ can(crud, Effective::QbRealm)
65
+ can(crud + [:skip, :sync], Effective::QbReceipt) { |receipt| !receipt.completed? }
66
+ end
67
+ ```
68
+
69
+ ## Configuring Quickbooks Company
70
+
71
+ This gem has only been tested with a Canadian Quickbooks Online store.
72
+
73
+ It has GST, HST and Tax Exempt tax codes and rates set up ahead of time by Quickbooks.
74
+
75
+ ## License
76
+
77
+ MIT License. Copyright [Code and Effect Inc.](http://www.codeandeffect.com/)
78
+
79
+ ## Testing
80
+
81
+ There are tests, but the access token and refresh token doesn't work well.
82
+
83
+ You must visit /admin/quickbooks and copy & paste the test credentials into ~/.env
84
+
85
+ ```ruby
86
+ rails test
87
+ ```
88
+
89
+ ## Contributing
90
+
91
+ 1. Fork it
92
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
93
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
94
+ 4. Push to the branch (`git push origin my-new-feature`)
95
+ 5. Bonus points for test coverage
96
+ 6. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,18 @@
1
+ require "bundler/setup"
2
+
3
+ APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
4
+ load "rails/tasks/engine.rake"
5
+
6
+ load "rails/tasks/statistics.rake"
7
+
8
+ require "bundler/gem_tasks"
9
+
10
+ require "rake/testtask"
11
+
12
+ Rake::TestTask.new(:test) do |t|
13
+ t.libs << 'test'
14
+ t.pattern = 'test/**/*_test.rb'
15
+ t.verbose = false
16
+ end
17
+
18
+ task default: :test
@@ -0,0 +1,3 @@
1
+ //= link_directory ../javascripts .js
2
+ //= link_directory ../stylesheets .css
3
+ //= link_tree ../images
@@ -0,0 +1 @@
1
+ //= require_tree ./effective_qb_online
@@ -0,0 +1 @@
1
+ @import 'effective_qb_online/base';
@@ -0,0 +1,20 @@
1
+ module Admin
2
+ class QbOnlineController < ApplicationController
3
+ before_action(:authenticate_user!) if defined?(Devise)
4
+ before_action { EffectiveResources.authorize!(self, :admin, :effective_qb_online) }
5
+
6
+ include Effective::CrudController
7
+
8
+ page_title 'Quickbooks Online'
9
+
10
+ # /admin/quickbooks
11
+ def index
12
+ @api = EffectiveQbOnline.api
13
+
14
+ authorize! :index, Effective::QbRealm
15
+
16
+ render(@api.present? ? 'index' : 'instructions')
17
+ end
18
+
19
+ end
20
+ end
@@ -0,0 +1,11 @@
1
+ module Admin
2
+ class QbRealmsController < ApplicationController
3
+ before_action(:authenticate_user!) if defined?(Devise)
4
+ before_action { EffectiveResources.authorize!(self, :admin, :effective_qb_online) }
5
+
6
+ include Effective::CrudController
7
+
8
+ on :save, redirect: -> { effective_qb_online.admin_quickbooks_path }
9
+
10
+ end
11
+ end
@@ -0,0 +1,17 @@
1
+ module Admin
2
+ class QbReceiptsController < ApplicationController
3
+ before_action(:authenticate_user!) if defined?(Devise)
4
+ before_action { EffectiveResources.authorize!(self, :admin, :effective_qb_online) }
5
+
6
+ include Effective::CrudController
7
+
8
+ on :skip, redirect: -> { effective_qb_online.admin_quickbooks_path }
9
+
10
+ on :sync, redirect: -> {
11
+ resource.completed? ? effective_qb_online.admin_quickbooks_path : :edit
12
+ }
13
+
14
+ submit :sync, 'Save and Sync'
15
+
16
+ end
17
+ end
@@ -0,0 +1,52 @@
1
+ # http://caaa.test:3000/quickbooks/oauth/authorize
2
+
3
+ module Effective
4
+ class QbOauthController < ApplicationController
5
+ before_action(:authenticate_user!) if defined?(Devise)
6
+
7
+ # Any user that has priviledges with the Quickbooks Online company could authenticate
8
+ # But we assume this user also has admin priviledges on our site
9
+ # This should only be done once anyway
10
+ before_action { EffectiveResources.authorize!(self, :admin, :effective_qb_online) }
11
+
12
+ def authorize
13
+ grant_url = EffectiveQbOnline.oauth2_client.auth_code.authorize_url(
14
+ redirect_uri: redirect_uri,
15
+ response_type: 'code',
16
+ state: SecureRandom.hex(12),
17
+ scope: 'com.intuit.quickbooks.accounting'
18
+ )
19
+
20
+ redirect_to(grant_url)
21
+ end
22
+
23
+ # This matches the Quickbooks Redirect URI and we have to set it up ahead of time.
24
+ def callback
25
+ return unless params[:code].present? && params[:realmId].present? && params[:state].present?
26
+
27
+ token = EffectiveQbOnline.oauth2_client.auth_code.get_token(params[:code], redirect_uri: redirect_uri)
28
+ return unless token
29
+
30
+ realm = Effective::QbRealm.all.first_or_initialize
31
+
32
+ realm.update!(
33
+ realm_id: params[:realmId],
34
+ access_token: token.token,
35
+ refresh_token: token.refresh_token,
36
+ access_token_expires_at: Time.at(token.expires_at),
37
+ refresh_token_expires_at: (Time.at(token.expires_at) + 100.days)
38
+ )
39
+
40
+ flash[:success] = 'Successfully connected with Quickbooks Online'
41
+
42
+ redirect_to(effective_qb_online.admin_quickbooks_path)
43
+ end
44
+
45
+ private
46
+
47
+ def redirect_uri
48
+ effective_qb_online.quickbooks_oauth_callback_url
49
+ end
50
+
51
+ end
52
+ end
@@ -0,0 +1,41 @@
1
+ module Admin
2
+ class EffectiveQbReceiptsDatatable < Effective::Datatable
3
+ datatable do
4
+ order :updated_at
5
+
6
+ col :updated_at, visible: false
7
+ col :created_at, visible: false
8
+ col :id, visible: false
9
+
10
+ col :updated_at
11
+
12
+ col :order, search: :string
13
+
14
+ col :sales_receipt_id, label: 'QB Sales Receipt' do |receipt|
15
+ if receipt.sales_receipt_id.present?
16
+ link_to("Sales Receipt", api.sales_receipt_url(receipt.sales_receipt_id), target: '_blank')
17
+ end
18
+ end
19
+
20
+ col :customer_id, label: 'QB Customer' do |receipt|
21
+ if receipt.sales_receipt_id.present?
22
+ link_to("Customer", api.customer_url(receipt.customer_id), target: '_blank')
23
+ end
24
+ end
25
+
26
+ col :status
27
+ col :result
28
+
29
+ actions_col
30
+ end
31
+
32
+ collection do
33
+ Effective::QbReceipt.deep.all
34
+ end
35
+
36
+ def api
37
+ @api ||= EffectiveQbOnline.api
38
+ end
39
+
40
+ end
41
+ end
@@ -0,0 +1,14 @@
1
+ module EffectiveQbOnlineHelper
2
+ def qb_receipt_effective_orders_collection(qb_receipt)
3
+ raise('expected a non-persisted Effective::QbReceipt') unless qb_receipt.kind_of?(Effective::QbReceipt) && qb_receipt.new_record?
4
+
5
+ existing = Effective::QbReceipt.select('order_id')
6
+ orders = Effective::Order.purchased.sorted.includes(:user).where.not(id: existing)
7
+
8
+ orders.map do |order|
9
+ label = "#{order} - #{order.user} - #{price_to_currency(order.total)}"
10
+ [label, order.to_param]
11
+ end
12
+
13
+ end
14
+ end
@@ -0,0 +1,13 @@
1
+ class QbSyncOrderJob < ApplicationJob
2
+ queue_as :default
3
+
4
+ def perform(order)
5
+ raise('expected a purchased Effective::Order') unless order.kind_of?(Effective::Order) && order.purchased?
6
+
7
+ puts "Starting QB Sync Order Job for order #{order}"
8
+
9
+ qb_receipt = Effective::QbReceipt.create_from_order!(order)
10
+ qb_receipt.sync!
11
+ end
12
+
13
+ end
@@ -0,0 +1,208 @@
1
+ # The Quickbooks namespace comes from quickbooks-ruby gem
2
+ # https://github.com/ruckus/quickbooks-ruby
3
+
4
+ module Effective
5
+ class QbApi
6
+ attr_accessor :realm
7
+
8
+ def initialize(realm:)
9
+ raise('expected an Effective::QbRealm') unless realm.kind_of?(Effective::QbRealm)
10
+ @realm = realm
11
+ end
12
+
13
+ def app_url
14
+ Quickbooks.sandbox_mode ? 'https://app.sandbox.qbo.intuit.com/app' : 'https://app.qbo.intuit.com/app'
15
+ end
16
+
17
+ def sales_receipt_url(obj)
18
+ "#{app_url}/salesreceipt?txnId=#{obj.try(:sales_receipt_id) || obj.try(:id) || obj}"
19
+ end
20
+
21
+ def customer_url(obj)
22
+ "#{app_url}/customerdetail?nameId=#{obj.try(:customer_id) || obj.try(:id) || obj}"
23
+ end
24
+
25
+ def price_to_amount(price)
26
+ raise('Expected an Integer price') unless price.kind_of?(Integer)
27
+ (price / 100.0).round(2)
28
+ end
29
+
30
+ def build_address(address)
31
+ raise('Expected a Effective::Address') unless address.kind_of?(Effective::Address)
32
+
33
+ Quickbooks::Model::PhysicalAddress.new(
34
+ line1: address.address1,
35
+ line2: address.address2,
36
+ line3: address.try(:address3),
37
+ city: address.city,
38
+ country: address.country,
39
+ country_sub_division_code: address.country_code,
40
+ postal_code: address.postal_code
41
+ )
42
+ end
43
+
44
+ # Singular
45
+ def company_info
46
+ with_service('CompanyInfo') { |service| service.fetch_by_id(realm.realm_id) }
47
+ end
48
+
49
+ def accounts
50
+ with_service('Account') { |service| service.all }
51
+ end
52
+
53
+ # Only accounts we can use for the Deposit to Account setting
54
+ def accounts_collection
55
+ accounts
56
+ .select { |account| ['Bank', 'Other Current Asset'].include?(account.account_type) }
57
+ .sort_by { |account| [account.account_type, account.name] }
58
+ .map { |account| [account.name, account.id, account.account_type] }
59
+ .group_by(&:last)
60
+ end
61
+
62
+ def items
63
+ with_service('Item') { |service| service.all }
64
+ end
65
+
66
+ def items_collection
67
+ items
68
+ .reject { |item| item.type == 'Category' }
69
+ .sort_by { |item| [item.type, item.name] }
70
+ .map { |item| [item.name, item.id, item.type] }
71
+ .group_by(&:last)
72
+ end
73
+
74
+ def payment_methods
75
+ with_service('PaymentMethod') { |service| service.all }
76
+ end
77
+
78
+ def payment_methods_collection
79
+ payment_methods.sort_by(&:name).map { |payment_method| [payment_method.name, payment_method.id] }
80
+ end
81
+
82
+ def find_or_create_customer(user:)
83
+ find_customer(user: user) || create_customer(user: user)
84
+ end
85
+
86
+ def find_customer(user:)
87
+ raise('expected a user that responds to email') unless user.respond_to?(:email)
88
+
89
+ with_service('Customer') do |service|
90
+ # Find by email
91
+ customer = service.find_by(:PrimaryEmailAddr, user.email)&.first
92
+
93
+ # Find by given name and family name
94
+ customer ||= if user.respond_to?(:first_name) && user.respond_to?(:last_name)
95
+ service.query("SELECT * FROM Customer WHERE GivenName LIKE '#{scrub(user.first_name)}' AND FamilyName LIKE '#{scrub(user.last_name)}'")&.first
96
+ end
97
+
98
+ # Find by display name
99
+ customer || service.find_by(:display_name, scrub(user.to_s))&.first
100
+ end
101
+ end
102
+
103
+ def create_customer(user:)
104
+ raise('expected a user that responds to email') unless user.respond_to?(:email)
105
+
106
+ with_service('Customer') do |service|
107
+ customer = Quickbooks::Model::Customer.new(
108
+ primary_email_address: Quickbooks::Model::EmailAddress.new(user.email),
109
+ display_name: scrub(user.to_s),
110
+ given_name: scrub(user.try(:first_name)),
111
+ family_name: scrub(user.try(:last_name))
112
+ )
113
+
114
+ service.create(customer)
115
+ end
116
+ end
117
+
118
+ def delete_customer(customer:)
119
+ with_service('Customer') { |service| service.delete(customer) }
120
+ end
121
+
122
+ def create_sales_receipt(sales_receipt:)
123
+ with_service('SalesReceipt') { |service| service.create(sales_receipt) }
124
+ end
125
+
126
+ def find_sales_receipt(id:)
127
+ with_service('SalesReceipt') { |service| service.find_by(:id, id) }
128
+ end
129
+
130
+ def tax_codes
131
+ with_service('TaxCode') { |service| service.all }
132
+ end
133
+
134
+ def tax_rates
135
+ with_service('TaxRate') { |service| service.all }
136
+ end
137
+
138
+ # Returns a Hash of BigNumeral Tax Rate => TaxCode Object
139
+ # { 0.0 => Quickbooks::Model::TaxCode }
140
+ def taxes_collection
141
+ rates = tax_rates()
142
+ codes = tax_codes()
143
+
144
+ # Find Exempt 0.0
145
+ exempt = codes.find do |code|
146
+ rate_id = code.sales_tax_rate_list.tax_rate_detail.first&.tax_rate_ref&.value
147
+ rate = rates.find { |rate| rate.id == rate_id } if rate_id
148
+
149
+ code.name.downcase.include?('exempt') && rate && rate.rate_value == 0.0
150
+ end
151
+
152
+ exempt = [['0.0', exempt]] if exempt.present?
153
+
154
+ # Find The rest
155
+ tax_codes = codes.map do |code|
156
+ rate_id = code.sales_tax_rate_list.tax_rate_detail.first&.tax_rate_ref&.value
157
+ rate = rates.find { |rate| rate.id == rate_id } if rate_id
158
+
159
+ [rate.rate_value.to_s, code] if rate && (exempt.blank? || rate.rate_value > 0.0)
160
+ end
161
+
162
+ (Array(exempt) + tax_codes.compact).to_h
163
+ end
164
+
165
+ private
166
+
167
+ def with_service(name, &block)
168
+ klass = "Quickbooks::Service::#{name}".constantize
169
+
170
+ with_authenticated_request do |access_token|
171
+ service = klass.new(company_id: realm.realm_id, access_token: access_token)
172
+ yield(service)
173
+ end
174
+ end
175
+
176
+ def with_authenticated_request(max_attempts: 3, &block)
177
+ attempts = 0
178
+
179
+ begin
180
+ token = OAuth2::AccessToken.new(EffectiveQbOnline.oauth2_client, realm.access_token, refresh_token: realm.refresh_token)
181
+ yield(token)
182
+ rescue OAuth2::Error, Quickbooks::AuthorizationFailure => e
183
+ puts "Quickbooks OAuth Error: #{e.message}"
184
+
185
+ attempts += 1
186
+ raise "unable to refresh Quickbooks OAuth2 token" if attempts >= max_attempts
187
+
188
+ # Refresh
189
+ refreshed = token.refresh!
190
+
191
+ realm.update!(
192
+ access_token: refreshed.token,
193
+ refresh_token: refreshed.refresh_token,
194
+ access_token_expires_at: Time.at(refreshed.expires_at),
195
+ refresh_token_expires_at: (Time.at(refreshed.expires_at) + 100.days)
196
+ )
197
+
198
+ retry
199
+ end
200
+ end
201
+
202
+ def scrub(value)
203
+ return nil unless value.present?
204
+ value.gsub(':', '')
205
+ end
206
+
207
+ end
208
+ end
@@ -0,0 +1,37 @@
1
+ # One Quickbooks Realm / Company
2
+
3
+ module Effective
4
+ class QbRealm < ActiveRecord::Base
5
+
6
+ effective_resource do
7
+ # As per Quickbooks oAuth
8
+ realm_id :string
9
+
10
+ access_token :text
11
+ access_token_expires_at :datetime
12
+
13
+ refresh_token :text
14
+ refresh_token_expires_at :datetime
15
+
16
+ # Set on /admin/quickbooks
17
+ deposit_to_account_id :string
18
+ payment_method_id :string
19
+
20
+ timestamps
21
+ end
22
+
23
+ validates :realm_id, presence: true
24
+ validates :realm_id, uniqueness: true, if: -> { new_record? }
25
+
26
+ validates :access_token, presence: true
27
+ validates :access_token_expires_at, presence: true
28
+
29
+ validates :refresh_token, presence: true
30
+ validates :refresh_token_expires_at, presence: true
31
+
32
+ def to_s
33
+ 'Quickbooks Online Settings'
34
+ end
35
+
36
+ end
37
+ end
@@ -0,0 +1,98 @@
1
+ module Effective
2
+ class QbReceipt < ActiveRecord::Base
3
+ belongs_to :order, class_name: 'Effective::Order'
4
+
5
+ log_changes if respond_to?(:log_changes)
6
+
7
+ has_many :qb_receipt_items, inverse_of: :qb_receipt, dependent: :delete_all
8
+ accepts_nested_attributes_for :qb_receipt_items
9
+
10
+ acts_as_statused(:todo, :completed, :errored, :skipped)
11
+
12
+ effective_resource do
13
+ # QuickBooks Customer
14
+ customer_id :string
15
+
16
+ # Quickbooks Online SalesReceipt id, once sync'd
17
+ sales_receipt_id :string
18
+
19
+ # Any error message from our sync
20
+ result :text
21
+
22
+ # Acts as Statused
23
+ status :string
24
+ status_steps :text
25
+
26
+ timestamps
27
+ end
28
+
29
+ scope :deep, -> { includes(order: :user, qb_receipt_items: [order_item: :purchasable]) }
30
+
31
+ # Create a QbReceiptItem for each OrderItem
32
+ before_validation(if: -> { order.present? }) do
33
+ order.order_items.each { |order_item| qb_receipt_item(order_item: order_item) }
34
+ end
35
+
36
+ validates :qb_receipt_items, presence: true
37
+
38
+ with_options(if: -> { completed? }) do
39
+ validates :customer_id, presence: true
40
+ validates :sales_receipt_id, presence: true
41
+ end
42
+
43
+ # Create a QbReceipt from an Effective::Order
44
+ def self.create_from_order!(order)
45
+ raise('Expected a purchased Effective::Order') unless order.kind_of?(Effective::Order) && order.purchased?
46
+ Effective::QbReceipt.where(order: order).first_or_create
47
+ end
48
+
49
+ def to_s
50
+ order.to_s
51
+ end
52
+
53
+ # Find or build
54
+ def qb_receipt_item(order_item:)
55
+ qb_receipt_items.find { |item| item.order_item == order_item } ||
56
+ qb_receipt_items.build(order_item: order_item)
57
+ end
58
+
59
+ def sync!(force: false)
60
+ raise('Already created SalesReceipt with Quickbooks Online') if sales_receipt_id.present? && !force
61
+ save!
62
+
63
+ api = EffectiveQbOnline.api
64
+
65
+ begin
66
+ sales_receipt = Effective::QbSalesReceipt.build_from_receipt!(self, api: api)
67
+ sales_receipt = api.create_sales_receipt(sales_receipt: sales_receipt)
68
+
69
+ # Sanity check
70
+ if (expected = api.price_to_amount(order.total)) != sales_receipt.total
71
+ raise("A Quickbooks Online Sales Receipt has been created with an unexpected total. Quickbooks total is #{sales_receipt.total} but we expected #{expected}. Please adjust the Sales Receipt on Quickbooks")
72
+ end
73
+
74
+ assign_attributes(result: 'completed successfully', sales_receipt_id: sales_receipt.id)
75
+ complete!
76
+ rescue => e
77
+ assign_attributes(result: e.message)
78
+ error!
79
+ end
80
+
81
+ true
82
+ end
83
+
84
+ def skip!
85
+ assign_attributes(result: 'skipped')
86
+ skipped!
87
+ end
88
+
89
+ def complete!
90
+ completed!
91
+ end
92
+
93
+ def error!
94
+ errored!
95
+ end
96
+
97
+ end
98
+ end