Skip to content

Health Card Eligibility Extension

Canonical../StructureDefinition/health-card-eligibility
Statusdraft · 1.26.0
BaseExtension (constraint)
ContextPatient.identifier (element)
SourceFSH · JSON

The payer's settled answer about one provincial health card, carried on the card's own Identifier, together with a marker for a check that could not reach the payer.

FSH

The authored source of truth: extensions/HealthCardEligibilityExtension.fsh (edit on GitHub). One FSH file may define several related artifacts.

// HealthCardEligibilityExtension.fsh
// The settled payer answer to "is this health card good", carried ON THE CARD.
//
// WHY IT RIDES THE IDENTIFIER AND NOT THE PATIENT. An eligibility answer is an answer about one
// exact card: a province, a number and a version code together. A patient whose card is replaced
// has a different card, and the answer given for the old one says nothing about the new one. Put
// on `Patient` the answer would outlive the card it was about and be read against a card nobody
// ever asked the payer about, which is a wrong answer rather than a missing one. Put on
// `Patient.identifier` it is discarded with the card it belongs to, by construction. This is the
// same idiom the card's version code and the status-card type already use.
//
// AND NOT A `CoverageEligibilityResponse`. R4 models an eligibility check as a request/response
// pair, and that pair is the record of one exchange with the payer -- who asked, when, under what
// benefit, with what errors. What this element holds is narrower and is a property of the chart
// rather than of an exchange: the CURRENT answer for this card, one value, replaced in place each
// time it changes. Every surface that shows a card wants that one value without a search, and a
// reader that had to resolve the newest of a patient's responses to render a card would be
// answering a different question. A `CoverageEligibilityResponse` raised from a check is expected
// to be able to exist alongside; nothing here replaces it.
//
// FOUR CHILDREN, AND ONE OF THEM IS NOT PART OF THE ANSWER. `eligible` and `answeredAt` are the
// answer and both are required for it to be readable. `coverageExpiry` qualifies it. The fourth,
// `attemptFailedAt`, is not an answer at all -- it records that a check could not reach the payer.
// It stands beside the answer children without disturbing them, which is what lets the last real
// answer survive an outage.
//
// `coverageExpiry` IS DEPRECATED, AND IS NOT BEING REMOVED. It restates `Identifier.period.end`,
// which R4 already defines as the window an identifier is valid for use and which this guide
// already uses for the Indian status card's expiry on the same resource. Two places to write one
// fact is a defect whichever one a producer picks. But the child is published, answers already
// stored carry it, and deleting a published child breaks every consumer that reads it -- so it
// stays, marked deprecated in the text that ships, with `Identifier.period.end` named as the
// element to write.
//
// ────────────────────────────────────────────────────────────────────────────────────────────────
// THE CANONICAL HERE IS NOT THE ONE THE SOFTWARE WRITES TODAY.
//
// This publishes under `../StructureDefinition/...`, the base
// `conventions.md` names. Nexus EMR writes it under
// `http://services.well.com/fhir/identifier/eligibility`, on a host this specification does not
// govern -- minted in code before the guide declared it, and not followed here, because a guide
// that adopts every address its implementation happens to have minted has no canonical base at
// all. Moving the writer, and the answers already stored on cards behind it, is tracked as
// FHIR-381. The handbook states the rule and indexes every published artifact in this position:
// see `docs/guide/patterns/canonical-urls.md`.
// ────────────────────────────────────────────────────────────────────────────────────────────────

Extension: HealthCardEligibility
Id: health-card-eligibility
Title: "Health Card Eligibility Extension"
Description: """
The payer's settled answer about one provincial health card, carried on the card's own
`Identifier`, together with a marker for a check that could not reach the payer.

**It belongs to one exact card.** A card is a province, a number and a version code together, and
an answer given for one card says nothing about a card that has since changed. Carried here, the
answer is discarded with the card it was about rather than outliving it on the patient.

**It belongs on the health card and on nothing else.** `Patient.identifier` also carries the medical
record number, the legacy paper chart number and whatever identifiers a source system contributed,
and a payer eligibility answer means nothing on any of them. The extension's `contextInvariant`
requires the identifier it rides to carry the HL7 v2-0203 type `JHN` (jurisdictional health number),
which is the same discriminator the Patient profile uses to recognise a provincial health number. An
eligibility answer stamped on an MRN or a chart number is not merely unhelpful; it is invalid.

**`answeredAt` is when the payer gave THIS answer, not when the patient was last checked.** The
answer is rewritten only when it changes, so a check that confirms an existing answer leaves the
moment alone. Nothing may infer freshness from it: an answer dated months ago may have been
reconfirmed this morning.

**A failed attempt is not an answer.** `attemptFailedAt` records that a check could not reach the
payer. It never touches the answer children, so the last real answer survives an outage, and it
dates the failure rather than the answer, so freshness cannot be inferred from it either. It is
cleared by the next check that reaches the payer. An extension carrying only `attemptFailedAt`
carries no answer at all.

**Read the answer whole or not at all.** `eligible` and `answeredAt` are both required, and a
malformed `coverageExpiry` invalidates the whole answer rather than being dropped -- dropping it
would hand back an answer that reads as covered indefinitely. An unreadable `attemptFailedAt` is
the one child held to a softer rule: it reads as no marker, because a corrupt display-only child
must not be able to hide a real answer.

**Absence of `coverageExpiry` is not an unlimited entitlement.** It means the payer delivered no
expiry with this answer.

**`coverageExpiry` is DEPRECATED. Write the expiry to `Identifier.period.end` instead.** R4 defines
`Identifier.period` as the time period during which the identifier is or was valid for use, which is
exactly what a card's coverage expiry is, and this guide already records the Indian status card's
expiry there on the same resource. A child duplicating a base element gives a reader two places to
look and no rule for which wins when they disagree. The child stays defined because answers already
stored carry it and must keep reading back; a producer that has not moved yet is not thereby
nonconformant. Readers SHOULD read `Identifier.period.end` first and fall back to `coverageExpiry`,
and SHOULD NOT treat a disagreement between them as an error -- prefer `Identifier.period.end`.

**MIGRATION.** Nexus EMR writes this extension today at
`http://services.well.com/fhir/identifier/eligibility` (FHIR-381): that address is live in stored
patient records and in code, and is under migration to the canonical declared here. Write the
canonical; when reading, match both addresses exactly. The final segments differ (`eligibility`
against `health-card-eligibility`), so a suffix rule does not reach the pre-migration one and a
reader built on it misses every answer stored before the writer moves.
"""
* ^url = "../StructureDefinition/health-card-eligibility"
* ^status = #draft
* ^date = "2026-09-04"
* ^publisher = "WELLSTAR Technologies Corp."
* ^jurisdiction = urn:iso:std:iso:3166#CA "Canada"
* ^context[0].type = #element
* ^context[0].expression = "Patient.identifier"
// The context element alone says only "some identifier on a Patient", which is every identifier the
// chart carries. The jurisdictional health number is recognised by its v2-0203 `JHN` type -- the
// discriminator the Patient profile's `identifier[jurisdictional]` slice already uses -- so the same
// test is the one that says this answer is on the card it is an answer about. Evaluated against the
// Identifier, it is false for the MRN (typed `MR`), false for the paper chart number (which
// deliberately carries no type at all), and false for the Indian registration number.
* ^contextInvariant[0] = "type.coding.where(system = 'http://terminology.hl7.org/CodeSystem/v2-0203' and code = 'JHN').exists()"
* . ^short = "Settled payer eligibility answer for this health card"
* value[x] 0..0
* extension contains
    eligible 0..1 MS and
    answeredAt 0..1 MS and
    coverageExpiry 0..1 MS and
    attemptFailedAt 0..1 MS
* extension[eligible] ^short = "The payer's answer; required for the answer to be readable"
* extension[eligible].value[x] 1..1 MS
* extension[eligible].value[x] only boolean
* extension[eligible].valueBoolean ^definition = "The payer's settled answer for this card. Present with `answeredAt` or the extension carries no readable answer."
* extension[answeredAt] ^short = "When the payer gave THIS answer; required for the answer to be readable"
* extension[answeredAt].value[x] 1..1 MS
* extension[answeredAt].value[x] only instant
* extension[answeredAt].valueInstant ^definition = "When the payer gave the answer stored here -- NOT when the patient was last checked. The answer is rewritten only on change, so a re-check that agrees leaves this moment untouched and no freshness may be inferred from it."
* extension[coverageExpiry] ^short = "DEPRECATED -- write Identifier.period.end instead"
* extension[coverageExpiry] ^definition = "**Deprecated. Write the coverage expiry to `Identifier.period.end` and not here.** R4 defines `Identifier.period` as the time period during which the identifier is or was valid for use, which is what a card's coverage expiry is, and this guide already records the Indian status card's expiry there on the same resource; a child duplicating a base element leaves a reader with two places to look and no rule for which wins. This child remains defined, and must keep reading back, because answers stored before the move carry it. Readers SHOULD prefer `Identifier.period.end` and fall back to this."
* extension[coverageExpiry].value[x] 1..1 MS
* extension[coverageExpiry].value[x] only date
* extension[coverageExpiry].valueDate ^definition = "The coverage expiry the payer delivered alongside the answer. Absent means the payer delivered none, NOT that coverage is unlimited. **Deprecated: `Identifier.period.end` is the element to write.** Still must-support, because stored answers carry it and a reader has to be able to read one back."
* extension[attemptFailedAt] ^short = "When a check last failed to reach the payer; not part of the answer"
* extension[attemptFailedAt].value[x] 1..1 MS
* extension[attemptFailedAt].value[x] only instant
* extension[attemptFailedAt].valueInstant ^definition = "When a check last failed to reach the payer. It carries no answer, gates nothing, and dates the failure rather than the answer. Written once for a run of failures -- it names when the failures began -- and cleared by the next check that reaches the payer."