Skip to content

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:

  1. Gathers the tax context — channel, account, location, and exemption flags.
  2. Runs every registered TaxStrategy in parallel.
  3. Each strategy returns zero or more tax lines per order line.
  4. 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[BasicSalesTaxStrategy]
    R --> S2[ExciseEngine Extension]
    S1 --> TL1["Tax Line: Sales 8.25%"]
    S2 --> TL2["Tax Line: Excise $1.50/unit"]
    TL1 --> OL
    TL2 --> OL

Built-in: BasicSalesTaxStrategy & BasicExciseTaxStrategy

Core ships two built-in strategies (vectis/modules/tax/strategies.py):

  • BasicSalesTaxStrategy — percentage sales tax via cascading jurisdiction lookup (described below).
  • BasicExciseTaxStrategy — per-unit or percentage excise tax read from the same tax_rates table (tax_type='excise').

The sales-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:

  1. Create a tax rate entry with jurisdiction (country + optional state/city).
  2. Set the percentage rate (e.g., 8.25 for 8.25%).
  3. 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

The tax engine sorts strategies by their declared stage number, then by registration order. Conventions:

Stage Tier Examples
100 Sales tax BasicSalesTaxStrategy, 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, tax recomputes on the redistributed line amounts so allocation and tax stay aligned.

Extension: TaxJar (ext_taxjar)

TaxJar replaces the pre-2026 stub:

  • CalculationTaxJarCalculationStrategy runs in the tax engine at stage 100, calling calculate_tax() (which POSTs to TaxJar's /v2/taxes endpoint) and returning line-level expected tax with jurisdiction detail.
  • Filing reports — a TaxFilingStrategy implementation generates period reports and summaries from TaxJar's transactions API. Submitting a filing records it locally only — TaxJar has no programmatic file-return endpoint; actual remittance is handled by TaxJar AutoFile if you're enrolled in TaxJar's dashboard.

Configure via the extension's admin page (API token, ship-from address). 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 registers an additional tax strategy at stage 200 that:

  • Calculates per-unit excise amounts based on the tax classes, rates, and jurisdictions synced from the Excise Engine service into the extension's own tables.
  • Returns excise tax lines alongside any sales tax lines from the stage-100 tier.

The extension is activated per-channel in the admin extensions panel; its admin page lives at Extensions → ExciseEngine (/extensions/excise_engine), with Cmd-K quick actions for sync, the tax calculator, and tax classes.

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-leveltax_exempt flag on the account. Exempts all orders from all locations under that account.
  • Location-leveltax_exempt flag 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; ExciseEngine also inspects product attributes.

Checkout Integration

Tax calculation happens after promotions and shipping:

  1. Order lines are created with their discounted amounts.
  2. resolve_all runs and writes tax lines to each order line.
  3. tax_total is computed as the sum of all tax line amounts.
  4. grand_total is 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.