# Taxclassrates

> The tax rates of one geographic zone, one rate per tax class.

Source: CBX documentation, version 4.0 preview (unreleased). Canonical page: https://docs.configbox.at/docs/4.0-preview/technical/property-types/taxclassrates. Last updated 2026-07-27.

---
The tax rates of one geographic zone, one rate per tax class.

**Storage kind:** `derived` · **Column:** none

It sits on a geography record — a country, state, county or city — and edits that zone's row in
`#__configbox_tax_class_rates` for every tax class that exists. You will not add one to a new model:
it is specific to the four geography models, and `taxclasstype` is what tells it which.

## Settings

| Setting | Meaning |
|---|---|
| `taxclasstype` | `'country'` \| `'state'` \| `'county'` \| `'city'` — which zone column keys the rates |

`taxclasstype` decides the key column (`country_id`, `state_id`, `county_id`, `city_id`) used in both
the join and the write. It must match the model the property is on; nothing checks that, and a
mismatch writes rates against the wrong zone.

The label is suppressed (`doesShowAdminLabel()` returns false) — the rendered grid carries its own
headings.

## The record shape follows the data

The property does not map to one column. `getSelectsForGetRecord()` and `getJoinsForGetRecord()`
loop over `ConfigboxPrices::getTaxClasses()` and emit **one join and two selects per tax class**,
aliased `tcr_<id>`, producing record keys:

| Key | Holds |
|---|---|
| `tax_rate_tcr_<taxClassId>` | the percentage rate |
| `tax_code_tcr_<taxClassId>` | the reporting code |

So the number of values is not fixed by the definition — it follows the tax classes that exist.
Generated artifacts describe the property, not the per-class values, because the per-class values
are not knowable from the definition alone. A consumer must read the tax classes first.

## Writing is delete-then-insert

`store()` deletes **every** rate row for this zone and re-inserts one per tax class where either the
rate or the code was submitted. Consequences worth knowing:

- A save that does not carry the fields **clears the zone's rates**. Unlike
  [`multiselect`](https://docs.configbox.at/docs/4.0-preview/technical/property-types/multiselect), this type does not distinguish absent from empty — there is no
  "no instruction" path. Partial updates through the API must include them.
- A tax class with an empty rate *and* an empty code gets no row at all, rather than a zero row.
  Absent means "no rate for this class here", and the calculation reads it that way.
- The delete step is skipped when `$data->id` is `0`, since a zone being created has nothing to
  clear; the insert step runs either way. In practice the id is always set by then —
  `KenedoModel::store()` writes the new key onto the data object *before* it calls any property's
  `store()` — so rates can be entered on the same save that creates the zone.

German-locale decimal commas are converted to dots on the way in, for `de-DE`, `de-AT` and `de-CH`
only. Other locales that use a comma are not covered.

Being `derived`, it contributes no column to the base table and `storeExternally` does not apply.

@see multiselect.md — the other type whose values live in another table
@see ../com_configbox_calculation_engine.md
@see ../../admin-guide/commerce/set-up-taxes.md — tax classes and zones from the operator's side
