solidus_ship_compliant 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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