dodil/erp-brick-activity
tests/test_rules.py
"""The activity rules, with no database — named for the CRM spec's stories (modules/crm)."""
from datetime import datetime, timezone
import pytest
from brick_activity.domain.activity import (CANCELLED, DONE, OPEN, Activity, ActivityIncomplete,
ActivityRuleBroken, Contact, normalise_email, task_board)
NOW = datetime(2026, 10, 16, 12, 0, tzinfo=timezone.utc)
def log(kind="CALL", **kw):
return Activity.log("A-1", kind, kw.pop("subject", "Rang Anna"), "tom@co.example", 4242, now=NOW, **kw)
def test_crm02_a_call_is_logged_in_one_step_and_happened_now_unless_told_otherwise():
a = log(note="wants a proposal by Friday")
assert (a.kind, a.direction, a.occurred_at) == ("CALL", "outbound", NOW.isoformat())
assert log(occurred_at="2026-10-15T09:00:00").occurred_at == "2026-10-15T09:00:00+00:00"
def test_something_that_happened_cannot_be_in_the_future_or_have_a_due_date():
with pytest.raises(ActivityRuleBroken, match="future"):
log(occurred_at="2026-10-17T09:00:00Z")
with pytest.raises(ActivityRuleBroken, match="log a TASK"):
log(due_on="2026-10-20")
def test_a_task_needs_a_due_date_or_it_is_a_note():
with pytest.raises(ActivityIncomplete, match="due"):
log("TASK")
t = log("TASK", subject="Send proposal", due_on="2026-10-20")
assert (t.status, t.due_on, t.occurred_at) == (OPEN, "2026-10-20", None)
def test_kinds_are_a_closed_list_and_a_subject_is_required():
with pytest.raises(ActivityIncomplete, match="one of NOTE"):
log("LUNCH")
with pytest.raises(ActivityIncomplete, match="subject"):
log(subject=" ")
def test_completing_a_task_keeps_what_it_said_and_is_idempotent():
t = log("TASK", subject="Send proposal", due_on="2026-10-20", note="pricing v2")
assert t.complete(NOW, "sent, she replied") and not t.complete(NOW)
assert (t.status, t.note) == (DONE, "pricing v2\n\nsent, she replied")
assert t.reopen() and t.status == OPEN and t.done_at is None
assert t.cancel() and t.status == CANCELLED
def test_a_call_is_not_a_task():
with pytest.raises(ActivityRuleBroken, match="record of what happened"):
log().complete(NOW)
def test_crm12_tasks_grouped_overdue_today_this_week():
t = lambda s, d: log("TASK", subject=s, due_on=d)
board = task_board([t("late", "2026-10-10"), t("now", "2026-10-16"), t("soon", "2026-10-22"),
t("later", "2026-11-30")], "2026-10-16")
assert {k: [a.subject for a in v] for k, v in board.items()} == {
"overdue": ["late"], "today": ["now"], "this_week": ["soon"]}
def test_a_done_task_is_not_on_the_board():
t = log("TASK", subject="x", due_on="2026-10-10")
t.complete(NOW)
assert task_board([t], "2026-10-16")["overdue"] == []
def test_emails_are_compared_lower_case_and_must_be_emails():
assert normalise_email(" Anna@Acme.COM ") == "anna@acme.com"
with pytest.raises(ActivityIncomplete):
normalise_email("anna at acme")
def test_a_contact_who_left_is_unsubscribed_and_kept():
c = Contact.new("C-1", 4242, "Anna Berg", "anna@acme.com", "tom@co.example")
c.subscribed = True
c.left_company()
assert (c.status, c.subscribed, c.full_name) == ("left", False, "Anna Berg")