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
@@ -0,0 +1,26 @@
1
+ module Effective
2
+ class QbReceiptItem < ActiveRecord::Base
3
+ belongs_to :qb_receipt
4
+ belongs_to :order_item
5
+
6
+ log_changes(to: :qb_receipt) if respond_to?(:log_changes)
7
+
8
+ effective_resource do
9
+ # Will be blank when first created. Populated by QbSalesReceipt.build_from_receipt!
10
+ item_id :string
11
+
12
+ timestamps
13
+ end
14
+
15
+ scope :deep, -> { includes(:qb_receipt, :order_item) }
16
+
17
+ def to_s
18
+ item_id.presence || 'New Qb Receipt Item'
19
+ end
20
+
21
+ def order_item_qb_name
22
+ item_id || order_item.purchasable.try(:qb_item_id) || order_item.purchasable.try(:qb_item_name)
23
+ end
24
+
25
+ end
26
+ end
@@ -0,0 +1,101 @@
1
+ module Effective
2
+ class QbSalesReceipt
3
+
4
+ # Build the Quickbooks SalesReceipt from a QbReceipt
5
+ def self.build_from_receipt!(receipt, api: nil)
6
+ raise('Expected a persisted Effective::QbReceipt') unless receipt.kind_of?(Effective::QbReceipt) && receipt.persisted?
7
+
8
+ api ||= EffectiveQbOnline.api
9
+ raise('Expected a connected Quickbooks API') unless api.present?
10
+
11
+ order = receipt.order
12
+ raise('Expected a purchased Effective::Order') unless order.purchased?
13
+
14
+ user = order.user
15
+ raise('Expected a user with an email') unless user.respond_to?(:email)
16
+
17
+ realm = api.realm
18
+ raise('Missing Deposit to Account') unless realm.deposit_to_account_id.present?
19
+ raise('Missing Payment Method') unless realm.payment_method_id.present?
20
+
21
+ taxes = api.taxes_collection
22
+ raise("Missing Tax Code for tax rate #{order.tax_rate.presence || 'blank'}") unless taxes[order.tax_rate.to_s].present?
23
+ raise("Missing Tax Code for tax exempt 0.0 rate") unless taxes['0.0'].present?
24
+
25
+ # Find and validate items
26
+ items = api.items()
27
+
28
+ receipt.qb_receipt_items.each do |receipt_item|
29
+ purchasable = receipt_item.order_item.purchasable
30
+ raise("Expected a purchasable for Effective::OrderItem #{receipt_item.order_item.id}") unless purchasable.present?
31
+
32
+ # Find item by receipt item
33
+ item = items.find { |item| [item.id, item.name].include?(receipt_item.item_id) }
34
+
35
+ # Find item by purchasable qb_item_id and qb_item_name
36
+ item ||= begin
37
+ purchasable_id_name = [purchasable.try(:qb_item_id), purchasable.try(:qb_item_name)]
38
+ items.find { |item| ([item.id, item.name] & purchasable_id_name).present? }
39
+ end
40
+
41
+ if item.blank?
42
+ raise("Unknown Quickbooks Item for #{purchasable} (#{purchasable.class.name} ##{purchasable.id})")
43
+ end
44
+
45
+ receipt_item.update!(item_id: item.id)
46
+ end
47
+
48
+ # Find or build customer
49
+ if receipt.customer_id.blank?
50
+ customer = api.find_or_create_customer(user: user)
51
+ receipt.update!(customer_id: customer.id)
52
+ end
53
+
54
+ # Receipt
55
+ sales_receipt = Quickbooks::Model::SalesReceipt.new(
56
+ customer_id: receipt.customer_id,
57
+ deposit_to_account_id: api.realm.deposit_to_account_id, # The ID of the Account Entity you want hte SalesReceipt to be deposited to
58
+ payment_method_id: api.realm.payment_method_id, # The ID of the PaymentMethod Entity
59
+ payment_ref_number: order.to_param, # Optional payment reference number/string
60
+ txn_date: order.purchased_at.to_date,
61
+ customer_memo: order.note_to_buyer,
62
+ private_note: order.note_internal,
63
+ bill_email: Quickbooks::Model::EmailAddress.new(order.email),
64
+ email_status: 'EmailSent'
65
+ )
66
+
67
+ # Allows Quickbooks to auto-generate the transaction number
68
+ sales_receipt.auto_doc_number!
69
+
70
+ # Addresses
71
+ sales_receipt.bill_address = api.build_address(order.billing_address) if order.billing_address.present?
72
+ sales_receipt.ship_address = api.build_address(order.shipping_address) if order.shipping_address.present?
73
+
74
+ # Line Items
75
+ tax_code = taxes[order.tax_rate.to_s]
76
+ tax_exempt = taxes['0.0']
77
+
78
+ receipt.qb_receipt_items.each do |receipt_item|
79
+ order_item = receipt_item.order_item
80
+ line_item = Quickbooks::Model::Line.new(amount: api.price_to_amount(order_item.subtotal), description: order_item.name)
81
+
82
+ line_item.sales_item! do |line|
83
+ line.item_id = receipt_item.item_id
84
+ line.tax_code_id = (order_item.tax_exempt? ? tax_exempt.id : tax_code.id)
85
+
86
+ line.unit_price = api.price_to_amount(order_item.price)
87
+ line.quantity = order_item.quantity
88
+ end
89
+
90
+ sales_receipt.line_items << line_item
91
+ end
92
+
93
+ # Double check
94
+ raise("Invalid SalesReceipt generated for Effective::Order #{order.id}") unless sales_receipt.valid?
95
+
96
+ # Return a Quickbooks::Model::SalesReceipt that is ready to create
97
+ sales_receipt
98
+ end
99
+
100
+ end
101
+ end
@@ -0,0 +1,31 @@
1
+ %table.table.table-sm
2
+ - company_info = api.company_info
3
+
4
+ %tbody
5
+ %tr
6
+ %td Name
7
+ %td= company_info.company_name
8
+
9
+ %tr
10
+ %td Country
11
+ %td= company_info.country
12
+
13
+ %tr
14
+ %td OAuth2 Connection
15
+ %td= link_to('Reconnect to Quickbooks Online', effective_qb_online.quickbooks_oauth_path)
16
+
17
+ %tr
18
+ %td Deposit to Account
19
+ %td
20
+ = effective_form_with(model: [:admin, api.realm], engine: true) do |f|
21
+ .row
22
+ .col= f.select :deposit_to_account_id, api.accounts_collection, grouped: true, label: false
23
+ .col= f.save
24
+
25
+ %tr
26
+ %td Payment Method
27
+ %td
28
+ = effective_form_with(model: [:admin, api.realm], engine: true) do |f|
29
+ .row
30
+ .col= f.select :payment_method_id, api.payment_methods_collection, label: false
31
+ .col= f.save
@@ -0,0 +1,15 @@
1
+ - realm = api.realm
2
+
3
+ %p
4
+ QB_REALM_ID=#{realm.realm_id}
5
+ %br
6
+ QB_ACCESS_TOKEN=#{realm.access_token}
7
+ %br
8
+ QB_REFRESH_TOKEN=#{realm.refresh_token}
9
+ %br
10
+ QB_DEPOSIT_TO_ACCOUNT=#{realm.deposit_to_account_id}
11
+ %br
12
+ QB_PAYMENT_METHOD=#{realm.payment_method_id}
13
+
14
+ %p
15
+ %small Copy these values into effective_qb_online/.env to run the test suite
@@ -0,0 +1,14 @@
1
+ %h1.effective-admin-heading= @page_title
2
+
3
+ - if Rails.env.development?
4
+ = card('Test Credentials') do
5
+ = render('test_credentials', api: @api)
6
+
7
+ = card('Company') do
8
+ = render('company', api: @api)
9
+
10
+ = card('Sales Receipts') do
11
+ .text-right.mb-2
12
+ = link_to 'New Sales Receipt', effective_qb_online.new_admin_qb_receipt_path, class: 'btn btn-primary btn-sm'
13
+
14
+ = render_datatable(Admin::EffectiveQbReceiptsDatatable.new)
@@ -0,0 +1,18 @@
1
+ %h1.effective-page-title= @page_title
2
+
3
+ %p Welcome to the Quickbooks Online first run instructions!
4
+
5
+ %p
6
+ 1.) Ask Code & Effect to add a Redirect URI
7
+ to the Quickbooks Online Effective Orders application.
8
+
9
+ %ul
10
+ %li The uri is: #{effective_qb_online.quickbooks_oauth_callback_url}
11
+
12
+ %p
13
+ 2.) Then
14
+ = link_to('click here', effective_qb_online.quickbooks_oauth_path)
15
+ and sign in to the Quickbooks Online company you wish to connect.
16
+
17
+ %p
18
+ 3.) Once connected, you will be be returned to this website. All done!
@@ -0,0 +1,40 @@
1
+ = effective_form_with(model: [:admin, qb_receipt], engine: true) do |f|
2
+
3
+ - if f.object.new_record?
4
+ = f.select :order_id, qb_receipt_effective_orders_collection(f.object), required: true,
5
+ label: 'Unsynchronized purchased order',
6
+ hint: "If you don't see your purchased order in the list, it already has an existing sales receipt."
7
+
8
+ - if f.object.persisted?
9
+ = f.static_field :order
10
+
11
+ = f.static_field :updated_at
12
+
13
+ = f.static_field :status
14
+ = f.static_field :result, label: 'Quickbooks Online Result'
15
+
16
+ %table.table.table-sm
17
+ %thead
18
+ %tr
19
+ %th Id
20
+ %th Order Item
21
+ %th Existing Name
22
+ %th Quickbooks Online Item
23
+
24
+ %tbody
25
+ - items_collection = EffectiveQbOnline.api.items_collection
26
+ - items = items_collection.flatten(2)
27
+
28
+ = f.fields_for :qb_receipt_items do |fi|
29
+ %tr
30
+ %td= fi.object.order_item_id
31
+ %td= fi.object.order_item
32
+ %td
33
+ - if fi.object.item_id.present?
34
+ - existing = items.find { |(name, id)| id == fi.object.item_id }&.first
35
+
36
+ = existing || fi.object.order_item_qb_name || '-'
37
+
38
+ %td= fi.select :item_id, items_collection, grouped: true, label: false
39
+
40
+ = f.submit 'Save and Sync'
@@ -0,0 +1,4 @@
1
+ %h1.effective-admin-heading= @page_title
2
+
3
+ = card(@qb_receipt) do
4
+ = render_resource_form(@qb_receipt)
@@ -0,0 +1,4 @@
1
+ %h1.effective-admin-heading= @page_title
2
+
3
+ = card(@qb_receipt) do
4
+ = render_resource_form(@qb_receipt)
@@ -0,0 +1,23 @@
1
+ EffectiveQbOnline.setup do |config|
2
+ config.qb_realms_table_name = :qb_realms
3
+ config.qb_receipts_table_name = :qb_receipts
4
+ config.qb_receipt_items_table_name = :qb_receipt_items
5
+
6
+ # Layout Settings
7
+ # Configure the Layout per controller, or all at once
8
+ # config.layout = { application: 'application', admin: 'admin' }
9
+
10
+ # Quickbooks Online Application
11
+ # Client and Seceret
12
+ config.oauth_client_id = ENV['QUICKBOOKS_ONLINE_OAUTH_CLIENT_ID']
13
+ config.oauth_client_secret = ENV['QUICKBOOKS_ONLINE_OAUTH_CLIENT_SECRET']
14
+
15
+ # Quickbooks API
16
+ # https://github.com/ruckus/quickbooks-ruby
17
+ Quickbooks.sandbox_mode = (ENV['QUICKBOOKS_ONLINE_SANDBOX'].to_s == 'true')
18
+
19
+ # Effective Orders
20
+ # Add the following to your config/intializers/effective_orders.rb
21
+ # config.use_effective_qb_online = true
22
+
23
+ end
data/config/routes.rb ADDED
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ Rails.application.routes.draw do
4
+ mount EffectiveQbOnline::Engine => '/', as: 'effective_qb_online'
5
+ end
6
+
7
+ EffectiveQbOnline::Engine.routes.draw do
8
+ # Public routes
9
+ scope module: 'effective' do
10
+ get '/quickbooks/oauth/authorize', to: 'qb_oauth#authorize', as: :quickbooks_oauth
11
+ get '/quickbooks/oauth/callback', to: 'qb_oauth#callback', as: :quickbooks_oauth_callback
12
+ end
13
+
14
+ namespace :admin do
15
+ resources :qb_realms, only: [:edit, :update]
16
+
17
+ resources :qb_receipts, except: [:show, :destroy] do
18
+ post :skip, on: :member
19
+ post :sync, on: :member
20
+ end
21
+
22
+ get '/quickbooks', to: 'qb_online#index', as: :quickbooks
23
+ end
24
+
25
+ end
@@ -0,0 +1,40 @@
1
+ class CreateEffectiveQbOnline < ActiveRecord::Migration[6.1]
2
+ def change
3
+ create_table <%= @qb_realms_table_name %> do |t|
4
+ t.string :realm_id
5
+
6
+ t.integer :deposit_to_account_id
7
+ t.integer :payment_method_id
8
+
9
+ t.text :access_token
10
+ t.datetime :access_token_expires_at
11
+
12
+ t.text :refresh_token
13
+ t.datetime :refresh_token_expires_at
14
+
15
+ t.timestamps
16
+ end
17
+
18
+ create_table <%= @qb_receipts_table_name %> do |t|
19
+ t.integer :order_id
20
+ t.integer :customer_id
21
+
22
+ t.text :result
23
+
24
+ t.string :status
25
+ t.text :status_steps
26
+
27
+ t.timestamps
28
+ end
29
+
30
+ create_table <%= @qb_receipt_items_name %> do |t|
31
+ t.integer :qb_receipt_id
32
+ t.integer :order_item_id
33
+
34
+ t.integer :item_id
35
+
36
+ t.timestamps
37
+ end
38
+
39
+ end
40
+ end
data/db/seeds.rb ADDED
@@ -0,0 +1 @@
1
+ puts "Running effective_qb_online seeds"
@@ -0,0 +1,17 @@
1
+ module EffectiveQbOnline
2
+ class Engine < ::Rails::Engine
3
+ engine_name 'effective_qb_online'
4
+
5
+ # Set up our default configuration options.
6
+ initializer 'effective_qb_online.defaults', before: :load_config_initializers do |app|
7
+ eval File.read("#{config.root}/config/effective_qb_online.rb")
8
+ end
9
+
10
+ # Include acts_as_addressable concern and allow any ActiveRecord object to call it
11
+ initializer 'effective_qb_online.active_record' do |app|
12
+ ActiveSupport.on_load :active_record do
13
+ end
14
+ end
15
+
16
+ end
17
+ end
@@ -0,0 +1,3 @@
1
+ module EffectiveQbOnline
2
+ VERSION = '0.1.0'.freeze
3
+ end
@@ -0,0 +1,55 @@
1
+ require 'quickbooks-ruby'
2
+ require 'effective_resources'
3
+ require 'effective_datatables'
4
+ require 'effective_qb_online/engine'
5
+ require 'effective_qb_online/version'
6
+
7
+ module EffectiveQbOnline
8
+ def self.config_keys
9
+ [
10
+ :qb_realms_table_name, :qb_receipts_table_name, :qb_receipt_items_table_name,
11
+ :oauth_client_id, :oauth_client_secret,
12
+ :layout
13
+ ]
14
+ end
15
+
16
+ include EffectiveGem
17
+
18
+ def self.oauth2_client
19
+ OAuth2::Client.new(
20
+ oauth_client_id,
21
+ oauth_client_secret,
22
+ site: 'https://appcenter.intuit.com/connect/oauth2',
23
+ authorize_url: 'https://appcenter.intuit.com/connect/oauth2',
24
+ token_url: 'https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer'
25
+ )
26
+ end
27
+
28
+ def self.api(realm: nil)
29
+ realm ||= Effective::QbRealm.first
30
+ return nil if realm.blank?
31
+
32
+ Effective::QbApi.new(realm: realm)
33
+ end
34
+
35
+ def self.sync_order!(order, perform_now: false)
36
+ raise 'expected a purchased Effective::Order' unless order.kind_of?(Effective::Order) && order.purchased?
37
+
38
+ if perform_now
39
+ qb_receipt = Effective::QbReceipt.create_from_order!(order)
40
+ qb_receipt.sync!
41
+ else
42
+ QbSyncOrderJob.perform_later(order)
43
+ end
44
+
45
+ true
46
+ end
47
+
48
+ def self.skip_order!(order)
49
+ raise 'expected a purchased Effective::Order' unless order.kind_of?(Effective::Order) && order.purchased?
50
+
51
+ qb_receipt = Effective::QbReceipt.create_from_order!(order)
52
+ qb_receipt.skip!
53
+ end
54
+
55
+ end
@@ -0,0 +1,32 @@
1
+ module EffectiveMemberships
2
+ module Generators
3
+ class InstallGenerator < Rails::Generators::Base
4
+ include Rails::Generators::Migration
5
+
6
+ desc 'Creates an EffectiveQbOnline initializer in your application.'
7
+
8
+ source_root File.expand_path('../../templates', __FILE__)
9
+
10
+ def self.next_migration_number(dirname)
11
+ if not ActiveRecord::Base.timestamped_migrations
12
+ Time.new.utc.strftime("%Y%m%d%H%M%S")
13
+ else
14
+ '%.3d' % (current_migration_number(dirname) + 1)
15
+ end
16
+ end
17
+
18
+ def copy_initializer
19
+ template ('../' * 3) + 'config/effective_qb_online.rb', 'config/initializers/effective_qb_online.rb'
20
+ end
21
+
22
+ def create_migration_file
23
+ @qb_realms_table_name = ':' + EffectiveQbOnline.qb_realms_table_name.to_s
24
+ @qb_receipts_table_name = ':' + EffectiveQbOnline.qb_receipts_table_name.to_s
25
+ @qb_receipt_items_table_name = ':' + EffectiveQbOnline.qb_receipt_items_table_name.to_s
26
+
27
+ migration_template ('../' * 3) + 'db/migrate/01_create_effective_qb_online.rb.erb', 'db/migrate/create_effective_qb_online.rb'
28
+ end
29
+
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,4 @@
1
+ # Visit http://localhost:3000/rails/mailers
2
+
3
+ class EffectiveQbOnlineMailerPreview < ActionMailer::Preview
4
+ end
@@ -0,0 +1,8 @@
1
+ namespace :effective_qb_online do
2
+
3
+ # bundle exec rake effective_qb_online:seed
4
+ task seed: :environment do
5
+ load "#{__dir__}/../../db/seeds.rb"
6
+ end
7
+
8
+ end