Skip to main content
Version: Latest
Technical ManualLooking for step-by-step instructions? In the User Manual: Taxes

Taxation Framework

Feature NameTaxation
Feature IDCrestApps.OrchardCore.Taxation
CategoryCommerce
Other featuresTaxJar (CrestApps.OrchardCore.Taxation.TaxJar, see TaxJar)

Provides a provider-agnostic, extensible, international taxation framework that any Orchard Core content type or CrestApps module (Products, Subscriptions, Payments, Commerce, Bookings, Services, and custom content) can plug into.

Overview​

The framework is built on a single principle:

Tax is a determination, not a property.

Instead of storing a TaxRate on a product, the engine determines the applicable taxes from a combination of what is sold, who buys it, where it is sourced, when the transaction happens, and how the tax is calculated. The result is a deterministic, auditable, line-by-line breakdown that can be captured as an immutable snapshot on the transaction.

The framework is split into three assemblies:

AssemblyPurpose
CrestApps.OrchardCore.Taxation.AbstractionsInterfaces, models, DTOs, enums, and constants, including the TaxationPart and its TaxationPartSettings, so a module such as Products reads an item's classification as the typed part without depending on the Taxation module. Lightweight, no infrastructure dependencies.
CrestApps.OrchardCore.Taxation.CoreThe determination engine, default calculation methods, sourcing strategies, resolvers, catalog stores, and dependency-injection wiring.
CrestApps.OrchardCore.TaxationThe Orchard Core module: the TaxationPart editors, its migration, and the content-item taxable-item provider, which prices an item from its ProductPart.Price.

Getting started​

Enable the Taxation feature under Tools → Features, then:

  1. Create your tax categories, types, jurisdictions, optional tables, and rules from the admin UI (see Managing taxation from the admin UI).
  2. Attach the Taxation part to a content type and classify it (see The TaxationPart).
  3. At checkout, convert your objects into taxable items and call ITaxService.CalculateAsync (see Calculating tax).

Managing taxation from the admin UI​

Once the feature is enabled, an admin with the Manage taxation permission gets a Commerce → Taxation menu with five screens: Categories, Types, Jurisdictions, Tables, and Rules. Each screen is a searchable list with Add, Edit, and Delete actions, and is rendered through Orchard Core's display management so the list items and editors can be extended or overridden by other modules.

Setting up taxes end to end is a workflow of a few ordered steps. The order matters, because rules reference jurisdictions, types, and categories, and content classification reuses the categories you create.

1. Define what you sell — Categories​

Go to Commerce → Taxation → Categories and add a category for each kind of thing you tax (for example Electronics, or a finer Television). A category has:

  • Name — a human-readable label (the unique key, fixed after creation).
  • Code — the value matched by tax rules and stored on taxable items (for example Electronics).
  • Parent code — an optional parent category code, forming a hierarchy. A rule for a category also applies to every category under it: a rule for Electronics taxes an item classified as Television when Television's parent is Electronics. A rule for Television never taxes an item classified only as Electronics.
  • Description — optional notes.

Create the broad categories you match rules against, plus any finer classifications you want to assign to individual items.

2. Name your kinds of tax — Types​

Go to Commerce → Taxation → Types and manage the list of tax types — the labels that group and report the kind of tax a rule produces (for example SalesTax, VAT, or GST). A tax type has:

  • Name — the value stored on the resulting tax lines and offered in the rule editor (the unique key, fixed after creation).
  • Description — optional notes.

The catalog is seeded with a set of well-known types on first run so existing behavior is preserved, but the list is fully editable: add the types your business uses, rename or remove the ones you do not. A tax type is a reporting and grouping label only — it never changes how an amount is calculated.

3. Define where you tax — Jurisdictions​

Go to Commerce → Taxation → Jurisdictions and add a taxing authority for each place you collect tax. Jurisdictions are hierarchical (country → region → county → city → special district) and are matched to an address by their non-empty components. Key fields include the Level, an optional Parent jurisdiction, the geographic components (Country, Region, County, City, Postal code), and optional Effective from/to dates. Country is chosen from a dropdown of ISO 3166-1 countries (the stored value is the ISO country code), and Effective from/to are date-only pickers.

tip

When the Addresses feature is enabled, the Country dropdown is populated from your managed Country content items instead of the built-in list, so you can curate the countries you operate in. The stored value is still the ISO country code, so switching the Addresses feature on or off never orphans existing jurisdictions or rules.

4. Provide lookup tables — Tables (optional)​

Go to Commerce → Taxation → Tables when you need a rule whose amount depends on where the taxable base falls in a set of brackets rather than a single rate. A tax table is a named, versioned list of rows, and it is the data source for the Tax table lookup, Progressive, and Threshold calculation methods. Skip this step if you only use flat rates or fixed amounts. A tax table has:

  • Name — a human-readable label (the unique key, fixed after creation).
  • Effective from/to — optional date-only window during which the table is valid. A table is only used for a transaction whose date falls inside this window; a rule that requires a table but has no table in effect on the transaction date is skipped (it never silently taxes at zero).
  • Rows — one or more brackets, each with a Minimum (inclusive lower bound), an optional Maximum (exclusive upper bound; leave empty for the top bracket), a Rate, and a Fixed amount. Use Add row / Remove to manage the list. A row also has a BaseAmount, which the built-in calculation methods don't read; the editor doesn't show it, but keeps a value a recipe set for a custom method.

Rows are validated when you save. Minimums cannot be negative, at most one open-ended row (no maximum) is allowed, bounded ranges must be ordered and non-overlapping, and an open-ended row must start at or above the highest bounded maximum. This prevents overlapping brackets from double-counting in progressive calculations. A table that is still referenced by a rule cannot be deleted; remove or repoint the rules first.

How a rule reads the rows depends on the method it uses:

  • Tax table lookup finds the single row whose Minimum ≤ base < Maximum and charges base × Rate + Fixed amount.
  • Progressive taxes each bracket only on the portion of the base that falls inside it, then sums the brackets (classic tiered/marginal calculation).
  • Threshold taxes only the amount above the matching row's minimum.

Each time you save a table its Version is incremented, and that version is captured on every tax line and snapshot that used it, so historical calculations stay reproducible even after you edit the table.

5. Define how tax applies — Rules​

Go to Commerce → Taxation → Rules and click Add Tax Rule. A dialog lists the available calculation methods — Percentage, Fixed amount, Per unit, Per weight, Per volume, Progressive, Threshold, and Tax table lookup. The method you pick becomes the rule's source and is fixed for the life of the rule, exactly like the source-aware editors used elsewhere in the platform (for example AI data sources and deployments). The editor then shows the fields shared by every rule — the Name (the rule's unique identifier, fixed after creation), the Jurisdiction, the Category it applies to (or Any category), the Tax type, an optional Display name, the Customer type (or Any customer), a Priority, Effective from/to dates, minimum/maximum thresholds, and flags such as Enabled, Included in price, Compound, Reverse charge, and Applies to shipping — plus only the calculation fields the selected method needs: a Rate for the percentage method, a Fixed amount for the fixed/per-unit/per-weight/per-volume methods, or a Tax table selector for the table-driven methods (TaxTable, Progressive, Threshold), populated from the tables you created in step 4. A disabled rule is never applied, and rules outside their effective window are ignored.

The Reverse charge flag marks a rule whose liability shifts to a registered business buyer (for example the EU B2B reverse charge). When it is set and the customer is a B2B customer, the engine does not charge the tax; instead it emits a zero-amount line marked Reverse charge so the obligation is still visible on the document. For any other customer the rule behaves normally.

The Display name (TaxName) is optional: it is the label shown to customers on invoices and receipts, and when it is left empty the tax line falls back to the rule Name. The Tax type labels and groups the kind of tax the rule produces (for example SalesTax, VAT, or GST); the dropdown is populated from the Tax types you manage under Commerce → Taxation → Types, and a rule that already stores a value not present in the catalog keeps it. The tax type never changes how the amount is calculated.

6. Classify your content​

Attach the Taxation part to your content types (see The TaxationPart) and, on each item, pick its Tax category and optional Tax classification from the dropdowns. Those dropdowns are populated from the categories you created in step 1, so there are no free-text codes to keep in sync.

With categories, jurisdictions, and rules in place and your content classified, the engine determines and applies the correct tax automatically at checkout — no per-type tax code is required.

tip

Create at least one Category before configuring a content type. The Tax category and Tax classification dropdowns in the TaxationPart settings and item editors are sourced from the categories catalog, so an empty catalog leaves only the None option.

Core concepts​

The engine works with a taxable item (ITaxableItem) — a classification-carrying abstraction that is not coupled to Product, Subscription, or content items. Anything can be a taxable item: a product, a variant, a subscription plan, a service, a booking, an event, digital content, shipping, or another charge.

Content / Product / Subscription / Custom object
│
▼
ITaxableItem
│
▼
Tax determination (jurisdictions + rules + customer + nexus + exemptions)
│
▼
Tax calculation (calculation methods + tax tables + rounding)
│
▼
TaxCalculationResult (auditable tax lines)
│
▼
TaxSnapshot (immutable transaction record)

The TaxationPart​

The TaxationPart lets any content type participate in taxation without modifying the Taxation module. The part stores classification and tax identity only — never a final tax rate.

PropertyDescription
TaxableWhether the content item is taxable.
TaxCategoryCodeThe tax category (for example Electronics).
TaxClassificationCodeThe finer classification (for example Television).
ExternalTaxCodeAn optional external/provider tax code for mapping to third-party systems.

Attach the part through the content-type editor, or with a migration:

internal sealed class TelevisionMigrations : DataMigration
{
private readonly IContentDefinitionManager _contentDefinitionManager;

public TelevisionMigrations(IContentDefinitionManager contentDefinitionManager)
{
_contentDefinitionManager = contentDefinitionManager;
}

public async Task<int> CreateAsync()
{
await _contentDefinitionManager.AlterTypeDefinitionAsync("Television", type => type
.WithPart("TaxationPart", part => part
.WithDisplayName("Taxation")
.WithSettings(new TaxationPartSettings
{
DefaultTaxCategoryCode = "Electronics",
DefaultTaxClassificationCode = "Television",
AllowClassificationOverride = true,
})
)
);

return 1;
}
}

A content editor can then leave the defaults or override the classification per item. During checkout the content item is discovered as a taxable item automatically — no custom tax logic is required for the Television type.

TaxationPart settings​

SettingDescription
DefaultTaxCategoryCodeThe category applied when the item does not specify one. Selected from the categories catalog.
DefaultTaxClassificationCodeThe classification applied when the item does not specify one. Selected from the categories catalog.
AllowClassificationOverrideWhether editors may override the classification per content item.

In the content-type editor, Default tax category and Default tax classification are dropdowns populated from the tax categories you created under Commerce → Taxation → Categories, so create your categories first. When AllowClassificationOverride is disabled, editors do not see the category/classification fields and the defaults configured here are always used.

Domain model​

All taxation data is stored through the CrestApps catalog abstraction, so each entity is versioned, named, and clonable.

EntityStorePurpose
TaxJurisdictionITaxJurisdictionStoreA hierarchical taxing authority (country → state/province → county → city → special district), matched to an address by its non-empty components.
TaxCategoryITaxCategoryStoreA classification such as Electronics/Television or Alcohol/Wine, with optional external codes.
TaxTypeINamedCatalog<TaxType>A user-managed label that groups and reports the kind of tax a rule produces (for example SalesTax, VAT, GST). Seeded with well-known values and never affects calculation.
TaxRuleITaxRuleStoreDetermines whether and how a tax applies. Versioned, prioritized, with effective dates.
TaxTableITaxTableStoreBrackets, ranges, and schedules for progressive, threshold, and lookup taxes.
ExemptionCertificateIExemptionCertificateStoreA customer exemption scoped by tax type, jurisdiction, and classification.
MerchantTaxRegistrationIMerchantTaxRegistrationStoreThe merchant's obligation (nexus) to collect a tax in a jurisdiction.

Tax rules​

A TaxRule binds a jurisdiction, tax type, classification, and customer criteria to a calculation method. The calculation method is stored in the rule's Source property (inherited from SourceCatalogEntry), which is set when the rule is created and fixed thereafter:

await ruleStore.CreateAsync(new TaxRule
{
Name = "California sales tax",
TaxType = TaxTypeNames.SalesTax,
TaxName = "CA Sales Tax",
TaxCode = "US-CA-SALES",
JurisdictionId = californiaJurisdictionId,
CategoryCode = "Electronics",
Source = TaxCalculationMethodNames.Percentage,
Rate = 0.075m,
});

The TaxName is optional; when it is left empty the tax line falls back to the rule Name. Rules carry Version, Priority, EffectiveFromUtc, EffectiveToUtc, MinimumAmount, MaximumAmount, ThresholdBasis, CustomerType, IsCompound, IncludedInPrice, ReverseCharge, and AppliesToShipping. Historical rules must remain immutable once used by a transaction; publish a new version rather than mutating a rule that has already been applied.

Thresholds: each item or the whole order​

MinimumAmount and MaximumAmount limit a rule to amounts at least the minimum and below the maximum. TaxRule.ThresholdBasis (TaxThresholdBasis) says which amount:

ThresholdBasisCompared with
Item (the default)The item's taxable amount (TaxRuleQuery.TaxableAmount).
OrderWhat the order's items come to, before tax, shipping left out (TaxRuleQuery.OrderAmount: the sum of ITaxableBaseCalculator.GetTaxableBase over the context's items that aren't TaxableItemKind.Shipping). TaxService sets it on every query; a query without it falls back to the item's amount.

CatalogTaxRuleProvider applies it. The admin calls it Amounts compared with (Each item or The whole order); the TaxRule recipe step takes "ThresholdBasis": "Item" or "Order".

Import duty​

TaxTypeNames.ImportDuty (ImportDuty) is a well-known tax type: a duty on goods brought into a country, charged at checkout. New sites are seeded with it. A duty is an ordinary rule:

{
"name": "TaxRule",
"TaxRules": [
{
"ItemId": "ca-import-duty",
"Name": "Canada import duty",
"Source": "Percentage",
"TaxType": "ImportDuty",
"TaxName": "Import duty",
"JurisdictionId": "ca",
"Rate": 0.05,
"MinimumAmount": 150,
"ThresholdBasis": "Order",
"Enabled": true
}
]
}

Every item of an order to the jurisdiction whose items come to 150 or more is charged the duty. The checkout summary groups the invoice's tax lines by display name, rate and IncludedInPrice (CheckoutInvoiceSummary.cshtml), so the duty shows once, as its own line, such as Import duty 5%, and an included tax as VAT 19% (included).

Nexus vs. a jurisdiction having a tax​

The engine distinguishes a jurisdiction has a tax from the merchant is obligated to collect it. When no MerchantTaxRegistration records exist, the engine operates in permissive manual-rule mode and collects the tax. As soon as registrations exist, a rule's tax is only collected when an active registration covers its jurisdiction and tax type.

A registration also supports economic nexus thresholds. Set MerchantTaxRegistration.ThresholdAmount to the sales volume that must be reached before the obligation begins (leave it null to be obligated as soon as the registration is active), and keep MerchantTaxRegistration.ThresholdAccumulatedAmount updated with the running sales into that jurisdiction as orders are recorded. The registration only establishes nexus once the accumulated amount reaches the threshold. The framework does not own a sales ledger, so the host is responsible for maintaining the accumulated total — this keeps the engine deterministic while still letting you model destination-based economic-nexus rules such as the US Wayfair thresholds.

Calculating tax​

The primary API is ITaxService:

public interface ITaxService
{
Task<TaxCalculationResult> CalculateAsync(
TaxCalculationContext context,
CancellationToken cancellationToken = default);
}

Build a TaxCalculationContext with everything needed for a deterministic determination:

var context = new TaxCalculationContext
{
Currency = "USD",
TransactionDateUtc = clock.UtcNow,
Destination = new TaxAddress { Country = "US", Region = "CA", City = "Los Angeles", PostalCode = "90001" },
Customer = new CustomerTaxProfile { CustomerType = CustomerTaxType.B2C },
Items =
[
new TaxableItem
{
Id = "sku-123",
Kind = TaxableItemKind.Physical,
Quantity = 1m,
UnitPrice = 500m,
Currency = "USD",
TaxCategoryCode = "Electronics",
TaxClassificationCode = "Television",
},
],
};

var result = await taxService.CalculateAsync(context);

To convert existing objects (content items, products, subscriptions) into taxable items, use ITaxableItemResolver, which delegates to the registered ITaxableItemProvider implementations:

var taxableItem = await taxableItemResolver.ResolveAsync(contentItem);

The result​

TaxCalculationResult returns the taxable amount, total tax, grand total, and a TaxLine for every applied tax. Each line explains how the tax was determined:

FieldDescription
TaxCode, TaxName, TaxTypeIdentity of the tax.
JurisdictionId, JurisdictionNameThe taxing authority.
Rate, TaxableAmount, TaxAmountThe computed values.
CalculationMethodThe method used.
IncludedInPrice, IsCompoundPricing and compounding flags.
Treatment, TreatmentReasonHow the supply was treated (Taxable, Exempt, ZeroRated, ReverseCharge, or OutOfScope) and a human-readable explanation.
RuleId, RuleVersion, TableId, TableVersionAudit references to the exact rule and table versions used.

Exempt customers, valid exemption certificates, and reverse-charge rules do not silently drop a tax. The engine still emits a TaxLine with TaxAmount = 0 and the appropriate Treatment, so invoices and receipts can show a compliant Exempt or Reverse charge line instead of omitting the tax entirely. Only a rule that the merchant has no nexus for produces no line at all.

Calculation methods​

The engine ships with the following calculation methods, all registered by name and resolved through ITaxCalculationMethodProvider:

Name (TaxCalculationMethodNames)Behavior
PercentagePercentage of the taxable base. Supports tax-inclusive extraction.
FixedAmountA flat amount per line.
PerUnitAmount multiplied by quantity.
PerWeightAmount multiplied by weight.
PerVolumeAmount multiplied by volume.
TaxTableA single matching bracket from a tax table.
ProgressiveTaxes each bracket portion (tiered).
ThresholdTaxes only the amount above a threshold.

Tax-inclusive vs. tax-exclusive pricing​

Set TaxableItem.PriceIncludesTax (or TaxCalculationContext.DefaultPriceType) to control whether the price already contains tax. For inclusive taxes the engine nets the base out of the gross price before applying each rule, so the total stays equal to the displayed price. Both inclusive percentage taxes and inclusive fixed-amount taxes (FixedAmount, PerUnit, PerWeight, PerVolume) are un-grossed: the engine subtracts the fixed portions and divides out the combined percentage rate, clamping the net base at zero. Table-driven inclusive taxes (TaxTable, Progressive, Threshold) cannot be un-grossed because their amount depends on the base itself; keep those rules tax-exclusive.

Tax sourcing​

A rule with a JurisdictionId applies only when that jurisdiction is among the ones resolved for the item's address (ITaxJurisdictionResolver). With no address, or an address outside it, the rule does not apply; rules without a jurisdiction apply everywhere. So the address the checkout supplies decides which jurisdiction rules can apply.

The built-in DefaultCheckoutTaxProfileProvider uses the address the order ships to, else the billing address, as the destination, and the store's address under Settings > Commerce > Tax origin (TaxOriginSettings) as the origin. Other modules add to the profile through ICheckoutTaxProfileHandler, such as the customer's exemption from Customers; see Taxation at checkout.

Do not assume the shipping address is always the tax location. Sourcing is pluggable through ITaxSourcingStrategy (resolved by ITaxSourcingStrategyProvider), with these built-in strategies: Origin, Destination, CustomerResidence, CustomerBusiness, ServiceLocation, and EventLocation. The engine picks a strategy from the taxable item's Kind (for example digital goods use the destination, services and bookings use the service location, and events use the event location) and falls back through destination, item origin, context origin, and customer residence.

Snapshots, immutability, and refunds​

When taxation becomes part of a transaction, capture it with ITaxSnapshotFactory:

var snapshot = snapshotFactory.Create(context, result);

A TaxSnapshot is an immutable deep copy of the determination, including every tax line with its rule and table versions. Changing a rate, rule, or tax table tomorrow never changes yesterday's snapshot. Refunds and adjustments reverse the original snapshot rather than recalculating with today's rules.

Refunding tax from a snapshot​

ITaxRefundCalculator derives the tax portion of a refund from the original transaction's snapshot, never from current rules, so a rate change after the sale can never alter a refund:

// Full refund: reproduces the snapshot's tax exactly.
var full = refundCalculator.CalculateFullRefund(snapshot);

// Partial refund: proportional to the amount being refunded, allocated across the
// original tax lines so each jurisdiction is refunded per the original determination.
var partial = refundCalculator.CalculateProportionalRefund(snapshot, refundTotalAmount: 54m);

A refund at or above the snapshot total returns the full refund; a non-positive amount returns nothing. Refunds never introduce a second tax calculation — the historical snapshot is the single source of truth.

Rounding​

Rounding is explicit and controlled by TaxationOptions (or TaxCalculationContext.RoundingLevel):

Level (TaxRoundingLevel)Behavior
LineEach tax line is rounded independently.
TaxAmounts are aggregated per tax type, then rounded.
JurisdictionAmounts are aggregated per jurisdiction, then rounded.
TransactionThe whole transaction is rounded once.

TaxationOptions also configures DecimalPlaces, MidpointRounding, and per-currency decimal overrides.

Extending the framework​

External modules integrate without modifying the Taxation source. Register your implementations in a Startup class:

public sealed class Startup : StartupBase
{
public override void ConfigureServices(IServiceCollection services)
{
// Convert your own objects into taxable items.
services.AddTaxableItemProvider<SubscriptionTaxableItemProvider>();

// Add a custom calculation strategy.
services.AddTaxCalculationMethod<MyCustomTaxCalculationMethod>();

// Add a custom sourcing strategy.
services.AddTaxSourcingStrategy<MyCustomSourcingStrategy>();

// Delegate determination to an external provider (Avalara, Stripe Tax, etc.).
services.AddTaxDeterminationProvider<MyExternalTaxProvider>();
}
}

The main extension points are:

InterfaceResponsibility
ITaxableItem / ITaxableItemProviderRepresent and produce taxable items from arbitrary objects.
ITaxServiceThe determination engine entry point.
ITaxCalculationMethodA calculation strategy resolvable by name. It also declares, through its Inputs (TaxCalculationMethodInputs), which rule configuration fields (rate, fixed amount, or tax table) the Rules editor should show when the method is selected.
ITaxSourcingStrategyWhere the tax is sourced from.
ITaxRuleProviderSupplies applicable rules for a query.
ITaxClassificationProviderSupplies an inherited classification (category / classification / external code) for a content item that does not carry its own — for example from a taxonomy term.
ITaxJurisdictionResolverMaps an address to jurisdictions.
ITaxExemptionResolverDetermines customer exemptions.
IMerchantTaxRegistrationProviderDetermines merchant nexus.
ITaxableBaseCalculatorComputes the taxable base for an item.
ITaxRoundingStrategyApplies the rounding policy.
ITaxSnapshotFactoryCaptures immutable transaction snapshots.
ITaxRefundCalculatorDerives full and proportional refund tax from a snapshot.
ITaxDeterminationProviderShort-circuits the engine with an external determination.

An ITaxDeterminationProvider whose CanHandle returns true takes over the entire calculation, which is how third-party tax services can be layered on top of the same abstractions; TaxJar is one. Providers are asked by their Order, lowest first, and the first that can handle the context determines it. The engine checks that every item is in the context's currency before asking any provider.

TaxJar​

The TaxJar feature (CrestApps.OrchardCore.Taxation.TaxJar, it needs Taxation) works out tax with TaxJar for orders to the countries it's set up for, in place of the store's rules. The settings screen is described in the User Manual.

SettingsSettings > Commerce > TaxJar (settings group taxjar, permission ManageTaxation), stored as TaxJarSettings
ProviderTaxJarTaxDeterminationProvider (ITaxDeterminationProvider, Order 100), with the named HttpClient TaxJar (10-second timeout)
OptionsTaxJarOptions: ApiKey (unprotected), UseSandbox, Countries, and BaseUrl (https://api.taxjar.com/v2/, or https://api.sandbox.taxjar.com/v2/ with the sandbox)

TaxJarSettings.ApiKey is protected with data protection (purpose CrestApps.OrchardCore.Taxation.TaxJar.ApiKey). The settings page never shows it again, keeps it when the field is left blank, and removes it with Remove the token. Countries defaults to US. TaxJarOptionsConfiguration reads the settings into TaxJarOptions, which the provider reads through IOptionsMonitor<TaxJarOptions>.CurrentValue. Saving the settings calls IOptionsUpdateNotifier.RequestUpdate<TaxJarOptions>() (the options are registered with AddSignalOptionsChangeTokenSource), so the new settings are used once they are saved, on every node when a distributed signal is enabled, without restarting the tenant.

CanHandle is true when there's a token, TaxCalculationContext.DefaultPriceType is Exclusive, and the destination's country is in Countries. A context whose prices include tax, such as a checkout in a market whose prices include tax, stays with the store's rules, because TaxJar adds tax to a price.

DetermineAsync posts the order to taxes with the token as a bearer token:

TaxJar fieldFrom
from_country, from_zip, from_state, from_city, from_streetTaxCalculationContext.Origin (Settings > Commerce > Tax origin)
to_country, to_zip, to_state, to_city, to_streetTaxCalculationContext.Destination
amount, shippingThe non-shipping and the shipping items' unit price times quantity, less their discounts
line_itemsEach non-shipping item: id, quantity, unit_price, discount, and product_tax_code from TaxableItem.TaxClassificationCode
exemption_typeother for a customer whose CustomerTaxProfile.IsTaxExempt is true

The answer becomes one TaxLine for each line item, and one for shipping, that owes tax: Rate is TaxJar's combined_tax_rate, TaxableAmount and TaxAmount its taxable_amount and tax_collectable, JurisdictionName the jurisdictions joined as city, county, state, country, and CalculationMethod TaxJar. The tax type and name are SalesTax and Sales tax for the United States, and VAT and VAT elsewhere.

When TaxJar answers with an error, or doesn't answer, the provider logs it and throws an InvalidOperationException. TaxService catches a determination provider's failure (anything but cancellation), logs a warning, and works the tax out with the built-in engine, so a connector that's down doesn't stop sales.

Per-category taxation with taxonomies​

A common question is "how do I apply a different tax code to a whole category of products — for example an excise tax on all Tobacco items?"

You do not put multiple codes on each item. An item carries a single tax category code (plus an optional classification code); the multiplicity of applied taxes comes from multiple rules matching that one category in a jurisdiction. A rule with no CategoryCode matches every category (a general sales tax), and it naturally stacks with a CategoryCode = "Tobacco" excise rule so a pack of cigarettes is taxed by both.

To manage the category assignment per group of products rather than item-by-item, enable the OrchardCore.Taxonomies feature and let items inherit their classification from the taxonomy term they belong to:

  1. Create a taxonomy (for example Product Categories) with terms such as Electronics, Tobacco, Alcohol.
  2. Attach the Taxation part to the term content type, and set the Tax category (and optional classification) on each term — e.g. the Tobacco term gets category Tobacco.
  3. Attach a Taxonomy field to your product type and tag each product with its term(s).

Now any product tagged Tobacco is taxed as Tobacco without setting a code on the product itself. The resolution precedence is:

  1. The item's own TaxationPart category (an explicit code on the item always wins).
  2. The classification supplied by the registered ITaxClassificationProviders, consulted in ascending Order; the first provider that returns a non-empty category wins (the built-in provider reads the tagged taxonomy term's TaxationPart).
  3. The type default configured in the TaxationPart settings.

Only the category is inherited when the item omits it; a classification code set explicitly on the item is preserved. Write your own ITaxClassificationProvider to source inherited codes from any other place (an ERP, a parent content item, etc.).

Worked example — an excise tax on Tobacco​

Suppose a US store sells electronics and tobacco. California charges 7.5% sales tax on everything and an extra 30% excise tax on tobacco.

  1. Categories — create ELEC (Electronics) and TOBACCO (Tobacco).
  2. Taxonomy — create a Product Categories taxonomy whose term content type has the Taxation part attached. On the Tobacco term set Tax category = Tobacco; on the Electronics term set Tax category = Electronics.
  3. Products — add a Taxonomy field to the Product type and tag each product with its category term. You do not set a tax code on the product itself.
  4. Jurisdiction — create California (US / CA, level State).
  5. Rules in California:
    • a general rule with no category at 7.5% (applies to every product), and
    • a tobacco rule with category TOBACCO at 30%.

A television tagged Electronics inherits category ELEC, matches only the general rule → 7.5%. A pack of cigarettes tagged Tobacco inherits category TOBACCO, matches both rules → 7.5% + 30% stacked. No per-item tax codes were entered.

The whole setup is reproducible as a recipe — the taxonomy term simply carries a TaxationPart value:

{
"steps": [
{
"name": "content",
"data": [
{
"ContentType": "ProductCategory",
"DisplayText": "Tobacco",
"TaxationPart": { "Taxable": true, "TaxCategoryCode": "TOBACCO" }
}
]
}
]
}

Common scenarios​

You want to…Do this
Apply one tax to everythingCreate a jurisdiction and a single rule with no category code. Every taxable item matches it.
Tax a single product differentlySet the Tax category directly on that item's Taxation part. An explicit item code always overrides inheritance.
Give every item of a type a default codeSet the default tax category in the content type's TaxationPart settings. It applies when the item and its taxonomy term leave the code empty.
Tax a whole category/group differentlyEnable OrchardCore.Taxonomies, attach the Taxation part to the term type, set the code on the term, and tag products with the term. See Per-category taxation with taxonomies.
Stack an extra tax (excise, environmental fee) on some itemsKeep the general rule (no category) and add a second rule scoped to that category. Both match and stack.
Exempt a customer or regionImplement an ITaxExemptionResolver; see Extending the framework.
Delegate to Avalara / Stripe TaxRegister an ITaxDeterminationProvider that short-circuits the engine.
Move a catalog between environmentsExport the categories, types, jurisdictions, tables, and rules as a recipe or deployment plan.

Recipes and deployment​

The five catalog entities — Tax categories, Tax types, Tax jurisdictions, Tax tables, and Tax rules — can be imported and exported as code.

Recipe steps (names TaxCategory, TaxType, TaxJurisdiction, TaxTable, TaxRule) each take a plural array payload. Environment-owned fields (CreatedUtc, ModifiedUtc, Author, OwnerId, and Version) are never imported; Version is stamped by the environment and incremented on each save, so it stays an authoritative audit identity and a recipe can never regress or reuse it. An entry is matched by its ItemId and updated in place, or created when new. Names are unique within each entity: an entry whose name another entry already uses is reported as a recipe error and not saved, the same as in the admin:

{
"steps": [
{
"name": "TaxCategory",
"TaxCategories": [
{ "ItemId": "electronics", "Name": "Electronics", "Code": "ELEC" },
{ "ItemId": "tobacco", "Name": "Tobacco", "Code": "TOBACCO" }
]
},
{
"name": "TaxType",
"TaxTypes": [
{ "ItemId": "sales-tax", "Name": "SalesTax" },
{ "ItemId": "excise-tax", "Name": "ExciseTax", "Description": "Excise duty on regulated goods." }
]
},
{
"name": "TaxJurisdiction",
"TaxJurisdictions": [
{ "ItemId": "us-ca", "Name": "California", "CountryCode": "US", "RegionCode": "CA", "Level": "State" }
]
},
{
"name": "TaxTable",
"TaxTables": [
{
"ItemId": "luxury-brackets",
"Name": "Luxury brackets",
"Rows": [
{ "Minimum": 0, "Maximum": 1000, "Rate": 0.05 },
{ "Minimum": 1000, "Rate": 0.10 }
]
}
]
},
{
"name": "TaxRule",
"TaxRules": [
{ "ItemId": "ca-sales", "Name": "CA sales tax", "JurisdictionId": "us-ca", "Source": "Percentage", "Rate": 0.075 },
{ "ItemId": "ca-tobacco", "Name": "CA tobacco excise", "JurisdictionId": "us-ca", "CategoryCode": "TOBACCO", "Source": "Percentage", "Rate": 0.30 },
{ "ItemId": "ca-luxury", "Name": "CA luxury tax", "JurisdictionId": "us-ca", "Source": "Progressive", "TaxTableId": "luxury-brackets" }
]
}
]
}

When the CrestApps.OrchardCore.Recipes feature is enabled, JSON Schema is contributed for each of these steps (and for the TaxationPart and its settings), giving editor validation and IntelliSense while authoring recipes.

EU VAT standard rates​

The Taxation module ships the EU VAT standard rates recipe (EuVatStandardRates, Recipes/eu-vat.recipe.json, not a setup recipe), run from Configuration > Recipes. It enables Taxation and adds the 27 EU member states as TaxJurisdictions (level Country, with the ISO code as code and country, Greece as GR), and a Percentage TaxRule for each (TaxType VAT, TaxName VAT, the country's standard rate, AppliesToShipping, priority 0). Its entries have fixed ItemIds (4evatj… for jurisdictions, 4evatr… for rules), so running it again updates the same entries. Rates change: check them before relying on the recipe, and add reduced-rate rules by category.

Deployment steps with the same five names are available under Configuration → Deployment, so an existing tenant's taxation catalog can be exported into a deployment plan and re-imported elsewhere. The exported JSON is identical in shape to the recipe payloads above.

Taxes on orders and the one-stop shop​

When a cart's checkout completes, OrderCheckoutHandler copies the invoice's tax lines to Order.TaxLines (List<TaxLine>), so an order keeps every tax it was charged: its name, type, jurisdiction, rate, taxable amount, amount and whether it was included in the price. The Orders API shows them as the order's taxes; see Carts and Orders.

The VAT one-stop shop (OSS) report (OrderVatOssReport, name order-vat-oss, registered by the Orders module when Reports is on, permission ManageOrders) adds them up for the EU one-stop-shop return. OrderVatOss.Summarize(orders, originCountry):

  • reads the orders paid in the period;
  • takes each order's country from its shipping address, else its billing address, and keeps only EU member states (OrderVatOss.EuCountries, Greece as GR) other than the origin country (TaxOriginSettings.Origin.Country);
  • adds up the tax lines whose TaxType is VAT and whose amount isn't zero, by country, rate and currency;
  • takes refunds off in proportion: each line counts 1 - RefundedAmount / Total of its taxable amount and tax.

The report shows, per currency, the VAT due and the number of countries, and a table of country, rate, orders, taxable amount and VAT, with a total. Orders paid before Order.TaxLines existed have no tax lines, so they aren't counted.

Determinism​

Given the same TaxCalculationContext plus the same tax rule and tax table versions, the result is always the same. The engine avoids hidden dependencies on mutable global state and uses the injected IClock for all time-based logic.