Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RfC Make shipping rates taxable like line items or shipments #759

Closed
wants to merge 7 commits into from
Closed

RfC Make shipping rates taxable like line items or shipments #759

wants to merge 7 commits into from

Conversation

mamhoff
Copy link
Contributor

@mamhoff mamhoff commented Jan 28, 2016

The taxation code for shipping rates is distributed over Spree::ShippingRate, Spree::Stock::Estimator and the worst parts of Spree::TaxRate. The aim of the taxation refactoring I'm doing is to isolate taxation code to as few moving parts as possible. As discussed on Slack, I'm introducing a spec for taxable items so that I can easily test whether a shipment can possibly be tax adjusted using the tax adjustment mechanism we already have: Spree::Taxrate.adjust.

@cbrunsdon
Copy link
Contributor

On first pass this looks like a fantastic step forward. Nice call. Will review in depth when I get a minute.

@mamhoff mamhoff changed the title WIP Make shipping rates taxable like line items or shipments RfC Make shipping rates taxable like line items or shipments Jan 29, 2016
@mamhoff
Copy link
Contributor Author

mamhoff commented Jan 29, 2016

I think this works for a new shop. For existing shipping rates, this is a breaking change. However, things are mitigated by the fact that all order-relevant data is actually stored on the shipments.

Do we need to migrate? If so: How would we do that?

@jhawthorn
Copy link
Contributor

I really like this and I think it's a good direction, but I have some concerns we should think about when reviewing.

As Martin points out, we'll need some sort of migration strategy to move over older shipping rates.

This is the first time we've calculated and stored tax rates on an item which was not included in the order's total proper. This could have issues if a site did any reporting by counting adjustments through their order_id. Maybe we should consider having order_id=nil for these objects.

To be clear, I think this is a good step forward, I just want extra caution as always with anything tax-related.

@mamhoff
Copy link
Contributor Author

mamhoff commented Feb 5, 2016

As for the old shipping rates: We have to consider that old shipping rates might have been taxed using different tax rates, as they might have been deleted or updated in the meantime. This opens up another issue I've been hoping to be able to defer: How can we transparently handle changing tax rates? I.e.: A country raises their Sales tax by the beginning of, say, 2016... Ideally I'd have tax rates be immutable, and some kind of applicable_from and applicable_until field to filter for those that are valid right now.

For deleted tax rates, which are paranoid, we can backfill the tax rates' applicability dates using created_at and deleted_at. For updated ones, we're out of luck (but in these cases the current code also applies the updated ones now).

It'd be a lot simpler to move that migration to the end of this branch, and not try stuffing it into the current situation, as there's lots of places where that might be necessary.

@mamhoff
Copy link
Contributor Author

mamhoff commented Feb 5, 2016

As for the order_id = nil question: I'm hesitant on that one, as it would mean that shipping rate taxes would not be recalculated in the order updater.

Given that the all_adjustments association only made it into Solidus in October last year, I would doubt people use it to generate reports - and if they did, I'd wonder what sense it would make to group tax adjustments and (potentially non-eligible) promotion adjustments in one report.

it { is_expected.to respond_to(:pre_tax_amount) }
it { is_expected.to respond_to(:pre_tax_amount=) }

# TODO taxable items should not need a pre tax amount column

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Annotation keywords like TODO should be all upper case, followed by a colon, and a space, then a note describing the problem.

The shared example group "a taxable item" spells out what API has to have to
be tax adjusted using the item adjuster class.
This mostly consists in adding an attribute alias for the
misnamed `cost` attribute to `amount`.
With the common API for shipments in place, this method can be simplified a lot.

We also get rid of an unnecessary `.any?` check here.
Adds `included_tax_total`, `additional_tax_total` and `pre_tax_amount` to
Spree::ShippingRates.

`promo_total`, which would have also been possible, is implemented as a dummy setter
and a getter that always returns zero. Shipping rates do not have promotions applied
to them, that is what happens when they are used for a shipment.
Rather that calculating from only one tax rate on the fly,
use Solidus' taxation system using adjustments to calculate tax
amounts.
@mamhoff
Copy link
Contributor Author

mamhoff commented Feb 17, 2016

Rebased on current master, including the new shipping specs.

let(:original_shipment) { shipped_order.shipments.first }
let(:original_stock_location) { original_shipment.stock_location }
let(:original_inventory_unit) { shipped_order.inventory_units.first }
let(:original_variant) { original_inventory_unit.variant }
let(:shipping_method) { create(:shipping_method, tax_category: original_variant.tax_category)}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Space missing inside }.

Shipments are now correctly adjusted again in a large majority of cases.

For the time being, the adjusting should happen in `Shipment`.

it_behaves_like 'a taxable item'


Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extra blank line detected.

There is a concern over shipping rate adjustments messing up
shop reporting - as those do not actually count towards the order total,
but are actually more estimations of what's going happen if people
use the `Spree::Order#all_adjustments` association, which the shipping rate
adjustments would be part of.

We had earlier discussed setting the `order_id` for those adjustments to `nil`,
but that makes the shipping rate adjustments invalid. Setting the `order_id` on
adjustments to `nil` would require the presence validation for the order to be
conditional on something - in effect, touching the `Spree::Adjustment` class,
which is not a beast I want to touch here.

Now, there is a documented Boolean on adjustments that literally says:

>  1. *eligible?*
>
>     This boolean attributes stores whether this adjustment is currently
>     eligible for its order. Only eligible adjustments count towards the
>     order's adjustment total. This allows an adjustment to be preserved if
>     it becomes ineligible so it might be reinstated.

While this was initially conceived for promotion adjustments, it can just as well
be used for tax adjustments.

This commit adds new instance methods on taxable items (Spree::LineItem, Spree::Shipment and newly
Spree::ShippingRate) indicating whether they contribute to the order total. If they do, their
adjustments surely must as well, and if they don't (as in the case of shipping rates), they
answer `false` when asked: Are you `eligible?`.

While this is not a cure-for-all (If your reporting does not honour the `eligible` bit, you'll
still have wrong numbers in your tax reports), I think it provides the necessary
distinguiability for reporting.

Also, the complexity increase for the taxation system is small, and the adjustment system in
general is not even touched.

The order updater explicitly only calls adjustments on line items and shipments, or adjustments that
have the order instance has adjustable when calculating the `adjustment_total`.
`Spree::Order#all_adjustments` is only executed for re-calculating adjustment amounts.
@mamhoff
Copy link
Contributor Author

mamhoff commented Feb 17, 2016

I do not think we need to migrate, even for existing shops:

Shipping rate adjustments actually do not show up anywhere except for the select box where customers choose a shipping method in the shipment step. I'm not sure we need a migration for this: If the order is complete, we don't show shipping options anymore; if it's not complete and the customer chooses the shipping method again, he'll have gone through a state transition on the order where shipments and shipping rates get re-calculated.

Setting order_id to nil:

This would require patching Spree::Adjustment to not validate the presence of the order id under some circumstances. I'm now using the eligible bit, which indicates whether an adjustment actually counts toward the adjustment total of an order, and I ask the item being tax adjusted whether it's eligible itself when I set it. See the commit message for 2eb5d0a for more explanations.

@mamhoff
Copy link
Contributor Author

mamhoff commented Feb 18, 2016

In private conversation with @jordan-brough he voiced concerns about further cementing eligible adjustments, or using the adjustment system for something other than actually adjusting orders.

Likewise, other members of the community would like to see something like taxation system that is pluggable, and not even necessarily in core.

#862 addresses some of the issues we currently have with shipping rates, and the refactoring of the display_price method can be ported to that as well. Furthermore, it's a better step towards isolating Spree's inner workings from the taxation stuff.

These were supposed to be complementary, but can also be thought of different ways of achieving similar goals. @jordan-brough I'd still like you to look over this though; many thanks for yesterday's conversation!

@jordan-brough
Copy link
Contributor

@mamhoff thanks for all the details! I've been looking at this today and am still wrapping my brain around everything. Will look some more and leave more thoughts tomorrow.

@jhawthorn
Copy link
Contributor

I'm starting to be less terrified by this change :). Making the shipping rate adjustments eligible=false helps my confidence in safe upgrades a lot.

A couple concerns still:

  • eligible? on the taxable items isn't the most descriptive. Looking at line_item.eligible? the purpose of that method is unclear. Maybe something like tax_adjustments_eligible?.
  • The adjustments on ShippingRates don't seem to be deleted when they are. (Orphan records added to the DB when refresh_rates is called)
  • This adds a lot of queries when calculating shipping rates. Maybe unavoidable?

@mamhoff
Copy link
Contributor Author

mamhoff commented Feb 24, 2016

I'm actually less and less a fan of this. It introduces useless columns (promo_total for something that can't be promoted, for example - but really all the totals as they aren't ever used).

#862 showed me that this is possible entirely without adjustments, at the expense of quite a few database calls. I think I can find a solid middle ground with a new AR model Spree::Tax::ShippingRateTaxEstimation which would just have an amount and a tax rate, and has no direct connection to the order (because it doesn't need it). That can also be easily used for migrating old shipping rates.

Having ItemAdjustments do a bunch of re-calculations every time the order is updated IMO doesn't help a lot either. Stay tuned.

@mamhoff
Copy link
Contributor Author

mamhoff commented Feb 25, 2016

Closing this in favor of #904. Much more fun.

@mamhoff mamhoff closed this Feb 25, 2016
@mamhoff mamhoff deleted the tax-adjustable-shipping-rates branch May 24, 2016 19:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants