@sguild/dispatcher 2.0.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/README.md +4 -1
  2. package/contracts/README.md +30 -0
  3. package/contracts/coach-availability/README.md +355 -0
  4. package/contracts/coach-availability/README.v2.md +263 -0
  5. package/contracts/coach-availability/schema/payloads/coach.assigned-v1.json +91 -0
  6. package/contracts/coach-availability/validation/delivery-assignment.md +89 -0
  7. package/contracts/coach-availability/validation/sales-offer-construction.md +76 -0
  8. package/contracts/coaching-confirmation/README.md +96 -0
  9. package/contracts/coaching-confirmation/schema/payloads/coaching.lesson.confirmation_decided-v1.json +142 -0
  10. package/contracts/coaching-confirmation/schema/payloads/lead.coach.confirmation.requested-v1.json +124 -0
  11. package/contracts/credit-reservation-funding-state/README.md +147 -0
  12. package/contracts/credit-reservation-lock/README.md +433 -0
  13. package/contracts/credit-reservation-lock/delivery-state-vocabulary.md +73 -0
  14. package/contracts/credit-reservation-lock/reservation-create-api.md +191 -0
  15. package/contracts/credit-reservation-lock/reservation-release-api.md +171 -0
  16. package/contracts/credit-reservation-lock/schema/payloads/credit.locked-v1.json +1 -1
  17. package/contracts/credit-reservation-lock/schema/payloads/credit.reserved-v1.json +2 -3
  18. package/contracts/credit-reservation-lock/validation/lesson-lifecycle.md +318 -0
  19. package/contracts/event-envelope/README.md +205 -0
  20. package/contracts/event-envelope/schema/envelope-v1.json +2 -2
  21. package/contracts/event-envelope/validation/event-vocabulary.md +270 -0
  22. package/contracts/event-types-registry.json +337 -24
  23. package/contracts/external-actions/README.md +338 -0
  24. package/contracts/finance-mart/README.md +238 -0
  25. package/contracts/finance-mart/cac-payback.md +113 -0
  26. package/contracts/finance-mart/cash-position.md +98 -0
  27. package/contracts/finance-mart/cohort-summary.md +72 -0
  28. package/contracts/finance-mart/customer-journey-audit.md +92 -0
  29. package/contracts/finance-mart/ltv.md +92 -0
  30. package/contracts/finance-mart/margin.md +87 -0
  31. package/contracts/finance-mart/pnl.md +83 -0
  32. package/contracts/finance-mart/reconciliation.md +98 -0
  33. package/contracts/finance-mart/revenue-recognition-rollup.md +87 -0
  34. package/contracts/finance-mart/unit-economics.md +94 -0
  35. package/contracts/growth-warehouse-api/README.md +162 -0
  36. package/contracts/identity/README.md +184 -0
  37. package/contracts/identity/person-canonical-fields.md +120 -0
  38. package/contracts/identity/person-externals.md +267 -0
  39. package/contracts/identity/person-resolution-semantics.md +144 -0
  40. package/contracts/identity/person-role-taxonomy.md +120 -0
  41. package/contracts/identity/schema/payloads/intake.captured-v2.json +60 -0
  42. package/contracts/identity/schema/payloads/intake.matched-v2.json +123 -0
  43. package/contracts/identity/schema/payloads/person.updated-v1.json +8 -2
  44. package/contracts/identity/schema/payloads/role.assigned-v1.json +50 -0
  45. package/contracts/identity/schema/payloads/role.retired-v1.json +54 -0
  46. package/contracts/identity/validation/client-table.md +131 -0
  47. package/contracts/identity/validation/coach-handling.md +100 -0
  48. package/contracts/identity/validation/person-graph.md +140 -0
  49. package/contracts/lead-lifecycle/README.md +187 -0
  50. package/contracts/lead-lifecycle/schema/payloads/lead.handoff.context.recorded-v1.json +108 -0
  51. package/contracts/lead-lifecycle/schema/payloads/lead.qualified-v1.json +54 -0
  52. package/contracts/lead-lifecycle/schema/payloads/sales.lead.onboarded-v1.json +120 -0
  53. package/contracts/lesson-lifecycle/README.md +118 -0
  54. package/contracts/lesson-lifecycle/schema/payloads/lesson.cancelled-v1.json +30 -0
  55. package/contracts/lesson-lifecycle/schema/payloads/lesson.delivered-v1.json +29 -0
  56. package/contracts/lesson-lifecycle/schema/payloads/lesson.rescheduled-v1.json +157 -0
  57. package/contracts/lesson-lifecycle/schema/payloads/lesson.scheduled-v1.json +107 -0
  58. package/contracts/lesson-lifecycle/validation/README.md +5 -0
  59. package/contracts/mart-consumer-api/README.md +108 -0
  60. package/contracts/order-flow/README.md +106 -0
  61. package/contracts/order-flow/schema/payloads/order.created-v1.json +58 -0
  62. package/contracts/order-flow/schema/payloads/order.updated-v1.json +63 -0
  63. package/contracts/payment-flow/README.md +157 -0
  64. package/contracts/platform-comms/README.md +84 -0
  65. package/contracts/platform-comms/schema/payloads/platform.comms.inbound-v1.json +83 -0
  66. package/contracts/platform-geography-snapshot/README.md +205 -0
  67. package/contracts/platform-geography-snapshot/schema/payloads/geography.market.archived-v1.json +36 -0
  68. package/contracts/platform-geography-snapshot/schema/payloads/geography.market.upserted-v1.json +59 -0
  69. package/contracts/platform-geography-snapshot/schema/payloads/geography.service-area.archived-v1.json +36 -0
  70. package/contracts/platform-geography-snapshot/schema/payloads/geography.service-area.upserted-v1.json +65 -0
  71. package/contracts/portfolio-mart/README.md +133 -0
  72. package/contracts/portfolio-mart/cohort-funnel-panel.md +76 -0
  73. package/contracts/portfolio-mart/cross-discipline-performance.md +91 -0
  74. package/contracts/portfolio-mart/cross-market-performance.md +84 -0
  75. package/contracts/portfolio-mart/health-composites.md +88 -0
  76. package/contracts/portfolio-mart/org-topology.md +70 -0
  77. package/contracts/portfolio-mart/portfolio-level-funnel-health.md +92 -0
  78. package/contracts/portfolio-mart/validation/consumer-isolation.md +33 -0
  79. package/contracts/portfolio-mart/validation/decoupling-discipline.md +34 -0
  80. package/contracts/refund-flow/README.md +136 -0
  81. package/contracts/refund-flow/sales-callable-refund-initiation-api.md +218 -0
  82. package/contracts/sales-scheduling-surface/README.md +532 -0
  83. package/contracts/sales-scheduling-surface/schema/payloads/delivery.lesson-hold.cancelled-v1.json +42 -0
  84. package/contracts/sales-scheduling-surface/schema/payloads/delivery.lesson-hold.created-v1.json +115 -0
  85. package/contracts/sales-scheduling-surface/validation/composite-hold-create.md +97 -0
  86. package/contracts/sales-scheduling-surface/validation/lock-state-machine-conformance.md +84 -0
  87. package/contracts/sales-scheduling-surface/validation/sales-close-orchestration.md +77 -0
  88. package/contracts/warehouse-silver/README.md +118 -0
  89. package/contracts/warehouse-silver/coaching-utilization-columns.md +105 -0
  90. package/dist/events.d.ts +63 -0
  91. package/dist/events.js +293 -0
  92. package/dist/index.d.ts +2 -0
  93. package/dist/index.js +7 -1
  94. package/dist/postgres-consumer.js +2 -1
  95. package/dist/validator.js +1 -0
  96. package/package.json +1 -1
@@ -0,0 +1,433 @@
1
+ # Credit Reservation Lock Contract
2
+
3
+ **Status:** v1.4.1
4
+ **Date:** 2026-05-19
5
+ **Owner:** Platform (contract); Revenue (implementation)
6
+ **Consumers:** Delivery (primary subscriber for day-of and handoff signals), Sales (subscribes to `customer.handoff` to close out Leads), Platform (warehouse ingestion), Growth (funnel close), Coaching (lock-aware availability projection on `credit.reserved`, `credit.locked`, `credit.released`, `credit.forfeited` per ADR-0008)
7
+ **Related ADRs:** ADR-0001 (tenant_id), ADR-0002 (canonical entity ID template), ADR-0003 (Person canonical, customer.handoff trigger), ADR-0005 (event envelope), ADR-0006 (state machine decision)
8
+ **Related contracts:** Identity Contract v1.0.0 (`../identity/README.md`), Event Envelope Contract v1.0.0 (`../event-envelope/README.md`), Sales Scheduling Surface Contract v1.2.0 (`../sales-scheduling-surface/README.md`)
9
+ **Sub-specs (authoritative):** `reservation-create-api.md`, `reservation-release-api.md`, `delivery-state-vocabulary.md`
10
+
11
+ ## 1. Purpose and scope
12
+
13
+ This contract specifies the credit reservation lock state machine: the lifecycle of a credit reservation from the moment a lesson is scheduled through to its terminal outcome (consumed, released, or forfeited). It defines the states, the transitions, the events emitted, the credit ledger entries posted at each step, and the special `customer.handoff` behavior on first lock for a Person.
14
+
15
+ Out of scope: how credits are priced, how packs are sold, how invoices are generated, how scheduling is implemented, how cancellation deadlines are configured per-Org or per-tier (Revenue policy), how Refund Debit and other ledger entry types not directly involved in the lock lifecycle are produced.
16
+
17
+ ## 2. Normative language
18
+
19
+ The key words MUST, MUST NOT, REQUIRED, SHALL, SHALL NOT, SHOULD, SHOULD NOT, and MAY are to be interpreted per RFC 2119.
20
+
21
+ ## 3. Terminology
22
+
23
+ - **Credit.** A unit of coach time. Default 10 minutes per credit. A 30-minute lesson costs 3 credits; a 60-minute lesson costs 6. The credit count for any reservation is denoted **N** in this contract; N is a positive integer determined by the lesson's duration (or by the offering's reduced credit price for trial lessons).
24
+ - **Credit Account.** The Revenue record holding a Person's available credits, identified by `crd_acct_<UUID v7 canonical>` (reserved). One Credit Account per Person within a tenant.
25
+ - **Reservation.** A claim that a credit account makes against a specific scheduled lesson. Identified by `crr_<UUID v7 canonical>` in Revenue. Reduces available balance but does not post a ledger entry until lock.
26
+ - **Lock.** The state where a Reservation Lock Debit ledger entry has posted (at T-24h on a funded reservation). Customer can no longer cancel without forfeit; Sguild-side cancellation can still release.
27
+ - **Available balance.** `credit_account.balance − sum_of_open_reservations.N`. The credit count available for new reservations after accounting for already-claimed reservations.
28
+ - **Lesson.** A scheduled service-delivery event in Delivery. Identified by `les_<UUID v7 canonical>` (reserved per ADR-0002).
29
+ - **Cancellation deadline.** The configurable cutoff for customer-initiated cancellations. Default 24 hours pre-lesson; per-Org and per-tier overrides allowed in Revenue policy.
30
+
31
+ ## 4. State machine
32
+
33
+ ### 4.1 Lifecycle states
34
+
35
+ The state machine has two active states and three terminal states:
36
+
37
+ | State | Type | Description |
38
+ |---|---|---|
39
+ | `reserved` | active | Claim recorded against credit account. No ledger entry posted yet. Anyone can cancel without forfeit. |
40
+ | `locked` | active | T-24h passed on a funded reservation. Reservation Lock Debit entry posted. Customer can no longer cancel without forfeit; Sguild-side cancellation can still release. |
41
+ | `consumed` | terminal | Lesson delivered. Lock dissolved (Adjustment with reason "Credits Consumed") and Lesson Debit posted. Net account impact: −N credits. |
42
+ | `released` | terminal | Cancellation reversed the reservation. Net account impact: 0 credits (lock dissolved if any, no debit). Several initiator/reason flavors. |
43
+ | `forfeited` | terminal | Customer cancelled after lock or no-show. Lock dissolved (Adjustment with reason "Credits Forfeited") and Credit Forfeit posted. Net account impact: −N credits. |
44
+
45
+ ### 4.2 Funding sub-state on reservation
46
+
47
+ A reservation in the `reserved` state has a funding sub-state, separate from the lifecycle state:
48
+
49
+ - `pending`: insufficient credits in the account, invoice not yet paid.
50
+ - `funded`: sufficient credits in the account OR invoice paid.
51
+
52
+ The funding sub-state is what the T-24h lock check reads to decide `reserved → locked` vs `reserved → released (system_unpaid)`. The funding sub-state has no meaning after the reservation enters a terminal state.
53
+
54
+ ### 4.3 Transitions
55
+
56
+ | From | To | Trigger | Producer | Event emitted |
57
+ |---|---|---|---|---|
58
+ | (start) | `reserved` | Lesson scheduled, claim recorded | Revenue | `credit.reserved` |
59
+ | `reserved` | `reserved` (funded) | Funding settles (credits become sufficient OR invoice paid) | Revenue | `credit.funded` |
60
+ | `reserved` | `locked` | T-24h auto-job, reservation is `funded` | Revenue | `credit.locked` (and `customer.handoff` if first lock for Person) |
61
+ | `reserved` | `released` (system_unpaid) | T-24h auto-job, reservation is `pending` | Revenue | `credit.released` |
62
+ | `reserved` | `released` (any other initiator) | Cancellation arrives before T-24h | Revenue | `credit.released` |
63
+ | `locked` | `consumed` | Lesson delivery event from Delivery | Revenue | `credit.consumed` |
64
+ | `locked` | `released` | Sguild-side cancellation (admin, coach, weather, logistics) OR administrative void after lock | Revenue | `credit.released` |
65
+ | `locked` | `forfeited` | Customer-initiated cancellation after lock OR no-show | Revenue | `credit.forfeited` |
66
+
67
+ All terminal states are absorbing.
68
+
69
+ ### 4.4 Reschedule policy
70
+
71
+ Customer-initiated rescheduling of a `locked` lesson is NOT a transition path. The state machine has no `locked → reserved` transition. To reschedule a locked lesson, the customer must forfeit the existing lock and create a new reservation for the new lesson, which will independently progress through the state machine.
72
+
73
+ Sguild-initiated rescheduling (admin moves the lesson) is a `locked → released` (Sguild-side) followed by a new `(start) → reserved` for the rescheduled lesson. The customer keeps their credits and gets a fresh reservation.
74
+
75
+ Rescheduling before T-24h (while still `reserved`) follows the same pattern: cancel the current reservation (released, no ledger entries), create a new reservation for the new lesson.
76
+
77
+ ## 5. Customer handoff coupling
78
+
79
+ When `credit.locked` fires AND it is the FIRST lock for a Person across all credits and lessons, Revenue SHALL emit `customer.handoff` immediately after `credit.locked`. This is the Sales-to-Delivery handoff per the amended ADR-0003.
80
+
81
+ `customer.handoff` payload v1:
82
+ - `person_id`
83
+ - `first_lesson_id`
84
+ - `credit_reservation_id`
85
+ - `handoff_at`
86
+
87
+ Idempotency: Revenue tracks per-Person "first lock fired" state. If the dispatcher redelivers `credit.locked` because a consumer crashed, the side-effect `customer.handoff` SHALL NOT be re-emitted.
88
+
89
+ ### 5.1 Handoff is irrevocable
90
+
91
+ Once the FIRST lock fires for a Person, `customer.handoff` is emitted and Delivery owns that human's daily-touch responsibility. The handoff is not retracted by the lock's terminal state:
92
+
93
+ - If the first lock transitions to `released` (Sguild-side cancellation or administrative void), the lock state machine completes for that lesson but the Person remains Delivery-managed.
94
+ - If the first lock transitions to `forfeited`, the same applies; the handoff stands.
95
+ - A customer whose first lesson was forfeited or cancelled and never rescheduled is an Delivery-managed customer in a stalled state. Delivery retains responsibility for re-engagement, not Sales.
96
+
97
+ The handoff signal is the lock-fired event, not the lock outcome. This matches the late-handoff business intent: Sales' incentive aligns with leads who became real enough to have a lock fire on them.
98
+
99
+ ### 5.2 Free vs paid trials
100
+
101
+ Trial lessons are not free; they are sold at a reduced credit price specified in the offering tied to the order. The reservation for a trial uses the trial's N value (smaller than a regular lesson) but otherwise progresses through the state machine identically. `customer.handoff` fires on the first lock regardless of whether that first lesson is a trial or a regular lesson.
102
+
103
+ ### 5.3 Group lessons
104
+
105
+ A lesson with multiple participants charges exactly one credit account: the buyer's. There is no per-participant credit cost. The reservation, the lock, and the lifecycle are all single-account. Multiple participants share the same `lesson_id` but only the buyer's `credit_account` has a reservation against it.
106
+
107
+ ## 6. Cancellation policy interface
108
+
109
+ Revenue queries its own cancellation policy at the moment of cancellation to decide the resulting transition. The policy is not purely time-based; it considers initiator, timing, and reason jointly.
110
+
111
+ ```
112
+ cancellationPolicy(
113
+ person_id,
114
+ lesson_id,
115
+ cancelled_at,
116
+ lesson_starts_at,
117
+ initiator, // "customer" | "admin" | "coach" | "system_weather" | "system_logistics" | "system_unpaid" | "system_other"
118
+ reason_code // see credit.released / credit.forfeited payload enums
119
+ ) → { state: "released" | "forfeited", reversal_reason: "Credits Released" | "Administrative Void" | "Credits Forfeited" }
120
+ ```
121
+
122
+ Default behavior:
123
+
124
+ - **Sguild-initiated cancellations** (initiator ∈ {`admin`, `coach`, `system_weather`, `system_logistics`, `system_unpaid`, `system_other`}): always `released`. The reversal_reason is `Credits Released` for normal operational cancellations, `Administrative Void` for admin overrides or data corrections.
125
+ - **Customer-initiated cancellation arriving 24+ hours before lesson start**: `released` with reversal_reason `Credits Released`.
126
+ - **Customer-initiated cancellation arriving inside the 24-hour window**: `forfeited` with reversal_reason `Credits Forfeited`.
127
+
128
+ The 24-hour customer cancellation deadline is the Revenue policy default; per-Organization, per-program-type, or per-customer-tier overrides may shorten or lengthen the window. The state machine treats the policy as a black-box query.
129
+
130
+ ### 6.1 Auto-release versus explicit-operator subsets (v1.1.0)
131
+
132
+ The `reason_code` parameter on `cancellationPolicy` is structured into two subsets that determine whether the resulting `→ released` transition fires automatically or requires explicit operator approval at the lock-state-machine boundary. The distinction is encoded in the §9.6 `credit.released` payload's `reason_code` field at schema_version 2 so the refund flow can route on it without re-deriving from `initiator` or operator notes.
133
+
134
+ **Auto-release subset** (Delivery system-initiated transitions; no operator approval at the lock-state-machine boundary, though Phase-2 narrowing may surface declaration-layer operator actions for some values):
135
+
136
+ - `site_closure`. The lesson site closed for a program-wide event Delivery declares.
137
+ - `coach_unavailable_reschedule_failed`. Delivery's reschedule attempts have failed within the program's reschedule policy window. The window is configured per-program but the per-reservation transition is automatic once the window expires.
138
+ - `force_majeure`. A non-weather event-class cancellation Delivery declares (regional emergencies, declared health events, regulatory holds). The per-reservation transition is automatic; the declaration step itself is operator-driven and lives at the Delivery scheduling layer rather than on the credit-reservation-lock event surface.
139
+ - `weather`. Weather-induced cancellation per the program's weather policy.
140
+ - `administrative_void`. System-error or duplicate-reservation void. The Sguild-side equivalent of a typo correction.
141
+
142
+ **Explicit-operator subset** (operator confirms refund eligibility against program rules before the transition fires):
143
+
144
+ - `customer_requested_in_window`. Customer requested cancellation, operator confirmed the request is within the program's cancellation window and refund eligibility holds.
145
+ - `customer_requested_exception`. Customer requested cancellation outside the program's cancellation window, operator approved a refund as a policy exception. The split between `_in_window` and `_exception` makes the policy decision visible in the event payload for downstream reconciliation.
146
+ - `policy_exception`. Operator-applied refund exception not driven by a customer cancellation request.
147
+ - `bad_debt_writeoff`. Revenue-initiated bad-debt write-off processed through Delivery's lock-state cancellation interface. The originator is Revenue's reconciliation flow; the Delivery operator confirms the lock-state transition does not collide with in-flight changes on the lesson record rather than independently deciding bad-debt status.
148
+
149
+ Consumers SHALL treat unknown `reason_code` values as safe-to-ignore per the additive-discipline rule in §11. New values may be added in Phase-2 narrowing as policy work surfaces specifics; the additive-discipline accommodates the additions without consumer breakage.
150
+
151
+ `reason_code` is a separate field from `initiator` and `reversal_reason`. `initiator` records who initiated the cancellation. `reversal_reason` records the credit-accounting category. `reason_code` records the cancellation cause in a refund-flow-actionable way. Typical combinations on `credit.released` v2: `initiator=system_weather, reason_code=weather, reversal_reason=Credits Released`; `initiator=admin, reason_code=customer_requested_in_window, reversal_reason=Credits Released`; `initiator=admin, reason_code=administrative_void, reversal_reason=Administrative Void`.
152
+
153
+ ## 7. Credit ledger entry types
154
+
155
+ The credit account ledger uses the following Entry Type enum:
156
+
157
+ | Entry Type | Sign | Description |
158
+ |---|---|---|
159
+ | `Purchase Credit` | +N | Customer purchase of credits, posted by Payment Job |
160
+ | `Lesson Debit` | −N | Lesson delivered, posted by Lesson Completion Job |
161
+ | `Refund Debit` | −N | Customer refund of credits, posted by Refund Job (out of scope for this contract) |
162
+ | `Adjustment` | ±N | Programmatic or manual correction; carries a `reason_code` |
163
+ | `Reservation Lock Debit` | −N | Lock posted at T-24h, posted by Reservation Job |
164
+ | `Credit Forfeit` | −N | Customer-side forfeiture, posted by Forfeit Job |
165
+
166
+ Every ledger entry SHALL also carry a `Created Via` field with one of: `Payment Job`, `Lesson Completion Job`, `Forfeit Job`, `Reservation Job`, `Refund Job`, `Manual`, `Backfill`.
167
+
168
+ ### 7.1 Adjustment reason codes
169
+
170
+ The `Adjustment` entry type carries a `reason_code`. The four codes used at lock dissolution are:
171
+
172
+ | Reason | Used at | Paired entry |
173
+ |---|---|---|
174
+ | `Credits Consumed` | `locked → consumed` | Lesson Debit (−N), same Created Via (Lesson Completion Job) |
175
+ | `Credits Forfeited` | `locked → forfeited` | Credit Forfeit (−N), same Created Via (Forfeit Job) |
176
+ | `Credits Released` | `locked → released` (Sguild operational) | none |
177
+ | `Administrative Void` | `locked → released` (admin override or data correction) | none |
178
+
179
+ Additional `reason_code` values may be added for non-lock adjustments (goodwill credits, extenuating-circumstance credits, data corrections, expiration writeoffs, etc.) in v1.x without contract bumps. The four lock-resolution reasons listed are the only `reason_code` values that this contract specifies normatively; others are Revenue-internal.
180
+
181
+ ### 7.2 Lock dissolution invariant
182
+
183
+ For every transition out of `locked`, exactly one Adjustment entry of type "Lock Debit Reversal" SHALL be posted, with one of the four `reason_code` values above and a link back to the Reservation Lock Debit entry it reverses. The Adjustment posts +N to balance. Lock dissolution is universal; the paired debit entry (Lesson Debit or Credit Forfeit) is conditional on the terminal state.
184
+
185
+ ## 8. Per-outcome ledger pattern
186
+
187
+ | Terminal | Entry sequence | Net balance impact |
188
+ |---|---|---|
189
+ | `reserved → released` (any pre-lock cancel) | none (no lock to reverse, no debit) | 0 |
190
+ | `reserved → released` (system_unpaid at T-24h) | none (lock never posted) | 0 |
191
+ | `locked → consumed` | Adjustment +N (reason: Credits Consumed); Lesson Debit −N | −N |
192
+ | `locked → released` (Sguild operational) | Adjustment +N (reason: Credits Released) | 0 |
193
+ | `locked → released` (admin override) | Adjustment +N (reason: Administrative Void) | 0 |
194
+ | `locked → forfeited` (customer late or no-show) | Adjustment +N (reason: Credits Forfeited); Credit Forfeit −N | −N |
195
+
196
+ A reservation's full ledger footprint is reconstructible by following the Reservation Lock Debit entry forward to its paired Adjustment and any subsequent Lesson Debit or Credit Forfeit entry.
197
+
198
+ ## 9. Events
199
+
200
+ Each event SHALL ride on the Event Envelope (per `../event-envelope/README.md`) with `producer = "revenue"`, `subject = <person_id>`, `actor = "system:revenue"` for automated transitions or `actor = <person_id>` for human-triggered transitions.
201
+
202
+ ### 9.1 `credit.purchased`
203
+
204
+ Emitted when a Purchase Credit ledger entry posts (customer pack purchase). Subscribers: Sales (close out related Leads), Growth (funnel attribution), Delivery (awareness of new credit availability).
205
+
206
+ Payload v1:
207
+ - `credit_account_id`
208
+ - `person_id`
209
+ - `purchased_credits` (N, the count added)
210
+ - `order_id` (the originating order from Revenue)
211
+ - `purchased_at`
212
+
213
+ ### 9.2 `credit.reserved`
214
+
215
+ Emitted when Revenue records a reservation claim. Subscribers: Delivery (awareness of the upcoming hold claim), Coaching (reservation awareness; coach-specific booking facts arrive on Delivery's hold-created event when `lesson_id` is absent), Platform warehouse.
216
+
217
+ Payload v1:
218
+ - `credit_reservation_id`
219
+ - `credit_account_id`
220
+ - `person_id`
221
+ - `lesson_id` (optional on Sales-originated reservations until Delivery hold-create mints `les_`)
222
+ - `organization_id`
223
+ - `reserved_credits` (N)
224
+ - `reserved_at`
225
+ - `lesson_starts_at`
226
+
227
+ ### 9.3 `credit.funded`
228
+
229
+ Emitted when funding sub-state flips from `pending` to `funded`. Subscribers: Sales (signal that customer is committing financially), Delivery.
230
+
231
+ Payload v1:
232
+ - `credit_reservation_id`
233
+ - `funding_source` (enum: `credits_available`, `invoice_paid`)
234
+ - `funded_at`
235
+
236
+ ### 9.4 `credit.locked`
237
+
238
+ Emitted when the reservation enters `locked` state at T-24h. Subscribers: Delivery (day-of preparation), Sales (close Lead via the customer.handoff sidecar), Coaching (state-transition only; the slot stays subtracted in the availability projection).
239
+
240
+ Payload v1:
241
+ - `credit_reservation_id`
242
+ - `lesson_id`
243
+ - `person_id`
244
+ - `locked_credits` (N, posted as Reservation Lock Debit)
245
+ - `locked_at`
246
+
247
+ ### 9.5 `credit.consumed`
248
+
249
+ Emitted at delivery. Subscribers: Delivery (post-delivery follow-up), Revenue (revenue recognition), Platform warehouse.
250
+
251
+ Payload v1:
252
+ - `credit_reservation_id`
253
+ - `lesson_id`
254
+ - `consumed_credits` (N)
255
+ - `consumed_at`
256
+
257
+ ### 9.6 `credit.released`
258
+
259
+ Emitted on `→ released` transition from any source. Subscribers: Delivery, Sales (if pre-lock release of a Lead's first reservation), Coaching (unsubtracts the slot in the availability projection), Platform warehouse.
260
+
261
+ Payload v1:
262
+ - `credit_reservation_id`
263
+ - `credit_account_id`
264
+ - `released_credits` (N)
265
+ - `initiator` (enum: `customer`, `admin`, `coach`, `system_weather`, `system_logistics`, `system_unpaid`, `system_other`)
266
+ - `reversal_reason` (enum: `Credits Released`, `Administrative Void`)
267
+ - `reason_note` (optional free-form for context)
268
+ - `cancelled_at`
269
+
270
+ Payload v2 (additive over v1; v1 consumers continue to work):
271
+ - All v1 fields above.
272
+ - `reason_code` (optional, enum). Captures the cancellation cause separate from who initiated and from the credit-accounting category. Values are partitioned into the auto-release and explicit-operator subsets per §6.1. Auto-release values: `site_closure`, `coach_unavailable_reschedule_failed`, `force_majeure`, `weather`, `administrative_void`. Explicit-operator values: `customer_requested_in_window`, `customer_requested_exception`, `policy_exception`, `bad_debt_writeoff`. Consumers SHALL treat unknown values as safe-to-ignore per §11.
273
+
274
+ Producers SHALL emit `reason_code` from the v1.1.0 cutover date (per §16's change log entry); before the cutover date, the field is omitted and consumers see v1 semantics. Consumers MAY rely on `reason_code` from the cutover date. Consumers gating on payload schema use `schema_version >= 2` per the event envelope contract's per-event-type versioning.
275
+
276
+ ### 9.7 `credit.forfeited`
277
+
278
+ Emitted on `locked → forfeited`. Subscribers: Delivery (re-engagement workflow), Revenue (recognition of forfeited credits as recognized revenue per policy), Coaching (unsubtracts the slot in the availability projection), Platform warehouse.
279
+
280
+ Payload v1:
281
+ - `credit_reservation_id`
282
+ - `lesson_id`
283
+ - `forfeited_credits` (N)
284
+ - `forfeiture_reason` (enum: `late_cancel`, `no_show`)
285
+ - `forfeited_at`
286
+
287
+ Forfeiture is always customer-side. Sguild-initiated cancellations emit `credit.released`, never `credit.forfeited`, regardless of timing.
288
+
289
+ ## 10. Auto-transitions and scheduled jobs
290
+
291
+ Three Revenue-owned scheduled jobs implement the state machine's time-based transitions:
292
+
293
+ **Reservation Job (T-24h lock check).** Runs at T-24h before each scheduled lesson. For each `reserved` reservation: if `funded`, transition to `locked` and post the Reservation Lock Debit entry; if `pending`, transition to `released` with `initiator=system_unpaid` and post no ledger entry.
294
+
295
+ **Lesson Completion Job.** Runs after lesson delivery (triggered by Delivery' `lesson.delivered` event). Transitions `locked → consumed`. Posts the Adjustment entry (reason Credits Consumed) and the Lesson Debit entry.
296
+
297
+ **Forfeit Job.** Runs after a no-show is recorded by Delivery or after a customer late-cancellation is processed. Transitions `locked → forfeited`. Posts the Adjustment entry (reason Credits Forfeited) and the Credit Forfeit entry.
298
+
299
+ Each job SHALL be idempotent: if it runs twice for the same reservation, it produces the same ledger footprint. Idempotency keys SHALL include the `credit_reservation_id` and the target terminal state.
300
+
301
+ If any job fails, retries SHALL re-run the job atomically; partial state is not allowed (e.g., Adjustment posted but Lesson Debit missing must be reconciled before the lock is considered dissolved).
302
+
303
+ ### 10.1 Auto-release subset cascades (v1.1.0)
304
+
305
+ The five auto-release `reason_code` values in §6.1 (`site_closure`, `coach_unavailable_reschedule_failed`, `force_majeure`, `weather`, `administrative_void`) name cancellation causes whose per-reservation transitions fire automatically without operator approval at the lock-state-machine boundary. The auto-release subset cascades through the existing `reserved → released` and `locked → released` transitions in §4.3; this section does not add new transitions.
306
+
307
+ Two notes on the operator's role with auto-release values:
308
+
309
+ For `force_majeure` and `site_closure`, the declaration step (the operator declaring an event force-majeure for a program, site, region, or date range; the operator declaring a site closure) is operator-driven and lives at the Delivery scheduling layer. Once declared, per-reservation cancellations cascade automatically and emit on the credit-reservation-lock event surface with the corresponding `reason_code`. The declaration itself does not emit on the credit-reservation-lock surface in v1.1.0; if Phase-2 narrowing surfaces a need to expose the declaration event downstream, the additive-discipline accommodates a future `force_majeure_declared` or `site_closure_declared` value.
310
+
311
+ For `coach_unavailable_reschedule_failed`, the determinism is configuration-bounded by the program's reschedule policy window. The window is operator-configured per-program; the per-reservation transition is automatic once the window expires.
312
+
313
+ The four explicit-operator `reason_code` values (`customer_requested_in_window`, `customer_requested_exception`, `policy_exception`, `bad_debt_writeoff`) require operator confirmation of refund eligibility before the transition fires. The operator workflow for confirmation is Delivery-internal and out of scope for this contract.
314
+
315
+ ## 11. Versioning policy
316
+
317
+ This contract follows the same semver discipline as the Identity Contract and the Event Envelope Contract:
318
+
319
+ - **Patch:** editorial clarifications.
320
+ - **Minor:** additive changes. New optional payload fields, new transition triggers that map to existing transitions, new enum values for `initiator`, `reversal_reason`, `forfeiture_reason`, `Adjustment.reason_code`, or `Created Via`.
321
+ - **Major:** breaking changes. Removing or renaming states, removing transitions, removing entry types, breaking payload changes.
322
+
323
+ State additions or removals are major version bumps. Adding a new entry type to the Entry Type enum is minor (additive). Adding a new Adjustment reason code is minor.
324
+
325
+ Cancellation deadline policy is NOT versioned by this contract; it is a Revenue policy parameter that can change without contract bumps.
326
+
327
+ ## 12. Consumer responsibilities
328
+
329
+ ### 12.1 Idempotency
330
+ Consumers MUST be idempotent over `event_id` per the Event Envelope contract.
331
+
332
+ ### 12.2 First-lock detection trust
333
+ Delivery and Sales SHALL trust Revenue's `customer.handoff` emission as the authoritative first-lock signal. Consumers SHALL NOT independently compute "is this Person's first lock"; that determination belongs to Revenue.
334
+
335
+ ### 12.3 State machine state is Revenue's
336
+ Consumers MAY observe state via events but the authoritative state of any reservation is Revenue's. To query current state, consumers call Revenue's API; consumers SHALL NOT maintain their own copy of reservation state for authoritative use.
337
+
338
+ ### 12.4 Cancellation submission
339
+ Cancellation events from Delivery or Sales (a customer cancels through their portal, an ops staff cancels on behalf of a customer) SHALL be submitted to Revenue as cancellation requests with `initiator` and `reason_code`. Revenue runs the state machine and emits the resulting transition event. Consumers SHALL NOT directly emit `credit.released` or `credit.forfeited`. The Sales-callable reservation-release API is specified in `reservation-release-api.md`.
340
+
341
+ ### 12.6 Reservation creation
342
+
343
+ Sales-originated close orchestration SHALL create the Revenue credit reservation through the Sales-callable reservation-create API specified in `reservation-create-api.md` before calling Delivery hold-create. The endpoint returns a Revenue `crr_` in lifecycle state `reserved`; it does not create a Delivery lesson hold or advance Delivery's lesson-hold state.
344
+
345
+ ### 12.7 Delivery state vocabulary
346
+
347
+ Revenue lifecycle states and Delivery lesson-hold states are distinct vocabularies. The authoritative mapping is specified in `delivery-state-vocabulary.md`. Consumers SHALL NOT describe a Revenue `crr_` as being in Delivery `requested`, `held`, or `confirmed` state.
348
+
349
+ ### 12.5 Reschedule pattern
350
+ Consumers wanting to reschedule a `locked` lesson SHALL submit a forfeit request followed by a new reservation, not a reschedule operation against the state machine.
351
+
352
+ ### 12.8 Atomic multi-create
353
+
354
+ Sales-originated composite offers MAY use `POST /api/v1/reservations/atomic-multi-create` to create N reservations in a single Revenue transaction. Each item in the batch results in an independent `crr_` with its own lifecycle state, funding sub-state, lock job, release path, refund path, and ledger footprint. No composite reservation state or bundle-level ledger entry is introduced.
355
+
356
+ Consumers:
357
+
358
+ - SHALL submit one `Idempotency-Key` header scoped by `organization_id` for the whole batch.
359
+ - SHALL include `originating_offer_id` once on the request envelope. Each item SHALL include a unique `offer_item_id` for per-item correlation. Consumers SHALL NOT repeat or disagree on `organization_id` or `person_id` across items; those fields live only on the envelope.
360
+ - SHALL treat the endpoint as all-or-none on Revenue's side. If the endpoint returns a non-2xx response, none of the Revenue reservations were created and no `credit.reserved` events were emitted.
361
+ - SHALL NOT assume that a successful Revenue multi-create atomically includes Delivery hold-creates. The Revenue-to-Delivery boundary remains a cross-service orchestration boundary; Sales owns the orchestration cleanup if Revenue multi-create succeeds but a subsequent Delivery hold-create fails (using the existing single-reservation release API per `reservation-release-api.md` for each returned `crr_`).
362
+ - SHALL NOT use per-item idempotency keys. The single batch-level key guarantees all-or-none replay semantics; per-item keys would imply partial replay, which this endpoint does not support.
363
+
364
+ HTTP 200 `result: "existing"` means all N lessons already had active Revenue reservations (idempotent replay). HTTP 201 `result: "created"` means all N were newly created. HTTP 409 `conflict_reason: "partial_existing_state"` means some lessons already had active reservations and some did not; Sales must investigate and not retry automatically.
365
+
366
+ ## 13. Producer responsibilities (Revenue)
367
+
368
+ ### 13.1 State machine implementation
369
+ Revenue SHALL implement the state machine exactly as specified.
370
+
371
+ ### 13.2 Event emission
372
+ Revenue SHALL emit exactly one event per state transition, on the Event Envelope, with the payload defined in §9.
373
+
374
+ ### 13.3 Ledger entry posting
375
+ Revenue SHALL post the ledger entries specified in §8 for each state transition, with correct Entry Type, Created Via, signed amount, and references. Lock dissolution (Adjustment with reason) is universal at every `locked → terminal` transition.
376
+
377
+ ### 13.4 First-lock detection
378
+ Revenue SHALL track per-Person "first lock fired" state and emit `customer.handoff` exactly once per Person, on the first `credit.locked`.
379
+
380
+ ### 13.5 Scheduled jobs
381
+ Revenue SHALL run the Reservation Job, Lesson Completion Job, and Forfeit Job per §10. Each job SHALL be idempotent.
382
+
383
+ ### 13.6 Cancellation policy
384
+ Revenue SHALL maintain the cancellation policy as a black-box query. Per-Org and per-tier overrides are policy-internal.
385
+
386
+ ### 13.7 Idempotent emission
387
+ Revenue SHALL NOT emit duplicate events for the same state transition. Retries reuse the same `event_id`.
388
+
389
+ ### 13.9 Atomic multi-create (v1.4.0)
390
+
391
+ Revenue SHALL implement `POST /api/v1/reservations/atomic-multi-create` per §12.8. The implementation constraints are:
392
+
393
+ - Revenue SHALL wrap all N `creditReservation.create` writes and all N `credit.reserved` event publishes in a single Prisma transaction. A validation failure on any item before the transaction opens rolls back all writes and no events emit.
394
+ - Revenue SHALL accept `originating_offer_id` and per-item `offer_item_id` / `client_item_id` on the request and MAY persist them as audit correlation fields on the reservation record. Revenue SHALL NOT surface `originating_offer_id` on the `credit.reserved` event payload unless a later minor version adds it with a demonstrated consumer need.
395
+ - Revenue SHALL enforce all-or-none idempotency at the batch level: same `Idempotency-Key` + same normalized batch payload returns the same full array. Same key + different normalized payload returns HTTP 409 with `conflict_reason: idempotency_payload_mismatch`.
396
+ - Revenue SHALL NOT permit batch sizes larger than 20 items.
397
+ - `customer.handoff` first-lock behavior is unchanged: if multiple reservations for the same Person lock in the same Reservation Job run, Revenue's first-lock implementation SHALL choose the earliest `lesson_starts_at` as the deterministic first lock, with `credit_reservation_id` as the tiebreaker. The `customer.handoff` payload remains singular and unchanged from v1.0.0.
398
+
399
+ ### 13.8 reason_code emission on credit.released v2 (v1.1.0)
400
+
401
+ From the cutover date specified in §16's v1.1.0 change log entry, Revenue SHALL include the `reason_code` field on every `credit.released` envelope at payload schema_version 2, populated from one of the values in §6.1. The value SHALL be drawn from the cancellation policy's input alongside `initiator` and `reversal_reason`. Before the cutover date, Revenue continues to emit at schema_version 1 with `reason_code` omitted; consumer behavior follows the additive-discipline pattern in §11.
402
+
403
+ Revenue's `cancellationPolicy` (§6) accepts `reason_code` as an input and routes the resulting transition state alongside `initiator` and `reversal_reason`. The policy backend's mapping from `reason_code` to the resulting transition state is Revenue-internal; the contract surface is the input-and-output shape of the policy plus the emission obligation.
404
+
405
+ ## 14. Security and privacy
406
+
407
+ ### 14.1 PII in payloads
408
+ Payloads carry `person_id`, `credit_reservation_id`, and, once Delivery has minted it, `lesson_id`, all pseudonymous. No contact information; comms routing for related notifications goes through Platform's identity service Guardian-aware comms-routing endpoint.
409
+
410
+ ### 14.2 Tenant isolation
411
+ All state machine operations are tenant-scoped. Locks, credits, lessons, and Persons all carry `tenant_id`.
412
+
413
+ ### 14.3 Audit trail
414
+ The state machine's events plus the ledger entries together form the audit trail. Replaying the events for a Person reconstructs the credit lifecycle.
415
+
416
+ ## 15. Future work
417
+
418
+ - **Partial forfeiture or partial release.** If policy evolves to support partial outcomes (e.g., 50% forfeit), payloads gain amount fields additively.
419
+ - **Pack expiration.** Currently no expiration. If introduced later, requires lot-level accounting (a non-trivial v1.x or v2 change).
420
+ - **Multi-credit-account billing for group lessons.** Currently single-account per lesson. If group billing models change, the contract needs extension.
421
+ - **Subscription credits.** If credit replenishment becomes recurring (subscription model rather than packs), the Purchase Credit entry type and `credit.purchased` event may need additional fields.
422
+ - **Reschedule-as-first-class operation.** If forfeit-and-rebook becomes too punitive operationally, a `locked → reserved` transition might be introduced for limited reschedule scenarios. Defer until policy demands.
423
+
424
+ ## 16. Change log
425
+
426
+ - **v1.4.1** (2026-05-19): Patch. Corrects the default credit unit from 5 minutes per credit to 10 minutes per credit. A 30-minute lesson costs 3 credits and a 60-minute lesson costs 6. No lifecycle state, transition, event payload, ledger entry type, funding sub-state, cancellation policy, lock timing, or `customer.handoff` behavior changes.
427
+ - **v1.4.0** (2026-05-19): Additive minor. Adds `POST /api/v1/reservations/atomic-multi-create` for Sales-originated composite offers (§12.8, §13.9). The endpoint wraps N Revenue reservation creates in a single Prisma transaction, guaranteeing all-or-none semantics on Revenue's side. Each item results in an independent `crr_` with the full existing lifecycle. Adds `originating_offer_id` on request envelope and per-item `offer_item_id` / `client_item_id` for composite-offer correlation. No new `credit.*` event types, no new lifecycle states, no ledger entry type changes, no `customer.handoff` behavior change (first-lock determinism tiebreaker named in §13.9 for the multi-lock-in-same-job-run case: earliest `lesson_starts_at`, then `credit_reservation_id`). Revenue ack memo: `coordination/memos/2026/2026-05-18-revenue-composite-offer-reservation-position.md`. Implementation: `src/app/api/v1/reservations/atomic-multi-create/route.ts`.
428
+ - **v1.3.0** (2026-05-16): Additive minor. Resolves the Sales, Delivery, and Revenue lesson-id sequencing contradiction on the Sales close path. Revenue reservation-create remains before Delivery hold-create, but `lesson_id` is optional on the reservation-create API and optional on `credit.reserved` v1 until Delivery mints the Delivery-owned `les_` during hold-create. Delivery's `delivery.lesson-hold.created` event, or an explicit reconciliation write, is the attachment point for adding `lesson_id` back to Revenue state. Memos: `2026-05-16-sales-revenue-reservation-create-lesson-id-sequencing-ask`, `2026-05-16-delivery-reservation-create-lesson-id-sequencing-answer`, `2026-05-16-revenue-reservation-create-lesson-id-sequencing-ack`.
429
+ - **v1.2.1** (2026-05-16): Patch. Changes the default reservation lock and customer cancellation deadline from T-48h to T-24h. The lifecycle states, transition set, payload fields, event names, ledger entry types, lock-dissolution invariant, and first-lock `customer.handoff` coupling do not change. Revenue remains responsible for per-Org and per-tier policy overrides through the cancellation policy black box.
430
+ - **v1.2.0** (2026-05-16): Additive minor. Adds three authoritative sub-specs: `reservation-create-api.md` for the Sales-callable reservation-create endpoint, `reservation-release-api.md` for the Sales-callable cancellation or release submission endpoint, and `delivery-state-vocabulary.md` for the mapping between Revenue lifecycle states and Delivery lesson-hold states. Updates §3 to use Revenue's live `crr_` reservation prefix, adds §12.6 and §12.7, and amends §12.4 to point cancellation submissions at the release API sub-spec. No event payload, ledger-entry, or lifecycle transition changes.
431
+ - **v1.1.0** (2026-05-08): Additive minor. Adds the `reason_code` field to `credit.released` payload at schema_version 2, partitioned into auto-release and explicit-operator subsets per §6.1. Adds §6.1 (auto-release versus explicit-operator subsets), §10.1 (auto-release subset cascades), and §13.8 (reason_code emission on credit.released v2). Producers SHALL emit `reason_code` from the cutover date 2026-05-22 (v1.1.0 publish date plus 14 days); before the cutover, producers continue to emit at schema_version 1 with `reason_code` omitted. Consumers MAY rely on `reason_code` from the cutover date. v1 consumers continue to work indefinitely; the new field is safe-to-ignore per §11. ADR-0006 is amended in parallel with a dated note under Decision; original body preserved. Phase-2 narrowing of the enum (tightening permissive values, retiring unused ones, adding values operators surface during the policy work) lands as later v1.x patches under the additive-discipline. Memos: `2026-04-28-revenue-refund-against-canceled-lessons` (the original gap), `2026-05-02-delivery-refund-cancellation-reason-codes` (Delivery's proposal), `2026-05-08-revenue-refund-reason-codes-signoff` (Revenue's signoff with reframings), `2026-05-08-delivery-reason-codes-signoff-accepted` (Delivery's confirmation closing the Phase-1 contract-shape conversation).
432
+ - **v1.0.1** (2026-04-27): Editorial. Replaced "Operations" with "Delivery" throughout to match the renamed domain (Operations was briefly canonical earlier in 2026 and was renamed back to Delivery on 2026-04-27 to avoid collision with the company-level Operations function). No state, transition, event, payload, ledger entry type, reason code, or job behavior changed. Consumers running against v1.0.0 are interface-compatible with v1.0.1.
433
+ - **v1.0.0** (2026-04-25): Initial release. Five-state machine (reserved, locked, consumed, released, forfeited) with funding sub-state on reserved. Six entry types in the credit ledger. Seven Created Via values. Four Adjustment reason codes for lock dissolution; additional reason codes reserved for goodwill and extenuating-circumstance adjustments without contract bumps. Seven domain events plus the customer.handoff coupling on first lock. N-parameterized credit amounts (credit-unit default corrected to 10 minutes per credit in v1.4.1; trials use reduced N per offering). Group lessons charge one credit account regardless of participant count. Rescheduling locked lessons requires forfeit + rebook.
@@ -0,0 +1,73 @@
1
+ # Credit Reservation Lock, Delivery State Vocabulary Mapping
2
+
3
+ **Status:** v1.0.0
4
+ **Date:** 2026-05-16
5
+ **Owner:** Revenue and Delivery
6
+ **Consumers:** Sales close orchestration, Delivery scheduling, Revenue ledger reconciliation, Coaching lock-aware projection
7
+ **Parent contract:** Credit Reservation Lock Contract v1.2.0
8
+
9
+ ## 1. Purpose and scope
10
+
11
+ This sub-spec reconciles two related vocabularies:
12
+
13
+ - Revenue's credit reservation lifecycle, expressed on the Revenue `crr_` record and emitted through `credit.*` events.
14
+ - Delivery's lesson-hold state, expressed on the Delivery reservation-lock or lesson-hold record and emitted through `delivery.lesson-hold.*` events.
15
+
16
+ The two vocabularies are intentionally not the same enum. They describe linked records at different ownership boundaries. Consumers SHALL use the vocabulary of the surface they are reading or writing and SHALL NOT infer one enum value as a stored value on the other domain's record.
17
+
18
+ ## 2. Revenue lifecycle states
19
+
20
+ Revenue lifecycle states are authoritative for commercial state and ledger effects:
21
+
22
+ | Revenue lifecycle state | Meaning |
23
+ |---|---|
24
+ | `reserved` | Revenue has created the credit reservation claim. No lock debit has posted. |
25
+ | `locked` | Revenue has posted the Reservation Lock Debit at the lock boundary. |
26
+ | `consumed` | Lesson delivery completed and Revenue posted the consumption ledger pattern. |
27
+ | `released` | Reservation released without final customer debit. |
28
+ | `forfeited` | Customer-side forfeiture posted. |
29
+
30
+ Revenue owns these states and the `credit.*` events that report transitions among them.
31
+
32
+ ## 3. Delivery lesson-hold states
33
+
34
+ Delivery lesson-hold states are authoritative for scheduling hold state:
35
+
36
+ | Delivery state | Meaning |
37
+ |---|---|
38
+ | `requested` | Sales has a Revenue `crr_` reservation available for Delivery hold-create, but Delivery has not yet created the lesson hold. |
39
+ | `held` | Delivery created the lesson hold against the Revenue reservation. |
40
+ | `confirmed` | Delivery sees the lesson hold as confirmed for scheduling execution. Funding has cleared from the Delivery-facing point of view. |
41
+ | `consumed` | Delivery has recorded lesson completion for the hold. |
42
+ | `released` | Delivery has released or cancelled the hold. |
43
+
44
+ Delivery owns these states and the `delivery.lesson-hold.*` events that report scheduling-side facts.
45
+
46
+ ## 4. Mapping
47
+
48
+ The common close-orchestration mapping is:
49
+
50
+ | Flow point | Revenue lifecycle | Delivery lesson-hold state | Notes |
51
+ |---|---|---|---|
52
+ | After Sales reservation-create succeeds | `reserved` | `requested` context exists for Delivery hold-create | `requested` is not stored on the Revenue `crr_`. It is the Delivery-side pre-hold condition that a live `reserved` Revenue reservation can satisfy. |
53
+ | After Delivery hold-create succeeds | `reserved` | `held` | Revenue remains `reserved`; Delivery has accepted the hold. |
54
+ | After funding clears before lock debit | `reserved` with funding state `funded` | `confirmed` or `held` depending on Delivery execution policy | Funding state is Revenue-owned. Delivery may expose `confirmed` as its scheduling state, but Revenue lifecycle is still `reserved` until lock. |
55
+ | After T-24h lock job succeeds | `locked` | `confirmed` | Revenue has posted the lock debit. Delivery state usually remains `confirmed`. |
56
+ | After lesson completion | `consumed` | `consumed` | Terminal states align by name but are still stored on different records. |
57
+ | After release before lock | `released` | `released` | No lock debit reversal is needed on the Revenue side. |
58
+ | After release after lock | `released` | `released` | Revenue posts the lock reversal pattern. |
59
+ | After customer late cancellation or no-show | `forfeited` | `released` or terminal cancellation state | Delivery does not expose `forfeited` in v1.0.0. Consumers needing forfeiture truth read Revenue. |
60
+
61
+ ## 5. Contract wording rule
62
+
63
+ Contract text SHALL NOT say that a Revenue `crr_` is in Delivery `requested`, `held`, or `confirmed` state. It SHALL say one of:
64
+
65
+ - A Revenue `crr_` is in Revenue lifecycle state `reserved`, `locked`, `consumed`, `released`, or `forfeited`.
66
+ - A Delivery lesson hold or reservation-lock record is in Delivery state `requested`, `held`, `confirmed`, `consumed`, or `released`.
67
+ - Delivery hold-create requires a live Revenue `crr_` in lifecycle state `reserved`, plus Delivery-side preconditions that allow creating a `requested → held` scheduling transition.
68
+
69
+ This wording rule prevents consumers from branching on the wrong enum and keeps ledger authority in Revenue while scheduling authority stays in Delivery.
70
+
71
+ ## 6. Versioning
72
+
73
+ Patch versions may clarify examples. Minor versions may add mapping rows for new Delivery states or Revenue lifecycle states that are additive on their parent contracts. Removing or renaming states in either parent contract is governed by that parent contract's major-version rules.