effective_qb_online 0.1.0

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