# Courses

The **courses** are a domain of a **studio that offers courses with a few seats each**: the **studio** schedules its courses and calls a few off, and **customers** reserve seats, book them, pay, and sometimes cancel. In a request, the domain is named **`courses`**.

## Subjects

- **A course** is a subject of its own, **`/courses/<uuid>`**. It guards its **seats**, since every rule about a seat asks how many of them are taken: reserving, booking, and releasing seats are events of the course.
- **A booking** is a subject of its own, **`/bookings/<code>`**, under a short **code** such as `B3R2J5` rather than a UUID, since a code is what people read out and type. It is a `B` and five characters from `23456789ABCDEFGHJKMNPQRSTUVWXYZ`, which leaves out what is easily misread. A booking holds its payments and how it ends.
- **A reservation** is not a subject of its own, but a hold on seats of a course for a while, named by a **`reservationId`** within the course.
- **Customers** have no events of their own. They appear only as references in the data, as **`/customers/<uuid>`** in `bookedBy`.

For more on subjects, see **[Subjects](/docs/eventsourcingdb/subjects)**.

## Events

| Event type | What it records | Data |
|---|---|---|
| `io.synthevents.courses.course-scheduled` | The studio offered a course with its sessions. | `title`, `sessions`, `seats`, `fee` |
| `io.synthevents.courses.seats-reserved` | Seats of a course are held for somebody who is about to book. | `reservationId`, `seats`, `reservedAt`, `reservedUntil` |
| `io.synthevents.courses.reservation-canceled` | Somebody gave a reservation up. | `reservationId`, `canceledAt` |
| `io.synthevents.courses.seats-booked` | The seats of a reservation were booked for a booking. | `booking`, `seats`, `reservationId`, `bookedAt` |
| `io.synthevents.courses.seats-released` | The seats of a canceled booking are free again. | `booking`, `seats`, `releasedAt` |
| `io.synthevents.courses.course-canceled` | The studio called a course off. | `canceledAt` |
| `io.synthevents.courses.booking-placed` | A booking was placed for a course. | `course`, `bookedBy`, `seats`, `total`, `courseStartsAt`, `payableUntil`, `reservationId`, `placedAt` |
| `io.synthevents.courses.booking-confirmed` | The seats of a booking are booked, so the booking holds. | `confirmedAt` |
| `io.synthevents.courses.booking-failed` | The seats of a booking could not be booked. | `failedAt` |
| `io.synthevents.courses.payment-received` | A payment for a booking came in. | `amount`, `method`, `isFinal`, `receivedAt` |
| `io.synthevents.courses.booking-paid` | A booking is paid. | `paidAt` |
| `io.synthevents.courses.booking-canceled` | A booking was canceled. | `reason`, `canceledAt` |

A booking runs over **two subjects in four steps**, since no command touches more than one: seats are **reserved** on the course, the booking is **placed**, the course **books** the reserved seats for it, and the booking is **confirmed**. If the seats cannot be booked, because the reservation lapsed and somebody else took them in the meantime, the booking **fails** instead. Canceling runs the other way round: the booking is **canceled**, and the course **releases** its seats.

## Data

- **`title`** names the course. **`sessions`** lists its sessions in order, each with **`startsAt`** and **`endsAt`**. A course **begins** with its first session.
- **`seats`** is a number of seats: those a course offers, a reservation holds, or a booking takes.
- **`fee`** is what a seat costs, **`total`** what a booking costs, and **`amount`** what a payment brings, each a sum of money in **cents**, with the unit and the currency written out, so that nobody takes the cents for euros: `{"currency": "EUR", "unit": "cent", "value": 20000}`.
- **`reservationId`** names a reservation within its course, as a UUID.
- **`course`** names a course as **`/courses/<uuid>`**, **`booking`** a booking as **`/bookings/<code>`**, and **`bookedBy`** a customer as **`/customers/<uuid>`**.
- **`courseStartsAt`** is when the course of a booking begins. The booking keeps it, since its rules see only their own subject.
- **`payableUntil`** is the **day** by which a booking is due to be paid, such as `2025-01-14`, in the time zone of the studio.
- **`method`** says how a payment came in: **`transfer`**, **`cash`**, **`online`**, or **`other`**. **`isFinal`** says whether the studio takes the payment as settling the booking, even if the sum falls short of the total.
- **`reason`** says why a booking was canceled: **`requested`**, **`course-canceled`**, or **`unpaid`**.
- **`reservedAt`**, **`canceledAt`**, **`bookedAt`**, **`releasedAt`**, **`placedAt`**, **`confirmedAt`**, **`failedAt`**, **`receivedAt`**, and **`paidAt`** are **business times**, in UTC to the **millisecond**, such as `2025-01-07T15:47:45.184Z`. They give the moment of the event, written the way an application usually captures it, which keeps them apart from the event's own `time`, which has nanoseconds. **`reservedUntil`**, **`courseStartsAt`**, and the times of the sessions are written the same way.

Every field is present in every event of its type, and no other field is. The **[backup for EventSourcingDB](/docs/synthevents/formats#backup-for-eventsourcingdb)** carries the schemas that say so.

A `booking-placed` event, formatted for reading, looks like this:

```json
{
  "specversion": "1.0",
  "id": "4",
  "source": "tag:synthevents.io,2026:synthevents;version=golden;domain=courses;scenario=everyday;from=2025-01-01T00:00:00Z;to=2026-01-01T00:00:00Z;eventCount=500;seed=golden",
  "subject": "/bookings/B9R8D5",
  "type": "io.synthevents.courses.booking-placed",
  "time": "2025-01-07T15:47:45.184613994Z",
  "datacontenttype": "application/json",
  "data": {
    "bookedBy": "/customers/358c6e2b-dfec-4b16-a919-7999e4ede485",
    "course": "/courses/f943a4d8-a841-477c-b540-128f13583bfd",
    "courseStartsAt": "2025-02-06T16:00:00.000Z",
    "payableUntil": "2025-01-14",
    "placedAt": "2025-01-07T15:47:45.184Z",
    "reservationId": "469cdebd-e899-4cda-8c27-d2f8c2585e7c",
    "seats": 3,
    "total": {
      "currency": "EUR",
      "unit": "cent",
      "value": 60000
    }
  }
}
```

*Note that this event comes from a test of SynthEvents, which generates its data sets with the version `golden` and the seed `golden`. In a data set of the public service, `version` names the version of SynthEvents that generated it.*

## Rules

The events follow the **rules of the studio**. A command that breaks one of them is refused and leaves no event.

### Courses

- **Scheduling** offers a course with a title, at least one session, at least one seat, and a fee of more than nothing. Its sessions lie ahead, come in order without overlapping, and each ends after it begins.
- What takes seats or calls the course off **ends with its first session**: seats can be reserved and booked only before, a booking can be canceled only before, and the course can be called off only before. What only gives seats back does not end: a reservation can be given up and the seats of a booking released at any time, since they follow something that happened in time.
- A course that is **called off** takes no new reservations and no new bookings, but still releases seats.

### Seats

- The **free seats** of a course are its seats, minus those booked and not released, minus those held by reservations that still hold.
- **Reserving** holds seats until **`reservedUntil`**, a moment the studio sets. When it passes, the seats are free again, **without an event** saying so. A reservation that lapsed can still be booked, as long as enough seats are free.
- **Booking** seats takes the seats of a reservation, and books seats for a booking only once.

### Bookings

- **Placing** a booking needs a code that is not taken: a code drawn twice is refused, and the customer draws another one. A booking takes at least one seat, costs more than nothing, and is placed before its course begins.
- **`payableUntil`** follows from the studio's policy: a number of days after the day the booking is placed, and fewer when the course begins soon. No rule depends on it; whether a booking is **overdue** follows from it and the time.
- Only a booking that is placed and still open can be **confirmed** or **fail**.

### Payments

- A **payment** comes in for a confirmed booking that is neither paid nor canceled, and brings more than nothing.
- When the payments reach the total, or when the studio takes a payment as **final**, `booking-paid` follows `payment-received`, with the same time, as what follows from it. A payment that brings more than is open is taken as it is.

### Cancellations

- **Canceling** cancels a confirmed booking as a whole, until its course begins, also when it is paid. Whether money goes back is not part of the domain.
- When the studio **calls a course off**, it first cancels the course and then each of its bookings, with the reason `course-canceled`.
- The **reason** is told, not checked: the rules of a booking cannot see its course.

### The Studio's Policy

The **moment**, the end of a **reservation**, and the **day a booking is due** are the studio's to set, not the customer's. The rules go by a **policy**: how long a reservation holds, how many days a booking has to be paid and how many when its course begins soon, the time zone of the studio, and its clock. They take every business time from the clock and reckon every day in the time zone. The policy is set by the **scenario**.

Every command except scheduling and placing decides on the **state as it is now**, as a desk does: a seat is refused because of what is true of the course, not because something happened to it in between.

## Scenarios

- **[Pottery Studio in Everyday Operation](/docs/synthevents/courses-everyday)** (`everyday`) is a pottery studio with its customers.
