Appearance
VAT Rate Administration
Business Context
Business-Level Definition
VAT rates are set by law and change without warning. Every invoice the product completes, every cost figure it shows and every report it exports depends on the rate that was in force on the day the invoiced period started.
This feature is where the platform operator keeps those rates: one value per medium, each with the date it starts applying, each kept forever so that an invoice from three years ago still reproduces the number it showed then.
Requirements Definition
The product completes invoices for its customers: the user enters one amount and the system derives the other. That derivation is only as trustworthy as the rate behind it, and the rate is a legal fact the product has no way to infer.
Keeping rates in the hands of the operator rather than in a release matters for three reasons. A rate change is announced weeks ahead and takes effect on a fixed date, so it must be enterable in advance and start applying by itself. When it changes, nothing already invoiced may move — a report that quietly reprices last year is worse than one that is late. And in the previous system one medium's rate was fixed in the software and another was entered but never actually used, so a legislative change meant a release, and a rate an administrator had carefully filled in had no effect at all. Both are the kind of fault nobody notices until an auditor does.
Technical Context
User Stories / Use Cases
- As the platform operator, I want to enter a rate change before it takes effect, so the switch happens on the day the law says.
- As the platform operator, I want to see the full history per medium, so I can tell what was applied to any past period.
- As the platform operator, I want to correct a rate I typed wrongly, and to see that the correction is recorded.
- As the super admin, I want to be the only one who can correct or withdraw a rate that is already history, so the rates past invoices were completed with are not changed casually.
- As the platform operator, I want to export the catalogue to a spreadsheet, as I could in the previous system.
- As the platform operator, I want every medium the product bills to be covered, so no medium silently falls back to a value fixed somewhere else.
- As an energy manager in a customer organisation, I want the amounts on my invoice completed with the right rate without knowing this catalogue exists.
UI/UX Design
A single screen in the platform administration application, with its own entry in the main navigation next to the other cross-tenant catalogues (for now; it may later move under a shared reference-data section). Rates are grouped by medium, each group ordered by validity date, newest first, with the currently applying row marked.
Screen drafts, drawn with the administration application's own components and tokens: design/.

| State | What is shown |
|---|---|
| Catalogue populated | One group per medium; each row shows the rate, its start date, its legal reference, and whether it applies today |
| Rates in force today | A summary above the groups shows, per medium, the rate that applies today, its start date and the next scheduled change if one is entered |
| Medium with no rate | The group is listed and marked as having no rate, rather than being omitted — a missing medium has to be visible; the screen says that invoices for that medium cannot be completed and both amounts have to be entered by hand |
| Adding a rate | Medium, rate, start date and an optional legal reference; the start date may be in the future, in which case the form says the rate starts applying by itself on that date and which rate applies until then |
| Start date collides with an existing row | The form refuses and points at the existing row for that medium and date |
| Correcting a row | The same form, pre-filled, with the medium read-only; the screen states that the correction applies only to invoices completed from now on and that invoices already completed are not recalculated |
| Withdrawing a rate | Any rate can be withdrawn when needed; confirmation naming the medium and date; the row stops applying but stays visible in the history, struck through |
| Hiding withdrawn rows | Withdrawn rows are shown by default; a toggle hides them |
| Historical row | A row that has been superseded by a later rate for its medium; correcting and withdrawing it is offered only to the super admin, other operators see it as locked |
| Read-only operator | The same screen without the add, correct and withdraw actions |
| Export | An Export action in the page header downloads the rows currently shown — respecting the medium filter and whether withdrawn rows are hidden — as an XLSX file; available to anyone who can read the catalogue |





The screen follows the admin application's existing table and form patterns, including its keyboard operation and field-level error reporting; nothing here needs a bespoke interaction.
Functional Requirements
- Maintain one rate per medium and start date, with no end date: a rate runs until the next rate for that medium begins.
- Accept a start date in the future and begin applying it on that date without further action.
- Cover every billable medium; apply no rate that is fixed anywhere but this catalogue.
- Reject a second rate for the same medium and start date.
- Allow a rate to be corrected, and record who corrected it and when.
- Allow any rate to be withdrawn when needed, keeping it visible in the history and freeing its medium and date for re-entry.
- Never physically remove a rate, expired or withdrawn: the history of every medium stays readable.
- Never recalculate an invoice already completed when a rate is added, corrected or withdrawn: each invoice keeps the rate recorded on it, and only invoices completed afterwards see the change — the behaviour of the previous system, where both amounts were stored when the invoice was saved.
- Reserve the correction and withdrawal of a historical rate — one superseded by a later rate for the same medium — to the super admin; the rate in force and future rates can be changed by any operator with write access.
- Keep the legal reference optional.
- Export the catalogue as XLSX, with the same filter as the screen.
- Signal a medium with no rate on the screen only; no notification is sent.
- Serve the rate in force for a medium on a given date to the calculation that completes an invoice.
- Report that no rate is in force, rather than returning a default, where a date precedes the earliest rate for that medium.
- Record an entry in the platform audit trail for every creation, correction and withdrawal — the event, and the rate row it is filed against.
- Restrict every change to the platform administration surface; a customer organisation reads the catalogue and cannot alter it.
Internationalization & Localization
The screen's labels come from the platform's own labelling mechanism, as the rest of the administration application does — see GUI Labelling. The legal reference is free text entered in the language of the legislation and is never translated: it is a citation, and a translated citation is no longer traceable.
Non-Functional Requirements
- Durability. The catalogue is small and permanent; no row is ever physically removed, so every historical derivation stays reproducible.
- Availability. A rate lookup is on the invoice write path, so the catalogue must be readable whenever invoices can be entered.
Performance
The catalogue holds a handful of rows per medium and is read once per invoice write. No index beyond the unique key is needed, and no measurement target is set: a lookup that reads at most a few dozen rows is not a hot path.
Transactional Operations
Each change is a single row write committed with its audit entry, so a recorded change and its trail cannot diverge. Collision on the same medium and start date is prevented by the unique key rather than by a read-then-write check, which two administrators entering the same legislative change would otherwise both pass.
Processes & Related Systems / Components
| Trigger | Owner of the trigger | What this feature does |
|---|---|---|
| Legislation changes | platform operator | A row per affected medium is entered, with its start date and citation |
| Invoice saved or corrected | Cost Calculation | Serve the rate in force for the invoice's medium and period start |
| Invoice completed | Cost Calculation | The rate used is recorded on the invoice, so later catalogue changes do not move it |
| Rate corrected or withdrawn | this feature | Invoices already completed keep the rate recorded on them; only invoices completed afterwards see the change |
The consumer named in the table is the Consumption and Cost Calculation analysis, which is written against this catalogue and links it from its own document.
Diagrams & Models
Entering a rate change and its effect on the next invoice:
sequenceDiagram
actor A as Platform operator
participant S as VAT rate administration
participant C as Rate catalogue
actor U as Energy manager
participant K as Cost Calculation
A->>S: New rate for a medium, from a date
S->>C: Store the row, write the audit entry
C-->>S: Stored
U->>K: Save an invoice for a period
K->>C: Rate for this medium at the period start
C-->>K: The rate in force, or none
K->>K: Complete the second amount, record the rate used
Which rate applies to a given invoice:
flowchart TD
A[Invoice medium and period start] --> B{Rows for this medium?}
B -->|none| C[No rate in force]
B -->|some| D[Keep rows whose start date is on or before the period start]
D --> E{Any left?}
E -->|no| C
E -->|yes| F[Take the latest of them]
F --> G[Rate in force]
C --> H[Both amounts requested from the user]
API Analysis
See api-a.md.
Domain Model (ER diagram) & Data Attribute Table
erDiagram
VAT_RATE ||--o{ INVOICE : "completes"
GAUGE ||--o{ INVOICE : "is billed for"
- vatRate — created by this feature; one rate per medium and start date, in the cross-tenant schema.
- invoice — records which rate was used, so a completed amount stays reproducible.
- gauge — supplies the medium that selects the rate.
Data
The catalogue is this feature's reference data. It ships seeded with the statutory history of every billable medium since 1 January 2013, so that invoices migrated from the previous system — whose rate table held no data in the code base — can be completed and checked against the rate in force at their period start. Nothing before 2013 is seeded: an invoice from an earlier period reports no rate in force and asks for both amounts.
| Medium | From | Rate | Source |
|---|---|---|---|
| electricity, gas | 2013-01-01 | 21 % | Act 235/2004 Coll. as amended by Act 500/2012 Coll. — basic rate |
| electricity, gas | 2021-11-01 | 0 % | Extraordinary remission of VAT on electricity and gas for November and December 2021, Financial Bulletin 34/2021 |
| electricity, gas | 2022-01-01 | 21 % | End of that remission — basic rate |
| heat | 2013-01-01 | 15 % | Act 500/2012 Coll. — first reduced rate |
| heat | 2020-01-01 | 10 % | Act 80/2019 Coll. — heat and cooling moved to the second reduced rate |
| heat | 2024-01-01 | 12 % | Act 349/2023 Coll. — single reduced rate |
| water | 2013-01-01 | 15 % | Act 500/2012 Coll. — first reduced rate |
| water | 2020-05-01 | 10 % | Act 256/2019 Coll. — water supply and sewerage moved to the second reduced rate |
| water | 2024-01-01 | 12 % | Act 349/2023 Coll. — single reduced rate |
| fuel | 2013-01-01 | 21 % | Basic rate — the value the previous system applied to every fuel |
| phm | 2013-01-01 | 21 % | Basic rate |
sql
INSERT INTO shared.vat_rate (medium, rate_percent, valid_from, legal_reference) VALUES
('electricity', 21.00, '2013-01-01', 'Zákon č. 235/2004 Sb., ve znění zákona č. 500/2012 Sb.'),
('electricity', 0.00, '2021-11-01', 'Mimořádné prominutí DPH, Finanční zpravodaj č. 34/2021'),
('electricity', 21.00, '2022-01-01', 'Zákon č. 235/2004 Sb. — konec prominutí'),
('gas', 21.00, '2013-01-01', 'Zákon č. 235/2004 Sb., ve znění zákona č. 500/2012 Sb.'),
('gas', 0.00, '2021-11-01', 'Mimořádné prominutí DPH, Finanční zpravodaj č. 34/2021'),
('gas', 21.00, '2022-01-01', 'Zákon č. 235/2004 Sb. — konec prominutí'),
('heat', 15.00, '2013-01-01', 'Zákon č. 235/2004 Sb., ve znění zákona č. 500/2012 Sb.'),
('heat', 10.00, '2020-01-01', 'Zákon č. 80/2019 Sb.'),
('heat', 12.00, '2024-01-01', 'Zákon č. 349/2023 Sb.'),
('water', 15.00, '2013-01-01', 'Zákon č. 235/2004 Sb., ve znění zákona č. 500/2012 Sb.'),
('water', 10.00, '2020-05-01', 'Zákon č. 256/2019 Sb.'),
('water', 12.00, '2024-01-01', 'Zákon č. 349/2023 Sb.'),
('fuel', 21.00, '2013-01-01', 'Zákon č. 235/2004 Sb. — základní sazba'),
('phm', 21.00, '2013-01-01', 'Zákon č. 235/2004 Sb. — základní sazba');The table is the seed's content, not a legal opinion: the operator confirms it before go-live and corrects it through the screen. Two statutory nuances are deliberately not modelled. Firewood left the reduced rate for the basic rate in 2024 while other fuels were always at the basic rate; the catalogue keeps one fuel rate, as the previous system did. And between 2020 and 2023 hot water supplied as such stayed at 15 % while heat moved to 10 %; the catalogue has one heat rate, so an invoice for hot water from that period is completed at the heat rate unless both amounts are entered by hand — from 2024 both are at 12 % and the distinction no longer changes a figure.
Test Data
Use the project's Test Data location named in the overlay — the platform seed is where this catalogue's rows are created. Search it for the table name to find the seeded rates; the cases worth having are a medium with two rates whose validity dates straddle a test invoice period, and a medium with no rate at all.
Logging
The project has no shared logging-conventions document yet, so this feature's events are stated in full here.
.info— rate created, corrected or withdrawn: medium, rate, validity date, actor,requestId..info— rate served to a calculation: medium, requested date, validity date of the row returned..warn— no rate in force for a medium and date: medium, requested date..error— a change was rejected by the unique key: medium, validity date.
Monitoring
One feature-level need: the count of lookups that found no rate in force. It is normally zero, and a non-zero count means either a medium nobody has rated or an invoice reaching further back than the catalogue does — both worth noticing before a customer reports a blank amount.
Caching
None. The catalogue is small, read once per invoice write, and a cached rate that outlives a correction is exactly the failure this feature exists to prevent.
Backward Compatibility and Migration
The previous system held all media in one row per validity date, and the rows migrate into one row per medium and date, preserving every historical value and its start date.
Its generic list export is carried over as an XLSX export of the catalogue. Two of its behaviours are deliberately not carried over. Its fuel rate was fixed in the software rather than read from the catalogue; fuel and vehicle fuels become ordinary rows, seeded with the value that was fixed in code so that migrated invoices keep their amounts. And it carried a separate rate for hot water that no calculation ever read: heat and hot water share the heat medium here, and the migrated hot-water values are not carried across, because carrying a value that never applied would change historical figures rather than preserve them. Its rate table has no rows in the code base; the seed above replaces them, and the operator's live rows are compared with it once before go-live — any row that differs is kept as the operator entered it.
Legal Context
The rates in this catalogue are statutory. The product does not determine them: it applies the value recorded for a medium and date, which is why each row can carry a citation of the amendment it comes from (optional, but expected wherever the source is known) and why no row is ever discarded. Keeping the catalogue current is an obligation of whoever operates the platform, and the audit trail on each row is what evidences when a rate was entered and by whom. No licence or contract is required to operate the feature.
Cybersecurity Considerations
Writing is restricted to the platform administration surface and gated by a permission distinct from the read permission, so an operator who may see the catalogue cannot necessarily change it. Changing history — correcting or withdrawing a rate already superseded — needs a third permission held only by the super admin. The rows live in the cross-tenant schema with row-level security enabled: a customer organisation's context can read a platform row and can write none, and the write policy only admits a context with no tenant set. Every change is recorded in the platform audit trail.
Data Privacy. No personal data beyond the actor recorded on each change, which is the same actor metadata every record in the system carries.
Risk Assessment
Business Risks
A wrong rate is invisible at the point it is entered and visible on every invoice completed afterwards: amounts are plausible, internally consistent, and wrong. The mitigations are that each row carries the citation it came from, that the history is on one screen where a wrong value stands out against its neighbours, and that a correction cannot silently move figures already completed, because the rate used is recorded on the invoice.
The mirror risk is an absent rate: a medium nobody rated cannot complete an invoice at all. It is made visible rather than papered over — the screen lists a medium with no rate, the lookup reports none rather than defaulting, and the count is monitored.
Technical Risks
| Risk | Consequence | Mitigation |
|---|---|---|
| Two administrators enter the same change at once | Duplicate rates for one medium and date | The unique key rejects the second write |
| A rate is corrected after invoices used it | Historical amounts could move | The rate used is recorded on each invoice; completed invoices are never recalculated; only later completions see the change |
| A historical rate is changed by mistake | The history no longer shows what past invoices were completed with | Only the super admin may change a historical rate, and every change is in the audit trail with its before and after values |
| The catalogue is read in a tenant context that could also write it | A customer could change statutory data | Write policy admits only the platform context; read and write permissions are separate |
| A migrated row lands on the wrong medium | Invoices complete at a plausible but wrong rate | Migration maps one named column to one named medium, and the seeded values are checked against the citation |
| Cascade — this feature is unavailable | Invoices cannot be completed from one amount; cost figures already stored are unaffected | Both amounts can be entered by hand while it is unavailable |
Auditing, Reporting & Measurement
Every creation, correction and withdrawal is filed in the platform audit trail against the rate row it changed, with the actor and the time; the audited field set is on the entity's page. That trail plus the row's own citation is what answers the only question ever asked of this catalogue after the fact: which rate was in force, since when, and on whose authority. The feature reports nothing else; consumption of the rates is reported through the invoices that used them.