# The ClassBookings kit — classes, courses and lessons with places, a waiting list and a register

> `Osysharp.ClassBookings` is bookable sessions with a fixed number of numbered places that are never overbooked: a first-come waiting list whose freed places are offered for a limited time (or given at once) on a durable clock, booking windows, late cancellations and no-shows heard as events, check-in, walk-ins and a register that closes by itself, reminders, weekly series in the app's time zone with one-off changes, instructors, rooms and equipment that no two sessions hold at once, app-defined booking rules, and booking for the people one acts for — with a timetable, a my-bookings page and the register.

<!-- id: scheduling-class-bookings-kit · area: scheduling · stability: preview · html: https://osysharp.com/reference/scheduling/class-bookings-kit/ -->

## Summary        {#summary}
`use Osysharp.ClassBookings@0;` gives an app sessions people book places in. The app derives its own kind of session, says
who runs them, and — if people book for others — who acts for whom:

```osy syntax
entity CookingClass : BookableSession { … security { allow read when IsAuthenticated; } }
policy ManagesSessions => IsDesk;
policy TakesAttendance => IsDesk;                    // a session's own instructor takes its register anyway
policy BooksFor(Principal person) => person == user || ActsForDependant(person);
```

## Signature      {#signature}
```osy syntax
virtual policy ManagesSessions => false;   virtual policy TakesAttendance => ManagesSessions;
virtual policy BooksSessions => IsAuthenticated;   virtual policy BooksFor(Principal person) => person == user;
virtual policy VisitorsSeePlacesLeft => false;   // may somebody NOT signed in see how full a session is
public policy TeachesSession(BookableSession session);
app.ClassBookings = new ClassBookingsSetup {
  OfferWindow = TimeSpan.FromHours(2), Waitlist = WaitlistMode.Offer,          // or Promote
  OpensBefore = TimeSpan.Zero, ClosesBefore = TimeSpan.Zero, CancelFreeUntil = TimeSpan.FromHours(12),
  RemindBefore = TimeSpan.FromHours(24), CloseRegisterAfter = TimeSpan.FromMinutes(30),
  SelfCheckIn = false, CheckInOpensBefore = TimeSpan.FromMinutes(30),
  People = new SessionPeople(), Maker = new SessionMaker(),
};
public interface IBookingRule { string? Refusal(BookableSession session, Principal person); }

BookingOutcome BookSession(BookableSession s, Principal person);   BookingOutcome CancelBooking(Booking b, bool waive = false);
BookingOutcome AcceptOffer(Booking b);   BookingOutcome DeclineOffer(Booking b);
BookingOutcome CheckIn(Booking b);   void UndoCheckIn(Booking b);   void MarkNoShow(Booking b);
BookingOutcome WalkIn(BookableSession s, Principal person);   int CloseRegister(BookableSession s);
void SetCapacity(BookableSession s, int capacity);   int CancelSession(BookableSession s);
void MoveSession(BookableSession s, DateTime startsAt, DateTime? endsAt = null);
SessionAssignment AssignResource(BookableSession s, SessionResource r);   void ReleaseResource(BookableSession s, SessionResource r);
SeriesPlan PlanSeries(SessionSeries series, DateOnly through);   List<BookingOutcome> BookSeries(SessionSeries series, Principal person);
int EndSeries(SessionSeries series, DateOnly from);   int ForgetBookingsOf(Principal person);
int PlacesLeft(BookableSession s);   int WaitingCount(BookableSession s);   int? QueuePosition(Booking b);
List<SessionLine> Timetable(DateTime from, int days, Principal person);   List<BookingLine> MyBookings(bool withPast = false);
// events: BookingConfirmed · JoinedWaitlist · PlaceOffered · OfferLapsed · BookingCancelled · LateCancellation ·
//         SessionCancelled · SessionMoved · PlacesChanged · SessionReminder · CheckedIn · NoShow
// pages: /timetable · /my-bookings · /register
```

## Description    {#description}

### Never overbooked — the data says so      {#places}
A held place is a numbered `Seat`, unique per session and never above its capacity, and a booking's state, seat and times
change only through the kit's functions. Two people booking the last place at once cannot both have it; the loser is put on
the waiting list or told why not. A session's capacity changes through `SetCapacity`, which never strands a held place,
gives added places to the people waiting, and is heard as `PlacesChanged` (with how many places there were) — for
whatever the app keeps or shows about the session itself.

### The waiting list      {#waitlist}
A full session puts a person at the back of its list (or refuses, when the session has none). A freed place goes to the
first person in line whom the app's rules still allow: offered and held for `OfferWindow` — never past the session's
booking close — then passed on, or, with `WaitlistMode.Promote`, booked for them at once. Each offer has its own durable
timer, so an answered offer is never undone by an old one. Places pass on through the session's clock, which runs as the
system, so a member who cancels never needs to see who is waiting.

### Windows, late cancellation and no-shows      {#windows}
Booking opens `OpensBefore` the start and closes `ClosesBefore` it. Giving up a confirmed place inside `CancelFreeUntil`
is a late cancellation: the place is still freed, the booking is marked, and the app hears `LateCancellation` — to take a
fee or a credit with its own payments. The desk may waive it. After the start only the desk cancels.

### The register      {#register}
Whoever `TakesAttendance`, and a session's own instructor, check people in, walk them in (a free place booked and checked
in at once, past the booking window and rules, never past the places) and mark them absent once the session has started.
Each session's clock reminds the people booked `RemindBefore` the start (`SessionReminder`) and closes the register
`CloseRegisterAfter` its end, marking anyone booked and not checked in absent (`NoShow`). A moved session re-arms both.

### What does a session hold: instructors, rooms, equipment?      {#resources}
A `SessionResource` is something a session holds — an instructor (with their account), a room, a lane, a piece of
equipment, with the most people it takes. `AssignResource` refuses a resource another session holds at an overlapping
time, and a room smaller than the session; `MoveSession` re-checks every resource at the new time.

### How do series and courses work?      {#series}
A `SessionSeries` is a weekly session at a local time in a time zone — 18:00 stays 18:00 through daylight saving.
`PlanSeries` makes the sessions not made yet, as the app's own kind (`SessionMaker`), holding the series' lead and place; a
moved or cancelled meeting stays as it is. `BookSeries` books a whole course; `EndSeries` cancels it from a day on.

### Who sees whom      {#privacy}
Everyone signed in sees places and counts; who holds a place is seen by the person, whoever booked or acts for them, the
session's instructor and whoever takes registers.

Somebody who has **not signed in** sees no counts until the app says so. Showing them the session is the app's own grant
on its session type; how full it is, is a second decision:

```osy syntax
entity YogaClass : BookableSession { security { allow read when IsAuthenticated || IsAnonymous; } }   // the class itself
policy VisitorsSeePlacesLeft => true;                                                                // …and how full it is
```

With it, a visitor reads the rows of bookings with every column that says whose they are hidden, so `PlacesLeft`,
`WaitingCount` and `Timetable` answer for them as for anyone. Without it they are refused — `PlacesLeft` throws
`NotAuthorized`, naming that line — rather than told that every place is free. With Osysharp.Privacy, `Osysharp.Privacy.ClassBookings` arrives by
itself and answers requests for a person's bookings.

## Examples       {#examples}

### A yoga studio with a weekly class and a booking rule      {#example-studio}
```osy title="a studio's own kind of class, its policies and settings, a rule, and a weekly series" test app=scheduling-class-bookings-kit
app YogaStudio {
  use Osysharp.Ui;
  use Osysharp.Workflow;
  use Osysharp.Identity@0;
  use Osysharp.ClassBookings@0;
  model "model/**/*.osy";
}

using Osysharp.Identity;
using Osysharp.ClassBookings;

[Role] enum StudioRole { Authenticator, Member, Teacher, Desk }
policy IsDesk => RoleGrant.Any(g => g.Member == user && g.Role == StudioRole.Desk);
policy ManagesSessions => IsDesk;
policy TakesAttendance => IsDesk;

app.ClassBookings = new ClassBookingsSetup { CancelFreeUntil = TimeSpan.FromHours(6), Waitlist = WaitlistMode.Promote };

/// The studio's own class: the kit's session with a level of its own.
entity YogaClass : BookableSession {
  [MaxLength(40)] string Level = "All levels";
  security { allow read when IsAuthenticated; allow create, update when ManagesSessions; }
}

/// Members only for the advanced class.
entity Membership {
  [Required] Principal Member;
  security { allow read when IsAuthenticated; }
}

class AdvancedNeedsMembership : IBookingRule {
  public string? Refusal(BookableSession session, Principal person) {
    var cls = YogaClass.FirstOrDefault(c => c.Id == session.Id);
    if (cls == null || cls.Level != "Advanced") { return null; }
    return Membership.Any(m => m.Member == person) ? null : "The advanced class is for members.";
  }
}

/// Every Tuesday at 07:00 in Stockholm for ten weeks.
SessionSeries MorningFlow() {
  return new SessionSeries {
    Title = "Morning flow", TimeZone = TimeZoneInfo.FindSystemTimeZoneById("Europe/Stockholm"), Day = DayOfWeek.Tuesday, StartsAt = new TimeOnly(7, 0),
    Length = TimeSpan.FromMinutes(75), Capacity = 14, From = DateOnly.FromDateTime(DateTime.UtcNow),
    Until = DateOnly.FromDateTime(DateTime.UtcNow).AddDays(70),
  };
}
```

## See also       {#see-also}
- [The Guardianship kit — who acts for whom: children, carers, consents and emergency contacts](https://osysharp.com/reference/security/guardianship-kit/) — `ActsForDependant`, for parents booking their children
- [The UserAccounts kit — sign-in done properly, once](https://osysharp.com/reference/security/accounts-kit/) — accounts for walk-ins who never sign in
- [Time zones — TimeZoneInfo](https://osysharp.com/reference/stdlib/zones/) — the time zone a series is planned in
