Skip to content
StarLive demo →
ModuleChecked against main · 31 Oct 20268 min read

Booking module

The Booking module schedules, reschedules and cancels appointments, keeps local copies of companies, staff and customers fed by events, and serves the dashboard statistics.

Owns
Appointments and the dashboard statistics
Schema
booking, four tables
Endpoints
8, behind appointments permissions
Copies
Companies, staff and customers from other modules

Booking owns appointments, and keeps its own copies of the companies, staff and customers it needs, so it never reads another module’s tables.

Feature Folder What it does
Appointments Appointments/ Schedule, reschedule, cancel, list by date range, get by id. Status is Scheduled or Cancelled.
Service providers ServiceProviders/ A copy of every Organizations employee with the Staff role, under the same id.
Attendees Attendees/ A copy of every CRM customer, under the same id.
Booking companies BookingCompanies/ A copy of each company’s id, name and time zone. Appointments reference it.
Dashboard Dashboard/ Appointment counts, booked minutes, utilization, returning-client rate, trends, staff load and peak hours.
Demo data SeedDemoAppointments/, TopUpDemoAppointments/ Up to three appointments per seeded demo attendee, plus a daily top-up.
Contracts AppointMe.Booking.Contracts Three appointment events, plus AttendeeProjected and ServiceProviderProjected.

The three copies are projections: other modules’ events keep them current, and recurring jobs repair them. The staff and customer copies have their own ID types (ServiceProviderId, AttendeeId), and BookingCompany reuses CompanyId, as described in IDs & value objects.

  • Directorysrc/Booking/
    • DirectoryAppointMe.Booking/
      • DirectoryAppointments/
        • DirectoryScheduleAppointment/
          • …
        • DirectoryRescheduleAppointment/
          • …
        • DirectoryCancelAppointment/
          • …
        • …
      • DirectoryAttendees/
        • DirectoryReconcileAttendees/ # CRM event handlers, daily job
          • …
        • …
      • DirectoryBookingCompanies/
        • DirectoryReconcileBookingCompanies/ # Organizations event handler, hourly job
          • …
        • …
      • DirectoryServiceProviders/
        • DirectoryReconcileServiceProviders/ # Organizations event handlers, hourly job
          • …
        • …
      • DirectoryDashboard/ # GetDashboardStats, GetPeakHours
        • …
      • DirectoryConfiguration/ # AddBookingModule
        • …
      • DirectoryDatabase/ # BookingDbContext, BookingFilters, migrations
        • …
      • …
    • DirectoryAppointMe.Booking.Contracts/
      • …
    • DirectoryAppointMe.Booking.Tests/
      • …

Eight endpoints, all needing an appointments permission and X-Company-Id, and no write endpoints for providers or attendees, because Organizations and CRM own them.

Method Route Permission Purpose
GET /api/v1/appointments appointments:view Appointments that overlap From to To, cancelled ones included, sorted by start, with provider and attendee names.
GET /api/v1/appointments/{id} appointments:view One appointment with names.
POST /api/v1/appointments appointments:schedule Body { providerId, attendeeId, start, end, notes? }. Checks that the provider and attendee exist in the company. Returns 201 with the id.
PUT /api/v1/appointments/{id}/reschedule appointments:reschedule Body { providerId, start, end }. Only a Scheduled appointment. Returns 204.
POST /api/v1/appointments/{id}/cancel appointments:cancel Sets Cancelled. Only a Scheduled appointment. Returns 204.
GET /api/v1/booking/service-providers appointments:view Active providers, sorted by name.
GET /api/v1/booking/dashboard/stats appointments.statistics:view Query From, To, Bucket (day, week or month), TimeZone, and optionally CompareFrom with CompareTo. KPIs, trend buckets and per-provider load.
GET /api/v1/booking/dashboard/peak-hours appointments.statistics:view Query From, To, TimeZone. Average bookings for each weekday and hour.

Every time range goes through DateTimeOffsetPeriod.Create, so it must be UTC with the end after the start, or the call fails with 400. Scheduling doesn’t check for overlaps: a provider can be double-booked.

The dashboard counts only scheduled appointments of active providers. Capacity is a fixed convention, not real working hours:

src/Booking/AppointMe.Booking/Dashboard/GetDashboardStats/DashboardStatsCalculator.csView source ↗
// Bookable-capacity convention for the template: every active provider is assumed
// bookable 8 hours per business day (Mon–Sat). Replace with real working hours
// when a scheduling domain exists.
private const int BookableHoursPerBusinessDay = 8;

Five permissions in AppointmentPermissions: every role can book, but by default only Owner and Manager see the dashboard.

Permission Key Owner Manager Staff Receptionist
AppointmentPermissions.View appointments:view ✓ ✓ ✓ ✓
AppointmentPermissions.Schedule appointments:schedule ✓ ✓ ✓ ✓
AppointmentPermissions.Reschedule appointments:reschedule ✓ ✓ ✓ ✓
AppointmentPermissions.Cancel appointments:cancel ✓ ✓ ✓ ✓
AppointmentPermissions.ViewStatistics appointments.statistics:view ✓ ✓ — —

The defaults are in BookingDefaultGrantPolicy. GET /booking/service-providers has no permission of its own and reuses appointments:view. How defaults and overrides combine: Permissions.

Booking consumes seven events from Organizations and CRM to keep its copies current, and publishes five of its own.

Consumed event From Handler What it does
CompanyRegistered Organizations CompanyRegisteredEventHandler Upserts the BookingCompany.
EmployeeRegistered Organizations EmployeeRegisteredEventHandler Runs ServiceProviderSynchronizer: creates, updates or restores the provider if the employee has Staff, and soft-deletes an existing one if not.
EmployeeRolesUpdated Organizations EmployeeRolesUpdatedEventHandler Same synchronizer.
EmployeeDeleted Organizations EmployeeDeletedEventHandler Soft-deletes the provider.
CustomerRegisteredEvent CRM CustomerRegisteredEventHandler Upserts the attendee through AttendeeSynchronizer.
CustomerUpdatedEvent CRM CustomerUpdatedEventHandler Upserts the attendee.
CustomerDeletedEvent CRM CustomerDeletedEventHandler Soft-deletes the attendee.

The synchronizer decides from the employee’s roles alone:

src/Booking/AppointMe.Booking/ServiceProviders/ReconcileServiceProviders/ServiceProviderSynchronizer.csView source ↗
var isStaff = snapshot?.Roles.Contains(Role.Staff) == true;
await ((existing, snapshot, isStaff) switch
{
// Doesn't exist, needs to be created
(existing: null, snapshot: not null, isStaff: true) =>
CreateServiceProvider(snapshot, cancellationToken),
// Exists, needs update (and potential restoration)
(existing: not null, snapshot: not null, isStaff: true) =>
UpdateServiceProvider(existing, snapshot),
// Exists but no longer has role, needs deletion
(existing: not null, snapshot: _, isStaff: false) =>
DeleteServiceProvider(existing),
_ => Task.CompletedTask
});
Published event Raised by Handled by
AppointmentScheduledEvent Appointment.Schedule Nothing yet.
AppointmentRescheduledEvent appointment.Reschedule Nothing yet.
AppointmentCancelledEvent appointment.Cancel Nothing yet.
ServiceProviderProjected ServiceProvider.Create The API host’s DemoSeedingSaga seeds customers after the first provider appears.
AttendeeProjected Attendee.Create DemoSeedingSaga seeds up to three appointments for each of the first 150 attendees.

Booking also handles commands: SeedDemoAppointmentsCommand from the saga, and its own reconcile and top-up commands. How the handlers get their company and run as the system identity: Domain events & messaging.

Four tables in the booking schema, three jobs that repair the copies if an event goes missing, and one demo-only job.

Table Entity Query filters Notes
booking.Appointments Appointment CompanyId No soft delete: cancelling sets Status. Start, End and ScheduledAt are UTC datetime2. References BookingCompanies.
booking.ServiceProviders ServiceProvider SoftDelete, CompanyId Id is the Organizations EmployeeId.
booking.Attendees Attendee SoftDelete, CompanyId Id is the CRM CustomerId.
booking.BookingCompanies BookingCompany None Id is the CompanyId. The per-company jobs loop over this table.
Job id Schedule What it does
booking:company-reconciliation 0 * * * * (hourly) Reads every company through ICompanyRehydrationSource and upserts BookingCompanies.
booking:service-provider-reconciliation 0 * * * * (hourly) For each company, compares providers with IEmployeeRehydrationSource and runs the synchronizer on every pair.
booking:attendee-reconciliation 0 3 * * * (daily, 03:00) For each company, pages through ICustomerRehydrationSource 100 at a time and upserts attendees.
booking:top-up-demo-appointments 0 3 * * * (daily, 03:00) Only when Demo:Enabled is on: 15 appointments per company.

AddBookingModule() takes no configuration. It registers BookingDbContext (schema booking), the endpoints, three Dapper repositories, the synchronizers, the job classes, the demo seeder, the permissions and the recurring-job registrars. How the jobs run and get their company: Background jobs.

Why copy companies, staff and customers into Booking?2 min · optional

Booking never queries the organizations or crm schemas. It keeps its own rows, keyed by the same GUIDs, and the calendar query is one SQL statement inside booking. Organizations and CRM can change their tables without breaking it.

The cost is eventual consistency, which the Gotcha above shows. Events keep the copies close, and the reconciliation jobs pull full snapshots through the Contracts interfaces to repair anything an event missed.

Most Booking changes are in one handler or one calculator.

Change Start here
Prevent double-booking ScheduleAppointmentCommandHandler and RescheduleAppointmentCommandHandler: look for a Scheduled appointment of the same provider that overlaps the new period before saving.
Use real working hours BookableHoursPerBusinessDay in DashboardStatsCalculator, and StatsBucketing.CountBusinessDays in Shared for the Monday-to-Saturday rule.
Make another role bookable The isStaff check in ServiceProviderSynchronizer.
Copy another customer field Add it to the CRM events and CustomerSnapshot, then to Attendee, AttendeeTypeConfiguration and AttendeeSynchronizer, plus a migration.
React to appointments Nothing handles the appointment events yet, so a new handler in another module is the starting point. See Cross-module events. Notifications are planned for PRO: see Free vs PRO.

AppointMe.Booking.Tests covers every domain method, the DTO enricher and both dashboard calculators, but not the synchronizers, event handlers or reconciliation jobs.

Folder Covers
Appointments/ Schedule, reschedule and cancel, including rejecting a cancelled appointment. AppointmentDtoEnricher names and initials.
Attendees/, ServiceProviders/, BookingCompanies/ The domain methods on each copy: create and update on all three, delete on attendees and providers, restore on providers.
Dashboard/ DashboardStatsCalculator: cancelled excluded, time-zone buckets, Monday weeks, returning clients, Sunday-free capacity, idle providers, compare periods. PeakHoursCalculator: seven days of 24 hourly averages. Both handlers reject a caller without appointments.statistics:view.
Terminal window
dotnet test src/Booking/AppointMe.Booking.Tests