"""Pausing the lottery from code (B-78, DAT-69, AUD-11). A pause only stops the *next* round from opening (rounds/service.py:rounds_can_open); a round in progress still closes, draws and pays. Besides the operator's own switch there are pauses the system raises itself when something needs a human look — a transaction given up for dead that confirmed after all (`resurrected_tx`, B-78) and, in later findings, a deep reorg (`reorg`, B-85) and a payout whose winning bet left the chain (`payout_blocked`, B-82). """ from sqlalchemy.ext.asyncio import AsyncSession from app.audit.log import write_audit_log from app.rounds.config import get_round_config OPERATOR_REASON = "operator" AUTOMATIC_REASONS = ("reorg", "payout_blocked", "resurrected_tx") async def pause_automatically(session: AsyncSession, reason: str) -> None: """Pause the lottery for an automatic `reason` in the caller's transaction and write the `lottery_paused` row. An automatic reason is written even over a pause that is already active (DAT-69): the cause that needs checking must not be hidden behind an earlier one, and an operator pause is never what overrides it.""" if reason not in AUTOMATIC_REASONS: raise ValueError(f"not an automatic pause reason: {reason!r}") config = await get_round_config(session) config.paused = True config.paused_reason = reason await write_audit_log(session, "lottery_paused", {"reason": reason}) async def pause_by_operator(session: AsyncSession, note: str | None = None): """The operator's pause (`POST /admin/pause`, B-90's `START_PAUSED`): never overwrites an automatic reason already in force — that is the one that needs a look — and the audit row always says `operator`. Returns the config row; the caller commits.""" config = await get_round_config(session) if not (config.paused and config.paused_reason in AUTOMATIC_REASONS): config.paused_reason = OPERATOR_REASON config.paused = True payload = {"reason": OPERATOR_REASON} if note: payload["note"] = note await write_audit_log(session, "lottery_paused", payload) return config