feat: add multi-chat routing and bill-specific dashboard countdown
Some checks failed
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

View File

@@ -26,26 +26,26 @@ class FakeSaapa:
class FakeNotifier:
def __init__(self, *, fail: bool = False, fail_on_call: int | None = None) -> None:
self.messages: list[str] = []
self.messages: list[tuple[str, str]] = []
self.fail = fail
self.fail_on_call = fail_on_call
self.calls = 0
def send(self, text: str) -> None:
def send(self, chat_id: str, text: str) -> None:
self.calls += 1
if self.fail or self.calls == self.fail_on_call:
raise ExternalServiceError("Eitaa unavailable")
self.messages.append(text)
self.messages.append((chat_id, text))
def _service(
path: Path,
saapa: FakeSaapa,
notifier: FakeNotifier,
bill_ids: tuple[str, ...] = ("123",),
bill_chat_ids: Mapping[str, tuple[str, ...]] | None = None,
) -> OutageService:
return OutageService(
bill_ids=bill_ids,
bill_chat_ids=bill_chat_ids or {"123": ("chat-a",)},
lookahead_days=5,
saapa=saapa, # type: ignore[arg-type]
notifier=notifier,
@@ -65,7 +65,7 @@ def test_first_run_sends_upcoming_once(tmp_path: Path, now: datetime, outage: Ou
assert first.sent == 1
assert first.state_changed is True
assert "خاموشی جدید" in notifier.messages[0]
assert "خاموشی جدید" in notifier.messages[0][1]
assert second.sent == 0
assert second.state_changed is False
assert path.read_text(encoding="utf-8") == first_state
@@ -82,9 +82,9 @@ def test_changed_outage_sends_field_diff(tmp_path: Path, now: datetime, outage:
result = service.run(now)
assert result.sent == 1
assert "تغییر در برنامه" in notifier.messages[-1]
assert "09:00" in notifier.messages[-1]
assert "10:00" in notifier.messages[-1]
assert "تغییر در برنامه" in notifier.messages[-1][1]
assert "09:00" in notifier.messages[-1][1]
assert "10:00" in notifier.messages[-1][1]
def test_cancellation_requires_two_successful_omissions_and_can_reactivate(
@@ -103,12 +103,12 @@ def test_cancellation_requires_two_successful_omissions_and_can_reactivate(
assert first_missing.sent == 0
assert second_missing.sent == 1
assert "دو بررسی پیاپی" in notifier.messages[-1]
assert "دو بررسی پیاپی" in notifier.messages[-1][1]
saapa.snapshots["123"] = [outage]
restored = service.run(now)
assert restored.sent == 1
assert "بازگشت برنامه" in notifier.messages[-1]
assert "بازگشت برنامه" in notifier.messages[-1][1]
def test_failed_notification_does_not_advance_state(
@@ -130,13 +130,80 @@ def test_fetch_failure_for_one_bill_does_not_block_another(
) -> None:
saapa = FakeSaapa({"123": ExternalServiceError("SAAPA failed for 123"), "456": [outage]})
notifier = FakeNotifier()
service = _service(tmp_path / "state.json", saapa, notifier, ("123", "456"))
service = _service(
tmp_path / "state.json",
saapa,
notifier,
{"123": ("chat-a",), "456": ("chat-b",)},
)
result = service.run(now)
assert result.sent == 1
assert result.errors == ["SAAPA failed for 123"]
assert len(notifier.messages) == 1
assert notifier.messages[0][0] == "chat-b"
def test_each_bill_is_sent_to_its_configured_chat(
tmp_path: Path, now: datetime, outage: Outage
) -> None:
second_outage = replace(outage, outage_number="404992999002")
saapa = FakeSaapa({"123": [outage], "456": [second_outage]})
notifier = FakeNotifier()
service = _service(
tmp_path / "state.json",
saapa,
notifier,
{"123": ("chat-a",), "456": ("chat-b",)},
)
result = service.run(now)
assert result.sent == 2
assert [chat_id for chat_id, _ in notifier.messages] == ["chat-a", "chat-b"]
assert "شناسه قبض: 123" in notifier.messages[0][1]
assert "شناسه قبض: 456" in notifier.messages[1][1]
def test_one_bill_is_sent_to_all_its_configured_chats(
tmp_path: Path, now: datetime, outage: Outage
) -> None:
saapa = FakeSaapa({"123": [outage]})
notifier = FakeNotifier()
service = _service(
tmp_path / "state.json",
saapa,
notifier,
{"123": ("chat-a", "chat-b")},
)
result = service.run(now)
assert result.sent == 2
assert len(saapa.calls) == 1
assert [chat_id for chat_id, _ in notifier.messages] == ["chat-a", "chat-b"]
def test_state_advances_only_after_all_chats_accept_notification(
tmp_path: Path, now: datetime, outage: Outage
) -> None:
path = tmp_path / "state.json"
notifier = FakeNotifier(fail_on_call=2)
service = _service(
path,
FakeSaapa({"123": [outage]}),
notifier,
{"123": ("chat-a", "chat-b", "chat-c")},
)
result = service.run(now)
assert result.sent == 2
assert result.errors == ["Eitaa unavailable"]
assert [chat_id for chat_id, _ in notifier.messages] == ["chat-a", "chat-c"]
assert result.state_changed is False
assert not path.exists()
def test_partial_delivery_persists_only_successful_events(