solidus_tax_cloud 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.circleci/config.yml +35 -0
- data/.gem_release.yml +5 -0
- data/.gitignore +20 -0
- data/.rspec +2 -0
- data/.rubocop.yml +7 -0
- data/.rubocop_todo.yml +132 -0
- data/CHANGELOG.md +52 -0
- data/Gemfile +33 -0
- data/LICENSE +26 -0
- data/README.md +130 -0
- data/Rakefile +6 -0
- data/TaxCloudImplementationVerificationGuide.pdf +0 -0
- data/app/assets/javascripts/spree/backend/solidus_tax_cloud.js +2 -0
- data/app/assets/javascripts/spree/frontend/solidus_tax_cloud.js +2 -0
- data/app/assets/stylesheets/spree/backend/solidus_tax_cloud.css +4 -0
- data/app/assets/stylesheets/spree/backend/solidus_tax_cloud.scss +0 -0
- data/app/assets/stylesheets/spree/frontend/solidus_tax_cloud.css +4 -0
- data/app/decorators/models/solidus_tax_cloud/spree/app_configuration_decorator.rb +18 -0
- data/app/decorators/models/solidus_tax_cloud/spree/line_item_decorator.rb +47 -0
- data/app/decorators/models/solidus_tax_cloud/spree/order_decorator.rb +42 -0
- data/app/decorators/models/solidus_tax_cloud/spree/product_decorator.rb +25 -0
- data/app/decorators/models/solidus_tax_cloud/spree/shipment_decorator.rb +27 -0
- data/app/models/spree/calculator/tax_cloud_calculator.rb +111 -0
- data/app/models/spree/tax_cloud.rb +66 -0
- data/app/overrides/spree/admin/products/_form.rb +9 -0
- data/app/overrides/spree/admin/shared/_configuration_menu.rb +9 -0
- data/bin/console +17 -0
- data/bin/r +15 -0
- data/bin/rails +15 -0
- data/bin/rake +7 -0
- data/bin/sandbox +84 -0
- data/bin/sandbox_rails +18 -0
- data/bin/setup +8 -0
- data/config/initializers/tax_cloud_usps_username.rb +5 -0
- data/config/locales/en.yml +13 -0
- data/config/routes.rb +7 -0
- data/db/migrate/20121220192438_create_spree_tax_cloud_transactions.rb +13 -0
- data/db/migrate/20121220193944_create_spree_tax_cloud_cart_items.rb +22 -0
- data/db/migrate/20130829215819_fix_scale_of_cart_item_prices.rb +15 -0
- data/db/migrate/20140623225628_add_tic_to_products.rb +11 -0
- data/lib/assets/javascripts/spree/frontend/solidus_tax_cloud.js.erb +1 -0
- data/lib/assets/stylesheets/spree/frontend/solidus_tax_cloud.css.erb +5 -0
- data/lib/controllers/backend/spree/admin/tax_cloud_settings_controller.rb +29 -0
- data/lib/decorators/backend/controllers/solidus_tax_cloud/spree/admin/orders_controller_decorator.rb +21 -0
- data/lib/decorators/frontend/controllers/solidus_tax_cloud/spree/checkout_controller_decorator.rb +24 -0
- data/lib/generators/solidus_tax_cloud/install/install_generator.rb +29 -0
- data/lib/generators/solidus_tax_cloud/templates/ca-bundle.crt +3895 -0
- data/lib/solidus_tax_cloud.rb +10 -0
- data/lib/solidus_tax_cloud/engine.rb +23 -0
- data/lib/solidus_tax_cloud/error.rb +6 -0
- data/lib/solidus_tax_cloud/factories.rb +4 -0
- data/lib/solidus_tax_cloud/version.rb +5 -0
- data/lib/tasks/.gitkeep +0 -0
- data/lib/tasks/tax_cloud.rake +74 -0
- data/lib/views/backend/spree/admin/products/_edit_tax_cloud_tic.html.erb +6 -0
- data/lib/views/backend/spree/admin/tax_cloud_settings/edit.html.erb +32 -0
- data/solidus_tax_cloud.gemspec +39 -0
- data/spec/features/checkout_spec.rb +511 -0
- data/spec/models/tax_cloud_api_spec.rb +192 -0
- data/spec/spec_helper.rb +27 -0
- data/spec/support/capybara.rb +20 -0
- data/spec/support/tax_cloud.rb +20 -0
- data/spec/support/transactions.rb +5 -0
- metadata +204 -0
data/Rakefile
ADDED
Binary file
|
File without changes
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SolidusTaxCloud
|
4
|
+
module Spree
|
5
|
+
module AppConfigurationDecorator
|
6
|
+
def self.prepended(base)
|
7
|
+
base.class_eval do
|
8
|
+
preference :taxcloud_default_product_tic, :string, default: '00000'
|
9
|
+
preference :taxcloud_shipping_tic, :string, default: '11010'
|
10
|
+
end
|
11
|
+
|
12
|
+
Rails.application.config.spree.calculators.tax_rates << ::Spree::Calculator::TaxCloudCalculator
|
13
|
+
end
|
14
|
+
|
15
|
+
::Spree::AppConfiguration.prepend self
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SolidusTaxCloud
|
4
|
+
module Spree
|
5
|
+
module LineItemDecorator
|
6
|
+
def tax_cloud_cache_key
|
7
|
+
if ActiveRecord::Base.try(:cache_versioning)
|
8
|
+
cache_key
|
9
|
+
else
|
10
|
+
key = "Spree::LineItem #{id}: #{quantity}x<#{variant.cache_key}>@#{total_excluding_vat}#{currency}"
|
11
|
+
|
12
|
+
if order.ship_address
|
13
|
+
key += "shipped_to<#{order.ship_address.try(:cache_key)}>"
|
14
|
+
elsif order.billing_address
|
15
|
+
key += "billed_to<#{order.bill_address.try(:cache_key)}>"
|
16
|
+
end
|
17
|
+
|
18
|
+
key
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def tax_cloud_cache_version
|
23
|
+
if ActiveRecord::Base.try(:cache_versioning)
|
24
|
+
key = "Spree::LineItem #{id}: #{quantity}x<#{variant.cache_version}>@#{total_excluding_vat}#{currency}"
|
25
|
+
|
26
|
+
if order.ship_address
|
27
|
+
key += "shipped_to<#{order.ship_address.try(:cache_version)}>"
|
28
|
+
elsif order.billing_address
|
29
|
+
key += "billed_to<#{order.bill_address.try(:cache_version)}>"
|
30
|
+
end
|
31
|
+
|
32
|
+
key
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def price_with_discounts
|
37
|
+
round_to_two_places(total_excluding_vat / quantity)
|
38
|
+
end
|
39
|
+
|
40
|
+
def round_to_two_places(amount)
|
41
|
+
BigDecimal(amount.to_s).round(2, BigDecimal::ROUND_HALF_UP)
|
42
|
+
end
|
43
|
+
|
44
|
+
::Spree::LineItem.prepend self
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SolidusTaxCloud
|
4
|
+
module Spree
|
5
|
+
module OrderDecorator
|
6
|
+
def self.prepended(base)
|
7
|
+
base.class_eval do
|
8
|
+
state_machine.after_transition to: :complete, do: :capture_tax_cloud
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def capture_tax_cloud
|
13
|
+
return unless is_taxed_using_tax_cloud?
|
14
|
+
|
15
|
+
response = ::Spree::TaxCloud.transaction_from_order(self).authorized_with_capture
|
16
|
+
if response != 'OK'
|
17
|
+
Rails.logger.error "ERROR: TaxCloud returned an order capture response of #{response}."
|
18
|
+
end
|
19
|
+
log_tax_cloud(response)
|
20
|
+
end
|
21
|
+
|
22
|
+
# Order.tax_zone.tax_rates is used here to check if the order is taxable by Tax Cloud.
|
23
|
+
# It's not possible check against the order's tax adjustments because
|
24
|
+
# an adjustment is not created for 0% rates. However, US orders must be
|
25
|
+
# submitted to Tax Cloud even when the rate is 0%.
|
26
|
+
# Note that we explicitly use ship_address instead of tax_address,
|
27
|
+
# as per compliance with Tax Cloud instructions.
|
28
|
+
def is_taxed_using_tax_cloud?
|
29
|
+
::Spree::TaxRate.for_address(ship_address).any? { |rate| rate.calculator_type == 'Spree::Calculator::TaxCloudCalculator' }
|
30
|
+
end
|
31
|
+
|
32
|
+
def log_tax_cloud(response)
|
33
|
+
# Implement into your own application.
|
34
|
+
# You could create your own Log::TaxCloud model then use either HStore or
|
35
|
+
# JSONB to store the response.
|
36
|
+
# The response argument carries the response from an order transaction.
|
37
|
+
end
|
38
|
+
|
39
|
+
::Spree::Order.prepend self
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SolidusTaxCloud
|
4
|
+
module Spree
|
5
|
+
module ProductDecorator
|
6
|
+
def self.prepended(base)
|
7
|
+
base.class_eval do
|
8
|
+
validates_format_of :tax_cloud_tic, with: /\A\d{5}\z/, message: I18n.t('spree.standard_taxcloud_tic')
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
# Use the store-default TaxCloud product TIC if none is defined for this product
|
13
|
+
def tax_cloud_tic
|
14
|
+
read_attribute(:tax_cloud_tic) || ::Spree::Config.taxcloud_default_product_tic
|
15
|
+
end
|
16
|
+
|
17
|
+
# Empty strings are written as nil (which avoids the format validation)
|
18
|
+
def tax_cloud_tic=(tic)
|
19
|
+
write_attribute(:tax_cloud_tic, tic.presence)
|
20
|
+
end
|
21
|
+
|
22
|
+
::Spree::Product.prepend self
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SolidusTaxCloud
|
4
|
+
module Spree
|
5
|
+
module ShipmentDecorator
|
6
|
+
def tax_cloud_cache_key
|
7
|
+
if ActiveRecord::Base.try(:cache_versioning)
|
8
|
+
cache_key
|
9
|
+
else
|
10
|
+
"#{cache_key}--from:#{stock_location.cache_key}--to:#{order.shipping_address.cache_key}"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def tax_cloud_cache_version
|
15
|
+
if ActiveRecord::Base.try(:cache_versioning)
|
16
|
+
"#{cache_version}--from:#{stock_location.cache_version}--to:#{order.shipping_address.cache_version}"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def price_with_discounts
|
21
|
+
total_excluding_vat
|
22
|
+
end
|
23
|
+
|
24
|
+
::Spree::Shipment.prepend self
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Spree
|
4
|
+
class Calculator::TaxCloudCalculator < Calculator::DefaultTax
|
5
|
+
def self.description
|
6
|
+
I18n.t('spree.tax_cloud')
|
7
|
+
end
|
8
|
+
|
9
|
+
# Default tax calculator still needs to support orders for legacy reasons
|
10
|
+
# Orders created before Spree 2.1 had tax adjustments applied to the order, as a whole.
|
11
|
+
# Orders created with Spree 2.2 and after, have them applied to the line items individually.
|
12
|
+
def compute_order(_order)
|
13
|
+
raise 'Spree::TaxCloud is designed to calculate taxes at the shipment and line-item levels.'
|
14
|
+
end
|
15
|
+
|
16
|
+
# When it comes to computing shipments or line items: same same.
|
17
|
+
def compute_shipment_or_line_item(item)
|
18
|
+
if rate.included_in_price
|
19
|
+
raise 'TaxCloud cannot calculate inclusive sales taxes.'
|
20
|
+
else
|
21
|
+
round_to_two_places(tax_for_item(item))
|
22
|
+
# TODO: take discounted_amount into account. This is a problem because TaxCloud API does not take discounts nor does it return percentage rates.
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
alias compute_shipment compute_shipment_or_line_item
|
27
|
+
alias compute_line_item compute_shipment_or_line_item
|
28
|
+
|
29
|
+
def compute_shipping_rate(_shipping_rate)
|
30
|
+
if rate.included_in_price
|
31
|
+
raise 'TaxCloud cannot calculate inclusive sales taxes.'
|
32
|
+
else
|
33
|
+
# Sales tax will be applied to the Shipment itself, rather than to the Shipping Rates.
|
34
|
+
# Note that this method is called from ShippingRate.display_price, so if we returned
|
35
|
+
# the shipping sales tax here, it would display as part of the display_price of the
|
36
|
+
# ShippingRate, which is not consistent with how US sales tax typically works -- i.e.,
|
37
|
+
# it is an additional amount applied to a sale at the end, rather than being part of
|
38
|
+
# the displayed cost of a good or service.
|
39
|
+
0
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def tax_for_item(item)
|
46
|
+
order = item.order
|
47
|
+
item_address = order.ship_address || order.billing_address
|
48
|
+
|
49
|
+
# Only calculate tax when we have an address and it's in our jurisdiction
|
50
|
+
return 0 unless item_address.present? && calculable.zone.include?(item_address)
|
51
|
+
|
52
|
+
# Cache will expire if the order, any of its line items, or any of its shipments change.
|
53
|
+
# When the cache expires, we will need to make another API call to TaxCloud.
|
54
|
+
Rails.cache.fetch(
|
55
|
+
['TaxCloudRatesForItem', item.tax_cloud_cache_key],
|
56
|
+
version: item.tax_cloud_cache_version,
|
57
|
+
time_to_idle: 30.minutes,
|
58
|
+
) do
|
59
|
+
# In the case of a cache miss, we recompute the amounts for _all_ the LineItems and Shipments for this Order.
|
60
|
+
# TODO An ideal implementation will break the order down by Shipments / Packages
|
61
|
+
# and use the actual StockLocation address for each separately, and create Adjustments
|
62
|
+
# for the Shipments to reflect tax on shipping.
|
63
|
+
transaction = Spree::TaxCloud.transaction_from_order(order)
|
64
|
+
lookup_cart_items = transaction.lookup.cart_items
|
65
|
+
|
66
|
+
# Now we will loop back through the items and assign them amounts from the lookup.
|
67
|
+
# This inefficient method is due to the fact that item_id isn't preserved in the lookup.
|
68
|
+
# TODO There may be a way to refactor this,
|
69
|
+
# possibly by overriding the TaxCloud::Responses::Lookup model
|
70
|
+
# or the CartItems model.
|
71
|
+
index = -1 # array is zero-indexed
|
72
|
+
|
73
|
+
item_tax_amount = nil
|
74
|
+
|
75
|
+
# Retrieve line_items from lookup
|
76
|
+
order.line_items.each do |line_item|
|
77
|
+
tax_amount = lookup_cart_items[index += 1].tax_amount
|
78
|
+
|
79
|
+
if line_item == item
|
80
|
+
item_tax_amount = tax_amount
|
81
|
+
else
|
82
|
+
Rails.cache.write(
|
83
|
+
['TaxCloudRatesForItem', line_item.tax_cloud_cache_key],
|
84
|
+
tax_amount,
|
85
|
+
version: line_item.tax_cloud_cache_version,
|
86
|
+
time_to_idle: 30.minutes,
|
87
|
+
)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
order.shipments.each do |shipment|
|
92
|
+
tax_amount = lookup_cart_items[index += 1].tax_amount
|
93
|
+
|
94
|
+
if shipment == item
|
95
|
+
item_tax_amount = tax_amount
|
96
|
+
else
|
97
|
+
Rails.cache.write(
|
98
|
+
['TaxCloudRatesForItem', shipment.tax_cloud_cache_key],
|
99
|
+
tax_amount,
|
100
|
+
version: shipment.tax_cloud_cache_version,
|
101
|
+
time_to_idle: 30.minutes,
|
102
|
+
)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
# Lastly, return the particular rate that we were initially looking for
|
107
|
+
item_tax_amount
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Spree
|
4
|
+
class TaxCloud
|
5
|
+
def self.transaction_from_order(order)
|
6
|
+
stock_location = order.shipments.first.try(:stock_location) || Spree::StockLocation.active.where('city IS NOT NULL and state_id IS NOT NULL').first
|
7
|
+
raise I18n.t('spree.ensure_one_valid_stock_location') unless stock_location
|
8
|
+
|
9
|
+
destination = address_from_spree_address(order.ship_address || order.billing_address)
|
10
|
+
begin
|
11
|
+
destination = destination.verify # Address validation may fail, and that is okay.
|
12
|
+
rescue ::TaxCloud::Errors::ApiError
|
13
|
+
end
|
14
|
+
|
15
|
+
transaction = ::TaxCloud::Transaction.new(
|
16
|
+
customer_id: order.user_id || order.email,
|
17
|
+
order_id: order.number,
|
18
|
+
cart_id: order.number,
|
19
|
+
origin: address_from_spree_address(stock_location),
|
20
|
+
destination: destination
|
21
|
+
)
|
22
|
+
|
23
|
+
index = -1 # array is zero-indexed
|
24
|
+
# Prepare line_items for lookup
|
25
|
+
order.line_items.each { |line_item| transaction.cart_items << cart_item_from_item(line_item, index += 1) }
|
26
|
+
# Prepare shipments for lookup
|
27
|
+
order.shipments.each { |shipment| transaction.cart_items << cart_item_from_item(shipment, index += 1) }
|
28
|
+
transaction
|
29
|
+
end
|
30
|
+
|
31
|
+
# Note that this method can take either a Spree::StockLocation (which has address
|
32
|
+
# attributes directly on it) or a Spree::Address object
|
33
|
+
def self.address_from_spree_address(address)
|
34
|
+
::TaxCloud::Address.new(
|
35
|
+
address1: address.address1,
|
36
|
+
address2: address.address2,
|
37
|
+
city: address.city,
|
38
|
+
state: address.try(:state).try(:abbr),
|
39
|
+
zip5: address.zipcode.try(:[], 0...5)
|
40
|
+
)
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.cart_item_from_item(item, index)
|
44
|
+
case item
|
45
|
+
when Spree::LineItem
|
46
|
+
::TaxCloud::CartItem.new(
|
47
|
+
index: index,
|
48
|
+
item_id: item.try(:variant).try(:sku).presence || "LineItem #{item.id}",
|
49
|
+
tic: (item.product.tax_cloud_tic || Spree::Config.taxcloud_default_product_tic),
|
50
|
+
price: item.price_with_discounts,
|
51
|
+
quantity: item.quantity
|
52
|
+
)
|
53
|
+
when Spree::Shipment
|
54
|
+
::TaxCloud::CartItem.new(
|
55
|
+
index: index,
|
56
|
+
item_id: "Shipment #{item.number}",
|
57
|
+
tic: Spree::Config.taxcloud_shipping_tic,
|
58
|
+
price: item.price_with_discounts,
|
59
|
+
quantity: 1
|
60
|
+
)
|
61
|
+
else
|
62
|
+
raise I18n.t('spree.cart_item_cannot_be_made')
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
Deface::Override.new(
|
4
|
+
virtual_path: 'spree/admin/products/_form',
|
5
|
+
name: 'add_tic_to_admin_product_edit',
|
6
|
+
insert_after: "[data-hook='admin_product_form_tax_category']",
|
7
|
+
partial: 'spree/admin/products/edit_tax_cloud_tic',
|
8
|
+
original: 'a6d7d1941bde020c34025a78466febd8453cf71a'
|
9
|
+
)
|
@@ -0,0 +1,9 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
Deface::Override.new(
|
4
|
+
virtual_path: 'spree/admin/shared/_configuration_menu',
|
5
|
+
name: 'add_tax_cloud_admin_menu_link',
|
6
|
+
insert_bottom: "[data-hook='admin_configurations_sidebar_menu']",
|
7
|
+
text: "<%= configurations_sidebar_menu_item 'TaxCloud Settings', edit_admin_tax_cloud_settings_path %>",
|
8
|
+
original: 'dcaffe234d5b43d5b7673bc13f227500957c9e73'
|
9
|
+
)
|
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_tax_cloud"
|
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/r
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
# This command will automatically be run when you run "rails" with Rails gems
|
5
|
+
# installed from the root of your application.
|
6
|
+
|
7
|
+
ENGINE_ROOT = File.expand_path('..', __dir__)
|
8
|
+
ENGINE_PATH = File.expand_path('../lib/solidus_tax_cloud/engine', __dir__)
|
9
|
+
|
10
|
+
# Set up gems listed in the Gemfile.
|
11
|
+
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
|
12
|
+
require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
|
13
|
+
|
14
|
+
require 'rails/all'
|
15
|
+
require 'rails/engine/commands'
|
data/bin/rails
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# frozen_string_literal: true
|
4
|
+
|
5
|
+
app_root = 'spec/dummy'
|
6
|
+
|
7
|
+
unless File.exist? "#{app_root}/bin/rails"
|
8
|
+
system "bin/rake", app_root or begin # rubocop:disable Style/AndOr
|
9
|
+
warn "Automatic creation of the dummy app failed"
|
10
|
+
exit 1
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
Dir.chdir app_root
|
15
|
+
exec 'bin/rails', *ARGV
|