@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.
- package/README.md +4 -1
- package/contracts/README.md +30 -0
- package/contracts/coach-availability/README.md +355 -0
- package/contracts/coach-availability/README.v2.md +263 -0
- package/contracts/coach-availability/schema/payloads/coach.assigned-v1.json +91 -0
- package/contracts/coach-availability/validation/delivery-assignment.md +89 -0
- package/contracts/coach-availability/validation/sales-offer-construction.md +76 -0
- package/contracts/coaching-confirmation/README.md +96 -0
- package/contracts/coaching-confirmation/schema/payloads/coaching.lesson.confirmation_decided-v1.json +142 -0
- package/contracts/coaching-confirmation/schema/payloads/lead.coach.confirmation.requested-v1.json +124 -0
- package/contracts/credit-reservation-funding-state/README.md +147 -0
- package/contracts/credit-reservation-lock/README.md +433 -0
- package/contracts/credit-reservation-lock/delivery-state-vocabulary.md +73 -0
- package/contracts/credit-reservation-lock/reservation-create-api.md +191 -0
- package/contracts/credit-reservation-lock/reservation-release-api.md +171 -0
- package/contracts/credit-reservation-lock/schema/payloads/credit.locked-v1.json +1 -1
- package/contracts/credit-reservation-lock/schema/payloads/credit.reserved-v1.json +2 -3
- package/contracts/credit-reservation-lock/validation/lesson-lifecycle.md +318 -0
- package/contracts/event-envelope/README.md +205 -0
- package/contracts/event-envelope/schema/envelope-v1.json +2 -2
- package/contracts/event-envelope/validation/event-vocabulary.md +270 -0
- package/contracts/event-types-registry.json +337 -24
- package/contracts/external-actions/README.md +338 -0
- package/contracts/finance-mart/README.md +238 -0
- package/contracts/finance-mart/cac-payback.md +113 -0
- package/contracts/finance-mart/cash-position.md +98 -0
- package/contracts/finance-mart/cohort-summary.md +72 -0
- package/contracts/finance-mart/customer-journey-audit.md +92 -0
- package/contracts/finance-mart/ltv.md +92 -0
- package/contracts/finance-mart/margin.md +87 -0
- package/contracts/finance-mart/pnl.md +83 -0
- package/contracts/finance-mart/reconciliation.md +98 -0
- package/contracts/finance-mart/revenue-recognition-rollup.md +87 -0
- package/contracts/finance-mart/unit-economics.md +94 -0
- package/contracts/growth-warehouse-api/README.md +162 -0
- package/contracts/identity/README.md +184 -0
- package/contracts/identity/person-canonical-fields.md +120 -0
- package/contracts/identity/person-externals.md +267 -0
- package/contracts/identity/person-resolution-semantics.md +144 -0
- package/contracts/identity/person-role-taxonomy.md +120 -0
- package/contracts/identity/schema/payloads/intake.captured-v2.json +60 -0
- package/contracts/identity/schema/payloads/intake.matched-v2.json +123 -0
- package/contracts/identity/schema/payloads/person.updated-v1.json +8 -2
- package/contracts/identity/schema/payloads/role.assigned-v1.json +50 -0
- package/contracts/identity/schema/payloads/role.retired-v1.json +54 -0
- package/contracts/identity/validation/client-table.md +131 -0
- package/contracts/identity/validation/coach-handling.md +100 -0
- package/contracts/identity/validation/person-graph.md +140 -0
- package/contracts/lead-lifecycle/README.md +187 -0
- package/contracts/lead-lifecycle/schema/payloads/lead.handoff.context.recorded-v1.json +108 -0
- package/contracts/lead-lifecycle/schema/payloads/lead.qualified-v1.json +54 -0
- package/contracts/lead-lifecycle/schema/payloads/sales.lead.onboarded-v1.json +120 -0
- package/contracts/lesson-lifecycle/README.md +118 -0
- package/contracts/lesson-lifecycle/schema/payloads/lesson.cancelled-v1.json +30 -0
- package/contracts/lesson-lifecycle/schema/payloads/lesson.delivered-v1.json +29 -0
- package/contracts/lesson-lifecycle/schema/payloads/lesson.rescheduled-v1.json +157 -0
- package/contracts/lesson-lifecycle/schema/payloads/lesson.scheduled-v1.json +107 -0
- package/contracts/lesson-lifecycle/validation/README.md +5 -0
- package/contracts/mart-consumer-api/README.md +108 -0
- package/contracts/order-flow/README.md +106 -0
- package/contracts/order-flow/schema/payloads/order.created-v1.json +58 -0
- package/contracts/order-flow/schema/payloads/order.updated-v1.json +63 -0
- package/contracts/payment-flow/README.md +157 -0
- package/contracts/platform-comms/README.md +84 -0
- package/contracts/platform-comms/schema/payloads/platform.comms.inbound-v1.json +83 -0
- package/contracts/platform-geography-snapshot/README.md +205 -0
- package/contracts/platform-geography-snapshot/schema/payloads/geography.market.archived-v1.json +36 -0
- package/contracts/platform-geography-snapshot/schema/payloads/geography.market.upserted-v1.json +59 -0
- package/contracts/platform-geography-snapshot/schema/payloads/geography.service-area.archived-v1.json +36 -0
- package/contracts/platform-geography-snapshot/schema/payloads/geography.service-area.upserted-v1.json +65 -0
- package/contracts/portfolio-mart/README.md +133 -0
- package/contracts/portfolio-mart/cohort-funnel-panel.md +76 -0
- package/contracts/portfolio-mart/cross-discipline-performance.md +91 -0
- package/contracts/portfolio-mart/cross-market-performance.md +84 -0
- package/contracts/portfolio-mart/health-composites.md +88 -0
- package/contracts/portfolio-mart/org-topology.md +70 -0
- package/contracts/portfolio-mart/portfolio-level-funnel-health.md +92 -0
- package/contracts/portfolio-mart/validation/consumer-isolation.md +33 -0
- package/contracts/portfolio-mart/validation/decoupling-discipline.md +34 -0
- package/contracts/refund-flow/README.md +136 -0
- package/contracts/refund-flow/sales-callable-refund-initiation-api.md +218 -0
- package/contracts/sales-scheduling-surface/README.md +532 -0
- package/contracts/sales-scheduling-surface/schema/payloads/delivery.lesson-hold.cancelled-v1.json +42 -0
- package/contracts/sales-scheduling-surface/schema/payloads/delivery.lesson-hold.created-v1.json +115 -0
- package/contracts/sales-scheduling-surface/validation/composite-hold-create.md +97 -0
- package/contracts/sales-scheduling-surface/validation/lock-state-machine-conformance.md +84 -0
- package/contracts/sales-scheduling-surface/validation/sales-close-orchestration.md +77 -0
- package/contracts/warehouse-silver/README.md +118 -0
- package/contracts/warehouse-silver/coaching-utilization-columns.md +105 -0
- package/dist/events.d.ts +63 -0
- package/dist/events.js +293 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +7 -1
- package/dist/postgres-consumer.js +2 -1
- package/dist/validator.js +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
# Coach Availability Contract v2.1.2
|
|
2
|
+
|
|
3
|
+
**Status:** v2.1.3
|
|
4
|
+
**Date:** 2026-05-19
|
|
5
|
+
**Owner:** Coaching
|
|
6
|
+
**Consumers:** Sales (offer construction at the close), Delivery (coach assignment at scheduling and lesson time)
|
|
7
|
+
**Related ADRs:** ADR-0001, ADR-0003 amended, ADR-0005, ADR-0006, ADR-0008 (partially superseded), ADR-0018 (this version's load-bearing decision)
|
|
8
|
+
**Validations:** `validation/sales-offer-construction.md`, `validation/delivery-assignment.md` (both updated alongside this version)
|
|
9
|
+
|
|
10
|
+
## 1. Purpose and scope
|
|
11
|
+
|
|
12
|
+
Coach availability is the shared concern Coaching exists to own. Sales reads availability and eligibility at offer-construction time to know whether a lesson can be promised before a credit reservation is requested. Delivery reads the same surface at assignment time to attach a coach to a confirmed lesson.
|
|
13
|
+
|
|
14
|
+
v2.0.0 supersedes v1.0.0's per-slot model with a continuous-band model. Availability is returned as `{coach_id, date, free_intervals[], bookings[]}` rather than `{coach_id, window, state}` per slot. Eligibility responses gain a structured `why` field for ineligible coaches so consumers can debug without an out-of-band query. The travel-time eligibility constraint (drive from prior booked lesson, drive from home) is first-class in the eligibility math.
|
|
15
|
+
|
|
16
|
+
v2.1.0 adds a lightweight roster read for consumer selector hydration. The roster read exposes Coaching-owned role facts and `person_id`; it does not expose canonical Person labels or market display labels. v2.1.1 adds `service_area_id` to availability booking overlays so consumers can show where an existing reservation sits before estimating drive time. v2.1.2 adds candidate-zip travel buffers to the availability read, computed by Coaching's travel-time matrix.
|
|
17
|
+
|
|
18
|
+
Out of scope: coach assignment to a specific lesson (Delivery owns it; `coach.assigned` is a Delivery event), Person identity (Platform), the credit reservation lock state machine (Delivery and Revenue jointly per ADR-0006; Coaching is a subscriber), pricing, payroll. Capacity adjustments and certification publication remain v1.0.0 scope items and stay out of v2.0.0.
|
|
19
|
+
|
|
20
|
+
## 2. Normative language
|
|
21
|
+
|
|
22
|
+
The key words MUST, MUST NOT, REQUIRED, SHALL, SHALL NOT, SHOULD, SHOULD NOT, MAY follow RFC 2119.
|
|
23
|
+
|
|
24
|
+
## 3. Terminology
|
|
25
|
+
|
|
26
|
+
- **Coach.** A Person with a `coa_` role record at Coaching. One Coach per Person per Organization. A Coach belongs to exactly one Market.
|
|
27
|
+
- **Free interval.** A half-open `[start, end)` time range during which the coach is available, derived from `AvailabilityTemplate` and `AvailabilityException`, with bookings subtracted. Always expressed in ISO 8601 with timezone.
|
|
28
|
+
- **Booking.** A row in `coaching.coach_lesson_booking` representing a lesson that the coach is on the hook for. State is one of `reserved | locked | consumed | released`.
|
|
29
|
+
- **Eligibility.** The boolean predicate "may this Coach take this lesson," composed of (coach in lesson's Market) AND (lesson interval fits a free interval that day) AND (drive from prior booked lesson on the same day fits `maxInterLessonTravelMinutes`, else drive from `coach.zip` fits `maxDriveMinutesFromHome`) AND (required certifications held).
|
|
30
|
+
- **Drive-time computation.** Performed by `coaching/modules/travel-time` against `coaching.zip_distance_cache`, with Google Maps Distance Matrix as the fresh-source provider and a heuristic fallback when the API key is unset. Tagged in the cache by `source`.
|
|
31
|
+
- **Authoritative state.** Revenue's lock-state-machine record per ADR-0006. Coaching's `CoachLessonBooking` is eventually-consistent against the `credit.*` event stream.
|
|
32
|
+
|
|
33
|
+
## 4. Surface
|
|
34
|
+
|
|
35
|
+
Four read endpoints. All return `application/json`. All accept `organization_id` as a required parameter (tenancy scope, per ADR-0001).
|
|
36
|
+
|
|
37
|
+
### 4.1 Availability read
|
|
38
|
+
|
|
39
|
+
`GET /coaching/v2/availability`
|
|
40
|
+
|
|
41
|
+
Query parameters:
|
|
42
|
+
|
|
43
|
+
- `organization_id` (required, string). Tenancy scope.
|
|
44
|
+
- `coach_id` (optional, string). If present, scope to a single coach. If omitted, return all coaches in the organization.
|
|
45
|
+
- `market_id` (reserved, string). The v2.1.0 route returns HTTP 400 with `error: market_filter_not_supported` when this parameter is supplied. Coaching will enable this filter in a later minor once service-area to market mapping is hydrated into the read model.
|
|
46
|
+
- `service_area_id` (optional, string). If present, scope to coaches who cover this service area. This is the live geography filter in v2.1.0.
|
|
47
|
+
- `lesson_zip` (optional, string). Candidate lesson ZIP. When present, Coaching computes travel-matrix buffers around active bookings whose lesson ZIP differs from the candidate ZIP. Same-ZIP bookings produce no travel buffer.
|
|
48
|
+
- `window_start` (required, ISO 8601 with timezone). Start of the read window.
|
|
49
|
+
- `window_end` (required, ISO 8601 with timezone). End. Maximum span 31 days; longer reads return HTTP 400.
|
|
50
|
+
|
|
51
|
+
Response (HTTP 200):
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
{
|
|
55
|
+
"as_of": "2026-05-16T18:42:11.123Z",
|
|
56
|
+
"organization_id": "org_...",
|
|
57
|
+
"coaches": [
|
|
58
|
+
{
|
|
59
|
+
"coach_id": "coa_...",
|
|
60
|
+
"market_id": null,
|
|
61
|
+
"days": [
|
|
62
|
+
{
|
|
63
|
+
"date": "2026-05-17",
|
|
64
|
+
"free_intervals": [
|
|
65
|
+
{ "start": "2026-05-17T09:00:00-10:00", "end": "2026-05-17T15:30:00-10:00" }
|
|
66
|
+
],
|
|
67
|
+
"bookings": [
|
|
68
|
+
{
|
|
69
|
+
"lesson_id": "les_...",
|
|
70
|
+
"start": "2026-05-17T10:00:00-10:00",
|
|
71
|
+
"end": "2026-05-17T11:00:00-10:00",
|
|
72
|
+
"service_area_id": "sva_...",
|
|
73
|
+
"lesson_zip": "75201",
|
|
74
|
+
"state": "locked",
|
|
75
|
+
"reservation_id": "crr_...",
|
|
76
|
+
"lock_id": "lck_..."
|
|
77
|
+
}
|
|
78
|
+
],
|
|
79
|
+
"travel_buffers": [
|
|
80
|
+
{
|
|
81
|
+
"lesson_id": "les_...",
|
|
82
|
+
"start": "2026-05-17T09:40:00-10:00",
|
|
83
|
+
"end": "2026-05-17T10:00:00-10:00",
|
|
84
|
+
"minutes": 20,
|
|
85
|
+
"source": "google_maps",
|
|
86
|
+
"from_zip": "75248",
|
|
87
|
+
"to_zip": "75201",
|
|
88
|
+
"position": "before"
|
|
89
|
+
}
|
|
90
|
+
]
|
|
91
|
+
}
|
|
92
|
+
]
|
|
93
|
+
}
|
|
94
|
+
]
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
`free_intervals` already has bookings and returned travel buffers subtracted; a consumer that wants to overlay bookings visually can use the arrays directly. `bookings` lists every booking on the coach for that date regardless of state (so released or consumed bookings show too, for historical reconciliation); consumers filter by `state` if they only want active ones. `service_area_id` is nullable and identifies the booked lesson's service area when Coaching has received that fact from Delivery or a backfill. `lesson_zip` is nullable and is the booked lesson ZIP Coaching used for travel-matrix buffer computation. Consumers that need a service-area display label SHALL hydrate it from Platform canonical geography.
|
|
99
|
+
|
|
100
|
+
`travel_buffers` is present and may be empty. It is computed only when the request includes `lesson_zip`. Each buffer is tied to a booked `lesson_id`, carries its matrix `minutes`, and sits either immediately `before` or immediately `after` that booking. Same-ZIP comparisons produce no buffer. Missing booked-lesson ZIPs produce no buffer because Coaching cannot ask the matrix for a pair. Buffers may overlap each other and may overlap booked lessons; consumers should render them as explanatory overlays, not as distinct assignments.
|
|
101
|
+
|
|
102
|
+
`as_of` is the eligibility composer's read instant; consumers SHOULD log this when surfacing availability to operators so stale-read traceability is intact.
|
|
103
|
+
|
|
104
|
+
`market_id` in the response is nullable in v2.1.0. Until Coaching hydrates the service-area to market mapping into the read model, consumers MUST treat null as "not populated by Coaching" and use Platform canonical geography for market labels, timezones, and market-level grouping.
|
|
105
|
+
|
|
106
|
+
### 4.2 Eligibility by lesson ID
|
|
107
|
+
|
|
108
|
+
`GET /coaching/v2/eligibility/by-lesson`
|
|
109
|
+
|
|
110
|
+
Query parameters:
|
|
111
|
+
|
|
112
|
+
- `organization_id` (required).
|
|
113
|
+
- `lesson_id` (required, string). The Delivery lesson ID. Coaching resolves the lesson's `lesson_address` (or `lesson_zip`), `lesson_start`, `lesson_end`, and `required_certifications[]` via a synchronous read against Delivery's lesson surface keyed by `lesson_id`. The `lesson.scheduled` event payload does not carry these fields today; the read-against-Delivery path is the contract's source of truth so Delivery's event surface does not need to widen on Coaching's behalf. If Delivery's lesson read fails or times out, Coaching returns HTTP 502 with `error: lesson_fetch_failed`; consumers MAY retry.
|
|
114
|
+
|
|
115
|
+
Response (HTTP 200):
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
{
|
|
119
|
+
"as_of": "...",
|
|
120
|
+
"lesson_id": "les_...",
|
|
121
|
+
"eligible_coaches": [
|
|
122
|
+
{ "coach_id": "coa_...", "why": "fits" }
|
|
123
|
+
],
|
|
124
|
+
"ineligible": [
|
|
125
|
+
{
|
|
126
|
+
"coach_id": "coa_...",
|
|
127
|
+
"why": "drive_from_prev_too_long",
|
|
128
|
+
"detail": {
|
|
129
|
+
"previous_lesson_id": "les_...",
|
|
130
|
+
"previous_end": "2026-05-17T10:30:00-10:00",
|
|
131
|
+
"needed_minutes": 45,
|
|
132
|
+
"allowed_minutes": 30
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
]
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
The `why` enum values are `fits | no_band | drive_from_prev_too_long | drive_from_home_too_long | wrong_market | missing_cert | already_booked`. Each ineligible entry carries a `detail` object with the relevant inputs. Consumers MUST treat unknown `why` values as opaque ineligibility (forward-compatible expansion).
|
|
140
|
+
|
|
141
|
+
### 4.3 Eligibility by description
|
|
142
|
+
|
|
143
|
+
`GET /coaching/v2/eligibility/by-description`
|
|
144
|
+
|
|
145
|
+
Query parameters:
|
|
146
|
+
|
|
147
|
+
- `organization_id` (required).
|
|
148
|
+
- `lesson_address` OR `lesson_zip` (at least one required). `lesson_address` is the street address; `lesson_zip` is a normalized ZIP code. Both are first-class locators per Sales' offer-construction shape (close-orchestration often has a resolved ZIP before a full address). When both are present, Coaching prefers the address for travel-time routing and uses the ZIP as a service-area validation. When only `lesson_zip` is present, Coaching resolves to a service area via `coaching.zip_service_area_cache` and runs travel-time at ZIP granularity.
|
|
149
|
+
- `lesson_start` (required, ISO 8601 with timezone).
|
|
150
|
+
- `lesson_end` (required, ISO 8601 with timezone). MUST be later than `lesson_start`.
|
|
151
|
+
- `required_certifications` (optional, comma-separated cert IDs).
|
|
152
|
+
- `preferred_coach_id` (optional). When present, the response sorts the preferred coach first in `eligible_coaches`.
|
|
153
|
+
|
|
154
|
+
Response shape mirrors §4.2 minus the `lesson_id` field at the top. The response includes a `resolved_service_area_id` field so consumers can confirm which service area Coaching mapped the locator to.
|
|
155
|
+
|
|
156
|
+
Error responses:
|
|
157
|
+
|
|
158
|
+
- HTTP 400 with `error: missing_locator` when neither `lesson_address` nor `lesson_zip` is present.
|
|
159
|
+
- HTTP 422 with `error: address_outside_service_areas` when an address-only request cannot be parsed to a known ZIP.
|
|
160
|
+
- HTTP 422 with `error: zip_outside_service_areas` when a ZIP doesn't map to any service area in `zip_service_area_cache`.
|
|
161
|
+
- HTTP 422 with `error: zip_ambiguous` when a ZIP maps to multiple service areas and the cache flags ambiguity (operator-repair surface).
|
|
162
|
+
|
|
163
|
+
### 4.4 Coach roster read
|
|
164
|
+
|
|
165
|
+
`GET /coaching/v2/roster`
|
|
166
|
+
|
|
167
|
+
Query parameters:
|
|
168
|
+
|
|
169
|
+
- `organization_id` (required, string). Tenancy scope.
|
|
170
|
+
- `coach_id` (optional, string). If present, scope to one Coach.
|
|
171
|
+
- `service_area_id` (optional, string). If present, scope to coaches whose home service area or coverage list includes this service area.
|
|
172
|
+
- `market_id` (reserved, string). The v2.1.0 route returns HTTP 400 with `error: market_filter_not_supported` when this parameter is supplied. Coaching will enable this filter in a later minor once service-area to market mapping is hydrated into the read model.
|
|
173
|
+
- `include_archived` (optional, boolean). Defaults to false. When false, archived coaches do not appear.
|
|
174
|
+
|
|
175
|
+
Response (HTTP 200):
|
|
176
|
+
|
|
177
|
+
```
|
|
178
|
+
{
|
|
179
|
+
"as_of": "2026-05-19T14:00:00.000Z",
|
|
180
|
+
"organization_id": "org_...",
|
|
181
|
+
"coaches": [
|
|
182
|
+
{
|
|
183
|
+
"coach_id": "coa_...",
|
|
184
|
+
"person_id": "per_...",
|
|
185
|
+
"status": "active",
|
|
186
|
+
"home_service_area_id": "sva_...",
|
|
187
|
+
"service_area_ids": ["sva_..."],
|
|
188
|
+
"market_id": null,
|
|
189
|
+
"can_cross_service_areas": true,
|
|
190
|
+
"max_drive_minutes_from_home": 30,
|
|
191
|
+
"max_inter_lesson_travel_minutes": 20
|
|
192
|
+
}
|
|
193
|
+
]
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
This endpoint is for consumer selector hydration and roster scoping. It is not an identity endpoint. `person_id` is the key consumers use to read canonical Person labels from Platform. Market display name and timezone come from Platform canonical geography, not from Coaching.
|
|
198
|
+
|
|
199
|
+
### 4.5 Address and ZIP → service-area resolution
|
|
200
|
+
|
|
201
|
+
Eligibility routes on the lesson's locator (address or ZIP). Coaching maintains a zip-to-service-area lookup table (`coaching.zip_service_area_cache`) populated from Platform's canonical geography catalog plus an internal manual-override mechanism for zips that span multiple areas. The lookup is one read per eligibility call; misses fall back to "ask Platform" until the cache is warmed.
|
|
202
|
+
|
|
203
|
+
When the input is `lesson_address` only, Coaching extracts the ZIP via a simple parser (a five-digit run in the address string, validated against `zip_service_area_cache`) and routes through the same path as `lesson_zip`. v2.0.0 does not call a geocoder; a future minor version may add one if address-only requests without parsable ZIPs become common.
|
|
204
|
+
|
|
205
|
+
Ambiguous zips (one ZIP serving multiple service areas) are tagged in the cache as `source: ambiguous` and escalated to Platform via memo rather than resolved locally per Platform's canonical-geography ownership.
|
|
206
|
+
|
|
207
|
+
### 4.6 Compatibility shim for v1.0.0
|
|
208
|
+
|
|
209
|
+
The original v2 draft expected a two-week v1.0.0 compatibility shim. The v2-live implementation removed v1 routes during the big-bang internal rewrite instead. Consumers must use v2 endpoints. If the missing shim causes operational pain, file on the `2026-05-16-coaching-availability-v2-incoming` thread and Coaching will decide whether to stand up a temporary successor route from the v2 service layer.
|
|
210
|
+
|
|
211
|
+
## 5. Producer responsibilities
|
|
212
|
+
|
|
213
|
+
- Coaching SHALL compute eligibility from live state, not from a stale projection. `as_of` reflects the eligibility composer's read instant.
|
|
214
|
+
- Coaching SHALL respect the travel-time fallback chain per `coordination/adrs/coaching/001-travel-time-engine-and-fallback.md`. When the response is computed against heuristic travel time, the response MAY include a `travel_time_source: "heuristic"` field at the coach level so consumers can flag approximate eligibility.
|
|
215
|
+
- Coaching SHALL maintain `CoachLessonBooking` rows with eventually-consistent state against Revenue's `credit.*` stream and Delivery's lesson lifecycle events.
|
|
216
|
+
- The freshness SLO from v1.0.0 stays: Coaching does not invent a stricter bound. Operators reading stale availability that costs offers is a Coaching incident.
|
|
217
|
+
- Coaching SHALL NOT expose canonical Person labels or market display labels on the roster or availability response. It exposes `person_id` and geography ids so consumers can hydrate those labels from Platform.
|
|
218
|
+
|
|
219
|
+
## 6. Consumer responsibilities
|
|
220
|
+
|
|
221
|
+
- Consumers MUST treat unknown `why` enum values as opaque ineligibility.
|
|
222
|
+
- Consumers MUST NOT depend on the slot quantization in the v1.0.0 shim past the deprecation window.
|
|
223
|
+
- Consumers SHOULD log `as_of` when surfacing availability to operators.
|
|
224
|
+
- Consumers that need coach display labels SHALL read Platform's Person facts API using `person_id` from the roster response. Consumers that need market display name or timezone SHALL read Platform canonical geography.
|
|
225
|
+
|
|
226
|
+
## 7. Versioning and deprecation
|
|
227
|
+
|
|
228
|
+
- v2.1.3 is the load-bearing version.
|
|
229
|
+
- v1.0.0 is retired in the live implementation per §4.6.
|
|
230
|
+
- Future minor versions (v2.x.0) MAY add new endpoints or new `why` enum values. Removing endpoints or removing `why` values is a major version bump.
|
|
231
|
+
|
|
232
|
+
## 8. Cross-domain dependencies (resolved during v2 review)
|
|
233
|
+
|
|
234
|
+
- **Delivery lesson lifecycle and hold event surface.** Per Delivery's 2026-05-16 ack, the registered lesson lifecycle events Coaching subscribes to are: `lesson.scheduled` (seeds booking facts: time, participant, organization, coach assignment if any, duration), `coach.assigned` (authoritative coach assignment per ADR-0008), `lesson.cancelled` (booking removed), `lesson.delivered` (post-delivery signal, equivalent of the original `consumed` notion). For Sales-originated holds, Coaching also consumes `delivery.lesson-hold.created` from the sales-scheduling-surface contract as scheduling-side evidence, including optional `lesson_zip` when Delivery can source it from the lesson site. `lesson.attended` exists for attendance reconciliation; Coaching subscribes only if participant-level attendance becomes load-bearing for eligibility, which it does not in v2.0.0.
|
|
235
|
+
- **Booking state transitions.** `reserved | locked | released | consumed` semantics come from Revenue's `credit.*` stream, not from Delivery lesson events. This preserves the no-duplicate-financial-truth boundary per the credit-reservation-lock contract.
|
|
236
|
+
- **Sales close-orchestration call site.** Sales acked the v2 shape; no enum changes required. Sales will carry `as_of` as the v2 freshness anchor.
|
|
237
|
+
- **Platform canonical geography.** `service_area.market_id` exposure via the geography catalog continues to be the authoritative source. Coaching additionally builds a zip-to-service-area cache populated from Platform's catalog; if a zip's service area attribution becomes ambiguous (e.g., a zip spans multiple service areas), Coaching escalates to Platform via a separate memo rather than resolving the ambiguity locally.
|
|
238
|
+
|
|
239
|
+
## 9. Sub-specs
|
|
240
|
+
|
|
241
|
+
None for v2.1.0. The surface is small enough to fit here. v2.x.0 sub-specs (e.g., a partial-band scheduling surface for multi-coach lessons) live as siblings to this README when they land.
|
|
242
|
+
|
|
243
|
+
## 10. Change log
|
|
244
|
+
|
|
245
|
+
### v2.1.3 (2026-05-19)
|
|
246
|
+
|
|
247
|
+
Patch clarification. Documents Coaching's subscription to Delivery's `delivery.lesson-hold.created` scheduling-side event for Sales-originated holds, including optional `lesson_zip` when sourced by Delivery. No availability response shape changes.
|
|
248
|
+
|
|
249
|
+
### v2.1.2 (2026-05-19)
|
|
250
|
+
|
|
251
|
+
Add optional `lesson_zip` to `GET /coaching/v2/availability` and return per-day `travel_buffers[]` computed from Coaching's travel-time matrix. Same-ZIP comparisons produce no buffer. Returned free intervals subtract both active bookings and returned travel buffers so Sales offer construction sees the same travel spacing the lane overlay shows.
|
|
252
|
+
|
|
253
|
+
### v2.1.1 (2026-05-19)
|
|
254
|
+
|
|
255
|
+
Add `service_area_id` to each availability `bookings[]` row for Sales swim-lane reservation overlays. The field is additive and nullable; labels remain Platform canonical geography, not Coaching-owned text.
|
|
256
|
+
|
|
257
|
+
### v2.1.0 (2026-05-19)
|
|
258
|
+
|
|
259
|
+
Add `GET /coaching/v2/roster` as a lightweight roster read for Sales and Delivery selector hydration. Clarify that coach display labels come from Platform Person facts via `person_id`, and market display labels plus timezone come from Platform canonical geography. Reconcile the `market_id` filter with the deployed route behavior by reserving it until Coaching hydrates service-area to market mapping into the read model; `service_area_id` is the live filter in this version.
|
|
260
|
+
|
|
261
|
+
### v2.0.0 (2026-05-16)
|
|
262
|
+
|
|
263
|
+
Initial continuous-band v2 surface. Availability returns per-coach, per-day free intervals plus bookings. Eligibility routes return structured `why` values and use lesson address or ZIP for travel-time-aware service-area routing.
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://contracts.sguild/coach-availability/schema/payloads/coach.assigned-v1.json",
|
|
4
|
+
"title": "coach.assigned payload v1",
|
|
5
|
+
"description": "Payload for the coach.assigned event_type per ADR-0008. Delivery is the single writer for coach assignment facts; Coaching consumes this event to update its assigned-vs-eligible projection. The payload is PII-free.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"required": [
|
|
9
|
+
"lesson_id",
|
|
10
|
+
"previous_coach_id",
|
|
11
|
+
"coach_id",
|
|
12
|
+
"participant_id",
|
|
13
|
+
"lesson_site_id",
|
|
14
|
+
"service_area_id",
|
|
15
|
+
"lesson_type_id",
|
|
16
|
+
"window",
|
|
17
|
+
"assigned_at"
|
|
18
|
+
],
|
|
19
|
+
"properties": {
|
|
20
|
+
"lesson_id": {
|
|
21
|
+
"type": "string",
|
|
22
|
+
"minLength": 1
|
|
23
|
+
},
|
|
24
|
+
"previous_coach_id": {
|
|
25
|
+
"type": [
|
|
26
|
+
"string",
|
|
27
|
+
"null"
|
|
28
|
+
],
|
|
29
|
+
"minLength": 1
|
|
30
|
+
},
|
|
31
|
+
"coach_id": {
|
|
32
|
+
"type": "string",
|
|
33
|
+
"minLength": 1
|
|
34
|
+
},
|
|
35
|
+
"participant_id": {
|
|
36
|
+
"type": [
|
|
37
|
+
"string",
|
|
38
|
+
"null"
|
|
39
|
+
],
|
|
40
|
+
"minLength": 1
|
|
41
|
+
},
|
|
42
|
+
"lesson_site_id": {
|
|
43
|
+
"type": [
|
|
44
|
+
"string",
|
|
45
|
+
"null"
|
|
46
|
+
],
|
|
47
|
+
"minLength": 1
|
|
48
|
+
},
|
|
49
|
+
"service_area_id": {
|
|
50
|
+
"type": [
|
|
51
|
+
"string",
|
|
52
|
+
"null"
|
|
53
|
+
],
|
|
54
|
+
"minLength": 1
|
|
55
|
+
},
|
|
56
|
+
"lesson_type_id": {
|
|
57
|
+
"type": [
|
|
58
|
+
"string",
|
|
59
|
+
"null"
|
|
60
|
+
],
|
|
61
|
+
"minLength": 1
|
|
62
|
+
},
|
|
63
|
+
"window": {
|
|
64
|
+
"$ref": "#/$defs/window"
|
|
65
|
+
},
|
|
66
|
+
"assigned_at": {
|
|
67
|
+
"type": "string",
|
|
68
|
+
"format": "date-time"
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
"$defs": {
|
|
72
|
+
"window": {
|
|
73
|
+
"type": "object",
|
|
74
|
+
"additionalProperties": false,
|
|
75
|
+
"required": [
|
|
76
|
+
"start",
|
|
77
|
+
"end"
|
|
78
|
+
],
|
|
79
|
+
"properties": {
|
|
80
|
+
"start": {
|
|
81
|
+
"type": "string",
|
|
82
|
+
"format": "date-time"
|
|
83
|
+
},
|
|
84
|
+
"end": {
|
|
85
|
+
"type": "string",
|
|
86
|
+
"format": "date-time"
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Validation: Delivery assignment
|
|
2
|
+
|
|
3
|
+
**Contract:** `coach-availability` v1.1.0
|
|
4
|
+
**Consumer:** Delivery (assignment-time eligibility, coach-day planner module)
|
|
5
|
+
**Date:** 2026-05-16
|
|
6
|
+
|
|
7
|
+
## Why this validation exists
|
|
8
|
+
|
|
9
|
+
Delivery is the consumer that previously owned the surface this contract now exposes. Post-split, Delivery is a peer consumer of Coaching's read endpoints rather than the producer of an internal surface. This file documents what Delivery calls, when, with what inputs, and how Delivery's coach-assignment function and coach-day planner module compose the eligibility response with Delivery-side scheduling facts. The producer SHALL preserve this shape across patch and minor versions; any change that breaks Delivery's shape is a v2-class change.
|
|
10
|
+
|
|
11
|
+
## Delivery's surface area
|
|
12
|
+
|
|
13
|
+
Delivery calls the `coach-availability` contract at two points: at scheduling time (when a lesson is created and a coach is being assigned) and at lesson-day operations time (when the coach-day planner surfaces the operator's morning queue and re-validates assignments against current availability state).
|
|
14
|
+
|
|
15
|
+
The contract is involved in eligibility reads only. The write that emits `coach.assigned` lives in Delivery's assignment module, not in Coaching, and is unchanged by the Coaching split.
|
|
16
|
+
|
|
17
|
+
### Inputs Delivery has at assignment time
|
|
18
|
+
|
|
19
|
+
- `organization_id`. Resolved from the lesson's organization context.
|
|
20
|
+
- `lesson_id`. The `les_` identifier of the lesson being assigned. Always present at assignment time; the lesson record exists by the time Delivery calls Coaching for eligibility.
|
|
21
|
+
|
|
22
|
+
Delivery does NOT pass `service_area_id`, `window`, `lesson_type_id`, or `required_certifications` directly. Those inputs are read by Coaching from Delivery's lesson surface, keyed off the `lesson_id`. The contract's design intent is that the producer (Coaching) reads inputs the producer cannot otherwise know (the lesson's program rule references), and the consumer (Delivery) passes only what the consumer owns (the lesson identity).
|
|
23
|
+
|
|
24
|
+
### Endpoint Delivery calls
|
|
25
|
+
|
|
26
|
+
`GET /coaching/v1/eligibility/by-lesson` per §4.2.1.
|
|
27
|
+
|
|
28
|
+
Delivery does not call `POST /coaching/v1/eligibility/by-description` because Delivery always has a `les_` at assignment time. The by-description endpoint is for offer construction before a lesson record exists; that is Sales' shape.
|
|
29
|
+
|
|
30
|
+
Delivery may call `GET /coaching/v1/availability` (§4.1) for operator-tooling display purposes (the coach-day planner's morning view, the "show me all coach availability across the next two weeks" overview). The availability endpoint is acceptable for read-only inspection; it is not the assignment-decision call.
|
|
31
|
+
|
|
32
|
+
## Read frequency
|
|
33
|
+
|
|
34
|
+
Delivery calls eligibility at three points in the assignment lifecycle:
|
|
35
|
+
|
|
36
|
+
1. At scheduling time, when a lesson is first created and an automatic coach assignment is attempted. One call per lesson.
|
|
37
|
+
2. At reassignment time, when an operator manually changes a coach assignment. One call per reassignment to validate the new coach is eligible.
|
|
38
|
+
3. At lesson-day operations time, when the coach-day planner re-validates each upcoming assignment against current availability state to catch any drift since the original assignment. One call per active assignment per planner refresh; refresh cadence is operator-tooling-internal and not contract-defined.
|
|
39
|
+
|
|
40
|
+
Read three is the most frequent. Coaching's producer SHOULD assume the by-lesson endpoint receives read traffic at a multiple of the active-assignment count per organization per refresh interval.
|
|
41
|
+
|
|
42
|
+
## Stale-projection handling
|
|
43
|
+
|
|
44
|
+
If the projection is stale at scheduling time (read one) and the slot has been taken between the response's `projection_as_of` and the moment Delivery commits the assignment, the assignment write into Delivery's storage layer succeeds locally (Delivery's storage does not gate on Coaching's projection), but the underlying credit-reservation-lock state on Revenue may already be `released` or `forfeited` for the original reservation, in which case the assignment is moot.
|
|
45
|
+
|
|
46
|
+
Delivery SHALL handle this case by re-validating against the credit-reservation-lock contract before emitting `coach.assigned`. If the underlying reservation is not in `reserved` or `locked` state, Delivery SHALL NOT emit `coach.assigned`; the assignment is a Delivery-internal record that is not yet authoritative. Operator-tooling surfaces this state ("assignment pending lock confirmation").
|
|
47
|
+
|
|
48
|
+
If the projection is stale at read three (planner refresh) and the projection says a coach is no longer available for an assignment that has already fired `coach.assigned`, the planner SHALL surface the discrepancy to the operator rather than auto-reassign. The discrepancy is informational; the authoritative assignment is Delivery's.
|
|
49
|
+
|
|
50
|
+
## Coach-day planner integration
|
|
51
|
+
|
|
52
|
+
`delivery/modules/coach-day/` is the planner that surfaces the operator's daily view. Pre-split, the planner imported directly from `delivery/modules/coach/` to compose Coach state with Lesson and Service Area facts into a Coach Day result type. Post-split, the planner consumes this contract for the coach-side facts and continues to own the Coach Day result type per the per-domain module pattern's rule "the module owns the result type."
|
|
53
|
+
|
|
54
|
+
The rewrite per ADR-0008 action item 7 changes the planner's data flow but not its result type or its public surface. Specifically:
|
|
55
|
+
|
|
56
|
+
The planner's `loadAllCoachProfiles` repo function (which today reads from Delivery's local Coach storage) becomes a contract call against Coaching's surface. The repo abstraction stays in `delivery/modules/coach-day/repo.ts`; only the implementation under it changes from a local read to a remote read.
|
|
57
|
+
|
|
58
|
+
The planner's eligibility composition (which today computes capacity and certification matches in Delivery code) becomes consumption of Coaching's decomposed predicates from the eligibility response. The planner SHALL NOT re-implement the predicates locally; the contract's response is authoritative.
|
|
59
|
+
|
|
60
|
+
The planner's `actions.ts` entry point (`getCoachDays`) remains as the user-vocabulary action; the operator-tooling surface is unchanged.
|
|
61
|
+
|
|
62
|
+
The planner SHOULD batch contract reads where possible. The `by-lesson` endpoint is per-lesson; the `availability` endpoint covers a window across all coaches in a service area. For the planner's morning view (a service-area-wide read), the availability endpoint is the right call; for per-assignment validation, the by-lesson endpoint.
|
|
63
|
+
|
|
64
|
+
## Boundary that does not move
|
|
65
|
+
|
|
66
|
+
`coach.assigned` remains a Delivery emission per ADR-0008. The Coaching split does not relocate the assignment event; the contract does not produce assignment events; Delivery's assignment write authority is unchanged.
|
|
67
|
+
|
|
68
|
+
Delivery continues to own:
|
|
69
|
+
|
|
70
|
+
- The `delivery/modules/coach-day/` planner (composing module per the layout standard).
|
|
71
|
+
- The `delivery/modules/coach-assignment/` module (or wherever the assignment write lives in Delivery's repo today; the module-layout standard's location rule does not change with the split).
|
|
72
|
+
- Lesson, Lesson Site, Service Area, attendance, the lock state machine on the Delivery side per ADR-0006.
|
|
73
|
+
- `coach.assigned` as the producer-of-record event for assignment.
|
|
74
|
+
|
|
75
|
+
Delivery no longer owns:
|
|
76
|
+
|
|
77
|
+
- `coa_` Coach role records (relocated to Coaching per the amended ADR-0003).
|
|
78
|
+
- Coach availability and capacity (Coaching's per ADR-0008).
|
|
79
|
+
- The eligibility predicate computation (Coaching's per §7.4 of the README).
|
|
80
|
+
|
|
81
|
+
The boundary is "Coaching produces the eligibility answer; Delivery composes it with scheduling-side facts and emits the assignment."
|
|
82
|
+
|
|
83
|
+
## Open questions Delivery reserves the right to raise
|
|
84
|
+
|
|
85
|
+
None for v1.0.0. Delivery is the proposing domain for ADR-0008 and the drafting author of this contract; the surface reflects Delivery's actual consumption needs as best understood pre-stand-up. If a Delivery-side implementation surfaces a missing field or a mis-shaped response during action items 6 through 8, the change lands as a v1.x patch or minor (depending on whether the change is editorial or surface) per §5 of the README.
|
|
86
|
+
|
|
87
|
+
## Sign-off
|
|
88
|
+
|
|
89
|
+
Delivery is the proposing domain; sign-off on this contract is implicit in the parent thread's settlement and ADR-0008's acceptance. Delivery does not file a separate sign-off memo. Sales' sign-off on the parent thread is the gate before stand-up uses the contract per ADR-0008 action item 4.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Validation: Sales offer construction
|
|
2
|
+
|
|
3
|
+
**Contract:** `coach-availability` v1.1.0
|
|
4
|
+
**Consumer:** Sales (offer-construction surface, closing seam)
|
|
5
|
+
**Date:** 2026-05-16
|
|
6
|
+
|
|
7
|
+
## Why this validation exists
|
|
8
|
+
|
|
9
|
+
Sales is the consumer that prompted the Coaching split. The ownership change matters only if Sales' offer-construction shape lands as a first-class consumer of the contract rather than a Delivery-helper that Sales happens to call. This file documents what Sales calls, when, with what inputs, and how Sales handles stale-projection cases. The producer SHALL preserve this shape across patch and minor versions; any change that breaks Sales' shape is a v2-class change.
|
|
10
|
+
|
|
11
|
+
## Sales' surface area
|
|
12
|
+
|
|
13
|
+
Sales calls the `coach-availability` contract at one logical point in the close orchestration: offer construction. The closing operator selects a Coach and a Window, then submits a reservation request to Revenue's credit-reservation-lock surface, then if the reservation lands Sales calls Delivery's assignment surface to attach the coach to the first lesson. The contract is involved in step one only; steps two and three are governed by the credit-reservation-lock contract and Delivery's assignment surface respectively.
|
|
14
|
+
|
|
15
|
+
### Inputs Sales has at offer construction
|
|
16
|
+
|
|
17
|
+
- `organization_id`. Resolved from the Lead's organization context.
|
|
18
|
+
- `service_area_id`. Resolved from the Lead's location intake (address, region, or service-area selection during qualification).
|
|
19
|
+
- `window`. The slot the operator and customer are negotiating, expressed as a half-open `[start, end)` interval in the customer's timezone.
|
|
20
|
+
- `lesson_type_id`. The lesson type the offer is for (kid intro, adult lap, group lesson, etc.). Resolved from the Lead's program selection during qualification.
|
|
21
|
+
- `required_certifications`. Derived from the lesson type's program-rule references. Sales reads program rules from a Sales-internal cache of lesson-type metadata; the source of truth lives in Delivery's lesson-type table, but Sales does not call Delivery for it on every offer.
|
|
22
|
+
|
|
23
|
+
Sales does NOT have at offer construction: a `lesson_id` (no `les_` exists yet; the lesson record is created by Delivery on lock confirmation), a `coach_id` (the operator is choosing one from the eligibility response).
|
|
24
|
+
|
|
25
|
+
### Endpoint Sales calls
|
|
26
|
+
|
|
27
|
+
`POST /coaching/v1/eligibility/by-description` per §4.2.2.
|
|
28
|
+
|
|
29
|
+
Sales does not call `GET /coaching/v1/availability` (§4.1) at offer construction. The availability endpoint is for inspection and operator-tooling overlays; the eligibility-by-description endpoint is the offer-construction call because it composes availability with capacity, service area, and certification predicates in a single response.
|
|
30
|
+
|
|
31
|
+
## Read frequency
|
|
32
|
+
|
|
33
|
+
Sales calls the eligibility endpoint multiple times per close. Typical shape:
|
|
34
|
+
|
|
35
|
+
1. Operator opens an offer-construction view; Sales calls eligibility with the Lead's window of intent (a wide window around the customer's stated availability).
|
|
36
|
+
2. Operator narrows the window during conversation; Sales re-calls eligibility with each material narrowing.
|
|
37
|
+
3. Operator selects a coach and a slot; Sales calls eligibility one more time immediately before submitting the reservation request, to detect any in-flight changes since the operator's last view.
|
|
38
|
+
|
|
39
|
+
Read three is the load-bearing one for stale-read handling. The interval between read three and the reservation submission to Revenue is the freshness window where a different operator's reservation could land in the projection's lag.
|
|
40
|
+
|
|
41
|
+
## Stale-projection handling
|
|
42
|
+
|
|
43
|
+
If the projection is stale at read three and the slot has been taken between the response's `projection_as_of` and Revenue's authoritative state at the moment Sales submits the reservation, the reservation request to Revenue returns a conflict. Sales SHALL:
|
|
44
|
+
|
|
45
|
+
1. Treat the conflict as a normal-path race, not an error condition.
|
|
46
|
+
2. Re-call eligibility with the same inputs (a fresh read).
|
|
47
|
+
3. Surface to the operator that the slot was taken and offer the operator the next-best eligible slots from the fresh response.
|
|
48
|
+
4. Not retry the original reservation request. The original window is no longer available; retrying would either fail again or claim a different slot than the one the operator selected.
|
|
49
|
+
|
|
50
|
+
Sales SHALL NOT cache eligibility responses across operator commits. Caching at the operator-tooling layer for sub-second display purposes is acceptable; caching across commits is not.
|
|
51
|
+
|
|
52
|
+
Sales SHALL log the `as_of` and `projection_as_of` values from each eligibility response so a stale-read root-cause is traceable. Operator-reported "the slot was free a second ago" is a normal-path event, not a Coaching incident, but the log line is needed to confirm the projection lag was within SLO.
|
|
53
|
+
|
|
54
|
+
## Assignment-at-close orchestration
|
|
55
|
+
|
|
56
|
+
Once the reservation lands at Revenue and the lock attaches, Sales orchestrates the first-lesson assignment by calling Delivery's assignment surface, not Coaching's. The relevant orchestration shape:
|
|
57
|
+
|
|
58
|
+
1. Sales submits reservation request to Revenue, receives `crr_` and `lck_` (or the equivalent identifiers per the credit-reservation-lock contract).
|
|
59
|
+
2. Sales calls Delivery's assignment surface (out of scope for this contract; lives in Delivery's repo) with the operator's chosen coach.
|
|
60
|
+
3. Delivery's assignment surface validates eligibility one more time (Delivery calls Coaching's eligibility-by-lesson endpoint per §4.2.1; see `delivery-assignment.md`), assigns the coach if eligible, emits `coach.assigned` (per ADR-0008, `coach.assigned` is a Delivery event).
|
|
61
|
+
|
|
62
|
+
Sales does NOT call `coach.assigned` directly. Sales does NOT write into Coaching's surface. Sales' role at the close ends with the reservation-and-assignment hand-off.
|
|
63
|
+
|
|
64
|
+
## Identity and comms-routing
|
|
65
|
+
|
|
66
|
+
Outbound communications to the Lead's Person (offer letter, lesson confirmation, scheduling instructions) route through Platform's Guardian-aware comms-routing endpoint per the identity contract. This is unchanged by the Coaching split; the routing rule applies uniformly across Sales' communications regardless of which domain the substantive content originates in.
|
|
67
|
+
|
|
68
|
+
Outbound communications about the assigned Coach (mentioning the coach's name in an offer letter, for example) also route through Platform's Person facts API for the coach's identity fields. Sales does NOT read coach identity fields from this contract; the contract returns `coa_` references, not Person fields.
|
|
69
|
+
|
|
70
|
+
## Open questions Sales reserves the right to raise
|
|
71
|
+
|
|
72
|
+
None for v1.0.0. Sales' acknowledgment of the lock-aware position in `2026-05-01-sales-coaching-availability-ack` covered the substantive shape; this file is the documentation of that shape, not new ground. If Sales surfaces a future shape change (the softly-held v1.x extension is the most likely candidate per §9.1 of the README), it lands as a memo against the parent thread or as a fresh thread.
|
|
73
|
+
|
|
74
|
+
## Sign-off
|
|
75
|
+
|
|
76
|
+
Sales signs off by responding to the Delivery sibling memo announcing this contract's draft on the parent thread (`2026-05-01-delivery-coaching-split`). Sign-off is the trigger for promoting the contract from draft to merged in the coordination repo and for Coaching to begin the lock-event subscriber implementation per ADR-0008 action item 8.
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# Coaching Confirmation
|
|
2
|
+
|
|
3
|
+
**Status:** v1.1.0
|
|
4
|
+
**Date:** 2026-05-18
|
|
5
|
+
**Owner:** coaching (confirmation workflow)
|
|
6
|
+
**Consumers:** sales (cadence Stage 3b), platform-warehouse (analytics ingestion)
|
|
7
|
+
**Related ADRs:** ADR-0001 (tenant_id), ADR-0002 (canonical entity ID template), ADR-0005 (event envelope), ADR-0008 (Coaching domain boundary), ADR-0009 (dispatcher transport, producer transactional guarantee)
|
|
8
|
+
**Related contracts:** Event Envelope Contract (`../event-envelope/README.md`), Lead Lifecycle Contract (`../lead-lifecycle/README.md`), Coach Availability Contract (`../coach-availability/README.md`)
|
|
9
|
+
**Sub-specs (authoritative):** `schema/payloads/lead.coach.confirmation.requested-v1.json`, `schema/payloads/coaching.lesson.confirmation_decided-v1.json`
|
|
10
|
+
**Validations:** none yet
|
|
11
|
+
|
|
12
|
+
## 1. Purpose and scope
|
|
13
|
+
|
|
14
|
+
This contract specifies the dispatcher handshake between Sales and Coaching for the Stage 3b coach-confirmation workflow. Sales requests a coach confirmation after a Lead enters the coach-confirmation stage. Coaching decides whether the proposed coach and lesson window are confirmed or denied, then emits a decision event back through dispatcher fanout.
|
|
15
|
+
|
|
16
|
+
In scope: the inbound Sales request event name, the inbound Sales request payload schema, the outbound Coaching decision event name, the outbound Coaching decision payload schema, producer responsibilities, consumer responsibilities, and privacy rules for the confirmation handshake.
|
|
17
|
+
|
|
18
|
+
Out of scope: Sales cadence-stage state transitions, Sales close-orchestration internals, coach eligibility reads, coach assignment writes, customer communications, and lesson outcomes. Availability remains in the coach-availability contract. Coach assignment remains Delivery-owned per ADR-0008.
|
|
19
|
+
|
|
20
|
+
## 2. Normative language
|
|
21
|
+
|
|
22
|
+
The key words MUST, MUST NOT, REQUIRED, SHALL, SHALL NOT, SHOULD, SHOULD NOT, and MAY are to be interpreted per RFC 2119.
|
|
23
|
+
|
|
24
|
+
## 3. Terminology
|
|
25
|
+
|
|
26
|
+
**Confirmation request.** A Sales-produced request asking Coaching to confirm or deny a proposed coach for a proposed lesson window.
|
|
27
|
+
|
|
28
|
+
**Confirmation decision.** Coaching's answer to a confirmation request. A decision is either `confirmed` or `denied`.
|
|
29
|
+
|
|
30
|
+
**Confirmation request ID.** A Sales-minted idempotency and correlation key. Coaching echoes it on the decision event and uses it as the event subject.
|
|
31
|
+
|
|
32
|
+
## 4. Event types
|
|
33
|
+
|
|
34
|
+
### 4.1 `lead.coach.confirmation.requested`
|
|
35
|
+
|
|
36
|
+
Produced by Sales. This event starts the handshake. Coaching consumes it to decide whether the proposed coach and lesson window are confirmed or denied. Platform warehouse may ingest it for confirmation latency, deny-rate, and re-engagement analytics.
|
|
37
|
+
|
|
38
|
+
The event envelope subject SHALL be `confirmation_request_id`. The actor SHALL be the Sales operator or system actor that created the request. Payload v1 is authoritative at `schema/payloads/lead.coach.confirmation.requested-v1.json`.
|
|
39
|
+
|
|
40
|
+
The request payload SHALL carry enough context for Coaching to decide without reading Sales state: `organization_id`, `lead_id`, `confirmation_request_id`, `coach_id`, `service_area_id`, `lesson_type_id`, `lesson_window`, optional `required_certifications`, `requested_at`, and an optional Sales source pointer for audit.
|
|
41
|
+
|
|
42
|
+
### 4.2 `coaching.lesson.confirmation_decided`
|
|
43
|
+
|
|
44
|
+
Produced by Coaching. Sales consumes this event to resolve Stage 3b. Platform warehouse may ingest it for confirmation latency, deny-rate, and re-engagement analytics.
|
|
45
|
+
|
|
46
|
+
The event envelope subject SHALL be `confirmation_request_id`. The actor SHALL be the operator id that made the decision, or `system:coaching` only for future system-decided paths. Payload v1 is authoritative at `schema/payloads/coaching.lesson.confirmation_decided-v1.json`.
|
|
47
|
+
|
|
48
|
+
Known `decision` values are `confirmed` and `denied`. Known `denial_reason` values at v1 are `coach_unavailable`, `capacity_changed`, `coverage_mismatch`, `certification_mismatch`, `operator_declined`, and `other`. Consumers MUST treat unknown future `denial_reason` values as opaque denial reasons.
|
|
49
|
+
|
|
50
|
+
## 5. Producer responsibilities
|
|
51
|
+
|
|
52
|
+
Coaching SHALL emit `coaching.lesson.confirmation_decided` exactly once per `confirmation_request_id` accepted into the decision workflow.
|
|
53
|
+
|
|
54
|
+
Coaching SHALL publish the event in the same Prisma transaction as its local audit record, satisfying ADR-0009's producer-transactional guarantee.
|
|
55
|
+
|
|
56
|
+
Coaching SHALL echo the request's `organization_id`, `lead_id`, `confirmation_request_id`, `coach_id`, `service_area_id`, `lesson_type_id`, and `lesson_window` on the decision payload so Sales can reconcile without a back-lookup.
|
|
57
|
+
|
|
58
|
+
Coaching SHALL set `denial_reason` to null when `decision=confirmed` and to a non-null reason when `decision=denied`.
|
|
59
|
+
|
|
60
|
+
Sales SHALL emit `lead.coach.confirmation.requested` in the same Prisma transaction as its local confirmation request or stage-transition record, satisfying ADR-0009's producer-transactional guarantee.
|
|
61
|
+
|
|
62
|
+
Sales SHALL mint `confirmation_request_id` before publishing and persist it locally so the inbound Coaching decision can be correlated without reading dispatcher history.
|
|
63
|
+
|
|
64
|
+
## 6. Consumer responsibilities
|
|
65
|
+
|
|
66
|
+
Sales consumes `coaching.lesson.confirmation_decided` as the authoritative answer to its matching `lead.coach.confirmation.requested` event.
|
|
67
|
+
|
|
68
|
+
Sales SHALL correlate by `confirmation_request_id`, not by coach or window alone.
|
|
69
|
+
|
|
70
|
+
Sales SHALL deduplicate by dispatcher `event_id`. The event id is deterministic from `confirmation_request_id`, so replays of the same workflow decision do not create a second logical decision.
|
|
71
|
+
|
|
72
|
+
Sales SHALL treat `notes` as operator context only. It MUST NOT depend on `notes` for state-machine transitions.
|
|
73
|
+
|
|
74
|
+
## 7. Security and privacy
|
|
75
|
+
|
|
76
|
+
This event intentionally avoids customer contact details, full lead notes, guardian details, and coach private contact details. It carries operational identifiers, the proposed lesson window, decision metadata, and optional short operator context.
|
|
77
|
+
|
|
78
|
+
Producers and consumers MAY log ids, timestamps, decision, and denial reason. They SHOULD avoid long-lived logging of `notes` unless there is a clear operator need.
|
|
79
|
+
|
|
80
|
+
## 8. Versioning
|
|
81
|
+
|
|
82
|
+
This contract follows the additive discipline from the Event Envelope Contract. New optional fields and new denial reasons may land as v1.x changes when consumers can safely ignore them.
|
|
83
|
+
|
|
84
|
+
Removing fields, renaming fields, changing `decision` semantics, changing correlation from `confirmation_request_id`, or adding content-bearing PII requires a major version bump and a deprecation window.
|
|
85
|
+
|
|
86
|
+
Per-event `schema_version` is scoped to `coaching.lesson.confirmation_decided`.
|
|
87
|
+
Per-event `schema_version` is scoped independently for `lead.coach.confirmation.requested` and `coaching.lesson.confirmation_decided`.
|
|
88
|
+
|
|
89
|
+
## 9. Trigger to revisit
|
|
90
|
+
|
|
91
|
+
Revisit this contract if Coaching needs to read Sales state to make the decision, if Delivery becomes a consumer, if the decision workflow becomes multi-step, or if Sales needs confirmation decisions to reserve capacity rather than merely drive cadence.
|
|
92
|
+
|
|
93
|
+
## 10. Change log
|
|
94
|
+
|
|
95
|
+
- **v1.1.0** (2026-05-18): Registers Sales-produced `lead.coach.confirmation.requested` under this contract and adds its v1 payload schema per `2026-05-17-sales-cadence-stage-map-sales-side-commitments`.
|
|
96
|
+
- **v1.0.0** (2026-05-17): Initial release. Adds the dedicated confirmation handshake contract and the Coaching-produced `coaching.lesson.confirmation_decided` v1 event per `2026-05-17-coaching-cadence-confirmation-event-position`.
|