M6-T04: add EnergyContract CRUD + versions + profile validation API
- app/services/contracts.py: create_contract, add_version (append-only,
auto-closes prior version), activate_contract (mutex), and
active_contract_version_at (half-open [from, to)) for the billing engine.
- app/schemas/energy_contract.py + routes/api/energy_contracts.py: 6 endpoints
(GET/POST contracts, GET/PATCH contracts/{id}, POST .../versions, GET profiles).
Values validated against pricing profiles (422 on mismatch); no DELETE.
- main.py registers router; OpenAPI re-exported; tests for CRUD/version/activate.
This commit is contained in:
@@ -695,6 +695,287 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/energy/profiles": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"api-energy-contracts"
|
||||
],
|
||||
"summary": "Get Profiles",
|
||||
"description": "List all available pricing profile structures.\n\nReturns the full profile structure for each supported contract kind\n(``manual`` and ``tibber``). The front-end uses this to dynamically\nrender the correct fields and labels for the contract creation/editing form.",
|
||||
"operationId": "get_profiles_api_energy_profiles_get",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProfilesResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/energy/contracts": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"api-energy-contracts"
|
||||
],
|
||||
"summary": "List Energy Contracts",
|
||||
"description": "List all energy contracts with their active status.\n\nReturns a flat list (no embedded version history); use\nGET /api/energy/contracts/{id} to fetch the full version history for a\nspecific contract.",
|
||||
"operationId": "list_energy_contracts_api_energy_contracts_get",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ContractListResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"tags": [
|
||||
"api-energy-contracts"
|
||||
],
|
||||
"summary": "Create Energy Contract",
|
||||
"description": "Create a new energy contract with an initial pricing version.\n\nThe ``values`` dict is validated against the YAML profile for the given\n``kind``; non-conforming values result in 422 Unprocessable Entity.\nThe new contract is created with ``active=False``; use\nPATCH /api/energy/contracts/{id} with ``active=true`` to activate it.",
|
||||
"operationId": "create_energy_contract_api_energy_contracts_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/ContractCreate"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ContractDetailResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/energy/contracts/{contract_id}": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"api-energy-contracts"
|
||||
],
|
||||
"summary": "Get Energy Contract",
|
||||
"description": "Return a single energy contract with its full version history.\n\nVersions are ordered by ``effective_from`` ascending so the caller can\neasily inspect the pricing timeline.",
|
||||
"operationId": "get_energy_contract_api_energy_contracts__contract_id__get",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "contract_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"title": "Contract Id"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ContractDetailResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"patch": {
|
||||
"tags": [
|
||||
"api-energy-contracts"
|
||||
],
|
||||
"summary": "Patch Energy Contract",
|
||||
"description": "Partially update a contract: rename or change activation status.\n\n- ``name``: updates the human-readable label.\n- ``active=true``: activates this contract (all others are deactivated).\n- ``active=false``: deactivates this contract (no effect on others).\n\nAt most one contract may be active at any time; the service layer enforces\nmutual exclusion.",
|
||||
"operationId": "patch_energy_contract_api_energy_contracts__contract_id__patch",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "contract_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"title": "Contract 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/ContractPatch"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ContractDetailResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/energy/contracts/{contract_id}/versions": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"api-energy-contracts"
|
||||
],
|
||||
"summary": "Add Contract Version",
|
||||
"description": "Add a new pricing version to an existing contract.\n\nThis is how price changes are recorded: the current open version is\nautomatically closed (its ``effective_to`` is set to ``body.effective_from``)\nand a new version is created starting at ``body.effective_from``.\n\nThe ``values`` dict must conform to the contract's pricing profile.\nNon-conforming values return 422. If ``effective_from`` is not strictly\nafter the previous version's ``effective_from``, 422 is returned without\nwriting any rows.\n\nHistorical versions are never modified; this endpoint is append-only.",
|
||||
"operationId": "add_contract_version_api_energy_contracts__contract_id__versions_post",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "contract_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"title": "Contract 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/VersionCreate"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ContractDetailResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/expose": {
|
||||
"get": {
|
||||
"tags": [
|
||||
@@ -1933,6 +2214,253 @@
|
||||
],
|
||||
"title": "ConfigUpdateResponse"
|
||||
},
|
||||
"ContractCreate": {
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"maxLength": 255,
|
||||
"minLength": 1,
|
||||
"title": "Name"
|
||||
},
|
||||
"kind": {
|
||||
"type": "string",
|
||||
"maxLength": 32,
|
||||
"minLength": 1,
|
||||
"title": "Kind"
|
||||
},
|
||||
"currency": {
|
||||
"type": "string",
|
||||
"maxLength": 8,
|
||||
"minLength": 1,
|
||||
"title": "Currency",
|
||||
"default": "EUR"
|
||||
},
|
||||
"values": {
|
||||
"additionalProperties": true,
|
||||
"type": "object",
|
||||
"title": "Values"
|
||||
},
|
||||
"effective_from": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Effective From",
|
||||
"description": "UTC datetime from which the first pricing version is effective. Defaults to the current UTC time when omitted."
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"name",
|
||||
"kind",
|
||||
"values"
|
||||
],
|
||||
"title": "ContractCreate",
|
||||
"description": "Request body for POST /api/energy/contracts.\n\n``effective_from`` defaults to the current UTC time if not provided,\ngiving the first version an open-ended start from \"now\".\n``kind`` is validated at the application layer against the profile registry;\nclients should send ``\"manual\"`` or ``\"tibber\"``."
|
||||
},
|
||||
"ContractDetailResponse": {
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"title": "Id"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"title": "Name"
|
||||
},
|
||||
"kind": {
|
||||
"type": "string",
|
||||
"title": "Kind"
|
||||
},
|
||||
"active": {
|
||||
"type": "boolean",
|
||||
"title": "Active"
|
||||
},
|
||||
"currency": {
|
||||
"type": "string",
|
||||
"title": "Currency"
|
||||
},
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"title": "Created At"
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"title": "Updated At"
|
||||
},
|
||||
"versions": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/ContractVersionResponse"
|
||||
},
|
||||
"type": "array",
|
||||
"title": "Versions"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"name",
|
||||
"kind",
|
||||
"active",
|
||||
"currency",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"versions"
|
||||
],
|
||||
"title": "ContractDetailResponse",
|
||||
"description": "Response schema for a single EnergyContract with full version history.\n\nReturned by GET /api/energy/contracts/{id} and by successful POST / PATCH\noperations where the caller needs to see all version data."
|
||||
},
|
||||
"ContractListResponse": {
|
||||
"properties": {
|
||||
"items": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/ContractResponse"
|
||||
},
|
||||
"type": "array",
|
||||
"title": "Items"
|
||||
},
|
||||
"total": {
|
||||
"type": "integer",
|
||||
"title": "Total"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"items",
|
||||
"total"
|
||||
],
|
||||
"title": "ContractListResponse",
|
||||
"description": "Response schema for GET /api/energy/contracts."
|
||||
},
|
||||
"ContractPatch": {
|
||||
"properties": {
|
||||
"name": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"maxLength": 255,
|
||||
"minLength": 1
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Name"
|
||||
},
|
||||
"active": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Active"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"title": "ContractPatch",
|
||||
"description": "Request body for PATCH /api/energy/contracts/{id}.\n\nAll fields are optional. Sending ``active=true`` activates this contract\n(deactivating all others); ``active=false`` deactivates it without affecting\nother contracts."
|
||||
},
|
||||
"ContractResponse": {
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"title": "Id"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"title": "Name"
|
||||
},
|
||||
"kind": {
|
||||
"type": "string",
|
||||
"title": "Kind"
|
||||
},
|
||||
"active": {
|
||||
"type": "boolean",
|
||||
"title": "Active"
|
||||
},
|
||||
"currency": {
|
||||
"type": "string",
|
||||
"title": "Currency"
|
||||
},
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"title": "Created At"
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"title": "Updated At"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"name",
|
||||
"kind",
|
||||
"active",
|
||||
"currency",
|
||||
"created_at",
|
||||
"updated_at"
|
||||
],
|
||||
"title": "ContractResponse",
|
||||
"description": "Response schema for a single EnergyContract (without embedded versions).\n\nUsed for list responses where embedding all versions would be expensive."
|
||||
},
|
||||
"ContractVersionResponse": {
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"title": "Id"
|
||||
},
|
||||
"effective_from": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"title": "Effective From"
|
||||
},
|
||||
"effective_to": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Effective To"
|
||||
},
|
||||
"values": {
|
||||
"additionalProperties": true,
|
||||
"type": "object",
|
||||
"title": "Values"
|
||||
},
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"title": "Created At"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"effective_from",
|
||||
"effective_to",
|
||||
"values",
|
||||
"created_at"
|
||||
],
|
||||
"title": "ContractVersionResponse",
|
||||
"description": "Response schema for a single EnergyContractVersion row."
|
||||
},
|
||||
"DeviceInfoSchema": {
|
||||
"properties": {
|
||||
"identifiers": {
|
||||
@@ -2868,6 +3396,24 @@
|
||||
"title": "ProfileSummary",
|
||||
"description": "One entry in the GET /api/modbus/profiles response."
|
||||
},
|
||||
"ProfilesResponse": {
|
||||
"properties": {
|
||||
"profiles": {
|
||||
"items": {
|
||||
"additionalProperties": true,
|
||||
"type": "object"
|
||||
},
|
||||
"type": "array",
|
||||
"title": "Profiles"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"profiles"
|
||||
],
|
||||
"title": "ProfilesResponse",
|
||||
"description": "Response schema for GET /api/energy/profiles.\n\n``profiles`` is a list of raw profile dicts as produced by\n``list_profiles()`` (Pydantic model dumps). Each entry contains at minimum\n``kind`` and ``label``; the full nested structure allows the front-end to\nrender a type-appropriate form for each pricing profile."
|
||||
},
|
||||
"PublicIPCheckResponse": {
|
||||
"properties": {
|
||||
"status": {
|
||||
@@ -3257,6 +3803,27 @@
|
||||
"type"
|
||||
],
|
||||
"title": "ValidationError"
|
||||
},
|
||||
"VersionCreate": {
|
||||
"properties": {
|
||||
"effective_from": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"title": "Effective From"
|
||||
},
|
||||
"values": {
|
||||
"additionalProperties": true,
|
||||
"type": "object",
|
||||
"title": "Values"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"effective_from",
|
||||
"values"
|
||||
],
|
||||
"title": "VersionCreate",
|
||||
"description": "Request body for POST /api/energy/contracts/{id}/versions."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -516,6 +516,235 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/HTTPValidationError'
|
||||
/api/energy/profiles:
|
||||
get:
|
||||
tags:
|
||||
- api-energy-contracts
|
||||
summary: Get Profiles
|
||||
description: 'List all available pricing profile structures.
|
||||
|
||||
|
||||
Returns the full profile structure for each supported contract kind
|
||||
|
||||
(``manual`` and ``tibber``). The front-end uses this to dynamically
|
||||
|
||||
render the correct fields and labels for the contract creation/editing form.'
|
||||
operationId: get_profiles_api_energy_profiles_get
|
||||
responses:
|
||||
'200':
|
||||
description: Successful Response
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ProfilesResponse'
|
||||
/api/energy/contracts:
|
||||
get:
|
||||
tags:
|
||||
- api-energy-contracts
|
||||
summary: List Energy Contracts
|
||||
description: 'List all energy contracts with their active status.
|
||||
|
||||
|
||||
Returns a flat list (no embedded version history); use
|
||||
|
||||
GET /api/energy/contracts/{id} to fetch the full version history for a
|
||||
|
||||
specific contract.'
|
||||
operationId: list_energy_contracts_api_energy_contracts_get
|
||||
responses:
|
||||
'200':
|
||||
description: Successful Response
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ContractListResponse'
|
||||
post:
|
||||
tags:
|
||||
- api-energy-contracts
|
||||
summary: Create Energy Contract
|
||||
description: 'Create a new energy contract with an initial pricing version.
|
||||
|
||||
|
||||
The ``values`` dict is validated against the YAML profile for the given
|
||||
|
||||
``kind``; non-conforming values result in 422 Unprocessable Entity.
|
||||
|
||||
The new contract is created with ``active=False``; use
|
||||
|
||||
PATCH /api/energy/contracts/{id} with ``active=true`` to activate it.'
|
||||
operationId: create_energy_contract_api_energy_contracts_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/ContractCreate'
|
||||
responses:
|
||||
'201':
|
||||
description: Successful Response
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ContractDetailResponse'
|
||||
'422':
|
||||
description: Validation Error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/HTTPValidationError'
|
||||
/api/energy/contracts/{contract_id}:
|
||||
get:
|
||||
tags:
|
||||
- api-energy-contracts
|
||||
summary: Get Energy Contract
|
||||
description: 'Return a single energy contract with its full version history.
|
||||
|
||||
|
||||
Versions are ordered by ``effective_from`` ascending so the caller can
|
||||
|
||||
easily inspect the pricing timeline.'
|
||||
operationId: get_energy_contract_api_energy_contracts__contract_id__get
|
||||
parameters:
|
||||
- name: contract_id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
title: Contract Id
|
||||
responses:
|
||||
'200':
|
||||
description: Successful Response
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ContractDetailResponse'
|
||||
'422':
|
||||
description: Validation Error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/HTTPValidationError'
|
||||
patch:
|
||||
tags:
|
||||
- api-energy-contracts
|
||||
summary: Patch Energy Contract
|
||||
description: 'Partially update a contract: rename or change activation status.
|
||||
|
||||
|
||||
- ``name``: updates the human-readable label.
|
||||
|
||||
- ``active=true``: activates this contract (all others are deactivated).
|
||||
|
||||
- ``active=false``: deactivates this contract (no effect on others).
|
||||
|
||||
|
||||
At most one contract may be active at any time; the service layer enforces
|
||||
|
||||
mutual exclusion.'
|
||||
operationId: patch_energy_contract_api_energy_contracts__contract_id__patch
|
||||
parameters:
|
||||
- name: contract_id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
title: Contract 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/ContractPatch'
|
||||
responses:
|
||||
'200':
|
||||
description: Successful Response
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ContractDetailResponse'
|
||||
'422':
|
||||
description: Validation Error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/HTTPValidationError'
|
||||
/api/energy/contracts/{contract_id}/versions:
|
||||
post:
|
||||
tags:
|
||||
- api-energy-contracts
|
||||
summary: Add Contract Version
|
||||
description: 'Add a new pricing version to an existing contract.
|
||||
|
||||
|
||||
This is how price changes are recorded: the current open version is
|
||||
|
||||
automatically closed (its ``effective_to`` is set to ``body.effective_from``)
|
||||
|
||||
and a new version is created starting at ``body.effective_from``.
|
||||
|
||||
|
||||
The ``values`` dict must conform to the contract''s pricing profile.
|
||||
|
||||
Non-conforming values return 422. If ``effective_from`` is not strictly
|
||||
|
||||
after the previous version''s ``effective_from``, 422 is returned without
|
||||
|
||||
writing any rows.
|
||||
|
||||
|
||||
Historical versions are never modified; this endpoint is append-only.'
|
||||
operationId: add_contract_version_api_energy_contracts__contract_id__versions_post
|
||||
parameters:
|
||||
- name: contract_id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
title: Contract 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/VersionCreate'
|
||||
responses:
|
||||
'201':
|
||||
description: Successful Response
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ContractDetailResponse'
|
||||
'422':
|
||||
description: Validation Error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/HTTPValidationError'
|
||||
/api/expose:
|
||||
get:
|
||||
tags:
|
||||
@@ -1464,6 +1693,212 @@ components:
|
||||
required:
|
||||
- sections
|
||||
title: ConfigUpdateResponse
|
||||
ContractCreate:
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
maxLength: 255
|
||||
minLength: 1
|
||||
title: Name
|
||||
kind:
|
||||
type: string
|
||||
maxLength: 32
|
||||
minLength: 1
|
||||
title: Kind
|
||||
currency:
|
||||
type: string
|
||||
maxLength: 8
|
||||
minLength: 1
|
||||
title: Currency
|
||||
default: EUR
|
||||
values:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
title: Values
|
||||
effective_from:
|
||||
anyOf:
|
||||
- type: string
|
||||
format: date-time
|
||||
- type: 'null'
|
||||
title: Effective From
|
||||
description: UTC datetime from which the first pricing version is effective.
|
||||
Defaults to the current UTC time when omitted.
|
||||
type: object
|
||||
required:
|
||||
- name
|
||||
- kind
|
||||
- values
|
||||
title: ContractCreate
|
||||
description: 'Request body for POST /api/energy/contracts.
|
||||
|
||||
|
||||
``effective_from`` defaults to the current UTC time if not provided,
|
||||
|
||||
giving the first version an open-ended start from "now".
|
||||
|
||||
``kind`` is validated at the application layer against the profile registry;
|
||||
|
||||
clients should send ``"manual"`` or ``"tibber"``.'
|
||||
ContractDetailResponse:
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
title: Id
|
||||
name:
|
||||
type: string
|
||||
title: Name
|
||||
kind:
|
||||
type: string
|
||||
title: Kind
|
||||
active:
|
||||
type: boolean
|
||||
title: Active
|
||||
currency:
|
||||
type: string
|
||||
title: Currency
|
||||
created_at:
|
||||
type: string
|
||||
format: date-time
|
||||
title: Created At
|
||||
updated_at:
|
||||
type: string
|
||||
format: date-time
|
||||
title: Updated At
|
||||
versions:
|
||||
items:
|
||||
$ref: '#/components/schemas/ContractVersionResponse'
|
||||
type: array
|
||||
title: Versions
|
||||
type: object
|
||||
required:
|
||||
- id
|
||||
- name
|
||||
- kind
|
||||
- active
|
||||
- currency
|
||||
- created_at
|
||||
- updated_at
|
||||
- versions
|
||||
title: ContractDetailResponse
|
||||
description: 'Response schema for a single EnergyContract with full version
|
||||
history.
|
||||
|
||||
|
||||
Returned by GET /api/energy/contracts/{id} and by successful POST / PATCH
|
||||
|
||||
operations where the caller needs to see all version data.'
|
||||
ContractListResponse:
|
||||
properties:
|
||||
items:
|
||||
items:
|
||||
$ref: '#/components/schemas/ContractResponse'
|
||||
type: array
|
||||
title: Items
|
||||
total:
|
||||
type: integer
|
||||
title: Total
|
||||
type: object
|
||||
required:
|
||||
- items
|
||||
- total
|
||||
title: ContractListResponse
|
||||
description: Response schema for GET /api/energy/contracts.
|
||||
ContractPatch:
|
||||
properties:
|
||||
name:
|
||||
anyOf:
|
||||
- type: string
|
||||
maxLength: 255
|
||||
minLength: 1
|
||||
- type: 'null'
|
||||
title: Name
|
||||
active:
|
||||
anyOf:
|
||||
- type: boolean
|
||||
- type: 'null'
|
||||
title: Active
|
||||
type: object
|
||||
title: ContractPatch
|
||||
description: 'Request body for PATCH /api/energy/contracts/{id}.
|
||||
|
||||
|
||||
All fields are optional. Sending ``active=true`` activates this contract
|
||||
|
||||
(deactivating all others); ``active=false`` deactivates it without affecting
|
||||
|
||||
other contracts.'
|
||||
ContractResponse:
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
title: Id
|
||||
name:
|
||||
type: string
|
||||
title: Name
|
||||
kind:
|
||||
type: string
|
||||
title: Kind
|
||||
active:
|
||||
type: boolean
|
||||
title: Active
|
||||
currency:
|
||||
type: string
|
||||
title: Currency
|
||||
created_at:
|
||||
type: string
|
||||
format: date-time
|
||||
title: Created At
|
||||
updated_at:
|
||||
type: string
|
||||
format: date-time
|
||||
title: Updated At
|
||||
type: object
|
||||
required:
|
||||
- id
|
||||
- name
|
||||
- kind
|
||||
- active
|
||||
- currency
|
||||
- created_at
|
||||
- updated_at
|
||||
title: ContractResponse
|
||||
description: 'Response schema for a single EnergyContract (without embedded
|
||||
versions).
|
||||
|
||||
|
||||
Used for list responses where embedding all versions would be expensive.'
|
||||
ContractVersionResponse:
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
title: Id
|
||||
effective_from:
|
||||
type: string
|
||||
format: date-time
|
||||
title: Effective From
|
||||
effective_to:
|
||||
anyOf:
|
||||
- type: string
|
||||
format: date-time
|
||||
- type: 'null'
|
||||
title: Effective To
|
||||
values:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
title: Values
|
||||
created_at:
|
||||
type: string
|
||||
format: date-time
|
||||
title: Created At
|
||||
type: object
|
||||
required:
|
||||
- id
|
||||
- effective_from
|
||||
- effective_to
|
||||
- values
|
||||
- created_at
|
||||
title: ContractVersionResponse
|
||||
description: Response schema for a single EnergyContractVersion row.
|
||||
DeviceInfoSchema:
|
||||
properties:
|
||||
identifiers:
|
||||
@@ -2113,6 +2548,28 @@ components:
|
||||
- description
|
||||
title: ProfileSummary
|
||||
description: One entry in the GET /api/modbus/profiles response.
|
||||
ProfilesResponse:
|
||||
properties:
|
||||
profiles:
|
||||
items:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
type: array
|
||||
title: Profiles
|
||||
type: object
|
||||
required:
|
||||
- profiles
|
||||
title: ProfilesResponse
|
||||
description: 'Response schema for GET /api/energy/profiles.
|
||||
|
||||
|
||||
``profiles`` is a list of raw profile dicts as produced by
|
||||
|
||||
``list_profiles()`` (Pydantic model dumps). Each entry contains at minimum
|
||||
|
||||
``kind`` and ``label``; the full nested structure allows the front-end to
|
||||
|
||||
render a type-appropriate form for each pricing profile.'
|
||||
PublicIPCheckResponse:
|
||||
properties:
|
||||
status:
|
||||
@@ -2384,3 +2841,19 @@ components:
|
||||
- msg
|
||||
- type
|
||||
title: ValidationError
|
||||
VersionCreate:
|
||||
properties:
|
||||
effective_from:
|
||||
type: string
|
||||
format: date-time
|
||||
title: Effective From
|
||||
values:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
title: Values
|
||||
type: object
|
||||
required:
|
||||
- effective_from
|
||||
- values
|
||||
title: VersionCreate
|
||||
description: Request body for POST /api/energy/contracts/{id}/versions.
|
||||
|
||||
Reference in New Issue
Block a user