Repository navigation
feat(scheduling): add externally delivered scheduling - #129
Draft
KitKat31337 wants to merge 12 commits into
Draft
KitKat31337 wants to merge 12 commits into
KitKat31337 wants to merge 12 commits into
Conversation
Establish the invitation resource and effective scheduling context from Calnode discussion Calnode#47 and issue Calnode#92. Snapshot duration, host roles, and scheduling windows with hashed expiring token groundwork. Keep public redemption, atomic booking claims, delivery, and webhook enrichment deferred.
Contributor
|
Run failed. View the logs →
|
KitKat31337
marked this pull request as draft
October 2, 2026 17:40
Contributor
Author
|
@shockalotti I... couldn't sleep... |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Summary
Adds customer-specific scheduling invitations so an external system can request an appointment using a reusable Calnode event type, with an authorized duration, host selection, and scheduling window.
For example, a ticketing system can create a 60-minute invitation for a particular technician, send the returned scheduling link to the customer, and receive signed webhook events containing the resulting booking and original ticket reference.
Calnode owns availability, host assignment, booking state, and subsequent appointment management. The calling system owns initial invitation delivery and its ticket workflow.
Implements the initial Calnode scope discussed in discussion #47 and tracked in issue #92.
WIP status
This draft brings the five planned implementation slices together for review:
The branch includes the Calnode workflow from invitation creation through booking and constrained appointment management. Live Microsoft 365/Teams, email, and external adapter integration testing remain outstanding.
The Zammad adapter and ticket synchronization are separate work.
External scheduling workflow
The initial invitation is immediately active and uses external delivery. Calnode does not send the initial scheduling-request email.
Invitation resource and API
Adds authenticated endpoints using existing API-key or staff-session authentication:
POST/v1/scheduling-invitationsGET/v1/scheduling-invitationsGET/v1/scheduling-invitations/{id}GET/v1/scheduling-invitations/{id}/slotsPOST/v1/scheduling-invitations/{id}/cancelInvitation access is scoped to the creator, who must own the source event type. Host membership or workspace administrator status alone does not grant access to another owner’s invitations.
Active private event types are supported. Paid event types are rejected in this initial implementation.
Public scheduling uses:
/s/{token}GET /v1/schedule/{token}/slotsPOST /v1/schedule/{token}/bookThe raw token and scheduling URL are returned only at issuance. Subsequent authenticated reads do not expose or reconstruct them.
Duration policies
Adds optional minimum, maximum, and increment fields to event types, with controls in the existing event-type editor.
Invitation duration overrides must satisfy the effective policy. Invalid values are rejected without rounding or clamping.
Existing event types retain fixed-duration behavior when the policy fields are unset. Ordinary public bookings continue to use the event type’s default duration.
The implementation also handles partial updates, explicit policy resets, event-type duplication, and legacy duration-only updates to explicitly fixed policies. Appointment duration remains separate from slot interval.
Retained constraints and shared availability
Each invitation retains its authorized recipient, duration, slot interval, buffers, host roles and routing strategy, scheduling window, location behavior, and external reference.
A supplied
host_idmust identify an active member of the event type’s host set and is retained as a required host. Without that restriction, the invitation snapshots the existing required, rotation, and optional host configuration.Availability, booking, and rescheduling use the existing slot engine with the effective invitation configuration. Working hours, date overrides, calendar conflicts, existing bookings, and intake questions remain live inputs.
Current notice and future-booking policies intersect the retained limits using the stricter values. Archived or disabled event types and archived required hosts prevent new bookings and rescheduling.
Date-only window bounds represent inclusive calendar dates in the supplied IANA timezone. Conversion uses calendar-date arithmetic to handle daylight-saving transitions. Precise RFC3339 bounds are also supported.
The entire appointment must fit inside the authorized window. Windows do not bypass working hours, notice requirements, future limits, or conflicts.
Single-use booking and appointment management
Booking submission loads authoritative values from the invitation. Customers cannot replace its recipient, duration, hosts, event type, window, or external metadata.
External calendars are checked before the booking transaction. Local availability and conflicts are then rechecked on the transaction connection using the shared slot engine.
The transaction:
booked.A unique invitation-to-booking index provides an additional single-use invariant. Concurrent token redemption and competing local bookings are covered by regression tests.
Existing calendar, meeting, confirmation, and reminder work runs after commit using the persisted appointment interval.
Subsequent management uses the existing booking-management credential. Rescheduling retains duration, assigned hosts, window, and external correlation, and rechecks live availability. Host reassignment of invitation bookings is rejected.
Cancelling a booking leaves its original invitation booked and consumed. A new appointment request requires a new invitation.
The branch also addresses spanning-booking and retained-buffer conflict checks, and returns a conflict response for invitation rescheduling races.
Signed lifecycle and booking webhooks
Adds these subscription events to the existing webhook API and editor:
scheduling_invitation.createdscheduling_invitation.bookedscheduling_invitation.cancelledscheduling_invitation.expiredRelated booking creation, rescheduling, and cancellation events include invitation correlation, effective duration, appointment interval, assigned hosts, available location information, and generic external metadata such as a ticket reference.
Invitation lifecycle events reach the creator’s subscriptions. Related booking events reach both the primary host’s and creator’s subscriptions, with deduplication when they are the same owner.
Lifecycle transitions capture payloads in a transactional outbox. The worker expires eligible invitations and moves pending events into the existing signed delivery and retry jobs.
Delivery is at least once, with stable event and delivery identifiers for receiver deduplication. Ordering is not guaranteed. A booked event can arrive before booking creation delivery or generated meeting-link availability.
Existing booking side-effect enqueueing remains best effort after commit. The remaining commit-to-enqueue recovery gap is tracked in the fork’s booking-event durability issue.
Credential handling
Scheduling tokens contain 32 cryptographically random bytes and are stored only as SHA-256 hashes.
Credential endpoints include rate limits, request-log token redaction, no-store responses, no-referrer policy, and noindex/nofollow headers.
Invitation and related management pages use a restrictive CSP and omit analytics, arbitrary injected head scripts, assistants, Markdown embeds, and remote images. Same-origin branding and existing intake components are reused.
Public responses do not expose external ticket metadata, and webhook payloads do not contain scheduling credentials.
The link grants scheduling authority for its named recipient; it does not independently verify the visitor’s identity.
Database migrations
Adds:
00071_scheduling_invitations.sql00072_invitation_booking_lifecycle.sqlThese numbers reserve migration
00070for directory-order PR #127, which is assumed to land first. The directory feature remains separate from this PR.Migration tests cover populated databases at versions 69, 70, and 71, data preservation, lifecycle backfills, uniqueness constraints, rollback, and re-upgrade.
Earlier experimental invitation builds used versions
00070/00071. Their databases require a separate compatibility upgrade if their data must be retained; their applied Goose history must not be rewritten.Test coverage and documentation
Adds regression coverage for ownership, duration policies, retained host configuration, timezone and DST boundaries, whole-appointment window enforcement, token states, submission tampering, concurrent consumption, competing bookings, management constraints, rescheduling races, signed webhook correlation, expiration processing, credential-log redaction, and migrations.
Updates the changelog and architecture documentation, and adds
docs/SCHEDULING_INVITATIONS.mdwith API examples, receiver guidance, snapshot semantics, migration coordination, and current limitations.Remaining review and integration work
Before deployment, verify live Microsoft 365/Teams meeting creation, all-host calendar updates and cancellation, confirmation and reminder delivery, and the separately implemented ticket adapter.
External calendars can change between their final availability check and database commit. Local Calnode conflicts and invitation consumption are transactional; external provider operations retain the existing best-effort boundary.
Deferred scope includes verification codes, token replacement, draft/edit/activation flows, initial invitation emails, payments, a polished invitation-management dashboard, ticket adapters and synchronization, and invitation retention/purging.
Creation has no idempotency key in this initial API. Lost issuance responses cannot recover the scheduling credential. New invitation terminal and error messages are currently English.
Refs #92.