Public Access
feat: step_reminder — 1-hour pre-deadline nudge for non-voters
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -135,6 +135,23 @@ def weekly_run_emailed(db, family):
|
||||
return r
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def weekly_run_reminded(db, family):
|
||||
from app.models import WeeklyRun
|
||||
r = WeeklyRun(
|
||||
family_id=family.id,
|
||||
week_start_date=WEEK,
|
||||
status="running",
|
||||
scraped_at=datetime.now(timezone.utc),
|
||||
generated_at=datetime.now(timezone.utc),
|
||||
emailed_at=datetime.now(timezone.utc),
|
||||
reminded_at=datetime.now(timezone.utc),
|
||||
)
|
||||
db.add(r)
|
||||
db.flush()
|
||||
return r
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# alerts.py tests
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -299,6 +316,84 @@ def test_step_email_stale_banner(db, weekly_run_generated, meal_plan, pending_it
|
||||
assert "prices" in sent[0]["html"].lower() or "stale" in sent[0]["html"].lower()
|
||||
|
||||
|
||||
# ── step_reminder ──────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_step_reminder_idempotent(db, weekly_run_reminded):
|
||||
original_ts = weekly_run_reminded.reminded_at
|
||||
from app.services.orchestrator.steps import step_reminder
|
||||
step_reminder(weekly_run_reminded, db)
|
||||
assert weekly_run_reminded.reminded_at == original_ts
|
||||
|
||||
|
||||
def test_step_reminder_skips_when_not_emailed(db, weekly_run_generated, monkeypatch):
|
||||
sent = []
|
||||
|
||||
class FakeBackend:
|
||||
def send(self, **kwargs):
|
||||
sent.append(kwargs)
|
||||
|
||||
monkeypatch.setattr(
|
||||
"app.services.orchestrator.steps.get_email_backend",
|
||||
lambda: FakeBackend(),
|
||||
)
|
||||
from app.services.orchestrator.steps import step_reminder
|
||||
step_reminder(weekly_run_generated, db)
|
||||
# emailed_at is None on weekly_run_generated — should be a no-op
|
||||
assert len(sent) == 0
|
||||
assert weekly_run_generated.reminded_at is None
|
||||
|
||||
|
||||
def test_step_reminder_sends_to_non_voter(
|
||||
db, weekly_run_emailed, meal_plan, pending_item, member, monkeypatch
|
||||
):
|
||||
sent = []
|
||||
|
||||
class FakeBackend:
|
||||
def send(self, **kwargs):
|
||||
sent.append(kwargs)
|
||||
|
||||
monkeypatch.setattr(
|
||||
"app.services.orchestrator.steps.get_email_backend",
|
||||
lambda: FakeBackend(),
|
||||
)
|
||||
from app.services.orchestrator.steps import step_reminder
|
||||
step_reminder(weekly_run_emailed, db)
|
||||
assert weekly_run_emailed.reminded_at is not None
|
||||
assert len(sent) == 1
|
||||
assert sent[0]["to"] == "alice@example.com"
|
||||
assert "1 hour" in sent[0]["subject"].lower() or "closes" in sent[0]["subject"].lower()
|
||||
|
||||
|
||||
def test_step_reminder_skips_voter(
|
||||
db, weekly_run_emailed, meal_plan, pending_item, member, monkeypatch
|
||||
):
|
||||
from app.models import MealPlanVote
|
||||
vote = MealPlanVote(
|
||||
meal_plan_item_id=pending_item.id,
|
||||
family_member_id=member.id,
|
||||
vote=True,
|
||||
)
|
||||
db.add(vote)
|
||||
db.flush()
|
||||
|
||||
sent = []
|
||||
|
||||
class FakeBackend:
|
||||
def send(self, **kwargs):
|
||||
sent.append(kwargs)
|
||||
|
||||
monkeypatch.setattr(
|
||||
"app.services.orchestrator.steps.get_email_backend",
|
||||
lambda: FakeBackend(),
|
||||
)
|
||||
from app.services.orchestrator.steps import step_reminder
|
||||
step_reminder(weekly_run_emailed, db)
|
||||
# member already voted — no reminder
|
||||
assert len(sent) == 0
|
||||
assert weekly_run_emailed.reminded_at is not None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# step_deadline tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user