workarea-ship_station 1.0.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 (95) 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 +54 -0
  6. data/.gitignore +19 -0
  7. data/.rubocop.yml +3 -0
  8. data/CHANGELOG.md +13 -0
  9. data/Gemfile +17 -0
  10. data/LICENSE +52 -0
  11. data/README.md +89 -0
  12. data/Rakefile +57 -0
  13. data/app/assets/images/workarea/admin/ship_station/.keep +0 -0
  14. data/app/assets/images/workarea/storefront/ship_station/.keep +0 -0
  15. data/app/assets/javascripts/workarea/admin/ship_station/.keep +0 -0
  16. data/app/assets/javascripts/workarea/storefront/ship_station/.keep +0 -0
  17. data/app/assets/stylesheets/workarea/admin/ship_station/.keep +0 -0
  18. data/app/assets/stylesheets/workarea/storefront/ship_station/.keep +0 -0
  19. data/app/controllers/workarea/admin/orders_controller.decorator +50 -0
  20. data/app/controllers/workarea/storefront/ship_station_webhook_controller.rb +52 -0
  21. data/app/helpers/.keep +0 -0
  22. data/app/mailers/.keep +0 -0
  23. data/app/models/.keep +0 -0
  24. data/app/models/workarea/order.decorator +25 -0
  25. data/app/services/workarea/ship_station/order.rb +161 -0
  26. data/app/services/workarea/ship_station/webhook.rb +27 -0
  27. data/app/services/workarea/ship_station/webhook/item_ship_notify.rb +33 -0
  28. data/app/views/.keep +0 -0
  29. data/app/views/workarea/admin/orders/_ship_station.html.haml +12 -0
  30. data/app/views/workarea/admin/orders/_ship_station_bar_actions.html.haml +7 -0
  31. data/app/views/workarea/admin/orders/hold_date.html.haml +27 -0
  32. data/app/workers/workarea/ship_station/save_order.rb +24 -0
  33. data/bin/rails +25 -0
  34. data/config/initializers/appends.rb +9 -0
  35. data/config/initializers/workarea.rb +11 -0
  36. data/config/locales/en.yml +20 -0
  37. data/config/routes.rb +13 -0
  38. data/lib/workarea/ship_station.rb +39 -0
  39. data/lib/workarea/ship_station/bogus_gateway.rb +297 -0
  40. data/lib/workarea/ship_station/engine.rb +10 -0
  41. data/lib/workarea/ship_station/gateway.rb +92 -0
  42. data/lib/workarea/ship_station/response.rb +17 -0
  43. data/lib/workarea/ship_station/version.rb +5 -0
  44. data/test/dummy/Rakefile +6 -0
  45. data/test/dummy/app/assets/config/manifest.js +3 -0
  46. data/test/dummy/app/assets/images/.keep +0 -0
  47. data/test/dummy/app/assets/javascripts/application.js +14 -0
  48. data/test/dummy/app/assets/stylesheets/application.css +15 -0
  49. data/test/dummy/app/controllers/application_controller.rb +2 -0
  50. data/test/dummy/app/controllers/concerns/.keep +0 -0
  51. data/test/dummy/app/helpers/application_helper.rb +2 -0
  52. data/test/dummy/app/jobs/application_job.rb +2 -0
  53. data/test/dummy/app/mailers/application_mailer.rb +4 -0
  54. data/test/dummy/app/models/concerns/.keep +0 -0
  55. data/test/dummy/app/views/layouts/application.html.erb +15 -0
  56. data/test/dummy/app/views/layouts/mailer.html.erb +13 -0
  57. data/test/dummy/app/views/layouts/mailer.text.erb +1 -0
  58. data/test/dummy/bin/bundle +3 -0
  59. data/test/dummy/bin/rails +4 -0
  60. data/test/dummy/bin/rake +4 -0
  61. data/test/dummy/bin/setup +25 -0
  62. data/test/dummy/bin/update +25 -0
  63. data/test/dummy/config.ru +5 -0
  64. data/test/dummy/config/application.rb +34 -0
  65. data/test/dummy/config/boot.rb +5 -0
  66. data/test/dummy/config/environment.rb +5 -0
  67. data/test/dummy/config/environments/development.rb +52 -0
  68. data/test/dummy/config/environments/production.rb +83 -0
  69. data/test/dummy/config/environments/test.rb +45 -0
  70. data/test/dummy/config/initializers/application_controller_renderer.rb +8 -0
  71. data/test/dummy/config/initializers/assets.rb +12 -0
  72. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  73. data/test/dummy/config/initializers/content_security_policy.rb +25 -0
  74. data/test/dummy/config/initializers/cookies_serializer.rb +5 -0
  75. data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  76. data/test/dummy/config/initializers/inflections.rb +16 -0
  77. data/test/dummy/config/initializers/mime_types.rb +4 -0
  78. data/test/dummy/config/initializers/workarea.rb +5 -0
  79. data/test/dummy/config/initializers/wrap_parameters.rb +9 -0
  80. data/test/dummy/config/locales/en.yml +33 -0
  81. data/test/dummy/config/puma.rb +34 -0
  82. data/test/dummy/config/routes.rb +5 -0
  83. data/test/dummy/config/spring.rb +6 -0
  84. data/test/dummy/db/seeds.rb +2 -0
  85. data/test/dummy/lib/assets/.keep +0 -0
  86. data/test/dummy/log/.keep +0 -0
  87. data/test/integration/workarea/admin/ship_station_integration_test.rb +36 -0
  88. data/test/integration/workarea/storefront/ship_station_webhook_test.rb +37 -0
  89. data/test/integration/workarea/storefront/ship_station_webhooks/notify_shipment_test.rb +56 -0
  90. data/test/services/workarea/ship_station/order_test.rb +67 -0
  91. data/test/teaspoon_env.rb +6 -0
  92. data/test/test_helper.rb +10 -0
  93. data/test/workers/workarea/ship_station/save_order_test.rb +16 -0
  94. data/workarea-ship_station.gemspec +20 -0
  95. metadata +157 -0
@@ -0,0 +1,52 @@
1
+ module Workarea
2
+ module Storefront
3
+ class ShipStationWebhookController < Storefront::ApplicationController
4
+ skip_before_action :verify_authenticity_token
5
+
6
+ def event
7
+ body = JSON.parse(request.body.read)
8
+
9
+ # return a bad response if the resource url in the
10
+ # body is not shipstation.
11
+ uri = URI(body["resource_url"])
12
+ return unsuccessful_response unless !!uri.host.match(/.shipstation.com/)
13
+
14
+ begin
15
+ ShipStation::Webhook.process(body)
16
+ successful_response
17
+
18
+ rescue Mongoid::Errors::DocumentNotFound => error
19
+ not_found_response(params: error.params, problem: error.problem)
20
+ rescue ShipStation::Webhook::Error::NotFound, ShipStation::Webhook::Error::UnhandledWebhook => error
21
+ not_found_response(error: "UnhandledWebhook: #{error}")
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def verify_signature
28
+ request_valid = ShipStation::WebhookRequestSignature.valid?(
29
+ request_signature: request.headers['X-Flow-Signature'],
30
+ request_body: request.raw_post
31
+ )
32
+ unsuccessful_response unless request_valid
33
+ end
34
+
35
+ def successful_response
36
+ render json: { status: 200 }
37
+ end
38
+
39
+ def unsuccessful_response
40
+ head :bad_request
41
+ end
42
+
43
+ def error_response(payload)
44
+ render json: payload, status: :unprocessable_entity
45
+ end
46
+
47
+ def not_found_response(payload)
48
+ render json: payload, status: :not_found
49
+ end
50
+ end
51
+ end
52
+ end
data/app/helpers/.keep ADDED
File without changes
data/app/mailers/.keep ADDED
File without changes
data/app/models/.keep ADDED
File without changes
@@ -0,0 +1,25 @@
1
+ module Workarea
2
+ decorate Order, with: :ship_station do
3
+ decorated do
4
+ field :ship_station_order_id, type: String
5
+ field :ship_station_exported_at, type: Time
6
+ field :ship_station_on_hold_until, type: Time
7
+
8
+ index({ ship_station_order_id: 1}, { background: true })
9
+ end
10
+
11
+ def ship_station_exported?
12
+ !!ship_station_exported_at
13
+ end
14
+
15
+ def ship_station_on_hold?
16
+ !!ship_station_on_hold_until
17
+ end
18
+
19
+ def set_ship_station_exported_at!
20
+ update!(
21
+ ship_station_exported_at: Time.current
22
+ )
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,161 @@
1
+ module Workarea
2
+ module ShipStation
3
+ class Order
4
+ module ProductImageUrl
5
+ include Workarea::ApplicationHelper
6
+ include Workarea::I18n::DefaultUrlOptions
7
+ include ActionView::Helpers::AssetUrlHelper
8
+ include Core::Engine.routes.url_helpers
9
+ extend self
10
+
11
+ def mounted_core
12
+ self
13
+ end
14
+ end
15
+
16
+ attr_reader :order
17
+
18
+ def initialize(order_id)
19
+ wa_order = Workarea::Order.find(order_id)
20
+ @order = Workarea::Storefront::OrderViewModel.new(wa_order)
21
+ end
22
+
23
+ def to_h
24
+ {
25
+ orderNumber: order.id,
26
+ orderKey: order.id,
27
+ orderDate: order.placed_at,
28
+ paymentDate: order.placed_at,
29
+ orderStatus: 'awaiting_shipment',
30
+ customerUsername: order.email,
31
+ customerEmail: order.email,
32
+ billTo: address(order.billing_address),
33
+ shipTo: address(order.shipping_address),
34
+ items: items,
35
+ amountPaid: order.total_price.to_s,
36
+ taxAmount: order.tax_total.to_s,
37
+ shippingAmount: order.shipping_total.to_s,
38
+ paymentMethod: payment_methods,
39
+ requestedShippingService: shipping_service_name,
40
+ serviceCode: shipping_service_code,
41
+ shipByDate: ship_by_date
42
+ }
43
+ end
44
+
45
+ private
46
+
47
+ def ship_by_date
48
+ return unless Workarea::ShipStation.config.ship_by_date_lead_days.present?
49
+
50
+ Time.current + Workarea::ShipStation.config.ship_by_date_lead_days.days
51
+ end
52
+
53
+ def shipping
54
+ @shipping = Workarea::Shipping.find_by_order(order.id)
55
+ end
56
+
57
+ def shipping_service_code
58
+ return unless shipping.present?
59
+ shipping.shipping_service.service_code
60
+ end
61
+
62
+ def shipping_service_name
63
+ return unless shipping.present?
64
+ shipping.shipping_service.name
65
+ end
66
+
67
+ def payment
68
+ @payment = Workarea::Payment.find(order.id)
69
+ end
70
+
71
+ def address(obj)
72
+ return unless obj.present?
73
+ {
74
+ name: obj.first_name + ' ' + obj.last_name,
75
+ company: obj.company,
76
+ street1: obj.street,
77
+ street2: obj.street_2,
78
+ city: obj.city,
79
+ state: obj.region,
80
+ postalCode: obj.postal_code,
81
+ country: obj.country.alpha2,
82
+ phone: obj.phone_number
83
+ }
84
+ end
85
+
86
+ def items
87
+ order_items = order.items.map do |order_item|
88
+
89
+ hash = {
90
+ lineItemKey: order_item.id,
91
+ sku: order_item.sku,
92
+ name: order_item.product_name,
93
+ imageUrl: ProductImageUrl.product_image_url(order_item.image, :detail),
94
+ quantity: order_item.quantity,
95
+ unitPrice: order_item.original_unit_price.to_f,
96
+ taxAmount: item_tax(order_item).to_f,
97
+ options: item_options(order_item),
98
+ adjustment: false
99
+ }
100
+
101
+ item_shipping_sku = shipping_sku(order_item.sku)
102
+ if item_shipping_sku.present?
103
+ weight_data = {
104
+ weight: {
105
+ value: item_shipping_sku.weight,
106
+ units: item_shipping_sku.weight_units.to_s
107
+ }
108
+ }
109
+
110
+ hash.merge!(weight_data)
111
+ end
112
+
113
+ hash
114
+ end
115
+
116
+ order_items + discount_items
117
+ end
118
+
119
+ def shipping_sku(sku)
120
+ return unless shipping.present?
121
+ Workarea::Shipping::Sku.find(sku) rescue nil
122
+ end
123
+
124
+ def item_tax(order_item)
125
+ return unless shipping.present?
126
+ item = shipping.price_adjustments.adjusting('tax').detect { |pa| pa["data"]["order_item_id"].to_s == order_item.id.to_s }
127
+ return 0 unless item.present?
128
+ item.amount.to_f
129
+ end
130
+
131
+ def item_options(order_item)
132
+ order_item.details.map { |k, v| { name: k, value: v } }
133
+ end
134
+
135
+ def payment_methods
136
+ payment.tenders.map(&:slug).join(',')
137
+ end
138
+
139
+ def discount_items
140
+ discounts = order.price_adjustments.select { |p| p.discount? }
141
+ return [] unless discounts.present?
142
+
143
+ discounts.map do |d|
144
+ {
145
+ lineItemKey: nil,
146
+ sku: "DISCOUNT CODE",
147
+ name: d.description,
148
+ imageUrl: nil,
149
+ weight: {
150
+ value: 0,
151
+ units: "ounces"
152
+ },
153
+ quantity: 1,
154
+ unitPrice: d.amount.to_f,
155
+ adjustment: true
156
+ }
157
+ end
158
+ end
159
+ end
160
+ end
161
+ end
@@ -0,0 +1,27 @@
1
+ module Workarea
2
+ module ShipStation
3
+ class Webhook
4
+ class Error < RuntimeError; end
5
+ class Error::NotFound < RuntimeError; end
6
+ class Error::UnhandledWebhook < RuntimeError; end
7
+
8
+ def self.process(attrs)
9
+ resource = attrs["resource_type"]
10
+ begin
11
+ klass = "Workarea::ShipStation::Webhook::#{resource.downcase.classify}".constantize
12
+ rescue NameError => _error
13
+ raise Error::UnhandledWebhook, "no class defined to handle #{resource}"
14
+ end
15
+ klass.new(attrs).process
16
+ end
17
+
18
+ attr_reader :attrs
19
+
20
+ def initialize(attrs)
21
+ @attrs = attrs
22
+ end
23
+
24
+ def process; end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,33 @@
1
+ module Workarea
2
+ module ShipStation
3
+ class Webhook
4
+ class ItemShipNotify < Webhook
5
+ class ShipStationItemShipNotifyError < StandardError; end
6
+
7
+ def process
8
+ uri = URI(attrs["resource_url"])
9
+ rest_endpoint = "https://" + uri.host
10
+ query_hash = Rack::Utils.parse_query(uri.query)
11
+
12
+ response = gateway(rest_endpoint).get_shipping(query_hash)
13
+
14
+ raise ShipStationItemShipNotifyError, response.body["ExceptionMessage"] unless response.success?
15
+
16
+ response.body["shipments"].each do |shipment|
17
+ fulfillment = Workarea::Fulfillment.find(shipment["orderKey"])
18
+ tracking_number = shipment["trackingNumber"]
19
+ items = shipment["shipmentItems"].map { |item| { id: item["lineItemKey"], quantity: item["quantity"] } }
20
+
21
+ fulfillment.ship_items(tracking_number, items)
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def gateway(rest_endpoint)
28
+ Workarea::ShipStation.gateway(rest_endpoint)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
data/app/views/.keep ADDED
File without changes
@@ -0,0 +1,12 @@
1
+ %li
2
+ %strong= t('workarea.admin.orders.attributes.checkout.ship_station_exported_at')
3
+ - if @order.ship_station_exported?
4
+ = local_time_ago(@order.ship_station_exported_at)
5
+ - else
6
+ "-"
7
+ %li
8
+ %strong= t('workarea.admin.orders.attributes.checkout.ship_station_order_id')
9
+ = @order.ship_station_order_id.presence || "-"
10
+ %li
11
+ %strong= t('workarea.admin.orders.attributes.checkout.ship_station_on_hold_until')
12
+ = @order.ship_station_on_hold_until.presence || "-"
@@ -0,0 +1,7 @@
1
+ - if @order.ship_station_order_id.present?
2
+ .grid__cell
3
+ %button.workflow-bar__button= link_to t('workarea.admin.orders.place_on_hold'), hold_date_order_path
4
+ - if @order.ship_station_on_hold?
5
+ .grid__cell
6
+ = form_tag clear_hold_date_order_path, method: :patch do
7
+ %button.workflow-bar__button{ type: :submit }= t('workarea.admin.orders.clear_on_hold')
@@ -0,0 +1,27 @@
1
+ - @page_title = @order.name
2
+
3
+ .view
4
+ .view__header
5
+ .grid.grid--middle.grid--right
6
+ .grid__cell.grid__cell--50
7
+ .view__heading
8
+ = link_to_index_for(@order)
9
+ %h1= t('workarea.admin.orders.order_hold', id: @order.name)
10
+ .grid__cell.grid__cell--25
11
+ = render_aux_navigation_for(@order)
12
+
13
+ .view__container.view__container--narrow
14
+ .grid
15
+ .grid__cell.grid__cell--80
16
+ %p= t('workarea.admin.orders.hold_until_note')
17
+
18
+ - if @order.ship_station_on_hold?
19
+ %strong= t('workarea.admin.orders.on_hold_until', date: @order.ship_station_on_hold_until.try(:to_s, :date_only))
20
+
21
+ = form_tag save_hold_date_order_path, method: 'patch' do
22
+ .property__name
23
+ = label_tag 'hold_until', t('workarea.admin.orders.hold_until_date'), class: 'property__name'
24
+ = datetime_picker_tag 'hold_until', @order.ship_station_on_hold_until.try(:to_s, :date_only), class: 'text-box text-box', data: { datepicker_field: { } }
25
+ .workflow-bar
26
+ .grid.grid--auto.grid--right.grid--middle
27
+ .grid__cell= button_tag t('workarea.admin.orders.place_on_hold'), value: 'place_on_hold', class: 'workflow-bar__button workflow-bar__button--create'
@@ -0,0 +1,24 @@
1
+ module Workarea
2
+ module ShipStation
3
+ class SaveOrder
4
+ class ShipStationSaveOrderError < 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
+ order = Workarea::Order.find(id)
15
+ shipstation_details = Workarea::ShipStation::Order.new(id).to_h
16
+ response = ShipStation.gateway.create_order(shipstation_details)
17
+
18
+ raise ShipStationSaveOrderError, response.body["ExceptionMessage"] unless response.success?
19
+ order.ship_station_order_id = response.body["orderId"]
20
+ order.set_ship_station_exported_at!
21
+ end
22
+ end
23
+ end
24
+ 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/ship_station/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,9 @@
1
+ Workarea.append_partials(
2
+ 'admin.order_attributes',
3
+ 'workarea/admin/orders/ship_station'
4
+ )
5
+
6
+ Workarea.append_partials(
7
+ 'admin.order_show_workflow_bar_left',
8
+ 'workarea/admin/orders/ship_station_bar_actions'
9
+ )
@@ -0,0 +1,11 @@
1
+ Workarea.configure do |config|
2
+ config.ship_station = ActiveSupport::Configurable::Configuration.new
3
+ config.ship_station.api_timeout = 10
4
+ config.ship_station.open_timeout = 10
5
+
6
+ # setting this config value will set a ship_by_date for new orders.
7
+ # This value will be added to the current date and sent in the ship_by_date field.
8
+ #
9
+ # This value must be an integer.
10
+ config.ship_station.ship_by_date_lead_days = nil
11
+ end
@@ -0,0 +1,20 @@
1
+ en:
2
+ workarea:
3
+ admin:
4
+ orders:
5
+ attributes:
6
+ checkout:
7
+ ship_station_exported_at: Exported To ShipStation At
8
+ ship_station_order_id: ShipStation Order ID
9
+ ship_station_order_id: ShipStation Order ID
10
+ ship_station_on_hold_until: ShipStation Hold Until
11
+ clear_on_hold: Clear Order Hold
12
+ clear_on_hold_success: The order's hold has been cleared.
13
+ clear_on_hold_error: The order's hold could not be cleard. Please try again.
14
+ hold_until_date: Hold Until Date
15
+ hold_until_note: Setting a hold date will set this order's status to "on hold" in ShipStation.
16
+ on_hold_until: This order is currently on hold until %{date}
17
+ order_hold: Place %{id} on Hold
18
+ place_on_hold: Place Order on Hold
19
+ placed_on_hold_success: The order has been placed on hold.
20
+ placed_on_hold_error: The order could not be placed on hold. Please try again.