Tax Rate
A TaxRate represents a named, workspace-scoped tax definition used to compute fiscal obligations on invoice line items and accounting postings. Each rate carries a percentage, a tax type classification (VAT, GST, SALES_TAX, EXCISE, WITHHOLDING, or EXEMPT), an optional country scope for multi-jurisdiction workspaces, and optional links to two LedgerAccount relations for collected (output) and deductible (input) tax accounting. TaxRates are referenced by InvoiceItem (via applied_tax_rate_pk), WorkspacePostingMapping, and JournalEntryLine, making them the central lookup table for the double-entry accounting layer.
| Naming | Value |
|---|---|
| Object | Tax Rate |
Resource type (JSON:API type) | tax_rate |
| Collection / records root | tax_rates |
| REST base | /v1/tax-rates |
| Entity class | TaxRate |
API operations
| Operation | Method & path | Status |
|---|---|---|
| List | GET /v1/tax-rates | โ Implemented |
| Retrieve | GET /v1/tax-rates/{id} | โ Implemented |
| Create | POST /v1/tax-rates | ๐ก Planned |
| Update | PATCH /v1/tax-rates/{id} | ๐ก Planned |
| Delete | DELETE /v1/tax-rates/{id} | ๐ก Planned |
Data model
Attributes
| Field | Type | Required | Constraints | Allowed values | Description |
|---|---|---|---|---|---|
| tax_rate_id | string, UUID, ๐ system | โ Yes | UNIQUE; generated via gen_random_uuid() on insert | โ | Public immutable identifier for the tax rate. Exposed in the API; the internal pk is never surfaced. |
| name | string | โ Yes | max 255 chars; UNIQUE(workspace_pk, name) โ full (non-partial) unique constraint at the database level via the table-level @Unique decorator | โ | Human-readable label for the tax rate (e.g. 'TVA 20%', 'GST 10%'). Must be unique within the workspace; the uniqueness constraint is not partial on deleted_at IS NULL, so soft-deleting and re-creating a rate with the same name will violate the constraint. |
| rate | string (decimal 5,2) | โ Yes | CHECK rate >= 0 AND rate <= 100; stored as DECIMAL(5,2), surfaced as string to preserve precision | 0.00 โ 100.00 | Percentage rate applied to the taxable base. Precision to two decimal places. Range constraint enforced at the database level. |
| tax_type | enum (TaxTypeEnum) | โ Yes | Must be one of the allowed enum values; stored as native PostgreSQL enum tax_type_enum | VAT, GST, SALES_TAX, EXCISE, WITHHOLDING, EXEMPT | Fiscal classification of the tax. Determines how accounting postings treat the rate (e.g. EXEMPT generates no tax ledger lines; WITHHOLDING is typically a deduction). |
| country | enum (CountryCodeEnum), nullable | โช No | ISO 3166-1 alpha-2; stored as native PostgreSQL enum country_code_enum; nullable | Any ISO 3166-1 alpha-2 code supported by CountryCodeEnum, e.g. FR, DE, US, GB | Optional jurisdiction scope for the rate. When set, signals that this rate applies only to transactions in that country. Null means the rate is jurisdiction-agnostic. |
| is_default | boolean | โ Yes | Default false; no database-level single-default-per-workspace constraint | true, false | When true, indicates this is the workspace's preferred rate for new invoice lines and postings. Application logic should enforce at most one default per tax_type per workspace. |
| is_active | boolean | โ Yes | Default true | true, false | Controls whether the rate is available for selection in new transactions. Inactive rates remain for historical reference on existing records but are excluded from rate pickers. |
| effective_from | date, nullable | โช No | Stored as PostgreSQL DATE; nullable | โ | Inclusive start date from which the rate is valid. Null means the rate has always been in effect. Used for jurisdiction compliance when rates change (e.g. VAT rate adjustments). |
| effective_to | date, nullable | โช No | Stored as PostgreSQL DATE; nullable | โ | Inclusive end date after which the rate is no longer valid. Null means the rate has no expiry. Application code should filter rates by effective_from/effective_to when applying them to a document date. |
| created_at | datetime, ๐ system | โ Yes | Set once on insert via onCreate lifecycle hook; never updated; DB column is TIMESTAMPTZ NOT NULL DEFAULT now() | โ | Timestamp of record creation in UTC. |
| updated_at | datetime, ๐ system | โ Yes | Set on insert via onCreate and updated on every write via onUpdate lifecycle hook; DB column is TIMESTAMPTZ NOT NULL DEFAULT now() | โ | Timestamp of the last modification in UTC. Always present โ the database column is NOT NULL. |
| deleted_at | datetime, nullable | โช No | Nullable; null means not deleted | โ | Soft-delete timestamp. All live queries must filter deleted_at IS NULL. Soft-deleted rates are preserved for historical accuracy on past invoice lines and journal entries that referenced them. |
Relationships
| Name | Type | Required | Description |
|---|---|---|---|
| workspace | to-one (workspace) | โ Yes | The workspace that owns this tax rate. Enforces multi-tenant isolation: rates from one workspace are never visible to another. FK: workspace_pk โ core_api.workspaces(pk). |
| collected_account | to-one (ledger_account), nullable | โช No | The LedgerAccount used to post collected (output) tax on sales invoices. Typically a VAT-payable or output-tax liability account. When null, the accounting pipeline falls back to workspace-level posting mappings. FK: collected_account_pk โ core_api.ledger_accounts(pk). |
| deductible_account | to-one (ledger_account), nullable | โช No | The LedgerAccount used to post deductible (input) tax on purchase invoices. Typically a VAT-recoverable or input-tax asset account. When null, the accounting pipeline falls back to workspace-level posting mappings. FK: deductible_account_pk โ core_api.ledger_accounts(pk). |
System-computed
- tax_rate_id is generated via gen_random_uuid() as the PostgreSQL column default on insert; the MikroORM entity also calls randomUUID() as a JS-side default, meaning the value is always set before any database round-trip.
- created_at is set once on insert via the MikroORM @Property onCreate lifecycle hook (new Date()). It is never overwritten. The DB column is TIMESTAMPTZ NOT NULL DEFAULT now().
- updated_at is set on insert via onCreate and refreshed on every subsequent write via the onUpdate lifecycle hook. The DB column is TIMESTAMPTZ NOT NULL DEFAULT now(), so it is always present and never null.
- deleted_at is null for active records. Setting it to a non-null timestamp constitutes a soft delete. No hard-delete path exists for this entity โ historical invoice lines and journal entries reference tax_rate_pk and must remain linkable.
- The table-level @Unique({ properties: ['workspace', 'name'] }) decorator maps to a full (non-partial) UNIQUE(workspace_pk, name) constraint in the migration. Unlike the ledger_accounts precedent which uses a partial index (WHERE deleted_at IS NULL), tax_rates uses a full unique constraint. Soft-deleting and then re-creating a rate with the same name will violate this constraint; a follow-up migration adding a partial index and dropping the full constraint is recommended.
- is_default has no database-enforced single-default-per-workspace constraint. Application-layer logic in the service must guarantee at most one default per tax_type (or globally) per workspace before persisting.
- rate is stored as DECIMAL(5,2) and surfaced as a string in the API to avoid floating-point rounding errors in JSON serialisation. Callers must treat it as a fixed-precision decimal string.
- effective_from and effective_to are stored as PostgreSQL DATE (date-only, no time component). The application layer is responsible for filtering available rates against a document's issue date when selecting the applicable rate.
- Downstream consumers that hold a hard FK to tax_rates include: core_api.invoice_items (applied_tax_rate_pk), core_api.workspace_posting_mappings (tax_rate_pk), and core_api.journal_entry_lines (tax_rate_ref_pk). Soft-deleting a tax_rate does not cascade to these references โ historical records continue to point to the soft-deleted row.
Example
{
"data": {
"type": "tax_rate",
"id": "d4e7a3c2-81f5-4b2e-9d6a-1c0f3e8b5a47",
"attributes": {
"tax_rate_id": "d4e7a3c2-81f5-4b2e-9d6a-1c0f3e8b5a47",
"name": "TVA 20%",
"rate": "20.00",
"tax_type": "VAT",
"country": "FR",
"is_default": true,
"is_active": true,
"effective_from": "2024-01-01",
"effective_to": null,
"created_at": "2024-01-15T09:30:00.000Z",
"updated_at": "2024-03-10T14:22:00.000Z",
"deleted_at": null
},
"relationships": {
"workspace": {
"data": { "type": "workspace", "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890" }
},
"collected_account": {
"data": { "type": "ledger_account", "id": "9f8e7d6c-5b4a-3210-fedc-ba9876543210" }
},
"deductible_account": {
"data": { "type": "ledger_account", "id": "1a2b3c4d-5e6f-7890-1234-abcdef567890" }
}
}
}
}apps/api/src/database/entities/TaxRate.ts ยท domain: financial-graph ยท tier: Supporting