diff --git a/drivers/place/area_management.cr b/drivers/place/area_management.cr index 3505168dddd..1cb25f10a9b 100644 --- a/drivers/place/area_management.cr +++ b/drivers/place/area_management.cr @@ -38,6 +38,10 @@ class Place::AreaManagement < PlaceOS::Driver }, ], }, + + # If another systems has different desk IDs configured you can add them to + # desk metadata and then specify the alternative field names here + # desk_id_mappings: ["floorsensedeskid", "vergesensedeskid"] }) alias AreaSetting = NamedTuple( diff --git a/drivers/place/booking_approval_workflows.cr b/drivers/place/booking_approval_workflows.cr index 9cf628f825b..d3ee24207ac 100644 --- a/drivers/place/booking_approval_workflows.cr +++ b/drivers/place/booking_approval_workflows.cr @@ -4,6 +4,8 @@ require "digest/md5" require "placeos" require "file" +require "./booking_model" + class Place::BookingApprovalWorkflows < PlaceOS::Driver descriptive_name "Desk Booking Approval Workflows" generic_name :BookingApproval @@ -114,60 +116,6 @@ class Place::BookingApprovalWorkflows < PlaceOS::Driver end end - class Booking - include JSON::Serializable - - # This is to support events - property action : String? - - property id : Int64 - property booking_type : String - property booking_start : Int64 - property booking_end : Int64 - property timezone : String? - - # events use resource_id instead of asset_id - property asset_id : String? - property resource_id : String? - - def asset_id : String - (@asset_id || @resource_id).not_nil! - end - - property user_id : String - property user_email : String - property user_name : String - - property zones : Array(String) - - property checked_in : Bool? - property rejected : Bool? - property approved : Bool? - property process_state : String? - property last_changed : Int64? - - property approver_name : String? - property approver_email : String? - - property booked_by_name : String - property booked_by_email : String - - property checked_in : Bool? - property title : String? - property description : String? - - property extension_data : Hash(String, JSON::Any) - - def in_progress? - now = Time.utc.to_unix - now >= @booking_start && now < @booking_end - end - - def changed - Time.unix(last_changed.not_nil!) - end - end - # Booking id => event, timestamp @debounce = {} of Int64 => {String?, Int64} diff --git a/drivers/place/booking_approver.cr b/drivers/place/booking_approver.cr index e431a9b742a..ac330005a3d 100644 --- a/drivers/place/booking_approver.cr +++ b/drivers/place/booking_approver.cr @@ -1,4 +1,5 @@ require "placeos-driver" +require "./booking_model" class Place::BookingApprover < PlaceOS::Driver descriptive_name "Booking Auto Approver" @@ -27,36 +28,6 @@ class Place::BookingApprover < PlaceOS::Driver @debug = setting(Bool, :debug) end - class Booking - include JSON::Serializable - - property id : Int64 - property action : String - - property user_id : String - property user_email : String - property user_name : String - - property resource_id : String - property zones : Array(String) - property booking_type : String - - property booking_start : Int64 - property booking_end : Int64 - - property timezone : String? - property title : String? - property description : String? - - property checked_in : Bool - - property booked_by_email : String - property booked_by_name : String - - property process_state : String? - property last_changed : Int64? - end - private def approve_booking(booking : Booking) return false unless booking.action == "create" staff_api.approve(booking.id).get diff --git a/drivers/place/booking_check_in_helper_readme.md b/drivers/place/booking_check_in_helper_readme.md index 97c5246a425..f85231dec64 100644 --- a/drivers/place/booking_check_in_helper_readme.md +++ b/drivers/place/booking_check_in_helper_readme.md @@ -34,7 +34,7 @@ To build the email with the links to your frontend interfaces you need to create ```yaml email_templates: - booking: + bookings: check_in_prompt: subject: Reminder about your meeting: %{meeting_summary} html: > diff --git a/drivers/place/booking_model.cr b/drivers/place/booking_model.cr new file mode 100644 index 00000000000..1b83c09d6f6 --- /dev/null +++ b/drivers/place/booking_model.cr @@ -0,0 +1,55 @@ +require "json" + +class Place::Booking + include JSON::Serializable + + # This is to support events + property action : String? + + property id : Int64 + property booking_type : String + property booking_start : Int64 + property booking_end : Int64 + property timezone : String? + + # events use resource_id instead of asset_id + property asset_id : String? + property resource_id : String? + + def asset_id : String + (@asset_id || @resource_id).not_nil! + end + + property user_id : String + property user_email : String + property user_name : String + + property zones : Array(String) + + property checked_in : Bool? + property rejected : Bool? + property approved : Bool? + property process_state : String? + property last_changed : Int64? + + property approver_name : String? + property approver_email : String? + + property booked_by_name : String + property booked_by_email : String + + property checked_in : Bool { false } + property title : String? + property description : String? + + property extension_data : Hash(String, JSON::Any) { {} of String => JSON::Any } + + def in_progress? + now = Time.utc.to_unix + now >= @booking_start && now < @booking_end + end + + def changed + Time.unix(last_changed.not_nil!) + end +end diff --git a/drivers/place/booking_notifier.cr b/drivers/place/booking_notifier.cr new file mode 100644 index 00000000000..bd26d43a7f9 --- /dev/null +++ b/drivers/place/booking_notifier.cr @@ -0,0 +1,360 @@ +require "placeos-driver" +require "placeos-driver/interface/mailer" +require "digest/md5" +require "placeos" +require "file" + +require "./booking_model" + +class Place::BookingNotifier < PlaceOS::Driver + descriptive_name "Booking Notifier" + generic_name :BookingNotifier + description %(notifies users when a booking takes place) + + default_settings({ + timezone: "Australia/Sydney", + date_time_format: "%c", + time_format: "%l:%M%p", + date_format: "%A, %-d %B", + + booking_type: "desk", + disable_attachments: true, + + notify: { + zone_id1: { + name: "Sydney Building 1", + email: ["concierge@place.com"], + notify_manager: true, + notify_booking_owner: true, + }, + zone_id2: { + name: "Melb Building", + attachments: {"file-name.pdf" => "https://s3/your_file.pdf"}, + notify_booking_owner: true, + }, + }, + }) + + accessor staff_api : StaffAPI_1 + + # We want to use the first driver in the system that is a mailer + def mailer + system.implementing(Interface::Mailer)[0] + end + + def calendar + system[:Calendar] + end + + def on_load + # Some form of asset booking has occured + monitor("staff/booking/changed") { |_subscription, payload| parse_booking(payload) } + on_update + end + + # See: https://crystal-lang.org/api/latest/Time/Format.html + @date_time_format : String = "%c" + @time_format : String = "%l:%M%p" + @date_format : String = "%A, %-d %B" + @time_zone : Time::Location = Time::Location.load("Australia/Sydney") + + @booking_type : String = "desk" + @bookings_checked : UInt64 = 0_u64 + @error_count : UInt64 = 0_u64 + + @disable_attachments : Bool = true + + # Zone_id => notify settings + @notify_lookup : Hash(String, SiteDetails) = {} of String => SiteDetails + + class SiteDetails + include JSON::Serializable + + getter name : String + getter email : Array(String) { [] of String } + getter attachments : Hash(String, String) { {} of String => String } + getter notify_manager : Bool? + getter notify_booking_owner : Bool? + end + + def on_update + @booking_type = setting?(String, :booking_type).presence || "desk" + + time_zone = setting?(String, :calendar_time_zone).presence || "Australia/Sydney" + @time_zone = Time::Location.load(time_zone) + @date_time_format = setting?(String, :date_time_format) || "%c" + @time_format = setting?(String, :time_format) || "%l:%M%p" + @date_format = setting?(String, :date_format) || "%A, %-d %B" + + @notify_lookup = setting(Hash(String, SiteDetails), :notify) + attach = setting?(Bool, :disable_attachments) + @disable_attachments = attach.nil? ? true : !!attach + + schedule.clear + schedule.every(5.minutes) { check_bookings } + end + + # Booking id => event, timestamp + @debounce = {} of Int64 => {String?, Int64} + + protected def parse_booking(payload) + logger.debug { "received booking event payload: #{payload}" } + booking_details = Booking.from_json payload + + # Ignore when a bookings state is updated + return if {"process_state", "metadata_changed"}.includes?(booking_details.action) + return unless booking_details.action.nil? + + # Ignore the same event in a short period of time + previous = @debounce[booking_details.id]? + return if previous && previous[0] == booking_details.action + @debounce[booking_details.id] = {booking_details.action, Time.utc.to_unix} + + building_zone, notify_details, attachments = get_building_name(booking_details.zones) + return unless notify_details && building_zone && attachments + + building_key = notify_details.name.downcase.gsub(' ', '_') + + timezone = booking_details.timezone.presence || @time_zone.name + location = Time::Location.load(timezone) + + # https://crystal-lang.org/api/0.35.1/Time/Format.html + # date and time (Tue Apr 5 10:26:19 2016) + starting = Time.unix(booking_details.booking_start).in(location) + ending = Time.unix(booking_details.booking_end).in(location) + + # Ignore changes to meetings that have already ended + return if Time.utc > ending + + attach = attachments.first? + + args = { + booking_id: booking_details.id, + start_time: starting.to_s(@time_format), + start_date: starting.to_s(@date_format), + start_datetime: starting.to_s(@date_time_format), + end_time: ending.to_s(@time_format), + end_date: ending.to_s(@date_format), + end_datetime: ending.to_s(@date_time_format), + starting_unix: booking_details.booking_start, + + asset_id: booking_details.asset_id, + user_id: booking_details.user_id, + user_email: booking_details.user_email, + user_name: booking_details.user_name, + reason: booking_details.title, + + level_zone: booking_details.zones.reject { |z| z == building_zone }.first?, + building_zone: building_zone, + building_name: notify_details.name, + + approver_name: booking_details.approver_name, + approver_email: booking_details.approver_email, + + booked_by_name: booking_details.booked_by_name, + booked_by_email: booking_details.booked_by_email, + + attachment_name: attach.try &.[](:file_name), + attachment_url: attach.try &.[](:uri), + } + + attachments.clear if @disable_attachments + third_party = booking_details.user_email != booking_details.booked_by_email + + send_to = notify_details.email.dup + send_to << booking_details.user_email if notify_details.notify_booking_owner + + if notify_details.notify_manager + email = get_manager(booking_details.user_email) + send_to << email if email + end + + mailer.send_template( + to: send_to, + template: {"bookings", third_party ? "booked_by_notify" : "booking_notify"}, + args: args, + attachments: attachments + ) + staff_api.booking_state(booking_details.id, "notified").get + + @bookings_checked += 1 + self[:bookings_checked] = @bookings_checked + rescue error + logger.error { error.inspect_with_backtrace } + self[:last_error] = { + error: error.message, + time: Time.local.to_s, + user: payload, + } + @error_count += 1 + self[:error_count] = @error_count + end + + def get_building_name(zones : Array(String)) + zones.each do |zone_id| + details = @notify_lookup[zone_id]? + if details + attachments = details.attachments.compact_map { |n, l| get_attachment(n, l) } + logger.debug { "attaching #{attachments.size} files" } + return {zone_id, details, attachments} + end + end + {nil, nil, nil} + end + + protected def get_attachment(filename : String, uri : String) + return {file_name: filename, content: "", uri: uri} if @disable_attachments + + ext = filename.split('.')[-1] + file = Digest::MD5.base64digest(uri).gsub(/[^0-9a-zA-Z\.]/, "") + ext + + # Local cache is pre-encoded + if File.exists?(file) + content = File.read(file) + logger.debug { "attachment saved locally #{filename} - #{content.bytesize}" } + return {file_name: filename, content: content, uri: uri} + end + + # Download the file from the internet + buffer = IO::Memory.new + begin + buf = Bytes.new(64) + HTTP::Client.get(uri) do |response| + raise "HTTP request failed with #{response.status_code}" unless response.success? + body_io = response.body_io + while ((bytes = body_io.read(buf)) > 0) + buffer.write(buf[0, bytes]) + end + end + rescue error + logger.warn(exception: error) { "unable to download attachment: #{uri}" } + return nil + end + + encoded = Base64.strict_encode(buffer) + File.write file, encoded + + logger.debug { "attachment downloaded #{filename} - #{encoded.bytesize}" } + + {file_name: filename, content: encoded, uri: uri} + end + + @check_bookings_mutex = Mutex.new + + @[Security(Level::Support)] + def check_bookings(months_from_now : Int32 = 2) + # Clean up old debounce data + expired = 5.minutes.ago.to_unix + @debounce.reject! { |_, (_event, entered)| expired > entered } + + @check_bookings_mutex.synchronize do + @notify_lookup.each do |building_zone, details| + building_name = details.name + email = details.email + attachments = details.attachments.compact_map { |n, l| get_attachment(n, l) } + building_key = building_name.downcase.gsub(' ', '_') + + perform_booking_check(building_zone, building_name, building_key, email, details.notify_booking_owner, details.notify_manager, attachments, months_from_now) + end + end + end + + protected def perform_booking_check(building_zone, building_name, building_key, emails, notify_owner, notify_manager, attachments, months_from_now = 2) + now = Time.utc.to_unix + later = months_from_now.months.from_now.to_unix + + bookings = staff_api.query_bookings( + type: @booking_type, + period_start: now, + period_end: later, + zones: [building_zone], + approved: false, + rejected: false, + created_before: 2.minutes.ago.to_unix + ).get.as_a + + bookings = bookings + staff_api.query_bookings( + type: @booking_type, + period_start: now, + period_end: later, + zones: [building_zone], + approved: true, + rejected: false, + created_before: 2.minutes.ago.to_unix + ).get.as_a + + bookings = Array(Booking).from_json(bookings.to_json) + logger.debug { "checking #{bookings.size} requested bookings in #{building_name}" } + bookings.each do |booking_details| + timezone = booking_details.timezone.presence || @time_zone.name + location = Time::Location.load(timezone) + + starting = Time.unix(booking_details.booking_start).in(location) + ending = Time.unix(booking_details.booking_end).in(location) + + attach = attachments.first? + + args = { + booking_id: booking_details.id, + start_time: starting.to_s(@time_format), + start_date: starting.to_s(@date_format), + start_datetime: starting.to_s(@date_time_format), + end_time: ending.to_s(@time_format), + end_date: ending.to_s(@date_format), + end_datetime: ending.to_s(@date_time_format), + starting_unix: booking_details.booking_start, + + asset_id: booking_details.asset_id, + user_id: booking_details.user_id, + user_email: booking_details.user_email, + user_name: booking_details.user_name, + reason: booking_details.title, + + level_zone: booking_details.zones.reject { |z| z == building_zone }.first?, + building_zone: building_zone, + building_name: building_name, + + booked_by_name: booking_details.booked_by_name, + booked_by_email: booking_details.booked_by_email, + + attachment_name: attach.try &.[](:file_name), + attachment_url: attach.try &.[](:uri), + } + + attachments.clear if @disable_attachments + + begin + if booking_details.process_state.nil? + third_party = booking_details.user_email != booking_details.booked_by_email + + send_to = emails.dup + send_to << booking_details.user_email if notify_owner + + if notify_manager + email = get_manager(booking_details.user_email) + send_to << email if email + end + + mailer.send_template( + to: send_to, + template: {"bookings", third_party ? "booked_by_notify" : "booking_notify"}, + args: args, + attachments: attachments + ) + staff_api.booking_state(booking_details.id, "notified").get + end + rescue error + logger.error(exception: error) { "while processing booking id #{booking_details.id}" } + end + end + end + + @[Security(Level::Support)] + def get_manager(staff_email : String) + manager = calendar.get_user_manager(staff_email).get + (manager["email"]? || manager["username"]).as_s + rescue error + logger.warn { "failed to email manager of #{staff_email}\n#{error.inspect_with_backtrace}" } + nil + end +end diff --git a/drivers/place/booking_notifier_readme.md b/drivers/place/booking_notifier_readme.md new file mode 100644 index 00000000000..be0c6104ac5 --- /dev/null +++ b/drivers/place/booking_notifier_readme.md @@ -0,0 +1,97 @@ +# Booking Notifier Readme + +Docs on how to configure the booking notifier helper. +This helper provides a simple way to notify users of bookings. + +* The notifier monitors for new asset bookings (defaults to desks) +* periodically checks for new bookings +* for buildings or floors, it notifies a selection of: pre-defined email addresses, the owner of the booking and / or the manager of the booking owner + + +## Requirements + +Requires the following drivers in the system + +* StaffAPI - for querying bookings +* Mailer - for sending emails, this also will be where the templates are configured +* Calendar - for querying a users manager (only if manager notification is desired) + + +## Booking Notifier Configuration + +```yaml + # How do we want dates to be formatted in the email template + timezone: "Australia/Sydney" + date_time_format: "%c" + time_format: "%l:%M%p" + date_format: "%A, %-d %B" + + # What type of asset are we notifying people about + booking_type: "desk" + + # Do we want to be emailing out attachments? + disable_attachments: true + + # what zones are we notifying about? + notify: { + # You can configure notification settings for building and floor zones + zone_id1: { + # name of the building or floor that will be in the email template + name: "Sydney Building 1", + # optional list of emails you always want to be notified of bookings in this zone + email: ["concierge@place.com"], + # do we want to notify the booking owners manager? + notify_manager: true, + # do we want to notify the booking owner? + notify_booking_owner: true, + }, + zone_id2: { + name: "Melb Building", + attachments: {"file-name.pdf" => "https://s3/your_file.pdf"}, + notify_booking_owner: true, + }, + } +``` + + +## Template configuration on Mailer + +There are two templates that are expected: + +* `booking_notify` (the booking owner booked the asset) +* `booked_by_notify` (someone booked on the owners behalf) + +```yaml +email_templates: + bookings: + booking_notify: + subject: Thank you for booking a desk + html: > + + your desk %{asset_id} has been booked for %{start_date} + +``` + +The variables available to mix into the email template are: + booking_id + start_time (formatted as per Booking Notifier Configuration) + start_date + start_datetime + end_time + end_date + end_datetime + starting_unix + asset_id + user_id (where user is the booking owner) + user_email + user_name + reason (or booking title) + level_zone + building_zone + building_name + approver_name + approver_email + booked_by_name + booked_by_email + attachment_name + attachment_url diff --git a/drivers/place/booking_notifier_spec.cr b/drivers/place/booking_notifier_spec.cr new file mode 100644 index 00000000000..170088cd094 --- /dev/null +++ b/drivers/place/booking_notifier_spec.cr @@ -0,0 +1,111 @@ +require "placeos-driver/spec" +require "placeos-driver/interface/mailer" + +DriverSpecs.mock_driver "Place::BookingCheckInHelper" do + system({ + Mailer: {MailerMock}, + Calendar: {CalendarMock}, + StaffAPI: {StaffAPIMock}, + }) + + exec(:check_bookings).get + + system(:StaffAPI)[:queries].should eq 4 + system(:StaffAPI)[:booking_state].should eq "1--notified" + system(:Mailer)[:template].should eq ["bookings", "booking_notify"] + system(:Mailer)[:to].should eq ["concierge@place.com", "user1234@org.com", "manager@site.com"] +end + +# :nodoc: +class MailerMock < DriverSpecs::MockDriver + include PlaceOS::Driver::Interface::Mailer + + # need this for the interface + def send_mail( + to : String | Array(String), + subject : String, + message_plaintext : String? = nil, + message_html : String? = nil, + resource_attachments : Array(ResourceAttachment) = [] of ResourceAttachment, + attachments : Array(Attachment) = [] of Attachment, + cc : String | Array(String) = [] of String, + bcc : String | Array(String) = [] of String, + from : String | Array(String) | Nil = nil + ) + true + end + + # we don't have templates defined so we'll override this for testing + def send_template( + to : String | Array(String), + template : Tuple(String, String), + args : TemplateItems, + resource_attachments : Array(ResourceAttachment) = [] of ResourceAttachment, + attachments : Array(Attachment) = [] of Attachment, + cc : String | Array(String) = [] of String, + bcc : String | Array(String) = [] of String, + from : String | Array(String) | Nil = nil + ) + self[:template] = template + self[:to] = to + end +end + +# :nodoc: +class CalendarMock < DriverSpecs::MockDriver + def get_user_manager(staff_email : String) + { + email: "manager@site.com" + } + end +end + +# :nodoc: +class StaffAPIMock < DriverSpecs::MockDriver + @called : Int32 = 0 + + def query_bookings( + type : String, + period_start : Int64? = nil, + period_end : Int64? = nil, + zones : Array(String) = [] of String, + user : String? = nil, + email : String? = nil, + state : String? = nil, + created_before : Int64? = nil, + created_after : Int64? = nil, + approved : Bool? = nil, + rejected : Bool? = nil, + checked_in : Bool? = nil + ) + logger.debug { "Querying desk bookings!" } + + @called += 1 + self[:queries] = @called + return [] of String if @called >= 2 + + now = Time.local + start = now.at_beginning_of_day.to_unix + ending = now.at_end_of_day.to_unix + [{ + id: 1, + booking_type: type, + booking_start: start, + booking_end: ending, + asset_id: "desk-123", + user_id: "user-1234", + user_email: "user1234@org.com", + user_name: "Bob Jane", + zones: zones + ["zone-building"], + checked_in: true, + rejected: false, + booked_by_name: "Bob Jane", + booked_by_email: "user1234@org.com" + }] + end + + def booking_state(booking_id : String | Int64, state : String) + self[:booking_state] = "#{booking_id}--#{state}" + true + end +end diff --git a/drivers/place/bookings.cr b/drivers/place/bookings.cr index 538a36e5dbc..5087a993c2a 100644 --- a/drivers/place/bookings.cr +++ b/drivers/place/bookings.cr @@ -28,6 +28,10 @@ class Place::Bookings < PlaceOS::Driver include_cancelled_bookings: false, hide_qr_code: false, + custom_qr_url: "https://domain.com/path", + + # This image is displayed along with the capacity when the room is not bookable + offline_image: "https://domain.com/room_image.svg" }) accessor calendar : Calendar_1 @@ -92,6 +96,7 @@ class Place::Bookings < PlaceOS::Driver # Write to redis last on the off chance there is a connection issue self[:room_name] = setting?(String, :room_name).presence || config.control_system.not_nil!.display_name.presence || config.control_system.not_nil!.name + self[:room_capacity] = setting?(Int32, :room_capacity) || config.control_system.not_nil!.capacity self[:default_title] = @default_title self[:disable_book_now] = @disable_book_now self[:disable_end_meeting] = @disable_end_meeting @@ -100,6 +105,7 @@ class Place::Bookings < PlaceOS::Driver self[:control_ui] = setting?(String, :control_ui) self[:catering_ui] = setting?(String, :catering_ui) + self[:custom_qr_url] = setting?(String, :custom_qr_url) self[:show_qr_code] = !(setting?(Bool, :hide_qr_code) || false) end @@ -262,7 +268,7 @@ class Place::Bookings < PlaceOS::Driver end # Check if pending is enabled - if @pending_period.to_i > 0_i64 + if @pending_period.to_i > 0_i64 || @pending_before.to_i > 0_i64 self[:current_pending] = @current_pending = current_pending self[:next_pending] = @next_pending = next_pending self[:pending] = current_pending || next_pending diff --git a/drivers/place/desk_bookings_locations.cr b/drivers/place/desk_bookings_locations.cr index 18a43666aea..e8c34504e06 100644 --- a/drivers/place/desk_bookings_locations.cr +++ b/drivers/place/desk_bookings_locations.cr @@ -1,6 +1,7 @@ require "json" require "placeos-driver" require "placeos-driver/interface/locatable" +require "./booking_model" class Place::DeskBookingsLocations < PlaceOS::Driver include Interface::Locatable @@ -168,41 +169,6 @@ class Place::DeskBookingsLocations < PlaceOS::Driver end end - class Booking - include JSON::Serializable - - # This is to support events - property action : String? - - property id : Int64 - property booking_type : String - property booking_start : Int64 - property booking_end : Int64 - property timezone : String? - - # events use resource_id instead of asset_id - property asset_id : String? - property resource_id : String? - - def asset_id : String - (@asset_id || @resource_id).not_nil! - end - - property user_id : String - property user_email : String - property user_name : String - - property zones : Array(String) - - property checked_in : Bool? - property rejected : Bool? - - def in_progress? - now = Time.utc.to_unix - now >= @booking_start && now < @booking_end - end - end - # Email => Array of bookings @bookings : Hash(String, Array(Booking)) = Hash(String, Array(Booking)).new diff --git a/drivers/place/desk_bookings_locations_spec.cr b/drivers/place/desk_bookings_locations_spec.cr index a15deef3540..fde20bb4563 100644 --- a/drivers/place/desk_bookings_locations_spec.cr +++ b/drivers/place/desk_bookings_locations_spec.cr @@ -14,8 +14,8 @@ DriverSpecs.mock_driver "Place::DeskBookingsLocations" do resp = exec(:device_locations, "placeos-zone-id").get puts resp resp.should eq([ - {"location" => "booking", "checked_in" => true, "asset_id" => "desk-123", "booking_id" => 1, "building" => "zone-building", "level" => "placeos-zone-id", "ends_at" => 1610110799, "mac" => "user-1234", "staff_email" => "user1234@org.com", "staff_name" => "Bob Jane"}, - {"location" => "booking", "checked_in" => false, "asset_id" => "desk-456", "booking_id" => 2, "building" => "zone-building", "level" => "placeos-zone-id", "ends_at" => 1610110799, "mac" => "user-456", "staff_email" => "zdoo@org.com", "staff_name" => "Zee Doo"}, + {"location" => "booking", "type" => "desk", "checked_in" => true, "asset_id" => "desk-123", "booking_id" => 1, "building" => "zone-building", "level" => "placeos-zone-id", "ends_at" => ending, "mac" => "user-1234", "staff_email" => "user1234@org.com", "staff_name" => "Bob Jane"}, + {"location" => "booking", "type" => "desk", "checked_in" => false, "asset_id" => "desk-456", "booking_id" => 2, "building" => "zone-building", "level" => "placeos-zone-id", "ends_at" => ending, "mac" => "user-456", "staff_email" => "zdoo@org.com", "staff_name" => "Zee Doo"}, ]) end @@ -27,32 +27,38 @@ class StaffAPIMock < DriverSpecs::MockDriver now = Time.local start = now.at_beginning_of_day.to_unix ending = now.at_end_of_day.to_unix - [{ - id: 1, - booking_type: type, - booking_start: start, - booking_end: ending, - asset_id: "desk-123", - user_id: "user-1234", - user_email: "user1234@org.com", - user_name: "Bob Jane", - zones: zones + ["zone-building"], - checked_in: true, - rejected: false, - }, - { - id: 2, - booking_type: type, - booking_start: start, - booking_end: ending, - asset_id: "desk-456", - user_id: "user-456", - user_email: "zdoo@org.com", - user_name: "Zee Doo", - zones: zones + ["zone-building"], - checked_in: false, - rejected: false, - }] + [ + { + id: 1, + booking_type: type, + booking_start: start, + booking_end: ending, + asset_id: "desk-123", + user_id: "user-1234", + user_email: "user1234@org.com", + user_name: "Bob Jane", + zones: zones + ["zone-building"], + checked_in: true, + rejected: false, + booked_by_name: "Bob Jane", + booked_by_email: "user1234@org.com", + }, + { + id: 2, + booking_type: type, + booking_start: start, + booking_end: ending, + asset_id: "desk-456", + user_id: "user-456", + user_email: "zdoo@org.com", + user_name: "Zee Doo", + zones: zones + ["zone-building"], + checked_in: false, + rejected: false, + booked_by_name: "Zee Doo", + booked_by_email: "zdoo@org.com", + } + ] end def zone(zone_id : String)