Skip to content
Updated Sep 22, 2026 by Pablo Coufal · Owner: analysisactivefeatureconfluence-migration Edit on GitHub

Invoice Management ​

Business Context ​

Business-Level Definition ​

Invoice Management covers the entry, editing, deletion, and export of invoices (expenses) recording what a client has been billed for energy/fuel consumption on a given gauge, plus validating those invoices against reading-derived consumption and computed prices. It is the billing-side counterpart to Reading Management within the Ingestion domain: Reading Management captures what was measured, Invoice Management captures what was billed.

Requirements Definition ​

  • CRUD of invoices tied to a gauge (F96, F98, F99)
  • Export of the invoice overview to XLSX (optionally to PDF) (F100)
  • Attribute additions in the metering-point overview surfacing the most recent invoice's dates (F32)
  • Validation of invoiced consumption against reading-derived consumption within a tolerance (F93)
  • Validation of invoiced price against a computed expected price (F94)
  • Detection of gaps/overlaps in invoice period continuity per gauge (F102)
  • ČR ERÚ / contract-price based invoice check — confirmed deferred to v2 (F358), not part of this feature's v1 scope

Open Questions ​

  • Open question (business): F94 expected-price calculation — the exact formula and inputs (distribution tariff, breaker size, contract price) are not yet specified; depends on Epic 3.4 / Epic 8.6.
  • Resolved (dev): F93's reading-derived consumption input — per-tariff consumption for an arbitrary period, computed from readings — is GET /v1/consumption/summary, owned by Consumption Aggregation & Effective Consumption. It totals a period that need not align to calendar buckets, groups by tariff, and returns the coverage behind the figure.

Acceptance Criteria ​

Ready for implementation now:

  • A user can create an invoice for a gauge with the required fields; dateFrom <= dateTo is enforced.
  • A user can edit an existing invoice, subject to the same dateFrom <= dateTo rule.
  • A user can delete an invoice; this is a soft delete — the record is excluded from default reads/lists but not physically removed.
  • A user can export the current (filtered) invoice overview to XLSX (optionally to PDF), with gauge identifier (new), invoiceNumber, dateFrom, dateTo, one column per invoiceValue segment (kind × tariff, dynamic per invoice), price, priceWithoutVat, createdBy, and note as columns.
  • A user can list and filter invoices (by gauge, by period) and retrieve full detail for a single invoice.
  • At invoice save time, a user is shown an inline signal when a billing-period gap/overlap exists versus the previous invoice for the same gauge.

Pending open questions above — not yet implementable:

  • Consumption-tolerance validation at invoice entry (F93)
  • Price validation at invoice entry (F94)
  • Notice creation for a detected continuity gap (F102)
  • Surfacing invoice-date attributes in the metering-point overview (F32) — the lookup logic is confirmed, but the metering-point overview endpoint itself has not yet been analyzed as its own feature

N/A — not available in the source material.

Technical Context ​

User Stories / Use Cases ​

  • As a client user, I want to record an invoice for a gauge so the billed cost is captured for reporting.
  • As a client user, I want to edit or delete an invoice that was entered incorrectly.
  • As a client user, I want to export the invoice overview to PDF so I can share it externally.
  • As a client user, I want the system to warn me when a billing period is missing so I don't end up with a silent gap in invoice history. (F102 — not yet implemented)
  • As a client user, I want the system to flag a suspiciously large deviation between invoiced and metered consumption. (F93 — not yet implemented)

UI/UX Design ​

Invoice entry/edit form

  • Static fields: invoiceNumber (required); dateFrom/dateTo (required, inline dateFrom <= dateTo validation); price/priceWithoutVat (required, VAT-linked); note (optional). fileId stays hidden/disabled in v1 UI — depends on Epic 9.1 (document entity doesn't exist yet).
  • Dynamic invoiceValue rows: repeatable group — kind (consumption / costWithVat / costWithoutVat), tariff (total / high / low / null), value, unit (filtered by kind). Client-side duplicate-(kind, tariff) validation mirroring the DB UNIQUE constraint. Save is disabled until at least one consumption row exists.
  • Recommend reusing the VT/NT tariff-entry pattern already designed for the Odečty (Readings) tab (Reading Management, Figma "Majetek — měřidlo · Odečet · v2"), extended with the kind dimension, for visual consistency.

Invoice list

  • Filters: gauge, period. Columns match GET /v1/invoices: invoiceNumber, dateFrom, dateTo, price, priceWithoutVat, createdBy. Export action in the toolbar (XLSX/PDF picker), respects active filters.

Delete confirmation

  • Single confirm dialog, soft-delete only. EM2's three-way record/file/both choice is deferred to Epic 9.4 (Feature 329) — not part of this dialog.

Gap-warning dialog (F102) — interim decision, not final

  • Blocking-but-dismissible modal at invoice save time: "There's a gap between this invoice and the previous one for this gauge — proceed anyway?" Underlying Notice creation/resolution UI belongs to Epic 6.1, whenever it lands — this only fixes the moment and wording of the save-time prompt.

Functional Requirements ​

FR1 — Invoice creation (F96): required fields are gaugeId, invoiceNumber, dateFrom, dateTo, price, priceWithoutVat, plus at least one invoiceValue row of kind consumption. tenantId is inferred from the auth context, never accepted in the request body. note is optional.

  • fileId references an existing document — created via the Document API, not inline as part of invoice creation; upload UX (new file vs. picking an existing document) and delete granularity (record/file/both) are defined by the File Storage domain (Epic 9.1/9.4), not duplicated here.

FR2 — Invoice editing (F98): any field from FR1 may be updated on an existing invoice. dateFrom <= dateTo is re-validated on every edit.

FR3 — Invoice deletion (F99): soft-delete only (deletedAt). Deliberate deviation from EM2, which hard-deletes invoices today. No time-window or billing-lock restriction identified yet.

  • EM2 offers a three-way delete choice (record only / file only / both) via bespoke, invoice-specific code — confirmed not to reuse EM2's generic file-deletion mechanism used by other entities.
  • This is Feature 329 (Epic 9.4, File Storage domain), not part of this feature's v1 scope.

FR4 — Invoice export (F100): exports the currently-filtered invoice overview to XLSX (matches EM2); PDF is included as an additional format alongside it.

  • Columns, in order: gauge identifier (new — EM2's export is scoped to a single gauge, so has no such column; needed here since this export spans gauges), invoiceNumber, dateFrom, dateTo, one column per invoiceValue segment (kind × tariff, dynamic per invoice — matches EM2, which exports per-segment values as separate columns sourced from invoice_value), price, priceWithoutVat, createdBy, note.
  • EM2 exports only the raw per-segment value, not its unit — same limitation carried over for v1.

FR5 — Metering-point overview attributes (F32): the "last invoice saved" / "last invoice validity" dates are resolved query-time against invoice (ordering by dateTo DESC per gauge) — no denormalized cache field is stored on gauge. Wiring this into the metering-point overview endpoint itself is pending that feature's own analysis — not a blocker for this feature's own scope.

FR6 — Consumption tolerance check (F93): when an invoice is saved, invoiced consumption is compared against reading-derived consumption for the same period, obtained from GET /v1/consumption/summary grouped by tariff. A warning triggers when either the relative or the absolute threshold is exceeded. The tolerance is a global, per-client configuration value. Where the total is marked provisional, or its coverage shows the period is only partly measured, the comparison says so rather than reporting a deviation that is an artefact of missing data.

FR7 — Price check (F94): Not yet implemented. Invoiced amount is compared against a computed expected amount. Also tolerance-based: a warning triggers when the deviation exceeds the given threshold. Same global per-client configuration as FR6.

  • price/priceWithoutVat are the invoice's authoritative, always-required totals.
  • Open question: when the invoiceValue entity's costWithVat/costWithoutVat rows come into use, should they be required to reconcile (sum) with price/priceWithoutVat, or are they independent? Note: there is no EM2 precedent (zero historical rows) to draw on.

FR8 — Invoice date continuity check (F102): hybrid model — gap/overlap detection runs synchronously and non-blockingly at invoice save time (comparing the new dateFrom against the previous invoice's dateTo for the same gauge), returned inline in the invoice create/edit response. A detected gap creates a Notice (Actions domain) that the user can resolve with a justification, rather than a transient UI toast; Notice creation is pending the notice entity (Epic 6.1).

FR9 — ČR ERÚ / contract-price check (F358): confirmed deferred to v2. Not part of this feature's v1 scope.

FR10 — Invoice list, filtering & detail retrieval: GET /v1/invoices returns a paginated, tenant-scoped list filterable by gaugeId and period, returning header fields only by default (no invoiceValue rows, for list performance). An optional includeConsumptionValues query parameter additionally returns each row's consumption-kind invoiceValue entries (tariff, value, unit); no other kind values are included, and the default response is unchanged. GET /v1/invoices/{id} returns full detail including all invoiceValue rows — needed e.g. to prefill the edit form. Missing from the initial CRUD split; FR1–FR3 explicitly cover only create/edit/delete.

FR11 — Invoiced consumption, monthly aggregation: GET /v1/invoices/consumption-by-month returns, for a given gaugeId and year, invoiced consumption totals grouped by calendar month and tariff (consumption-kind invoiceValue rows only), scoped to the caller's tenant.

Non-Functional Requirements ​

  • Performance Considerations: idx_invoice_gaugeId_dateTo (on gaugeId + dateTo DESC) is required from day one — it serves two confirmed access patterns: FR5's per-gauge "most recent invoice" read, and FR8's per-gauge continuity check on every write. Without it, both patterns force a sort over all of a gauge's invoices at query time.
  • Transactional Operations: invoice create/edit/delete are single-row operations — no multi-table atomic requirement identified for this feature. Bulk invoice import (Feature 97) is a separate capability under Epic 2.1, not this feature.

API Analysis ​

POST   /v1/invoices              Create invoice
PATCH  /v1/invoices/:id          Edit invoice
DELETE /v1/invoices/:id          Soft-delete invoice
GET    /v1/invoices/export       Export current (filtered) invoice list to XLS (optionally PDF)
GET    /v1/invoices              List/filter invoices, filterable by gaugeId and period, paginated; optional includeConsumptionValues param adds per-row consumption values
GET    /v1/invoices/:id          Get full invoice detail, including invoiceValue rows
GET    /v1/invoices/consumption-by-month   Monthly invoiced-consumption totals for a gauge/year

The reading-derived side of the comparison is not an invoice endpoint: it is `GET /v1/consumption/summary`
(see Consumption Aggregation & Effective Consumption), called with the invoice's own period and grouped by tariff.

Domain Model (ER diagram) & Data Attribute Table ​

No ER diagram in source material. Related entity pages (Confluence, not yet migrated into this repo's Entity DAT catalog): invoice (primary entity for this feature), gauge, invoiceValue (invoiced consumption/cost, modeled as a separate child entity).

invoice.fileId references an uploaded document, but the document entity itself does not yet have an entity page (File Storage domain, Epic 9.1, not yet analyzed) — this reference cannot be linked until that entity exists.

Data ​

Pending — sample rows will follow once the open creditNote and revisio_record_id questions are resolved, so example records reflect the final column set.

Test Data ​

N/A — not covered in source material.

Logging ​

  • .info — successful invoice creation, edit, or soft-delete
  • .warn — invoice date continuity gap detected at save time
  • .error — none identified yet

Monitoring ​

N/A — not covered in source material beyond the logging entries above.

Caching ​

N/A — not covered in source material.

Backward Compatibility and Migration ​

  • Confirmed 1:1 column mapping from EM2's invoices table: gauge_id→gaugeId, invoice_number→invoiceNumber, date_from→dateFrom, date_to→dateTo, price→price, price_without_vat→priceWithoutVat, file_id→fileId, note→note.
  • creditNote — confirmed: 1:1 carryover from EM2 (flag on a standard invoice, no dedicated entry flow).
  • revisio_record_id — confirmed active integration (client 46, Město Slaný), but not migrated as a column on invoice. Documented separately on a dedicated migration page (Confluence id 676003843).
  • EM2 hard-deletes invoices (no deletedAt equivalent exists today) — EM3's deletedAt column has no historical data to migrate; it starts null for all migrated rows.
  • Bulk invoice import/migration mechanics are covered under Epic 2.1 (Feature 97), not this feature.
  • EM2's invoice_value table (per-segment consumption/cost, 14-value flat Segment enum shared with GaugeReadingValue) maps to EM3's invoiceValue — but the shape changes: EM3 decomposes into two orthogonal dimensions (kind and tariff) and adds a UNIQUE (invoiceId, kind, tariff) constraint EM2 never had.
    • The costWithVat/costWithoutVat rows coexist with invoice.price/.priceWithoutVat.
  • Import origin tracking: EM2 cannot reliably distinguish ISDOC import vs. bulk import on an invoice (only guesses via the attached file's extension) → resolved: a dedicated import-tracking page (Confluence id 680951830) documents that the entity invoice.importId references has a sourceType field (bulkImport / isdoc) — derived during migration from the linked file's extension.
  • The complete field-by-field migration map is maintained on a separate child page (Confluence id 680951813) — out of scope for this migration (per the migration-map exclusion in this pass).

N/A — not addressed in the source Confluence page; migrated as a reference copy without new legal analysis.

Cybersecurity Considerations ​

N/A — not addressed in the source Confluence page; migrated as a reference copy without new security analysis.

Risk Assessment ​

N/A — not addressed in the source Confluence page; migrated as a reference copy without new risk analysis.

Auditing, Reporting & Measurement ​

N/A — not addressed in the source Confluence page beyond the Logging entries above.


Migrated as a reference copy from Confluence page "Invoice Management" (id 667811842). Content reorganized to fit this repo's feature-doc template; not re-analyzed against the current codebase. The child "Migration Map" page (Confluence id 680951813) and the "revisio_record_id" integration page (id 676003843) were intentionally not migrated in this pass, per the scope guardrail excluding kontrolní body / [Podklady] / Migration Map child pages.