Tax Calculation¶
Vectis computes taxes through a multi-strategy system that runs all registered tax strategies simultaneously. This supports scenarios where multiple tax types apply to a single order (e.g., sales tax + excise tax).
How Tax Resolution Works¶
During checkout, after order lines are created, the tax engine calls resolve_all which:
- Gathers the tax context — channel, account, location, and exemption flags.
- Runs every registered
TaxStrategyin parallel. - Each strategy returns zero or more tax lines per order line.
- Tax lines are stored as JSONB on the order line and rolled up into the order's
tax_total.
graph LR
OL[Order Line] --> R[resolve_all]
R --> S1[BasicPercentageTaxStrategy]
R --> S2[ExciseIQ Extension]
S1 --> TL1["Tax Line: Sales 8.25%"]
S2 --> TL2["Tax Line: Excise $1.50/unit"]
TL1 --> OL
TL2 --> OL Built-in: BasicPercentageTaxStrategy¶
The default tax strategy performs jurisdiction lookup from the tax_rates table:
- Matches the shipping address to a jurisdiction (country, state/province, city).
- Applies the configured percentage rate to the discounted line amount.
- Supports tax-inclusive and tax-exclusive pricing modes.
Setting Up Tax Rates¶
In the admin under Settings → Tax Rates:
- Create a tax rate entry with jurisdiction (country + optional state/city).
- Set the percentage rate (e.g.,
8.25for 8.25%). - Assign a tax category if you need different rates for different product types.
Tip
Tax rates stack by specificity. A state-level rate of 6% plus a city-level rate of 2.25% yields 8.25% for that city. Configure from broadest to most specific.
Stage-Sorted Strategy Engine (Decided #151)¶
The tax engine sorts strategies by their declared stage number, then by registration order. Conventions:
| Stage | Tier | Examples |
|---|---|---|
| 100 | Sales tax | BasicPercentageTaxStrategy, ext_taxjar |
| 200 | Excise | ext_excise_engine |
| 300 | VAT | (reserved) |
Sales-tax strategies receive the raw discounted subtotal. Excise strategies receive the tax-adjusted subtotal returned by the sales-tax tier — so an excise rate computed against "$80 after $20 in sales tax" stays consistent.
When orders use the order-subtotal redistribution path (Decided #171), tax recomputes on the redistributed line amounts so allocation and tax stay aligned.
Extension: TaxJar (ext_taxjar)¶
TaxJar replaces the pre-2026 stub with a full integration:
- Calculation —
TaxJar.taxForOrderruns in the tax engine at stage100, returning line-level expected tax with jurisdiction detail. - Filing — a daily Temporal
TaxJarFilingWorkflowaggregates settled orders into a TaxJar filing payload, so monthly remittance is automatic. - Order sync —
TaxJarOrderSyncStrategyrecords every settled order and refund into TaxJar's transactions API for cross-platform sales-tax accounting.
Configure via the extension's admin page (API token, ship-from address, filing schedule). The extension is channel-scoped — each channel can carry its own TaxJar credentials.
Extension: ExciseEngine (ext_excise_engine)¶
For industries requiring excise tax (alcohol, tobacco, fuel), the ext_excise_engine extension (which replaces the deprecated ext_exciseiq, Decided #225) registers an additional tax strategy at stage 200 that:
- Calculates per-unit excise amounts based on product traits and jurisdiction.
- Returns excise tax lines alongside any sales tax lines from the stage-100 tier.
- Reads from
compliance.state_restrictions(Decided #169) to pick the right rule per ship-to state.
The engine is activated per-channel in the admin extensions panel. The one-click migration tool copies legacy ext_exciseiq config into the new extension.
Per-Line Tax Detail¶
Each order line stores a tax_lines JSONB array. Every entry contains:
| Field | Description |
|---|---|
strategy | Which tax strategy produced this line |
amount | The tax amount for this line |
rate | The effective tax rate (percentage or per-unit) |
jurisdiction | The jurisdiction that sourced the rate |
description | Human-readable label (e.g., "TX State Sales Tax") |
This granularity supports tax reporting, jurisdiction-level breakdowns, and audit trails.
Tax Exemption¶
B2B accounts and locations can be marked as tax-exempt:
- Account-level —
tax_exemptflag on the account. Exempts all orders from all locations under that account. - Location-level —
tax_exemptflag on a specific location. Exempts only orders shipping to that location.
When an exempt buyer checks out, the tax context carries the exemption flag and strategies return zero tax.
Warning
Tax exemption is binary — the buyer is either fully exempt or fully taxed. Partial exemptions (e.g., exempt on some product categories) require a custom tax strategy.
Tax Context¶
The tax engine builds a context object containing the channel, account, location, shipping address, exemption flags, and line items (with post-discount amounts). Each strategy uses whichever fields it needs — the basic strategy uses the shipping address; ExciseIQ also inspects product attributes.
Checkout Integration¶
Tax calculation happens after promotions and shipping:
- Order lines are created with their discounted amounts.
resolve_allruns and writes tax lines to each order line.tax_totalis computed as the sum of all tax line amounts.grand_totalis updated:subtotal - discount_total + tax_total + shipping_total.
Note
Tax is always calculated on the post-discount amount. A $100 item with a 20% discount is taxed on $80.
Admin Panel¶
From Settings → Tax in the admin:
- Create and manage tax rates by jurisdiction.
- Assign tax categories to products with special rates.
- Enable/disable tax extensions per channel.
- View per-order and per-line tax breakdowns in the order detail view.