M7-T05: add meter CRUD API + retroactive recompute + OpenAPI regen

This commit is contained in:
2026-06-25 17:01:54 +02:00
parent 82bb329500
commit 839faaf95b
6 changed files with 1746 additions and 0 deletions
+341
View File
@@ -1379,6 +1379,155 @@
}
}
},
"/api/energy/meters": {
"get": {
"tags": [
"api-energy-meters"
],
"summary": "List Energy Meters",
"description": "List all meter epochs in ascending ``started_at`` order.\n\nReturns the full historical sequence of meter installations across all\ncommodities. The active meter (``ended_at=null``) appears last because it\nhas the latest ``started_at``.",
"operationId": "list_energy_meters_api_energy_meters_get",
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/MeterListResponse"
}
}
}
}
}
},
"post": {
"tags": [
"api-energy-meters"
],
"summary": "Declare Energy Meter",
"description": "Declare a new meter epoch (swap, home move, or initial declaration).\n\nCloses the current active meter for the given commodity at ``started_at``\nand opens a new active meter. If no active meter exists, the new meter is\nsimply created without closing anything.\n\n**Validation**: ``started_at`` must be **≥** the current active meter's\nown ``started_at`` (no chronological backdate below the active epoch's\nstart). Equal timestamps are allowed (replaces the current meter at the\nsame logical moment). Violation → 422.\n\n**Retroactive recompute**: if ``started_at`` is in the past, billing\nrecords from that point forward are re-judged via ``recompute_range`` to\nreflect the new meter attribution. The response includes the count of\nrecomputed periods in ``recomputed_periods`` (not part of ``MeterResponse``\n— the recompute is transparent; callers should re-fetch costs if needed).",
"operationId": "declare_energy_meter_api_energy_meters_post",
"parameters": [
{
"name": "X-CSRF-Token",
"in": "header",
"required": false,
"schema": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "X-Csrf-Token"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/MeterDeclareRequest"
}
}
}
},
"responses": {
"201": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/MeterResponse"
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
},
"/api/energy/meters/{meter_id}": {
"patch": {
"tags": [
"api-energy-meters"
],
"summary": "Patch Energy Meter",
"description": "Partially update a meter epoch: rename, edit note, or correct started_at.\n\n- ``label``: updates the human-readable label.\n- ``note``: updates the free-form note.\n- ``started_at``: **retroactive correction** — shifts this meter's start\n boundary. The service layer maintains timeline continuity by also\n updating the preceding meter's ``ended_at``. Validation:\n * Must be strictly after the previous meter's own ``started_at``.\n * Must be strictly before this meter's ``ended_at`` (if closed).\n Violation → 422.\n\n**Retroactive recompute when ``started_at`` changes**: billing records in\nthe window ``[min(old, new), now)`` are re-judged to reflect the corrected\nmeter attribution.\n\nNot found → 404.",
"operationId": "patch_energy_meter_api_energy_meters__meter_id__patch",
"parameters": [
{
"name": "meter_id",
"in": "path",
"required": true,
"schema": {
"type": "integer",
"title": "Meter Id"
}
},
{
"name": "X-CSRF-Token",
"in": "header",
"required": false,
"schema": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "X-Csrf-Token"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/MeterPatchRequest"
}
}
}
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/MeterResponse"
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
},
"/api/expose": {
"get": {
"tags": [
@@ -3334,6 +3483,198 @@
"title": "ManualTariffSchema",
"description": "Fixed-tariff breakdown for manual contracts.\n\nPrices are the *effective* buy prices as used by the billing engine\n(energy_buy_x + energy_tax + ode) and the raw sell prices."
},
"MeterDeclareRequest": {
"properties": {
"label": {
"type": "string",
"maxLength": 255,
"minLength": 1,
"title": "Label"
},
"started_at": {
"type": "string",
"format": "date-time",
"title": "Started At",
"description": "UTC (or server-local naive) datetime from which this meter epoch starts. May be in the past (retroactive declaration)."
},
"reason": {
"$ref": "#/components/schemas/MeterReason",
"description": "Why this epoch was created. One of: initial, meter_swap, home_move, other."
},
"note": {
"anyOf": [
{
"type": "string",
"maxLength": 1024
},
{
"type": "null"
}
],
"title": "Note"
},
"commodity": {
"type": "string",
"maxLength": 32,
"minLength": 1,
"title": "Commodity",
"description": "Energy commodity this meter measures. Defaults to 'electricity'.",
"default": "electricity"
}
},
"type": "object",
"required": [
"label",
"started_at",
"reason"
],
"title": "MeterDeclareRequest",
"description": "Request body for POST /api/energy/meters.\n\nDeclares a new meter epoch (swap, home move, or initial declaration). The\nservice layer closes the current active meter for the given commodity at\n``started_at`` and opens a new one.\n\n``started_at`` follows the Principle-A localisation convention: a\ntimezone-naive value is interpreted as the **server's local wall-clock time**\n(e.g. CEST midnight → stored as UTC the night before); a timezone-aware\nvalue is converted to UTC as-is. Omitting ``started_at`` is not allowed —\nevery meter declaration must carry an explicit start timestamp.\n\n``commodity`` defaults to ``\"electricity\"``; the field is available for\nfuture use with ``gas`` or ``heating``."
},
"MeterListResponse": {
"properties": {
"items": {
"items": {
"$ref": "#/components/schemas/MeterResponse"
},
"type": "array",
"title": "Items"
},
"total": {
"type": "integer",
"title": "Total"
}
},
"type": "object",
"required": [
"items",
"total"
],
"title": "MeterListResponse",
"description": "Response schema for GET /api/energy/meters.\n\nMeters are returned in ascending ``started_at`` order so the caller sees\nthe historical installation sequence."
},
"MeterPatchRequest": {
"properties": {
"label": {
"anyOf": [
{
"type": "string",
"maxLength": 255,
"minLength": 1
},
{
"type": "null"
}
],
"title": "Label"
},
"note": {
"anyOf": [
{
"type": "string",
"maxLength": 1024
},
{
"type": "null"
}
],
"title": "Note"
},
"started_at": {
"anyOf": [
{
"type": "string",
"format": "date-time"
},
{
"type": "null"
}
],
"title": "Started At",
"description": "Retroactive correction of the meter epoch start timestamp. Triggers billing recompute over the affected window."
}
},
"type": "object",
"title": "MeterPatchRequest",
"description": "Request body for PATCH /api/energy/meters/{id}.\n\nAll fields are optional. Only non-``None`` values are applied.\n\nUpdating ``started_at`` is a **retroactive correction**: the service layer\nmaintains timeline continuity (adjusting the preceding meter's ``ended_at``)\nand the API layer triggers ``recompute_range`` over the affected window so\nthat billing attribution is re-judged."
},
"MeterReason": {
"type": "string",
"enum": [
"initial",
"meter_swap",
"home_move",
"other"
],
"title": "MeterReason",
"description": "Allowed values for the meter epoch creation reason."
},
"MeterResponse": {
"properties": {
"id": {
"type": "integer",
"title": "Id"
},
"label": {
"type": "string",
"title": "Label"
},
"commodity": {
"type": "string",
"title": "Commodity"
},
"started_at": {
"type": "string",
"format": "date-time",
"title": "Started At"
},
"ended_at": {
"anyOf": [
{
"type": "string",
"format": "date-time"
},
{
"type": "null"
}
],
"title": "Ended At"
},
"reason": {
"type": "string",
"title": "Reason"
},
"note": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Note"
},
"created_at": {
"type": "string",
"format": "date-time",
"title": "Created At"
}
},
"type": "object",
"required": [
"id",
"label",
"commodity",
"started_at",
"ended_at",
"reason",
"note",
"created_at"
],
"title": "MeterResponse",
"description": "Response schema for a single Meter epoch row.\n\n``ended_at`` is ``null`` for the currently active meter."
},
"MetricInfo": {
"properties": {
"key": {
+308
View File
@@ -1077,6 +1077,138 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/HTTPValidationError'
/api/energy/meters:
get:
tags:
- api-energy-meters
summary: List Energy Meters
description: 'List all meter epochs in ascending ``started_at`` order.
Returns the full historical sequence of meter installations across all
commodities. The active meter (``ended_at=null``) appears last because it
has the latest ``started_at``.'
operationId: list_energy_meters_api_energy_meters_get
responses:
'200':
description: Successful Response
content:
application/json:
schema:
$ref: '#/components/schemas/MeterListResponse'
post:
tags:
- api-energy-meters
summary: Declare Energy Meter
description: 'Declare a new meter epoch (swap, home move, or initial declaration).
Closes the current active meter for the given commodity at ``started_at``
and opens a new active meter. If no active meter exists, the new meter is
simply created without closing anything.
**Validation**: ``started_at`` must be **≥** the current active meter''s
own ``started_at`` (no chronological backdate below the active epoch''s
start). Equal timestamps are allowed (replaces the current meter at the
same logical moment). Violation → 422.
**Retroactive recompute**: if ``started_at`` is in the past, billing
records from that point forward are re-judged via ``recompute_range`` to
reflect the new meter attribution. The response includes the count of
recomputed periods in ``recomputed_periods`` (not part of ``MeterResponse``
— the recompute is transparent; callers should re-fetch costs if needed).'
operationId: declare_energy_meter_api_energy_meters_post
parameters:
- name: X-CSRF-Token
in: header
required: false
schema:
anyOf:
- type: string
- type: 'null'
title: X-Csrf-Token
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/MeterDeclareRequest'
responses:
'201':
description: Successful Response
content:
application/json:
schema:
$ref: '#/components/schemas/MeterResponse'
'422':
description: Validation Error
content:
application/json:
schema:
$ref: '#/components/schemas/HTTPValidationError'
/api/energy/meters/{meter_id}:
patch:
tags:
- api-energy-meters
summary: Patch Energy Meter
description: "Partially update a meter epoch: rename, edit note, or correct\
\ started_at.\n\n- ``label``: updates the human-readable label.\n- ``note``:\
\ updates the free-form note.\n- ``started_at``: **retroactive correction**\
\ — shifts this meter's start\n boundary. The service layer maintains timeline\
\ continuity by also\n updating the preceding meter's ``ended_at``. Validation:\n\
\ * Must be strictly after the previous meter's own ``started_at``.\n \
\ * Must be strictly before this meter's ``ended_at`` (if closed).\n Violation\
\ → 422.\n\n**Retroactive recompute when ``started_at`` changes**: billing\
\ records in\nthe window ``[min(old, new), now)`` are re-judged to reflect\
\ the corrected\nmeter attribution.\n\nNot found → 404."
operationId: patch_energy_meter_api_energy_meters__meter_id__patch
parameters:
- name: meter_id
in: path
required: true
schema:
type: integer
title: Meter Id
- name: X-CSRF-Token
in: header
required: false
schema:
anyOf:
- type: string
- type: 'null'
title: X-Csrf-Token
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/MeterPatchRequest'
responses:
'200':
description: Successful Response
content:
application/json:
schema:
$ref: '#/components/schemas/MeterResponse'
'422':
description: Validation Error
content:
application/json:
schema:
$ref: '#/components/schemas/HTTPValidationError'
/api/expose:
get:
tags:
@@ -2595,6 +2727,182 @@ components:
Prices are the *effective* buy prices as used by the billing engine
(energy_buy_x + energy_tax + ode) and the raw sell prices.'
MeterDeclareRequest:
properties:
label:
type: string
maxLength: 255
minLength: 1
title: Label
started_at:
type: string
format: date-time
title: Started At
description: UTC (or server-local naive) datetime from which this meter
epoch starts. May be in the past (retroactive declaration).
reason:
$ref: '#/components/schemas/MeterReason'
description: 'Why this epoch was created. One of: initial, meter_swap,
home_move, other.'
note:
anyOf:
- type: string
maxLength: 1024
- type: 'null'
title: Note
commodity:
type: string
maxLength: 32
minLength: 1
title: Commodity
description: Energy commodity this meter measures. Defaults to 'electricity'.
default: electricity
type: object
required:
- label
- started_at
- reason
title: MeterDeclareRequest
description: 'Request body for POST /api/energy/meters.
Declares a new meter epoch (swap, home move, or initial declaration). The
service layer closes the current active meter for the given commodity at
``started_at`` and opens a new one.
``started_at`` follows the Principle-A localisation convention: a
timezone-naive value is interpreted as the **server''s local wall-clock time**
(e.g. CEST midnight → stored as UTC the night before); a timezone-aware
value is converted to UTC as-is. Omitting ``started_at`` is not allowed —
every meter declaration must carry an explicit start timestamp.
``commodity`` defaults to ``"electricity"``; the field is available for
future use with ``gas`` or ``heating``.'
MeterListResponse:
properties:
items:
items:
$ref: '#/components/schemas/MeterResponse'
type: array
title: Items
total:
type: integer
title: Total
type: object
required:
- items
- total
title: MeterListResponse
description: 'Response schema for GET /api/energy/meters.
Meters are returned in ascending ``started_at`` order so the caller sees
the historical installation sequence.'
MeterPatchRequest:
properties:
label:
anyOf:
- type: string
maxLength: 255
minLength: 1
- type: 'null'
title: Label
note:
anyOf:
- type: string
maxLength: 1024
- type: 'null'
title: Note
started_at:
anyOf:
- type: string
format: date-time
- type: 'null'
title: Started At
description: Retroactive correction of the meter epoch start timestamp. Triggers
billing recompute over the affected window.
type: object
title: MeterPatchRequest
description: 'Request body for PATCH /api/energy/meters/{id}.
All fields are optional. Only non-``None`` values are applied.
Updating ``started_at`` is a **retroactive correction**: the service layer
maintains timeline continuity (adjusting the preceding meter''s ``ended_at``)
and the API layer triggers ``recompute_range`` over the affected window so
that billing attribution is re-judged.'
MeterReason:
type: string
enum:
- initial
- meter_swap
- home_move
- other
title: MeterReason
description: Allowed values for the meter epoch creation reason.
MeterResponse:
properties:
id:
type: integer
title: Id
label:
type: string
title: Label
commodity:
type: string
title: Commodity
started_at:
type: string
format: date-time
title: Started At
ended_at:
anyOf:
- type: string
format: date-time
- type: 'null'
title: Ended At
reason:
type: string
title: Reason
note:
anyOf:
- type: string
- type: 'null'
title: Note
created_at:
type: string
format: date-time
title: Created At
type: object
required:
- id
- label
- commodity
- started_at
- ended_at
- reason
- note
- created_at
title: MeterResponse
description: 'Response schema for a single Meter epoch row.
``ended_at`` is ``null`` for the currently active meter.'
MetricInfo:
properties:
key: