Skip to content

Appointment Response Profile

Canonical../StructureDefinition/nexus-appointment-response
Statusdraft · 1.26.0
BaseAppointmentResponse (constraint)
SourceFSH · JSON

A participant's answer to a booking: whether the person an appointment is for has accepted it, is still deciding, or has declined. Written by an integrating system outside the EMR and read wherever a booking's confirmation state is displayed, which is derived from participantStatus rather than from Appointment.status. An answer supersedes an earlier answer rather than amending it, and the latest one describes where the booking stands.

FSH

The authored source of truth: resources/StructureDefinition-nexus-appointment-response.fsh (edit on GitHub). One FSH file may define several related artifacts.

// StructureDefinition-nexus-appointment-response.fsh
// A participant's answer to a booking: FHIR R4 `AppointmentResponse`, the resource that carries
// whether the person a booking is for has confirmed it, is still deciding, or has refused.
//
// THIS TYPE WAS PREVIOUSLY DESCRIBED HERE AS OUT OF SCOPE, and that was wrong twice over. The
// scheduling material said confirmation was represented through `Appointment.status` transitions
// alone and that `AppointmentResponse` was tentative future scope. Neither holds: the type is
// written into this estate today by an integrating system outside the EMR, and the day sheet and
// the chart timeline both derive a booking's confirmation state from `participantStatus` rather
// than from `Appointment.status`. Reading only `Appointment.status` will not tell a consumer
// whether a booking is confirmed. Profiling it is what makes the two statements agree.
//
// WHAT PRODUCES ONE. An external integration POSTs it through the FHIR API; nothing inside the EMR
// authors one. That matters to a consumer because it sets what may be missing: a producer outside
// the estate supplies what it has, and the surfaces here were built around that.
//
// ⚠ AN ABSENT `participantStatus` IS FILLED IN, AND `tentative` IS THEREFORE AMBIGUOUS. The
// element is 1..1 in R4, so a resource omitting it would be rejected outright and the sender would
// see only a validation failure it could not act on. Rather than lose the answer, an omitted or
// blank status is stored as `tentative` -- the code claiming the least, since a sender that said
// nothing has told us nothing about acceptance, where `accepted` would assert a confirmation
// nobody made and `needs-action` would invite a workflow to chase one. So `tentative` means EITHER
// "the participant is still deciding" OR "the producer did not say", and a consumer cannot
// distinguish them from the resource alone. Do not read it as evidence that anyone was asked.
//
// A code the sender DID supply is never rewritten, including one outside the value set: that is a
// sender error the store reports, and silently replacing it would store an answer nobody gave.
//
// ⚠ THE BINDING IS R4'S OWN AND IS NOT NARROWED, deliberately. Three of the four codes drive a
// display state (`accepted`, `tentative`, `declined`); `needs-action` drives none, because "has
// been asked and has not answered" is what an unanswered booking already says. That is a
// CONSUMPTION fact, not a constraint: `needs-action` is legal here, it is produced, and a profile
// that bound it away would invalidate live data to buy a distinction no surface makes. A consumer
// should expect all four and treat an unrecognised one as no answer at all.
//
// AN ANSWER SUPERSEDES, IT DOES NOT AMEND. A participant may answer the same booking more than
// once -- a reminder is acknowledged and the answer later changes -- and every consumer here reads
// the LATEST answer per appointment and ignores the rest. There is no delete in this model: an
// answer is withdrawn by a newer answer, which is why this profile is exposed without one.
//
// NO DATE OF ITS OWN. R4 gives this resource no element for when the answer was given: `start` and
// `end` are a proposed new time for the appointment, not a timestamp on the response. Recency is
// therefore the server's own `meta.lastUpdated`, which is what "the latest answer" is decided by
// and the only ordering available -- R4 defines no date search parameter on this type either.

Alias: $NexusAppointmentResponse = ../StructureDefinition/nexus-appointment-response
Alias: $participationstatus = http://hl7.org/fhir/ValueSet/participationstatus

// --- Profile Definition ---

Profile:        NexusAppointmentResponse
Parent:         AppointmentResponse
Id:             nexus-appointment-response
Title:          "Appointment Response Profile"
Description:    "A participant's answer to a booking: whether the person an appointment is for has accepted it, is still deciding, or has declined. Written by an integrating system outside the EMR and read wherever a booking's confirmation state is displayed, which is derived from `participantStatus` rather than from `Appointment.status`. An answer supersedes an earlier answer rather than amending it, and the latest one describes where the booking stands."
* ^url = $NexusAppointmentResponse // Canonical URL from alias
* ^status = #draft
* ^experimental = false
* ^publisher = "WELLSTAR Technologies Corp."

// NAMING. New profile ids on this guide are `nexus-<slug>`; the `-emr-core-` infix on the older
// scheduling profiles is inherited from a rename and is being retired, so it is not extended to
// new artifacts. Canonical URLs are permanent, which is why a new one is minted in the shape the
// guide is moving toward rather than the shape it is moving away from.

// THE effective date for this profile: NONE, deliberately (see the EffectiveDate / NoEffectiveDate
// RuleSets). An answer records no clinical event and R4 gives it no date element -- `start` and
// `end` describe a proposed appointment time, not when the participant answered. Declaring `none`
// rather than nothing stops a consumer inventing a date from whichever element it finds first and
// then dating a chart surface by a time somebody proposed for a visit.
* insert NoEffectiveDate

// SMART on FHIR exposure -- READ AND WRITE, NO DELETE. An external producer creates and revises
// these through the FHIR API, so read and write are both real interactions rather than a guess.
// Delete is not granted: an answer is superseded by a newer answer, never removed, and every
// consumer here already resolves the latest one. Destroying an answer would silently move a
// booking's displayed state backwards with nothing recording that it had moved.
* insert SmartExposedNoDelete(appointmentresponse)

// --- Element Constraints ---

* . ^short      = "A participant's answer to a booking"
* . ^definition = """
  Whether the participant an appointment names has accepted it, is still deciding, or has declined
  it.

  This is where a booking's confirmation state is recorded. It is a separate fact ABOUT an
  appointment rather than a change to it: recording an answer does not alter the appointment, so a
  refusal here leaves the slot booked in the schedule until somebody acts on it.
  """

* meta.source 0..1 MS
* meta.source ^short = "Identifies EMR instance & pipeline version the resource came from"
* meta.source ^definition = "A URI that identifies the EMR pipeline and version from which this resource originated."

* text 0..1 MS
* text ^short = "Narrative"
* text ^definition = "A human-readable narrative summary of the answer for human interpretation."

// -----------------------------------------
// Identity
// -----------------------------------------
* identifier 0..* MS
* identifier ^short = "Business identifier(s) for the answer"
* identifier ^definition = """
Unique identifiers for this answer, where the producing system has one.

Optional: an answer produced outside the estate may carry no identifier of its own, and nothing
here resolves one. Where the source system does have one, carry it rather than dropping it.
"""
* insert SourceIdentifier(identifier)

// -----------------------------------------
// The booking being answered
// -----------------------------------------
// Already 1..1 in base R4; restated here as Must Support because it is the join every consumer
// makes. Answers arrive as one flat list for a whole day or a whole patient and each is matched
// back to its booking through this reference alone -- an answer that named none would belong to no
// row and is dropped rather than displayed anywhere.
* appointment 1..1 MS
* appointment only Reference(NexusEmrCoreAppointment)
* appointment ^short = "The appointment this answers"
* appointment ^definition = "The booking whose confirmation state this answer describes. Required by base R4, and the only route from an answer back to the appointment it belongs to: a consumer indexes answers by this reference and reads the latest one per appointment."

// -----------------------------------------
// The answer itself
// -----------------------------------------
// 1..1 and required-bound in base R4; both are restated rather than narrowed. See the header for
// why the four-code set is kept whole even though one code drives no display state.
* participantStatus 1..1 MS
* participantStatus from $participationstatus (required)
* participantStatus ^short = "accepted | declined | tentative | needs-action"
* participantStatus ^definition = """
The participant's answer to the booking.

A consumer should expect all four codes. `accepted`, `tentative` and `declined` each describe a
distinct state of a booking that has not yet happened; `needs-action` says the participant has been
asked and has not answered, which is what an unanswered booking already says.
"""
* participantStatus ^comment = "An absent or blank status supplied by a producer is stored as `tentative`, because the element is mandatory in R4 and rejecting the resource would discard the answer entirely. `tentative` therefore means EITHER that the participant is still deciding OR that the producer said nothing, and the two are indistinguishable from the resource. A status a producer did supply is never rewritten."

// -----------------------------------------
// Who answered
// -----------------------------------------
// 0..1, matching base R4, and the lower bound is genuine rather than an oversight: a producer
// outside the estate may record an answer against a booking without naming the party who gave it.
// The CONSEQUENCE is worth stating, because it is invisible until a chart looks empty -- R4's
// `patient` search parameter on this type resolves through `actor`, so an answer with no actor is
// unreachable by any patient-scoped read. It is still reachable by the booking it answers, which
// is how a day's board finds it.
//
// `0..1` IS TRUE OF THE ELEMENT AND NOT OF THE RESOURCE, which is the part a reader of the
// cardinality alone will get wrong. R4 puts a single invariant on this type -- `apr-1`,
// `participantType.exists() or actor.exists()` -- so the actor may be absent only when
// `participantType` carries the role in its place. An answer that names neither is invalid against
// base R4 and no conformant server will accept it. Saying `0..1` without saying that would teach
// the one shape this type forbids.
* actor 0..1 MS
* actor ^short = "The party whose answer this is"
* actor ^definition = "The participant who answered -- usually the Patient the appointment is for, and a Practitioner, RelatedPerson, Device, HealthcareService or Location where a booking is answered on another party's behalf."
* actor ^comment = "Optional, and an absent actor narrows how the answer can be found: a search scoped to one patient matches through this element, so an answer without it is returned only by a search naming the appointment. Populate it wherever the producing system knows who answered. The optionality is conditional, not free: R4's `apr-1` invariant requires `participantType` or `actor`, so an answer that omits the actor SHALL state the participant role instead -- the generic `PART` where the producing system does not know it. An instance carrying neither is invalid against base R4."

// WHAT IS DELIBERATELY NOT CONSTRAINED, because the absence is the statement.
//
// `start` and `end` are R4's counter-proposal: a participant declining the time offered while
// proposing another. No producer here negotiates a time that way -- a new time is a change to the
// appointment -- so nothing is said about them, and a consumer meeting them is meeting data this
// guide does not describe.
//
// `participantType` and `comment` pass through unchanged and neither is constrained here. Nothing
// in the shape of an answer depends on them, and constraining an element on the strength of one
// producer's habits would freeze a shape nobody chose.
//
// `participantType` is unconstrained, not unobliged. R4's `apr-1` already requires it whenever
// `actor` is absent, and this profile leaves that requirement where the base put it rather than
// raising the cardinality here: the obligation is CONDITIONAL, and a `1..*` would demand a role on
// every answer including the ones that name the answering party outright.