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.
- checksums.yaml +7 -0
- data/.gitignore +15 -0
- data/.rspec +1 -0
- data/.rubocop.yml +231 -0
- data/.travis.yml +21 -0
- data/Gemfile +19 -0
- data/LICENSE +26 -0
- data/README.md +49 -0
- data/Rakefile +21 -0
- data/app/assets/javascripts/spree/backend/solidus_ship_complaint.js +2 -0
- data/app/assets/javascripts/spree/frontend/solidus_ship_complaint.js +2 -0
- data/app/assets/stylesheets/spree/backend/solidus_ship_complaint.css +4 -0
- data/app/assets/stylesheets/spree/frontend/solidus_ship_complaint.css +4 -0
- data/app/controllers/spree/admin/orders_controller_decorator.rb +7 -0
- data/app/controllers/spree/admin/ship_compliant_settings_controller.rb +27 -0
- data/app/controllers/spree/checkout_controller_decorator.rb +6 -0
- data/app/models/spree/calculator/ship_compliant_calculator.rb +83 -0
- data/app/models/spree/ship_compliant.rb +96 -0
- data/app/models/spree/taxon_decorator.rb +11 -0
- data/app/models/spree/variant_decorator.rb +73 -0
- data/app/overrides/spree/admin/shared/_configuration_menu/_add_ship_compliant_config_menu.html.erb.deface +3 -0
- data/app/views/spree/admin/shared/_ship_complaint_tabs.html.erb +9 -0
- data/app/views/spree/admin/ship_compliant_settings/edit.html.erb +30 -0
- data/bin/rails +7 -0
- data/config/locales/en.yml +23 -0
- data/config/routes.rb +5 -0
- data/lib/generators/solidus_ship_compliant/install/install_generator.rb +20 -0
- data/lib/solidus_ship_compliant.rb +2 -0
- data/lib/solidus_ship_compliant/configuration.rb +7 -0
- data/lib/solidus_ship_compliant/engine.rb +31 -0
- data/lib/solidus_ship_compliant/error.rb +4 -0
- data/lib/solidus_ship_compliant/factories.rb +6 -0
- data/lib/solidus_ship_compliant/version.rb +3 -0
- data/lib/tasks/solidus_ship_compliant/export.rake +78 -0
- data/solidus_ship_compliant.gemspec +35 -0
- data/spec/controllers/admin/ship_compliant_settings_controller_spec.rb +39 -0
- data/spec/factories/variant_factory_override.rb +0 -0
- data/spec/features/checkout_spec.rb +0 -0
- data/spec/models/calculator/ship_compliant_calculator_spec.rb +19 -0
- data/spec/models/spree/ship_compliant_spec.rb +21 -0
- data/spec/models/spree/taxon_spec.rb +16 -0
- data/spec/models/spree/variant_spec.rb +70 -0
- data/spec/spec_helper.rb +24 -0
- data/spec/support/ability.rb +15 -0
- data/spec/support/database_cleaner.rb +23 -0
- data/spec/support/devise.rb +5 -0
- data/spec/support/factory_bot.rb +5 -0
- data/spec/support/preferences.rb +5 -0
- data/spec/support/spree.rb +14 -0
- metadata +307 -0
@@ -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,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,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 %>
|
data/bin/rails
ADDED
@@ -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
|
data/config/routes.rb
ADDED
@@ -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,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,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
|