Skip to content

Dates

Dates

Tax is dated law. Rates change, categories move in and out of tax, exemption certificates expire. An engine that only ever answers "as of now" cannot reissue an invoice, cannot restate a period, and cannot be audited — and the failure is quiet, because the answer still looks like a number.

Two dates, because they do two jobs

new TaxQuery(
    // ...
    suppliedAt: new DateTimeImmutable('2026-12-30'),   // when the supply happened
    reportedOn: new DateTimeImmutable('2027-01-03'),   // which return it lands in
);

suppliedAt is the tax point. It decides which law applies: which rate was in force, whether the category was taxable, whether the buyer's exemption was valid that day. Null means today, which is what an ordinary live sale wants.

reportedOn decides which return period the supply is filed in. It defaults to the tax point, and is only worth setting when the two genuinely differ — goods supplied on 30 December and invoiced on 3 January are rated at December's rate while national rules may put them in either period. One date cannot do both jobs, and collapsing them means either mispricing the supply or misfiling it.

Both are echoed on the assessment as taxPoint and reportedOn, so an audit can see what the engine actually resolved against rather than inferring it.

One date, everywhere in the assessment

The tax point is threaded through every dated lookup for that supply:

Resolved on the tax point Where the dated data lives
the rate TaxRateSource::rateFor($jurisdiction, $category, $at)
product taxability ProductTaxability::determine($jurisdiction, $category, $amount, $at)
the buyer's exemption validity TaxExemption validity window
the seller's registrations SellerRegistration validity window (validFrom/validUntil)
marketplace, holidays and sourcing dated rules in the register

The seller's side matters for the same reason as the buyer's: recalculating last year's invoices with today's registrations produces a return for a period the seller was not registered in. See seller registrations.

This is a correctness property, not a convenience. An assessment priced with one year's rate and another year's taxability is internally inconsistent, and a state that started taxing a category last year would otherwise have the engine charge tax on a supply made before the law existed.

// A state that exempted groceries until 2025-12-31 and taxes them from 2026-01-01.
$grocery = fn (string $date): TaxQuery => new TaxQuery(
    amount: Money::of('100.00', 'USD'),
    pricing: Pricing::Exclusive,
    place: $kansas,
    customer: CustomerType::Consumer,
    seller: $registrations,
    category: TaxClass::Groceries,
    suppliedAt: new DateTimeImmutable($date),
);

$calculator->assess($grocery('2025-06-15'))->treatment;  // TaxTreatment::Exempt
$calculator->assess($grocery('2026-06-15'))->treatment;  // TaxTreatment::Standard

($query->on() reads the resolved tax point back — it is the getter the engine itself uses, not a builder.)

Window boundaries are inclusive on both sides: 31 December is the last day of the old rule and 1 January the first day of the new one. An off-by-one here is a whole day of invoices priced wrong.

What a source does with a date it cannot honour

RegisterRateSource and RegisterTaxability select rate and rule windows on the supply date. A rate source that cannot answer for that date returns null, so the engine can refuse rather than use an unrelated rate.

Boundary artifacts are snapshots of the selected register release. They do not carry effective-date windows, so the shipped boundary resolver cannot reconstruct an annexation from a newer snapshot. Select an appropriate historical release, or bind a resolver with dated boundaries when that distinction matters.

NexusThresholds::for() and SourcingRules::for() accept an optional date; the US regime supplies the tax point. Published rounding and delivery rules use that date as well. Nexus still returns advisory figures rather than determining whether a seller's cumulative activity crossed a threshold. A capture-floor date establishes available coverage, not an earlier legal commencement that the engine can infer.