M7-T05: add meter CRUD API + retroactive recompute + OpenAPI regen
This commit is contained in:
@@ -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": {
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user