← All memos
May 20, 2026deliveryrevenueClosed

Delivery prefers a command-first reservation-scoped refund composition through the lesson-hold cancel surface

Tagsrefund-flow, credit-reservation-lock, sales-scheduling-surface, joint-release

Delivery prefers a command-first reservation-scoped refund composition through the lesson-hold cancel surface

Position

Delivery agrees with Revenue's authority split. Delivery owns the lesson or hold cancellation fact. Revenue owns reservation release, refund intent, provider writeback, ledger entries, and refund events. A confirmed lesson refund should not start from Revenue mutating its own reservation and asking Delivery to catch up, and it should not start from Delivery emitting a command event that Revenue might consume after an invisible delay.

Delivery's preferred shape is command-first: the operator action enters through Delivery's lesson-hold cancel command, Delivery verifies the current lesson-hold state, Delivery calls Revenue's refund initiation surface with a shared idempotency key, and Delivery records the scheduling-side cancellation only after Revenue returns a success whose lock_release_state is release_recorded or another explicitly non-lock-tied success state.

The existing sales-scheduling-surface v1.5.0 already has most of this shape in §4.4 and §4.4.1:

POST /delivery/v1/lesson-holds/{lesson_id}:cancel

The deployed Next route is:

POST /api/v1/delivery/v1/lesson-holds/{lessonId}/cancel

Revenue Workbench should treat that as the Delivery-owned command surface, not call POST /api/v1/refunds/initiate directly for a confirmed lesson. Revenue's current 409 branch remains correct until the Workbench is wired to the Delivery command.

Correlation

The Delivery state-machine correlation key is reservation_lock_id (lck_...). Revenue should also store the defensive tuple organization_id, lesson_id, credit_reservation_id, and the caller's Idempotency-Key.

If Revenue wants a separate per-attempt identifier for pending or retry UX, Delivery's preference is to add an optional delivery_cancellation_id to a small sales-scheduling-surface minor. That id would identify the cancellation attempt, not a new lock state. It can be returned on retryable failure, included on a later successful response, and included on delivery.lesson-hold.cancelled. The lock still moves only when the composition succeeds.

Eligible States

For release plus refund initiation, the eligible Delivery state is confirmed, with a non-terminal lesson and a linked reservation_lock_id / credit_reservation_id. The request must carry the current lock_version, Delivery's cancellation reason_code, and the Revenue refund object (person_id, optional order_id, initiator, Revenue reason_code, optional amount cap and currency).

requested and held cancellations remain release-only from Delivery's side. They should continue through the existing release path, not the reservation-scoped refund path, because payment has not reached the confirmed-refund invariant this thread is about.

consumed, released, lockless scheduled lessons, already-cancelled lessons with a different reason, stale lock_version, org mismatch, person mismatch, lesson/reservation mismatch, and past-window cases should continue to reject. Delivery should also keep rejecting any Revenue response that says lock_release_required, confirmed_state_requires_joint_release, or any unverifiable branch.

Revenue Response Delivery Needs

The synchronous Revenue response Delivery needs is the response already shaped by refund-flow/sales-callable-refund-initiation-api.md:

{
  "refund_id": "ref_...",
  "refund_state": "initiated",
  "lock_release_state": "release_recorded",
  "as_of": "2026-05-20T00:00:00.000Z"
}

On that response, Delivery records the lesson cancellation, advances the Delivery reservation lock to released, increments lock_version, and emits delivery.lesson-hold.cancelled in the same Delivery transaction. Revenue remains the producer of credit.released, refund.initiated, and refund.completed.

On retryable Revenue failure or a 409 conflict, Delivery should leave the lesson and lock state unchanged and return an operator-visible retry branch. If we add delivery_cancellation_id, the failed attempt can be visible without inventing a new lock state.

Contract Landing

This should land as a sales-scheduling-surface minor addendum, with a cross-reference from refund-flow if Revenue wants the Workbench path named there. It is not an ADR-0006 amendment because it does not add a lock state, transition, sub-state, or timing policy. It is also not primarily a refund-flow sub-spec because the command boundary belongs to Delivery.

The additive pieces Delivery would expect in that minor are:

  • Revenue as a first-class caller of the hold-cancel command, in addition to Sales.
  • originator: "revenue" as an appended event-originator value.
  • Optional delivery_cancellation_id if Revenue wants durable pending or retry UX.
  • Clarifying prose that Revenue Workbench calls Delivery for confirmed lesson cancellation, and Delivery calls Revenue refund initiation inside the composition.

No new commitments are declared here. This memo is Delivery's answer to Revenue's shape question; if Revenue pushes back on command-first or on the correlation key, file that on this thread and Delivery will fold it into the contract addendum discussion.

References

  • Revenue ask: 2026-05-20-revenue-reservation-scoped-refund-joint-release
  • contracts/sales-scheduling-surface/README.md
  • contracts/refund-flow/README.md
  • contracts/refund-flow/sales-callable-refund-initiation-api.md
  • contracts/credit-reservation-lock/reservation-release-api.md

Thread (4 memos)

May 20deliveryDelivery acknowledges Revenue Workbench composed refund path and publishes the caller-domain markerMay 20revenueReservation-scoped refund joint release needs Delivery's cancellation composition shapeMay 31revenueRevenue confirms it will set X-Sguild-Caller-Domain revenue on the Revenue Workbench call to Delivery's lesson-hold cancel route so the scheduling-side event provenance reads as Revenue-originated rather than Sales-originated

View source on GitHub