solidus_active_shipping 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (155) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +11 -0
  3. data/.rspec +1 -0
  4. data/.simplecov +5 -0
  5. data/.travis.yml +21 -0
  6. data/Gemfile +18 -0
  7. data/README.md +192 -0
  8. data/Rakefile +21 -0
  9. data/app/assets/javascripts/admin/product_packages/edit.js.coffee +4 -0
  10. data/app/assets/javascripts/admin/product_packages/index.js.coffee +28 -0
  11. data/app/assets/javascripts/admin/product_packages/new.js.coffee +7 -0
  12. data/app/assets/javascripts/spree/backend/solidus_active_shipping.js +0 -0
  13. data/app/assets/javascripts/spree/frontend/solidus_active_shipping.js +0 -0
  14. data/app/assets/stylesheets/spree/backend/solidus_active_shipping.css +0 -0
  15. data/app/assets/stylesheets/spree/frontend/solidus_active_shipping.css +0 -0
  16. data/app/controllers/spree/admin/active_shipping_settings_controller.rb +27 -0
  17. data/app/controllers/spree/admin/product_packages_controller.rb +17 -0
  18. data/app/controllers/spree/admin/products_controller_decorator.rb +12 -0
  19. data/app/controllers/spree/checkout_controller_decorator.rb +11 -0
  20. data/app/controllers/spree/orders_controller_decorator.rb +11 -0
  21. data/app/models/spree/calculator/shipping/active_shipping/base.rb +208 -0
  22. data/app/models/spree/calculator/shipping/canada_post/base.rb +17 -0
  23. data/app/models/spree/calculator/shipping/canada_post/expedited.rb +11 -0
  24. data/app/models/spree/calculator/shipping/canada_post/parcel_surface.rb +11 -0
  25. data/app/models/spree/calculator/shipping/canada_post/priority_worldwide_intl.rb +12 -0
  26. data/app/models/spree/calculator/shipping/canada_post/regular.rb +11 -0
  27. data/app/models/spree/calculator/shipping/canada_post/small_packets_air.rb +11 -0
  28. data/app/models/spree/calculator/shipping/canada_post/small_packets_surface.rb +11 -0
  29. data/app/models/spree/calculator/shipping/canada_post/xpresspost.rb +11 -0
  30. data/app/models/spree/calculator/shipping/canada_post/xpresspost_international.rb +11 -0
  31. data/app/models/spree/calculator/shipping/fedex/base.rb +21 -0
  32. data/app/models/spree/calculator/shipping/fedex/express_saver.rb +13 -0
  33. data/app/models/spree/calculator/shipping/fedex/first_overnight.rb +13 -0
  34. data/app/models/spree/calculator/shipping/fedex/ground.rb +13 -0
  35. data/app/models/spree/calculator/shipping/fedex/ground_home_delivery.rb +13 -0
  36. data/app/models/spree/calculator/shipping/fedex/international_economy.rb +13 -0
  37. data/app/models/spree/calculator/shipping/fedex/international_economy_freight.rb +13 -0
  38. data/app/models/spree/calculator/shipping/fedex/international_first.rb +13 -0
  39. data/app/models/spree/calculator/shipping/fedex/international_ground.rb +13 -0
  40. data/app/models/spree/calculator/shipping/fedex/international_priority.rb +13 -0
  41. data/app/models/spree/calculator/shipping/fedex/international_priority_freight.rb +13 -0
  42. data/app/models/spree/calculator/shipping/fedex/international_priority_saturday_delivery.rb +13 -0
  43. data/app/models/spree/calculator/shipping/fedex/one_day_freight.rb +13 -0
  44. data/app/models/spree/calculator/shipping/fedex/one_day_freight_saturday_delivery.rb +13 -0
  45. data/app/models/spree/calculator/shipping/fedex/priority_overnight.rb +13 -0
  46. data/app/models/spree/calculator/shipping/fedex/priority_overnight_saturday_delivery.rb +11 -0
  47. data/app/models/spree/calculator/shipping/fedex/saver.rb +11 -0
  48. data/app/models/spree/calculator/shipping/fedex/standard_overnight.rb +11 -0
  49. data/app/models/spree/calculator/shipping/fedex/three_day_freight.rb +11 -0
  50. data/app/models/spree/calculator/shipping/fedex/three_day_freight_saturday_delivery.rb +11 -0
  51. data/app/models/spree/calculator/shipping/fedex/two_day.rb +11 -0
  52. data/app/models/spree/calculator/shipping/fedex/two_day_freight.rb +11 -0
  53. data/app/models/spree/calculator/shipping/fedex/two_day_freight_saturday_delivery.rb +11 -0
  54. data/app/models/spree/calculator/shipping/fedex/two_day_saturday_delivery.rb +11 -0
  55. data/app/models/spree/calculator/shipping/ups/base.rb +30 -0
  56. data/app/models/spree/calculator/shipping/ups/express.rb +11 -0
  57. data/app/models/spree/calculator/shipping/ups/ground.rb +11 -0
  58. data/app/models/spree/calculator/shipping/ups/next_day_air.rb +11 -0
  59. data/app/models/spree/calculator/shipping/ups/next_day_air_early_am.rb +11 -0
  60. data/app/models/spree/calculator/shipping/ups/next_day_air_saver.rb +11 -0
  61. data/app/models/spree/calculator/shipping/ups/saver.rb +11 -0
  62. data/app/models/spree/calculator/shipping/ups/second_day_air.rb +11 -0
  63. data/app/models/spree/calculator/shipping/ups/standard.rb +11 -0
  64. data/app/models/spree/calculator/shipping/ups/three_day_select.rb +11 -0
  65. data/app/models/spree/calculator/shipping/ups/worldwide_expedited.rb +11 -0
  66. data/app/models/spree/calculator/shipping/usps/base.rb +84 -0
  67. data/app/models/spree/calculator/shipping/usps/express_mail.rb +19 -0
  68. data/app/models/spree/calculator/shipping/usps/express_mail_international.rb +46 -0
  69. data/app/models/spree/calculator/shipping/usps/first_class_mail_international.rb +31 -0
  70. data/app/models/spree/calculator/shipping/usps/first_class_mail_international_large_envelope.rb +31 -0
  71. data/app/models/spree/calculator/shipping/usps/first_class_mail_parcel.rb +28 -0
  72. data/app/models/spree/calculator/shipping/usps/first_class_package_international.rb +49 -0
  73. data/app/models/spree/calculator/shipping/usps/global_express_guaranteed.rb +48 -0
  74. data/app/models/spree/calculator/shipping/usps/media_mail.rb +19 -0
  75. data/app/models/spree/calculator/shipping/usps/priority_mail.rb +19 -0
  76. data/app/models/spree/calculator/shipping/usps/priority_mail_flat_rate_envelope.rb +19 -0
  77. data/app/models/spree/calculator/shipping/usps/priority_mail_international.rb +49 -0
  78. data/app/models/spree/calculator/shipping/usps/priority_mail_international_large_flat_rate_box.rb +43 -0
  79. data/app/models/spree/calculator/shipping/usps/priority_mail_international_medium_flat_rate_box.rb +43 -0
  80. data/app/models/spree/calculator/shipping/usps/priority_mail_international_small_flat_rate_box.rb +43 -0
  81. data/app/models/spree/calculator/shipping/usps/priority_mail_large_flat_rate_box.rb +19 -0
  82. data/app/models/spree/calculator/shipping/usps/priority_mail_medium_flat_rate_box.rb +19 -0
  83. data/app/models/spree/calculator/shipping/usps/priority_mail_small_flat_rate_box.rb +19 -0
  84. data/app/models/spree/calculator/shipping/usps/standard_post.rb +19 -0
  85. data/app/models/spree/content_item_decorator.rb +7 -0
  86. data/app/models/spree/line_item_decorator.rb +4 -0
  87. data/app/models/spree/package_builder.rb +114 -0
  88. data/app/models/spree/product_decorator.rb +10 -0
  89. data/app/models/spree/product_package.rb +10 -0
  90. data/app/models/spree/stock_location_decorator.rb +10 -0
  91. data/app/models/spree/variant_decorator.rb +3 -0
  92. data/app/overrides/spree/admin/shared/_configuration_menu/add_active_shipping_settings_tab.html.erb.deface +3 -0
  93. data/app/overrides/spree/admin/shared/_product_tabs/add_product_packages_tab.html.erb.deface +7 -0
  94. data/app/views/spree/admin/active_shipping_settings/edit.html.erb +92 -0
  95. data/app/views/spree/admin/product_packages/_form.html.erb +24 -0
  96. data/app/views/spree/admin/product_packages/edit.html.erb +15 -0
  97. data/app/views/spree/admin/product_packages/index.html.erb +46 -0
  98. data/app/views/spree/admin/product_packages/new.html.erb +15 -0
  99. data/config/locales/en.yml +93 -0
  100. data/config/locales/fr.yml +12 -0
  101. data/config/routes.rb +9 -0
  102. data/db/migrate/20130107030221_create_product_packages.rb +12 -0
  103. data/lib/solidus_active_shipping.rb +3 -0
  104. data/lib/solidus_active_shipping/engine.rb +44 -0
  105. data/lib/spree/active_shipping_configuration.rb +24 -0
  106. data/lib/spree/shipping_error.rb +3 -0
  107. data/solidus_active_shipping.gemspec +33 -0
  108. data/spec/cassettes/Checkout/with_valid_shipping_address/does_not_break_the_per-item_shipping_method_calculator.yml +134 -0
  109. data/spec/cassettes/FedEx_calculators/with_Canadian_origin_address/Spree_Calculator_Shipping_Fedex_Ground/1_1_1_1.yml +48 -0
  110. data/spec/cassettes/FedEx_calculators/with_Canadian_origin_address/Spree_Calculator_Shipping_Fedex_InternationalEconomy/1_1_2_1.yml +48 -0
  111. data/spec/cassettes/FedEx_calculators/with_Canadian_origin_address/Spree_Calculator_Shipping_Fedex_InternationalFirst/1_1_3_1.yml +48 -0
  112. data/spec/cassettes/FedEx_calculators/with_Canadian_origin_address/Spree_Calculator_Shipping_Fedex_InternationalPriority/1_1_4_1.yml +48 -0
  113. data/spec/cassettes/FedEx_calculators/with_US_origin_address/Spree_Calculator_Shipping_Fedex_ExpressSaver/1_2_5_1.yml +48 -0
  114. data/spec/cassettes/FedEx_calculators/with_US_origin_address/Spree_Calculator_Shipping_Fedex_FirstOvernight/1_2_1_1.yml +48 -0
  115. data/spec/cassettes/FedEx_calculators/with_US_origin_address/Spree_Calculator_Shipping_Fedex_GroundHomeDelivery/1_2_6_1.yml +48 -0
  116. data/spec/cassettes/FedEx_calculators/with_US_origin_address/Spree_Calculator_Shipping_Fedex_PriorityOvernight/1_2_2_1.yml +48 -0
  117. data/spec/cassettes/FedEx_calculators/with_US_origin_address/Spree_Calculator_Shipping_Fedex_StandardOvernight/1_2_3_1.yml +48 -0
  118. data/spec/cassettes/FedEx_calculators/with_US_origin_address/Spree_Calculator_Shipping_Fedex_TwoDay/1_2_4_1.yml +48 -0
  119. data/spec/cassettes/UPS_calculators/with_Canadian_origin_address/Spree_Calculator_Shipping_Ups_Express/1_1_1_1.yml +103 -0
  120. data/spec/cassettes/UPS_calculators/with_Canadian_origin_address/Spree_Calculator_Shipping_Ups_Saver/1_1_3_1.yml +103 -0
  121. data/spec/cassettes/UPS_calculators/with_Canadian_origin_address/Spree_Calculator_Shipping_Ups_Standard/1_1_4_1.yml +103 -0
  122. data/spec/cassettes/UPS_calculators/with_Canadian_origin_address/Spree_Calculator_Shipping_Ups_ThreeDaySelect/1_1_5_1.yml +103 -0
  123. data/spec/cassettes/UPS_calculators/with_Canadian_origin_address/Spree_Calculator_Shipping_Ups_WorldwideExpedited/1_1_2_1.yml +103 -0
  124. data/spec/cassettes/UPS_calculators/with_US_origin_address/Spree_Calculator_Shipping_Ups_Ground/1_2_1_1.yml +103 -0
  125. data/spec/cassettes/UPS_calculators/with_US_origin_address/Spree_Calculator_Shipping_Ups_NextDayAir/1_2_2_1.yml +103 -0
  126. data/spec/cassettes/UPS_calculators/with_US_origin_address/Spree_Calculator_Shipping_Ups_NextDayAirEarlyAm/1_2_3_1.yml +103 -0
  127. data/spec/cassettes/UPS_calculators/with_US_origin_address/Spree_Calculator_Shipping_Ups_NextDayAirSaver/1_2_4_1.yml +103 -0
  128. data/spec/cassettes/UPS_calculators/with_US_origin_address/Spree_Calculator_Shipping_Ups_SecondDayAir/1_2_5_1.yml +103 -0
  129. data/spec/controllers/admin/active_shipping_settings_controller_spec.rb +38 -0
  130. data/spec/controllers/admin/product_packages_controller_spec.rb +36 -0
  131. data/spec/factories/order_factory_override.rb +46 -0
  132. data/spec/factories/product_package_factory.rb +9 -0
  133. data/spec/factories/state_factory_override.rb +28 -0
  134. data/spec/features/checkout_spec.rb +37 -0
  135. data/spec/fixtures/normal_rates_request.xml +2 -0
  136. data/spec/integrations/calculators/fedex_spec.rb +56 -0
  137. data/spec/integrations/calculators/ups_spec.rb +56 -0
  138. data/spec/lib/spree/active_shipping/bogus_carrier.rb +18 -0
  139. data/spec/lib/spree/calculator/shipping/bogus_calculator.rb +17 -0
  140. data/spec/models/active_shipping_calculator_spec.rb +182 -0
  141. data/spec/models/carriers/usps_calculator_spec.rb +72 -0
  142. data/spec/models/package_builder_spec.rb +173 -0
  143. data/spec/spec.opts +6 -0
  144. data/spec/spec_helper.rb +80 -0
  145. data/spec/support/capybara.rb +24 -0
  146. data/spec/support/checkout_helper.rb +12 -0
  147. data/spec/support/feature_helper.rb +7 -0
  148. data/spec/support/package_helper.rb +9 -0
  149. data/spec/support/shared_contexts/checkout_setup.rb +12 -0
  150. data/spec/support/shared_contexts/package_setup.rb +34 -0
  151. data/spec/support/shared_contexts/shipping_carriers/fedex.rb +15 -0
  152. data/spec/support/shared_contexts/shipping_carriers/ups.rb +14 -0
  153. data/spec/support/shared_contexts/stock_location_setup.rb +26 -0
  154. data/spec/support/web_fixtures.rb +6 -0
  155. metadata +444 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: d467e39dfb8748765167db0bdb9537def5474d06
4
+ data.tar.gz: dbec361b367ebe9a75b8fcb1f63bdf440432fe62
5
+ SHA512:
6
+ metadata.gz: ea5a385c0ac36f65ff5381281ef11e7ed0ad5dd51878fdabe89d236b6827635759bffe48e94e1c5c155eed5be8d506daa304fd045aa2910c2ddbeacb6dc82d05
7
+ data.tar.gz: ffc02cdc9744b9bf64e8cb4c7a1c075c76ca542b05583e509606ee6a50f1b00eec445da5c444d86c3e303591609e157e9381c32c538546fa2b9f50de8022267c
@@ -0,0 +1,11 @@
1
+ spec/test_app
2
+ spec/dummy
3
+ *.swp
4
+ Gemfile.lock
5
+ .rvmrc
6
+ .DS_Store
7
+ coverage
8
+ .bundle
9
+ .idea
10
+ .ruby-*
11
+ spec/examples.txt
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
@@ -0,0 +1,5 @@
1
+ require 'simplecov'
2
+
3
+ # .simplecov
4
+ SimpleCov.start 'rails' do
5
+ end
@@ -0,0 +1,21 @@
1
+ sudo: false
2
+ cache: bundler
3
+ language: ruby
4
+ rvm:
5
+ - 2.3.1
6
+ env:
7
+ matrix:
8
+ - SOLIDUS_BRANCH=v1.0 DB=postgres
9
+ - SOLIDUS_BRANCH=v1.1 DB=postgres
10
+ - SOLIDUS_BRANCH=v1.2 DB=postgres
11
+ - SOLIDUS_BRANCH=v1.3 DB=postgres
12
+ - SOLIDUS_BRANCH=v1.4 DB=postgres
13
+ - SOLIDUS_BRANCH=v2.0 DB=postgres
14
+ - SOLIDUS_BRANCH=master DB=postgres
15
+ - SOLIDUS_BRANCH=v1.0 DB=mysql
16
+ - SOLIDUS_BRANCH=v1.1 DB=mysql
17
+ - SOLIDUS_BRANCH=v1.2 DB=mysql
18
+ - SOLIDUS_BRANCH=v1.3 DB=mysql
19
+ - SOLIDUS_BRANCH=v1.4 DB=mysql
20
+ - SOLIDUS_BRANCH=v2.0 DB=mysql
21
+ - SOLIDUS_BRANCH=master DB=mysql
data/Gemfile ADDED
@@ -0,0 +1,18 @@
1
+ source "https://rubygems.org"
2
+
3
+ branch = ENV.fetch('SOLIDUS_BRANCH', 'master')
4
+ gem "solidus", github: "solidusio/solidus", branch: branch
5
+
6
+ if branch == 'master' || branch >= "v2.0"
7
+ gem "rails-controller-testing", group: :test
8
+ end
9
+
10
+ gem 'sqlite3'
11
+ gem 'pg'
12
+ gem 'mysql2'
13
+
14
+ group :development, :test do
15
+ gem "pry-rails"
16
+ end
17
+
18
+ gemspec
@@ -0,0 +1,192 @@
1
+ Active Shipping
2
+ ===============
3
+
4
+ This is a Solidus extension that wraps the popular [active_shipping](http://github.com/Shopify/active_shipping/tree/master) plugin.
5
+
6
+ **NOTE : This is an old and complex extension we are working on bringing up to our standards. Use with caution**
7
+
8
+ [![Build Status](https://travis-ci.org/solidusio-contrib/solidus_active_shipping.svg?branch=master)](https://travis-ci.org/solidusio-contrib/solidus_active_shipping)
9
+
10
+ Installation
11
+ ------------
12
+
13
+ **1.** Add the gem to your application's Gemfile:
14
+
15
+ To install the latest edge version of this extension, place this line inside your application's Gemfile:
16
+
17
+ ```ruby
18
+ gem 'solidus_active_shipping', :git => "git://github.com/solidusio-contrib/solidus_active_shipping"
19
+ ```
20
+
21
+ **2.** Install migrations and migrate database:
22
+
23
+ ```
24
+ $ bundle exec rake railties:install:migrations
25
+ $ bundle exec rake db:migrate
26
+ ```
27
+
28
+ **3.** Run bundler:
29
+
30
+ ```
31
+ $ bundle
32
+ ```
33
+
34
+ Rate quotes from carriers
35
+ ---
36
+
37
+ So far, this gem supports getting quotes from UPS, USPS, Canada Post, and FedEx. In general, you will need a developer account to get rates. Please contact the shipping vendor that you wish to use about generating a developer account.
38
+
39
+ Once you have an account, you can go to the active shipping settings admin configuration screen to set the right fields. You need to set all of the Origin Address fields and the fields for the carrier you wish to use. To set the settings through a config file, you can assign values to the settings like so:
40
+
41
+ ```ruby
42
+ Spree::ActiveShipping::Config[:ups_login]
43
+ Spree::ActiveShipping::Config[:ups_password]
44
+ Spree::ActiveShipping::Config[:ups_key]
45
+ Spree::ActiveShipping::Config[:usps_login]
46
+ ```
47
+
48
+ **NOTE:** When setting up FedEx credentials, `:fedex_login` is the "Meter Number" that FedEx supplies you with.
49
+
50
+ It is important to note how this wrapper matches the calculators to the services available from the carrier API's, by default the base calculator matches the service name to the calculator class and returns the rate, this magic happens as follows:
51
+
52
+ 1. inside the calculator class
53
+ ```ruby
54
+ Spree::Calculator::Shipping::Fedex::GroundHomeDelivery::description #holds the service name
55
+ ```
56
+
57
+ 2. inside the calculator base
58
+ ```ruby
59
+ rates_result = retrieve_rates_from_cache(package, origin, destination) # <- holds the rates for given package in a parsed hash (see sample response below)
60
+ rate = rates_result[self.class.description] # <- matches with the description as the key
61
+ ```
62
+
63
+ this means that the calculator **Fedex::GroundHomeDelivery** will hit FedEx Servers and try to get the rates for the given package, since FedEx returns rates for package and returns all of its available services for the given shipment we need to identify which service we are targeting ( see caching results below ) the calculator will only pick the rates from a service that matches the **"FedEx Ground Home Delivery"** string, you can see how it works below:
64
+
65
+ a sample rate response already parsed looks like this:
66
+ ```ruby
67
+ {
68
+ "FedEx First Overnight" => 5886,
69
+ "FedEx Priority Overnight" => 2924,
70
+ "FedEx Standard Overnight" => 2529,
71
+ "FedEx 2 Day Am" => 1987,
72
+ "FedEx 2 Day" => 1774,
73
+ "FedEx Ground Home Delivery" => 925
74
+ }
75
+ ```
76
+
77
+ the rate hash that is parsed by the calculator has service descriptions as keys, this makes it easier to get the rates you need.
78
+
79
+ 3. getting the rates (all the above together)
80
+ ```ruby
81
+ calculator = Spree::Calculator::Shipping::Fedex::GroundHomeDelivery.new
82
+ calculator.description # "FedEx Ground Home Delivery"
83
+ rate = calculator.compute(<Package>)
84
+ rate # $9.25
85
+ ```
86
+
87
+ you can see the rates are given in cents from FedEx (in the rate hash example above), ```solidus_active_shipping``` converts them dividing them by 100 before sending them to you
88
+
89
+ **Note:** if you want to integrate to a new carrier service that is not listed below please take care when trying to match the service name key to theirs, there are times when they create dynamic naming conventions, please take as an example **USPS**, you can see the implementation of USPS has the **compute_packages** method overridden to match against a **service_code** key that had to be added to calculator services ( Issue #103 )
90
+
91
+ Global Handling Fee
92
+ -------------------
93
+
94
+ ```ruby
95
+ Spree::ActiveShipping::Config[:handling_fee]
96
+ ```
97
+
98
+ This property allows you to set a global handling fee that will be added to all calculated shipping rates. Specify the number of cents, not dollars. You can either set it manually or through the admin interface.
99
+
100
+ Weights
101
+ ---------------------
102
+
103
+ ## Global weight default
104
+ This property allows you to set a default weight that will be substituted for products lacking defined weights. You can either set it manually or through the admin interface.
105
+
106
+ ```ruby
107
+ Spree::ActiveShipping::Config[:default_weight]
108
+ ```
109
+
110
+ ## Weight units
111
+ ```ruby
112
+ Spree::ActiveShipping::Config[:units]
113
+ ```
114
+
115
+ Product/variant weights in Solidus are not tied to a specific weight system (:imperial or :metric) since the values are only numeric. This value is sent to ActiveShipping so that calculations can be made in the needed weight system. By default this value is set to :imperial but can be switched to :metric.
116
+
117
+ ```ruby
118
+ Spree::ActiveShipping::Config[:unit_multiplier]
119
+ ```
120
+
121
+ ActiveShipping use grams or ounces depending on the weight system specified. If your products weight values are set in a different unit, you can use this parameter to automatically convert them. For example, if your products have their weights in lb, you can set this value to 16 to convert them to oz (as expected by ActiveShipping if using :imperial) since 1 lb = 16 oz.
122
+
123
+ It is important to note that by default this variable is set to have a value of **16** expecting weights to be entered in **lb**.
124
+
125
+ For more details on how ActiveShipping use this data, refer to the [active_shipping] (http://github.com/Shopify/active_shipping/tree/master) documentation.
126
+
127
+ ### Example of converting from metric system to oz
128
+
129
+ Say you have your weights in **kg** you would have to set the multiplier to **0.028**
130
+
131
+ ```ruby
132
+ Spree::ActiveShipping::Config[:unit_multiplier] = 0.0283
133
+ ```
134
+
135
+ ## Product packages ##
136
+
137
+ This extension adds ProductPackages to the Spree::Product. This model can be used to explicitly define how a product is physically shipped.
138
+ For example, you can have a 200lbs product that ships in five smaller 40lbs packages. This allows you to use calculators who would be unavailable otherwise
139
+ because of their weight limits (if a calculator has a max weight of 150lbs, it would not be possible to ship the aforementioned product if we account
140
+ only for total product weight, while the product packages will allow you to ship it since each individual 40lbs product package is under the limit)
141
+
142
+ Cache
143
+ ------------
144
+
145
+ When Solidus tries to get rates for a given shipment it calls **Spree::Stock::Estimator**, this class is in charge of getting the rates back from any calculator active for a shipment, the way the estimator determines the shipping methods that will apply to the shipment varies from within spree versions but the general idea is this:
146
+
147
+ **NOTE:** Shipping methods are tied to calculators
148
+
149
+ ```ruby
150
+ private
151
+ def shipping_methods(package)
152
+ shipping_methods = package.shipping_methods
153
+ shipping_methods.delete_if { |ship_method| !ship_method.calculator.available?(package) }
154
+ shipping_methods.delete_if { |ship_method| !ship_method.include?(order.ship_address) }
155
+ shipping_methods.delete_if { |ship_method| !(ship_method.calculator.preferences[:currency].nil? || ship_method.calculator.preferences[:currency] == currency) }
156
+ shipping_methods
157
+ end
158
+ ```
159
+
160
+ The money line for **solidus_active_shipping** is when it calls the calculator's ```available?``` method, this method is actually calling the carrier services, and it checks for rates or errors in the form of ```Spree::ShippingError```, if the rates are there for the specified shipment, the calculator will store the parsed rates with a specific key for each package inside the cache, consider the following example to see why this works and why this is necessary:
161
+
162
+ - User orders N amount of products
163
+ - All of the products from this order are stored in Stock Location 1
164
+ - Once the order creates shipments you will end up with 1 shipment
165
+ - Calculators are active for the following services: **FedEx Ground Home Delivery**, **FedEx 2 Day**, **FedEx International Priority**
166
+ - Order calls the estimator for rates:
167
+ - Estimator will try to get the active shipping methods for this package, it will call available? on all of the active calculators to determine if they are all available for this shipment
168
+ - once it calls it for the first calculator (**FedEx Ground Home Delivery**) it will get the following rates back from FedEx
169
+
170
+ ```ruby
171
+ {
172
+ "FedEx First Overnight" => 5886,
173
+ "FedEx Priority Overnight" => 2924,
174
+ "FedEx Standard Overnight" => 2529,
175
+ "FedEx 2 Day Am" => 1987,
176
+ "FedEx 2 Day" => 1774,
177
+ "FedEx Ground Home Delivery" => 925
178
+ }
179
+ ```
180
+
181
+ - when it tries to get rates for the 2nd calculator (**FedEx 2 Day**) it will check the cache first and will find that for this package and stock location it already has rates stored in the cache and it won't call FedEx again, using this same rates
182
+ - when it hits the last calculator (**FedEx International Priority**) it will find that the cache doesn't have any rates for the given key, and the method ```available?``` called from the Estimator will return false thus removing the calculator's shipping method from the list of available calculators and won't return any rates back for it
183
+ - Consequently since this 3rd calculator (**FedEx International Priority**) is an international calculator it would have been removed as well by the line that checks if any shipping method is allowed in already defined Zones.
184
+
185
+ Testing
186
+ -------
187
+
188
+ Be sure to bundle your dependencies and then create a dummy test app for the specs to run against.
189
+
190
+ $ bundle
191
+ $ bundle exec rake test_app
192
+ $ bundle exec rspec spec
@@ -0,0 +1,21 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
3
+
4
+ require 'rspec/core/rake_task'
5
+ require 'spree/testing_support/extension_rake'
6
+
7
+ RSpec::Core::RakeTask.new
8
+
9
+ task :default do
10
+ if Dir["spec/dummy"].empty?
11
+ Rake::Task[:test_app].invoke
12
+ Dir.chdir("../../")
13
+ end
14
+ Rake::Task[:spec].invoke
15
+ end
16
+
17
+ desc 'Generates a dummy app for testing'
18
+ task :test_app do
19
+ ENV['LIB_NAME'] = 'solidus_active_shipping'
20
+ Rake::Task['extension:test_app'].invoke
21
+ end
@@ -0,0 +1,4 @@
1
+ ($ '#cancel_link').click (event) ->
2
+ event.preventDefault()
3
+
4
+ ($ '#product_packages').html('')
@@ -0,0 +1,28 @@
1
+ $ ->
2
+ ($ '#new_product_package_link').click (event) ->
3
+ event.preventDefault()
4
+
5
+ ($ '.no-objects-found').hide()
6
+
7
+ ($ this).hide()
8
+ $.ajax
9
+ type: 'GET'
10
+ url: @href
11
+ data: (
12
+ authenticity_token: AUTH_TOKEN
13
+ )
14
+ success: (r) ->
15
+ ($ '#product_packages').html r
16
+
17
+ ($ 'a.edit').click (event) ->
18
+ event.preventDefault()
19
+
20
+ ($ '#product_packages').html('')
21
+ $.ajax
22
+ type: 'GET'
23
+ url: @href
24
+ data: (
25
+ authenticity_token: AUTH_TOKEN
26
+ )
27
+ success: (r) ->
28
+ ($ '#product_packages').html r
@@ -0,0 +1,7 @@
1
+ ($ '#cancel_link').click (event) ->
2
+ event.preventDefault()
3
+
4
+ ($ '.no-objects-found').show()
5
+
6
+ ($ '#new_product_package_link').show()
7
+ ($ '#product_packages').html('')
@@ -0,0 +1,27 @@
1
+ class Spree::Admin::ActiveShippingSettingsController < Spree::Admin::BaseController
2
+
3
+ def edit
4
+ @preferences_UPS = [:ups_login, :ups_password, :ups_key, :shipper_number]
5
+ @preferences_FedEx = [:fedex_login, :fedex_password, :fedex_account, :fedex_key]
6
+ @preferences_USPS = [:usps_login]
7
+ @preferences_CanadaPost = [:canada_post_login]
8
+ @preferences_GeneralSettings = [:units, :unit_multiplier, :default_weight, :handling_fee,
9
+ :max_weight_per_package, :test_mode]
10
+
11
+ @config = Spree::ActiveShippingConfiguration.new
12
+ end
13
+
14
+ def update
15
+ config = Spree::ActiveShippingConfiguration.new
16
+
17
+ params.each do |name, value|
18
+ next unless config.has_preference? name
19
+ config[name] = value
20
+ end
21
+
22
+ redirect_to edit_admin_active_shipping_settings_path
23
+ end
24
+
25
+ end
26
+
27
+
@@ -0,0 +1,17 @@
1
+ module Spree
2
+ module Admin
3
+ class ProductPackagesController < ResourceController
4
+ belongs_to 'spree/product', :find_by => :slug
5
+ before_filter :load_data
6
+
7
+ private
8
+ def load_data
9
+ @product = Product.where(:slug => params[:product_id]).first
10
+ end
11
+
12
+ def permitted_product_package_attributes
13
+ [:length, :width, :height, :weight]
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,12 @@
1
+ Spree::Admin::ProductsController.class_eval do
2
+ def product_packages
3
+ @product = Spree::Product.find_by_slug!(params[:id])
4
+ @packages = @product.product_packages
5
+ @product.product_packages.build
6
+
7
+ respond_with(@object) do |format|
8
+ format.html { render :layout => !request.xhr? }
9
+ format.js { render :layout => false }
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,11 @@
1
+ # handle shipping errors gracefully during checkout
2
+ Spree::CheckoutController.class_eval do
3
+
4
+ rescue_from Spree::ShippingError, :with => :handle_shipping_error
5
+
6
+ private
7
+ def handle_shipping_error(e)
8
+ flash[:error] = e.message
9
+ redirect_to checkout_state_path(:address)
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ # handle shipping errors gracefully during order update
2
+ Spree::OrdersController.class_eval do
3
+
4
+ rescue_from Spree::ShippingError, :with => :handle_shipping_error
5
+
6
+ private
7
+ def handle_shipping_error(e)
8
+ flash[:error] = e.message
9
+ redirect_back_or_default(root_path)
10
+ end
11
+ end
@@ -0,0 +1,208 @@
1
+ # This is a base calculator for shipping calcualations using the ActiveShipping plugin. It is not intended to be
2
+ # instantiated directly. Create subclass for each specific shipping method you wish to support instead.
3
+ #
4
+ # Digest::MD5 is used for cache_key generation.
5
+ require 'digest/md5'
6
+ require_dependency 'spree/calculator'
7
+
8
+ module Spree
9
+ module Calculator::Shipping
10
+ module ActiveShipping
11
+ class Base < ShippingCalculator
12
+ def self.service_name
13
+ description
14
+ end
15
+
16
+ def available?(package)
17
+ # helps the available? method determine
18
+ # if rates are avaiable for this service
19
+ # before calling the carrier for rates
20
+ is_package_shippable?(package)
21
+
22
+ !compute(package).nil?
23
+ rescue Spree::ShippingError
24
+ false
25
+ end
26
+
27
+ def compute_package(package)
28
+ order = package.order
29
+ max_weight = get_max_weight(package)
30
+
31
+ stock_location = package.stock_location
32
+
33
+ origin = build_location(stock_location)
34
+ destination = build_location(order.ship_address)
35
+
36
+ rates_result = retrieve_rates_from_cache(package, origin, destination, max_weight)
37
+
38
+ return nil if rates_result.is_a?(Spree::ShippingError)
39
+ return nil if rates_result.empty?
40
+ rate = rates_result[self.class.description]
41
+
42
+ return nil unless rate
43
+ rate = rate.to_f + (Spree::ActiveShipping::Config[:handling_fee].to_f || 0.0)
44
+
45
+ # divide by 100 since active_shipping rates are expressed as cents
46
+ rate / 100.0
47
+ end
48
+
49
+ def timing(line_items)
50
+ order = line_items.first.order
51
+ # TODO: Figure out where stock_location is supposed to come from.
52
+ origin = ::ActiveShipping::Location.new(country: stock_location.country.iso,
53
+ city: stock_location.city,
54
+ state: (stock_location.state ? stock_location.state.abbr : stock_location.state_name),
55
+ zip: stock_location.zipcode)
56
+ addr = order.ship_address
57
+ destination = ::ActiveShipping::Location.new(country: addr.country.iso,
58
+ state: (addr.state ? addr.state.abbr : addr.state_name),
59
+ city: addr.city,
60
+ zip: addr.zipcode)
61
+ timings_result = Rails.cache.fetch(cache_key(package) + '-timings') do
62
+ retrieve_timings(origin, destination, packages(order))
63
+ end
64
+ raise timings_result if timings_result.is_a?(Spree::ShippingError)
65
+ return nil if timings_result.nil? || !timings_result.is_a?(Hash) || timings_result.empty?
66
+ timings_result[description]
67
+ end
68
+
69
+ protected
70
+
71
+ # weight limit in ounces or zero (if there is no limit)
72
+ def max_weight_for_country(_country)
73
+ 0
74
+ end
75
+
76
+ private
77
+
78
+ def get_max_weight(solidus_package)
79
+ order = solidus_package.order
80
+
81
+ # Default value from calculator
82
+ max_weight = max_weight_for_country(order.ship_address.country)
83
+
84
+ # If max_weight is zero or max_weight_per_package is less than max_weight
85
+ # We use the max_weight_per_package instead
86
+ if max_weight.zero? && max_weight_per_package.nonzero?
87
+ return max_weight_per_package
88
+ elsif max_weight > 0 && max_weight_per_package < max_weight && max_weight_per_package > 0
89
+ return max_weight_per_package
90
+ end
91
+
92
+ max_weight
93
+ end
94
+
95
+ def package_builder
96
+ @package_builder ||= Spree::PackageBuilder.new
97
+ end
98
+
99
+ def max_weight_per_package
100
+ Spree::ActiveShipping::Config[:max_weight_per_package] * Spree::ActiveShipping::Config[:unit_multiplier]
101
+ end
102
+
103
+ # check for known limitations inside a package
104
+ # that will limit you from shipping using a service
105
+ def is_package_shippable?(package)
106
+ # check for weight limits on service
107
+ country_weight_error? package
108
+ end
109
+
110
+ def country_weight_error?(package)
111
+ max_weight = max_weight_for_country(package.order.ship_address.country)
112
+ raise Spree::ShippingError, "#{I18n.t(:shipping_error)}: The maximum per package weight for the selected service from the selected country is #{max_weight} ounces." unless valid_weight_for_package?(package, max_weight)
113
+ end
114
+
115
+ # zero weight check means no check
116
+ # nil check means service isn't available for that country
117
+ def valid_weight_for_package?(package, max_weight)
118
+ return false if max_weight.nil?
119
+ return true if max_weight.zero?
120
+ package.weight <= max_weight
121
+ end
122
+
123
+ def retrieve_rates(origin, destination, shipment_packages)
124
+ response = carrier.find_rates(origin, destination, shipment_packages)
125
+ # turn this beastly array into a nice little hash
126
+ rates = response.rates.collect do |rate|
127
+ service_name = rate.service_name.encode('UTF-8')
128
+ [CGI.unescapeHTML(service_name), rate.price]
129
+ end
130
+ rate_hash = Hash[*rates.flatten]
131
+ return rate_hash
132
+ rescue ::ActiveShipping::Error => e
133
+
134
+ if [::ActiveShipping::ResponseError].include?(e.class) && e.response.is_a?(::ActiveShipping::Response)
135
+ params = e.response.params
136
+ if params.key?('Response') && params['Response'].key?('Error') && params['Response']['Error'].key?('ErrorDescription')
137
+ message = params['Response']['Error']['ErrorDescription']
138
+ # Canada Post specific error message
139
+ elsif params.key?('eparcel') && params['eparcel'].key?('error') && params['eparcel']['error'].key?('statusMessage')
140
+ message = e.response.params['eparcel']['error']['statusMessage']
141
+ else
142
+ message = e.message
143
+ end
144
+ else
145
+ message = e.message
146
+ end
147
+
148
+ error = Spree::ShippingError.new("#{I18n.t(:shipping_error)}: #{message}")
149
+ Rails.cache.write @cache_key, error # write error to cache to prevent constant re-lookups
150
+ raise error
151
+ end
152
+
153
+ def retrieve_timings(origin, destination, packages)
154
+ if carrier.respond_to?(:find_time_in_transit)
155
+ response = carrier.find_time_in_transit(origin, destination, packages)
156
+ return response
157
+ end
158
+ rescue ::ActiveShipping::ResponseError => re
159
+ if re.response.is_a?(::ActiveShipping::Response)
160
+ params = re.response.params
161
+ if params.key?('Response') && params['Response'].key?('Error') && params['Response']['Error'].key?('ErrorDescription')
162
+ message = params['Response']['Error']['ErrorDescription']
163
+ else
164
+ message = re.message
165
+ end
166
+ else
167
+ message = re.message
168
+ end
169
+
170
+ error = Spree::ShippingError.new("#{I18n.t(:shipping_error)}: #{message}")
171
+ Rails.cache.write @cache_key + '-timings', error # write error to cache to prevent constant re-lookups
172
+ raise error
173
+ end
174
+
175
+ def cache_key(package)
176
+ stock_location = package.stock_location.nil? ? '' : "#{package.stock_location.id}-"
177
+ order = package.order
178
+ ship_address = package.order.ship_address
179
+ contents_hash = Digest::MD5.hexdigest(package.contents.map { |content_item| content_item.variant.id.to_s + '_' + content_item.quantity.to_s }.join('|'))
180
+ @cache_key = "#{stock_location}#{carrier.name}-#{order.number}-#{ship_address.country.iso}-#{fetch_best_state_from_address(ship_address)}-#{ship_address.city}-#{ship_address.zipcode}-#{contents_hash}-#{I18n.locale}".delete(' ')
181
+ end
182
+
183
+ def fetch_best_state_from_address(address)
184
+ address.state ? address.state.abbr : address.state_name
185
+ end
186
+
187
+ def build_location(address)
188
+ ::ActiveShipping::Location.new(country: address.country.iso,
189
+ state: fetch_best_state_from_address(address),
190
+ city: address.city,
191
+ zip: address.zipcode)
192
+ end
193
+
194
+ def retrieve_rates_from_cache(package, origin, destination, max_weight)
195
+ Rails.cache.fetch(cache_key(package)) do
196
+ shipment_packages = package_builder.process(package, max_weight)
197
+ # shipment_packages = packages(package)
198
+ if shipment_packages.empty?
199
+ {}
200
+ else
201
+ retrieve_rates(origin, destination, shipment_packages)
202
+ end
203
+ end
204
+ end
205
+ end
206
+ end
207
+ end
208
+ end