M8-T12: scope energy contracts
This commit is contained in:
+160
-1
@@ -157,11 +157,15 @@ def test_energy_contract_columns(energy_db):
|
||||
inspector = inspect(energy_db)
|
||||
columns = {col["name"]: col for col in inspector.get_columns("energy_contract")}
|
||||
|
||||
required_non_nullable = {"id", "name", "kind", "active", "currency", "created_at", "updated_at"}
|
||||
required_non_nullable = {
|
||||
"id", "name", "kind", "scope", "active", "currency", "created_at", "updated_at"
|
||||
}
|
||||
for col_name in required_non_nullable:
|
||||
assert col_name in columns, f"Missing column: {col_name}"
|
||||
assert not columns[col_name]["nullable"], f"{col_name} should be NOT NULL"
|
||||
|
||||
assert any(index["name"] == "ix_energy_contract_scope" for index in inspector.get_indexes("energy_contract"))
|
||||
|
||||
|
||||
def test_energy_contract_version_columns(energy_db):
|
||||
"""energy_contract_version must have all required columns with correct nullability."""
|
||||
@@ -537,6 +541,7 @@ def test_energy_contract_insert_and_retrieve(energy_db):
|
||||
assert fetched is not None
|
||||
assert fetched.name == "My Manual Contract"
|
||||
assert fetched.kind == "manual"
|
||||
assert fetched.scope == "electricity"
|
||||
assert fetched.active is True
|
||||
assert fetched.currency == "EUR"
|
||||
|
||||
@@ -1208,3 +1213,157 @@ def test_migration_downgrade_removes_meter_table(tmp_path: Path):
|
||||
"meter_id must be removed from energy_cost_period after downgrade"
|
||||
)
|
||||
engine.dispose()
|
||||
|
||||
|
||||
def test_contract_scope_migration_preserves_historical_contract_audit(tmp_path: Path):
|
||||
"""A revision-17 fixture upgrades/downgrades without altering contract audit rows."""
|
||||
db_url = f"sqlite:///{tmp_path / 'contract_scope_history.db'}"
|
||||
cfg = _make_app_alembic_config(db_url)
|
||||
command.upgrade(cfg, "20260822_17_warmtelink_readings")
|
||||
engine = create_engine(db_url, connect_args={"check_same_thread": False})
|
||||
now = datetime.now(tz=timezone.utc).replace(tzinfo=None)
|
||||
values = '{"energy":{"buy":{"normal":0.4}}}'
|
||||
pricing = '{"historic":"unchanged"}'
|
||||
with engine.begin() as connection:
|
||||
contract_id = connection.execute(
|
||||
text(
|
||||
"INSERT INTO energy_contract (name, kind, active, currency, created_at, updated_at) "
|
||||
"VALUES ('Historic', 'manual', 1, 'EUR', :now, :now)"
|
||||
),
|
||||
{"now": now},
|
||||
).lastrowid
|
||||
version_id = connection.execute(
|
||||
text(
|
||||
"INSERT INTO energy_contract_version "
|
||||
"(contract_id, effective_from, effective_to, \"values\", created_at) "
|
||||
"VALUES (:contract_id, :now, NULL, :values, :now)"
|
||||
),
|
||||
{"contract_id": contract_id, "now": now, "values": values},
|
||||
).lastrowid
|
||||
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, degraded, computed_at, meter_id, "
|
||||
"source_binding_id) VALUES (:now, 1, 2, 0, 0, 3, 0, 3, 'EUR', :pricing, :version_id, "
|
||||
"0, :now, NULL, NULL)"
|
||||
),
|
||||
{"now": now, "pricing": pricing, "version_id": version_id},
|
||||
)
|
||||
|
||||
command.upgrade(cfg, "head")
|
||||
command.upgrade(cfg, "head")
|
||||
with engine.connect() as connection:
|
||||
assert connection.execute(text("SELECT version_num FROM alembic_version")).scalar_one() == (
|
||||
"20260822_18_contract_scopes"
|
||||
)
|
||||
assert connection.execute(text("SELECT scope FROM energy_contract")).scalar_one() == "electricity"
|
||||
assert connection.execute(text("SELECT COUNT(*) FROM energy_contract")).scalar_one() == 1
|
||||
assert connection.execute(text("SELECT COUNT(*) FROM energy_contract_version")).scalar_one() == 1
|
||||
assert connection.execute(text("SELECT COUNT(*) FROM energy_cost_period")).scalar_one() == 1
|
||||
assert connection.execute(text("SELECT \"values\" FROM energy_contract_version")).scalar_one() == values
|
||||
assert connection.execute(text("SELECT pricing FROM energy_cost_period")).scalar_one() == pricing
|
||||
assert connection.execute(
|
||||
text(
|
||||
"SELECT COUNT(*) FROM energy_contract_version v LEFT JOIN energy_contract c "
|
||||
"ON c.id = v.contract_id WHERE c.id IS NULL"
|
||||
)
|
||||
).scalar_one() == 0
|
||||
inspector = inspect(connection)
|
||||
assert any(item["name"] == "ix_energy_contract_scope" for item in inspector.get_indexes("energy_contract"))
|
||||
|
||||
command.downgrade(cfg, "20260822_17_warmtelink_readings")
|
||||
with engine.connect() as connection:
|
||||
assert "scope" not in {item["name"] for item in inspect(connection).get_columns("energy_contract")}
|
||||
assert connection.execute(text("SELECT COUNT(*) FROM energy_contract_version")).scalar_one() == 1
|
||||
assert connection.execute(text("SELECT COUNT(*) FROM energy_cost_period")).scalar_one() == 1
|
||||
engine.dispose()
|
||||
|
||||
|
||||
def test_contract_scope_migration_audit_failure_restores_revision_17(tmp_path: Path):
|
||||
"""A post-DDL audit failure leaves no SQLite batch-migration residue."""
|
||||
db_url = f"sqlite:///{tmp_path / 'contract_scope_audit_failure.db'}"
|
||||
cfg = _make_app_alembic_config(db_url)
|
||||
command.upgrade(cfg, "20260822_17_warmtelink_readings")
|
||||
engine = _engine_with_fk(db_url)
|
||||
now = datetime.now(tz=timezone.utc).replace(tzinfo=None)
|
||||
values = '{"energy":{"buy":{"normal":0.4}}}'
|
||||
pricing = '{"historic":"unchanged"}'
|
||||
with engine.begin() as connection:
|
||||
contract_id = connection.execute(
|
||||
text(
|
||||
"INSERT INTO energy_contract (name, kind, active, currency, created_at, updated_at) "
|
||||
"VALUES ('Historic', 'manual', 1, 'EUR', :now, :now)"
|
||||
),
|
||||
{"now": now},
|
||||
).lastrowid
|
||||
version_id = connection.execute(
|
||||
text(
|
||||
"INSERT INTO energy_contract_version "
|
||||
"(contract_id, effective_from, effective_to, \"values\", created_at) "
|
||||
"VALUES (:contract_id, :now, NULL, :values, :now)"
|
||||
),
|
||||
{"contract_id": contract_id, "now": now, "values": values},
|
||||
).lastrowid
|
||||
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, degraded, computed_at, meter_id, "
|
||||
"source_binding_id) VALUES (:now, 1, 2, 0, 0, 3, 0, 3, 'EUR', :pricing, :version_id, "
|
||||
"0, :now, NULL, NULL)"
|
||||
),
|
||||
{"now": now, "pricing": pricing, "version_id": version_id},
|
||||
)
|
||||
engine.dispose()
|
||||
|
||||
def _raise_after_ddl() -> None:
|
||||
raise RuntimeError("injected post-DDL audit failure")
|
||||
|
||||
cfg.attributes["m8_t12_post_ddl_audit_failure"] = _raise_after_ddl
|
||||
with pytest.raises(RuntimeError, match="injected post-DDL audit failure"):
|
||||
command.upgrade(cfg, "head")
|
||||
|
||||
engine = _engine_with_fk(db_url)
|
||||
with engine.connect() as connection:
|
||||
assert connection.execute(text("SELECT version_num FROM alembic_version")).scalar_one() == (
|
||||
"20260822_17_warmtelink_readings"
|
||||
)
|
||||
assert "scope" not in {item["name"] for item in inspect(connection).get_columns("energy_contract")}
|
||||
assert not any(
|
||||
item["name"] == "ix_energy_contract_scope"
|
||||
for item in inspect(connection).get_indexes("energy_contract")
|
||||
)
|
||||
assert connection.execute(text("SELECT COUNT(*) FROM energy_contract")).scalar_one() == 1
|
||||
assert connection.execute(text("SELECT COUNT(*) FROM energy_contract_version")).scalar_one() == 1
|
||||
assert connection.execute(text("SELECT COUNT(*) FROM energy_cost_period")).scalar_one() == 1
|
||||
assert connection.execute(text("SELECT \"values\" FROM energy_contract_version")).scalar_one() == values
|
||||
assert connection.execute(text("SELECT pricing FROM energy_cost_period")).scalar_one() == pricing
|
||||
assert connection.execute(
|
||||
text(
|
||||
"SELECT COUNT(*) FROM energy_contract_version v LEFT JOIN energy_contract c "
|
||||
"ON c.id = v.contract_id WHERE c.id IS NULL"
|
||||
)
|
||||
).scalar_one() == 0
|
||||
assert connection.execute(
|
||||
text(
|
||||
"SELECT COUNT(*) FROM energy_cost_period p LEFT JOIN energy_contract_version v "
|
||||
"ON v.id = p.contract_version_id "
|
||||
"WHERE p.contract_version_id IS NOT NULL AND v.id IS NULL"
|
||||
)
|
||||
).scalar_one() == 0
|
||||
assert connection.execute(text("PRAGMA foreign_key_check")).all() == []
|
||||
assert connection.execute(
|
||||
text("SELECT name FROM sqlite_master WHERE name LIKE '_alembic_tmp_%'")
|
||||
).all() == []
|
||||
engine.dispose()
|
||||
|
||||
del cfg.attributes["m8_t12_post_ddl_audit_failure"]
|
||||
command.upgrade(cfg, "head")
|
||||
engine = _engine_with_fk(db_url)
|
||||
with engine.connect() as connection:
|
||||
assert connection.execute(text("SELECT version_num FROM alembic_version")).scalar_one() == (
|
||||
"20260822_18_contract_scopes"
|
||||
)
|
||||
assert connection.execute(text("SELECT scope FROM energy_contract")).scalar_one() == "electricity"
|
||||
engine.dispose()
|
||||
|
||||
Reference in New Issue
Block a user