[Implementation 8] Expanding an N-Visit Purchase into a Visit Plan

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.
| Boundary | Rejected model: pre-create future reservations | Current model: create a visit plan |
|---|---|---|
| Immediately after purchase | Three appointment times enter the calendar | Three unscheduled occurrences enter the plan |
| Patient consent | Assumed for every future visit | Obtained for the exact time and terms of each proposal |
| Resource allocation | Practitioners, equipment, and rooms are occupied too early | Only a confirmed visit commitment occupies resources |
| Schedule change | Later reservations move in a chain | Unscheduled occurrences remain in the plan |
| Interval | Fixed from purchase time or an arbitrary date | Calculated from actual completion of the preceding occurrence |

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.
| Input | What the plan preserves | What the plan does not decide |
|---|---|---|
| Purchase authority and purchase ID | One AppointmentPlan per purchase | Payment approval and refund amount |
| Product ID, version, and hash | The exact catalog definition interpreted at purchase time | Price changes and sales policy |
| Patient reference and booking preference | A protected reference and immutable preference for later proposals | A confirmed appointment time |
| Product BOM and repeat count | One PlannedTreatment per occurrence | Actual 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.

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:
- It validates
minimumIntervalDays,preferredIntervalDays, andmaximumIntervalDayson catalog items and copies them to each occurrence. - When an execution plan contains a
BLOCKINGdependency,AppointmentProposalServicereads 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.

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 revision | Occurrence 1 | Occurrence 2 | Occurrence 3 | Remaining count for display |
|---|---|---|---|---|
| Initial revision | PENDING | PENDING | PENDING | 3 |
| Completion revision | COMPLETED | PENDING | PENDING | 2 |
“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.
| Target | State after the refund fact | Reason |
|---|---|---|
| Directly refunded remaining occurrence | CANCELLED | The external authority confirmed cancellation |
A BLOCKING successor that requires it | CANCELLED | Its predecessor obligation no longer exists |
Independent NON_BLOCKING item | Remains PENDING | It can be fulfilled independently |
| Previously completed occurrence in an older revision | Unchanged | Refund 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.
repeatCountcreates 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.
Summary
Section titled “Summary”An N-visit product starts as a plan with N visit obligations, not N future reservations.
- The purchase-completed event creates an
AppointmentPlanfrom the exact catalog and purchase data. repeatCountexpands intoPlannedTreatmentoccurrences 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.
Sources
Section titled “Sources”- clinic-appointment repository
PurchaseCompletedHandlerAppointmentPlanFactoryCatalogDefinitionValidatorAppointmentProposalServiceTreatmentFulfillmentHandlerPlanDirtySetResolverAppointmentPlanFactoryTestTreatmentFulfillmentHandlerTest- Package products as execution graphs
Comments
Leave a note or reaction with your GitHub account.