Item Price without VAT = retailPrice / (taxRate + 1) VAT Amount = retailPrice - itemPriceWithoutVAT
VAT stands for "Value-Added Tax", and it’s a method of calculating and collecting taxes when an item is sold.
In a VAT context, the tax is typically included in the item’s retail price, rather than being added as an additional cost. This means that the price shown to customers during browsing is the final price that they’ll pay for a given item.
For a $10 item with a 5% VAT vs 5% Sales Tax…
5% VAT |
5% Sales Tax |
|
The Customer Pays |
$10 |
$10.50 |
Tax Amount |
$0.48 |
$0.50 |
Merchant Keeps |
$9.52 |
$10 |
To calculate a VAT amount, we need to apply the tax rate to the pre-tax price of the item. Since the retail price includes VAT, we need to work backwards.
Item Price without VAT = retailPrice / (taxRate + 1) VAT Amount = retailPrice - itemPriceWithoutVAT
With our $10 item with 5% VAT example, the math would be:
itemPriceWithoutVAT = $10 / (0.05 +1) = $10 / 1.05 = $9.52 VAT Amount = $10 - $9.52 = $0.48
We can check our math by applying the tax rate to the pre-tax amount:
$9.52 * 0.05 = $0.48 $9.52 + $0.48 = $10.00
While tax providers can declare if the tax rate for a given shipping address is VAT or not, it’s important to also consider if the item prices have taxes included.
To declare whether an item has included taxes baked into its price, you’ll want to customize DefaultDelegatingTaxService#isItemIncludesTaxes(Cart, FulfillmentGroup, FulfillmentItem, CartItem, ContextInfo)
in the CartOperationService. While calculating taxes, the result of this method gets passed on to the TaxProvider
implementation which then must decide whether to apply the resolved tax as an included tax.
The SimpleTaxProvider
leverages SimpleTaxConfig
& SimpleTaxRecords
to identify the tax rate for the provided tax address. While this configuration allows "vat": true
to be declared for a given tax rate, the SimpleTaxProvider
uses the result of DefaultDelegatingTaxService#isItemIncludesTaxes(Cart, FulfillmentGroup, FulfillmentItem, CartItem, ContextInfo)
to decide if taxes should be calculated as included taxes.
Note
|
com.broadleafcommerce.tax.simple.SimpleTaxRecord |
A SimpleTaxRecord
is the core piece of tax configuration. A SimpleTaxRecord
will have the tax rate as well as fields for the taxing jurisdiction (e.g. country, state, postal code). SimpleTaxRecord
also has a vat
field to indicate if the tax is a VAT.
SimpleTaxRecords
are configured with SimpleTaxConfig
and used in SimpleTaxProvider
.
Note
|
com.broadleafcommerce.tax.simple.SimpleTaxConfig |
The SimpleTaxProvider is configured with properties, by default in "simple-tax-example.json".
{
"defaultRate": {"rate": "0.05"},
"taxTables" : {
"US": [{
"countryDefault": true,
"rate": "0"
},
{
"stateProvinceRegion": "TX",
"rate": "0.06375"
},
{
"stateProvinceRegion": "TX",
"city": "Celina",
"postalCode": "75009",
"rate": "0.0625"
}],
"GB": [{
"countryDefault": true,
"rate": "0.2",
"vat": true
}],
"VAT20": [{
"countryDefault": true,
"rate": "0.20",
"vat": true
}],
"VAT5": [{
"countryDefault": true,
"rate": "0.05",
"vat": true
}]
}
}
In the example above, the taxTables
map has 4 keys: US
, GB
, VAT20
, and VAT5
. You can see that the GB
, VAT20
, and VAT5
keys have "vat":true
. You can also see in the US
example that tax records can be configured by country, region, city, and postal code.
SimpleTaxProvider
will first check these keys for an item’s tax code. If no record is found by tax code, then it checks the country of the destination address (see SimpleTaxProvider#getTaxRecord
).
The CartOperationsServices documentation also has a section about VAT, see CartOps VAT configuration.
During tax calculation, the SimpleTaxProvider
looks up the appropriate SimpleTaxRecord
and adds TaxDetail
objects to the tax response. TaxDetail
has an isVat
flag which is used to declare whether the taxes were applied as included taxes or as an additional cost. Related classes FulfillmentItemTaxDetail
and OrderFulfillmentItemTaxDetail
also have the isVat
flag.
Note
|
com.broadleafcommerce.tax.domain.TaxDetail com.broadleafcommerce.tax.simple.SimpleTaxProvider |
A CartPricing
object is used to track the pricing data for the cart, such as subtotal, tax amounts, and fulfillment amounts. CartPricing also has fields taxIncludedType
and includedTaxAmount
to indicate if and how much tax is included in the subtotal. The subtotal is the sum of the retail prices of a cart, so in a VAT system, tax is included in the subtotal. taxIncludedType
can be YES
, NO
, or PARTIAL
. The amount of tax included in the subtotal is tracked in includedTaxAmount
.
Note
|
com.broadleafcommerce.cart.client.domain.CartPricing |
The method responsible for determining the includedTaxAmount
is: com.broadleafcommerce.cartoperation.service.pricing.DefaultDelegatingTaxService#calculateIncludedTaxes
. That method will check TaxInfo#isTaxIncluded
from all items in the cart to determine what amount of tax is included in the subtotal.
CartPricing#totalTax
will have the total amount of tax for the Cart, including VAT and Sales Taxes.
The formula for pricing a cart is subtotal + fulfillment + fees - adjustments + (totalTax - includedTax)
.
With our example from earlier, the $10 item with 5% tax, the CartPricing object would have the following values:
CartPricing Field | Value when isVat=true | Value when isVat=false |
---|---|---|
subtotal |
$10 |
$10 |
taxTotal |
$0.48 |
$0.50 |
taxIncludedType |
YES |
NO |
includedTaxAmount |
$0.48 |
$0 |
total |
$10 |
$10.50 |
The formula for pricing a cart is subtotal + fulfillment + fees - adjustments + (totalTax - includedTax)
.
The logic is in the DefaultCartTotalsCalculator
:
@Override
public MonetaryAmount calculateCartTotalBeforeTax(@lombok.NonNull CartPricing cartPricing) {
return cartPricing.getSubtotal()
.add(cartPricing.getFulfillmentTotal())
.add(cartPricing.getFeesTotal())
.subtract(cartPricing.getAdjustmentsTotal());
}
@Override
public MonetaryAmount calculateCartTotalAfterTax(@lombok.NonNull CartPricing cartPricing) {
return calculateCartTotalBeforeTax(cartPricing)
.add(getNonIncludedTax(cartPricing));
}
/**
* Determine the amount of tax that was not included in the subtotal and therefore should be
* added to the subtotal to get the grand total.
*
* @param cartPricing The pricing fields of a Cart to total.
* @return The amount of tax that should be added to the subtotal to get the grand total.
*/
protected MonetaryAmount getNonIncludedTax(CartPricing cartPricing) {
return cartPricing.getTotalTax().subtract(cartPricing.getIncludedTaxAmount());
}
After checkout, in OrderOperationServices, the CheckoutCompletionListener
creates an Order
from a Cart
. There is a setting available to control how VAT taxes are handled
Note
|
com.broadleafcommerce.orderoperation.service.messaging.checkout.CheckoutCompletionListener |
The property broadleaf.orderoperation.ordergeneration.removeVatFromItemPrice
controls if the item’s price on the Order should include the VAT. With our previous example, our cart with a $10 item would get converted to an Order
with an OrderItem
, which would have a corresponding OrderFulfillmentItem
, which has OrderFulfillmentTaxItemDetails
, which has the isVat
flag with the value from the SimpleTaxRecord
.
In DefaultCartOrderGenerationService#updateOrderItemTotalsForVat
and DefaultCartOrderFulfillmentGenerationService#buildOrderFulfillmentItem
, the removeVatFromItemPrice
flag is checked. If true, the VAT amount will be subtracted from the item’s price.
Below is a table that shows the difference when removeVatFromItemPrice
is enabled.
Field | removeVatFromItemPrice=true | removeVatFromItemPrice=false |
---|---|---|
item price |
$9.52 |
$10 |
item tax |
$0.48 |
$0.48 |
item total |
$10 |
$10 |
The removeVatFromItemPrice
flag controls how order data is saved in the database. If instead, you would like to alter how order data is displayed, that can be accomplished by customizing the front-end admin application. The order endpoint response includes all tax info about the order, including the isVAT
and includedAmount
fields.