feat: add direction field and outbound stage set to Chorus items
This commit is contained in:
parent
68d5cc91ab
commit
4dbda85ccd
7 changed files with 92 additions and 5 deletions
|
|
@ -15,6 +15,7 @@ def create_item(
|
|||
captured_at: datetime,
|
||||
discord_message_id: str | None,
|
||||
sender_id: str | None = None,
|
||||
direction: str = "inbound",
|
||||
) -> tuple[Item, bool]:
|
||||
if discord_message_id is not None:
|
||||
existing = session.scalar(
|
||||
|
|
@ -29,6 +30,7 @@ def create_item(
|
|||
captured_at=captured_at,
|
||||
discord_message_id=discord_message_id,
|
||||
sender_id=sender_id,
|
||||
direction=direction,
|
||||
stage="new",
|
||||
)
|
||||
session.add(item)
|
||||
|
|
@ -66,8 +68,11 @@ def update_item(session: Session, item_id: int, **fields) -> Item | None:
|
|||
|
||||
new_type = fields.get("type", item.type)
|
||||
new_stage = fields.get("stage", item.stage)
|
||||
if "stage" in fields and not is_valid_stage(new_type, new_stage):
|
||||
raise ValueError(f"'{new_stage}' is not a valid stage for type '{new_type}'")
|
||||
new_direction = fields.get("direction", item.direction)
|
||||
if "stage" in fields and not is_valid_stage(new_type, new_stage, new_direction):
|
||||
raise ValueError(
|
||||
f"'{new_stage}' is not a valid stage for type '{new_type}' direction '{new_direction}'"
|
||||
)
|
||||
|
||||
for key, value in fields.items():
|
||||
setattr(item, key, value)
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ class Item(Base):
|
|||
sender_id: Mapped[str | None] = mapped_column(String, nullable=True)
|
||||
type: Mapped[str | None] = mapped_column(String, nullable=True)
|
||||
stage: Mapped[str] = mapped_column(String, nullable=False, default="new")
|
||||
direction: Mapped[str] = mapped_column(String, nullable=False, default="inbound")
|
||||
notes: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
follow_up_date: Mapped[date | None] = mapped_column(Date, nullable=True)
|
||||
discord_message_id: Mapped[str | None] = mapped_column(String, unique=True, nullable=True)
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ class ItemCreate(BaseModel):
|
|||
captured_at: datetime
|
||||
discord_message_id: str | None = None
|
||||
sender_id: str | None = None
|
||||
direction: str = "inbound"
|
||||
|
||||
|
||||
class ItemUpdate(BaseModel):
|
||||
|
|
@ -28,6 +29,7 @@ class ItemOut(BaseModel):
|
|||
sender_id: str | None
|
||||
type: str | None
|
||||
stage: str
|
||||
direction: str
|
||||
notes: str | None
|
||||
follow_up_date: date | None
|
||||
discord_message_id: str | None
|
||||
|
|
|
|||
|
|
@ -1,8 +1,14 @@
|
|||
DONATION_STAGES = ["new", "replied", "pickup_scheduled", "picked_up", "logged_in_bookmark"]
|
||||
OTHER_STAGES = ["new", "done"]
|
||||
OUTBOUND_STAGES = [
|
||||
"new", "reviewed", "outreach_drafted", "outreach_sent",
|
||||
"replied", "pickup_scheduled", "picked_up", "logged_in_bookmark",
|
||||
]
|
||||
|
||||
|
||||
def valid_stages_for(item_type: str | None) -> list[str]:
|
||||
def valid_stages_for(item_type: str | None, direction: str = "inbound") -> list[str]:
|
||||
if direction == "outbound":
|
||||
return OUTBOUND_STAGES
|
||||
if item_type == "donation":
|
||||
return DONATION_STAGES
|
||||
if item_type == "other":
|
||||
|
|
@ -10,5 +16,5 @@ def valid_stages_for(item_type: str | None) -> list[str]:
|
|||
return ["new"]
|
||||
|
||||
|
||||
def is_valid_stage(item_type: str | None, stage: str) -> bool:
|
||||
return stage in valid_stages_for(item_type)
|
||||
def is_valid_stage(item_type: str | None, stage: str, direction: str = "inbound") -> bool:
|
||||
return stage in valid_stages_for(item_type, direction)
|
||||
|
|
|
|||
|
|
@ -61,3 +61,37 @@ def test_update_item_rejects_invalid_stage_for_type(db_session):
|
|||
|
||||
def test_update_item_returns_none_for_missing_id(db_session):
|
||||
assert crud.update_item(db_session, 9999, notes="x") is None
|
||||
|
||||
|
||||
def test_create_item_defaults_direction_to_inbound(db_session):
|
||||
item, _ = crud.create_item(
|
||||
db_session, modality="bh_email", raw_content="x", captured_at=_now(),
|
||||
discord_message_id="dir-1",
|
||||
)
|
||||
assert item.direction == "inbound"
|
||||
|
||||
|
||||
def test_create_item_accepts_outbound_direction(db_session):
|
||||
item, _ = crud.create_item(
|
||||
db_session, modality="lead_found", raw_content="found post", captured_at=_now(),
|
||||
discord_message_id="dir-2", direction="outbound",
|
||||
)
|
||||
assert item.direction == "outbound"
|
||||
|
||||
|
||||
def test_update_item_validates_stage_against_outbound_direction(db_session):
|
||||
item, _ = crud.create_item(
|
||||
db_session, modality="lead_found", raw_content="x", captured_at=_now(),
|
||||
discord_message_id="dir-3", direction="outbound",
|
||||
)
|
||||
updated = crud.update_item(db_session, item.id, stage="outreach_sent")
|
||||
assert updated.stage == "outreach_sent"
|
||||
|
||||
|
||||
def test_update_item_rejects_inbound_only_stage_for_outbound_item(db_session):
|
||||
item, _ = crud.create_item(
|
||||
db_session, modality="lead_found", raw_content="x", captured_at=_now(),
|
||||
discord_message_id="dir-4", direction="outbound",
|
||||
)
|
||||
with pytest.raises(ValueError):
|
||||
crud.update_item(db_session, item.id, stage="done")
|
||||
|
|
|
|||
|
|
@ -68,3 +68,21 @@ def test_patch_invalid_stage_returns_422(client):
|
|||
def test_get_missing_item_returns_404(client):
|
||||
resp = client.get("/items/999999")
|
||||
assert resp.status_code == 404
|
||||
|
||||
|
||||
def test_create_item_with_outbound_direction(client):
|
||||
resp = client.post("/items", json={
|
||||
"modality": "lead_found", "raw_content": "found on Nextdoor",
|
||||
"captured_at": "2026-07-13T07:00:00Z", "discord_message_id": "out-1",
|
||||
"direction": "outbound",
|
||||
})
|
||||
assert resp.status_code == 201
|
||||
assert resp.json()["direction"] == "outbound"
|
||||
|
||||
|
||||
def test_create_item_direction_defaults_to_inbound(client):
|
||||
resp = client.post("/items", json={
|
||||
"modality": "voice", "raw_content": "call",
|
||||
"captured_at": "2026-07-13T07:00:00Z", "discord_message_id": "out-2",
|
||||
})
|
||||
assert resp.json()["direction"] == "inbound"
|
||||
|
|
|
|||
|
|
@ -33,3 +33,24 @@ def test_is_valid_stage_false_for_mismatched_type():
|
|||
|
||||
def test_is_valid_stage_false_for_unknown_value():
|
||||
assert stages.is_valid_stage("donation", "archived") is False
|
||||
|
||||
|
||||
def test_outbound_stages_in_order():
|
||||
assert stages.OUTBOUND_STAGES == [
|
||||
"new", "reviewed", "outreach_drafted", "outreach_sent",
|
||||
"replied", "pickup_scheduled", "picked_up", "logged_in_bookmark",
|
||||
]
|
||||
|
||||
|
||||
def test_valid_stages_for_outbound_ignores_type():
|
||||
assert stages.valid_stages_for("donation", direction="outbound") == stages.OUTBOUND_STAGES
|
||||
assert stages.valid_stages_for(None, direction="outbound") == stages.OUTBOUND_STAGES
|
||||
|
||||
|
||||
def test_valid_stages_for_inbound_unchanged():
|
||||
assert stages.valid_stages_for("donation") == stages.DONATION_STAGES
|
||||
|
||||
|
||||
def test_is_valid_stage_outbound_accepts_outreach_stages():
|
||||
assert stages.is_valid_stage("donation", "outreach_sent", direction="outbound") is True
|
||||
assert stages.is_valid_stage("donation", "outreach_sent", direction="inbound") is False
|
||||
|
|
|
|||
Loading…
Reference in a new issue