Skip to content

feat(scheduling): add externally delivered scheduling - #129

Draft
KitKat31337 wants to merge 12 commits into
Calnode:mainfrom
KitKat31337:feat/scheduling-invitations-complete
Draft

KitKat31337 wants to merge 12 commits into
Calnode:mainfrom
KitKat31337:feat/scheduling-invitations-complete

Conversation

@KitKat31337

Copy link
Copy Markdown
Contributor

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:

  1. Invitation records and expiring tokens.
  2. Event-type duration policies and invitation overrides.
  3. Fixed-host restrictions using existing host roles.
  4. Scheduling windows enforced by the slot engine.
  5. Invitation lifecycle events and correlated booking webhooks.

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

  1. An event-type owner configures its default duration and, optionally, permitted duration overrides.
  2. An authenticated integration creates an invitation containing the recipient, event type, optional duration, eligible host restriction, date window, expiration, and external reference.
  3. Calnode returns an opaque scheduling URL once, at issuance.
  4. The calling system sends that URL to the customer.
  5. The customer chooses an available time and completes the event type’s current intake questions.
  6. Calnode creates the booking and consumes the invitation in the same database transaction.
  7. Signed lifecycle and booking webhooks allow the calling system to correlate the appointment with its original request.

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:

Method Endpoint Purpose
POST /v1/scheduling-invitations Create an invitation and return its scheduling URL
GET /v1/scheduling-invitations List the creator’s invitations with pagination
GET /v1/scheduling-invitations/{id} Read the retained configuration, status, external reference, and associated booking
GET /v1/scheduling-invitations/{id}/slots Preview invitation availability
POST /v1/scheduling-invitations/{id}/cancel Cancel an active, unconsumed invitation

Invitation 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}/slots
  • POST /v1/schedule/{token}/book

The 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_id must 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:

  • Creates the booking and assigned-host records.
  • Records recipient and intake information.
  • Conditionally transitions the invitation to booked.
  • Consumes the scheduling token.

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.created
  • scheduling_invitation.booked
  • scheduling_invitation.cancelled
  • scheduling_invitation.expired

Related 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.sql
  • 00072_invitation_booking_lifecycle.sql

These numbers reserve migration 00070 for 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.md with 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.

@pullfrog

pullfrog Bot commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Run failed. View the logs →

Pullfrog  | Rerun failed job ➔ | View workflow run | via Pullfrog | Using deepseek-v4.1-flash | 𝕏

@KitKat31337
KitKat31337 marked this pull request as draft October 2, 2026 17:40
@KitKat31337 KitKat31337 changed the title WIP: feat(scheduling): add externally delivered scheduling feat(scheduling): add externally delivered scheduling Oct 2, 2026
@KitKat31337

Copy link
Copy Markdown
Contributor Author

@shockalotti I... couldn't sleep...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant