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

Cost Calculation ​

Business Context ​

Business-Level Definition ​

An invoice is a single amount for a period. A cost is that amount placed in time, so it can be set against the consumption that caused it, against the same month last year, and against the other buildings in the portfolio.

This feature does that placement. It spreads each invoiced amount across the period it covers, keeps the amount with and without VAT, and defines what the cost of a whole building is — which is a question about who pays, not about who consumes.

Requirements Definition ​

Energy managers are judged on money, not on kilowatt-hours. Until an invoice becomes a series in time, the product can show what a building used but not what it cost, cannot compare a mild month against an expensive one, and cannot answer the question every budget meeting starts with: where did the money go, and is it more than last year.

Two rules make this harder than dividing by days. The amount is exact — a cost series that does not sum back to the invoice, to the last hundredth, is an accounting discrepancy, and one nobody will forgive. And a building's cost is not the sum of everything metered in it: a tenant on their own contract consumes inside the building but is billed by someone else, a sub-meter measures energy already counted by the meter above it, and energy sold to the grid is income, not cost.

Technical Context ​

User Stories / Use Cases ​

  • As an energy manager, I want an invoice to appear as cost in the months it covers, so I can compare cost with consumption.
  • As an energy manager, I want to enter one amount and have the other completed, so I do not retype what the system can derive.
  • As an energy manager, I want the rate used to stay with the invoice, so last year's figure does not change when the law does.
  • As a manager, I want the cost of a building to count only what the operator actually pays, so the number matches the budget.
  • As an accountant, I want the monthly costs of an invoice to add back to the invoice exactly, so the report reconciles.

UI/UX Design ​

N/A — the feature has no interface of its own. The invoice form, the invoice list and the billing screen belong to Invoice Management; cost series are displayed by Chart Engine.

Functional Requirements ​

Completing the invoice amounts

  • Accept one amount from the user, with an explicit statement of whether it includes VAT, and derive the other.
  • Determine the rate from the catalogue by the invoice's medium and the first day of the invoiced period.
  • Record the rate used on the invoice, so the derivation stays reproducible after the catalogue changes.
  • Require both amounts from the user where no rate is in force for that medium and date.
  • Store both amounts to exactly two decimal places.
  • Warn, but allow saving, where amounts entered or overridden by hand imply a rate more than 0.5 percentage points away from the rate in force.

Spreading cost over time

  • Spread each amount across the 15-minute intervals of the invoiced period, producing one series with VAT and one without.
  • Shape the spread by the consumption profile of the same gauge and period where one is known — the remote series, else the manual series, never the effective series, which changes with the source priority; spread evenly where none is.
  • Weight intervals not covered by the profile with the average of the covered part, then normalise the whole profile so the series sums exactly to the invoiced amount; write the series by cumulative rounding to two decimal places, so that it sums to the amount exactly and every contiguous run of intervals sums to its rounded share.
  • Produce one cost series per invoice with the tariff total; produce cost series per tariff band only where the invoice itemises the amounts per band, and reject the write where those items do not sum to the invoiced amount. Never split an amount across bands in proportion to the quantities.
  • Keep the link from every cost value back to the invoice it came from.
  • Carry a credit note as negative values over the period it corrects.
  • Recompute only the cost series of the invoice that changed; leave consumption untouched.
  • Remove the cost series of a deleted invoice.

Cost of a building

  • Sum the cost series of the gauges whose output mode is include and whose direction is import; the same attribute decides financial inclusion as decides physical inclusion, with no separate setting.
  • Leave out the gauges whose output mode is subtract, reportOnly or exclude: a tenant's invoice on a subtracted gauge is money the operator does not pay, and the arithmetic sign that the building-total formula gives a quantity has no meaning for money.
  • Treat energy delivered to the grid as income: an invoice or credit note on an include gauge with direction export is reported separately and never enters the building's cost.
  • Sum at read time, like a portfolio figure; store no cost series for a building's virtual gauge.
  • Leave the operator's net cost after re-billing a tenant to a later feature on outgoing invoices.

Internationalization & Localization ​

Amounts are stored in the currency of the invoice and converted for display by the conversion service that Chart Engine owns. The rate catalogue carries a free-text legal reference per row, which is written in the language of the legislation and not translated.

Non-Functional Requirements ​

  • Exactness. Amounts and their series are exact decimals end to end; no step of the spread is allowed to introduce a rounding drift.
  • Reproducibility. Given the invoice and the rate recorded on it, both amounts and the whole series can be recomputed at any later date.

Performance ​

Spreading one invoice touches the intervals of one billing period for one gauge — a bounded piece of work, run when the invoice is saved. Invoice-driven recalculation is never triggered by a reading; a corrected reading reshapes a cost series only when that series is next recomputed, which keeps a bulk reading import from rewriting every invoice in the same window.

Transactional Operations ​

An invoice and its derived series are one unit of work: saving an invoice stores the amounts, the rate used, and the cost series together, or stores none of them. The series is replaced wholesale on recalculation rather than adjusted in place, so a partially rewritten period cannot exist. The exactness rule is enforced at the end of that unit: the sum of the written series is compared against the invoiced amount, and a mismatch fails the write rather than storing a series that does not reconcile.

TriggerOwner of the triggerWhat this feature does
Invoice created or correctedInvoice ManagementComplete the second amount, spread both over the period, store the rate used
Invoice deletedInvoice ManagementRemove the cost series of that invoice
Consumption profile of the period recomputedConsumption CalculationReshape the cost series of invoices covering that period, keeping their totals
VAT rate added to the catalogueplatform operatorApply to invoices whose period starts on or after the new validity date; earlier invoices keep the rate recorded on them
Cost series storedthis featureHand the affected window to Consumption Aggregation & Effective Consumption

Diagrams & Models ​

Saving an invoice:

sequenceDiagram
    actor U as Energy manager
    participant I as Invoice Management
    participant K as Cost Calculation
    participant V as VAT rate catalogue
    participant P as Consumption series
    participant S as Cost series
    U->>I: Enter one amount and the period
    I->>K: Invoice saved
    K->>V: Rate for medium and period start
    V-->>K: Rate in force
    K->>K: Derive the second amount, record the rate
    K->>P: Profile for the gauge and period
    P-->>K: Interval weights, or none
    K->>K: Weight, normalise, check the sum
    K->>S: Write both series
    K-->>I: Amounts and series stored

How one amount becomes a series:

flowchart TD
    A[Invoiced amount and period] --> B{Profile known<br/>for the period?}
    B -->|no| C[Even weight on every interval]
    B -->|partly| D[Covered intervals keep their weight]
    D --> E[Uncovered intervals take the average of the covered part]
    B -->|fully| F[Use the profile as the weights]
    C --> G[Normalise so the sum equals the amount]
    E --> G
    F --> G
    G --> H[Write the series with VAT and without]
    H --> I{Sum equals the invoice?}
    I -->|yes| J[Commit]
    I -->|no| K[Fail the write]

API Analysis ​

N/A — the feature has no HTTP surface of its own. The invoice endpoints belong to Invoice Management and the read endpoints to Chart Engine.

Domain Model (ER diagram) & Data Attribute Table ​

erDiagram
    INVOICE ||--o{ INVOICE_VALUE : "is itemised by"
    INVOICE ||--o{ CONSUMPTION : "is spread into"
    VAT_RATE ||--o{ INVOICE : "completes"
    GAUGE ||--o{ INVOICE : "is billed for"
    GAUGE ||--o{ CONSUMPTION : "costs"
  • invoice — the amounts, the period, and the rate used.
  • invoiceValue — the billed quantities and amounts per tariff band.
  • vatRate — created by this feature; the statutory rate per medium and validity date.
  • consumption — carries the cost series as its money kinds, alongside the quantity series.
  • gauge — the metering point, its medium, and whether it counts towards the operator's cost.

Data ​

The rate catalogue is the feature's reference data and is seeded with the rates in force per medium:

sql
INSERT INTO vat_rate (medium, rate_percent, valid_from, legal_reference) VALUES
    ('electricity', 21.00, '2024-01-01', 'Zákon č. 349/2023 Sb.'),
    ('gas',         21.00, '2024-01-01', 'Zákon č. 349/2023 Sb.'),
    ('heat',        12.00, '2024-01-01', 'Zákon č. 349/2023 Sb.'),
    ('water',       12.00, '2024-01-01', 'Zákon č. 349/2023 Sb.');

A worked example, so the exactness rule is unambiguous: an invoice of 10 000.00 without VAT at 21 % is 12 100.00 with VAT. Spread evenly across a 31-day January, the first ten days carry 3 903.23 and the remaining twenty-one 8 196.77 — which is 12 100.00, not 12 099.99. Shaped by a profile in which half the consumption fell in the first ten days, the same invoice gives 6 050.00 and 6 050.00. The monthly total is identical either way; only the shape inside the month differs, and that shape is what every finer-grained chart and every year-on-year comparison shows.

Test Data ​

Use the project's Test Data location named in the overlay. Search it for invoices attached to the seeded demo gauges — specifically one invoice on a gauge that also has readings for the same period (the profile-shaped case), one on a gauge with no readings (the even-spread case), and one credit note.

Logging ​

The project has no shared logging-conventions document yet, so this feature's events are stated in full here.

  • .info — invoice spread into a cost series: invoice, gauge, period, whether a profile was used, requestId.
  • .info — second amount derived: invoice, medium, rate applied, rate validity date.
  • .warn — amounts imply a rate outside the tolerance: invoice, implied rate, rate in force.
  • .warn — no rate in force for the medium and date; both amounts required from the user.
  • .error — the written series did not sum to the invoiced amount and the write was rejected: invoice, difference.

Monitoring ​

One feature-level need: the count of writes rejected because the series did not reconcile with its invoice. It should be zero, and a non-zero value is a correctness defect rather than a load symptom.

Caching ​

None. Cost series are read through the aggregation layer together with consumption.

Backward Compatibility and Migration ​

Legacy invoices are migrated as invoices, and their series are recomputed here rather than carried over, for the same reason as consumption. Two differences from the legacy behaviour are expected. A legacy period whose profile covered less than 95 % of days was spread evenly; it is now shaped by the part of the profile that is known, so the shape inside such a period changes while its total does not. And legacy fuel invoices were taxed at a rate fixed in code; they are recomputed against the catalogue, so a fuel invoice from a period with a different statutory rate now carries that rate.

VAT rates are set by legislation, and the figure the product shows has to match what the law said on the day the period started. This is why the rate is a catalogue row with a validity date and a legal reference rather than a constant, and why the rate used is recorded on the invoice: an invoice reproduced two years later must show the same amounts it showed when it was entered. The feature makes no tax determination of its own — it applies the rate recorded for the medium and date.

Cybersecurity Considerations ​

Cost series are tenant business data and are protected exactly as consumption is, by the tenant predicate the application-level guard applies to the hypertable. The rate catalogue is platform reference data, readable by every tenant and writable only by the platform operator.

Data Privacy. No personal data. An invoice identifies a metering point and a supplier, not a person.

Risk Assessment ​

Business Risks ​

The costly failure is a series that does not reconcile with its invoice: a report that disagrees with the accounting system by a few hundredths destroys trust in every other number on the page. The write is therefore rejected rather than stored when the sums disagree.

The second is the building cost being counted from the wrong set of gauges — including a tenant's own contract overstates the operator's cost, excluding a sub-meter that the operator does pay understates it. The configuration that decides this is per gauge and must be reviewed where energy is re-billed to tenants.

Technical Risks ​

RiskConsequenceMitigation
Rounding drift across thousands of intervalsSeries no longer sums to the invoiceExact decimals throughout; the sum is checked before commit
Profile changes after the cost was spreadCost shape no longer matches consumptionRecalculation reshapes the series and preserves the total
Rate catalogue missing a medium for a periodSecond amount cannot be derivedBoth amounts are requested from the user instead of guessing a rate
Rate corrected after invoices were enteredHistoric figures would moveThe rate used is stored on the invoice; only later periods are affected
Cascade — this feature stopsCost charts, invoice checks and budget reporting stop gaining data; consumption is unaffectedCost and quantity series are computed independently of each other

Auditing, Reporting & Measurement ​

The amounts, the period and the rate used are recorded on the invoice, which carries its own audit trail; the derived series names the calculation that wrote it and the invoice it came from, so any monthly cost figure can be traced to the invoice behind it and recomputed. Reporting on cost is performed by the reporting features over the same series, not by this feature.