Skip to content

[Operations 4] Profile Changes Do Not Rewrite Confirmed Visit Commitments

A robot operator separates reevaluable appointment cards from protected confirmed commitments after receiving a CRM change signal
A profile change can be a reason to review a reservation. It is not a reason to rewrite a visit commitment that has already been confirmed.

A patient profile has changed in the CRM. Because that change may affect appointment eligibility, the appointment service may need to review a current proposal or hold. But if the appointment service stores the CRM’s source profile, or silently moves a visit that the patient and clinic have already agreed on, the ownership boundary is lost.

The conclusion is simple:

A CRM profile change is an input signal for reservation reevaluation, not a command to rewrite a confirmed visit commitment. Automatically reevaluate only PROPOSED and HELD; protect the current appointment, consent, and resource allocation of CONFIRMED.

This article does not explain how the CRM calculates an assessment. It focuses on the minimum event the appointment service accepts, the durable job and outcome model, fair dispatch across clinics, failure recovery, and the next action shown to STAFF. The implementation and the operations runbook are the source of those boundaries.

The operations screen should show job state, not the profile itself

Section titled “The operations screen should show job state, not the profile itself”

When STAFF investigates a profile change, the first useful facts are not the patient’s name or score. The screen should first answer how many jobs are waiting, what is running now, which jobs are waiting for another attempt, and which failures require operator attention.

The operations-screen concept places the following values from ProfileReevaluationOperationalSnapshot at the top:

  • PENDING: reevaluation jobs that have not been processed yet
  • RUNNING: jobs currently owned by a worker lease
  • RETRY_WAIT: jobs waiting for their next attempt after a transient failure
  • FAILED: jobs that exhausted automatic recovery and need an operator
  • active lease count and the age of the oldest backlog item
  • lease-renewal failure count and consecutive assessment-failure count
  • drainState: ACTIVE, DRAINING, or DRAINED

The action queue is not just another list. A list describes the current state; an action queue helps STAFF decide what to inspect first. Each row therefore includes targetRevision, priorityClass, dueAt, nextAttemptAt, the latest failure code, and outcome counts. The queue makes the HELD_PRESENT-before-PROPOSED_ONLY priority explicit.

An operations screen concept with PENDING, RUNNING, RETRY_WAIT, and FAILED cards, active lease and backlog metrics, an action queue, selected-job details, STAFF preview, and scoped ADMIN redrive

This is a concept based on the implemented operations snapshot and endpoint contract. The numbers are illustrative. It deliberately omits patient names, contact details, the source profile, and assessment content. STAFF can read and preview; only ADMIN can execute a redrive within an authenticated scope.

The detail panel keeps only the values needed for the next operational decision:

  • tenant group and clinic scope
  • the target profileRevision
  • targetPolicyRef and targetPolicyGeneration
  • current job state and the latest outcome
  • dueAt, nextAttemptAt, the latest failure code, and outcome counts

It does not show the source profile, feature values, scores, explanations, assessment body, patient name, or phone number. Those values are not needed to choose the next operation, and the appointment service is not their source of truth.

A CRM event starts reevaluation with a minimum payload

Section titled “A CRM event starts reevaluation with a minimum payload”

The implemented event model is PatientSchedulingAssessmentChanged. As the name suggests, it signals that a CRM assessment relevant to scheduling has changed; it does not carry the profile itself.

data class PatientSchedulingAssessmentChanged(
val eventId: String,
val tenantGroupId: Long,
val clinicId: Long,
val patientReferenceFingerprint: String,
val profileRevision: Long,
val materialChange: Boolean,
val assessmentRef: String,
val assessmentHash: String,
val occurredAt: Instant,
)

The event crosses the appointment-service boundary with:

  1. eventId for deduplication and audit
  2. tenantGroupId and clinicId for the processing scope
  3. a lower-case SHA-256 patientReferenceFingerprint that does not directly identify the patient
  4. profileRevision for freshness checks
  5. assessmentRef and assessmentHash for locating and verifying the assessment result
  6. materialChange to indicate whether the change can affect a scheduling decision
  7. occurredAt from the source event

The event contains no raw profile, features, scores, explanations, or corrections. Possessing a fingerprint does not let the appointment service recover the patient’s identity. This keeps the CRM as the source of truth for profile and assessment data, while the appointment service owns appointment state and resource allocation.

assessmentHash does not mean “trust whatever the CRM sent.” It is a contract value that lets the appointment service verify that the referenced assessment belongs to the revision named by the event. Events that fail trust, schema, or scope validation are not turned into reevaluation jobs; they are isolated for investigation or quarantine.

Merge the latest revision, then target only PROPOSED and HELD

Section titled “Merge the latest revision, then target only PROPOSED and HELD”

Several profile-change events can arrive for the same patient scope within seconds. Starting a full reservation scan for every event could let an older revision overwrite a newer decision. The storage model therefore separates a scope-level latest head from durable jobs for individual revisions.

tenantGroupId + clinicId + patientReferenceFingerprint
latestRevision head
revision-specific reevaluation job

ProfileReevaluationScope combines tenant group, clinic, and fingerprint. The fingerprint must use the lower-case SHA-256 form. ProfileReevaluationHeadRecord points to the newest revision, assessment reference, and hash observed for that scope, while ProfileReevaluationJobRecord processes one particular revision durably.

The job states expose the processing lifecycle:

StateMeaningNext STAFF action
PENDINGWaiting to be processedCheck the target revision and dueAt
RUNNINGA worker owns a leaseCheck the active lease and elapsed time
RETRY_WAITWaiting for a cooldown after a transient failureCheck nextAttemptAt and the failure code
COMPLETEDProcessing for the latest revision is completeCheck outcome counts and the outbox record
STALEA newer revision arrived, so this job will not continueMove to the latest job
FAILEDAutomatic recovery was exhaustedClassify the cause, then preview or investigate

The eligibility rule for one appointment is deliberately narrower:

val AppointmentCommitmentStatus.isProfileReevaluationEligible: Boolean
get() = this == AppointmentCommitmentStatus.PROPOSED ||
this == AppointmentCommitmentStatus.HELD

The absence of CONFIRMED is a protection rule, not an implementation gap. A CONFIRMED appointment is a visit commitment already agreed by the patient and clinic, and it is tied to resources such as a provider, time, or device. If the profile change requires a new decision, the system can create a separate proposal; patient consent and an operator confirmation must precede a change to the confirmed commitment.

Record assessment lookup and the final state decision as one job

Section titled “Record assessment lookup and the final state decision as one job”

A PROPOSED or HELD appointment is not written to a new state immediately. The worker claims the job through fair dispatch across clinics, fetches the assessment referenced by the event, and verifies the policy reference and generation. The job keeps heldTarget, proposedTarget, targetPolicyRef, targetPolicyGeneration, nextAttemptAt, lease data, and cursors.

Fair dispatch is not a repeated ORDER BY id LIMIT N. ClaimProfileReevaluationJobs takes both a global limit and a per-clinic limit, while ProfileReevaluationClinicCursor resumes keyset ordering from the clinic after the last one served. A large backlog in one clinic therefore cannot permanently starve clinics with smaller identifiers.

Once the assessment result arrives, each appointment receives an explicit outcome:

OutcomeMeaning
PROPOSAL_SUPERSEDEDReplace the existing proposal with a new one
HOLD_KEPTKeep the current hold because it is still valid
HOLD_REPLACEDAtomically replace the existing hold
FALLBACK_TO_PROPOSEDRelease the hold and return to a proposal when no replacement is available
SKIPPED_INELIGIBLESkip because the current state is not eligible for automatic reevaluation
SKIPPED_UNCHANGEDLeave the appointment unchanged because the evaluated input is the same

These outcomes must not be collapsed into “the assessment passed” or “the appointment was confirmed.” HOLD_REPLACED means that held capacity changed; it does not create a CONFIRMED visit. SKIPPED_INELIGIBLE can be a successful protection of the current state rather than an error.

A sequence diagram where a minimal CRM event passes through the appointment API and reevaluation worker, fetches an assessment, and reaches a final state decision for PROPOSED, HELD, protected CONFIRMED, RETRY_WAIT, or QUARANTINE
The diagram gives the horizontal dotted line no hidden meaning; Final State Decision is an explicit node. PROPOSED and HELD can record reevaluation outcomes, while CONFIRMED follows a protected path.

Putting every failure into automatic retry makes it impossible for operators to distinguish a temporary dependency problem from a data, authorization, or privacy incident.

If the CRM assessment endpoint is temporarily slow or the database returns a transient error, the job can move to RETRY_WAIT. It retains attemptCount, nextAttemptAt, lastFailureCode, lease data, and redrive lineage. Automatic redrive is limited by configured attempts and cooldown.

If signature, issuer, audience, schema, or tenant/clinic scope cannot be verified, if a raw profile appears to have crossed the boundary, or if quarantine repeats, the job is not released through generic retry. Stop the consumer, preserve the evidence, and investigate with the CRM and security on-call teams.

The FAILED rows on the operations screen show only a bounded failure code, never exception text or patient data. For example, ASSESSMENT_TIMEOUT may be a limited retry candidate, while PRIVACY_BOUNDARY requires investigation first.

Separate STAFF read access from ADMIN execution

Section titled “Separate STAFF read access from ADMIN execution”

Operational state and redrive live in a Spring Actuator endpoint rather than the regular appointment API.

GET /actuator/profileReevaluation
POST /actuator/profileReevaluation

The read operation returns ProfileReevaluationOperationalSnapshot. The write operation accepts ProfileReevaluationAdminAction, a reason, an idempotency key, tenant/clinic scope, target revision, and a limit. It does not trust an actor written into the request body. ProfileReevaluationAdminActorResolver obtains the audit actor only from the authenticated SchedulingUserPrincipal, then verifies issuer, token ID, authentication time, and the allowed clinic list.

The runbook sequence is:

  1. Verify the ADMIN role and SCOPE_profile-reevaluation:operate permission.
  2. Require tenantGroupId and clinicId, then verify that the authenticated principal is allowed to operate on that clinic.
  3. Use PREVIEW to inspect the actual target and target revision.
  4. Execute only when the preview matches the approved scope.
  5. Re-read the new job lineage, outcomes, backlog, and whether any CONFIRMED appointment changed.

This is also why the screen has no “retry everything” button. A failure could be a privacy incident, CRM latency, or a policy mismatch for one revision. Redriving a broad scope without classifying the cause can repeat the problem.

Rollout and rollback are separate from implementation complete

Section titled “Rollout and rollback are separate from implementation complete”

Having a job model and endpoint in code does not mean that every clinic is ready to use them. The runbook expands mutation mode in this order:

DISABLED → DRY_RUN → APPLY_PROPOSED → APPLY_PROPOSED_AND_HELD

The default targets are HELD=5m and PROPOSED=30m. They are queue-wait objectives, not guarantees that every job will finish in that time. A clinic-specific override cannot move an already-created job’s processing time backward.

Rollback is not a bulk reversal of already successful reservation transactions. It is a procedure that stops new mutation and checks these invariants:

  • mutation-mode=DISABLED or enabled=false
  • drainState=DRAINED and activeLeases=0
  • zero changes to CONFIRMED appointments
  • valid HELD allocation is not lost except through a normal atomic replacement
  • job, outcome, inbox, quarantine, outbox, and audit rows are preserved

Do not mark a rollback as proof that completed jobs returned to their original state. Re-read state and history to determine which jobs finished and which remain.

Distinguish current implementation, approved design, and operational readiness

Section titled “Distinguish current implementation, approved design, and operational readiness”
AreaWhat this article covers
Current implementationMinimal CRM event, scope-level latest head, durable job states and outcomes, PROPOSED/HELD eligibility, fair dispatch across clinics, an operational snapshot, and a redrive endpoint that verifies authentication and clinic scope
Approved operational designPREVIEW before scoped EXECUTE, DISABLED → DRY_RUN → APPLY_PROPOSED → APPLY_PROPOSED_AND_HELD, separate retry and quarantine handling, and protection of CONFIRMED during rollback
Operational readinessReal clinic allowlists, permission and redrive drills, assessment saturation alerts, drain rehearsals, privacy-incident response, and a read-only snapshot comparison before rollout
Follow-up improvementsA proposal→acceptance→confirmation flow with consent evidence, a notification contract for new patient proposals, and an audit link between operational outcomes and CRM assessments

Keeping implementation and readiness separate matters. The endpoint’s existence does not prove that a real clinic has practiced an ADMIN redrive. Conversely, the eligibility rule and outcome records that protect CONFIRMED are current code behavior and should not be described as merely future work.

A profile change still requires a final visit-commitment check

Section titled “A profile change still requires a final visit-commitment check”

The operational flow can be summarized as the following next actions:

  • The CRM sends a minimum event with a revision, fingerprint, assessment reference, and hash instead of the source profile.
  • The appointment service merges the latest revision and reevaluates only current PROPOSED and HELD commitments.
  • Assessment lookup failures become bounded RETRY_WAIT jobs; trust, privacy, and contract failures become QUARANTINE cases.
  • Outcomes are recorded as PROPOSAL_SUPERSEDED, HOLD_KEPT, HOLD_REPLACED, FALLBACK_TO_PROPOSED, or SKIPPED_*.
  • CONFIRMED is not changed automatically; a new proposal, patient consent, and operator confirmation are required when a change is needed.
  • STAFF reads the snapshot and preview, while ADMIN can redrive only within an authenticated clinic scope.
  • After rollback or drain, re-read completed jobs, outcomes, and allocations to establish the final state.

An operations screen should provide more clarity, not merely more information. In one view, STAFF should be able to distinguish whether a job is eligible, whether it can run now, whether a failure can be retried or must be investigated, and whether the confirmed visit commitment was protected.

For the underlying design boundaries, see the CRM profile change and reservation reevaluation visual companion. The visual companion presents the state, permission, and privacy boundaries as a separate board; this article translates them into STAFF’s next operational action.

Comments

Leave a note or reaction with your GitHub account.