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

Taxes #31

michaelbromley opened this issue Oct 9, 2018 · 7 comments

Taxes #31

michaelbromley opened this issue Oct 9, 2018 · 7 comments


Copy link

@michaelbromley michaelbromley commented Oct 9, 2018

Most countries have some kind of sales tax applied to goods being purchased.

In the UK for example, the current VAT rates are zero (0%), reduced (5%) and standard (20%). Here are rates in other EU countries

A tax is modeled as a type of Adjustment (#29) (AdjustmentType.TAX) and should be applied before any other adjustments, since other adjustment conditions would typically query the tax-inclusive prices.


  1. The ProductVariant entity should have a taxCategory property which points to a tax AdjustmentSource.
  2. The price property should be tax-inclusive. There should be a new property, priceBeforeTax which contains the price before taxes are applied.
Copy link
Contributor Author

@michaelbromley michaelbromley commented Oct 9, 2018

Note: It might make more sense to calculate the tax-inclusive price once at write-time rather than dynamically at read-time for perf reasons.

Whenever the price or taxCategory of a ProductVariant is changed, the tax-inclusive prices are calculated and saved to the DB.

michaelbromley added a commit that referenced this issue Oct 9, 2018
Copy link
Contributor Author

@michaelbromley michaelbromley commented Oct 10, 2018

After playing around with the initial implementation, it looks like it will not be flexible enough to account for differing tax zones.

I'll now have to look into a zone system (see which can then be used for configuring both tax adjustments and shipping adjustments.

For an example of how complex tax rules can get (and the need for user-defined zones), check out this example from the Shopify docs:

Shopify has built-in tax overrides for clothing products for the states of New York, Massachusetts, and Rhode Island. The following state tax exemptions for clothing products apply:

New York: Clothing products, footwear products, and items used to repair clothing products with an individual price under $110 are exempt from state sales tax. For example, two items with a combined price of $200 will each be exempt from sales tax, but one item with an individual price of $110 will be subject to sales tax.
Massachusetts: Clothing products with a price under $175 are exempt from state sales tax, and clothing products with a price of over $175 will collect tax only on the amount that their price is over $175. For example, an item of clothing with a price of $200 will be taxed on $25 because the first $175 is not taxable.
Rhode Island: Clothing and footwear products with an individual price of $250 or less are exempt from state sales tax, and only the incremental amount above $250 is subject to sales tax. For example, if a suit costs $275, then the tax applies only to $25.

michaelbromley added a commit that referenced this issue Oct 11, 2018
Copy link
Contributor Author

@michaelbromley michaelbromley commented Oct 12, 2018

Customer Tax Category

Researching this a bit further, and there is more complexity we need to support: different types of customer might be liable to different rates of tax. For example, a business customer in another EU country would be charged 0% VAT whereas a consumer would be charged regular VAT.

The use-cases are well described in this Sylius issue: Sylius/Sylius#8047

Therefore the current naive implemetation will need a complete redesign to account for:

  1. Destination (country / zone)
  2. Customer group (needs to be implemented)
  3. Tax category of the ProductVariant
Copy link
Contributor Author

@michaelbromley michaelbromley commented Oct 12, 2018

Another case study for various taxes to be considered by a UK business: reactioncommerce/reaction#972 (comment)

michaelbromley added a commit that referenced this issue Oct 17, 2018
Relates to #31 #26 #29
This merge introduces the basis of what seems to be a workable tax &
promotions system. There is still more to do, most importantly solving
the problem of how the admin can set the gross price of a
ProductVariant, as well as working out how the typical range of
promotion actions can be implemented (buy 1 get 1 free, money off order

However, this work can now be continued on the master branch.
Copy link
Contributor Author

@michaelbromley michaelbromley commented Oct 18, 2018

The above merge implements a workable taxation system implementing the requirements above.

Net vs Gross prices problem

There remains one major deficiency though: currently all ProductVariant prices are considered net (before tax) values. Taxes are then calculated based on the applicable rate. This is simple and flexible, but in the real world this is not always desirable. Consider:

A UK-based shop sells rocks primarily to the UK market. They wish to sell Rock A at a (gross) price of 99p. With standard UK VAT at 20%, there is no way to achieve this:

Net price (pence) Tax calculation Rounded gross price
82 82 * 1.2 = 98.4 98
83 83 * 1.2 = 99.9 100

Thus the closest possible gross prices are either £0.98 or £1.00.

For some companies, this will be a deal-breaker because they may want to set gross prices based on marketing or other factors, and be able to offer those exact gross prices to customers.


I can see 2 possible solutions to this problem:

  1. Allow fractions of a penny in the net price field
    The current tax calculation method can remain unchanged.
    👎 Will cause issues when applying a tax rate of 0% - we cannot charge a customer a fraction of a penny.
    👎 Negates the benefits of working only with integers at the API level (avoidance of floating-point maths issues)
    👎 Will complicate the ProductVariant API - we'll need to specify which price (net or gross) is the "true" price and add additional logic at the service layer to derive the other value.
  2. Allow prices to be set as gross, and alter the way taxes are calculated in this case.
    Aligns with how many (especially smaller or largely domestic) businesses think.
    We can keep using integers everywhere.
    👎 The calculation of taxes to non-default zones will get more complex - we'll need to remove the default zone tax and apply the new tax on top.
    👎 We'll have to store somewhere whether prices are net or gross and refer to this when doing totals calculations.

I think the second option is the better one here. The next question is then: where to store the fact that the prices include tax?

  1. In the TaxRate. This is the way Sylius does it. They have a flag on the TaxRate which says whether the prices are net or gross. I think this will lead to problems when shipping to non-default zones, e.g. if we have all prices inclusive of UK VAT @ 20%, and ship to Australia we want to charge zero VAT. We'd then need to know the rate of tax which is included in the price so that we can remove it, but at this point we have lost the information that the prices include VAT. See Sylius/Sylius#1568 for this issue playing out in the Sylius system.
  2. In the Channel. If we have the flag in the channel, we can get around the above problem. We see that the zone is "Rest of World", and we know that this is not the default Zone. We then check if the "prices include tax" flag is true. If so, we get the default Zone's tax rate and can remove it. This also implies other channels can have different pricing policies, which should be fine since the price amount is stored on a per-channel basis.
michaelbromley added a commit that referenced this issue Oct 19, 2018
Relates to #31, aims to solve the "net vs gross prices" problem outlined therein.

Next step is to get the tax calculations working correctly for Orders when tax is included in price.
michaelbromley added a commit that referenced this issue Oct 19, 2018
When the ProductVariant has tax-inclusive prices. Relates to #31
@michaelbromley michaelbromley added this to To do in Alpha Dec 31, 2018
@michaelbromley michaelbromley moved this from To do to In progress in Alpha Jan 1, 2019
Copy link
Contributor Author

@michaelbromley michaelbromley commented Jan 4, 2019

Note on EU VAT calculation

In discussion with a merchant, the following rule came to light:

  • Given the merchant is in the UK
  • And the customer has a non-EU billing address
  • And the order has a non-EU shipping address
  • Then there will be no VAT charged on the order

However, if that customer ships the order to the EU, then VAT is charged.

Likewise, if a customer has a EU billing address and ships and order outside the EU, VAT is charged.

This suggests that we need a configurable function which determines under which circumstances taxes should be applied, based on billing address, shipping address, and perhaps other factors. See activeTaxZone in RequestContext

Note on VAT on Shipping

The same merchant says that if the order contains only zero-rate goods, then no VAT is charged on the shipping. As soon as a 20% product is added to the order, then the shipping should also include 20% VAT. Need to look into this and verify the actual applicable law.

Copy link
Contributor Author

@michaelbromley michaelbromley commented Jan 8, 2019

This can be considered complete now. The actual tax calculator implementations for common cases are complex enough for their own tasks, as is the shipping tax case (#54).

@michaelbromley michaelbromley moved this from In progress to Done in Alpha Jan 8, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Linked pull requests

Successfully merging a pull request may close this issue.

None yet
1 participant