workarea-orderbot 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (116) hide show
  1. checksums.yaml +7 -0
  2. data/.github/ISSUE_TEMPLATE/bug_report.md +37 -0
  3. data/.github/ISSUE_TEMPLATE/documentation-request.md +17 -0
  4. data/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
  5. data/.github/workflows/ci.yml +58 -0
  6. data/.gitignore +23 -0
  7. data/.rubocop.yml +3 -0
  8. data/CHANGELOG.md +58 -0
  9. data/Gemfile +17 -0
  10. data/LICENSE +52 -0
  11. data/README.md +100 -0
  12. data/Rakefile +59 -0
  13. data/app/assets/images/workarea/admin/orderbot/.keep +0 -0
  14. data/app/assets/images/workarea/storefront/orderbot/.keep +0 -0
  15. data/app/assets/javascripts/workarea/admin/orderbot/.keep +0 -0
  16. data/app/assets/javascripts/workarea/storefront/orderbot/.keep +0 -0
  17. data/app/assets/stylesheets/workarea/admin/orderbot/.keep +0 -0
  18. data/app/assets/stylesheets/workarea/storefront/orderbot/.keep +0 -0
  19. data/app/controllers/.keep +0 -0
  20. data/app/helpers/.keep +0 -0
  21. data/app/mailers/.keep +0 -0
  22. data/app/models/workarea/order.decorator +19 -0
  23. data/app/models/workarea/orderbot/import_log.rb +19 -0
  24. data/app/models/workarea/orderbot/pricing_import_data.rb +13 -0
  25. data/app/models/workarea/orderbot/product_import_data.rb +32 -0
  26. data/app/models/workarea/user.decorator +7 -0
  27. data/app/services/workarea/orderbot/child_product.rb +108 -0
  28. data/app/services/workarea/orderbot/filters.rb +33 -0
  29. data/app/services/workarea/orderbot/order.rb +146 -0
  30. data/app/services/workarea/orderbot/order/item.rb +69 -0
  31. data/app/services/workarea/orderbot/order/tender/credit_card.rb +43 -0
  32. data/app/services/workarea/orderbot/order/tender/general.rb +32 -0
  33. data/app/services/workarea/orderbot/order/tender/gift_card.rb +32 -0
  34. data/app/services/workarea/orderbot/order/tender/store_credit.rb +32 -0
  35. data/app/services/workarea/orderbot/parent_product.rb +87 -0
  36. data/app/views/.keep +0 -0
  37. data/app/workers/workarea/orderbot/fulfillment/import_fulfillments.rb +37 -0
  38. data/app/workers/workarea/orderbot/fulfillment_importer.rb +50 -0
  39. data/app/workers/workarea/orderbot/inventory/import_inventory.rb +50 -0
  40. data/app/workers/workarea/orderbot/inventory_importer.rb +57 -0
  41. data/app/workers/workarea/orderbot/pricing/import_pricing.rb +42 -0
  42. data/app/workers/workarea/orderbot/pricing_importer.rb +70 -0
  43. data/app/workers/workarea/orderbot/product/import_child_products.rb +24 -0
  44. data/app/workers/workarea/orderbot/product/import_parent_products.rb +32 -0
  45. data/app/workers/workarea/orderbot/product_importer.rb +72 -0
  46. data/app/workers/workarea/orderbot/save_order.rb +38 -0
  47. data/bin/rails +25 -0
  48. data/config/initializers/configurations.rb +60 -0
  49. data/config/initializers/scheduled_jobs.rb +28 -0
  50. data/config/initializers/workarea.rb +16 -0
  51. data/config/routes.rb +2 -0
  52. data/lib/workarea/orderbot.rb +43 -0
  53. data/lib/workarea/orderbot/authentication.rb +41 -0
  54. data/lib/workarea/orderbot/bogus_gateway.rb +629 -0
  55. data/lib/workarea/orderbot/engine.rb +10 -0
  56. data/lib/workarea/orderbot/gateway.rb +102 -0
  57. data/lib/workarea/orderbot/response.rb +29 -0
  58. data/lib/workarea/orderbot/version.rb +5 -0
  59. data/test/dummy/Rakefile +6 -0
  60. data/test/dummy/app/assets/config/manifest.js +3 -0
  61. data/test/dummy/app/assets/images/.keep +0 -0
  62. data/test/dummy/app/assets/javascripts/application.js +14 -0
  63. data/test/dummy/app/assets/stylesheets/application.css +15 -0
  64. data/test/dummy/app/controllers/application_controller.rb +2 -0
  65. data/test/dummy/app/controllers/concerns/.keep +0 -0
  66. data/test/dummy/app/helpers/application_helper.rb +2 -0
  67. data/test/dummy/app/jobs/application_job.rb +2 -0
  68. data/test/dummy/app/mailers/application_mailer.rb +4 -0
  69. data/test/dummy/app/models/concerns/.keep +0 -0
  70. data/test/dummy/app/views/layouts/application.html.erb +15 -0
  71. data/test/dummy/app/views/layouts/mailer.html.erb +13 -0
  72. data/test/dummy/app/views/layouts/mailer.text.erb +1 -0
  73. data/test/dummy/bin/bundle +3 -0
  74. data/test/dummy/bin/rails +4 -0
  75. data/test/dummy/bin/rake +4 -0
  76. data/test/dummy/bin/setup +25 -0
  77. data/test/dummy/bin/update +25 -0
  78. data/test/dummy/config.ru +5 -0
  79. data/test/dummy/config/application.rb +34 -0
  80. data/test/dummy/config/boot.rb +5 -0
  81. data/test/dummy/config/environment.rb +5 -0
  82. data/test/dummy/config/environments/development.rb +52 -0
  83. data/test/dummy/config/environments/production.rb +83 -0
  84. data/test/dummy/config/environments/test.rb +45 -0
  85. data/test/dummy/config/initializers/application_controller_renderer.rb +8 -0
  86. data/test/dummy/config/initializers/assets.rb +12 -0
  87. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  88. data/test/dummy/config/initializers/content_security_policy.rb +25 -0
  89. data/test/dummy/config/initializers/cookies_serializer.rb +5 -0
  90. data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  91. data/test/dummy/config/initializers/inflections.rb +16 -0
  92. data/test/dummy/config/initializers/mime_types.rb +4 -0
  93. data/test/dummy/config/initializers/workarea.rb +5 -0
  94. data/test/dummy/config/initializers/wrap_parameters.rb +9 -0
  95. data/test/dummy/config/locales/en.yml +33 -0
  96. data/test/dummy/config/puma.rb +34 -0
  97. data/test/dummy/config/routes.rb +5 -0
  98. data/test/dummy/config/spring.rb +6 -0
  99. data/test/dummy/db/seeds.rb +2 -0
  100. data/test/dummy/lib/assets/.keep +0 -0
  101. data/test/dummy/log/.keep +0 -0
  102. data/test/services/workarea/orderbot/order_test.rb +151 -0
  103. data/test/services/workarea/orderbot/product/child_product_test.rb +255 -0
  104. data/test/services/workarea/orderbot/product/parent_product_test.rb +106 -0
  105. data/test/teaspoon_env.rb +6 -0
  106. data/test/test_helper.rb +10 -0
  107. data/test/workers/workarea/orderbot/fulfillment/import_fulfillments_test.rb +41 -0
  108. data/test/workers/workarea/orderbot/fulfillment_importer_test.rb +29 -0
  109. data/test/workers/workarea/orderbot/inventory/import_inventory_test.rb +49 -0
  110. data/test/workers/workarea/orderbot/inventory_importer_test.rb +21 -0
  111. data/test/workers/workarea/orderbot/pricing/import_pricing_test.rb +61 -0
  112. data/test/workers/workarea/orderbot/pricing_importer_test.rb +29 -0
  113. data/test/workers/workarea/orderbot/product_importer_test.rb +23 -0
  114. data/test/workers/workarea/orderbot/save_order_test.rb +41 -0
  115. data/workarea-orderbot.gemspec +20 -0
  116. metadata +172 -0
@@ -0,0 +1,70 @@
1
+ module Workarea
2
+ module Orderbot
3
+ class PricingImporter
4
+ include Sidekiq::Worker
5
+ sidekiq_options retry: 5
6
+
7
+ def perform(options = {})
8
+ return unless Orderbot.api_configured?
9
+
10
+ Orderbot::ImportLog.log('pricing') do |last_imported_at|
11
+ last_import = options[:from_updated_on] || last_imported_at
12
+
13
+ pricing_filters = {
14
+ response_model: "OrderGuideProduct",
15
+ active: true
16
+ }
17
+
18
+ pricing_response = gateway.get_pricing(pricing_filters)
19
+
20
+ raise 'get pricing error' unless pricing_response.success?
21
+
22
+ pricing_records = pricing_response.body
23
+
24
+ write_prices(pricing_records, last_import)
25
+ if pricing_response.total_pages.to_i > 1
26
+ count = 2
27
+
28
+ while count <= pricing_response.total_pages.to_i
29
+ page_filters = pricing_filters.merge(page: count)
30
+ response = gateway.get_products(page_filters)
31
+
32
+ write_prices(response.body, last_import)
33
+ count = count + 1
34
+ end
35
+ end
36
+
37
+ Workarea::Orderbot::Pricing::ImportPricing.new.perform
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ def gateway
44
+ Workarea::Orderbot.gateway
45
+ end
46
+
47
+ def write_prices(order_guides, last_update_threshold)
48
+ order_guides.each do | order_guide|
49
+
50
+ order_guide_id = order_guide["order_guide_id"]
51
+
52
+ # only import the prices if we care about this order guide ID
53
+ next if order_guide_id != Workarea.config.default_order_guide_id
54
+
55
+ products = order_guide["products"]
56
+
57
+ products.each do |product|
58
+
59
+ next if product["last_updated_on"].present? && Time.parse(product["last_updated_on"]).iso8601 < last_update_threshold.in_time_zone(Workarea.config.orderbot_api_timezone).iso8601
60
+
61
+ Orderbot::PricingImportData.create!(
62
+ order_guide_id: order_guide_id,
63
+ pricing_data: product
64
+ )
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,24 @@
1
+ module Workarea
2
+ module Orderbot
3
+ module Product
4
+ class ImportChildProducts
5
+ include Sidekiq::Worker
6
+
7
+ def perform
8
+ Orderbot::ProductImportData.child_products.each do |child_product|
9
+ result = begin
10
+ Workarea::Orderbot::ChildProduct.new(child_product).process
11
+ rescue StandardError => e
12
+ child_product.update_attributes!(error_message: e.message)
13
+ false
14
+ end
15
+
16
+ if result
17
+ child_product.delete
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,32 @@
1
+ module Workarea
2
+ module Orderbot
3
+ module Product
4
+ class ImportParentProducts
5
+ include Sidekiq::Worker
6
+
7
+ def perform
8
+ Orderbot::ProductImportData.parent_products.each do |parent_product|
9
+ result = begin
10
+ Orderbot::ParentProduct.new(parent_product).process
11
+
12
+ # if a parent product has no children then import the variants
13
+ # pricing and shipping skus
14
+ if !parent_product.has_children
15
+ Orderbot::ChildProduct.new(parent_product).process
16
+ end
17
+
18
+ true
19
+ rescue StandardError => e
20
+ parent_product.update_attributes!(error_message: e.message)
21
+ false
22
+ end
23
+
24
+ if result
25
+ parent_product.delete
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,72 @@
1
+ module Workarea
2
+ module Orderbot
3
+ class ProductImporter
4
+ include Sidekiq::Worker
5
+ sidekiq_options retry: 5
6
+
7
+ def perform(options = {})
8
+ return unless Orderbot.api_configured?
9
+
10
+ Orderbot::ImportLog.log('product') do |last_imported_at|
11
+ last_import = options[:from_updated_on] || last_imported_at
12
+
13
+ attrs = {
14
+ response_model: "integration",
15
+ sort_by: "LastUpdatedOn"
16
+ }.merge!(product_filters)
17
+
18
+ response = gateway.get_products(attrs)
19
+ write_products(response.body, last_import)
20
+
21
+ if response.total_pages.to_i > 1
22
+ count = 2
23
+
24
+ while count <= response.total_pages.to_i
25
+ page_attrs = attrs.merge(page: count)
26
+ response = gateway.get_products(page_attrs)
27
+ write_products(response.body, last_import)
28
+ count = count + 1
29
+ end
30
+ end
31
+ # import the ob parent products - Top level workarea prodcuts
32
+ Orderbot::Product::ImportParentProducts.new.perform
33
+
34
+ # import the children products - workarea variants, pricing and shipping skus
35
+ Orderbot::Product::ImportChildProducts.new.perform
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ def write_products(products, last_update_threshold)
42
+ products.each do | product_data|
43
+ next if Time.parse(product_data["updated_on"]).iso8601 < last_update_threshold.in_time_zone(Workarea.config.orderbot_api_timezone).iso8601
44
+
45
+ parent_product = product_data["parent_id"] == 0
46
+ parent_product_id = product_data["parent_id"]
47
+ has_children = product_data["has_children"]
48
+ product_id = product_data["product_id"]
49
+
50
+ ob_record = Orderbot::ProductImportData.find_or_create_by(product_id: product_id)
51
+ ob_record.update_attributes!(
52
+ product_data: product_data,
53
+ parent_product: parent_product,
54
+ parent_product_id: parent_product_id,
55
+ has_children: has_children
56
+ )
57
+ end
58
+ end
59
+
60
+ def gateway
61
+ Workarea::Orderbot.gateway
62
+ end
63
+
64
+ def product_filters
65
+ config_filters = Workarea.config.product_import_filters || {}
66
+ config_filters.merge!(order_guide: Workarea.config.default_order_guide_id) if Workarea.config.default_order_guide_id.present?
67
+
68
+ config_filters
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,38 @@
1
+ module Workarea
2
+ module Orderbot
3
+ class SaveOrder
4
+ class OrderbotSaveOrderError < StandardError; end
5
+ include Sidekiq::Worker
6
+ include Sidekiq::CallbacksWorker
7
+
8
+ sidekiq_options(
9
+ enqueue_on: { Workarea::Order => [:place] },
10
+ unique: :until_executing
11
+ )
12
+
13
+ def perform(id)
14
+ return unless Orderbot.api_configured?
15
+
16
+ order = Workarea::Order.find(id)
17
+ orderbot_order = Workarea::Orderbot::Order.new(order.id)
18
+ response = Workarea::Orderbot.gateway.create_order(orderbot_order.to_a)
19
+
20
+ raise OrderbotSaveOrderError, response.error_details unless response.success?
21
+ raise OrderbotSaveOrderError, messages(response.body) unless created?(response.body) # checks for a 200 response that fails to create the order
22
+
23
+ orderbot_order_id = response.body.first["order_id"]
24
+ order.set_orderbot_exported_data!(orderbot_order_id)
25
+ end
26
+
27
+ private
28
+
29
+ def created?(body)
30
+ body.first["orderbot_status_code"] == "success"
31
+ end
32
+
33
+ def messages(body)
34
+ body.first["messages"].join(",")
35
+ end
36
+ end
37
+ end
38
+ end
data/bin/rails ADDED
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env ruby
2
+ # This command will automatically be run when you run "rails" with Rails gems
3
+ # installed from the root of your application.
4
+
5
+ ENGINE_ROOT = File.expand_path('..', __dir__)
6
+ ENGINE_PATH = File.expand_path('../lib/workarea/orderbot/engine', __dir__)
7
+ APP_PATH = File.expand_path('../test/dummy/config/application', __dir__)
8
+
9
+ # Set up gems listed in the Gemfile.
10
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
11
+ require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
12
+
13
+ require "rails"
14
+ # Pick the frameworks you want:
15
+ require "active_model/railtie"
16
+ require "active_job/railtie"
17
+ # require "active_record/railtie"
18
+ # require "active_storage/engine"
19
+ require "action_controller/railtie"
20
+ require "action_mailer/railtie"
21
+ require "action_view/railtie"
22
+ # require "action_cable/engine"
23
+ require "sprockets/railtie"
24
+ require "rails/test_unit/railtie"
25
+ require 'rails/engine/commands'
@@ -0,0 +1,60 @@
1
+ Workarea::Configuration.define_fields do
2
+ fieldset 'Orderbot', namespaced: false do
3
+ field 'Orderbot API Email Address',
4
+ type: :string,
5
+ description: 'Email Address of Orderbot admin, used to authenticate to the API. This email address is the same that is used to login to the Orderbot admin.',
6
+ allow_blank: false,
7
+ encrypted: false
8
+
9
+ field 'Orderbot API Password',
10
+ type: :string,
11
+ description: 'Password of Orderbot admin, used to authenticate to the API. This password is the same that is used to login to the Orderbot admin.',
12
+ allow_blank: false,
13
+ encrypted: true
14
+
15
+ field 'Use Orderbot Staging Environment',
16
+ type: :boolean,
17
+ description: 'Use the Orderbot staging environment. Only recommended for experimenting with new and custom Orderbot features, talk to your Orderbot support representative for access.',
18
+ default: false
19
+
20
+ field 'Default Order Guide ID',
21
+ type: :integer,
22
+ description: 'What order guide ID is used to filter products on import. This value will also be sent when an order is exported to Orderbot.',
23
+ allow_blank: true
24
+
25
+ field 'Inventory Distribution Center ID',
26
+ type: :integer,
27
+ description: 'What inventory distribution center to import inventory from. Leaving this field blank will import inventory from all centers and is not recommended. This field will also be used as the default distribution center on order export.',
28
+ allow_blank: true
29
+
30
+ field 'Default Inventory Import Policy',
31
+ type: :symbol,
32
+ default: "standard",
33
+ description: 'The default policy to use when importing inventory from the Orderbot API.',
34
+ allow_blank: false,
35
+ values: -> do
36
+ Workarea.config.inventory_policies.map do |class_name|
37
+ [class_name.demodulize.titleize, class_name.demodulize.underscore]
38
+ end
39
+ end
40
+
41
+ field 'Product Import filters',
42
+ type: :hash,
43
+ values_type: :string,
44
+ default: {},
45
+ description: 'Additional filters to pass to the API when importing products. For example "group: robots"',
46
+ allow_blank: true
47
+
48
+ field 'Orderbot API Timezone',
49
+ type: :string,
50
+ default: "Eastern Time (US & Canada)",
51
+ description: 'Set this value to match the timezone Orderbot sends any date-time information via the API. Consult with Orderbot before changing this value.',
52
+ allow_blank: false,
53
+ values: -> { ActiveSupport::TimeZone.all.map(&:name) }
54
+
55
+ field 'Shipping Date Lead Time',
56
+ type: :duration,
57
+ default: 3.days,
58
+ description: 'Lead time used to generate the "ship date" field when an order is placed. This time will added to the current date and sent to Orderbot when an order is placed.'
59
+ end
60
+ end
@@ -0,0 +1,28 @@
1
+ Sidekiq::Cron::Job.create(
2
+ name: 'Workarea::Orderbot::ProductImporter',
3
+ klass: 'Workarea::Orderbot::ProductImporter',
4
+ cron: '0 2 * * *',
5
+ queue: 'low'
6
+ )
7
+
8
+
9
+ Sidekiq::Cron::Job.create(
10
+ name: 'Workarea::Orderbot::InventoryImporter',
11
+ klass: 'Workarea::Orderbot::InventoryImporter',
12
+ cron: '0 * * * *',
13
+ queue: 'low'
14
+ )
15
+
16
+ Sidekiq::Cron::Job.create(
17
+ name: 'Workarea::Orderbot::PricingImporter',
18
+ klass: 'Workarea::Orderbot::PricingImporter',
19
+ cron: '30 * * * *',
20
+ queue: 'low'
21
+ )
22
+
23
+ Sidekiq::Cron::Job.create(
24
+ name: 'Workarea::Orderbot::FulfillmentImporter',
25
+ klass: 'Workarea::Orderbot::FulfillmentImporter',
26
+ cron: '*/20 * * * *',
27
+ queue: 'low'
28
+ )
@@ -0,0 +1,16 @@
1
+ Workarea.configure do |config|
2
+ config.orderbot = ActiveSupport::Configurable::Configuration.new
3
+ config.orderbot.api_timeout = 30
4
+ config.orderbot.open_timeout = 30
5
+
6
+
7
+ config.orderbot.transaction_id = {
8
+ 'ActiveMerchant::Billing::BogusGateway' => -> (transaction) { transaction.response.authorization },
9
+ 'ActiveMerchant::Billing::StripeGateway' => -> (transaction) { transaction.params['id'] },
10
+ 'ActiveMerchant::Billing::BraintreeBlueGateway' => -> (transaction) { transaction.response.params["braintree_transaction"]["order_id"] },
11
+ 'ActiveMerchant::Billing::MonerisGateway' => -> (transaction) { transaction.response.params["trans_id"] },
12
+ 'ActiveMerchant::Billing::AuthorizeNetCimGateway' => -> (transaction) { transaction.response.params["direct_response"]["transaction_id"] },
13
+ 'ActiveMerchant::Billing::CyberSourceGateway' => -> (transaction) { transaction.response.params["reasonCode"] },
14
+ 'ActiveMerchant::Billing::CheckoutV2Gateway' => -> (transaction) { transaction.response.params["id"] }
15
+ }
16
+ end
data/config/routes.rb ADDED
@@ -0,0 +1,2 @@
1
+ Rails.application.routes.draw do
2
+ end
@@ -0,0 +1,43 @@
1
+ require 'workarea'
2
+ require 'workarea/storefront'
3
+ require 'workarea/admin'
4
+
5
+ require 'workarea/orderbot/engine'
6
+ require 'workarea/orderbot/version'
7
+
8
+ require 'workarea/orderbot/bogus_gateway'
9
+ require 'workarea/orderbot/authentication'
10
+ require 'workarea/orderbot/gateway'
11
+ require 'workarea/orderbot/response'
12
+
13
+ module Workarea
14
+ module Orderbot
15
+ def self.config
16
+ Workarea.config.orderbot
17
+ end
18
+
19
+ def self.api_user_name
20
+ Workarea.config.orderbot_api_email_address
21
+ end
22
+
23
+ def self.api_password
24
+ Workarea.config.orderbot_api_password
25
+ end
26
+
27
+ def self.api_configured?
28
+ api_user_name.present? && api_password.present?
29
+ end
30
+
31
+ def self.test?
32
+ Workarea.config.use_orderbot_staging_environment
33
+ end
34
+
35
+ def self.gateway
36
+ if Rails.env.test?
37
+ Orderbot::BogusGateway.new
38
+ else
39
+ Orderbot::Gateway.new(api_user_name: api_user_name, api_password: api_password, test: test?)
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,41 @@
1
+ module Workarea
2
+ module Orderbot
3
+ module Authentication
4
+ def token
5
+ response = get_token
6
+
7
+ body = JSON.parse(response.body)
8
+ body['token']
9
+ end
10
+
11
+ private
12
+
13
+ def get_token
14
+ Rails.cache.fetch(token_cache_key, expires_in: 5.minutes) do
15
+ conn = Faraday.new(url: rest_endpoint)
16
+ conn.basic_auth(api_user_name, api_password)
17
+ conn.get do |req|
18
+ req.url '/accesstoken'
19
+ req.headers['Content-Type'] = 'application/json'
20
+ end
21
+ end
22
+ end
23
+
24
+ def api_user_name
25
+ options[:api_user_name]
26
+ end
27
+
28
+ def api_password
29
+ options[:api_password]
30
+ end
31
+
32
+ def test
33
+ options[:test]
34
+ end
35
+
36
+ def token_cache_key
37
+ Digest::MD5.hexdigest "#{api_user_name}#{api_password}#{test}"
38
+ end
39
+ end
40
+ end
41
+ end