solidus_easypost 1.0.3 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (97) hide show
  1. checksums.yaml +5 -5
  2. data/.circleci/config.yml +41 -0
  3. data/.gem_release.yml +5 -0
  4. data/.github/stale.yml +17 -0
  5. data/.github_changelog_generator +2 -0
  6. data/.gitignore +21 -0
  7. data/.rspec +2 -0
  8. data/.rubocop.yml +7 -0
  9. data/.rubocop_todo.yml +36 -0
  10. data/CHANGELOG.md +113 -0
  11. data/Gemfile +33 -0
  12. data/LICENSE +26 -0
  13. data/README.md +194 -0
  14. data/Rakefile +6 -0
  15. data/app/controllers/spree/admin/postage_labels_controller.rb +20 -0
  16. data/app/decorators/models/solidus_easypost/spree/carton_decorator.rb +38 -0
  17. data/app/decorators/models/solidus_easypost/spree/shipment_decorator.rb +47 -0
  18. data/app/decorators/models/solidus_easypost/spree/shipping_rate_decorator.rb +13 -0
  19. data/app/models/solidus_easypost/parcel_dimension.rb +34 -0
  20. data/app/models/solidus_easypost/return_authorization.rb +27 -0
  21. data/app/overrides/spree/admin/orders/_shipment/add_postage_label.html.erb.deface +10 -0
  22. data/bin/console +17 -0
  23. data/bin/rails +7 -0
  24. data/bin/rails-engine +13 -0
  25. data/bin/rails-sandbox +16 -0
  26. data/bin/rake +7 -0
  27. data/bin/sandbox +86 -0
  28. data/bin/setup +8 -0
  29. data/config/initializers/webhooks.rb +7 -0
  30. data/config/locales/en.yml +8 -0
  31. data/config/routes.rb +9 -0
  32. data/db/migrate/20140515024440_add_easy_post_fields_to_shipping_rate.rb +9 -0
  33. data/db/migrate/20201025110912_add_tracker_id_to_cartons.rb +6 -0
  34. data/lib/generators/solidus_easypost/install/install_generator.rb +28 -0
  35. data/lib/generators/solidus_easypost/install/templates/initializer.rb +26 -0
  36. data/lib/solidus_easypost/address_builder.rb +45 -0
  37. data/lib/solidus_easypost/calculator/base_dimension_calculator.rb +28 -0
  38. data/lib/solidus_easypost/calculator/weight_dimension_calculator.rb +22 -0
  39. data/lib/solidus_easypost/configuration.rb +34 -0
  40. data/lib/solidus_easypost/engine.rb +19 -0
  41. data/lib/solidus_easypost/errors/unknown_partial_resource_error.rb +11 -0
  42. data/lib/solidus_easypost/estimator.rb +37 -0
  43. data/lib/solidus_easypost/parcel_builder.rb +27 -0
  44. data/lib/solidus_easypost/shipment_builder.rb +32 -0
  45. data/lib/solidus_easypost/shipping_method_selector.rb +17 -0
  46. data/lib/solidus_easypost/shipping_rate_calculator.rb +9 -0
  47. data/lib/solidus_easypost/testing_support/factories/address_factory.rb +10 -0
  48. data/lib/solidus_easypost/testing_support/factories/product_factory.rb +7 -0
  49. data/lib/solidus_easypost/testing_support/factories/shipment_factory.rb +30 -0
  50. data/lib/solidus_easypost/testing_support/factories/shipping_method_factory.rb +8 -0
  51. data/lib/solidus_easypost/testing_support/factories/stock_location_factory.rb +10 -0
  52. data/lib/solidus_easypost/testing_support/factories/variant_factory.rb +7 -0
  53. data/lib/solidus_easypost/testing_support/factories.rb +3 -0
  54. data/lib/solidus_easypost/tracker_webhook_handler.rb +14 -0
  55. data/lib/solidus_easypost/version.rb +5 -0
  56. data/lib/solidus_easypost.rb +32 -0
  57. data/solidus_easypost.gemspec +41 -0
  58. data/spec/cassettes/address_builder/from_address.yml +65 -0
  59. data/spec/cassettes/address_builder/from_stock_location.yml +65 -0
  60. data/spec/cassettes/estimator.yml +269 -0
  61. data/spec/cassettes/integration/checkout.yml +377 -0
  62. data/spec/cassettes/integration/order_shipping/with_purchase_labels.yml +373 -0
  63. data/spec/cassettes/integration/order_shipping/without_purchase_labels.yml +251 -0
  64. data/spec/cassettes/parcel_builder/from_package.yml +63 -0
  65. data/spec/cassettes/parcel_builder/from_return_authorization.yml +63 -0
  66. data/spec/cassettes/postage_labels/show.yml +72 -0
  67. data/spec/cassettes/return_authorization.yml +310 -0
  68. data/spec/cassettes/shipment_builder/from_package.yml +251 -0
  69. data/spec/cassettes/shipment_builder/from_return_authorization.yml +251 -0
  70. data/spec/cassettes/shipment_builder/from_shipment.yml +250 -0
  71. data/spec/controllers/spree/admin/postage_labels_controller_spec.rb +18 -0
  72. data/spec/features/admin/postage_labels_spec.rb +18 -0
  73. data/spec/integration/checkout_spec.rb +45 -0
  74. data/spec/integration/order_shipping_spec.rb +33 -0
  75. data/spec/models/solidus_easypost/parcel_dimension_spec.rb +80 -0
  76. data/spec/models/solidus_easypost/return_authorization_spec.rb +17 -0
  77. data/spec/models/spree/carton_spec.rb +57 -0
  78. data/spec/models/spree/shipment_spec.rb +69 -0
  79. data/spec/models/spree/shipping_rate_spec.rb +23 -0
  80. data/spec/solidus_easypost/address_builder_spec.rb +17 -0
  81. data/spec/solidus_easypost/calculator/weight_dimension_calculator_spec.rb +39 -0
  82. data/spec/solidus_easypost/estimator_spec.rb +52 -0
  83. data/spec/solidus_easypost/parcel_builder_spec.rb +113 -0
  84. data/spec/solidus_easypost/shipment_builder_spec.rb +28 -0
  85. data/spec/solidus_easypost/shipping_method_selector_spec.rb +33 -0
  86. data/spec/solidus_easypost/shipping_rate_calculator_spec.rb +12 -0
  87. data/spec/solidus_easypost/tracker_webhook_handler_spec.rb +58 -0
  88. data/spec/solidus_easypost_spec.rb +19 -0
  89. data/spec/spec_helper.rb +31 -0
  90. data/spec/support/easypost.rb +5 -0
  91. data/spec/support/factory_bot.rb +3 -0
  92. data/spec/support/helpers/api_stubs.rb +40 -0
  93. data/spec/support/helpers/configuration.rb +28 -0
  94. data/spec/support/helpers/shipping_methods.rb +26 -0
  95. data/spec/support/solidus.rb +1 -0
  96. data/spec/support/vcr.rb +12 -0
  97. metadata +170 -142
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusEasypost
4
+ class ParcelDimension
5
+ def initialize(**params)
6
+ raise ArgumentError, 'The weight param is mandatory!' unless valid_value?(value: params[:weight])
7
+
8
+ @weight = params[:weight]
9
+ @width = params[:width]
10
+ @height = params[:height]
11
+ @depth = params[:depth]
12
+ end
13
+
14
+ def to_h
15
+ hash = {
16
+ weight: weight
17
+ }
18
+
19
+ hash[:width] = width if valid_value?(value: width)
20
+ hash[:height] = height if valid_value?(value: height)
21
+ hash[:length] = depth if valid_value?(value: depth)
22
+
23
+ hash
24
+ end
25
+
26
+ private
27
+
28
+ def valid_value?(value:)
29
+ value.present? && !value.zero?
30
+ end
31
+
32
+ attr_reader :width, :height, :depth, :weight
33
+ end
34
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+
5
+ module SolidusEasypost
6
+ class ReturnAuthorization
7
+ extend Forwardable
8
+
9
+ attr_reader :return_authorization
10
+
11
+ def_delegators :@return_authorization, :stock_location, :order, :inventory_units
12
+
13
+ def initialize(return_authorization)
14
+ @return_authorization = return_authorization
15
+ end
16
+
17
+ def easypost_shipment
18
+ @easypost_shipment ||= ShipmentBuilder.from_return_authorization(self)
19
+ end
20
+
21
+ def return_label(rate)
22
+ easypost_shipment.buy(rate) unless easypost_shipment.postage_label
23
+
24
+ easypost_shipment.postage_label
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,10 @@
1
+ <!-- insert_bottom '[data-hook="stock-contents"] tbody' -->
2
+
3
+ <% if shipment.selected_easy_post_rate_id && shipment.shipped? %>
4
+ <tr class="postage-label">
5
+ <td colspan="5">
6
+ <strong><%= t('spree.postage_label') %>:</strong>
7
+ <%= link_to_with_icon('external-link', t('spree.open_postage_label'), admin_shipment_postage_label_path(shipment)) %>
8
+ </td>
9
+ </tr>
10
+ <% end %>
data/bin/console ADDED
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # frozen_string_literal: true
4
+
5
+ require "bundler/setup"
6
+ require "solidus_easypost"
7
+
8
+ # You can add fixtures and/or initialization code here to make experimenting
9
+ # with your gem easier. You can also use a different console, if you like.
10
+ $LOAD_PATH.unshift(*Dir["#{__dir__}/../app/*"])
11
+
12
+ # (If you use this, don't forget to add pry to your Gemfile!)
13
+ # require "pry"
14
+ # Pry.start
15
+
16
+ require "irb"
17
+ IRB.start(__FILE__)
data/bin/rails ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ if %w[g generate].include? ARGV.first
4
+ exec "#{__dir__}/rails-engine", *ARGV
5
+ else
6
+ exec "#{__dir__}/rails-sandbox", *ARGV
7
+ end
data/bin/rails-engine ADDED
@@ -0,0 +1,13 @@
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/solidus_easypost/engine', __dir__)
7
+
8
+ # Set up gems listed in the Gemfile.
9
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
10
+ require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
11
+
12
+ require 'rails/all'
13
+ require 'rails/engine/commands'
data/bin/rails-sandbox ADDED
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ app_root = 'sandbox'
4
+
5
+ unless File.exist? "#{app_root}/bin/rails"
6
+ warn 'Creating the sandbox app...'
7
+ Dir.chdir "#{__dir__}/.." do
8
+ system "#{__dir__}/sandbox" or begin
9
+ warn 'Automatic creation of the sandbox app failed'
10
+ exit 1
11
+ end
12
+ end
13
+ end
14
+
15
+ Dir.chdir app_root
16
+ exec 'bin/rails', *ARGV
data/bin/rake ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "rubygems"
5
+ require "bundler/setup"
6
+
7
+ load Gem.bin_path("rake", "rake")
data/bin/sandbox ADDED
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env bash
2
+
3
+ set -e
4
+
5
+ case "$DB" in
6
+ postgres|postgresql)
7
+ RAILSDB="postgresql"
8
+ ;;
9
+ mysql)
10
+ RAILSDB="mysql"
11
+ ;;
12
+ sqlite|'')
13
+ RAILSDB="sqlite3"
14
+ ;;
15
+ *)
16
+ echo "Invalid DB specified: $DB"
17
+ exit 1
18
+ ;;
19
+ esac
20
+
21
+ if [ ! -z $SOLIDUS_BRANCH ]
22
+ then
23
+ BRANCH=$SOLIDUS_BRANCH
24
+ else
25
+ BRANCH="master"
26
+ fi
27
+
28
+ extension_name="solidus_easypost"
29
+
30
+ # Stay away from the bundler env of the containing extension.
31
+ function unbundled {
32
+ ruby -rbundler -e'b = proc {system *ARGV}; Bundler.respond_to?(:with_unbundled_env) ? Bundler.with_unbundled_env(&b) : Bundler.with_clean_env(&b)' -- $@
33
+ }
34
+
35
+ rm -rf ./sandbox
36
+ unbundled bundle exec rails new sandbox --database="$RAILSDB" \
37
+ --skip-bundle \
38
+ --skip-git \
39
+ --skip-keeps \
40
+ --skip-rc \
41
+ --skip-spring \
42
+ --skip-test \
43
+ --skip-javascript
44
+
45
+ if [ ! -d "sandbox" ]; then
46
+ echo 'sandbox rails application failed'
47
+ exit 1
48
+ fi
49
+
50
+ cd ./sandbox
51
+ cat <<RUBY >> Gemfile
52
+ gem 'solidus', github: 'solidusio/solidus', branch: '$BRANCH'
53
+ gem 'solidus_auth_devise', '>= 2.1.0'
54
+ gem 'rails-i18n'
55
+ gem 'solidus_i18n'
56
+
57
+ gem '$extension_name', path: '..'
58
+
59
+ group :test, :development do
60
+ platforms :mri do
61
+ gem 'pry-byebug'
62
+ end
63
+ end
64
+ RUBY
65
+
66
+ unbundled bundle install --gemfile Gemfile
67
+
68
+ unbundled bundle exec rake db:drop db:create
69
+
70
+ unbundled bundle exec rails generate spree:install \
71
+ --auto-accept \
72
+ --user_class=Spree::User \
73
+ --enforce_available_locales=true \
74
+ --with-authentication=false \
75
+ --payment-method=none \
76
+ $@
77
+
78
+ unbundled bundle exec rails generate solidus:auth:install
79
+ unbundled bundle exec rails generate ${extension_name}:install
80
+
81
+ echo
82
+ echo "🚀 Sandbox app successfully created for $extension_name!"
83
+ echo "🚀 Using $RAILSDB and Solidus $BRANCH"
84
+ echo "🚀 Use 'export DB=[postgres|mysql|sqlite]' to control the DB adapter"
85
+ echo "🚀 Use 'export SOLIDUS_BRANCH=<BRANCH-NAME>' to control the Solidus version"
86
+ echo "🚀 This app is intended for test purposes."
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ gem install bundler --conservative
7
+ bundle update
8
+ bin/rake clobber
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ if defined?(SolidusWebhooks)
4
+ SolidusWebhooks.config.register_webhook_handler :easypost_trackers, ->(payload) do
5
+ SolidusEasypost.configuration.webhook_handler_class.call(payload)
6
+ end
7
+ end
@@ -0,0 +1,8 @@
1
+ en:
2
+ spree:
3
+ postage_label: 'Postage Label'
4
+ open_postage_label: 'Open'
5
+ admin:
6
+ postage_labels:
7
+ show:
8
+ no_postage_label: 'No postage label available for shipment %{shipment_number}.'
data/config/routes.rb ADDED
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ Spree::Core::Engine.routes.draw do
4
+ namespace :admin do
5
+ resources :shipments, only: [] do
6
+ resource :postage_label, only: :show
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddEasyPostFieldsToShippingRate < SolidusSupport::Migration[4.2]
4
+ def change
5
+ add_column :spree_shipping_rates, :name, :string
6
+ add_column :spree_shipping_rates, :easy_post_shipment_id, :string
7
+ add_column :spree_shipping_rates, :easy_post_rate_id, :string
8
+ end
9
+ end
@@ -0,0 +1,6 @@
1
+ class AddTrackerIdToCartons < ActiveRecord::Migration[5.2]
2
+ def change
3
+ add_column :spree_cartons, :easy_post_tracker_id, :string
4
+ add_index :spree_cartons, :easy_post_tracker_id
5
+ end
6
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusEasypost
4
+ module Generators
5
+ class InstallGenerator < Rails::Generators::Base
6
+ source_root File.expand_path('templates', __dir__)
7
+
8
+ class_option :auto_run_migrations, type: :boolean, default: false
9
+
10
+ def copy_initializer
11
+ template 'initializer.rb', 'config/initializers/solidus_easypost.rb'
12
+ end
13
+
14
+ def add_migrations
15
+ run 'bin/rails railties:install:migrations FROM=solidus_easypost'
16
+ end
17
+
18
+ def run_migrations
19
+ run_migrations = options[:auto_run_migrations] || ['', 'y', 'Y'].include?(ask('Would you like to run the migrations now? [Y/n]'))
20
+ if run_migrations
21
+ run 'bin/rails db:migrate'
22
+ else
23
+ puts 'Skipping bin/rails db:migrate, don\'t forget to run it!' # rubocop:disable Rails/Output
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ EasyPost.api_key = 'YOUR_API_KEY_HERE'
4
+
5
+ SolidusEasypost.configure do |config|
6
+ # Purchase labels from EasyPost when shipping shipments in Solidus?
7
+ # config.purchase_labels = true
8
+
9
+ # Create a tracker in EasyPost and receive webhooks for all cartons?
10
+ # config.track_all_cartons = false
11
+
12
+ # A class that responds to `#compute`, accepting an `EasyPost::Rate`
13
+ # instance and returning the cost for that rate.
14
+ # config.shipping_rate_calculator_class = 'SolidusEasypost::ShippingRateCalculator'
15
+
16
+ # A class that responds to `#shipping_method_for`, accepting an
17
+ # `EasyPost::Rate` instance and returning the shipping method for
18
+ # that rate.
19
+ # config.shipping_method_selector_class = 'SolidusEasypost::ShippingMethodSelector'
20
+
21
+ # A class that responds to '#compute', accepting a `SolidusEasypost::ReturnAuthorization`
22
+ # instance or a `Spree::Stock::Package` instance and returing the `SolidusEasypost::ParcelDimension` object.
23
+ # The `SolidusEasypost::Calculator::BaseDimensionCalculator` class can be extended to have a common
24
+ # functionality.
25
+ # config.parcel_dimension_calculator_class = 'SolidusEasypost::Calculator::WeightDimensionCalculator'
26
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusEasypost
4
+ class AddressBuilder
5
+ class << self
6
+ def from_address(address)
7
+ attributes = {
8
+ street1: address.address1,
9
+ street2: address.address2,
10
+ city: address.city,
11
+ zip: address.zipcode,
12
+ phone: address.phone
13
+ }
14
+
15
+ attributes[:company] = address.company if address.respond_to?(:company)
16
+ attributes[:name] = if address.respond_to?(:name)
17
+ address.name
18
+ elsif respond_to?(:full_name)
19
+ address.full_name
20
+ end
21
+ attributes[:state] = address.state ? address.state.abbr : address.state_name
22
+ attributes[:country] = address.country&.iso
23
+
24
+ ::EasyPost::Address.create attributes
25
+ end
26
+
27
+ def from_stock_location(stock_location)
28
+ attributes = {
29
+ street1: stock_location.address1,
30
+ street2: stock_location.address2,
31
+ city: stock_location.city,
32
+ zip: stock_location.zipcode,
33
+ phone: stock_location.phone,
34
+ name: stock_location.name,
35
+ company: stock_location.name
36
+ }
37
+
38
+ attributes[:state] = stock_location.state ? stock_location.state.abbr : stock_location.state_name
39
+ attributes[:country] = stock_location.country&.iso
40
+
41
+ ::EasyPost::Address.create attributes
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusEasypost
4
+ module Calculator
5
+ class BaseDimensionCalculator
6
+ def compute(resource)
7
+ case resource
8
+ when ::SolidusEasypost::ReturnAuthorization
9
+ compute_for_return_authorization(resource)
10
+ when ::Spree::Stock::Package
11
+ compute_for_package(resource)
12
+ else
13
+ raise SolidusEasypost::Errors::UnknownPartialResourceError
14
+ end
15
+ end
16
+
17
+ protected
18
+
19
+ def compute_for_return_authorization(return_authorization)
20
+ raise NotImplementedError
21
+ end
22
+
23
+ def compute_for_package(package)
24
+ raise NotImplementedError
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusEasypost
4
+ module Calculator
5
+ class WeightDimensionCalculator < BaseDimensionCalculator
6
+ protected
7
+
8
+ def compute_for_return_authorization(return_authorization)
9
+ total_weight = return_authorization.inventory_units.joins(:variant).sum(:weight)
10
+ SolidusEasypost::ParcelDimension.new(weight: total_weight)
11
+ end
12
+
13
+ def compute_for_package(package)
14
+ total_weight = package.contents.sum do |item|
15
+ item.quantity * item.variant.weight
16
+ end
17
+
18
+ SolidusEasypost::ParcelDimension.new(weight: total_weight)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusEasypost
4
+ class Configuration
5
+ attr_accessor :purchase_labels, :track_all_cartons
6
+ attr_writer :shipping_rate_calculator_class, :shipping_method_selector_class, :parcel_dimension_calculator_class,
7
+ :webhook_handler_class
8
+
9
+ def initialize
10
+ self.purchase_labels = true
11
+ self.track_all_cartons = false
12
+ end
13
+
14
+ def shipping_rate_calculator_class
15
+ @shipping_rate_calculator_class ||= 'SolidusEasypost::ShippingRateCalculator'
16
+ @shipping_rate_calculator_class.constantize
17
+ end
18
+
19
+ def shipping_method_selector_class
20
+ @shipping_method_selector_class ||= 'SolidusEasypost::ShippingMethodSelector'
21
+ @shipping_method_selector_class.constantize
22
+ end
23
+
24
+ def parcel_dimension_calculator_class
25
+ @parcel_dimension_calculator_class ||= 'SolidusEasypost::Calculator::WeightDimensionCalculator'
26
+ @parcel_dimension_calculator_class.constantize
27
+ end
28
+
29
+ def webhook_handler_class
30
+ @webhook_handler_class ||= 'SolidusEasypost::TrackerWebhookHandler'
31
+ @webhook_handler_class.constantize
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spree/core'
4
+ require 'solidus_easypost'
5
+
6
+ module SolidusEasypost
7
+ class Engine < Rails::Engine
8
+ include SolidusSupport::EngineExtensions
9
+
10
+ isolate_namespace ::Spree
11
+
12
+ engine_name 'solidus_easypost'
13
+
14
+ # use rspec for tests
15
+ config.generators do |g|
16
+ g.test_framework :rspec
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusEasypost
4
+ module Errors
5
+ class UnknownPartialResourceError < StandardError
6
+ def message
7
+ 'The passed resource must be a Spree::ReturnAuthorization or a Spree::Stock::Package!'
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusEasypost
4
+ class Estimator
5
+ def shipping_rates(package, _frontend_only = true)
6
+ easypost_rates = ShipmentBuilder.from_package(package).rates.sort_by(&:rate)
7
+
8
+ shipping_rates = easypost_rates.map { |rate| build_shipping_rate(rate) }.compact
9
+ shipping_rates.min_by(&:cost)&.selected = true
10
+
11
+ shipping_rates
12
+ end
13
+
14
+ private
15
+
16
+ def build_shipping_rate(rate)
17
+ shipping_method = shipping_method_selector.shipping_method_for(rate)
18
+ return unless shipping_method.available_to_users?
19
+
20
+ ::Spree::ShippingRate.new(
21
+ name: "#{rate.carrier} #{rate.service}",
22
+ cost: shipping_rate_calculator.compute(rate),
23
+ easy_post_shipment_id: rate.shipment_id,
24
+ easy_post_rate_id: rate.id,
25
+ shipping_method: shipping_method,
26
+ )
27
+ end
28
+
29
+ def shipping_rate_calculator
30
+ @shipping_rate_calculator ||= SolidusEasypost.configuration.shipping_rate_calculator_class.new
31
+ end
32
+
33
+ def shipping_method_selector
34
+ @shipping_method_selector ||= SolidusEasypost.configuration.shipping_method_selector_class.new
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusEasypost
4
+ class ParcelBuilder
5
+ class << self
6
+ def from_package(package)
7
+ parcel_dimension = SolidusEasypost
8
+ .configuration
9
+ .parcel_dimension_calculator_class
10
+ .new
11
+ .compute(package)
12
+
13
+ ::EasyPost::Parcel.create(parcel_dimension.to_h)
14
+ end
15
+
16
+ def from_return_authorization(return_authorization)
17
+ parcel_dimension = SolidusEasypost
18
+ .configuration
19
+ .parcel_dimension_calculator_class
20
+ .new
21
+ .compute(return_authorization)
22
+
23
+ ::EasyPost::Parcel.create(parcel_dimension.to_h)
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusEasypost
4
+ class ShipmentBuilder
5
+ class << self
6
+ def from_package(package)
7
+ ::EasyPost::Shipment.create(
8
+ to_address: AddressBuilder.from_address(package.order.ship_address),
9
+ from_address: AddressBuilder.from_stock_location(package.stock_location),
10
+ parcel: ParcelBuilder.from_package(package),
11
+ )
12
+ end
13
+
14
+ def from_shipment(shipment)
15
+ ::EasyPost::Shipment.create(
16
+ to_address: AddressBuilder.from_address(shipment.order.ship_address),
17
+ from_address: AddressBuilder.from_stock_location(shipment.stock_location),
18
+ parcel: ParcelBuilder.from_package(shipment.to_package),
19
+ )
20
+ end
21
+
22
+ def from_return_authorization(return_authorization)
23
+ ::EasyPost::Shipment.create(
24
+ from_address: AddressBuilder.from_stock_location(return_authorization.stock_location),
25
+ to_address: AddressBuilder.from_address(return_authorization.order.ship_address),
26
+ parcel: ParcelBuilder.from_return_authorization(return_authorization),
27
+ is_return: true
28
+ )
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusEasypost
4
+ class ShippingMethodSelector
5
+ def shipping_method_for(rate)
6
+ ::Spree::ShippingMethod.find_or_create_by(
7
+ carrier: rate.carrier,
8
+ service_level: rate.service,
9
+ ) do |shipping_method|
10
+ shipping_method.name = "#{rate.carrier} #{rate.service}"
11
+ shipping_method.calculator = ::Spree::Calculator::Shipping::FlatRate.create
12
+ shipping_method.shipping_categories = [::Spree::ShippingCategory.first]
13
+ shipping_method.available_to_users = false
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusEasypost
4
+ class ShippingRateCalculator
5
+ def compute(rate)
6
+ rate.rate
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ FactoryBot.modify do
4
+ factory :address do
5
+ address1 { '215 N 7th Ave' }
6
+ city { 'Manville' }
7
+ association(:state, name: 'New Jersey', abbr: 'NJ')
8
+ zipcode { '08835' }
9
+ end
10
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ FactoryBot.modify do
4
+ factory :base_product do
5
+ weight { 10.0 }
6
+ end
7
+ end