ExciseIQ Extension (Deprecated)¶
Decommissioned 2026-04-23
ext_exciseiq was retired by Decided #225. New installs use ext_excise_engine — see Building Extensions → Excise Tax for the current integration. The notes below are retained for operators still on a pre-2026.05 build during their migration window.
Extension name: ext-exciseiq (legacy) Strategy: TaxCalculationStrategy (name: exciseiq) Spec: Decided Tax #1, Decided #90, Decided #103, superseded by Decided #225
ExciseIQ provided excise tax calculation for tobacco, vape, and nicotine products with county-level jurisdiction precision. It ran alongside the built-in sales tax strategy via resolve_all. The replacement engine retains the same product.traits contract — see the migration note below.
Architecture¶
ExciseIQTaxStrategy
├── API path: ExciseIQAPIClient → ExciseIQ SaaS
└── Fallback path: LocalFallbackEngine → exciseiq_tax_* tables
↑
SyncExciseIQDataWorkflow (Temporal, daily)
Files¶
| File | Purpose |
|---|---|
extension.py | Entry point, manifest, config_schema, strategy registration |
strategy.py | ExciseIQTaxStrategy — routes to API or fallback |
api.py | Async httpx client for ExciseIQ SaaS (Basic auth, rate limiting) |
fallback.py | LocalFallbackEngine — 7 rate types, jurisdiction cascade |
models.py | SQLAlchemy models for local cache tables |
sync.py | Temporal activity for syncing data from ExciseIQ API |
resolvers.py | GraphQL queries and mutations for admin |
API Client¶
The ExciseIQAPIClient communicates with the ExciseIQ SaaS API:
- Auth: HTTP Basic (API key as username, empty password)
- Base URL:
https://api.exciseiqapi.com(configurable) - Rate limiting: Reads
X-RateLimit-*headers; backs off on 429
Endpoints¶
| Method | Path | Purpose |
|---|---|---|
| POST | /api/v1/tax/excise-tax-calculation | Live excise tax calculation |
| GET | /api/v1/tax/tax-classes | Sync tax classes |
| GET | /api/v1/tax/tax-regions | Sync regions |
| GET | /api/v1/tax/tax-rates | Sync rates |
| GET | /api/v1/location/counties | County lookup |
Rate Types¶
The fallback engine supports all ExciseIQ rate types:
| Rate Type | Formula |
|---|---|
percentage_of_price | line_total * rate |
percentage_of_cost | cost * qty * rate |
flat_cost_per_item | amount * qty |
flat_cost_per_milliliters | amount * ml * qty |
flat_cost_per_mfg_weight_oz | amount * weight * qty |
flat_cost_per_order | amount (once per order) |
percentage_of_price_x_milliliters | price * ml * rate * qty |
Product Traits¶
ExciseIQ uses the product.traits JSONB column. Relevant keys:
| Trait Key | Type | Purpose |
|---|---|---|
exciseiq_tax_class | string | Tax class slug for rate matching |
contains_nicotine | "yes"/"no" | Excise applicability flag |
product_type | string | "disposable", "e_liquid", "tobacco" |
milliliters | string/number | Product volume in ml |
nic_percentage | string/number | Nicotine percentage |
mfg_weight | string/number | Manufacturing weight in oz |
product_cost | string/number | Product cost for cost-based rates |
Variant traits override product traits when both are set.
Data Sync¶
Tax classes, regions, and rates sync from the ExciseIQ API via a Temporal scheduled workflow:
- Workflow:
SyncExciseIQDataWorkflow - Activity:
sync_exciseiq_data - Schedule:
vectis-sync-exciseiq-data(default: daily) - Manual trigger:
exciseiq_trigger_syncGraphQL mutation
Configuration¶
Stored in ChannelExtension.config JSONB. Configured via the admin Extensions page.
| Key | Type | Default | Description |
|---|---|---|---|
api_url | string | https://api.exciseiqapi.com | API base URL |
api_key | string | (required) | API authentication key |
calculate_tax_by | enum | shipping | Address type for jurisdiction |
fallback_on_api_failure | boolean | true | Use local rates when API fails |
enable_counties | boolean | false | Show county in checkout |
county_required | boolean | false | Require county at checkout |
sync_schedule | enum | daily | Sync frequency |
default_milliliters | string | 0 | Fallback ml value |
default_nic_percentage | string | 0 | Fallback nicotine % |
default_mfg_weight | string | 0 | Fallback weight (oz) |
default_cost | string | 0 | Fallback product cost |
debug_logging | boolean | false | Verbose logging |
GraphQL API¶
Queries¶
exciseiq_tax_classes(active_only)— synced tax classesexciseiq_tax_regions(state, active_only)— synced regionsexciseiq_tax_rates(region_id, class_id, active_only)— synced ratesexciseiq_counties(state, search, limit)— county dataexciseiq_sync_logs(limit)— sync history
Mutations¶
exciseiq_test_connection— verify API credentialsexciseiq_trigger_sync— start manual data syncexciseiq_calculate_tax(address, items)— admin tax calculator
Migration to ext_excise_engine¶
The replacement extension reads the same product.traits keys (exciseiq_tax_class, contains_nicotine, product_type, milliliters, nic_percentage, mfg_weight, product_cost) so existing catalog data continues to work without re-tagging. The one-click migration tool copies your ExciseIQ config (api_key, calculate_tax_by, default_* fallbacks) into the new extension's settings and disables the legacy strategy. The legacy seed data is no longer installed; uninstalling ext_exciseiq after migration is safe.