M8-T14: add generic meter cost periods
This commit is contained in:
@@ -1251,8 +1251,8 @@ def test_contract_scope_migration_preserves_historical_contract_audit(tmp_path:
|
||||
{"now": now, "pricing": pricing, "version_id": version_id},
|
||||
)
|
||||
|
||||
command.upgrade(cfg, "head")
|
||||
command.upgrade(cfg, "head")
|
||||
command.upgrade(cfg, "20260822_18_contract_scopes")
|
||||
command.upgrade(cfg, "20260822_18_contract_scopes")
|
||||
with engine.connect() as connection:
|
||||
assert connection.execute(text("SELECT version_num FROM alembic_version")).scalar_one() == (
|
||||
"20260822_18_contract_scopes"
|
||||
@@ -1359,7 +1359,7 @@ def test_contract_scope_migration_audit_failure_restores_revision_17(tmp_path: P
|
||||
engine.dispose()
|
||||
|
||||
del cfg.attributes["m8_t12_post_ddl_audit_failure"]
|
||||
command.upgrade(cfg, "head")
|
||||
command.upgrade(cfg, "20260822_18_contract_scopes")
|
||||
engine = _engine_with_fk(db_url)
|
||||
with engine.connect() as connection:
|
||||
assert connection.execute(text("SELECT version_num FROM alembic_version")).scalar_one() == (
|
||||
|
||||
@@ -0,0 +1,465 @@
|
||||
"""Migration and ORM contracts for the commodity-scoped meter cost ledger."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from decimal import Decimal
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from alembic import command
|
||||
from alembic.config import Config
|
||||
from sqlalchemy import create_engine, event as sa_event, insert, inspect, select, text
|
||||
from sqlalchemy.exc import IntegrityError, StatementError
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.models.energy import MeterCostPeriod
|
||||
from scripts.app_db_adopt import APP_BASELINE_REVISION
|
||||
|
||||
|
||||
REVISION_18 = "20260822_18_contract_scopes"
|
||||
REVISION_19 = "20260822_19_meter_cost_periods"
|
||||
UTC = timezone.utc
|
||||
|
||||
|
||||
def _config(database_url: str) -> Config:
|
||||
config = Config("alembic_app.ini")
|
||||
config.set_main_option("sqlalchemy.url", database_url)
|
||||
return config
|
||||
|
||||
|
||||
def _fk_engine(database_url: str):
|
||||
engine = create_engine(database_url)
|
||||
|
||||
@sa_event.listens_for(engine, "connect")
|
||||
def _enable_foreign_keys(connection, _record) -> None:
|
||||
connection.execute("PRAGMA foreign_keys = ON")
|
||||
|
||||
return engine
|
||||
|
||||
|
||||
def _period(**overrides) -> MeterCostPeriod:
|
||||
start = datetime(2026, 8, 22, 12, tzinfo=UTC)
|
||||
values = {
|
||||
"commodity": "heating",
|
||||
"period_start": start,
|
||||
"period_end": start + timedelta(minutes=15),
|
||||
"quantity": Decimal("999999999.123456"),
|
||||
"cost": Decimal("999999.123456789"),
|
||||
"currency": "EUR",
|
||||
"cost_breakdown": {"heating": "0.012345678"},
|
||||
"pricing_snapshot": {"heating": "10.000000", "kind": "district_heating"},
|
||||
"quality": "valid",
|
||||
"degraded": True,
|
||||
"degraded_reason": "test_fixture_without_audit_links",
|
||||
"created_at": start,
|
||||
"updated_at": start,
|
||||
}
|
||||
values.update(overrides)
|
||||
return MeterCostPeriod(**values)
|
||||
|
||||
|
||||
def test_meter_cost_period_empty_db_upgrade_shape_and_baseline(tmp_path: Path) -> None:
|
||||
database_url = f"sqlite:///{tmp_path / 'empty.db'}"
|
||||
config = _config(database_url)
|
||||
command.upgrade(config, "head")
|
||||
command.upgrade(config, "head")
|
||||
|
||||
engine = create_engine(database_url)
|
||||
inspector = inspect(engine)
|
||||
columns = {column["name"]: column for column in inspector.get_columns("meter_cost_period")}
|
||||
assert APP_BASELINE_REVISION == REVISION_19
|
||||
assert {"commodity", "period_start", "period_end", "quantity", "cost"} <= columns.keys()
|
||||
assert columns["meter_id"]["nullable"]
|
||||
assert columns["source_binding_id"]["nullable"]
|
||||
assert columns["contract_version_id"]["nullable"]
|
||||
assert {index["name"] for index in inspector.get_indexes("meter_cost_period")} >= {
|
||||
"ix_meter_cost_period_commodity_start",
|
||||
"ix_meter_cost_period_source_binding_id",
|
||||
}
|
||||
assert ("commodity", "period_start") in {
|
||||
tuple(constraint["column_names"])
|
||||
for constraint in inspector.get_unique_constraints("meter_cost_period")
|
||||
}
|
||||
foreign_keys = {
|
||||
foreign_key["constrained_columns"][0]: foreign_key
|
||||
for foreign_key in inspector.get_foreign_keys("meter_cost_period")
|
||||
}
|
||||
for column, table in {
|
||||
"meter_id": "meter",
|
||||
"source_binding_id": "meter_source_binding",
|
||||
"contract_version_id": "energy_contract_version",
|
||||
}.items():
|
||||
assert foreign_keys[column]["referred_table"] == table
|
||||
assert foreign_keys[column]["options"]["ondelete"] == "RESTRICT"
|
||||
engine.dispose()
|
||||
|
||||
|
||||
def test_meter_cost_period_revision_18_upgrade_preserves_electricity_rows(tmp_path: Path) -> None:
|
||||
database_url = f"sqlite:///{tmp_path / 'revision18.db'}"
|
||||
config = _config(database_url)
|
||||
command.upgrade(config, REVISION_18)
|
||||
engine = create_engine(database_url)
|
||||
stamp = datetime(2026, 8, 22, 12)
|
||||
with engine.begin() as connection:
|
||||
connection.execute(
|
||||
text(
|
||||
"INSERT INTO energy_cost_period "
|
||||
"(period_start, d1_kwh, d2_kwh, r1_kwh, r2_kwh, import_cost, "
|
||||
"export_revenue, net_cost, currency, pricing, contract_version_id, "
|
||||
"meter_id, source_binding_id, degraded, computed_at) "
|
||||
"VALUES (:stamp, 1, 2, 3, 4, 5, 6, 7, 'EUR', :pricing, NULL, NULL, NULL, 0, :stamp)"
|
||||
),
|
||||
{"stamp": stamp, "pricing": '{"historic":"unchanged"}'},
|
||||
)
|
||||
command.upgrade(config, "head")
|
||||
command.upgrade(config, "head")
|
||||
with engine.connect() as connection:
|
||||
assert connection.execute(text("SELECT version_num FROM alembic_version")).scalar_one() == REVISION_19
|
||||
assert connection.execute(text("SELECT COUNT(*) FROM energy_cost_period")).scalar_one() == 1
|
||||
assert connection.execute(text("SELECT pricing FROM energy_cost_period")).scalar_one() == (
|
||||
'{"historic":"unchanged"}'
|
||||
)
|
||||
assert connection.execute(text("SELECT COUNT(*) FROM meter_cost_period")).scalar_one() == 0
|
||||
command.downgrade(config, REVISION_18)
|
||||
assert "meter_cost_period" not in inspect(engine).get_table_names()
|
||||
engine.dispose()
|
||||
|
||||
|
||||
def test_meter_cost_period_decimal_json_and_degraded_round_trip(tmp_path: Path) -> None:
|
||||
database_url = f"sqlite:///{tmp_path / 'roundtrip.db'}"
|
||||
command.upgrade(_config(database_url), "head")
|
||||
engine = create_engine(database_url)
|
||||
with Session(engine) as session:
|
||||
normal = _period()
|
||||
degraded = _period(
|
||||
commodity="hot_water",
|
||||
meter_id=None,
|
||||
source_binding_id=None,
|
||||
contract_version_id=None,
|
||||
quality="unverifiable",
|
||||
degraded=True,
|
||||
degraded_reason="missing_binding",
|
||||
quantity=Decimal("0.000000"),
|
||||
cost=Decimal("0.000000000"),
|
||||
cost_breakdown={"hot_water": "0.000000000"},
|
||||
pricing_snapshot={"reason": "missing_binding"},
|
||||
)
|
||||
session.add_all((normal, degraded))
|
||||
session.commit()
|
||||
session.expire_all()
|
||||
stored = session.get(MeterCostPeriod, normal.id)
|
||||
assert stored is not None
|
||||
assert stored.quantity == Decimal("999999999.123456")
|
||||
assert stored.cost == Decimal("999999.123456789")
|
||||
assert stored.cost_breakdown == {"heating": "0.012345678"}
|
||||
assert all(not isinstance(value, float) for value in stored.pricing_snapshot.values())
|
||||
assert session.get(MeterCostPeriod, degraded.id).degraded_reason == "missing_binding"
|
||||
engine.dispose()
|
||||
|
||||
|
||||
def test_meter_cost_period_uses_decimal_text_and_normalises_json_snapshots(tmp_path: Path) -> None:
|
||||
database_url = f"sqlite:///{tmp_path / 'exact.db'}"
|
||||
command.upgrade(_config(database_url), "head")
|
||||
engine = create_engine(database_url)
|
||||
with Session(engine) as session:
|
||||
row = _period(
|
||||
cost_breakdown={"nested": [Decimal("2.000000000")]},
|
||||
pricing_snapshot={"rate": Decimal("2.000000")},
|
||||
)
|
||||
session.add(row)
|
||||
session.commit()
|
||||
session.expire_all()
|
||||
stored = session.get(MeterCostPeriod, row.id)
|
||||
assert stored is not None
|
||||
assert stored.cost_breakdown == {"nested": ["2.000000000"]}
|
||||
assert stored.pricing_snapshot == {"rate": "2.000000"}
|
||||
raw = session.execute(text("SELECT typeof(quantity), typeof(cost) FROM meter_cost_period")).one()
|
||||
assert raw == ("text", "text")
|
||||
with pytest.raises(ValueError, match="numeric JSON"):
|
||||
_period(cost_breakdown={"nested": [1]})
|
||||
with pytest.raises(ValueError, match="numeric JSON"):
|
||||
_period(pricing_snapshot={"nested": [1.25]})
|
||||
engine.dispose()
|
||||
|
||||
|
||||
def test_meter_cost_period_core_json_bind_normalises_decimal_and_rejects_numeric(tmp_path: Path) -> None:
|
||||
database_url = f"sqlite:///{tmp_path / 'core-json.db'}"
|
||||
command.upgrade(_config(database_url), "head")
|
||||
engine = create_engine(database_url)
|
||||
values = {
|
||||
"commodity": "heating",
|
||||
"period_start": datetime(2026, 8, 22, 12, tzinfo=UTC),
|
||||
"period_end": datetime(2026, 8, 22, 12, 15, tzinfo=UTC),
|
||||
"quantity": Decimal("0.000000"),
|
||||
"cost": Decimal("0.000000000"),
|
||||
"currency": "EUR",
|
||||
"cost_breakdown": {"nested": [Decimal("2.000000000")]},
|
||||
"pricing_snapshot": {"rate": Decimal("2.000000")},
|
||||
"quality": "valid",
|
||||
"degraded": True,
|
||||
"degraded_reason": "core fixture",
|
||||
"created_at": datetime(2026, 8, 22, 12, tzinfo=UTC),
|
||||
"updated_at": datetime(2026, 8, 22, 12, tzinfo=UTC),
|
||||
}
|
||||
with engine.begin() as connection:
|
||||
connection.execute(insert(MeterCostPeriod.__table__).values(values))
|
||||
with Session(engine) as session:
|
||||
stored = session.scalar(select(MeterCostPeriod))
|
||||
assert stored is not None
|
||||
assert stored.cost_breakdown == {"nested": ["2.000000000"]}
|
||||
assert stored.pricing_snapshot == {"rate": "2.000000"}
|
||||
for field, numeric_value in (("cost_breakdown", {"nested": [1]}), ("pricing_snapshot", {"rate": 1.25})):
|
||||
with engine.begin() as connection, pytest.raises(StatementError, match="numeric JSON"):
|
||||
connection.execute(
|
||||
insert(MeterCostPeriod.__table__).values(
|
||||
{**values, "commodity": f"invalid-{field}", field: numeric_value}
|
||||
)
|
||||
)
|
||||
engine.dispose()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("field", "value", "message"),
|
||||
[
|
||||
("quantity", Decimal("1000000000.000000"), "precision"),
|
||||
("quantity", Decimal("1.1234567"), "scale"),
|
||||
("cost", Decimal("1000000.000000000"), "precision"),
|
||||
("cost", Decimal("1.1234567899"), "scale"),
|
||||
],
|
||||
)
|
||||
def test_meter_cost_period_rejects_decimal_precision_and_scale_overflow(field, value, message) -> None:
|
||||
with pytest.raises(ValueError, match=message):
|
||||
_period(**{field: value})
|
||||
|
||||
|
||||
def test_meter_cost_period_accepts_signed_decimal_boundaries_without_float_bind(tmp_path: Path) -> None:
|
||||
database_url = f"sqlite:///{tmp_path / 'boundaries.db'}"
|
||||
command.upgrade(_config(database_url), "head")
|
||||
engine = create_engine(database_url)
|
||||
with Session(engine) as session:
|
||||
rows = (
|
||||
_period(quantity=Decimal("999999999.999999"), cost=Decimal("999999.999999999")),
|
||||
_period(
|
||||
commodity="hot_water",
|
||||
quantity=Decimal("-999999999.999999"),
|
||||
cost=Decimal("-999999.999999999"),
|
||||
),
|
||||
)
|
||||
session.add_all(rows)
|
||||
session.commit()
|
||||
session.expire_all()
|
||||
assert session.get(MeterCostPeriod, rows[0].id).quantity == Decimal("999999999.999999")
|
||||
assert session.get(MeterCostPeriod, rows[1].id).cost == Decimal("-999999.999999999")
|
||||
engine.dispose()
|
||||
|
||||
|
||||
def test_meter_cost_period_rejects_duplicate_period_invalid_interval_and_json_float(tmp_path: Path) -> None:
|
||||
database_url = f"sqlite:///{tmp_path / 'constraints.db'}"
|
||||
command.upgrade(_config(database_url), "head")
|
||||
engine = create_engine(database_url)
|
||||
with Session(engine) as session:
|
||||
first = _period()
|
||||
session.add(first)
|
||||
session.commit()
|
||||
session.add(_period(cost=Decimal("1.000000000")))
|
||||
with pytest.raises(IntegrityError):
|
||||
session.commit()
|
||||
session.rollback()
|
||||
session.add(_period(commodity="hot_water", period_end=datetime(2026, 8, 22, 12, tzinfo=UTC)))
|
||||
with pytest.raises(ValueError, match="after period_start"):
|
||||
session.flush()
|
||||
session.rollback()
|
||||
with pytest.raises(ValueError, match="timezone-aware"):
|
||||
_period(
|
||||
commodity="hot_water",
|
||||
period_start=datetime(2026, 8, 22, 12),
|
||||
period_end=datetime(2026, 8, 22, 12, 15, tzinfo=UTC),
|
||||
)
|
||||
with pytest.raises(ValueError, match="decimal strings"):
|
||||
_period(pricing_snapshot={"heating": 1.25})
|
||||
with pytest.raises(ValueError, match="normal meter cost periods"):
|
||||
session.add(_period(commodity="hot_water", degraded=False, degraded_reason=None))
|
||||
session.flush()
|
||||
session.rollback()
|
||||
engine.dispose()
|
||||
|
||||
|
||||
def test_meter_cost_period_uses_utc_instant_idempotency_and_db_constraints(tmp_path: Path) -> None:
|
||||
database_url = f"sqlite:///{tmp_path / 'utc.db'}"
|
||||
command.upgrade(_config(database_url), "head")
|
||||
engine = create_engine(database_url)
|
||||
utc_start = datetime(2026, 8, 22, 12, tzinfo=UTC)
|
||||
offset_start = datetime(2026, 8, 22, 14, tzinfo=timezone(timedelta(hours=2)))
|
||||
with Session(engine) as session:
|
||||
first = _period(period_start=utc_start, period_end=utc_start + timedelta(minutes=15))
|
||||
assert first.period_start == utc_start
|
||||
session.add(first)
|
||||
session.commit()
|
||||
session.add(
|
||||
_period(
|
||||
period_start=offset_start,
|
||||
period_end=offset_start + timedelta(minutes=15),
|
||||
cost=Decimal("1.000000000"),
|
||||
)
|
||||
)
|
||||
with pytest.raises(IntegrityError):
|
||||
session.commit()
|
||||
session.rollback()
|
||||
with engine.begin() as connection:
|
||||
values = {
|
||||
"start": "2026-08-23 00:00:00.000000",
|
||||
"quantity": "0.000000",
|
||||
"cost": "0.000000000",
|
||||
"now": "2026-08-23 00:00:00.000000",
|
||||
}
|
||||
for commodity, end in (("equal", values["start"]), ("reversed", "2026-08-22 23:59:59.000000")):
|
||||
with pytest.raises(IntegrityError):
|
||||
connection.execute(
|
||||
text(
|
||||
"INSERT INTO meter_cost_period "
|
||||
"(commodity, period_start, period_end, quantity, cost, currency, cost_breakdown, "
|
||||
"pricing_snapshot, quality, degraded, degraded_reason, created_at, updated_at) "
|
||||
"VALUES (:commodity, :start, :end, :quantity, :cost, 'EUR', '{}', '{}', 'valid', "
|
||||
"1, 'core test', :now, :now)"
|
||||
),
|
||||
{**values, "commodity": commodity, "end": end},
|
||||
)
|
||||
with pytest.raises(IntegrityError):
|
||||
connection.execute(
|
||||
text(
|
||||
"INSERT INTO meter_cost_period "
|
||||
"(commodity, period_start, period_end, quantity, cost, currency, cost_breakdown, "
|
||||
"pricing_snapshot, quality, degraded, degraded_reason, created_at, updated_at) "
|
||||
"VALUES ('missing-reason', :start, '2026-08-23 00:15:00.000000', :quantity, :cost, "
|
||||
"'EUR', '{}', '{}', 'valid', 1, NULL, :now, :now)"
|
||||
),
|
||||
values,
|
||||
)
|
||||
engine.dispose()
|
||||
|
||||
|
||||
def test_meter_cost_period_core_uses_utc_instant_idempotency_and_reloads_aware(tmp_path: Path) -> None:
|
||||
database_url = f"sqlite:///{tmp_path / 'core-utc.db'}"
|
||||
command.upgrade(_config(database_url), "head")
|
||||
engine = create_engine(database_url)
|
||||
utc_start = datetime(2026, 8, 22, 12, tzinfo=UTC)
|
||||
values = {
|
||||
"commodity": "heating",
|
||||
"period_start": utc_start,
|
||||
"period_end": utc_start + timedelta(minutes=15),
|
||||
"quantity": Decimal("0.000000"),
|
||||
"cost": Decimal("0.000000000"),
|
||||
"currency": "EUR",
|
||||
"cost_breakdown": {},
|
||||
"pricing_snapshot": {},
|
||||
"quality": "valid",
|
||||
"degraded": True,
|
||||
"degraded_reason": "core fixture",
|
||||
"created_at": utc_start,
|
||||
"updated_at": utc_start,
|
||||
}
|
||||
with engine.begin() as connection:
|
||||
row_id = connection.execute(insert(MeterCostPeriod.__table__).values(values)).inserted_primary_key[0]
|
||||
with engine.begin() as connection, pytest.raises(IntegrityError):
|
||||
connection.execute(
|
||||
insert(MeterCostPeriod.__table__).values(
|
||||
{
|
||||
**values,
|
||||
"period_start": datetime(2026, 8, 22, 14, tzinfo=timezone(timedelta(hours=2))),
|
||||
"period_end": datetime(2026, 8, 22, 14, 15, tzinfo=timezone(timedelta(hours=2))),
|
||||
}
|
||||
)
|
||||
)
|
||||
with engine.connect() as connection, pytest.raises(StatementError, match="timezone-aware"):
|
||||
connection.execute(
|
||||
insert(MeterCostPeriod.__table__).values(
|
||||
{**values, "commodity": "naive", "period_start": datetime(2026, 8, 23, 12)}
|
||||
)
|
||||
)
|
||||
with Session(engine) as session:
|
||||
stored = session.get(MeterCostPeriod, row_id)
|
||||
assert stored is not None
|
||||
assert stored.period_start == utc_start
|
||||
assert stored.period_start.tzinfo is not None
|
||||
assert stored.period_start.utcoffset() == timedelta(0)
|
||||
stored.quality = "unverifiable"
|
||||
session.commit()
|
||||
engine.dispose()
|
||||
|
||||
|
||||
def test_meter_cost_period_foreign_keys_restrict_deletion(tmp_path: Path) -> None:
|
||||
database_url = f"sqlite:///{tmp_path / 'foreign_keys.db'}"
|
||||
command.upgrade(_config(database_url), "head")
|
||||
engine = _fk_engine(database_url)
|
||||
timestamp = datetime(2026, 8, 22, 12)
|
||||
with engine.begin() as connection:
|
||||
meter_id = connection.execute(
|
||||
text(
|
||||
"INSERT INTO meter (uuid, label, commodity, started_at, ended_at, reason, note, created_at) "
|
||||
"VALUES ('meter-cost-test', 'Test', 'heating', :timestamp, NULL, 'initial', NULL, :timestamp)"
|
||||
),
|
||||
{"timestamp": timestamp},
|
||||
).lastrowid
|
||||
source_id = connection.execute(
|
||||
text(
|
||||
"INSERT INTO meter_source (uuid, name, kind, enabled, config, status, created_at, updated_at) "
|
||||
"VALUES ('source-cost-test', 'Test', 'warmtelink_serial', 1, '{}', 'online', :timestamp, :timestamp)"
|
||||
),
|
||||
{"timestamp": timestamp},
|
||||
).lastrowid
|
||||
channel_id = connection.execute(
|
||||
text(
|
||||
"INSERT INTO meter_source_channel (uuid, source_id, channel_key, label, unit, created_at, updated_at) "
|
||||
"VALUES ('channel-cost-test', :source_id, 'heating', 'Heating', 'GJ', :timestamp, :timestamp)"
|
||||
),
|
||||
{"source_id": source_id, "timestamp": timestamp},
|
||||
).lastrowid
|
||||
binding_id = connection.execute(
|
||||
text(
|
||||
"INSERT INTO meter_source_binding "
|
||||
"(uuid, meter_id, channel_id, started_at, ended_at, created_at, updated_at) "
|
||||
"VALUES ('binding-cost-test', :meter_id, :channel_id, :timestamp, NULL, :timestamp, :timestamp)"
|
||||
),
|
||||
{"meter_id": meter_id, "channel_id": channel_id, "timestamp": timestamp},
|
||||
).lastrowid
|
||||
contract_id = connection.execute(
|
||||
text(
|
||||
"INSERT INTO energy_contract (name, kind, scope, active, currency, created_at, updated_at) "
|
||||
"VALUES ('Test', 'district_heating', 'thermal', 1, 'EUR', :timestamp, :timestamp)"
|
||||
),
|
||||
{"timestamp": timestamp},
|
||||
).lastrowid
|
||||
version_id = connection.execute(
|
||||
text(
|
||||
"INSERT INTO energy_contract_version "
|
||||
"(contract_id, effective_from, effective_to, \"values\", created_at) "
|
||||
"VALUES (:contract_id, :timestamp, NULL, '{}', :timestamp)"
|
||||
),
|
||||
{"contract_id": contract_id, "timestamp": timestamp},
|
||||
).lastrowid
|
||||
connection.execute(
|
||||
text(
|
||||
"INSERT INTO meter_cost_period "
|
||||
"(commodity, period_start, period_end, meter_id, source_binding_id, contract_version_id, "
|
||||
"quantity, cost, currency, cost_breakdown, pricing_snapshot, quality, degraded, degraded_reason, "
|
||||
"created_at, updated_at) VALUES "
|
||||
"('heating', :timestamp, :period_end, :meter_id, :binding_id, :version_id, "
|
||||
"'0.001000', '0.010000000', 'EUR', '{}', '{}', 'valid', 0, NULL, :timestamp, :timestamp)"
|
||||
),
|
||||
{
|
||||
"timestamp": timestamp,
|
||||
"period_end": timestamp + timedelta(minutes=15),
|
||||
"meter_id": meter_id,
|
||||
"binding_id": binding_id,
|
||||
"version_id": version_id,
|
||||
},
|
||||
)
|
||||
for statement, values in (
|
||||
("DELETE FROM meter WHERE id = :id", {"id": meter_id}),
|
||||
("DELETE FROM meter_source_binding WHERE id = :id", {"id": binding_id}),
|
||||
("DELETE FROM energy_contract_version WHERE id = :id", {"id": version_id}),
|
||||
):
|
||||
with pytest.raises(IntegrityError):
|
||||
connection.execute(text(statement), values)
|
||||
engine.dispose()
|
||||
Reference in New Issue
Block a user