@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,108 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://contracts.sguild/lead-lifecycle/schema/payloads/lead.handoff.context.recorded-v1.json",
4
+ "title": "lead.handoff.context.recorded payload v1",
5
+ "description": "Payload for the lead.handoff.context.recorded event_type per contracts/lead-lifecycle/README.md §4.7. Emitted by Sales when close orchestration has a durable Sales-owned handoff-context snapshot for Delivery. The payload is curated relationship-start context only. It MUST NOT carry raw Sales note dumps, raw SMS bodies, call transcripts, phone numbers, email addresses, Guardian relationship facts, DOB, payment details, opt-out truth, or internal audit noise. Revenue's customer.handoff remains the first-lock handoff authority; this event carries Sales-owned context beside it.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": [
9
+ "lead_id",
10
+ "person_id",
11
+ "organization_id",
12
+ "communication_preferences",
13
+ "recorded_by",
14
+ "recorded_at"
15
+ ],
16
+ "anyOf": [
17
+ {
18
+ "required": ["first_lesson_id", "credit_reservation_id"],
19
+ "properties": {
20
+ "first_lesson_id": { "type": "string" },
21
+ "credit_reservation_id": { "type": "string" }
22
+ }
23
+ },
24
+ {
25
+ "required": ["originating_offer_id"],
26
+ "properties": {
27
+ "originating_offer_id": { "type": "string", "minLength": 1 }
28
+ }
29
+ }
30
+ ],
31
+ "properties": {
32
+ "lead_id": {
33
+ "type": "string",
34
+ "description": "The Sales Lead whose close-orchestration context is being handed to Delivery. lead_<UUID v7 canonical> per ADR-0002.",
35
+ "pattern": "^lead_[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$"
36
+ },
37
+ "person_id": {
38
+ "type": "string",
39
+ "description": "The canonical Person the Lead is attached to. Delivery keys customer tracking by Person after customer.handoff arrives.",
40
+ "pattern": "^per_[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$"
41
+ },
42
+ "organization_id": {
43
+ "type": "string",
44
+ "description": "The Organization for the close orchestration. Sales populates this from the close-orchestration request or qualification context; it is payload-required so Delivery can org-scope pending context before customer.handoff attaches."
45
+ },
46
+ "first_lesson_id": {
47
+ "type": ["string", "null"],
48
+ "description": "The first Delivery lesson correlated to the handoff context for single-lesson closes. les_<UUID v7 canonical>. Null or omitted for composite-offer contexts where the offer-level correlation is the stable v1 key.",
49
+ "pattern": "^les_[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$"
50
+ },
51
+ "credit_reservation_id": {
52
+ "type": ["string", "null"],
53
+ "description": "The Revenue credit reservation correlated to the handoff context for single-lesson closes. crr_<UUID v7 canonical>. Null or omitted for composite-offer contexts where the offer-level correlation is the stable v1 key.",
54
+ "pattern": "^crr_[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$"
55
+ },
56
+ "originating_offer_id": {
57
+ "type": ["string", "null"],
58
+ "description": "Optional Sales composite-offer correlation id. Present when the handoff context is tied to a package or composite close rather than one single lesson/reservation pair."
59
+ },
60
+ "lead_summary": {
61
+ "type": ["string", "null"],
62
+ "description": "Curated relationship-start summary from operator-authored Sales context and stable Lead or intake facts. This is not an automatic concatenation of all Sales notes and MUST NOT include raw comms bodies, contact details, Guardian facts, DOB, payment details, opt-out truth, or internal audit noise.",
63
+ "maxLength": 1000
64
+ },
65
+ "scheduling_notes": {
66
+ "type": ["string", "null"],
67
+ "description": "Reusable relationship-start scheduling constraints or context that should follow the customer across Delivery surfaces. Per-lesson operational notes stay on the sales-scheduling-surface hold-create or atomic-multi-create item notes fields.",
68
+ "maxLength": 1000
69
+ },
70
+ "communication_preferences": {
71
+ "type": "object",
72
+ "additionalProperties": false,
73
+ "required": ["preferred_channel"],
74
+ "description": "Preference hints only. This object does not carry contact addresses, consent/opt-out truth, Guardian routing truth, or authorization to message a minor directly.",
75
+ "properties": {
76
+ "preferred_channel": {
77
+ "type": "string",
78
+ "enum": ["sms", "voice_call", "email", "unknown"],
79
+ "description": "Operator-captured channel preference. 'unknown' means no preference was captured. This is a preference hint, not consent or routing authority."
80
+ },
81
+ "best_time_window": {
82
+ "type": ["string", "null"],
83
+ "description": "Optional human-readable preferred contact or lesson-planning time window. Example: 'weekday evenings after 5pm'.",
84
+ "maxLength": 120
85
+ },
86
+ "language_preference": {
87
+ "type": ["string", "null"],
88
+ "description": "Optional language preference when Sales has one durably captured. BCP 47 tag preferred when available; otherwise a known internal value.",
89
+ "maxLength": 64
90
+ },
91
+ "operator_notes": {
92
+ "type": ["string", "null"],
93
+ "description": "Optional short preference-context annotation. Must follow the same PII exclusions as lead_summary.",
94
+ "maxLength": 500
95
+ }
96
+ }
97
+ },
98
+ "recorded_by": {
99
+ "type": "string",
100
+ "description": "The Sales operator actor id that recorded the handoff-context snapshot, or 'system' for future automated snapshots."
101
+ },
102
+ "recorded_at": {
103
+ "type": "string",
104
+ "format": "date-time",
105
+ "description": "Sales commit time for the handoff-context snapshot. UTC."
106
+ }
107
+ }
108
+ }
@@ -0,0 +1,54 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://contracts.sguild/lead-lifecycle/schema/payloads/lead.qualified-v1.json",
4
+ "title": "lead.qualified payload v1",
5
+ "description": "Payload for the lead.qualified event_type per contracts/lead-lifecycle/README.md §4.6. Emitted by Sales when a sales rep marks a Lead as qualified — meaning the Person has been reached, expressed intent, and is ready for scheduling handoff to Delivery. Producer SHALL emit inside the same Prisma transaction that transitions the Lead's qualification state. The confirmed organization_id and market_id carried on this event are the authoritative org/market pair for Growth attribution — they close the provisional → confirmed loop and supersede any provisional market inference Growth made from campaign metadata.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": [
9
+ "lead_id",
10
+ "organization_id",
11
+ "market_id",
12
+ "org_market_id",
13
+ "qualification_signal",
14
+ "qualified_at"
15
+ ],
16
+ "properties": {
17
+ "lead_id": {
18
+ "type": "string",
19
+ "description": "The Lead being qualified. lead_<UUID v7 canonical> per ADR-0002.",
20
+ "pattern": "^lead_[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$"
21
+ },
22
+ "organization_id": {
23
+ "type": "string",
24
+ "description": "The Organization the Person is being qualified into. Authoritative — confirmed by the sales rep at qualification time, not inferred from campaign metadata. Platform canonical Organization.id."
25
+ },
26
+ "market_id": {
27
+ "type": "string",
28
+ "description": "The CanonicalMarket the Person is being qualified for. Authoritative — confirmed by the sales rep at qualification time, not inferred from zip or campaign. Platform canonical CanonicalMarket.id. Together with organization_id, resolves to an OrgMarket row."
29
+ },
30
+ "org_market_id": {
31
+ "type": "string",
32
+ "description": "The OrgMarket slug — '{org.slug}-{market.slug}', e.g. 'sguild-swim-dallas'. Denormalized for consumers that need the mart grain key without a join. Must match the OrgMarket row identified by (organization_id, market_id). Platform canonical OrgMarket.slug per ADR-0019."
33
+ },
34
+ "qualification_signal": {
35
+ "type": "string",
36
+ "enum": ["connected", "replied", "inbound_request"],
37
+ "description": "What signal drove qualification. 'connected' = sales rep had a live call and the Person expressed intent. 'replied' = Person replied to an outbound message and expressed intent. 'inbound_request' = Person self-initiated contact (e.g., booked a callback, filled a follow-up form)."
38
+ },
39
+ "qualified_at": {
40
+ "type": "string",
41
+ "format": "date-time",
42
+ "description": "When the qualification was recorded. Sales' wall-clock at the moment the rep marked the Lead qualified. UTC."
43
+ },
44
+ "operator_id": {
45
+ "type": ["string", "null"],
46
+ "description": "The sales rep who qualified the Lead. Sales-internal id format at v1. Null only for system-driven qualification paths (rare)."
47
+ },
48
+ "requalification_count": {
49
+ "type": ["integer", "null"],
50
+ "description": "How many times this Lead has been qualified (including this event). 1 for first qualification; >1 for re-qualification after a prior disqualification or re-engagement. Null if Sales has not yet implemented requalification tracking. Growth uses this to bump growth.qualified_intake.reQualifyCount.",
51
+ "minimum": 1
52
+ }
53
+ }
54
+ }
@@ -0,0 +1,120 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://contracts.sguild/lead-lifecycle/schema/payloads/sales.lead.onboarded-v1.json",
4
+ "title": "sales.lead.onboarded payload v1",
5
+ "description": "Payload for the sales.lead.onboarded event_type. Emitted by Sales on Stage 4 entry after close-orchestration succeeds and Sales persists Lead.lessonDate from the lesson hold window. This is the dispatcher notification described in the cadence stage-map coordination memo, produced inside the same logical close path as the Lead onboarded transition.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": [
9
+ "lead_id",
10
+ "person_id",
11
+ "stage",
12
+ "substage",
13
+ "lesson_id",
14
+ "lesson_date",
15
+ "lesson_window",
16
+ "onboarded_at"
17
+ ],
18
+ "properties": {
19
+ "lead_id": {
20
+ "type": "string",
21
+ "description": "The Lead entering Stage 4. lead_<UUID v7 canonical> per ADR-0002.",
22
+ "pattern": "^lead_[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$"
23
+ },
24
+ "person_id": {
25
+ "type": "string",
26
+ "description": "The canonical Person attached to the Lead. Stage 4 entry requires an identity-matched Lead. per_<UUID v7 canonical> per ADR-0003.",
27
+ "pattern": "^per_[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$"
28
+ },
29
+ "stage": {
30
+ "type": "string",
31
+ "const": "onboarded",
32
+ "description": "The Sales Lead stage after Stage 4 entry."
33
+ },
34
+ "substage": {
35
+ "type": "string",
36
+ "description": "The Sales Lead substage after Stage 4 entry. Expected initial value is 'reserved' for the close-orchestration success path; kept flexible so Sales can extend Stage 4 operator vocabulary without a breaking payload change."
37
+ },
38
+ "lesson_id": {
39
+ "type": "string",
40
+ "description": "The Delivery Lesson id returned by close-orchestration hold-create.",
41
+ "pattern": "^les_[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$"
42
+ },
43
+ "lesson_date": {
44
+ "type": "string",
45
+ "format": "date-time",
46
+ "description": "The lesson start timestamp Sales persists to Lead.lessonDate. Derived from lesson_window.start. UTC."
47
+ },
48
+ "lesson_window": {
49
+ "type": "object",
50
+ "additionalProperties": false,
51
+ "required": ["start", "end"],
52
+ "description": "The scheduled lesson window returned by close-orchestration.",
53
+ "properties": {
54
+ "start": {
55
+ "type": "string",
56
+ "format": "date-time",
57
+ "description": "Lesson window start. Must match lesson_date. UTC."
58
+ },
59
+ "end": {
60
+ "type": "string",
61
+ "format": "date-time",
62
+ "description": "Lesson window end. UTC."
63
+ }
64
+ }
65
+ },
66
+ "reservation_id": {
67
+ "type": ["string", "null"],
68
+ "description": "Sales-visible reservation id returned by the Revenue reservation call, when available."
69
+ },
70
+ "credit_reservation_id": {
71
+ "type": ["string", "null"],
72
+ "description": "Revenue credit reservation id associated with the Stage 4 entry, when available."
73
+ },
74
+ "reservation_lock_id": {
75
+ "type": ["string", "null"],
76
+ "description": "Reservation lock id returned by Delivery hold-create, when available."
77
+ },
78
+ "order_id": {
79
+ "type": ["string", "null"],
80
+ "description": "Revenue Order id associated with the Stage 4 entry, when known. Null when the close-orchestration response has not yet exposed an order correlation."
81
+ },
82
+ "organization_id": {
83
+ "type": ["string", "null"],
84
+ "description": "Organization id returned by Delivery hold-create, when available."
85
+ },
86
+ "service_area_id": {
87
+ "type": ["string", "null"],
88
+ "description": "Service Area id used for the reserved lesson, when available."
89
+ },
90
+ "lesson_type_id": {
91
+ "type": ["string", "null"],
92
+ "description": "Lesson Type id used for the reserved lesson, when available."
93
+ },
94
+ "coach_id": {
95
+ "type": ["string", "null"],
96
+ "description": "Coach id assigned to the reserved lesson, when available."
97
+ },
98
+ "lesson_site_id": {
99
+ "type": ["string", "null"],
100
+ "description": "Lesson site id returned by Delivery hold-create, when available."
101
+ },
102
+ "participant_id": {
103
+ "type": ["string", "null"],
104
+ "description": "Delivery participant id attached to the reserved lesson, when available."
105
+ },
106
+ "dispatcher_correlation_id": {
107
+ "type": ["string", "null"],
108
+ "description": "Sales-generated correlation id for downstream dispatcher-complete handling, when that producer edge is active."
109
+ },
110
+ "operator_id": {
111
+ "type": ["string", "null"],
112
+ "description": "Sales operator who completed close-orchestration and moved the Lead into Stage 4. Sales-internal id format at v1."
113
+ },
114
+ "onboarded_at": {
115
+ "type": "string",
116
+ "format": "date-time",
117
+ "description": "When Sales recorded Stage 4 entry. Sales wall-clock at commit. UTC."
118
+ }
119
+ }
120
+ }
@@ -0,0 +1,118 @@
1
+ # Lesson Lifecycle
2
+
3
+ **Status:** v1.1.0
4
+ **Date:** 2026-05-18
5
+ **Owner:** delivery (lessons)
6
+ **Consumers:** revenue (Lesson Completion Job, consumes `lesson.delivered`), sales (cadence, consumes `lesson.rescheduled`)
7
+ **Related ADRs:** ADR-0001 (tenant_id), ADR-0002 (canonical entity ID template; `les_` prefix), ADR-0005 (event envelope), ADR-0006 (credit reservation lock state machine), ADR-0009 (dispatcher transport)
8
+ **Related contracts:** Event Envelope Contract (`../event-envelope/README.md`), Credit Reservation Lock Contract (`../credit-reservation-lock/README.md`)
9
+ **Sub-specs (authoritative):** `schema/payloads/lesson.delivered-v1.json`, `schema/payloads/lesson.cancelled-v1.json`, `schema/payloads/lesson.rescheduled-v1.json`
10
+ **Validations:** none yet
11
+
12
+ ## 1. Purpose and scope
13
+
14
+ This contract specifies the Lesson lifecycle event family produced by Delivery as a lesson moves through scheduling, rescheduling, attendance, and a terminal outcome. Five `lesson.*` event types are registered in `coordination/contracts/event-types-registry.json`, all produced by Delivery: `lesson.scheduled`, `lesson.rescheduled`, `lesson.attended`, `lesson.delivered`, and `lesson.cancelled`. Each is named in `coordination/domains/delivery.md` §"Primary interfaces" as a Delivery-produced event.
15
+
16
+ At v1.1.0 this contract pins the payload shapes for `lesson.delivered`, `lesson.cancelled`, and `lesson.rescheduled`. `lesson.delivered` and `lesson.cancelled` are emitted by the Delivery lesson outcome write path (see `delivery/docs/outcome-write-side-postgres-design.md`). `lesson.rescheduled` is emitted by the Delivery reschedule write path when a scheduled lesson's window changes. `lesson.scheduled` and `lesson.attended` are documented here as part of the family in §4 but do not yet have payload schemas; they acquire schemas when a binding consumer registers, per §9.
17
+
18
+ Out of scope: the Lesson record itself (Delivery-internal, lives in the Delivery repo's Prisma model), the credit reservation lock state machine (that is the Credit Reservation Lock Contract, Revenue-owned; `lesson.delivered` triggers Revenue's Lesson Completion Job but this contract does not specify that job), attendance reconciliation internals (Delivery-internal), the dispatcher transport (ADR-0009 and the dispatcher SDK), and the cancellation submission to Revenue's lock state machine (a separate call, see §4.4 and §3).
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
+ - **Lesson.** A Delivery-owned scheduling fact per `coordination/domains/delivery.md`. Identified by `les_<UUID v7 canonical>` per ADR-0002.
27
+ - **Outcome.** A lesson's terminal status. Delivery's lesson outcome write path moves a lesson to one of three terminal statuses: `Completed`, `Canceled`, or `No-Show`. `lesson.delivered` fires on `Completed`; `lesson.cancelled` fires on `Canceled`. No-Show does not have a registered lifecycle event at v1.0.0 (the credit consequence of a no-show is carried by Revenue's `credit.forfeited`, not by a `lesson.*` event).
28
+ - **The credit path is separate.** A lesson outcome and the credit consequence of that outcome are distinct. `lesson.delivered` is the lifecycle signal that *triggers* Revenue's Lesson Completion Job; the Job's own state transition (`locked` to `consumed`) and ledger postings are Revenue-owned and surface as `credit.consumed`, not as a `lesson.*` event. `lesson.cancelled` is purely a lifecycle notification: the cancellation submission to Revenue's lock state machine is a separate call that produces `credit.released` or `credit.forfeited` per Credit Reservation Lock Contract §6 and §12.4. A consumer wanting the credit truth subscribes to the `credit.*` events, not to these.
29
+
30
+ ## 4. Event types
31
+
32
+ ### 4.1 lesson.scheduled
33
+
34
+ Fires when a lesson is scheduled. Producer: Delivery. No payload schema at v1.0.0; no cross-domain consumer is registered. Coaching MAY subscribe in a future version for assigned-versus-eligible projection enrichment per the coach-availability contract §4.2.1 producer-internal note. The payload schema lands when a binding consumer registers, per §9.
35
+
36
+ ### 4.2 lesson.attended
37
+
38
+ Fires on attendance reconciliation. Producer: Delivery. No payload schema at v1.0.0; no cross-domain consumer is registered. The payload schema lands when a binding consumer registers, per §9.
39
+
40
+ ### 4.3 lesson.delivered
41
+
42
+ Fires when a lesson reaches the `Completed` terminal state. The lifecycle signal that triggers Revenue's Lesson Completion Job.
43
+
44
+ Producer SHALL: emit exactly once per lesson delivery, inside the same Prisma transaction that writes the lesson's terminal status, per the producer-transactional-guarantee shape in ADR-0009.
45
+
46
+ Consumer: Revenue's Lesson Completion Job subscribes. On `lesson.delivered` it transitions the lesson's credit reservation from `locked` to `consumed` and posts the Adjustment (reason Credits Consumed) and Lesson Debit ledger entries, per Credit Reservation Lock Contract §10. The Job SHALL be idempotent per that contract; `credit_reservation_id`, when present on the payload, is the idempotency-key input.
47
+
48
+ Payload schema: `schema/payloads/lesson.delivered-v1.json`. Fields: `lesson_id` (required), `credit_reservation_id` (optional; present for reserved lessons, omitted for lessons delivered without an active reservation), `delivered_at` (required).
49
+
50
+ ### 4.4 lesson.cancelled
51
+
52
+ Fires when a lesson reaches the `Canceled` terminal state. A lifecycle notification only.
53
+
54
+ Producer SHALL: emit exactly once per lesson cancellation, inside the same Prisma transaction that writes the lesson's terminal status, per ADR-0009.
55
+
56
+ This event is deliberately decoupled from the credit path per §3. The cancellation submission to Revenue's lock state machine is a separate call that produces `credit.released` or `credit.forfeited` per Credit Reservation Lock Contract §6 and §12.4. `lesson.cancelled` does not carry a `credit_reservation_id` at v1.0.0 so that consumers are not invited to couple the lifecycle signal to the credit truth; adding it as an optional correlation field is a v2 candidate per §9 if a consumer need surfaces.
57
+
58
+ No cross-domain consumer is registered at v1.0.0. Subscribers MAY warehouse-load for lifecycle reporting.
59
+
60
+ Payload schema: `schema/payloads/lesson.cancelled-v1.json`. Fields: `lesson_id` (required), `cancellation_reason` (required, flexible string), `cancelled_at` (required).
61
+
62
+ ### 4.5 lesson.rescheduled
63
+
64
+ Fires when a scheduled lesson's date or time window changes in place. The lifecycle signal lets Sales keep lesson-date-keyed cadence state aligned without polling Delivery's current Lesson snapshot and guessing that a reschedule happened.
65
+
66
+ Producer SHALL: emit exactly once per successful window-changing reschedule, inside the same Prisma transaction that writes the new Lesson `startAt` and `endAt`, per ADR-0009.
67
+
68
+ Consumer: Sales cadence consumes day one. On `lesson.rescheduled`, Sales updates its Lead lesson-date projection from `new_window.start` for the Lead it already correlates to the Lesson. The event does not carry `lead_id`, Sales substage, or Sales cadence state; Sales owns that correlation.
69
+
70
+ Payload schema: `schema/payloads/lesson.rescheduled-v1.json`. Fields: `organization_id` (required), `lesson_id` (required), `reservation_lock_id` (required nullable), `reservation_id` (required nullable), `participant_id` (required nullable), `previous_window` (required), `new_window` (required), `previous_coach_id` (required nullable), `new_coach_id` (required nullable), `previous_lesson_site_id` (required nullable), `new_lesson_site_id` (required nullable), `service_area_id` (required nullable), `lesson_type_id` (required nullable), `reschedule_reason` (required nullable), `rescheduled_at` (required), and `rescheduled_by` (required nullable). Sales reads `new_window.start` as the updated lesson date.
71
+
72
+ ## 5. Versioning policy
73
+
74
+ ### 5.1 Semantic versioning
75
+
76
+ Per-event-type payload schemas version independently of this contract document and of the event envelope, per the event envelope contract §8.2. The contract version below tracks the document and the set of pinned schemas.
77
+
78
+ - **Patch** (1.0.0 to 1.0.1): editorial clarifications, typo fixes. No behavioral change.
79
+ - **Minor** (1.x.y to 1.x+1.0): additive changes. A new optional payload field (a new payload `schema_version`), a new event type entering the family, a payload schema pinned for a previously-unpinned event type. Consumers on older minors continue to work.
80
+ - **Major** (1.x.y to 2.0.0): breaking changes. Field removal, type change, narrowing a flexible string to an enum that drops in-use values, breaking payload changes.
81
+
82
+ ### 5.2 Deprecation policy
83
+
84
+ On major-version publication, the previous major enters a two-week deprecation window per the org contract-currency standard. Consumers running against deprecated versions after the window contribute to drift.
85
+
86
+ ### 5.3 Additive discipline within a major
87
+
88
+ New payload fields MUST default to null or a backwards-compatible value and land as a new payload `schema_version`. New `cancellation_reason` values are additive by construction (the field is a flexible string). Consumers MUST treat unknown enum-like values as safe-to-ignore.
89
+
90
+ ## 6. Consumer responsibilities
91
+
92
+ Consumers SHALL dedup on the envelope `event_id`; the dispatcher SDK provides the dedup primitive but the at-most-once promise is the consumer's to honor. Consumers SHALL reference Delivery entities by canonical ID and SHALL NOT cache lesson state derived from these events beyond what the event asserts (these are lifecycle signals, not a lesson read API). Consumers SHALL treat unknown `cancellation_reason` values as safe-to-ignore per §5.3.
93
+
94
+ For `lesson.delivered` specifically: Revenue's Lesson Completion Job SHALL be idempotent per Credit Reservation Lock Contract §10, since the dispatcher's at-least-once delivery plus retry may deliver the same `lesson.delivered` more than once.
95
+
96
+ ## 7. Producer responsibilities (Delivery)
97
+
98
+ Delivery SHALL emit exactly one `lesson.delivered` per lesson reaching `Completed`, exactly one `lesson.cancelled` per lesson reaching `Canceled`, and exactly one `lesson.rescheduled` per successful window-changing reschedule, each inside the same Prisma transaction that writes the domain fact, per ADR-0009's producer-transactional-guarantee.
99
+
100
+ Delivery SHALL emit canonical entity IDs on the payloads: `les_` for `lesson_id` and `crr_` for `credit_reservation_id`. Delivery resolves the `crr_` reservation id from its `reservation_lock` mirror at emit time; when the delivered lesson had no active reservation, Delivery SHALL omit `credit_reservation_id` rather than emit a placeholder.
101
+
102
+ Delivery SHALL set `occurred_at` on the envelope to its wall-clock at the transaction commit and SHALL NOT backdate or postdate, per the event envelope contract §4.3. The payload's `delivered_at`, `cancelled_at`, or `rescheduled_at` carry the same instant.
103
+
104
+ ## 8. Security and privacy
105
+
106
+ The pinned payloads do not carry PII. `lesson.delivered` carries canonical IDs (`les_`, `crr_`) and a timestamp. `lesson.cancelled` carries a canonical ID, a timestamp, and a `cancellation_reason` string drawn from an operational taxonomy that names no person. `lesson.rescheduled` carries Delivery and Revenue correlation IDs, before and after scheduling fields, an opaque reason, and an actor id. The envelope's `subject` and `actor` fields MAY carry a `per_` id per the event envelope contract; that is envelope-level PII handling and is out of scope for this contract.
107
+
108
+ ## 9. Future work
109
+
110
+ - **`lesson.scheduled` and `lesson.attended` payload schemas.** Pinned when each acquires a binding cross-domain consumer, per the registry's per-event-type schema-on-demand discipline. Pinning either is a minor version bump per §5.1.
111
+ - **`lesson.cancelled` v2 with `credit_reservation_id`.** If a consumer surfaces a need to correlate a cancellation to its reservation without a separate lookup, an optional `credit_reservation_id` lands as `lesson.cancelled` payload `schema_version` 2. Deliberately omitted at v1 per §4.4.
112
+ - **`cancellation_reason` enum tightening.** If the cancellation reason taxonomy stabilizes, tightening the flexible string to a closed enum is a candidate; narrowing that drops in-use values is a major bump per §5.1.
113
+ - **A `lesson.no_show` event.** No-Show is a lesson terminal outcome with no lifecycle event at v1.0.0. If a consumer needs the lifecycle signal distinct from Revenue's `credit.forfeited`, a `lesson.no_show` event type joins the family as a minor bump.
114
+
115
+ ## 10. Change log
116
+
117
+ - **v1.1.0** (2026-05-18). Adds `lesson.rescheduled` to the Lesson lifecycle family, pins `schema/payloads/lesson.rescheduled-v1.json`, and registers Sales cadence as the day-one consumer. The event is emitted transactionally from Delivery's reschedule write path and carries previous/new scheduling windows so Sales does not infer reschedules from current-state polling. Authored by Delivery per `2026-05-17-delivery-cadence-reschedule-event-position`.
118
+ - **v1.0.0** (2026-05-14). Initial contract. Documents the four-event Lesson lifecycle family; pins payload schemas for `lesson.delivered` and `lesson.cancelled` at payload `schema_version` 1. Authored by Delivery per the ownership confirmed on `2026-05-14-platform-dispatcher-producer-sdk-consumption`. Platform wires the registry `payload_schema` and `owning_contract` entries on notification per that memo.
@@ -0,0 +1,30 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://contracts.sguild/lesson-lifecycle/schema/payloads/lesson.cancelled-v1.json",
4
+ "title": "lesson.cancelled payload v1",
5
+ "description": "Payload for the lesson.cancelled event_type per contracts/lesson-lifecycle/README.md §4.4. Emitted by Delivery when a lesson reaches the Canceled terminal state, inside the same Prisma transaction that writes the lesson's terminal status (ADR-0009 producer-transactional-guarantee). This is the lesson-lifecycle notification only. It is deliberately decoupled from the credit path: the cancellation submission to Revenue's lock state machine is a separate call that produces credit.released or credit.forfeited per credit-reservation-lock §6 and §12.4, not this event. No cross-domain consumer is registered at v1.0.0.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": [
9
+ "lesson_id",
10
+ "cancellation_reason",
11
+ "cancelled_at"
12
+ ],
13
+ "properties": {
14
+ "lesson_id": {
15
+ "type": "string",
16
+ "description": "The cancelled lesson. les_<UUID v7 canonical> per ADR-0002.",
17
+ "pattern": "^les_[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$"
18
+ },
19
+ "cancellation_reason": {
20
+ "type": "string",
21
+ "description": "Why the lesson was cancelled. Flexible string at v1.0.0; the Delivery-internal values in use are 'Client Canceled', 'Coach Canceled', 'Bad Weather', 'Access Issue', 'Scheduling Error', and 'Duplicate Lesson'. Carried as a string rather than a closed enum so Delivery can extend the reason taxonomy without a contract version bump; tightening to an enum is a v2 candidate per §9.",
22
+ "minLength": 1
23
+ },
24
+ "cancelled_at": {
25
+ "type": "string",
26
+ "format": "date-time",
27
+ "description": "When Delivery recorded the lesson as cancelled (the lesson's terminal-status write). UTC. Per the event envelope contract §4.3, producers MUST NOT backdate or postdate."
28
+ }
29
+ }
30
+ }
@@ -0,0 +1,29 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://contracts.sguild/lesson-lifecycle/schema/payloads/lesson.delivered-v1.json",
4
+ "title": "lesson.delivered payload v1",
5
+ "description": "Payload for the lesson.delivered event_type per contracts/lesson-lifecycle/README.md §4.3. Emitted by Delivery when a lesson reaches the Completed terminal state, inside the same Prisma transaction that writes the lesson's terminal status (ADR-0009 producer-transactional-guarantee). Consumer: Revenue's Lesson Completion Job, which transitions the lesson's credit reservation from locked to consumed per credit-reservation-lock §10 and posts the Adjustment and Lesson Debit ledger entries.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": [
9
+ "lesson_id",
10
+ "delivered_at"
11
+ ],
12
+ "properties": {
13
+ "lesson_id": {
14
+ "type": "string",
15
+ "description": "The delivered lesson. les_<UUID v7 canonical> per ADR-0002. Revenue's Lesson Completion Job resolves the locked credit reservation from this when credit_reservation_id is absent.",
16
+ "pattern": "^les_[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$"
17
+ },
18
+ "credit_reservation_id": {
19
+ "type": "string",
20
+ "description": "The credit reservation tied to the delivered lesson, when one exists. crr_<UUID v7 canonical> per ADR-0002. Present for reserved lessons; omitted for lessons delivered without an active reservation (free trials, unreserved sessions). Revenue's Lesson Completion Job SHALL use it as the idempotency-key input per credit-reservation-lock §10 when present, and fall back to a lesson_id lookup when absent.",
21
+ "pattern": "^crr_[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$"
22
+ },
23
+ "delivered_at": {
24
+ "type": "string",
25
+ "format": "date-time",
26
+ "description": "When Delivery recorded the lesson as delivered (the lesson's terminal-status write). UTC. Per the event envelope contract §4.3, producers MUST NOT backdate or postdate."
27
+ }
28
+ }
29
+ }
@@ -0,0 +1,157 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://contracts.sguild/lesson-lifecycle/schema/payloads/lesson.rescheduled-v1.json",
4
+ "title": "lesson.rescheduled payload v1",
5
+ "description": "Payload for the lesson.rescheduled event_type per contracts/lesson-lifecycle/README.md section 4.5. Emitted by Delivery when a scheduled lesson's window changes, inside the same Prisma transaction that writes the new lesson window. Consumer: Sales cadence, which updates its lesson-date keyed follow-up state from new_window.start.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": [
9
+ "organization_id",
10
+ "lesson_id",
11
+ "reservation_lock_id",
12
+ "reservation_id",
13
+ "participant_id",
14
+ "previous_window",
15
+ "new_window",
16
+ "previous_coach_id",
17
+ "new_coach_id",
18
+ "previous_lesson_site_id",
19
+ "new_lesson_site_id",
20
+ "service_area_id",
21
+ "lesson_type_id",
22
+ "reschedule_reason",
23
+ "rescheduled_at",
24
+ "rescheduled_by"
25
+ ],
26
+ "properties": {
27
+ "organization_id": {
28
+ "type": "string",
29
+ "description": "Delivery tenancy scope for the lesson.",
30
+ "minLength": 1
31
+ },
32
+ "lesson_id": {
33
+ "type": "string",
34
+ "description": "The rescheduled lesson. les_<UUID v7 canonical> per ADR-0002.",
35
+ "pattern": "^les_[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$"
36
+ },
37
+ "reservation_lock_id": {
38
+ "type": [
39
+ "string",
40
+ "null"
41
+ ],
42
+ "description": "Delivery reservation-lock correlation when the lesson is lock-backed. lck_ is the canonical contract prefix; rlk_ is accepted for existing Delivery rows during prefix alignment.",
43
+ "pattern": "^(lck|rlk)_[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$"
44
+ },
45
+ "reservation_id": {
46
+ "type": [
47
+ "string",
48
+ "null"
49
+ ],
50
+ "description": "Revenue credit reservation correlation when present. crr_<UUID v7 canonical> per ADR-0002.",
51
+ "pattern": "^crr_[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$"
52
+ },
53
+ "participant_id": {
54
+ "type": [
55
+ "string",
56
+ "null"
57
+ ],
58
+ "description": "Delivery Participant taking the lesson when known. par_<UUID v7 canonical> per ADR-0002; null only for legacy rows whose participant was not source-backed at migration time.",
59
+ "pattern": "^par_[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$"
60
+ },
61
+ "previous_window": {
62
+ "$ref": "#/$defs/window"
63
+ },
64
+ "new_window": {
65
+ "$ref": "#/$defs/window"
66
+ },
67
+ "previous_coach_id": {
68
+ "type": [
69
+ "string",
70
+ "null"
71
+ ],
72
+ "description": "Coach assigned before the reschedule, when present. coa_<UUID v7 canonical> per ADR-0002.",
73
+ "pattern": "^coa_[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$"
74
+ },
75
+ "new_coach_id": {
76
+ "type": [
77
+ "string",
78
+ "null"
79
+ ],
80
+ "description": "Coach assigned after the reschedule, when present. May equal previous_coach_id for time-only reschedules.",
81
+ "pattern": "^coa_[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$"
82
+ },
83
+ "previous_lesson_site_id": {
84
+ "type": [
85
+ "string",
86
+ "null"
87
+ ],
88
+ "description": "Lesson site before the reschedule, when present. lsi_<UUID v7 canonical> per ADR-0002.",
89
+ "pattern": "^lsi_[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$"
90
+ },
91
+ "new_lesson_site_id": {
92
+ "type": [
93
+ "string",
94
+ "null"
95
+ ],
96
+ "description": "Lesson site after the reschedule, when present. May equal previous_lesson_site_id for time-only reschedules.",
97
+ "pattern": "^lsi_[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$"
98
+ },
99
+ "service_area_id": {
100
+ "type": [
101
+ "string",
102
+ "null"
103
+ ],
104
+ "description": "Service area associated with the lesson when known. sva_<UUID v7 canonical> per ADR-0002.",
105
+ "pattern": "^sva_[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$"
106
+ },
107
+ "lesson_type_id": {
108
+ "type": [
109
+ "string",
110
+ "null"
111
+ ],
112
+ "description": "Lesson type associated with the lesson when known. lty_<UUID v7 canonical> per ADR-0002.",
113
+ "pattern": "^lty_[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$"
114
+ },
115
+ "reschedule_reason": {
116
+ "type": [
117
+ "string",
118
+ "null"
119
+ ],
120
+ "description": "Optional operational reason. Opaque to consumers.",
121
+ "minLength": 1
122
+ },
123
+ "rescheduled_at": {
124
+ "type": "string",
125
+ "format": "date-time",
126
+ "description": "When Delivery recorded the reschedule. UTC. Per the event envelope contract, producers MUST NOT backdate or postdate."
127
+ },
128
+ "rescheduled_by": {
129
+ "type": [
130
+ "string",
131
+ "null"
132
+ ],
133
+ "description": "Operator or system actor id responsible for the reschedule; null only when unavailable.",
134
+ "minLength": 1
135
+ }
136
+ },
137
+ "$defs": {
138
+ "window": {
139
+ "type": "object",
140
+ "additionalProperties": false,
141
+ "required": [
142
+ "start",
143
+ "end"
144
+ ],
145
+ "properties": {
146
+ "start": {
147
+ "type": "string",
148
+ "format": "date-time"
149
+ },
150
+ "end": {
151
+ "type": "string",
152
+ "format": "date-time"
153
+ }
154
+ }
155
+ }
156
+ }
157
+ }