@classytic/tax-core
v0.2.0
Published
Shared contracts for jurisdiction tax engines — TaxResolver (invoice-time rating), TaxEngine/TaxObligation (filing-time compute), LedgerPostingAdapter + wireTaxToLedger, effective-dated rule windows, layered rule registries, and the ledger-facing TaxCode/
Readme
@classytic/tax-core
Shared contracts for jurisdiction tax engines. Zero jurisdiction logic,
zero rate tables, zero runtime dependencies — this package is the seams,
nothing else. Bangladesh lives in @classytic/bd-tax (the reference
implementation), Canada in @classytic/ca-tax, and the accounting engine
(@classytic/ledger) stays deliberately tax-free.
The design in one paragraph
Tax computation does not compose across countries — BD VAT, CA GST/HST,
and US sales tax share no math. What composes is everything AROUND the
math: how a line item asks "what rate applies right now" (resolver),
how a closed period becomes an amount owed (obligation), how an
obligation becomes journal entries (posting adapter), how rules are
versioned in time (effective windows), how hosts hot-patch when an
authority publishes mid-period (registries), and the posting-shaped
data ledgers consume (tax codes + report templates — the Odoo
account.tax insight, kept as pure data). tax-core owns exactly those
six seams.
Subpaths
| Subpath | Ships |
|---|---|
| ./resolver | TaxResolver — invoice-time rating contract: resolveClass (country-neutral tax classes → TaxClassRate), fiscal positions with audit trail, accountFor role mapping, TaxPackDescriptor introspection |
| ./obligation | TaxEngine<TInput, TBreakdown> → TaxObligation (integer minor units, typed breakdown) → LedgerPostingAdapter → wireTaxToLedger() (sequential, fail-fast, session-threaded) |
| ./effective | EffectiveDated windows (inclusive, open-ended current law) + isEffectiveAt / firstEffectiveAt / effectiveEntries / latestEntry + RuleProvenance |
| ./registry | createLayeredRules (row tables — extensions matched first) + createKeyedOverrides (keyed tables — newest wins): the boot-time hot-patch seam |
| ./ledger | TaxCode, TaxRepartitionLine, TaxExigibility, TaxReportLine, TaxReportTemplate, TaxCodesByRegion |
Two temporal surfaces, never conflated
// 1. INVOICE TIME — a line is being priced now
const resolver = createBdTaxResolver(); // from @classytic/bd-tax
const rate = resolver.resolveClass('STANDARD', invoiceDate); // { rateCode, rate, inputCreditAllowed, … }
const account = resolver.accountFor(rate.rateCode, 'output', 'standard');
// 2. FILING TIME — a period is being closed
import { wireTaxToLedger } from '@classytic/tax-core';
const postReturn = wireTaxToLedger(gst34Engine, gstLedgerAdapter, engine.repositories.journalEntries);
await postReturn(quarterData, { resolveAccount: (role) => accountIds.get(role), session });Consumers that only want computation (a filing tool, not an ERP) use
engine.validate + compute + render and never touch the ledger seam.
Rules for jurisdiction packs
- Money is integer minor units (paisa/cents) at every contract boundary. Packs computing in major units convert at their edge.
- Everything effective-dated. Old documents recompute under the law
of their date forever: never mutate a closed window — close it with
effectiveToand push a new entry. All lookups go through./effective; there is one implementation of "active on this date". - Provenance is data. Rate entries carry
sourceRef(the legal instrument),verifiedOn,notes— auditors ask, the table answers. - Unknown returns null, invalid throws.
resolveClass('NOPE')is a business decision for the caller;validate(garbage)is a bug at the boundary. - Hot-patches are dated, not blanket. Compose
./registryinto anextend<Country>TaxRules()facade; extensions carry the same windows as built-ins so a mid-year SRO patch is period-accurate. - Packs peer on tax-core only. They never import
@classytic/ledger— hosts wire the two at the application layer.
Reference implementation
@classytic/bd-tax — study it before writing a new pack: dated rate
schedules with a gazette-verification research trail
(research/FY2026-27/), the extension facade (extendBdTaxRules),
per-fiscal-year currency tests, and the update recipe
(docs/updating-rules.md). Architecture rationale: the fajr repo's
packages/tax/PLAN.md.
Primitives alignment
tax-core peers on @classytic/primitives >= 0.9 for TYPE-ONLY imports
(runtime stays zero-dependency): TaxPeriod extends DateRange
(/period), currency fields accept the branded CurrencyCode
(/currency), and minor-unit amounts pair with primitives' Money /
minorUnitFactor. Effective-dated windows and layered registries do NOT
exist in primitives — they are tax-core's own contribution (candidates to
upstream if a non-tax package ever needs them).
ESM only · Node ≥ 20 · exactOptionalPropertyTypes · zero runtime deps · MIT
