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

Tax Categories on Line Items respect updates to Variant and Product Tax Categories #6059

Open
wants to merge 6 commits into
base: main
Choose a base branch
from

Conversation

harmonymjb
Copy link
Contributor

Summary

This PR was initiated in response to this reported issue:
Fixes #4683

We discovered that the LineItem's tax_category_id is currently being set in a before_validation in the LineItem model.
This means that the tax_category_id on the LineItem is set only once, when the LineItem is validated on the first save.

Upon investigation, we found that the tax_category_id was added on LineItems for performance reasons long in Spree's past:
spree/spree#3481 (This PR doesn't display the correct commits, but in the comments the associated commits are linked)

We wanted to be cognizent of performance here, but additionally attempt to fix the issue, so benchmarks and methodology are included below.

Fixing the issue

The underlying issue here has to do with setting the tax_category_id only once when the LineItem is created.
This means that changing the Tax Category on a Variant or Product will only have an effect on new Line Items created after the change.
If the associated Tax Category is soft deleted, the Line Item will be considered by the adjuster to have no Tax Category, no matter which Tax Categories exist on the associated Variant or Product.

To solve this issue, we decided to ensure that the Variant or Product's Tax Categories are considered when computing taxes.
The tax_category_id on the LineItem model still exists, and it's value is set at the same time that adjustments are created. This means that the tax_category_id that is present on a LineItem
always points to the Tax Category that was used to create the tax adjustments that exist for the LineItem.
If the Variant's Tax Category is changed, the next time tax adjustments are computed for the LineItem (i.e. during recalculate), the LineItems tax_category_id will be updated to reflect the
new relevant Tax Category.

If the Tax Category associated with some LineItems is soft-deleted, taxes will not be calculated based on this soft-deleted Tax Category, but will defer to the variant_tax_category_id.
On the other hand if taxes are not recalculated for that LineItem, the LineItem's tax_category_id will still point to that record and will represent the Tax Category that was used when taxes were calculated. These records can then be retrieved with .with_discarded if needed.

Benchmarking

I made temporary changes to our order_updater_spec tax_recalculation context:

        before do
          50.times do
            create(:line_item, order:, variant: create(:variant, tax_category:), price: 10)
          end
        end

        it 'benchmarks recalculate' do
          Benchmark.bm { |x| x.report { 50.times { order.recalculate } } }
        end 

And I ran the test 10 times before and after the changes:

On main:

    user     system      total        real
    1.519667   0.015154   1.534821 (  1.633398)
    1.539250   0.014212   1.553462 (  1.656476)
    1.486230   0.023042   1.509272 (  1.609028)
    1.496671   0.024544   1.521215 (  1.621268)
    1.517082   0.018463   1.535545 (  1.619166)
    1.515210   0.019308   1.534518 (  1.605963)
    1.496039   0.024313   1.520352 (  1.614227)
    1.498017   0.026294   1.524311 (  1.621422)
    1.522568   0.018466   1.541034 (  1.685498)
    1.474430   0.020689   1.495119 (  1.591637)
    avg real (  1.625808)
    avg real per recalculate (  0.032516)

After Changes (this branch):

   user     system      total        real
   1.598846   0.020576   1.619422 (  1.725181)
   1.575519   0.020300   1.595819 (  1.700659)
   1.584723   0.020872   1.605595 (  1.710245)
   1.607924   0.014587   1.622511 (  1.698217)
   1.556669   0.025032   1.581701 (  1.684801)
   1.581550   0.025173   1.606723 (  1.714748)
   1.608484   0.018360   1.626844 (  1.731001)
   1.596435   0.020785   1.617220 (  1.723850)
   1.610086   0.022443   1.632529 (  1.742005)
   1.606265   0.032092   1.638357 (  1.726858)
   avg real (  1.715757)
   avg real per recalculate (  0.034315)

An average increase of 1.799ms per recalculate call on orders with 50 Line Items.

Checklist

Check out our PR guidelines for more details.

The following are mandatory for all PRs:

The following are not always needed:

  • 📖 I have updated the README to account for my changes.
  • 📑 I have documented new code with YARD.
  • 🛣️ I have opened a PR to update the guides.
  • ✅ I have added automated tests to cover my changes.
  • 📸 I have attached screenshots to demo visual changes.

@harmonymjb harmonymjb requested a review from a team as a code owner January 7, 2025 22:21
@github-actions github-actions bot added the changelog:solidus_core Changes to the solidus_core gem label Jan 7, 2025
@harmonymjb harmonymjb force-pushed the tax-categories-on-line-items-respect-updates branch from bd4fe21 to e8bc665 Compare January 7, 2025 22:26
@@ -9,7 +9,7 @@ def rates_for_item(item)
@rates_for_item ||= Spree::TaxRate.item_level.for_address(item.order.tax_address)

@rates_for_item.select do |rate|
rate.active? && rate.tax_categories.map(&:id).include?(item.tax_category_id)
rate.active? && rate.tax_categories.map(&:id).include?(item.try(:variant_tax_category_id) || item.tax_category_id)
Copy link
Member

Choose a reason for hiding this comment

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

Not every day that you see a correct use of try. I'd consider adding a comment (saying basically what you say in the PR). As time goes to infinity, somebody is eventually going to think this was a mistake.

Copy link
Member

Choose a reason for hiding this comment

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

Indeed and mentioned in the commit message as well. Really well done @harmonymjb and @adammathys

Copy link
Member

@kennyadsl kennyadsl left a comment

Choose a reason for hiding this comment

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

Thanks, great PR!

@harmonymjb harmonymjb force-pushed the tax-categories-on-line-items-respect-updates branch from e8bc665 to 7733b39 Compare January 9, 2025 00:37
harmonymjb and others added 6 commits January 9, 2025 10:29
This makes Variant#tax_category_id work consistently with
Variant#tax_category so they both reference the same tax_category

Co-authored-by: Adam Mueller <[email protected]>
This is similar to how variant itself delegates to product in
accessing tax_categories.
line_item sets it’s tax_category in a before_validation, but this will
be updated in a future commit.

Co-authored-by: Adam Mueller <[email protected]>
A future commit will update tax calculations to ensure the variant’s
tax_category is considered if it is updated compared with the
line_item’s
This test is demonstrating a false passing because the OrderInventory
instance actually receives the `verify` message on being created, and
previous to this change, that would happen after the `expect_` line,
causing this test to pass despite `verify` not being received during
the `destory`. This change adds a `target_shipment` to the
`line_item`, which is needed to trigger the `verify` call.
In the default tax_calculator, this change ensures that the variant’s
current tax_category is considered when calculating taxes.

In the case of the item_rates method, only line_item items will have
this method, so it is accessed with `try` first, with a fallback to
without `variant_`. `try` is necessary to use instead of `.&` or
`try!` because `Spree::Shipment`s and `Spree::ShippingRate`s do not
implement `#variant_tax_category_id`.

Co-authored-by: Adam Mueller <[email protected]>
The tax rates used for updating adjustments respect the current
tax_category of the variant. This update to the line_item’s
tax_category_id represents the tax_category that was used to calculate
the taxes.

Co-authored-by: Adam Mueller <[email protected]>
@harmonymjb harmonymjb force-pushed the tax-categories-on-line-items-respect-updates branch from 7733b39 to f6660e2 Compare January 9, 2025 18:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
changelog:solidus_core Changes to the solidus_core gem
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Tax categories that are still in use can be soft deleted, creating silent failures
4 participants