Skip to content

Treatment Plan — events

Per-module slice of docs/events-catalog.md (auto-generated). Update both files when adding or removing events.

Published

EventWhenPayload
treatment_plan.createdPlan createdConsumed by patient_timeline.
treatment_plan.status_changedStatus transitionCurrently no subscribers.
treatment_plan.confirmeddraft → pendingSnapshot payload (items, totals, patient). Subscriber: patient_timeline.
treatment_plan.closedany → closedIncludes closure_reason. Subscriber: patient_timeline.
treatment_plan.reactivatedclosed → draftSubscriber: patient_timeline.
treatment_plan.treatment_addedA PlannedTreatmentItem is added to a plan via POST /treatment-plans/{id}/items.plan_id, item_id, treatment_id, clinic_id, patient_id, budget_id (nullable), catalog_item_id (nullable), tooth_number (nullable), surfaces (nullable), unit_price (nullable, decimal-as-string), assigned_professional_id (nullable, snapshot of the doctor responsible for this line).
treatment_plan.treatment_removedItem removedIncludes budget_id. Subscriber: budget.
treatment_plan.treatment_completedItem finalized (all sessions terminal, ≥1 completed)Audit/recall path only — carries no price; earned-ledger generation moved to item_session_completed with the multi-session feature. Subscribers: patient_timeline, recalls.
treatment_plan.item_session_completedOne session of a plan item marked done (single-session items publish it once on completion)plan_id, item_id, session_id, sequence, label, amount, treatment_id, patient_id, completed_by, occurred_at. Consumed by payments (earned row, idempotent on (treatment_id, session_id)).
treatment_plan.budget_sync_requestedManual resyncSnapshot payload includes full items[]. Subscriber: budget.
treatment_plan.item_completed_without_noteCompletion checkConsumed by patient_timeline.

No double booking. When the last session finalizes the item, the service calls TreatmentService.perform(publish_price=False) so the resulting odontogram.treatment.performed carries unit_price: null — the sessions already booked the money in the payments earned ledger.

Subscribed

EventHandlerEffect
appointment.completedevents.py::on_appointment_completedMark planned items as performed if linked.
budget.acceptedevents.py::on_budget_acceptedpending → active; also closed(rejected_by_patient) → active when the patient accepts a resent version (issue #162). Idempotent.
budget.rejectedevents.py::on_budget_rejectedpending → closed (closure_reason=rejected_by_patient).
budget.renegotiatedevents.py::on_budget_renegotiatedpending → draft via reopen_from_budget (never writes the budget row — the publisher's open transaction holds it locked).
budget.cancelledevents.py::on_budget_cancelledpending → draft via reopen_from_budget (issue #162). No-op without plan_id (standalone budget).
budget.supersededevents.py::on_budget_supersededRepoint plan.budget_id to the resent version — only while the plan still points at the superseded budget (idempotent). Status untouched.
odontogram.treatment.performedevents.py::on_treatment_performedMark the matching pending item completed and cancel its pending sessions (no session events) — the performed event already carried the full price to payments; a later session completion would book the same money twice.

Adding a new event

  1. Add the constant to backend/app/core/events/types.py (EventType).
  2. Publish from a service method, after the DB commit succeeds.
  3. Add the row to the table(s) above.
  4. Run python backend/scripts/generate_catalogs.py to refresh the global catalog.