Tax
Configure tax for a market and connect a real tax engine to Litestore.
Litestore calculates tax itself, adds it to the order as its own line, records it and refunds it, whichever payment provider you use. Read this page when you configure tax for a market or need to connect a tax engine.
Product in each market
The TaxProvider interface separates who calculates the tax from everything else. A real tax engine plugs in without touching how checkout charges, how orders are recorded, reconciliation or refunds.
Because Litestore calculates tax itself, the checkout quote is always complete. The OrderIntent already contains the exact amount the customer will pay. The payment webhook turns that quote into an order and never has to ask the payment processor for the tax breakdown.
Providers
modules/checkout/tax/provider.ts contains three providers.
| Provider | name | Behaviour |
|---|---|---|
| Flat channel rate | channel | The market's configured rate. No channel means no tax. The schema default. |
| No tax | none | Always zero, for tax-free stores |
| Stripe Tax | stripe_tax | Real jurisdiction tax computed by Stripe, opt-in per channel |
All three have the same resolve method. You pass it a channel, the taxable amount in cents after discounts, a currency and, optionally, a destination address. It returns the tax to charge in cents and whether that tax is inclusive or exclusive.
The interface also has optional commit and reverse methods, and a provider that keeps no state simply leaves them out.
Provider selection
ENABLE_TAXES
This is an environment feature flag in env.ts. When it is off, getTaxProvider returns the no-tax provider, even if rates are configured. The flag is off by default.
This check exists because the flag used to do nothing, and Litestore charged tax whether it was on or not. Now, when the flag is off, Litestore does what the admin says and doesn't add tax to orders.
Channel setting
getTaxProvider then reads channel.taxProvider.
| Value | What Litestore does |
|---|---|
manual | It selects the flat channel rate, so a default store behaves the same as before. |
stripe_tax | It selects real tax for the shopper's jurisdiction, calculated by Stripe. |
| Any other value | It falls back to the flat channel rate and logs a loud warning. This covers the name of an old provider left in the column. |
| No setting at all | The channel gets the flat provider, which returns zero tax when there is no channel. |
The fallback for unknown values needs some explanation. The column accepts any text, so it can still contain the name of an engine that was never connected. Litestore could either pretend that engine works or fall back openly, and it falls back openly. The order still gets a complete tax amount from the flat rate, and Litestore tells the operator that the configured engine isn't the one running.
Inclusive and exclusive tax
Each channel stores a rate as Decimal(5, 4), for example 0.1900 for 19%. It also stores whether tax is inclusive or exclusive, with exclusive as the default. The two settings calculate tax differently, and they don't just display it differently.
inclusive the price already contains the tax (EU/AU style)
exclusive the tax is added on top (US style)Litestore first takes the subtotal out of the price and rounds it, and the tax is what is left over. This way subtotal + tax always equals the original price exactly.
Every piece of code that deals with tax uses this one function. That includes the amount Litestore charges, the amount the web pages display, and the tax engine.
Stripe Tax fallback
stripeTaxProvider.resolve uses the flat channel rate instead when the destination country is missing, when the taxable amount is zero or less, or when Stripe returns any error.
if (!destination?.country || subtotalCents <= 0) {
return flatChannelTaxProvider.resolve(input)
}
// …
} catch (error) {
logger.error("Stripe Tax calculation failed — falling back to the flat channel rate", { … })
return flatChannelTaxProvider.resolve(input)
}The source code states the goal. A taxable order must never quietly drop to zero tax, and checkout must never break because Stripe Tax is briefly unavailable. Instead, the failure is logged as an error in the server logs.
Both fallbacks answer the same question. If the engine can't produce a number, is it better to charge no tax or to charge the rate the store already set up? Charging nothing would quietly leave the store owing tax. Failing the checkout would lose an order over a problem the shopper didn't cause. Charging the flat rate keeps the quote complete.
Even when Stripe calculates the tax, Litestore is still in charge of the amount. It reads Stripe's result and charges it as its own line item, just as it does with the flat provider. Payment, order recording and refunds therefore work the same whichever engine you choose. Stripe's calculation id comes back as an opaque providerRef, which a later commit or reverse call could use.
Adding tax engines
The interface is designed so that adding a real tax engine means writing a new implementation, not restructuring code.
TaxProvider.resolve
Use the engine's calculation API.
commit
Use it for reporting, and call it in the order-creation path once the order is paid.
reverse
Use it for returns, and call it in the refund path next to the existing amountTax proration.
getTaxProvider
Base the choice on a store setting.
The maintainers deliberately didn't set up steps 2 to 4 in advance. The existing providers keep no state and have no commit or reverse, and adding calls that do nothing to the code that handles money would add risk without any benefit.
Every order uses the calculation step. That step is already in place, and it is the first thing a new engine has to support.
Related
Shipping
Litestore determines which shipping rates a shopper sees, builds tracking links and estimates arrival dates.
Checkout
Litestore turns a cart into a Stripe payment and holds stock for the shopper while they pay.
Providers
Some external services are swappable, Litestore calls others directly, and the app needs certain environment variables to start.