Skip to content
A robot receiving a purchase token and placing three unscheduled visit cards into a plan tray while leaving the calendar empty
Purchase creates visit rights and sequence. Each appointment time is agreed only when that visit is scheduled.

Buying three visits does not create three reservations

Section titled “Buying three visits does not create three reservations”

Suppose Patient A buys a three-visit laser treatment product. At purchase time, the appointment service knows that the clinic owes Patient A three treatments. It does not know the dates of the second and third visits, their practitioners, rooms, or equipment.

Turning the purchase directly into three reservations would occupy calendar time that the patient has not accepted. A delay to the first visit or a different recovery period would force the service to move a chain of premature reservations. A visit plan preserves each obligation while leaving the actual time open until patient consent and resource checks are complete.

BoundaryRejected model: pre-create future reservationsCurrent model: create a visit plan
Immediately after purchaseThree appointment times enter the calendarThree unscheduled occurrences enter the plan
Patient consentAssumed for every future visitObtained for the exact time and terms of each proposal
Resource allocationPractitioners, equipment, and rooms are occupied too earlyOnly a confirmed visit commitment occupies resources
Schedule changeLater reservations move in a chainUnscheduled occurrences remain in the plan
IntervalFixed from purchase time or an arbitrary dateCalculated from actual completion of the preceding occurrence
Comparison between the rejected model that pre-creates three future reservations and the current model that creates three unscheduled visit-plan occurrences, with no-interval and N-to-M-day variants
A purchase creates visit obligations. Appointment times and grouping several occurrences into one visit are separate decisions.

The purchase-completed event creates the visit-plan boundary

Section titled “The purchase-completed event creates the visit-plan boundary”

PurchaseCompletedHandler receives the completed purchase event and loads the exact product version. When the catalog definition is active and purchase ownership matches, it passes the definition and purchase data to AppointmentPlanFactory. The plan, inbox result, and outbox record are stored in the same transaction.

The appointment service owns the scheduling interpretation of purchased rights. It does not own product pricing, refund eligibility, or clinical completion. It preserves the identifiers and state history needed for future scheduling without replacing those external decisions.

InputWhat the plan preservesWhat the plan does not decide
Purchase authority and purchase IDOne AppointmentPlan per purchasePayment approval and refund amount
Product ID, version, and hashThe exact catalog definition interpreted at purchase timePrice changes and sales policy
Patient reference and booking preferenceA protected reference and immutable preference for later proposalsA confirmed appointment time
Product BOM and repeat countOne PlannedTreatment per occurrenceActual clinical completion

repeatCount expands into one PlannedTreatment per occurrence

Section titled “repeatCount expands into one PlannedTreatment per occurrence”

AppointmentPlanFactory does not keep repeatCount only as a mutable counter. For a three-visit product, it creates PlannedTreatmentRecord values with sequenceNo 1, 2, and 3.

val treatments = definition.items.flatMapIndexed { bomOrder, item ->
(1..item.repeatCount).map { sequenceNo ->
PlannedTreatmentRecord(
bomItemId = item.bomItemId,
sequenceNo = sequenceNo,
bomOrder = bomOrder,
minimumIntervalDays = item.minimumIntervalDays,
preferredIntervalDays = item.preferredIntervalDays,
maximumIntervalDays = item.maximumIntervalDays,
earliestStartAt = null,
latestStartAt = null,
status = PlannedTreatmentStatus.PLANNED,
// Duration and practitioner, equipment, and room requirements are copied too.
)
}
}

Both scheduling-window fields are null. The occurrences exist, but their booking windows have not been calculated. A purchase-time booking preference also remains an immutable input; the factory does not turn it into a confirmed visit.

Occurrence rows let the service answer concrete questions:

  • Which occurrence was completed?
  • Which occurrences are still planned?
  • Which duration and resource requirements apply to a specific occurrence?
  • Which occurrence did a refund or partial-completion fact affect?

A mutable remainingCount cannot preserve those answers by itself. Deriving the remaining count from plan items and revision history keeps an operations counter explainable.

A visit plan management screen showing one completed and two remaining occurrences out of three, the next booking window, occurrence fulfillment, operator tasks, and current implementation boundaries

This is a source-backed operations mockup, not a production screen capture. It lets staff reconcile purchased rights, occurrence state, clinical completion evidence, and the next booking window in one view. See the plan, appointment, and capacity visual and the product scheduling classification for the wider flow.

An interval is a future booking window, not a future reservation time

Section titled “An interval is a future booking window, not a future reservation time”

Many repeated products have no clinical delay between occurrences. Their interval fields can be absent, so other constraints such as patient preference and clinic capacity determine the next proposal. No clinical interval does not imply that the occurrences are automatically grouped into the same visit. Visit grouping belongs to a separate package-execution rule.

Other treatments require the next occurrence to happen between N and M days after the previous one. The anchor is the actual completion time of the preceding occurrence, not purchase time or a pre-created reservation. For a minimum of 21 days, a preference of 28 days, and a maximum of 42 days, proposal search should derive its window from those offsets.

The current implementation has two relevant parts:

  1. It validates minimumIntervalDays, preferredIntervalDays, and maximumIntervalDays on catalog items and copies them to each occurrence.
  2. When an execution plan contains a BLOCKING dependency, AppointmentProposalService reads the predecessor’s actual completion time and checks the minimum and maximum window.

One boundary is still disconnected. CatalogDefinitionValidator creates temporary adjacency edges between repeated occurrences only to verify that the expanded graph is acyclic. AppointmentPlanFactory materializes only dependencies that are explicit in the catalog definition. Expanding a repeated item therefore does not automatically persist dependencies from occurrence 1 to 2 and from 2 to 3.

The interval metadata is preserved, but the current factory does not create the adjacency needed to enforce that metadata between repeats. A stored or validated field is not the same as a rule enforced during proposal generation.

Flow from PurchaseCompletedHandler and AppointmentPlanFactory to three planned treatments, an immutable revision after actual completion, cancellation from an external refunded fact, and a separate note for missing repeated-session adjacency
Completion and refund append plan revisions instead of overwriting history. The dashed card is a disconnected implementation boundary, not a successful runtime flow.

Visit completion does more than decrement a remaining count

Section titled “Visit completion does more than decrement a remaining count”

After Patient A actually completes occurrence 1, the authoritative clinical source sends a completion fact. TreatmentFulfillmentHandler copies the active plan revision, marks only that occurrence COMPLETED, and appends a new revision. It does not update the rows in the previous revision.

Plan revisionOccurrence 1Occurrence 2Occurrence 3Remaining count for display
Initial revisionPENDINGPENDINGPENDING3
Completion revisionCOMPLETEDPENDINGPENDING2

“Two visits remaining” is useful UI, but the stored contract is not an integer changed from three to two. The revision shows which occurrence completed, when it completed, and which obligations remain. For partial fulfillment, the completed part stays in history while a separately identified remaining treatment is appended to the new revision.

The same distinction is required for interval calculation. The appointment service needs the predecessor’s completion state and actual completion time, not merely a used-count value of one.

A refund does not erase completion history

Section titled “A refund does not erase completion history”

Refund is an exceptional path, but an N-visit product must account for it. If Patient A completes the first occurrence and then receives a refund for the remainder, commerce or a refund service decides eligibility and amount. The appointment service reflects the authoritative REFUNDED fact in the plan.

PlanDirtySetResolver cancels the directly refunded plan item and the transitive BLOCKING successors that cannot proceed without it. Independent NON_BLOCKING items remain pending.

TargetState after the refund factReason
Directly refunded remaining occurrenceCANCELLEDThe external authority confirmed cancellation
A BLOCKING successor that requires itCANCELLEDIts predecessor obligation no longer exists
Independent NON_BLOCKING itemRemains PENDINGIt can be fulfilled independently
Previously completed occurrence in an older revisionUnchangedRefund does not delete actual completion history

Refund also appends a plan revision. “One of three completed, two refunded” is therefore not flattened into “zero remaining.” Completion and cancellation remain separately explainable.

Replaying the same purchase still produces one plan

Section titled “Replaying the same purchase still produces one plan”

A broker can redeliver an event, and a caller that did not receive the result can retry. If either path created another plan, all remaining counts and proposal candidates would be duplicated.

PurchaseCompletedHandler closes the duplicate path at two identifiers:

  • A terminal inbox row for the event ID returns a duplicate result.
  • An existing plan for the same tenant, clinic, purchase authority, and purchase ID converges to PURCHASE_ALREADY_PLANNED.

Plan persistence and the outbox record share a transaction. Completion and refund handling also has tests that replay the same event without creating another revision or outbox record. Idempotency enforces the business rule that one purchase maps to one visit plan.

The implemented boundary and the boundary still to connect

Section titled “The implemented boundary and the boundary still to connect”

The current implementation guarantees the following behavior:

  • A purchase-completed event loads an active product version and creates a visit plan.
  • repeatCount creates one plan item per occurrence while leaving appointment times empty.
  • Completion, partial completion, and refund facts append a plan revision instead of changing the previous revision.
  • Explicit execution dependencies let proposal generation enforce minimum and maximum windows from actual predecessor completion.
  • Replayed purchase and fulfillment events do not create duplicate plans or revisions.

It does not yet derive persisted execution dependencies between repeated occurrences from item-level interval metadata. Nor does the absence of an interval automatically group occurrences into one visit. The next implementation step must make BOM dependencies, repeated-occurrence adjacency, and visit-grouping constraints explicit in the execution plan.

Keeping stored values, validated values, and proposal-time enforcement separate gives readers an accurate view of the current system.

An N-visit product starts as a plan with N visit obligations, not N future reservations.

  • The purchase-completed event creates an AppointmentPlan from the exact catalog and purchase data.
  • repeatCount expands into PlannedTreatment occurrences with individual sequence numbers.
  • Purchase does not assign future appointment times.
  • Completion and refund append immutable plan revisions instead of mutating a counter.
  • An interval should be anchored to actual completion of the preceding occurrence.
  • Repeated-occurrence adjacency and visit grouping still require an explicit execution-plan connection.

Purchase rights, occurrence obligations, actual completion, and remaining work are now separate and explainable facts. The next article expands a package made of different treatments into an execution graph and asks which items can share a visit.

Comments

Leave a note or reaction with your GitHub account.