solidus_ship_compliant 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 (50) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +15 -0
  3. data/.rspec +1 -0
  4. data/.rubocop.yml +231 -0
  5. data/.travis.yml +21 -0
  6. data/Gemfile +19 -0
  7. data/LICENSE +26 -0
  8. data/README.md +49 -0
  9. data/Rakefile +21 -0
  10. data/app/assets/javascripts/spree/backend/solidus_ship_complaint.js +2 -0
  11. data/app/assets/javascripts/spree/frontend/solidus_ship_complaint.js +2 -0
  12. data/app/assets/stylesheets/spree/backend/solidus_ship_complaint.css +4 -0
  13. data/app/assets/stylesheets/spree/frontend/solidus_ship_complaint.css +4 -0
  14. data/app/controllers/spree/admin/orders_controller_decorator.rb +7 -0
  15. data/app/controllers/spree/admin/ship_compliant_settings_controller.rb +27 -0
  16. data/app/controllers/spree/checkout_controller_decorator.rb +6 -0
  17. data/app/models/spree/calculator/ship_compliant_calculator.rb +83 -0
  18. data/app/models/spree/ship_compliant.rb +96 -0
  19. data/app/models/spree/taxon_decorator.rb +11 -0
  20. data/app/models/spree/variant_decorator.rb +73 -0
  21. data/app/overrides/spree/admin/shared/_configuration_menu/_add_ship_compliant_config_menu.html.erb.deface +3 -0
  22. data/app/views/spree/admin/shared/_ship_complaint_tabs.html.erb +9 -0
  23. data/app/views/spree/admin/ship_compliant_settings/edit.html.erb +30 -0
  24. data/bin/rails +7 -0
  25. data/config/locales/en.yml +23 -0
  26. data/config/routes.rb +5 -0
  27. data/lib/generators/solidus_ship_compliant/install/install_generator.rb +20 -0
  28. data/lib/solidus_ship_compliant.rb +2 -0
  29. data/lib/solidus_ship_compliant/configuration.rb +7 -0
  30. data/lib/solidus_ship_compliant/engine.rb +31 -0
  31. data/lib/solidus_ship_compliant/error.rb +4 -0
  32. data/lib/solidus_ship_compliant/factories.rb +6 -0
  33. data/lib/solidus_ship_compliant/version.rb +3 -0
  34. data/lib/tasks/solidus_ship_compliant/export.rake +78 -0
  35. data/solidus_ship_compliant.gemspec +35 -0
  36. data/spec/controllers/admin/ship_compliant_settings_controller_spec.rb +39 -0
  37. data/spec/factories/variant_factory_override.rb +0 -0
  38. data/spec/features/checkout_spec.rb +0 -0
  39. data/spec/models/calculator/ship_compliant_calculator_spec.rb +19 -0
  40. data/spec/models/spree/ship_compliant_spec.rb +21 -0
  41. data/spec/models/spree/taxon_spec.rb +16 -0
  42. data/spec/models/spree/variant_spec.rb +70 -0
  43. data/spec/spec_helper.rb +24 -0
  44. data/spec/support/ability.rb +15 -0
  45. data/spec/support/database_cleaner.rb +23 -0
  46. data/spec/support/devise.rb +5 -0
  47. data/spec/support/factory_bot.rb +5 -0
  48. data/spec/support/preferences.rb +5 -0
  49. data/spec/support/spree.rb +14 -0
  50. metadata +307 -0
@@ -0,0 +1,6 @@
1
+ Spree::CheckoutController.class_eval do
2
+ rescue_from SolidusShipCompliant::Error do |exception|
3
+ flash[:error] = exception.message
4
+ redirect_to checkout_state_path(:address)
5
+ end
6
+ end
@@ -0,0 +1,83 @@
1
+ require_dependency 'spree/calculator'
2
+
3
+ module Spree
4
+ class Calculator::ShipCompliantCalculator < Calculator
5
+ CACHE_EXPIRATION_DURATION = 10.minutes
6
+
7
+ def self.description
8
+ Spree.t(:ship_compliant_tax)
9
+ end
10
+
11
+ def compute_order(order)
12
+ raise 'Spree::ShipCompliant is designed to calculate taxes at the shipment and line-item levels.'
13
+ end
14
+
15
+ def compute_line_item(item)
16
+ if rate.included_in_price
17
+ raise Spree.t(:ship_compliant_alert)
18
+ else
19
+ round_to_two_places(tax_for_item(item))
20
+ end
21
+ end
22
+
23
+ def compute_shipment(shipment)
24
+ if rate.included_in_price
25
+ raise Spree.t(:ship_compliant_alert)
26
+ else
27
+ 0
28
+ end
29
+ end
30
+
31
+ def compute_shipping_rate(shipping_rate)
32
+ if rate.included_in_price
33
+ raise Spree.t(:ship_compliant_alert)
34
+ else
35
+ 0
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ def tax_for_item(item)
42
+ order = item.order
43
+ item_address = order.tax_address || order.shipping_address
44
+
45
+ return 0 unless item_address.present? && calculable.zone.include?(item_address)
46
+
47
+ rails_cache_key = cache_key(order, item, item_address)
48
+
49
+ Rails.cache.fetch(rails_cache_key, time_to_idle: CACHE_EXPIRATION_DURATION) do
50
+ transaction = Bevv::ShipCompliant.new(order).transaction_from_order
51
+ tax_for_current_item = cache_response(transaction, order, item_address, item)
52
+ tax_for_current_item
53
+ end
54
+ end
55
+
56
+ def rate
57
+ calculable
58
+ end
59
+
60
+ def round_to_two_places(amount)
61
+ BigDecimal(amount.to_s).round(2, BigDecimal::ROUND_HALF_UP)
62
+ end
63
+
64
+ def cache_response(response, order, address, line_item = nil)
65
+ res = nil
66
+ cart_items = response.shipment_sales_tax_rates[0][:product_sales_tax_rates][:product_sales_tax_rate]
67
+
68
+ return 0 unless response || cart_items
69
+
70
+ cart_items.each do |item|
71
+ next unless item[:@product_key] == line_item.variant.product_key
72
+ res = item[:@sales_tax_due]
73
+ Rails.cache.write(cache_key(order, line_item, address), res, expires_in: CACHE_EXPIRATION_DURATION)
74
+ end
75
+
76
+ res
77
+ end
78
+
79
+ def cache_key(order, item, address)
80
+ ['Spree::LineItem', order.id, item.id, address.state.id, address.zipcode, item.amount, :amount_to_collect]
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,96 @@
1
+ module Spree
2
+ class ShipCompliant
3
+ extend ActiveModel::Naming
4
+ attr_reader :order, :reimbursement, :shipment, :rates
5
+
6
+ SHIPPING_SERVICE = 'UPS'
7
+
8
+ def initialize(order = nil)
9
+ @order = order
10
+ @rates = []
11
+ end
12
+
13
+ def transaction_from_order
14
+ stock_location = order.shipments.first.try(:stock_location) || Spree::StockLocation.active.where('city IS NOT NULL and state_id IS NOT NULL').first
15
+ raise Spree.t(:ensure_one_valid_stock_location) unless stock_location
16
+
17
+ payload = {
18
+ address_option: {
19
+ ignore_street_level_errors: true,
20
+ reject_if_address_suggested: 'false'
21
+ },
22
+ include_sales_tax_rates: true,
23
+ sales_order: {
24
+ bill_to: address_from_spree_address(order.bill_address),
25
+ customer_key: SolidusShipCompliant::Config.partner_key,
26
+ sales_order_key: order.number,
27
+ purchase_date: DateTime.now,
28
+ order_type: 'Internet',
29
+ shipments: {
30
+ shipment: {
31
+ shipment_status: 'SentToFulfillment',
32
+ discounts: nil,
33
+ fulfillment_house: 'InHouse',
34
+ handling: 0,
35
+ insured_amount: 0,
36
+ license_relationship: 'Default',
37
+ packages: nil,
38
+ ship_date: DateTime.now,
39
+ shipping_service: SHIPPING_SERVICE,
40
+ shipment_items: shipment_items,
41
+ ship_to: address_from_spree_address(order.ship_address)
42
+ }
43
+ }
44
+ }
45
+ }
46
+
47
+ begin
48
+ initialize_client
49
+ transaction = ::ShipCompliant::CheckCompliance.of_sales_order(payload)
50
+ rescue ::ShipCompliant::CheckComplianceResult
51
+ end
52
+
53
+ transaction
54
+ end
55
+
56
+ # Note that this method can take either a Spree::StockLocation (which has address
57
+ # attributes directly on it) or a Spree::Address object
58
+ def address_from_spree_address(address)
59
+ ::ShipCompliant::Address.new(
60
+ first_name: address.first_name,
61
+ last_name: address.last_name,
62
+ country: address.country.iso,
63
+ city: address.city,
64
+ state: address.try(:state).try(:abbr),
65
+ street1: address.address1,
66
+ street2: address.address2,
67
+ zip1: address.zipcode.try(:[], 0...5)).address
68
+ end
69
+
70
+ def shipment_items
71
+ items = []
72
+
73
+ order.line_items.map do |item|
74
+ items << {
75
+ shipment_item: {
76
+ discounts: nil,
77
+ brand_key: item.variant.brand_key,
78
+ product_key: item.variant.product_key,
79
+ product_quantity: item.quantity,
80
+ product_unit_price: item.price.to_i
81
+ }
82
+ }
83
+ end
84
+
85
+ items
86
+ end
87
+
88
+ def initialize_client
89
+ ::ShipCompliant.configure do |c|
90
+ c.partner_key = SolidusShipCompliant::Config.partner_key
91
+ c.username = SolidusShipCompliant::Config.username
92
+ c.password = SolidusShipCompliant::Config.password
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,11 @@
1
+ Spree::Taxon.class_eval do
2
+ def brand_key
3
+ words = name.split(' ')
4
+
5
+ if words.count > 1
6
+ words.map(&:chr).join.upcase
7
+ else
8
+ name.chars.first(3).join.upcase
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,73 @@
1
+ Spree::Variant.class_eval do
2
+ delegate :bottle_size, :default_case, :default_wholesale_case_price,
3
+ :percent_alcohol, :varietal, :vintage, :volume_amount,
4
+ :volume_unit, to: :product
5
+
6
+ def brand_key
7
+ brand = product.taxons.joins(:taxonomy).find_by(spree_taxonomies: { name: 'Brand' })
8
+ brand.try(:brand_key)
9
+ end
10
+
11
+ def product_key
12
+ [sku, option_values_ids].compact.join('-')
13
+ end
14
+
15
+ ##
16
+ # Ship Compliant necessary attributes:
17
+ #
18
+ # bottle_size: 750ml
19
+ # default_case: 12
20
+ # default_wholesale_case_price: 670
21
+ # percent_alcohol: 5%
22
+ # varietal: 'Cabernet Sauvignon'
23
+ # vintage: 2003
24
+ # volume_amount: 750
25
+ # volume_unit: milliliter
26
+ ##
27
+
28
+ def bottle_size
29
+ product.option_types.find_by(presentation: 'Bottle Size').try(:presentation)
30
+ end
31
+
32
+ def default_case
33
+ product.property('default_case') || 1
34
+ end
35
+
36
+ def default_wholesale_case_price
37
+ product.property('default_wholesale_case_price')
38
+ end
39
+
40
+ def percent_alcohol
41
+ product.property('percent_alcohol')
42
+ end
43
+
44
+ def varietal
45
+ product.property('varietal')
46
+ end
47
+
48
+ def vintage
49
+ product.property('vintage')
50
+ end
51
+
52
+ def volume_amount
53
+ product.property('volume_amount')
54
+ end
55
+
56
+ def volume_unit
57
+ product.property('volume_unit')
58
+ end
59
+
60
+ private
61
+
62
+ def option_values_ids
63
+ values = option_values.includes(:option_type).sort_by do |option_value|
64
+ option_value.option_type.position
65
+ end
66
+
67
+ values.to_a.map! do |ov|
68
+ "#{ov.id}"
69
+ end
70
+
71
+ values.any? ? values.join('-') : nil
72
+ end
73
+ end
@@ -0,0 +1,3 @@
1
+ <!-- insert_bottom "[data-hook=admin_configurations_sidebar_menu]" -->
2
+
3
+ <%= configurations_sidebar_menu_item Spree.t('admin.tab.ship_compliant'), edit_admin_ship_compliant_settings_path %>
@@ -0,0 +1,9 @@
1
+ <% content_for :tabs do %>
2
+ <nav>
3
+ <ul class="tabs" data-hook="admin_settings_ship_complaint_tabs">
4
+ <% if can? :display, Spree::ShipCompliant %>
5
+ <%= settings_tab_item plural_resource_name(Spree::ShipCompliant), spree.edit_admin_ship_compliant_settings_path %>
6
+ <% end %>
7
+ </ul>
8
+ </nav>
9
+ <% end %>
@@ -0,0 +1,30 @@
1
+ <%= render partial: 'spree/admin/shared/configuration_menu' %>
2
+
3
+ <% content_for :page_title do %>
4
+ <%= Spree.t(:ship_compliant_settings) %>
5
+ <% end %>
6
+
7
+ <%= form_tag admin_ship_compliant_settings_path, method: :put do %>
8
+ <div id="preferences" data-hook>
9
+ <div class="row">
10
+ <div class="col-md-6">
11
+ <fieldset class="no-border-bottom">
12
+ <% @preferences_ship_compliant.each do |name| %>
13
+ <div class="form-group">
14
+ <%= render "spree/admin/shared/preference_fields/#{@config.preference_type(name)}",
15
+ name: name.to_s,
16
+ value: @config[name],
17
+ label: t(name.to_s, scope: 'spree', default: name.to_s.humanize) %>
18
+ </div>
19
+ <% end %>
20
+ </fieldset>
21
+ </div>
22
+ </div>
23
+
24
+ <div class="form-buttons" data-hook="buttons">
25
+ <%= button Spree.t('actions.update'), 'refresh' %>
26
+ <span class="or"><%= Spree.t(:or) %></span>
27
+ <%= button_link_to Spree.t('actions.cancel'), admin_orders_url, icon: 'delete' %>
28
+ </div>
29
+ </div>
30
+ <% end %>
@@ -0,0 +1,7 @@
1
+ # This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application.
2
+
3
+ ENGINE_ROOT = File.expand_path('../..', __FILE__)
4
+ ENGINE_PATH = File.expand_path('../../lib/solidus_ship_compliant/engine', __FILE__)
5
+
6
+ require 'rails/all'
7
+ require 'rails/engine/commands'
@@ -0,0 +1,23 @@
1
+ en:
2
+ activerecord:
3
+ attributes:
4
+ spree/ship_compliant:
5
+ username: Username
6
+ password: Password
7
+ partner_key: Partner Key
8
+ spree:
9
+ admin:
10
+ tab:
11
+ ship_compliant: Ship Compliant Settings
12
+ vendors: Vendors
13
+ ship_compliant_tax: Ship Compliant Tax
14
+ ship_compliant_settings_updated: The shipment compliant settings have been successfully updated!
15
+ ship_compliant_settings: Ship Compliant Settings
16
+ taxonomy_brands_name: Brand
17
+ taxonomy_categories_name: Categories
18
+ taxonomy_types_name: Type
19
+ ship_compliant_alert: ShipCompliant cannot calculate inclusive sales taxes
20
+ store_locator: Store Location
21
+ store_locators: Store Locations
22
+ locator_address: Locator Address
23
+ available_shipping_locators: Available Shipping Locators
@@ -0,0 +1,5 @@
1
+ Spree::Core::Engine.routes.draw do
2
+ namespace :admin do
3
+ resource :ship_compliant_settings
4
+ end
5
+ end
@@ -0,0 +1,20 @@
1
+ module SolidusShipCompliant
2
+ module Generators
3
+ class InstallGenerator < Rails::Generators::Base
4
+ class_option :auto_run_migrations, type: :boolean, default: false
5
+
6
+ def add_migrations
7
+ run 'bundle exec rake railties:install:migrations FROM=solidus_ship_compliant'
8
+ end
9
+
10
+ def run_migrations
11
+ run_migrations = options[:auto_run_migrations] || ['', 'y', 'Y'].include?(ask('Would you like to run the migrations now? [Y/n]'))
12
+ if run_migrations
13
+ run 'bundle exec rake db:migrate'
14
+ else
15
+ puts 'Skipping rake db:migrate, don\'t forget to run it!'
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,2 @@
1
+ require 'solidus_core'
2
+ require 'solidus_ship_compliant/engine'
@@ -0,0 +1,7 @@
1
+ module SolidusShipCompliant
2
+ class Configuration < Spree::Preferences::Configuration
3
+ preference :username, :string, default: ''
4
+ preference :password, :string, default: ''
5
+ preference :partner_key, :string, default: ''
6
+ end
7
+ end
@@ -0,0 +1,31 @@
1
+ module SolidusShipCompliant
2
+ class Engine < Rails::Engine
3
+ require 'spree/core'
4
+ isolate_namespace Spree
5
+ engine_name 'solidus_ship_compliant'
6
+
7
+ config.autoload_paths += %W(#{config.root}/lib)
8
+
9
+ # use rspec for tests
10
+ config.generators do |g|
11
+ g.test_framework :rspec
12
+ end
13
+
14
+ initializer 'solidus_ship_compliant.environment', before: :load_config_initializers do |app|
15
+ SolidusShipCompliant::Config = SolidusShipCompliant::Configuration.new
16
+ end
17
+
18
+ initializer 'solidus_ship_compliant.register.calculators', after: 'spree.register.calculators' do |app|
19
+ SolidusShipCompliant::Config = SolidusShipCompliant::Configuration.new
20
+ app.config.spree.calculators.tax_rates << Spree::Calculator::ShipCompliantCalculator
21
+ end
22
+
23
+ def self.activate
24
+ Dir.glob(File.join(File.dirname(__FILE__), '../../app/**/*_decorator*.rb')) do |c|
25
+ Rails.configuration.cache_classes ? require(c) : load(c)
26
+ end
27
+ end
28
+
29
+ config.to_prepare(&method(:activate).to_proc)
30
+ end
31
+ end
@@ -0,0 +1,4 @@
1
+ module SolidusShipCompliant
2
+ class Error < StandardError
3
+ end
4
+ end
@@ -0,0 +1,6 @@
1
+ FactoryBot.define do
2
+ # Define your Spree extensions Factories within this file to enable applications, and other extensions to use and override them.
3
+ #
4
+ # Example adding this to your spec_helper will load these Factories for use:
5
+ # require 'solidus_ship_compliant/factories'
6
+ end
@@ -0,0 +1,3 @@
1
+ module SolidusShipCompliant
2
+ VERSION = '1.0.0'
3
+ end