Rounding and delivery
Rounding and delivery
Rounding methods and delivery exclusions are sourced rules. The engine owns the arithmetic, decisions and reconciliation; the host supplies transaction facts.
Rounding policies
The US regime obtains a dated TaxRounding through Contracts\RoundingRules.
The default RegisterRounding reads rounding records from the installed release.
Supported methods are half_up and up, with explicit decimal places, combined
local shares and line/invoice scope. Unsupported policies or gaps within published
rounding history raise UnresolvedTaxRule.
seller_election permits the host to choose RoundingScope::Line (the default)
or RoundingScope::Invoice using TaxQuery::$roundingScope or
TaxOrder::$roundingScope. A fixed published scope takes precedence over that choice.
An absent local-aggregation assertion is accepted for a state-only rate; it cannot
authorize aggregation of supplied local components.
An invoice policy keeps exact tax until lines and delivery portions have been
grouped by rate. Each group is rounded once, and the resulting minor units are
distributed deterministically. Exclusive lines keep their net amounts; inclusive
lines keep their gross amounts. Credits reverse rounding away from zero. Authority
breakdowns are allocated from each line's actual tax and reconcile to it.
TaxAssessment::$taxableBase retains a partial taxable net base even when the
provisional line tax is zero and invoice reconciliation later allocates a cent.
The default maths where no policy is published remains half-up. Calling
TaxRate::taxOnNet() directly is a pure arithmetic operation: supply its optional
TaxRounding argument to apply a policy, or use the calculator to resolve one.
The engine does not silently round again when a money context cannot represent
the policy's precision.
Delivery facts
Flag a delivery line and, where an exclusion applies, confirm its conditions:
use Brick\Money\Money;
use Cbox\Tax\ValueObjects\DeliveryCharge;
use Cbox\Tax\ValueObjects\SupplyLine;
$delivery = new SupplyLine(
id: 'delivery',
amount: Money::of('10.00', 'USD'),
isDeliveryCharge: true,
delivery: new DeliveryCharge(exclusionConditionsMet: true),
);
exclusionConditionsMet is a host assertion that all applicable published
conditions have been checked. It is not an unconditional exemption switch:
- With a published exclusion,
trueapplies it;falseassesses taxable delivery of taxable goods;nullrefuses because the facts are unknown. - With a published inclusion, the host assertion cannot override the rule.
- No applicable US delivery record means a refusal, including dates before a
capture floor. A host can bind
Contracts\DeliveryRulesfor coverage it maintains. - Taxable delivery of exempt goods requires an independent classification/rate that the current delivery model cannot resolve, and refuses explicitly.
- Taxable delivery of partially exempt goods needs an allocation rule for its taxable base and also refuses until that rule is modelled.
The component defaults to DeliveryComponent::Transport.
DeliveryComponent::Handling reads the separate handling rule. The order supplies
the delivered goods' taxability from their assessments; a standalone delivery
TaxQuery must supply DeliveryCharge::$goodsTaxable itself.
A US delivery rule is only reached where the seller holds that state's permit (or the
sale was marketplace-facilitated). Without one the supply is NotRegistered and there
is no charge to exclude freight from — see
seller registrations.
Goods are assessed first, then the delivery is allocated using the order's
ApportionmentBasis. US delivery uses the component rule and the goods' outcome;
the freight price is not treated as the price of another product for a per-item
exemption. EU delivery continues to follow the supplied products' rates.
Delivery portions are retained on TaxAssessment::$portions, including their
dates and rounding policy. Document charges are applied once at order level.
Upgrade notes
Custom MarketplaceRules implementations answer liability(): MarketplaceLiability
in place of platformOwes(): bool; return MarketplaceLiability::PlatformOwes for
what used to be true and SellerCollects for false. Custom DeliveryRules
implement treatment() in place of included().
Custom SourcingRules and NexusThresholds implementations must accept the new
optional ?DateTimeImmutable $at = null argument. Existing one-argument calls
still work and mean today. The US regime passes the supply date to both.
Identified mixed intrastate sourcing now refuses until a regime can select a place
per authority layer. Missing delivery facts use RefusalReason::DeliveryFactsRequired;
unsupported or conflicting rules use RefusalReason::TaxRuleUnsupported.
Both implement the package's Refusal contract for a structured 422 response.