feat: add multi-chat routing and bill-specific dashboard countdown
Test blackout notifier / test (push) Failing after 4s
Hourly blackout check / check (push) Successful in 3s

This commit is contained in:
Meghdad
2026-07-22 02:14:45 +03:30
parent 6c3e146234
commit 42687ee651
12 changed files with 372 additions and 100 deletions
+30 -17
View File
@@ -1,6 +1,7 @@
from __future__ import annotations
import logging
from collections.abc import Mapping
from dataclasses import dataclass, field
from datetime import datetime, timedelta
from typing import Any
@@ -29,13 +30,13 @@ class OutageService:
def __init__(
self,
*,
bill_ids: tuple[str, ...],
bill_chat_ids: Mapping[str, tuple[str, ...]],
lookahead_days: int,
saapa: SaapaClient,
notifier: Notifier,
state_repository: StateRepository,
) -> None:
self._bill_ids = bill_ids
self._bill_chat_ids = dict(bill_chat_ids)
self._lookahead_days = lookahead_days
self._saapa = saapa
self._notifier = notifier
@@ -46,14 +47,14 @@ class OutageService:
result = RunResult()
from_date, to_date = jalali_query_window(now, self._lookahead_days)
for bill_id in self._bill_ids:
for bill_id, chat_ids in self._bill_chat_ids.items():
try:
outages = self._saapa.fetch(bill_id, from_date, to_date)
except ExternalServiceError as error:
result.errors.append(str(error))
LOGGER.error("%s", error)
continue
self._process_bill(state, bill_id, outages, now, result)
self._process_bill(state, bill_id, chat_ids, outages, now, result)
if self._prune_expired(state, now):
result.state_changed = True
@@ -66,6 +67,7 @@ class OutageService:
self,
state: dict[str, Any],
bill_id: str,
chat_ids: tuple[str, ...],
outages: list[Outage],
now: datetime,
result: RunResult,
@@ -82,20 +84,26 @@ class OutageService:
for event_id, outage in current.items():
record = records.get(event_id)
if record is None:
if self._notify(new_outage_message(bill_id, outage, now), result):
if self._notify(chat_ids, new_outage_message(bill_id, outage, now), result):
records[event_id] = _record(outage, now)
result.state_changed = True
continue
previous = Outage.from_dict(record["snapshot"])
if record["status"] == "cancelled":
if self._notify(reactivated_outage_message(bill_id, outage, now), result):
if self._notify(
chat_ids, reactivated_outage_message(bill_id, outage, now), result
):
records[event_id] = _record(outage, now)
result.state_changed = True
continue
if outage.fingerprint() != record["notified_hash"]:
if self._notify(updated_outage_message(bill_id, previous, outage, now), result):
if self._notify(
chat_ids,
updated_outage_message(bill_id, previous, outage, now),
result,
):
records[event_id] = _record(outage, now)
result.state_changed = True
continue
@@ -118,7 +126,9 @@ class OutageService:
if record["missing_checks"] == 0:
record["missing_checks"] = 1
result.state_changed = True
elif self._notify(cancelled_outage_message(bill_id, previous, now), result):
elif self._notify(
chat_ids, cancelled_outage_message(bill_id, previous, now), result
):
record["status"] = "cancelled"
record["missing_checks"] = 2
record["notified_at"] = now.isoformat()
@@ -127,15 +137,18 @@ class OutageService:
if records and bill_id not in bills:
bills[bill_id] = bill_state
def _notify(self, message: str, result: RunResult) -> bool:
try:
self._notifier.send(message)
except ExternalServiceError as error:
result.errors.append(str(error))
LOGGER.error("%s", error)
return False
result.sent += 1
return True
def _notify(self, chat_ids: tuple[str, ...], message: str, result: RunResult) -> bool:
delivered_to_all = True
for chat_id in chat_ids:
try:
self._notifier.send(chat_id, message)
except ExternalServiceError as error:
result.errors.append(str(error))
LOGGER.error("%s", error)
delivered_to_all = False
continue
result.sent += 1
return delivered_to_all
@staticmethod
def _prune_expired(state: dict[str, Any], now: datetime) -> bool: