Skip to content

Add escalation scaffolding on the backend (W4-05) - #111

Open
anwang132 wants to merge 1 commit into
mainfrom
93/AddEscalationScaffoldingBackend
Open

anwang132 wants to merge 1 commit into
mainfrom
93/AddEscalationScaffoldingBackend

Conversation

@anwang132

Copy link
Copy Markdown
Contributor

Some conversations will need to be escalated, following a protocol Peer2Peer is still working out with CAPS (TDD 4.3.2, open question 3). We can't build the final behavior yet, but we can build the frame around it: an escalation record, a separate encryption key for anything sensitive it holds, a responder action that creates one, and a coordinator view where every read is logged. Once the protocol is final, filling in the details should be a small change.

Changes:

  • Added the Escalation model (app/models/escalation.py) per TDD 4.5.2, with an EscalationStatus enum (open, handed_off, closed), a nullable protected_payload (BYTEA), and key_version next to it, plus its migration. I added a check constraint so the payload and key version are always set together (or both null), and an index on conversation_id, since a conversation can have several escalations. - Added app/services/escalations.py, the only place escalation payloads are encrypdecrypted, the same way services/messages.py handles message bodies. Payloads use KeyPurpose.ESCALATION, never the message key. For now the payload is the {"source"portal"} placeholder. - Added POST /api/portal/conversations/{id}/escalate. It uses require_on_shift (merged) plus a check that the caller is the assigned responder. Anyone else gets 404, not 403, so responders can't learn which conversations exist. It creates an open escalaudits escalation.create, and returns 201 with the escalation id. - Added GET /api/admin/escalations and GET /api/admin/escalations/{id} (require_coordinator). Both write an escalation.view audit entry on every read; the detail route includes the escalation id. The list is newest first. Only the detail route decrypts the payload, and it sends Cache-Control: no-store (the same as account creation) so the decrypted payload isn't cached.
  • Added EscalationDTO (id, conversation_label, status, created_by_display_name, created_at) and EscalationDetailDTO, which adds payload, so the list response leaves payload out entirely. I also added CreatedEscalationDTO for the create response. conversation_label is "Texter N", or "Texter" if the conversation has no label number.
  • Added tests/test_escalations.py: the assigned responder can escalate and anyone else gets 404, the stored payload isn't plaintext and can't be decrypted with the message key, coordinators can list and read escalations, every read adds an audit row, and responders get 403 on the admin routes. Extra cases: signed out (401), off shift (403 not_on_shift), unknown ids (404), no audit row when access is refused, and no payload in the list.
  • Left TODO(escalation-protocol) comments where the open decisions plug in: what the payload holds, who gets contacted, and how status changes. None of that is invented here.

Notes for reviewers:

  • Nothing stops a responder from escalating a conversation after it closes, as long as they were assigned to it. The issue doesn't say otherwise, so I didn't add a rule. Easy to add if the protocol needs it.
  • The retention-purge exemption for escalated conversations and the UI are out of scope (handled later, per the issue).
  • To test: cd backend && uv run pytest tests/test_escalations.py

Closes #93

@github-actions

github-actions Bot commented Nov 4, 2026

Copy link
Copy Markdown

Backend Test Results

353 tests  +14   353 ✅ +14   8s ⏱️ -1s
  1 suites ± 0     0 💤 ± 0 
  1 files   ± 0     0 ❌ ± 0 

Results for commit 446f09d. ± Comparison against base commit 534d88a.

@anwang132
anwang132 requested a review from calebyhan November 4, 2026 19:13

@calebyhan calebyhan left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code looks good and covers everything in #93, but this needs a rebase before it can merge:

  1. Migration conflict. #110 added 4bfad84b2ce1_add_texter_token_revocation, which also revises 38f046bc3fc3, so merging this as is leaves two Alembic heads and breaks alembic upgrade head / CI. Per the issue: delete 6d6bdb1ebc89_add_escalations.py, pull main, and run make migration m="add escalations" again (re-add the check constraint and the escalation_status enum drop in downgrade() if autogenerate doesn't pick them up).
  2. Conflict in backend/app/main.py. main now registers admin_resources and portal_ws. Keep all four include_router lines and all the imports.

Happy to approve once that's done.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add escalation scaffolding on the backend (W4-05)

2 participants