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[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:

  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 (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:

  • CalculationTaxJar.taxForOrder runs in the tax engine at stage 100, returning line-level expected tax with jurisdiction detail.
  • Filing — a daily Temporal TaxJarFilingWorkflow aggregates settled orders into a TaxJar filing payload, so monthly remittance is automatic.
  • Order syncTaxJarOrderSyncStrategy records 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-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; ExciseIQ 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.