Availability
This page contains lot of information and details about Venue's availability for offline and online booking. Please remember that the Venue can manage its appointments with little restrictions than online booking and they're applied by locking the grid in agenda UI, while on the backend side of things, no strong enforcements are done.
Entities and logics
Venue
The Venue model has a simple availability logic based on time_table, which is composed by start_time, end_time, start_break, end_break and formatted as string, where each line is a day (starting from 0 - Sunday) and values are separated by ,.
The time_table can also be represented by bitmaks
==- Example:
,,,\n,,,\n09:00,18:00,,\n,9:00,12:00,14:00,18:00\n09:00,18:00\n09:00,18:00\n09:00,18:00
| MON | TUE | WED | THU | FRI | SAT | SUN |
|---|---|---|---|---|---|---|
| - | 9:00-18:00 | 9:00-12:00-14:00-18:00 | 9:00-18:00 | 9:00-18:00 | 9:00-18:00 | - |
| === |
Extra opening
The ExtraOpening model allow the override of Venue's time_table for a specific date. There are optional fields to precisely configure it for start_time, end_time, start_break and end_break. When none of these optional fields are set, we're in the case of an Extra Closing, that means that the salon is completely closed for that date.
Logics are the same as for Venue
==- Example of extra opening:
date: 1st January 2023
start_time: 10:00
end_time: 18:00
start_break: 12:00
end_break: 13:00
==- Example of extra closing:
date: 1st January 2023
start_time: -
end_time: -
start_break: -
end_break: -
===
Staff member
Things are a little more complicated with StaffMember model, which allow multiple time tables and a turnover (aka rota) over weeks.
There are time_table_1 and time_table_2, that works in the same way as for Venue. Then, a day_override_1 and day_override_2 string fields define presence (1 present, 0 absent) for each week day (7 values separated by ,) for either turn 1 and turn 2. Turnover is then defined by the weekly_shifts text field, containing either value 1 or 2 depending on the wanted turn for each week of the year (53 values).
!!! Time table fallback
If the StaffMember is present for a specific week day but its time_table_{N} is empty, the Venue's time_table will be used instead.
!!!
==- Example:
time_table_1
,,,\n,,,\n09:00,18:00,,\n,9:00,12:00,14:00,18:00\n09:00,18:00\n09:00,18:00\n09:00,18:00
| MON | TUE | WED | THU | FRI | SAT | SUN |
|---|---|---|---|---|---|---|
| - | 9:00-18:00 | 9:00-12:00-14:00-18:00 | 9:00-18:00 | 9:00-18:00 | 9:00-18:00 | - |
time_table_2
,,,\n,,,\n,,,\n,,,\n10:00,16:00\n10:00,16:00\n10:00,16:00
| MON | TUE | WED | THU | FRI | SAT | SUN |
|---|---|---|---|---|---|---|
| - | - | - | 10:00-16:00 | 10:00-16:00 | 10:00-16:00 | - |
day_override_1
0,0,1,1,1,1,1
| MON | TUE | WED | THU | FRI | SAT | SUN |
|---|---|---|---|---|---|---|
| - | present | present | present | present | present | - |
day_override_2
0,0,0,1,1,1,1
| MON | TUE | WED | THU | FRI | SAT | SUN |
|---|---|---|---|---|---|---|
| - | - | present | present | present | present | - |
weekly_shifts
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2
1st to 10th week of the year uses day_override_1 and time_table_1, from 11th to 13th week of the year uses day_override_2 and time_table_2
Custom time table
For a specific day, a StaffMember can have a CustomTimeTable that defines a custom time_table to be applied overriding any other setting (weekly_shifts, day_override_{N}, time_table_{N}). The behaviour is similar to Venue's Extra opening with the only difference that start_time and end_time are mandatory.
==- Example: date: 1st January 2023 start_time: 10:00 end_time: 18:00 start_break: 12:00 end_break: 13:00
Vacancy
The Vacancy model allow to StaffMember the completely unavailable feature that Venue's Extra opening provides.
It have time and duration (in seconds) fields to set a limited working hours for a specific day, but allow also to create multiple days-off through from_date and to_date fields; in this case, the whole days are unavailable with the StaffMember.
==- Example of single day vacancy:
time: 1st January 2023 10:00
duration: 3600
to_date: 1st January 2023
to_date: 1st January 2023
==- Example of multiple days vacancy:
time: 1st January 2023 00:00
duration: 259200
to_date: 1st January 2023
to_date: 3rd January 2023
===
Workstation
The Workstation model is listed here for consistency, since it's an entity that can be required to book an appointment, but there are no custom time tables or opening/closure logics applied to it.
Staff member treatment
As defined in StaffMemberTreatment dedicated page, this model is bookable and defines duration, additional_duration and extra_time_after fields (inherit from parent when blank or not defined).
Appointment
The Appointment model is the result of a booking when availability is present.
Availability related fields:
time_in_tz: when it startsduration: duration (in seconds) for the staff to perform itadditional_duration: duration (in seconds) where the customer waits and the staff is freecustom_duration: an override to bothdurationandadditional_durationfor the appointmentextra_time_after: duration (in seconds) needed to clean the place after the service
Hairdressr::Appointable
Hairdressr::Appointable is a module used by Venue, Staff Member and Workstation to make the availability from the Model's point of view easier to compute, with the functions:
is_present?: tells if the entity is present (not on vacation) in a dayhas_enough_space?: tells if the entity could be booked for a specific appointment (considering the appointments already booked)daily_bit_availability: return the availability bitmask of the entity (considering the appointments already booked)
Default appointment validations
There are a few validations on Appointment model that are applied for both offline and online booking, and they're pretty much all data consistency validation:
customermust be present ifstatus >= bookedcustomermust belong toappointment'svenuecustomermust not be blacklistedcustomermust not be deletedworkstationmust belong toappointment'svenuecustomer_informed_consentmust belong toappointment'svenuecustomer_informed_consentmust belong toappointment'scustomertime+total_durationmust fit in a single day (24h)staff_member_treatmentmust be present ifstatus >= bookedtimemust be present ifstatus >= booked
Online booking availability logics
These logics are implemented in Availabilities::DailyComputingService#_generate_staff_members_availability_matrix for matrix availability and during booking in Hairdressr::Appointable
==- Example:
Venue
| MON | TUE | WED | THU | FRI | SAT | SUN |
|---|---|---|---|---|---|---|
| - | 9:00-18:00 | 9:00-12:00-14:00-18:00 | 9:00-18:00 | 9:00-18:00 | 9:00-18:00 | - |
StaffMember
time_table_1
| MON | TUE | WED | THU | FRI | SAT | SUN |
|---|---|---|---|---|---|---|
| - | - | 10:00-18:00 | 9:00-12:00-14:00-18:00 | - | - | - |
time_table_2
| MON | TUE | WED | THU | FRI | SAT | SUN |
|---|---|---|---|---|---|---|
| - | 9:00-18:00 | 10:00-18:00 | 9:00-18:00 | 9:00-18:00 | 9:00-18:00 | - |
| === |
Availability matrices
To keep track of available slots in venues' agendas, availability is stored in VenueAvailabilityStaticContent (venue's time_slot and time_zone, services' menu, venue's staff members, venue's workstations, etc, which are the same for all the days) and VenueAvailabilityDynamicContent (effective availability for a single day) models.
These two models combined allow the search of availability in a particular day for a particular venue treatment/staff member/workstation, and they allow the creation of a matrix (table) where rows are bookable entities (staff_member_workstation_treatment, so the combination of staff_member_treatment and workstation_treatment) and columns are time_slots entries. The result looks like:
Where green slots are available slots (with x{N} and N indicates the number of available seats), red slots are unavailable and E A indicates where there are extra time after (aka cleaning time).
These are saved as array of array of integer and managed by AvailabilityMatrix, AvailabilityVector and AvailabilitySlot classes, which provide a bunch of helper methods to work with.
Each single matrix entry indicates the number of available slots (can be negative on overbooking), but also used as bitmask to save extra time after information.
Whenever one of the involved entities is updated, the system keeps track of it and a matrix re-calculation is scheduled with an async job.
To do that, an entry is enqueued on a dedicated Redis queue and processed by the availability_sync Rails task. The enqueuing/dequeuing logic is well explained here.
VenueAvailabilityStaticContent
Contains the availability arrays of information that are the same for all the days, based on Staff Members and the Treatments they perform, Workstations and the Treatments they perform.
The table is updated every day by Availabilities::DailyComputingService
Columns containing Staff Member Treatment data, they are arrays whose element position depends on the Staff Member Treatment Index. the columns are:
staff_member_treatments_map: Array of Staff Member Treatment IDs used to retrieve the Staff Member Treatment Index to extract from the other columns.staff_member_treatment_index = venue_availability_static_content.staff_member_treatments_map.index(staff_member_treatment_id)
staff_member_treatments_additional_duration_mapvenue_availability_static_content.staff_member_treatments_additional_duration_map[staff_member_treatment_index]
staff_member_treatments_available_from_web_mapvenue_availability_static_content.staff_member_treatments_available_from_web_map[staff_member_treatment_index]
staff_member_treatments_duration_mapvenue_availability_static_content.staff_member_treatments_duration_map[staff_member_treatment_index]
staff_member_treatments_prices_cents_mapvenue_availability_static_content.staff_member_treatments_prices_cents_map[staff_member_treatment_index]
staff_member_treatments_staff_member_positions_map: Array of Staff Member Treatment Position, the position is used by the algorithm prioritise the Staff Membersvenue_availability_static_content.staff_member_treatments_staff_member_positions_map[staff_member_treatment_index]
staff_member_treatments_staff_members_map: Array of Staff Member IDsstaff_member_id = venue_availability_static_content.staff_member_treatments_staff_members_map[staff_member_treatment_index]
staff_member_treatments_venue_treatments_map: Array of Venue Treatment IDsvenue_treatment_id = venue_availability_static_content.staff_member_treatments_venue_treatments_map[staff_member_treatment_index]
staff_members_full_names_mapvenue_availability_static_content.staff_members_full_names_map[staff_member_treatment_index]
Columns containing Venue Treatment data, they are arrays whose element position depends on Venue Treatment Index.
venue_treatments_map: Array of Venue Treatment IDs used to retrieve the Venue Treatment Index to extract data from the other columnsvenue_treatment_index = venue_availability_static_content.venue_treatments_map.index(venue_treatment_id)
venue_treatments_always_apply_extra_time_after_mapvenue_availability_static_content.venue_treatments_always_apply_extra_time_after_map[venue_treatment_index]
venue_treatments_available_from_web_mapvenue_availability_static_content.venue_treatments_available_from_web_map[venue_treatment_index]
venue_treatments_computed_names_mapvenue_availability_static_content.venue_treatments_computed_names_map[venue_treatment_index]
venue_treatments_extra_time_after_mapvenue_availability_static_content.venue_treatments_extra_time_after_map[venue_treatment_index]
venue_treatments_full_day_duration_mapvenue_availability_static_content.venue_treatments_full_day_duration_map[venue_treatment_index]
venue_treatments_names_mapvenue_availability_static_content.venue_treatments_names_map[venue_treatment_index]
venue_treatments_positions_map: Array of Venue Treatment Position, the position is used by the algorithm prioritise the Venue Treatment requestedvenue_availability_static_content.venue_treatments_positions_map[venue_treatment_index]
venue_treatments_prices_cents_mapvenue_availability_static_content.venue_treatments_prices_cents_map[venue_treatment_index]
venue_treatments_required_ids_map: Array of ID of Venue Treatment required by the indexed Venue Treatmentvenue_treatment_required_id = venue_availability_static_content.venue_treatments_required_ids_map[venue_treatment_index]
venue_treatments_treatment_category_ids_mapvenue_treatment_categroy_id = venue_availability_static_content.venue_treatments_treatment_category_ids_map[venue_treatment_index]
venue_treatments_treatment_ids_maptreatment_id = venue_availability_static_content.venue_treatments_treatment_ids_map[venue_treatment_index]
venue_treatments_venue_treatment_group_ids_mapvenue_treatment_group_id = venue_availability_static_content.venue_treatments_venue_treatment_group_ids_map[venue_treatment_index]
Columns containing Workstation Treatment data, they are arrays which positions depend on Workstation Treatment Index. the columns are:
workstation_treatments_map: Array of Workstation Treatment IDs used to retrieve the Workstation Treatment Index to extract data from the other columnsworkstation_treatment_index = venue_availability_static_content.workstation_treatments_map.index(workstation_treatment_id)
workstation_treatments_venue_treatments_map: Array of Venue Treatment IDsvenue_treatment_id = venue_availability_static_content.workstation_treatments_venue_treatments_map[workstation_treatment_index]
workstation_treatments_workstations_map: Array of Workstation IDsworkstation_id = venue_availability_static_content.workstation_treatments_workstations_map[workstation_treatment_index]
Columns containing Workstation data, they are arrays whose positions depend on Workstation Index.
workstations_mapArray of Workstation IDs used to retrieve the Workstation Index to extract data from the other columnsworkstation_index = venue_availability_static_content.workstations_map.index(workstation_id)
workstations_names_mapvenue_availability_static_content.workstations_names_map[workstation_index]
workstations_private_mapvenue_availability_static_content.workstations_private_map[workstation_index]
workstations_venue_treatments_map: Array of Venue Treatment IDsvenue_treatment_id = venue_availability_static_content.workstations_venue_treatments_map[workstation_index]
workstations_capacity_mapvenue_availability_static_content.workstations_capacity_map[workstation_index]
VenueAvailabilityDynamicContent
Contains the availability matrices and arrays with the availability information that changes every day based on Vacations, Extra Openings, Appointments etc.
The table is updated every day by Availabilities::DailyComputingService
appointments_staff_members_availability_matrix: Uala::AvailabilityMatrix of number of Staff Member Appointments, indexed by Staff Member Index and Time Slot Indexstaff_member_occupied_slots = venue_availability_dynamic_content.appointments_staff_members_availability_matrix[staff_member_index]
staff_members_availability_matrix: Uala::AvailabilityMatrix that show Staff Member daily availability (without considering Appointments) 0 not available 1, slot is available, indexed by Staff Member Index and Time Slot Indexstaff_member_available_slots = venue_availability_dynamic_content.staff_members_availability_matrix[staff_member_index]
appointments_workstations_availability_matrix: Uala::AvailabilityMatrix of number of Workstation Appointments, indexed by Workstation Index and Time Slot Indexworkstation_occopied_capacity_slots = venue_availability_dynamic_content.appointments_workstations_availability_matrix[workstation_index]
workstations_availability_matrix: Uala::AvailabilityMatrix that show Workstation daily availability (without considering Appointments) 0 not available N when N position in the slot are available, indexed by Workstation Index and Time Slot Indexworkstation_free_capacity_slots = venue_availability_dynamic_content.workstations_availability_matrix[workstation_index]
performing_staff_member_workstation_treatments_matrix: Uala::AvailabilityMatrix used to define if a Treatment is performed by a Staff Member using a Workstation treatment, indexed by Staff Member Workstation Treatment Index (a combination of Staff Member Treatment Index and Workstation Treatment Index) and Time Slot Indexstaff_member_workstation_treatment_occupied_slots = venue_availability_dynamic_content.performing_staff_member_workstation_treatments_matrix[staff_member_workstation_treatment_index]
performing_staff_member_workstation_treatments_smts_map: Array of Staff Member Treatment Indexes indexed by Staff Member Workstation Treatment Index, used to map themstaff_member_treatment_index = venue_availability_dynamic_content.performing_staff_member_workstation_treatments_smts_map[staff_member_workstation_treatment_index]
performing_staff_member_workstation_treatments_wts_map: Array of Workstation Treatment Indexes indexed by Staff Member Workstation Treatment Index, used to map themworkstation_treatment_index = venue_availability_dynamic_content.performing_staff_member_workstation_treatments_wts_map[staff_member_workstation_treatment_index]
staff_member_workstation_treatments_availability_matrix: Uala::AvailabilityMatrix to define if a Staff Member Treatment is available in a defined Workstation, indexed by Staff Member Workstation Treatment Index (a combination of Staff Member Treatment Index and Workstation Treatment Index) and Time Slot Indexstaff_member_workstation_treatment_slots = venue_availability_dynamic_content.staff_member_workstation_treatments_availability_matrix[staff_member_workstation_treatment_index]
staff_member_workstation_treatments_staff_member_treatments_map: Array of Staff Member Treatment Indexes indexed by Staff Member Workstation Treatment Index, used to map themstaff_member_treatment_index = venue_availability_dynamic_content.staff_member_workstation_treatments_staff_member_treatments_map[staff_member_workstation_treatment_index]
staff_member_workstation_treatments_workstation_treatments_map: Array of Workstation Treatment Indexes indexed by Staff Member Workstation Treatment Index, used to map themworkstation_treatment_index = venue_availability_dynamic_content.staff_member_workstation_treatments_workstation_treatments_map[staff_member_workstation_treatment_index]
staff_member_treatments_any_availability_map: Array that indicates if there is at least one slot available on that day, indexed by Staff Member Treatment Indexes.has_slot_that_day = venue_availability_dynamic_content.staff_member_treatments_any_availability_map[staff_member_workstation_treatment_index]
staff_member_treatments_availability_matrix: Uala::AvailabilityMatrix used to define if a staff member treatment is available, indexed by Staff Member Treatment Indexes and Time Slot Indexstaff_member_treatment_slots = venue_availability_dynamic_content.staff_member_treatments_availability_matrix[staff_member_workstation_treatment_index]
workstation_treatments_availability_matrix: Uala::AvailabilityMatrix that define if a Workstation Treatment is available, indexed by Workstation Treatment Index and Time Slot Indexworkstation_treatment_slots = venue_availability_dynamic_content.workstation_treatments_availability_matrix[staff_member_workstation_treatment_index]
Availabilities::FinderOverMatricesService
Availabilities::FinderOverMatricesService is the main service used to retrieve all the Available Slots when a Booking (set of Appointments) could be placed.
It's possible to restrict the options for staff members and workstations for all the Booking appointments.
The service retrieve the slots availables for one or multiple Appointments booked together.
To do so Availabilities::FinderOverMatricesService calls a function recursively which logic depends on the Appointment's order (level):
- if the Appointment has level 0 it retrieve all the available slots,
- otherwise it stops to retrieve the available slots as soon as it finds an unavailable slot (to prevent empty slots between Appointments).
To improve the Customer experience the algorithm try to associate the Booking's Appointments to the Staff Member and Workstation from the previous Appointment. Also the available slots are sorted by lower price, Staff Member's position and Workstation's position (used as priority) to place first the "best" available slot.
Treatwell Marketplace data flow
The Marketplace needs the availability computed from the SaaS for 2 features:
- Search the Venues that have a defined treatment available in a defined time.
- Book an appointment
To enable the Search feature the Job Single::AvailabilityJob sends in async all the Venues available slots for the next 90 days.
The Availabilities sent are computed by in a simplified TimeSlot#from_service_option_availability that uses mainly staff_member_workstation_treatments_availability_matrix, to make the computation faster.
The Booking feature needs the real time Availability to avoid errors.
The Marketplace retrieves them from /tw-integration/availability.json asking the slots available for a defined set of Treatments, date and optionally the Staff Member that must perform first treatment.
The slots are computed by Availabilities::FinderOverMatricesService as for the availability in the SaaS.
To make that service being able to respond to the Marketing request the input and output
is wrapped byTreatwell::BatchAvailabilitiesLookupService adding the logics specific for the Marketplace (Staff Member required only for the first treatment, prices etc)