diff --git a/drivers/place/auto_release_locker.cr b/drivers/place/auto_release_locker.cr new file mode 100644 index 00000000000..220e36d97f4 --- /dev/null +++ b/drivers/place/auto_release_locker.cr @@ -0,0 +1,79 @@ +require "placeos-driver" +require "./booking_model" + +class Place::AutoReleaseLocker < PlaceOS::Driver + descriptive_name "PlaceOS Auto Release Locker" + generic_name :AutoReleaseLocker + description %(automatic release locker on specified interval) + + default_settings({ + timezone: "Australia/Sydney", + booking_type: "locker", + release_schedule: "0 23 * * 5", + time_window_hours: 1, + }) + + accessor staff_api : StaffAPI_1 + + @timezone : Time::Location = Time::Location.load("Australia/Sydney") + @booking_type : String = "locker" + @release_schedule : String? = nil + @time_window_hours : Int32 = 1 + + def on_update + @release_schedule = setting?(String, :release_schedule).presence + timezone = setting?(String, :timezone).presence || "Australia/Sydney" + @timezone = Time::Location.load(timezone) + @booking_type = setting?(String, :booking_type).presence || "locker" + @time_window_hours = setting?(Int32, :time_window_hours) || 1 + + schedule.clear + + if release = @release_schedule + schedule.cron(release, @timezone) { release_lockers } + end + end + + # Finds the building ID for the current location services object + def get_building_id + zone_ids = staff_api.zones(tags: "building").get.as_a.map(&.[]("id").as_s) + (zone_ids & system.zones).first + rescue error + logger.warn(exception: error) { "unable to determine building zone id" } + nil + end + + @[Security(Level::Support)] + def get_bookings : Array(Booking) + results = [] of Booking + bookings = Array(Booking).from_json staff_api.query_bookings( + type: @booking_type, + period_start: Time.utc.to_unix, + period_end: (Time.utc + @time_window_hours.hours).to_unix, + zones: [get_building_id], + ).get.to_json + results = bookings.select { |booking| booking.checked_in } + + logger.debug { "found #{results.size} #{@booking_type} bookings" } + + results + rescue error + logger.warn(exception: error) { "unable to obtain list of #{@booking_type} bookings" } + [] of Booking + end + + def release_lockers + bookings = get_bookings + released = 0 + bookings.each do |booking| + logger.debug { "releasing booking #{booking.id} as it is within the time_after window" } + begin + staff_api.update_booking(booking_id: booking.id, booking_end: Time.utc.to_unix, checked_in: false).get + released += 1 + rescue error + logger.warn(exception: error) { "unable to release #{@booking_type} with booking id #{booking.id}" } + end + end + {total: bookings.size, released: released} + end +end diff --git a/drivers/place/auto_release_locker_spec.cr b/drivers/place/auto_release_locker_spec.cr new file mode 100644 index 00000000000..a537f1a4a13 --- /dev/null +++ b/drivers/place/auto_release_locker_spec.cr @@ -0,0 +1,163 @@ +require "placeos-driver/spec" + +DriverSpecs.mock_driver "Place::BookingCheckInHelper" do + system({ + StaffAPI: {StaffAPIMock}, + }) + resp = exec(:get_bookings).get + resp.should_not be_nil + resp.not_nil!.as_a.size.should eq 4 + resp = exec(:release_lockers).get + resp.not_nil!.as_h["total"].should eq 4 + resp.not_nil!.as_h["released"].should eq 4 +end + +# :nodoc: +class StaffAPIMock < DriverSpecs::MockDriver + BOOKINGS = [ + { + id: 1, + user_id: "user-one", + user_email: "user_one@example.com", + user_name: "User One", + asset_id: "locker_001", + zones: ["zone-1234"], + booking_type: "locker", + booking_start: (Time.utc - 10.hour).to_unix, + booking_end: (Time.utc + 5.hours).to_unix, + timezone: "Australia/Darwin", + title: "ignore", + description: "", + checked_in: true, + rejected: false, + approved: true, + booked_by_id: "user-one", + booked_by_email: "user_one@example.com", + booked_by_name: "User One", + process_state: "approved", + last_changed: Time.utc.to_unix, + created: Time.utc.to_unix, + }, + { + id: 2, + user_id: "user-one", + user_email: "user_one@example.com", + user_name: "User One", + asset_id: "locker_002", + zones: ["zone-1234"], + booking_type: "locker", + booking_start: (Time.utc - 5.minutes).to_unix, + booking_end: (Time.utc + 1.hour).to_unix, + timezone: "Australia/Darwin", + title: "notify", + description: "", + checked_in: true, + rejected: false, + approved: true, + booked_by_id: "user-one", + booked_by_email: "user_one@example.com", + booked_by_name: "User One", + process_state: "approved", + last_changed: Time.utc.to_unix, + created: Time.utc.to_unix, + }, + { + id: 3, + user_id: "user-one", + user_email: "user_one@example.com", + user_name: "User One", + asset_id: "locker_003", + zones: ["zone-1234"], + booking_type: "locker", + booking_start: (Time.utc - 11.minutes).to_unix, + booking_end: (Time.utc + 1.hour).to_unix, + timezone: "Australia/Darwin", + title: "reject", + description: "", + checked_in: true, + rejected: false, + approved: true, + booked_by_id: "user-one", + booked_by_email: "user_one@example.com", + booked_by_name: "User One", + process_state: "approved", + last_changed: Time.utc.to_unix, + created: Time.utc.to_unix, + }, + { + id: 4, + user_id: "user-one", + user_email: "user_one@example.com", + user_name: "User One", + asset_id: "locker_004", + zones: ["zone-1234"], + booking_type: "locker", + booking_start: (Time.utc - 5.hours).to_unix, + booking_end: (Time.utc + 1.hours).to_unix, + timezone: "Australia/Darwin", + title: "ignore_after_hours", + description: "", + checked_in: true, + rejected: false, + approved: true, + booked_by_id: "user-one", + booked_by_email: "user_one@example.com", + booked_by_name: "User One", + process_state: "approved", + last_changed: Time.utc.to_unix, + created: Time.utc.to_unix, + }, + ] + + 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 + ) + JSON.parse(BOOKINGS.to_json) + end + + def zones(q : String? = nil, + limit : Int32 = 1000, + offset : Int32 = 0, + parent : String? = nil, + tags : Array(String) | String? = nil) + zones = [ + { + created_at: 1660537814, + updated_at: 1681800971, + id: "zone-1234", + name: "Test Zone", + display_name: "Test Zone", + location: "", + description: "", + code: "", + type: "", + count: 0, + capacity: 0, + map_id: "", + tags: [ + "building", + ], + triggers: [] of String, + parent_id: "zone-0000", + timezone: "Australia/Sydney", + }, + ] + + JSON.parse(zones.to_json) + end + + def update_booking(booking_id : String | Int64, booking_end : Int64, checked_in : Bool) + true + end +end