Tax
A jurisdiction-aware tax calculation service inside Railbase — effective-dated rules, brackets, reverse charge and exemptions — that every other finance module calls.
Problems it solves
- VAT and sales tax get typed onto invoices by hand from a rate sheet in someone's spreadsheet, and nobody notices when the rate changed in January.
- A mid-year rate change leaves two versions of the truth: old documents must keep the old rate, new ones the new rate, and nothing records which applied when.
- Exempt customers get charged anyway, because the exemption certificate sits in an email folder rather than in the system that computes the tax.
- Progressive calculations — payroll, income tax — live in a spreadsheet formula nobody can audit or replay a year later.
- The same invoice recalculated twice gives two different pennies, because every module rounds its own way.
Tax is the calculation service the rest of your Railbase finance modules call. Give it a source document, a jurisdiction code, a category and a base amount; it finds the rule in force on that date, applies reverse-charge, exemption or bracket logic, and caches the result against that document.
It is a data-resident module: jurisdictions, rules, brackets, exemptions, calculations and returns are declared as tenant-scoped collections in the signed bundle, with screens at /tax.
What you get
- One calculator.
POST /api/tax/calculatetakes the source document, jurisdiction code, category, counterparty and base amount, and returns the resolved rule, tax amount, effective rate and the reverse-charge and exempt flags. - Effective dating. Only a
publishedrule whose window covers the calculation date is a candidate; when several qualify, the latesteffective_fromwins. Roll a rate forward by adding the next revision — cached calculations keep pointing at the rule that made them. - Deterministic money. Tax is computed on integer cents with round-half-away-from-zero, so identical input yields an identical amount. Keyed by tenant, source module, source id and line sequence, a re-run rewrites the row instead of duplicating it.
- Brackets for progressive regimes. A bracketed rule walks its brackets in sequence, caps each band's width, adds
flat_amount + width × rate, and lets an open top band take the rest. - Exemptions and reverse charge as data. An exemption binds a counterparty to a rule with a certificate reference and its own window; a match returns zero tax, flagged exempt. Reverse charge charges nothing on this side and says so.
- Six JSON reports off that cache. Liability by rule and jurisdiction, VAT output against input, tax owed by jurisdiction, withholding, returns by status with an overdue count, and filed-return history. Three accept a
from/torange.
In the estate
Tax publishes tax.calculation.computed. Sales subscribes: on a sales-sourced calculation it stamps the tax amount and recomputes the invoice total, and only while the invoice is still a draft; tax on a finalized document is an accountant's edit. It also listens to the core's record.changed feed: a new sales invoice carrying tax_jurisdiction in its metadata calculates itself. Tax never posts to the ledger and emits no journal request.
Where it stands today
- It does not install yet. The core checks a bundle against its own surface before accepting it, and this one is still refused — the screens above call endpoints outside the module's namespace, and some declared events cannot be proven from the code. That gate is the reason the module is listed as coming soon rather than sold.
- The catalogue has no write path. Tax writes only the calculation cache. Jurisdictions, rules, brackets, exemptions and returns are read but never created — you seed them from the admin console's data screens.
- The
/taxscreens are only half wired. The calculator form and the liability summary read Tax's own verbs and work. The rules, jurisdictions, brackets and calculations tables readGET /api/collections/…, which the core denies for a plugin collection with an empty access rule — those four render empty until they move onto Tax's own verbs. - The withholding report cannot break out by counterparty. The counterparty drives the exemption match but is not stored on the cached calculation, so every row lands under
(unspecified). - Both returns reports read a collection nothing writes, so they stay empty until returns are entered from the admin side.
- The declared settings are not consumed — the code ignores the default currency and default auto-trigger category, falling back to
USDandstandard. - Aggregation is currency-blind: report totals sum the stored document currency with no FX step, and the per-calculation breakdown field is written empty.
- AR and AP do not consume the calculation event yet — sales is the only subscriber.
All three verbs sit behind the tax.use permission of the bundled tax_operator role and scope to the session's tenant, not a request header.
Reviews
No reviews yet — be the first to review Tax.